I open source a convenient RecyclerView top top Android library, welcome you to visit github.com/lizijin/Sti… , if you use this library, please give your valuable comments.

It currently supports the following features:

  1. Support single type top suction function
  2. Support multiple types of top suction function
  3. Enable or disable the top suction function
  4. Support the function of suction top in specified position
  5. Supports setting top offset
  6. Support custom RecyclerView Item top boundary custom
  7. It works seamlessly with AppBarLayout

The main purpose of AppBarLayout design, I personally think there are the following:

  1. Cooperate with ScrollableView such as NestedScrollView and RecyclerView to complete nested sliding function. The initiative for creating slides is in the ScrollableView.
  2. ScrollableView depends on AppBarLayout. When AppBarLayout slides actively,ScollableView can adjust its position according to the position of AppBarLayout. Corresponding to 1, the initiative to create slides is in AppBarLayout.
  3. AppBarLayout inherits from the LinearLayout, which sets scroll markers on the sub-view to control whether the sub-view slides, whether it sucks the top when sliding up, and whether it slides down first.
  4. The OnOffsetChangedListener is exposed to make it more flexible to do things that AppBarLayout itself can’t.

This article will mainly focus on these points, combined with the source code to explain AppBarLayout.

1. View AppBarLayout from a subclass of LinearLayout

We all know that the AppBarLayout class inherits the LinearLayout class and is set to vertical. You’re all familiar with LinearLayout. In the length of AppBarLayout, I think we still need to emphasize several knowledge points. Although it is very simple, there are still some important details that will be ignored.

  1. Suppose AppBarLayout has four child Views: view1, view2, view3, and view4. View4 is drawn at the top, view1 is drawn at the bottom. As we probably all know, setting app:layout_scrollFlags=” Scroll “to a child view will make it scroll off the screen. We can set view1’s property app:layout_scrollFlags=” Scroll “. But if we only set view2’s layout_scrollFlags=”scroll”, then view2’s property is not set. The reason is that, assuming view2 can slide off the screen, it is bound to intersect with View1 and draw on view1. This effect is ugly, and Google avoided this bad user experience in the design. If the child View doesn’t have the scroll flag set, then its sibling, even if it has the scroll flag set, is invalid. The getTotalScrollRange method calculates how far AppBarLayout can slide off the screen. We can see if (flags & layoutparams.scroll_flag_scroll)! If = 0, the loop will be broken by break, and the sub-view behind will not participate in the calculation at all. The system code is as follows:
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); }Copy the code
  1. The onMeasure method of AppBarLayout is generally used to measure the LinearLayout. But why do I mention it here, because the ScrollableView that corresponds to it and the ScrollingViewBehavior that corresponds to it is kind of important, and we’ll talk about that, right

  2. The onLayout method of AppBarLayout is relatively common. To put it bluntly, it uses the layout idea of a LinearLayout. I mention it here for the same reason 2.

2. Event handling by AppBarLayout

AppBarLayout has a default behaviors, AppBarLayout $BaseBehavior, inherited from com. Google. Android. Material. The appbar. HeaderBehavior, The main purpose of this class is to handle touch events.

//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 ()) {// omit other events 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; }Copy the code

As you can see, the Move event calls the Scroll method, which as the name implies slides the AppBarLayout off or onto the screen.

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

The Scroll method is offset by offsetTopAndBottom. And this method, which returns a value, is mainly used to handle nested slides that are initiated by ScrollableView, but in this case, there’s no nested slide logic to deal with.

In general, when we use AppBarLayout and RecyclerView, the former is always on top of the latter. Then the problem comes, AppBarLayout slides out of the screen, if RecyclerView does not make the corresponding change, then there will be a blank in the middle of them, which is obviously unreasonable, then AppBarLayout is how to avoid this problem. The answer is through CoordinatorLayout dependencies and AppBarLayout$ScrollingViewBehavior.

3. ScrollingViewBehavior for RecyclerView measurement, Layout, slide with ABL

ScrollingViewBehavior does three things

  1. Slide along with APL
//ScrollingViewBehavior.java @Override public boolean onDependentViewChanged( @NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) { offsetChildAsNeeded(child, dependency); updateLiftedStateIfNeeded(child, dependency); return false; } // 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)); }}Copy the code
  1. ScrollableView measuring height, by the parent class HeaderScrollingViewBehavior implementation, the main algorithm is that measurement of the altitude of the highly – APL ScrollableView itself + APL sliding distance, the detail is very important, Think about why. Because the sliding distance of APL must be added, otherwise, when sliding up, the height of ScrollableView is not enough, and white vacuum zone will appear, affecting user experience.
//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; }Copy the code

3. Layout ScrollableView under APL, the code is relatively simple, mainly to calculate the position.

