了解AppBarLayout应该从这几个方面入手

简介: 了解AppBarLayout应该从这几个方面入手

AppBarLayout设计的主要目的,我个人认为有以下几个:


  1. 配合ScrollableView如NestedScrollView、RecyclerView等完成嵌套滑动功能。制造滑动的主动权是在ScrollableView上。
  2. 制定了ScrollableView依赖于AppBarLayout的规则,当AppBarLayout主动滑动时,ScollableView能够根据AppBarLayout的位置,调整自身的位置。与1相对应,制造滑动的主动权在AppBarLayout上。
  3. AppBarLayout继承于LinearLayout,它通过对子View设置scroll相关的标志,来控制子View是否跟随滑动、上滑的时候是否吸顶、下滑的时候是否优先跟随滑动。
  4. 对外暴露了OnOffsetChangedListener,以便更灵活地实现AppBarLayout本身不能做到的一些功能。


本文主要会围绕这几个点,结合源码讲解AppBarLayout。


1. 从LinearLayout的子类角度讲解看AppBarLayout


我们都知道AppBarLayout类是继承了LinearLayout类的,并且设置为垂直方向的。对于LinearLayout,大家都很熟悉了。在AppBarLayout的篇幅中,我觉得还是需要强调几个知识点的,虽然很简单,但是依然有一些很重要的细节会被忽略掉。


  1. 假设AppBarLayout有四个子View,view1、view2、view3、view4。view4是绘制在最上面的,view1是绘制在最下面的。我们可能都知道,对子view设置app:layout_scrollFlags="scroll"可以让子view滑出屏幕。我们可以设置view1的属性app:layout_scrollFlags="scroll"。但是如果我们只设置view2的layout_scrollFlags="scroll",那么view2的该属性相当于没有设置。原因在于,假设view2可以滑动出屏幕,那么它势必会与view1产生交集,而且会绘制在view1上面,这样的效果很丑,google大神在设计的时候规避掉了这种不好的用户体验。如果子View没有设置scroll标志,那么它后面的兄弟,即使设置了scroll标志,也是无效的。getTotalScrollRange方法是计算AppBarLayout可以滑动出屏幕的距离。我们可以看到如果不满足(flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0条件,循环是会被break掉的,后面的子view根本都不参与计算,系统代码如下:
public final int getTotalScrollRange() {
    if (totalScrollRange != INVALID_SCROLL_RANGE) {
      return totalScrollRange;
    }
    int range = 0;
    for (int i = 0, z = getChildCount(); i < z; i++) {
      final View child = getChildAt(i);
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      final int childHeight = child.getMeasuredHeight();
      final int flags = lp.scrollFlags;
      if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
        // We're set to scroll so add the child's height
        range += childHeight + lp.topMargin + lp.bottomMargin;
        if (i == 0 && ViewCompat.getFitsSystemWindows(child)) {
          // If this is the first child and it wants to handle system windows, we need to make
          // sure we don't scroll it past the inset
          range -= getTopInset();
        }
        if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
          // For a collapsing scroll, we to take the collapsed height into account.
          // We also break straight away since later views can't scroll beneath
          // us
          range -= ViewCompat.getMinimumHeight(child);
          break;
        }
      } else {
        // As soon as a view doesn't have the scroll flag, we end the range calculation.
        // This is because views below can not scroll under a fixed view.
        break;
      }
    }
    return totalScrollRange = Math.max(0, range);
  }


  1. AppBarLayout的onMeasure方法,比较普通说白了就是沿用了LinearLayout的测量思路。但是为什么要在这里提它呢,因为与它对应的ScrollableView对应的ScrollingViewBehavior的测量方法还是比较重要的,后面我们会讲
  2. AppBarLayout的onLayout方法,比较普通,说白了就是沿用了LinearLayout的layout思路。在这里提它的原因同2。

2. AppBarLayout对事件的处理


