You can use CoordinatorLayout to create all kinds of cool things

Custom Behavior – Mimicking Zhihu, FloatActionButton hidden and displayed

NestedScrolling mechanism for in-depth parsing

You can read the source code for CoordinatorLayout step by step

Custom Behavior – implementation of the discovery page imitating Sina Weibo

ViewPager, ScrollView nested ViewPager sliding conflict resolved

Custom behavior – perfect imitation QQ browser home page, Meituan business details page

Big news: Xiaobian I began to operate their own public account, currently engaged in Android development, in addition to sharing Android development knowledge, and workplace experience, interview experience, learning experience, life experience and so on. Hope that through the public account, let you see the program ape is not the same side, we will not only knock on the code, we will…

If you are interested, you can follow my public account Android technical person (Stormjun94), or pick up your mobile phone to scan, looking forward to your participation

rendering

Let’s first take a look at the effect of sina Weibo discovery page:

Now let’s take a look at the effect we achieved by imitating Sina Weibo

Analysis of implementation ideas

So let’s define two states, open and close.

  • The open state is when Tab+ViewPager has not been swiped to the top and the header has not been completely removed from the screen
  • The close state is when Tab+ViewPager slides to the top and the Header is removed from the screen

From the renderings, we can see that in the open state, when we slide up the RecyclerView inside the ViewPager, RecyclerView will not move up (the sliding event of RecyclerView is handed to the external container for processing, The entire layout (Header + Tab +ViewPager) is offset upwards. When Tab slides to the top, the RecyclerView inside the ViewPager slides up, and the RecyclerView slides up normally, that is, the external container does not intercept the sliding event.

In open state, we set the SwipeRefreshLayout setEnabled to false so that events will not be blocked. When the page is closed, set the SwipeRefreshLayout setEnabled to TRUE to support the pull-down refresh.

Based on the above analysis, we can divide the effect into two parts, the first part is Header, and the second part is Tab+ViewPager. The first part is referred to as Header and the second part as Content.

The effect that needs to be realized is: when the page state is open, when the Header is sliding up, the overall upward offset, when the RecyclerView inside the ViewPager is sliding up, consumption of its sliding event, and the overall upward movement. When the page state is close, it does not consume the RecyclerView sliding event.

In the last blog for CoordinatorLayout, we talked about how in CoordinatorLayout, you can customize the Behavior of the child View to handle events. It is a container that implements the NestedScrollingParent interface. Instead of handling the event directly, it hands it over to the child View’s Behavior as much as possible. Therefore, to reduce dependencies, we define the relationship between the two parts as Content depends on the Header. When the Header moves, the Content moves with it. Therefore, when dealing with sliding events, we only need to deal with the Behavior of the Header part, and the Behavior of the Content part does not need to deal with sliding events. We only need to rely on the Header and move accordingly.


Implementation of the Header section

The two key points of the Header implementation are

  1. When the page state is open, the RecyclerView inside the ViewPager slides up, consuming its sliding events and moving up as a whole. When the page state is close, it does not consume the RecyclerView sliding event
  2. When the page state is open, the overall offset is upward when the Header is swiped up.

Implementation of the first key point

The distinction between the page state being open and close is determined by whether the Header removes the screen, child-.getTranslationy () == getHeaderOffsetRange().

private boolean isClosed(View child) {
    boolean isClosed = child.getTranslationY() == getHeaderOffsetRange();
    return isClosed;
}

Copy the code

In the in-depth scrolling blog, we summarized the NestedScrolling mechanism as follows.

  • In Action_Down mode, the Scrolling child calls the startNestedScroll method. ChildHelper calls back the Parent’s startNestedScroll method.
  • For Action_move, the dispatchNestedPreScroll method is called when the Scrolling Child starts Scrolling. ChildHelper asks the Parent whether to scroll ahead of the Child, and if so, calls the Parent’s onNestedPreScroll method to scroll in collaboration with the Child
  • When ScrollingChild finishes Scrolling, the dispatchNestedScroll method is called. ChildHelper asks whether Scrolling Parent needs to scroll. The Parent’s onNestedScroll method is called
  • Scrolling Child stopNestedScroll is called for Action_down or Action_move. Ask for Scrolling parent’s stopNestedScroll method using ChildHelper.
  • If you want to Fling an action, use VelocityTrackerCompat to get the speed and call dispatchNestedPreFling while Action_up. If Scrolling is Scrolling and Scrolling is Scrolling, ChildHelper asks the Parent whether to scroll to the Fling before the child. We can call the dispatchNestedFling method and call the Parent’s onNestedFling method via ChildHelper

RecyclerView is ScrollingChild (NestedScrollingChild interface). RecyclerView will call the startNestedScroll method of CoordinatorLayout first when it starts sliding, And the CoordinatorLayout calls the startNestedScroll method of the Behavior of the child View. And only if Boolean startNestedScroll returns TRUE will the onNestedScroll and onNestedScroll methods in the following Behavior be called.

So, the onStartNestedScroll method in WeiboHeaderPagerBehavior can be written like this to ensure that it intercepts only vertical scroll events, if the current state is open and can continue to shrink up

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View
        directTargetChild, View target, int nestedScrollAxes) {
    if (BuildConfig.DEBUG) {
        Log.d(TAG, "onStartNestedScroll: nestedScrollAxes="+ nestedScrollAxes); } boolean canScroll = canScroll(child, 0); // Intercepts vertical scrolling events with the current state on and can continue to shrink upreturn(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) ! = 0 && canScroll && ! isClosed(child); }Copy the code

After intercepting the event, we need to consume the event before the RecyclerView slides, and move the Header to offset it up.

@Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed); //dy>0 scroll up; dy<0,scroll down Log.i(TAG,"onNestedPreScroll: dy=" + dy);
    floathalfOfDis = dy; // Set the final value of the Header directly to prevent errorsif(! canScroll(child, halfOfDis)) { child.setTranslationY(halfOfDis > 0 ? getHeaderOffsetRange() : 0); }else {
        child.setTranslationY(child.getTranslationY() - halfOfDis);
    }
    //consumed all scroll behavior after we started Nested Scrolling
    consumed[1] = dy;
}


