自定義 Behavior 仿新浪微博發現頁的實現

使用CoordinatorLayout打造各類炫酷的效果android

自定義Behavior —— 仿知乎,FloatActionButton隱藏與展現git

NestedScrolling 機制深刻解析github

一步步帶你讀懂 CoordinatorLayout 源碼面試

自定義 Behavior -仿新浪微博發現頁的實現windows

ViewPager,ScrollView 嵌套ViewPager滑動衝突解決瀏覽器

自定義 behavior - 完美仿 QQ 瀏覽器首頁,美團商家詳情頁bash

重磅消息:小編我開始運營本身的公衆號了, 目前從事於 Android 開發,除了分享 Android開發相關知識,還有職場心得,面試經驗,學習心得,人生感悟等等。但願經過該公衆號,讓你看到程序猿不同的一面,咱們不僅會敲代碼,咱們還會。。。。。。微信

有興趣的話能夠關注個人公衆號 徐公碼字(stormjun94),或者拿起你的手機掃一掃,期待你的參與app

Android 技術人

效果圖

咱們先來看一下新浪微博發現頁的效果:ide

接下來咱們在來看一下咱們仿照新浪微博實現的效果

仿新浪微博效果圖

實現思路分析

咱們這裏先定義兩種狀態,open 和 close 狀態。

  • open 狀態指 Tab+ViewPager 尚未滑動到頂部的時候,header 還 沒有被徹底移除屏幕的時候
  • close 狀態指 Tab+ViewPager 滑動到頂部的時候,Header 被移除屏幕的時候

從效果圖,咱們能夠看到 在 open 狀態下,咱們向上滑動 ViewPager 裏面的 RecyclerView 的 時候,RecyclerView 並不會向上移動(RecyclerView 的滑動事件交給 外部的容器處理,被被所有消費掉了),而是整個佈局(指 Header + Tab +ViewPager)會向上偏移 。當 Tab 滑動到頂部的時候,咱們向上滑動 ViewPager 裏面的 RecyclerView 的時候,RecyclerView 能夠正常向上滑動,即此時外部容器沒有攔截滑動事件

同時咱們能夠看到在 open 狀態的時候,咱們是不支持下拉刷新的,這個比較容易實現,監聽頁面的狀態,若是是 open 狀態,咱們設置 SwipeRefreshLayout setEnabled 爲 false,這樣不會 攔截事件,在頁面 close 的時候,設置 SwipeRefreshLayout setEnabled 爲 TRUE,這樣就能夠支持下拉刷新了。

基於上面的分析,咱們這裏能夠把整個效果劃分爲兩個部分,第一部分爲 Header,第二部分爲 Tab+ViewPager。下文統一把第一部分稱爲 Header,第二部分稱爲 Content 。

須要實現的效果爲:在頁面狀態爲 open 的時候,向上滑動 Header 的時候,總體向上偏移,ViewPager 裏面的 RecyclerView 向上滑動的時候,消費其滑動事件,並總體向上移動。在頁面狀態爲 close 的時候,不消耗 RecyclerView 的 滑動事件。

在上一篇博客 一步步帶你讀懂 CoordinatorLayout 源碼 中,咱們有提到在 CoordinatorLayout中,咱們能夠經過 給子 View 自定義 Behavior 來處理事件。它是一個容器,實現了 NestedScrollingParent 接口。它並不會直接處理事件,而是會盡量地交給子 View 的 Behavior 進行處理。所以,爲了減小依賴,咱們把這兩部分的關係定義爲 Content 依賴於 Header。Header 移動的時候,Content 跟着 移動。因此,咱們在處理滑動事件的時候,只須要處理好 Header 部分的 Behavior 就oK了,Content 部分的 Behavior 不須要處理滑動事件,只需依賴於 Header ,跟着作相應的移動便可。


Header 部分的實現

Header 部分實現的兩個關鍵點在於

  1. 在頁面狀態爲 open 的時候,ViewPager 裏面的 RecyclerView 向上滑動的時候,消費其滑動事件,並總體向上移動。在頁面狀態爲 close 的時候,不消耗 RecyclerView 的 滑動事件
  2. 在頁面狀態爲 open 的時候,向上滑動 Header 的時候,總體向上偏移。

第一個關鍵點的實現