AppBarLayout有一个默认的Behavior,AppBarLayout$BaseBehavior,继承自com.google.android.material.appbar.HeaderBehavior,该类的主要作用就是处理触摸事件的。


 //com.google.android.material.appbar.HeaderBehavior
 @Override
  public boolean onTouchEvent(
      @NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
    if (touchSlop < 0) {
      touchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
    }
    switch (ev.getActionMasked()) {
      //省略其他事件
      case MotionEvent.ACTION_MOVE:
        {
          final int activePointerIndex = ev.findPointerIndex(activePointerId);
          if (activePointerIndex == -1) {
            return false;
          }
          final int y = (int) ev.getY(activePointerIndex);
          int dy = lastMotionY - y;
          if (!isBeingDragged && Math.abs(dy) > touchSlop) {
            isBeingDragged = true;
            if (dy > 0) {
              dy -= touchSlop;
            } else {
              dy += touchSlop;
            }
          }
          if (isBeingDragged) {
            lastMotionY = y;
            // We're being dragged so scroll the ABL
            scroll(parent, child, dy, getMaxDragOffset(child), 0);
          }
          break;
        }
    return true;
  }

我们可以看到处理Move事件时,会调用scroll方法,顾名思义就是让AppBarLayout滑出屏幕或者滑入屏幕。

final int scroll(
      CoordinatorLayout coordinatorLayout, V header, int dy, int minOffset, int maxOffset) {
    return setHeaderTopBottomOffset(
        coordinatorLayout,
        header,
        getTopBottomOffsetForScrollingSibling() - dy,
        minOffset,
        maxOffset);
  }


scroll方法主要是通过offsetTopAndBottom来实现偏移的。而且该方法,会有返回值,主要是处理ScrollableView发起的嵌套滑动用的,但是在这里,没有嵌套滑动的逻辑需要处理。


一般情况下,我们使用AppBarLayout和RecyclerView的时候,它们的布局总是前者在后者的上面。那么问题来了,AppBarLayout滑出了屏幕,如果RecyclerView不作出相应的改变,那么它们中间势必会有一段空白,这显然是不合理的,那么AppBarlayout是如何规避这个问题的。答案是通过CoordinatorLayout的依赖关系和AppBarLayout$ScrollingViewBehavior。


3. ScrollingViewBehavior为RecyclerView测量、Layout、跟随ABL滑动


ScrollingViewBehavior主要作用就是三个


  1. 跟随APL滑动
//ScrollingViewBehavior.java
@Override
public boolean onDependentViewChanged(
    @NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
  offsetChildAsNeeded(child, dependency);
  updateLiftedStateIfNeeded(child, dependency);
  return false;
}
//根据APL的位置移动ScrollableView
private void offsetChildAsNeeded(@NonNull View child, @NonNull View dependency) {
  final CoordinatorLayout.Behavior behavior =
      ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
  if (behavior instanceof BaseBehavior) {
    // Offset the child, pinning it to the bottom the header-dependency, maintaining
    // any vertical gap and overlap
    final BaseBehavior ablBehavior = (BaseBehavior) behavior;
    ViewCompat.offsetTopAndBottom(
        child,
        (dependency.getBottom() - child.getTop())
            + ablBehavior.offsetDelta
            + getVerticalLayoutGap()
            - getOverlapPixelsForOffset(dependency));
  }
}

为ScrollableView测量高度,由父类HeaderScrollingViewBehavior实现,主要的算法是,ScrollableView本身测量的高度-APL的高度+APL可滑动的距离,这个细节还是蛮重要的,想想为什么要这么设计。因为必须要加上APL可滑动的距离,否则,往上滑的时候,ScrollableView的高度不够,会出现白色的真空地带,影响用户体验。


//HeaderScrollingViewBehavior
@Override
  public boolean onMeasureChild(
      @NonNull CoordinatorLayout parent,
      @NonNull View child,
      int parentWidthMeasureSpec,
      int widthUsed,
      int parentHeightMeasureSpec,
      int heightUsed) {
    final int childLpHeight = child.getLayoutParams().height;
    if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
        || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
      // If the menu's height is set to match_parent/wrap_content then measure it
      // with the maximum visible height
      final List<View> dependencies = parent.getDependencies(child);
      final View header = findFirstDependency(dependencies);
      if (header != null) {
        int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
        if (availableHeight > 0) {
          if (ViewCompat.getFitsSystemWindows(header)) {
            final WindowInsetsCompat parentInsets = parent.getLastWindowInsets();
            if (parentInsets != null) {
              availableHeight += parentInsets.getSystemWindowInsetTop()
                  + parentInsets.getSystemWindowInsetBottom();
            }
          }
        } else {
          // If the measure spec doesn't specify a size, use the current height
          availableHeight = parent.getHeight();
        }
        int height = availableHeight + getScrollRange(header);
        int headerHeight = header.getMeasuredHeight();
        if (shouldHeaderOverlapScrollingChild()) {
          child.setTranslationY(-headerHeight);
        } else {
          height -= headerHeight;
        }
        final int heightMeasureSpec =
            View.MeasureSpec.makeMeasureSpec(
                height,
                childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
                    ? View.MeasureSpec.EXACTLY
                    : View.MeasureSpec.AT_MOST);
        // Now measure the scrolling view with the correct height
        parent.onMeasureChild(
            child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
        return true;
      }
    }
    return false;
  }


3.把ScrollableView布局在APL下方,代码比较简单,主要是计算位置。


//HeaderScrollingViewBehavior
 @Override
  protected void layoutChild(
      @NonNull final CoordinatorLayout parent,
      @NonNull final View child,
      final int layoutDirection) {
    final List<View> dependencies = parent.getDependencies(child);
    final View header = findFirstDependency(dependencies);
    if (header != null) {
      final CoordinatorLayout.LayoutParams lp =
          (CoordinatorLayout.LayoutParams) child.getLayoutParams();
      final Rect available = tempRect1;
      available.set(
          parent.getPaddingLeft() + lp.leftMargin,
          header.getBottom() + lp.topMargin,
          parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
          parent.getHeight() + header.getBottom() - parent.getPaddingBottom() - lp.bottomMargin);
      final WindowInsetsCompat parentInsets = parent.getLastWindowInsets();
      if (parentInsets != null
          && ViewCompat.getFitsSystemWindows(parent)
          && !ViewCompat.getFitsSystemWindows(child)) {
        // If we're set to handle insets but this child isn't, then it has been measured as
        // if there are no insets. We need to lay it out to match horizontally.
        // Top and bottom and already handled in the logic above
        available.left += parentInsets.getSystemWindowInsetLeft();
        available.right -= parentInsets.getSystemWindowInsetRight();
      }
      final Rect out = tempRect2;
      GravityCompat.apply(
          resolveGravity(lp.gravity),
          child.getMeasuredWidth(),
          child.getMeasuredHeight(),
          available,
          out,
          layoutDirection);
      final int overlap = getOverlapPixelsForOffset(header);
      child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
      verticalLayoutGap = out.top - header.getBottom();
    } else {
      // If we don't have a dependency, let super handle it
      super.layoutChild(parent, child, layoutDirection);
      verticalLayoutGap = 0;
    }
  }

4. AppBarLayout的嵌套滑动


AppBarLayout嵌套滑动,指的是当滚动下方ScrollableView时,AppBarLayout会跟随滑动。主要有三种情况:


  1. ScrollableView向上滑动时,ABL跟随滑动
  2. ScrollableView向下滑动时,ABL跟随滑动
  3. ScrollableView在顶部,向下滑动时,ABL处理ScrollableView无法处理的滑动

以上Case1、Case2 对应的方法是AppBarLayout


BaseBehavior#onNestedScroll。

public void onNestedPreScroll(
    CoordinatorLayout coordinatorLayout,
    @NonNull T child,
    View target,
    int dx,
    int dy,
    int[] consumed,
    int type) {
  if (dy != 0) {
    int min;
    int max;
    if (dy < 0) {
      // We're scrolling down
      min = -child.getTotalScrollRange();
      max = min + child.getDownNestedPreScrollRange();
    } else {
      // We're scrolling up
      min = -child.getUpNestedPreScrollRange();
      max = 0;
    }
    if (min != max) {
      consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
    }
  }
  if (child.isLiftOnScroll()) {
    child.setLiftedState(child.shouldLift(target));
  }
}

当ScrollableView向上滑动时,ABL滑动距离在[-child.getUpNestedPreScrollRange(),0]范围内,0表示恢复原状,-child.getUpNestedPreScrollRange()表示滑出屏幕的距离,getUpNestedPreScrollRange()的值等于getTotalScrollRange()的值


int getUpNestedPreScrollRange() {
    return getTotalScrollRange();
  }

当ScrollableView向下滑动时,ABL滑动距离在[-child.getTotalScrollRange(),-child.getTotalScrollRange()+child.getDownNestedPreScrollRange()]范围内。

int getDownNestedPreScrollRange() {
    if (downPreScrollRange != INVALID_SCROLL_RANGE) {
      // If we already have a valid value, return it
      return downPreScrollRange;
    }
    int range = 0;
    for (int i = getChildCount() - 1; i >= 0; i--) {
      final View child = getChildAt(i);
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      final int childHeight = child.getMeasuredHeight();
      final int flags = lp.scrollFlags;
      if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) {
        // First take the margin into account
        int childRange = lp.topMargin + lp.bottomMargin;
        // The view has the quick return flag combination...
        if ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) {
          // If they're set to enter collapsed, use the minimum height
          childRange += ViewCompat.getMinimumHeight(child);
        } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
          // Only enter by the amount of the collapsed height
          childRange += childHeight - ViewCompat.getMinimumHeight(child);
        } else {
          // Else use the full height
          childRange += childHeight;
        }
        if (i == 0 && ViewCompat.getFitsSystemWindows(child)) {
          // If this is the first child and it wants to handle system windows, we need to make
          // sure we don't scroll past the inset
          childRange = Math.min(childRange, childHeight - getTopInset());
        }
        range += childRange;
      } else if (range > 0) {
        // If we've hit an non-quick return scrollable view, and we've already hit a
        // quick return view, return now
        break;
      }
    }
    return downPreScrollRange = Math.max(0, range);
  }
}