Copy the code

Of course, we also need to process the Fling events and consume all the Fling events while the page is not completely closed.

@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                float velocityX, float velocityY) {
    // consumed the flinging behavior until Closed
    return! isClosed(child); }Copy the code

As for the sliding to the top animation, I did it with mOverScroller + FlingRunnable. The complete code is below.

public class WeiboHeaderPagerBehavior extends ViewOffsetBehavior {
    private static final String TAG = "UcNewsHeaderPager";
    public static final int STATE_OPENED = 0;
    public static final int STATE_CLOSED = 1;
    public static final int DURATION_SHORT = 300;
    public static final int DURATION_LONG = 600;

    private int mCurState = STATE_OPENED;
    private OnPagerStateListener mPagerStateListener;

    private OverScroller mOverScroller;

    private WeakReference<CoordinatorLayout> mParent;
    private WeakReference<View> mChild;

    public void setPagerStateListener(OnPagerStateListener pagerStateListener) {
        mPagerStateListener = pagerStateListener;
    }

    public WeiboHeaderPagerBehavior() {
        init();
    }

    public WeiboHeaderPagerBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mOverScroller = new OverScroller(BaseAPP.getAppContext());
    }

    @Override
    protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        super.layoutChild(parent, child, layoutDirection);
        mParent = new WeakReference<CoordinatorLayout>(parent);
        mChild = new WeakReference<View>(child);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View
            directTargetChild, View target, int nestedScrollAxes) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "onStartNestedScroll: nestedScrollAxes="+ nestedScrollAxes); } boolean canScroll = canScroll(child, 0); // Intercepts vertical scrolling events with the current state on and can continue to shrink upreturn(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) ! = 0 && canScroll && ! isClosed(child); } @Override public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,float velocityX, floatvelocityY) { // consumed the flinging behavior until Closed boolean coumsed = ! isClosed(child); Log.i(TAG,"onNestedPreFling: coumsed=" +coumsed);
        return coumsed;
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                 float velocityX, float velocityY, boolean consumed) {
        Log.i(TAG, "onNestedFling: velocityY=" +velocityY);
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY,
                consumed);

    }

    private boolean isClosed(View child) {
        boolean isClosed = child.getTranslationY() == getHeaderOffsetRange();
        return isClosed;
    }

    public boolean isClosed() {
        return mCurState == STATE_CLOSED;
    }

    private void changeState(int newState) {
        if(mCurState ! = newState) { mCurState = newState;if (mCurState == STATE_OPENED) {
                if (mPagerStateListener != null) {
                    mPagerStateListener.onPagerOpened();
                }

            } else {
                if(mPagerStateListener ! = null) { mPagerStateListener.onPagerClosed(); }}}} // indicates whether the value of Header TransLationY has reached the threshold we specified, headerOffsetRange, has reached, and returnsfalse, // Otherwise, returntrue. Notice that TransLationY is negative. private boolean canScroll(View child,float pendingDy) {
        int pendingTranslationY = (int) (child.getTranslationY() - pendingDy);
        int headerOffsetRange = getHeaderOffsetRange();
        if (pendingTranslationY >= headerOffsetRange && pendingTranslationY <= 0) {
            return true;
        }
        return false;
    }



    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, final View child, MotionEvent
            ev) {

        boolean closed = isClosed();
        Log.i(TAG, "onInterceptTouchEvent: closed=" + closed);
        if(ev.getAction() == MotionEvent.ACTION_UP && ! closed) { handleActionUp(parent,child); }returnsuper.onInterceptTouchEvent(parent, child, ev); } @Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed); //dy>0 scroll up; dy<0,scroll down Log.i(TAG,"onNestedPreScroll: dy=" + dy);
        floathalfOfDis = dy; // Set the final value of the Header directly to prevent errorsif(! canScroll(child, halfOfDis)) { child.setTranslationY(halfOfDis > 0 ? getHeaderOffsetRange() : 0); }else{ child.setTranslationY(child.getTranslationY() - halfOfDis); } //consumed all scroll behavior after we started Nested Scrolling consumed[1] = dy; } // It is important to note that Header is passedsetTranslationY to move off screen, so this value is negative private intgetHeaderOffsetRange() {
        return BaseAPP.getInstance().getResources().getDimensionPixelOffset(R.dimen
                .weibo_header_offset);
    }

    private void handleActionUp(CoordinatorLayout parent, final View child) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "handleActionUp: ");
        }
        if(mFlingRunnable ! = null) { child.removeCallbacks(mFlingRunnable); mFlingRunnable = null; } mFlingRunnable = new FlingRunnable(parent, child);if(child. GetTranslationY () < getHeaderOffsetRange () / 6.0 f) {mFlingRunnable. ScrollToClosed (DURATION_SHORT); }else {
            mFlingRunnable.scrollToOpen(DURATION_SHORT);
        }

    }

    private void onFlingFinished(CoordinatorLayout coordinatorLayout, View layout) {
        changeState(isClosed(layout) ? STATE_CLOSED : STATE_OPENED);
    }

    public void openPager() {
        openPager(DURATION_LONG);
    }

    /**
     * @param duration open animation duration
     */
    public void openPager(int duration) {
        View child = mChild.get();
        CoordinatorLayout parent = mParent.get();
        if(isClosed() && child ! = null) {if(mFlingRunnable ! = null) { child.removeCallbacks(mFlingRunnable); mFlingRunnable = null; } mFlingRunnable = new FlingRunnable(parent, child); mFlingRunnable.scrollToOpen(duration); } } public voidclosePager() {
        closePager(DURATION_LONG);
    }

    /**
     * @param duration close animation duration
     */
    public void closePager(int duration) {
        View child = mChild.get();
        CoordinatorLayout parent = mParent.get();
        if(! isClosed()) {if(mFlingRunnable ! = null) { child.removeCallbacks(mFlingRunnable); mFlingRunnable = null; } mFlingRunnable = new FlingRunnable(parent, child); mFlingRunnable.scrollToClosed(duration); } } private FlingRunnable mFlingRunnable; /** * For animation , Why not use {@link android.view.ViewPropertyAnimator } to play animation * is of the * other {@link CoordinatorLayout.Behavior} that depend on this could not receiving the * correct result of * {@link View#getTranslationY()} after animation finished for whatever reason that i don't know
     */
    private class FlingRunnable implements Runnable {
        private final CoordinatorLayout mParent;
        private final View mLayout;

        FlingRunnable(CoordinatorLayout parent, View layout) {
            mParent = parent;
            mLayout = layout;
        }

        public void scrollToClosed(int duration) {
            float curTranslationY = ViewCompat.getTranslationY(mLayout);
            float dy = getHeaderOffsetRange() - curTranslationY;
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "scrollToClosed:offest:" + getHeaderOffsetRange());
                Log.d(TAG, "scrollToClosed: cur0:" + curTranslationY + ",end0:" + dy);
                Log.d(TAG, "scrollToClosed: cur:" + Math.round(curTranslationY) + ",end:" + Math
                        .round(dy));
                Log.d(TAG, "scrollToClosed: cur1:" + (int) (curTranslationY) + ",end:"+ (int) dy); } mOverScroller. StartScroll (0, Math. Round (curTranslationY - 0.1 f), 0, Math. Round (dy + 0.1 f), duration). start(); } public void scrollToOpen(int duration) {float curTranslationY = ViewCompat.getTranslationY(mLayout);
            mOverScroller.startScroll(0, (int) curTranslationY, 0, (int) -curTranslationY,
                    duration);
            start();
        }

        private void start() {
            if (mOverScroller.computeScrollOffset()) {
                mFlingRunnable = new FlingRunnable(mParent, mLayout);
                ViewCompat.postOnAnimation(mLayout, mFlingRunnable);
            } else {
                onFlingFinished(mParent, mLayout);
            }
        }

        @Override
        public void run() {
            if(mLayout ! = null && mOverScroller ! = null) {if (mOverScroller.computeScrollOffset()) {
                    if (BuildConfig.DEBUG) {
                        Log.d(TAG, "run: " + mOverScroller.getCurrY());
                    }
                    ViewCompat.setTranslationY(mLayout, mOverScroller.getCurrY());
                    ViewCompat.postOnAnimation(mLayout, this);
                } else {
                    onFlingFinished(mParent, mLayout);
                }
            }
        }
    }

    /**
     * callback for HeaderPager 's state */ public interface OnPagerStateListener { /** * do callback when pager closed */ void onPagerClosed(); /** * do callback when pager opened */ void onPagerOpened(); }}Copy the code