這裏區分頁面狀態是 open 仍是 close 狀態是經過 Header 是否移除屏幕來區分的,即 child.getTranslationY() == getHeaderOffsetRange() 。

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

複製代碼

NestedScrolling 機制深刻解析博客中,咱們對 NestedScrolling 機制作了以下的總結。

  • 在 Action_Down 的時候,Scrolling child 會調用 startNestedScroll 方法,經過 childHelper 回調 Scrolling Parent 的 startNestedScroll 方法。
  • 在 Action_move 的時候,Scrolling Child 要開始滑動的時候,會調用dispatchNestedPreScroll 方法,經過 ChildHelper 詢問 Scrolling Parent 是否要先於 Child 進行 滑動,若須要的話,會調用 Parent 的 onNestedPreScroll 方法,協同 Child 一塊兒進行滑動
  • 當 ScrollingChild 滑動完成的時候,會調用 dispatchNestedScroll 方法,經過 ChildHelper 詢問 Scrolling Parent 是否須要進行滑動,須要的話,會 調用 Parent 的 onNestedScroll 方法
  • 在 Action_down,Action_move 的時候,會調用 Scrolling Child 的stopNestedScroll ,經過 ChildHelper 詢問 Scrolling parent 的 stopNestedScroll 方法。
  • 若是須要處理 Fling 動做,咱們能夠經過 VelocityTrackerCompat 得到相應的速度,並在 Action_up 的時候,調用 dispatchNestedPreFling 方法,經過 ChildHelper 詢問 Parent 是否須要先於 child 進行 Fling 動做 在 Child 處理完 Fling 動做時候,若是 Scrolling Parent 還須要處理 Fling 動做,咱們能夠調用 dispatchNestedFling 方法,經過 ChildHelper ,調用 Parent 的 onNestedFling 方法

而 RecyclerView 也是 Scrolling Child (實現了 NestedScrollingChild 接口),RecyclerView 在開始滑動的 時候會先調用 CoordinatorLayout 的 startNestedScroll 方法,而 CoordinatorLayout 會 調用子 View 的 Behavior 的 startNestedScroll 方法。而且只有 boolean startNestedScroll 返回 TRUE 的 時候,纔會調用接下里 Behavior 中的 onNestedPreScroll 和 onNestedScroll 方法。

因此,咱們在 WeiboHeaderPagerBehavior 的 onStartNestedScroll 方法能夠這樣寫,能夠確保 只攔截垂直方向上的滾動事件,且當前狀態是打開的而且還能夠繼續向上收縮的時候還會攔截

@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);
    //攔截垂直方向上的滾動事件且當前狀態是打開的而且還能夠繼續向上收縮
    return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && canScroll &&
            !isClosed(child);

}


複製代碼

攔截事件以後,咱們須要在 RecyclerView 滑動以前消耗事件,而且移動 Header,讓其向上偏移。

@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);
    float halfOfDis = dy;
    //    不能滑動了,直接給 Header 設置 終值,防止出錯
    if (!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;
}


複製代碼

固然,咱們也須要處理 Fling 事件,在頁面沒有徹底關閉的 時候,消費全部 fling 事件。

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


複製代碼

至於滑動到頂部的動畫,我是經過 mOverScroller + FlingRunnable 來實現的 。完整代碼以下。

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);
        //攔截垂直方向上的滾動事件且當前狀態是打開的而且還能夠繼續向上收縮
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && canScroll &&
                !isClosed(child);

    }

    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                    float velocityX, float velocityY) {
        // 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();
                }

            }
        }

    }

    // 表示 Header TransLationY 的值是否達到咱們指定的閥值, headerOffsetRange,到達了,返回 false,
    // 不然,返回 true。注意 TransLationY 是負數。
    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);
        }

        return super.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);
        float halfOfDis = dy;
        //    不能滑動了,直接給 Header 設置 終值,防止出錯
        if (!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;
    }

    //    須要注意的是  Header 咱們是經過 setTranslationY 來移出屏幕的,因此這個值是負數
    private int getHeaderOffsetRange() {
        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.0f) {
            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 void closePager() {
        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.1f), 0, Math.round(dy +
                    0.1f), 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(); } } 複製代碼

第二個關鍵點的實現

在頁面狀態爲 open 的時候,向上滑動 Header 的時候,總體向上偏移。