getUpNestedPreScrollRange和getDownNestedScrollRange的区别是,getUpNestedPreScrollRange是从第一个View开始遍历,getDownNestedScrollRange是从第最后一个View开始遍历计算距离。


  1. ABL处理ScrollableView无法处理的滑动在onNestedScroll方法中,只在ScrollableView向下滑动时会触发。
public void onNestedScroll(
    CoordinatorLayout coordinatorLayout,
    @NonNull T child,
    View target,
    int dxConsumed,
    int dyConsumed,
    int dxUnconsumed,
    int dyUnconsumed,
    int type,
    int[] consumed) {
  if (dyUnconsumed < 0) {
    // If the scrolling view is scrolling down but not consuming, it's probably be at
    // the top of it's content
    consumed[1] =
        scroll(coordinatorLayout, child, dyUnconsumed, -child.getDownNestedScrollRange(), 0);
  }
}
int getDownNestedScrollRange() {
    if (downScrollRange != INVALID_SCROLL_RANGE) {
      // If we already have a valid value, return it
      return downScrollRange;
    }
    int range = 0;
    for (int i = 0, z = getChildCount(); i < z; i++) {
      final View child = getChildAt(i);
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      int childHeight = child.getMeasuredHeight();
      childHeight += lp.topMargin + lp.bottomMargin;
      final int flags = lp.scrollFlags;
      if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
        // We're set to scroll so add the child's height
        range += childHeight;
        if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
          // For a collapsing exit scroll, we to take the collapsed height into account.
          // We also break the range straight away since later views can't scroll
          // beneath us
          range -= ViewCompat.getMinimumHeight(child);
          break;
        }
      } else {
        // As soon as a view doesn't have the scroll flag, we end the range calculation.
        // This is because views below can not scroll under a fixed view.
        break;
      }
    }
    return downScrollRange = Math.max(0, range);
  }