Implementation of the second key point

When the page state is open, the overall offset is upward when the Header is swiped up.

In the implementation of the first key point, we are through the custom Behavior to handle the movement of RecyclerView inside the ViewPager, then we need to listen to the whole Header slide.

That’s rewriting the LinearLayout, giving the sliding event to the ScrollingParent (which is CoordinatorLayout), which gives it to the behavior of the child View.

public class NestedLinearLayout extends LinearLayout implements NestedScrollingChild {

    private static final String TAG = "NestedLinearLayout";

    private final int[] offset = new int[2];
    private final int[] consumed = new int[2];

    private NestedScrollingChildHelper mScrollingChildHelper;
    private int lastY;

    public NestedLinearLayout(Context context) {
        this(context, null);
    }

    public NestedLinearLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NestedLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initData();
    }

    private void initData() {
        if (mScrollingChildHelper == null) {
            mScrollingChildHelper = new NestedScrollingChildHelper(this);
            mScrollingChildHelper.setNestedScrollingEnabled(true);
        }
    }

  @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            caseMotionEvent.ACTION_DOWN: lastY = (int) event.getRawY(); / / when start to slide, tell the parent view startNestedScroll (ViewCompat. SCROLL_AXIS_HORIZONTAL | ViewCompat. SCROLL_AXIS_VERTICAL);break;
            case MotionEvent.ACTION_MOVE:

                return true;
        }
        return super.onInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "onTouchEvent: ACTION_MOVE=");
                int y = (int) (event.getRawY());
                int dy =lastY- y;
                lastY = y;
                Log.i(TAG, "onTouchEvent: lastY=" + lastY);
                Log.i(TAG, "onTouchEvent: dy="+ dy); // dy < 0 drop, dy>0if(dy >0) {// The parent class handles the slideif(startNestedScroll(viewcompat.scroll_axis_vertical) // dispatchNestedPreScroll(0, dy, consumed, Offset)) {// // the parent class is partially rolled}}else{
                    ifStartNestedScroll (viewcompat.scroll_axis_vertical) dispatchNestedScroll(0, 0, 0,dy, Offset)) {// // the parent class is partially rolled}}break;
        }
        return true;
    }



    private NestedScrollingChildHelper getScrollingChildHelper() {
        returnmScrollingChildHelper; } / / interface implementation -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- @ Override public voidsetNestedScrollingEnabled(boolean enabled) {
        getScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return getScrollingChildHelper().isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return getScrollingChildHelper().startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        getScrollingChildHelper().stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return getScrollingChildHelper().hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                                        int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedScroll(dxConsumed,
                dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed,
                                           int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy,
                consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY,
                                       boolean consumed) {
        return getScrollingChildHelper().dispatchNestedFling(velocityX,
                velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        returngetScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY); }}Copy the code