//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; }}Copy the code

4. Nested sliding of AppBarLayout

AppBarLayout nested sliding means that AppBarLayout follows the sliding as you scroll the ScrollableView below. There are three main cases:

  1. As ScrollableView slides up, ABL follows the slide
  2. When ScrollableView slides down, ABL follows the slide
  3. ScrollableView is at the top, and when sliding down, ABL handles slides that ScrollableView can’t handle

The methods for Case1 and Case2 are AppBarLayoutBaseBehavior#onNestedPreScroll, and the methods for Case3 are AppBarLayoutBaseBehavior#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)); }}Copy the code
  1. When ScrollableView sliding upwards, ABL sliding distance in [- child. GetUpNestedPreScrollRange (), 0] range, 0 means to restore to its original state, – child. GetUpNestedPreScrollRange () slip distance of the screen, getUpNestedPreScrollRange () value is equal to the getTotalScrollRange () value
int getUpNestedPreScrollRange() {
    return getTotalScrollRange();
  }
Copy the code
  1. When ScrollableView slides down, ABL sliding distance in [- 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); }}Copy the code

, is the difference between getUpNestedPreScrollRange and getDownNestedScrollRange getUpNestedPreScrollRange begins with the first View traversal, GetDownNestedScrollRange iterates the distance from the last View.

  1. ABL handles slides that ScrollableView can’t handle and in the onNestedScroll method, it only fires when ScrollableView slides down.
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); }}Copy the code
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); }Copy the code

5. AppBarLayout Scroll flag description

As we see in getDownNestedPreScrollRange methods, by iterating through the view, judgment, lp. ScrollFlags, etc to calculate the offset. So let’s talk about what these flags do

flag value meaning
SCROLL_FLAG_NO_SCROLL 0x0 Subviews do not allow sliding, default
SCROLL_FLAG_SCROLL 0x1 The child View allows sliding. If the sibling View in front of the child View does not set the flag, the flag bit is invalid
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED 1 < < 1 When a child View slides up out of the screen, the mininumHeight peaks and the flag of the child View behind it becomes invalid
SCROLL_FLAG_ENTER_ALWAYS 1 < < 2
SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED 1 < < 3
SCROLL_FLAG_SNAP 1 < < 4
SCROLL_FLAG_SNAP_MARGINS 1 < < 5

The relevant combination

combination value
FLAG_QUICK_RETURN SCROLL_FLAG_SCROLL `
FLAG_SNAP SCROLL_FLAG_SCROLL `
COLLAPSIBLE_FLAGS SCROLL_FLAG_EXIT_UNTIL_COLLAPSED `

Source code usage scenarios

Scenario 1 slide upwards. SCROLL_FLAG_SCROLL and SCROLL_FLAG_EXIT_UNTIL_COLLAPSED are determined when ABL follows the slide. Children traversal from front to back, if SCROLL_FLAG_SCROLL is not set, it will break traversal, if SCROLL_FLAG_SCROLL is set, Child. getMeasureheight, if SCROLL_FLAG_EXIT_UNTIL_COLLAPSED is also broken, Sliding distance – child. GetMinimumHeight, child. GetMinimumHeight will always stay on the screen.

The code snippet is as follows

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); }Copy the code

FLAG_QUICK_RETURN, SCroll_ENTER_always_collapsed, SCROLL_FLAG_EXIT_UNTIL_COLLAPSED, children from the bottom to the front.

  1. FLAG_QUICK_RETURN, which follows the flag for some distance when it is set to slide down. The distance is determined by SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED and SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
  2. If set SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED, sliding out distance to ViewCompat getMinimumHeight (child)
  3. If you do not meet the conditions of 2, but set the SCROLL_FLAG_EXIT_UNTIL_COLLAPSED, sliding out distance childHeight – ViewCompat. GetMinimumHeight (child), But due to set up the flag have a ViewCompat. GetMinimumHeight (child), suction a top effect is equal to sliding out entirely.
  4. If conditions 2 and 3 are not met, the slide distance is childHeight
  5. FLAG_QUICK_RETURN is different from SCROLL_FLAG_SCROLL. SCROLL_FLAG_SCROLL traverses from front to back, and breaks traversal if it’s not set. FLAG_QUICK_RETURN traverses from back to front, does not interrupt traversal if the Flag is not set, unless the Flag has been set and the slide distance is >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); }Copy the code

Scenario 3. ScrollableView is at the top. When sliding down, ABL handles slides that ScrollableView cannot handle. This scenario is the same as scenario 1. Only SCROLL_FLAG_SCROLL and SCROLL_FLAG_EXIT_UNTIL_COLLAPSED are evaluated. Go from front to back.

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); }Copy the code