5. AppBarLayout Scroll相关的flag详解


前文我们看到在getDownNestedPreScrollRange等方法中,通过遍历子view,判断lp.scrollFlags等标志来计算偏移量。那么下面具体讲讲这些flag的作用


flag 含义
SCROLL_FLAG_NO_SCROLL 0x0 子View不允许滑动,默认值
SCROLL_FLAG_SCROLL 0x1 子View允许滑动,如果该子View前面的兄弟View没有设置该flag,标志位失效
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED 1 << 1 子View向上滑动出屏幕时,会有mininumHeight的高度吸顶,它后面的子View的flag失效
SCROLL_FLAG_ENTER_ALWAYS 1 << 2
SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED 1 << 3
SCROLL_FLAG_SNAP 1 << 4
SCROLL_FLAG_SNAP_MARGINS 1 << 5


相关组合


组合
FLAG_QUICK_RETURN SCROLL_FLAG_SCROLL | SCROLL_FLAG_ENTER_ALWAYS
FLAG_SNAP SCROLL_FLAG_SCROLL | SCROLL_FLAG_SNAP
COLLAPSIBLE_FLAGS SCROLL_FLAG_EXIT_UNTIL_COLLAPSED | SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED


源码使用场景


场景一、 向上滑动,ABL跟随滑动时,会判断SCROLL_FLAG_SCROLL和SCROLL_FLAG_EXIT_UNTIL_COLLAPSED。children从前往后遍历,如果没有设置SCROLL_FLAG_SCROLL,会中断遍历,如果设置了SCROLL_FLAG_SCROLL,可滑动距离+child.getMeasureheight,如果同时设置了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,也会中断遍历,滑动距离-child.getMinimumHeight,child.getMinimumHeight会一直停留在屏幕中。