Implementation of the Content section

The implementation of the Content section also has two key points

  • Put it all under the Header
  • The Content moves with the Header. When the Header position changes, the Content needs to adjust its position as well.

Implementation of the first key point

Put it all under the Header. We can refer to the Behavior of APPBarLayout, which handles this.

/**
 * Copy from Android design library
 * <p/>
 * Created by xujun
 */
public abstract class HeaderScrollingViewBehavior extends ViewOffsetBehavior<View> {
    private final Rect mTempRect1 = new Rect();
    private final Rect mTempRect2 = new Rect();

    private int mVerticalLayoutGap = 0;
    private int mOverlayTop;

    public HeaderScrollingViewBehavior() {
    }

    public HeaderScrollingViewBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onMeasureChild(CoordinatorLayout parent, 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
      
        dependencies = parent.getDependencies(child); final View header = findFirstDependency(dependencies); if (header ! = null) { if (ViewCompat.getFitsSystemWindows(header) && ! ViewCompat.getFitsSystemWindows(child)) { // If the header is fitting system windows then we need to also, // otherwise we'
      ll get CoL's compatible measuring ViewCompat.setFitsSystemWindows(child, true); if (ViewCompat.getFitsSystemWindows(child)) { // If the set succeeded, trigger a new layout and return true child.requestLayout(); return true; } } if (ViewCompat.isLaidOut(header)) { int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec); if (availableHeight == 0) { // If the measure spec doesn't specify a size, use the current height
                        availableHeight = parent.getHeight();
                    }

                    final int height = availableHeight - header.getMeasuredHeight() + getScrollRange(header);
                    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;
    }

    @Override
    protected void layoutChild(final CoordinatorLayout parent, 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 = mTempRect1; 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 Rect out = mTempRect2; 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); mVerticalLayoutGap = out.top - header.getBottom(); }else {
            // If we don't have a dependency, let super handle it
            super.layoutChild(parent, child, layoutDirection);
            mVerticalLayoutGap = 0;
        }
    }

    float getOverlapRatioForOffset(final View header) {
        return 1f;
    }

    final int getOverlapPixelsForOffset(final View header) {
        return mOverlayTop == 0
                ? 0
                : MathUtils.constrain(Math.round(getOverlapRatioForOffset(header) * mOverlayTop),
                0, mOverlayTop);

    }

    private static int resolveGravity(int gravity) {
        return gravity == Gravity.NO_GRAVITY ? GravityCompat.START | Gravity.TOP : gravity;
    }

    protected abstract View findFirstDependency(List<View> views);

    protected int getScrollRange(View v) {
        return v.getMeasuredHeight();
    }

    /**
     * The gap between the top of the scrolling view and the bottom of the header layout in pixels.
     */
    final int getVerticalLayoutGap() {
        return mVerticalLayoutGap;
    }

    /**
     * Set the distance that this view should overlap any {@link AppBarLayout}.
     *
     * @param overlayTop the distance in px
     */
    public final void setOverlayTop(int overlayTop) {
        mOverlayTop = overlayTop;
    }

    /**
     * Returns the distance that this view should overlap any {@link AppBarLayout}.
     */
    public final int getOverlayTop() {
        return mOverlayTop;
    }

}