在第一個關鍵點的實現上,咱們是經過自定義 Behavior 來處理 ViewPager 裏面 RecyclerView 的移動的,那咱們要怎樣監聽整個 Header 的滑動了。

那就是重寫 LinearLayout,將滑動事件交給 ScrollingParent(這裏是CoordinatorLayout) 去處理,CoordinatorLayout 再交給子 View 的 behavior 去處理。

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()){
            case MotionEvent.ACTION_DOWN:
                lastY = (int) event.getRawY();
                // 當開始滑動的時候,告訴父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 下拉, dy>0 賞花
                if (dy >0) { // 上滑的時候才交給父類去處理
                    if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 若是找到了支持嵌套滾動的父類
                            && dispatchNestedPreScroll(0, dy, consumed, offset)) {//
                        // 父類進行了一部分滾動
                    }
                }else{
                    if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 若是找到了支持嵌套滾動的父類
                            && dispatchNestedScroll(0, 0, 0,dy, offset)) {//
                        // 父類進行了一部分滾動
                    }
                }
                break;
        }
        return true;
    }



    private NestedScrollingChildHelper getScrollingChildHelper() {
        return mScrollingChildHelper;
    }

    // 接口實現--------------------------------------------------

    @Override
    public void setNestedScrollingEnabled(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) {
        return getScrollingChildHelper().dispatchNestedPreFling(velocityX,
                velocityY);
    }
}

複製代碼

Content 部分的實現

Content 部分的實現也主要有兩個關鍵點

  • 總體置於 Header 之下
  • Content 跟着 Header 移動。即 Header 位置發生變化的時候,Content 也須要隨着調整位置。

第一個關鍵點的實現

總體置於 Header 之下。這個咱們能夠參考 APPBarLayout 的 behavior,它是這樣處理的。

/**
 * 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<View> 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; } } 複製代碼

這個基類的代碼仍是很好理解的,由於以前就說過了,正常來講被依賴的 View 會優先於依賴它的 View 處理,因此須要依賴的 View 能夠在 measure/layout 的時候,找到依賴的 View 並獲取到它的測量/佈局的信息,這裏的處理就是依靠着這種關係來實現的.

咱們的實現類,須要重寫的除了抽象方法 findFirstDependency 外,還須要重寫 getScrollRange,咱們把 Header 的 Id id_weibo_header 定義在 ids.xml 資源文件內,方便依賴的判斷.

至於縮放的高度,根據 結果圖 得知是 0,得出以下代碼

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 {
            return super.getScrollRange(v);
        }
    }

複製代碼

第二個關鍵點的實現:

Content 跟着 Header 移動。即 Header 位置發生變化的時候,Content 也須要隨着調整位置。

主要的邏輯就是 在 layoutDependsOn 方法裏面,判斷 dependcy 是否是 HeaderView ,是的話,返回TRUE,這樣在 Header 位置發生變化的時候,會回調 onDependentViewChanged 方法,在該方法裏面,作相應的偏移。TranslationY 是根據比例算出來的 translationY = (int) (-dependencyTranslationY / (getHeaderOffsetRange() * 1.0f) * getScrollRange(dependency));

完整代碼以下:

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) {
        float dependencyTranslationY = 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;
    }
}

複製代碼

題外話

  • NestedScrolling 機制,對比傳統的事件分發機制真的很強大。這種仿新浪微博發現頁效果, 若是用傳統的事件分發機制來作,估計很難實現,處理起來會有一大堆坑。
  • 看完了這種仿新浪微博發現頁的效果,你是否是學到了什麼?若是讓你 模仿 仿 QQ 瀏覽器首頁效果,你能實現話。

最後,特別感謝寫這篇博客 自定義Behavior的藝術探索-仿UC瀏覽器主頁 的開發者,沒有這篇博客做爲參考,這種效果我很大概率是實現 不了的。你們以爲效果還不錯的話,順手到 github 上面給我 star,謝謝。github 地址


參考文章:

自定義Behavior的藝術探索-仿UC瀏覽器主頁

github 地址

最後的最後,賣一下廣告,歡迎你們關注個人微信公衆號,掃一掃下方二維碼或搜索微信號 徐公碼字(stormjun94),便可關注。 目前專一於 Android 開發,主要分享 Android開發相關知識和一些相關的優秀文章,包括我的總結,職場經驗等。

相關文章
相關標籤/搜索