代码片段如下

  public final int getTotalScrollRange() {
    if (totalScrollRange != INVALID_SCROLL_RANGE) {
      return totalScrollRange;
    }
    int range = 0;
    for (int i = 0, z = getChildCount(); i < z; i++) {
      final View child = getChildAt(i);
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      final int childHeight = child.getMeasuredHeight();
      final int flags = lp.scrollFlags;
      if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
        // We're set to scroll so add the child's height
        range += childHeight + lp.topMargin + lp.bottomMargin;
        if (i == 0 && ViewCompat.getFitsSystemWindows(child)) {
          // If this is the first child and it wants to handle system windows, we need to make
          // sure we don't scroll it past the inset
          range -= getTopInset();
        }
        if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
          // For a collapsing scroll, we to take the collapsed height into account.
          // We also break straight away since later views can't scroll beneath
          // us
          range -= ViewCompat.getMinimumHeight(child);
          break;
        }
      } else {
        // As soon as a view doesn't have the scroll flag, we end the range calculation.
        // This is because views below can not scroll under a fixed view.
        break;
      }
    }
    return totalScrollRange = Math.max(0, range);
  }


场景二、 ScrollableView向下滑动时,ABL跟随滑动,会判断FLAG_QUICK_RETURN,SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED,SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,children从后往前判断。


  1. FLAG_QUICK_RETURN,设置了该flag 向下滑的时候,会跟随滑出一段距离。距离由SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED、SCROLL_FLAG_EXIT_UNTIL_COLLAPSED决定
  2. 如果设置了SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED,滑出距离为ViewCompat.getMinimumHeight(child)
  3. 如果不满足条件2,但是设置了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,滑出距离childHeight - ViewCompat.getMinimumHeight(child),但是由于设置了该flag 会有ViewCompat.getMinimumHeight(child)吸顶,效果等同于全部滑出。
  4. 如果不满足条件2和条件3,滑动距离为childHeight
  5. FLAG_QUICK_RETURN与SCROLL_FLAG_SCROLL不一样。SCROLL_FLAG_SCROLL从前往后遍历,一旦遇到没设置的就中断遍历了。FLAG_QUICK_RETURN从后往前遍历,遇到没设置的不会中断遍历,除非曾经遇到过设置过该Flag而且滑动距离>0的情况会中断遍历(很绕,看源码,多假设场景)
int getDownNestedPreScrollRange() {
    if (downPreScrollRange != INVALID_SCROLL_RANGE) {
      // If we already have a valid value, return it
      return downPreScrollRange;
    }
    int range = 0;
    for (int i = getChildCount() - 1; i >= 0; i--) {
      final View child = getChildAt(i);
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      final int childHeight = child.getMeasuredHeight();
      final int flags = lp.scrollFlags;
      if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) {
        // First take the margin into account
        int childRange = lp.topMargin + lp.bottomMargin;
        // The view has the quick return flag combination...
        if ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) {
          // If they're set to enter collapsed, use the minimum height
          childRange += ViewCompat.getMinimumHeight(child);
        } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
          // Only enter by the amount of the collapsed height
          childRange += childHeight - ViewCompat.getMinimumHeight(child);
        } else {
          // Else use the full height
          childRange += childHeight;
        }
        if (i == 0 && ViewCompat.getFitsSystemWindows(child)) {
          // If this is the first child and it wants to handle system windows, we need to make
          // sure we don't scroll past the inset
          childRange = Math.min(childRange, childHeight - getTopInset());
        }
        range += childRange;
      } else if (range > 0) {
        // If we've hit an non-quick return scrollable view, and we've already hit a
        // quick return view, return now
        break;
      }
    }
    return downPreScrollRange = Math.max(0, range);
  }