Copy the code

The code of this base class is easy to understand, because as mentioned before, normally the dependent View is processed before the dependent View. Therefore, the dependent View can find the dependent View and obtain its measurement /layout information during measure/layout. The processing here depends on this relationship.

In addition to the abstract method findFirstDependency, our implementation class also needs to rewrite getScrollRange. We define the Id of the Header, ID_weibo_header, in the ids. XML resource file to facilitate the judgment of dependence.

As for the height of the zoom, according to the resulting graph, it is 0, resulting in the following code

private int getFinalHeight() {
     Resources resources = BaseAPP.getInstance().getResources();

    return 0;
}

    @Override
    protected int getScrollRange(View v) {
        if (isDependOn(v)) {
            return Math.max(0, v.getMeasuredHeight() - getFinalHeight());
        } else {
            returnsuper.getScrollRange(v); }}Copy the code

Implementation of the second key point:

The Content moves with the Header. When the Header position changes, the Content needs to adjust its position as well.

The main logic behind this layoutDependsOn method is to determine if this method is a HeaderView or not. If this method is a HeaderView, the layoutDependsOn method returns TRUE. The onDependentViewChanged method is called back, in which the corresponding offset is made. DependencyTranslationY = (int) (-dependencytranslationy/(getHeaderOffsetRange() * 1.0f) * getScrollRange(dependency));

The complete code is as follows:

public class WeiboContentBehavior extends HeaderScrollingViewBehavior {
    private static final String TAG = "WeiboContentBehavior";

    public WeiboContentBehavior() {
    }

    public WeiboContentBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return isDependOn(dependency);
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "onDependentViewChanged");
        }
        offsetChildAsNeeded(parent, child, dependency);
        return false;
    }

    private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
        floatdependencyTranslationY = dependency.getTranslationY(); Int translationY = (int) (-dependencyTranslationy/(getHeaderOffsetRange() * 1.0f) * getScrollRange(dependency)); Log.i(TAG,"offsetChildAsNeeded: translationY=" + translationY);
        child.setTranslationY(translationY);

    }

    @Override
    protected View findFirstDependency(List<View> views) {
        for (int i = 0, z = views.size(); i < z; i++) {
            View view = views.get(i);
            if (isDependOn(view)) return view;
        }
        return null;
    }

    @Override
    protected int getScrollRange(View v) {
        if (isDependOn(v)) {
            return Math.max(0, v.getMeasuredHeight() - getFinalHeight());
        } else {
            return super.getScrollRange(v);
        }
    }

    private int getHeaderOffsetRange() {
        return BaseAPP.getInstance().getResources().getDimensionPixelOffset(R.dimen
                .weibo_header_offset);
    }

    private int getFinalHeight() {
        Resources resources = BaseAPP.getInstance().getResources();

        return 0;
    }

    private boolean isDependOn(View dependency) {
        return dependency != null && dependency.getId() == R.id.id_weibo_header;
    }
}

Copy the code

digression

  • NestedScrolling is really powerful compared to traditional event scrolling. This imitation of Sina Weibo discovery page effect, if the traditional event distribution mechanism to do, it is estimated that it is difficult to achieve, there will be a lot of pits.
  • Did you learn something from the sina Weibo discovery page? If let you imitate imitate QQ browser home page effect, you can achieve.

Finally, special thanks to the developer who wrote this blog about the art exploration of custom Behavior – imitation UC browser home page. Without this blog as a reference, I would not be able to achieve this effect. If you think the effect is good, feel free to give me the star on Github, thank you. Making the address


Reference article:

Explore the art of custom Behavior – imitation UC browser home page

Making the address

Finally, I would like to sell the advertisement and welcome you to pay attention to my wechat public number. You can follow it by scanning the qr code below or searching the wechat account Stormjun. Currently, I focus on Android development, mainly sharing Android development related knowledge and some relevant excellent articles, including personal summary, workplace experience and so on.