场景三、 ScrollableView在顶部,向下滑动时,ABL处理ScrollableView无法处理的滑动。该场景与场景一一样。只会判断SCROLL_FLAG_SCROLL和SCROLL_FLAG_EXIT_UNTIL_COLLAPSED。从前往后遍历。

int getDownNestedScrollRange() {
    if (downScrollRange != INVALID_SCROLL_RANGE) {
      // If we already have a valid value, return it
      return downScrollRange;
    }
    int range = 0;
    for (int i = 0, z = getChildCount(); i < z; i++) {
      final View child = getChildAt(i);
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      int childHeight = child.getMeasuredHeight();
      childHeight += lp.topMargin + lp.bottomMargin;
      final int flags = lp.scrollFlags;
      if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
        // We're set to scroll so add the child's height
        range += childHeight;
        if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
          // For a collapsing exit scroll, we to take the collapsed height into account.
          // We also break the range straight away since later views can't scroll
          // beneath us
          range -= ViewCompat.getMinimumHeight(child);
          break;
        }
      } else {
        // As soon as a view doesn't have the scroll flag, we end the range calculation.
        // This is because views below can not scroll under a fixed view.
        break;
      }
    }
    return downScrollRange = Math.max(0, range);
  }
相关文章
|
5月前
|
API Android开发 开发者
Android UI设计: 什么是RecyclerView?为什么它比ListView更好?
Android UI设计: 什么是RecyclerView?为什么它比ListView更好?
72 2
|
Android开发 前端开发
Android应用开发—RecyclerView绘制蒙层
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/voidreturn/article/details/77718470 背景:如何在跨越两个或两个以上的item绘制一个view,该view需要跟随recyclerView的滑动而整体移动。
1146 0
|
15天前
|
缓存 Android开发 开发者
Android RecycleView 深度解析与面试题梳理
本文详细介绍了Android开发中高效且功能强大的`RecyclerView`,包括其架构概览、工作流程及滑动优化机制,并解析了常见的面试题。通过理解`RecyclerView`的核心组件及其优化技巧,帮助开发者提升应用性能并应对技术面试。
40 8
|
2月前
|
XML 数据可视化 API
Android经典实战之约束布局ConstraintLayout的实用技巧和经验
ConstraintLayout是Android中一款强大的布局管理器,它通过视图间的约束轻松创建复杂灵活的界面。相较于传统布局,它提供更高灵活性与性能。基本用法涉及XML定义约束,如视图与父布局对齐。此外,它支持百分比尺寸、偏移量控制等高级功能,并配有ConstraintSet和编辑器辅助设计。合理运用可显著提高布局效率及性能。
122 0
|
5月前
|
设计模式 算法 Android开发
2024年Android网络编程总结篇,androidview绘制流程面试
2024年Android网络编程总结篇,androidview绘制流程面试
2024年Android网络编程总结篇,androidview绘制流程面试
|
5月前
|
XML 数据可视化 Android开发
深入探究Android中的自定义View组件开发
【4月更文挑战第12天】在安卓应用开发中,创建具有独特交互和视觉表现的自定义View组件是增强用户体验的重要手段。本文将详细阐述如何从头开始构建一个Android自定义View,包括理解View的工作原理、处理绘制流程、事件分发机制以及属性的自定义与管理。通过具体案例分析,我们将一步步实现一个可定制的动态进度条,不仅具备基础功能,还能根据业务需求进行扩展,体现高度的产品个性化。
|
XML Java Android开发
Android ExpandableListView 使用中遇到的问题集锦
Android ExpandableListView 使用中遇到的问题集锦
|
Android开发
面试:讲讲 Android 的事件分发机制
写在前面 转眼间 面试系列 已经到了第九期了,由于文章将会持续更新,导致标题难看性,所以以后的标题将更正为本文类似的格式。 好了,话不多说,还是直入主题吧。
1737 0