1. 程式人生 > >自定義Behavior之ToolBar上滑TabLayout顏色漸變

自定義Behavior之ToolBar上滑TabLayout顏色漸變

本篇文章介紹使用CoordinatorLayout的自定義Behavior來實現如下的效果

這裡寫圖片描述

分析本例效果

首先我們來分析下整個例子需要實現哪些效果:

  • ToolBar的上滑和下滑
  • TabLayout跟隨ToolBar上移和下移
  • TabLayout顏色會跟隨距離的變化發生漸變
  • 滑動時會有黏性效果
    • 滑動距離超過中間值後放開會自動滑向想要的方向
    • 滑動距離未超過中間值放開則會自動回彈

本例需要的幾個重要方法介紹

我們的例子中重寫了Behavior的幾個重要方法:

  • layoutDependsOn
  • onDependentViewChanged
  • onLayoutChild
  • onStartNestedScroll
  • onNestedPreScroll
  • onNestedScroll
  • onStopNestedScroll
  • onNestedScrollAccepted
  • onNestedPreFling

自定義 Behavior 實現思路

將ToolBar來作為依賴檢視,TabLayout所在的父佈局作為子檢視,TabLayout通過 Nested Scrolling 機制調整ToolBar的位置,進而因ToolBar位置的改變,從而計算出一個百分比值,利用這個百分比值來影響自身的位置以及顏色

實現過程具體分析

有了思路我們就能一步步來實現效果了

首先繼承自 Behavior,這是一個範型類,範型型別為被 Behavior 控制的檢視型別:

public class ToolBarScrollBehavior extends CoordinatorLayout.Behavior<View> {

    private static final String TAG = ToolBarScrollBehavior.class.getSimpleName();
    private WeakReference<View> mDependencyView;
    private WeakReference<TabLayout> mTabLayout;
    private OverScroller mOverScroller;
    private
Handler mHandler; private boolean isScrolling = false; private Context mContext; private ArgbEvaluator evaluator; public ToolBarScrollBehavior(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; mOverScroller = new OverScroller(context); mHandler = new Handler(); evaluator = new ArgbEvaluator(); } ...... }

解釋一下幾個重要變數的作用:

  • Scroller
    用來實現使用者釋放手指後的滑動動畫
  • Handler
    用來驅動 Scroller 的執行
  • dependentView
    是依賴檢視的一個弱引用,方便我們後面的操作
  • mTabLayout
    是子視圖裡TabLayout的一個弱引用
  • ArgbEvaluator
    是一個可以通過[0,1]的偏移量來計算兩種色彩漸變色的類
@Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
        if (params != null && params.height == CoordinatorLayout.LayoutParams.MATCH_PARENT) {
            child.layout(0, 0, parent.getWidth(), parent.getHeight());
            return true;
        }
        return super.onLayoutChild(parent, child, layoutDirection);
    }

由於CoodinatorLayout本質上是一個FrameLayout,不會像 LinearLayout 一樣能自動分配各個 View 的高度,本例由於ToolBar上滑後會隱藏,子檢視就會填滿整個螢幕,因此我們將CoodinatorLayout的寬和高填充子檢視

@Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        if (dependency != null && dependency.getId() == R.id.toolbar) {
            mDependencyView = new WeakReference<>(dependency);
            mTabLayout = new WeakReference<>((TabLayout) ((LinearLayout) child).getChildAt(0));
            return true;
        }
        return false;
    }

負責查詢該 Behavior 是否依賴於某個檢視,這裡我們判斷依賴檢視是否為ToolBar,是的話返回true,之後的其他操作都會圍繞ToolBar來執行了,我們可以在這裡拿到子檢視內的TabLayout,由於CoordinatorLayout 子檢視的層級關係,如果想在子檢視中使用 Behavior 進行控制,那麼這個子檢視一定是 CoordinatorLayout 的直接孩子,間接子檢視是不具有 behavior 屬性的,因此我們要在這裡拿到子檢視內的TabLayout引用,方便之後的顏色漸變操作

@Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        final float progress = Math.abs(dependency.getTranslationY() / (dependency.getHeight()));

        child.setTranslationY(dependency.getHeight() + dependency.getTranslationY());

        final int colorPrimary = getColor(R.color.colorPrimary);
        final int evaluate1 = (Integer) evaluator.evaluate(progress, Color.WHITE, colorPrimary);
        final int evaluate2 = (Integer) evaluator.evaluate(progress, colorPrimary, Color.WHITE);

        getTabLayoutView().setBackgroundColor(evaluate1);
        getTabLayoutView().setTabTextColors(evaluate2, evaluate2);
        getTabLayoutView().setSelectedTabIndicatorColor(evaluate2);

        return true;
    }

我們可以在這個方法裡做調整子檢視的操作,因為當依賴檢視發生變化的時候就會回撥這個方法
依賴檢視發生位移會影響translateY的值,我們主要用到的就是這個translateY
我們可以根據依賴檢視的translateY除以依賴檢視的高度來計算出一個百分比因數(0-1),通過這個因數配合ArgbEvaluator我們可以用來計算TabLayout顏色漸變的顏色值
最後同樣也要通過依賴檢視的translateY來讓子檢視始終緊跟依賴檢視下面

@Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

該方法在使用者按下手指的時候回撥,該方法在返回true的時候才會引發其他一系列的回撥,這裡我們只需要考慮垂直滑動,因此在垂直滑動條件成立的時候返回true

@Override
    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child,
                                       View directTargetChild, View target, int nestedScrollAxes) {
        isScrolling = false;
        mOverScroller.abortAnimation();
        super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

在這個方法裡我們可以做一些準備工作,比如讓之前的滑動動畫結束

@Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        // 在這個方法裡面只處理向上滑動
        if (dy < 0) {
            return;
        }
        View dependencyView = getDependencyView();
        float transY = dependencyView.getTranslationY() - dy;
        if (transY < 0 && -transY < getToolbarSpreadHeight()) {
            dependencyView.setTranslationY(transY);
            consumed[1] = dy;
        }
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        // 在這個方法裡只處理向下滑動
        if (dyUnconsumed > 0) {
            return;
        }
        View dependencyView = getDependencyView();
        float transY = dependencyView.getTranslationY() - dyUnconsumed;
        if (transY < 0) {
            dependencyView.setTranslationY(transY);
        }
    }

這兩個方法放在一起解釋,由於onNestedPreScroll方法會優先於onNestedScroll之前呼叫,因此我們可以將上滑動作分配到onNestedPreScroll,下滑動作分配到onNestedScroll,我們來分析下這樣實現的原理:

  • 上滑
    當用戶上滑時onNestedPreScroll優先呼叫,我們判斷滑動方向,向上滑動才繼續執行,通過調整依賴檢視的translateY值來進行上移操作,並且消耗相應的consumed值,之後會回撥onNestedScroll方法,如果dyUnconsumed還有值的話說明沒有上滑操作沒有完成,直接中斷,然後繼續回撥onNestedPreScroll方法,重複一遍上面的操作,直到onNestedScroll方法裡的dyUnconsumed消耗到0時就表示上滑到頭了,整個上滑操作完成
  • 下滑
    我們在onNestedPreScroll方法中只有上滑時dy>0的情況才繼續執行,因此下滑時dy<0的值不會在onNestedPreScroll中消耗掉,會直接傳遞到onNestedScroll方法中的dyUnconsumed,然後我們可以通過調整依賴檢視的translateY值來進行下移操作,並消耗相應的dyUnconsumed值,然後不斷重複上面步驟直到依賴檢視完全實現完畢,整個下滑操作完成

最後解釋下為什麼要分別分配到兩個方法中,因為如果依賴檢視完全摺疊了,子檢視又可以向下滾動,這時我們就不能決定是讓依賴檢視位移還是子檢視滾動了,只有讓子檢視向下滾動到頭才能保證唯一性

@Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
                                    float velocityX, float velocityY) {
        return onUserStopDragging(velocityY);
    }

使用者鬆開手指並且會發生慣性滾動之前呼叫,在這個方法內我們可以實現快速上滑或者快速下滑的操作

    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
        if (!isScrolling) {
            onUserStopDragging(800);
        }
    }

使用者鬆開手指如果不發生慣性滾動,就會執行該方法,這裡我們可以用來實現黏性滑動的效果

private boolean onUserStopDragging(float velocity) {
        View dependentView = getDependencyView();
        float translateY = dependentView.getTranslationY();
        float minHeaderTranslate = -(dependentView.getY() + getToolbarSpreadHeight());
        if (translateY == 0 || translateY == -getToolbarSpreadHeight()) {
            return false;
        }
        boolean targetState; // Flag indicates whether to expand the content.
        if (Math.abs(velocity) <= 800) {
            if (Math.abs(translateY) < Math.abs(translateY - minHeaderTranslate)) {
                targetState = false;
            } else {
                targetState = true;
            }
            velocity = 800; // Limit velocity's minimum value.
        } else {
            if (velocity > 0) {
                targetState = true;
            } else {
                targetState = false;
            }
        }

        float targetTranslateY = targetState ? minHeaderTranslate : -dependentView.getY();
        mOverScroller.startScroll(0, (int) translateY, 0, (int) (targetTranslateY), (int) (1000000 / Math.abs(velocity)));
        mHandler.post(flingRunnable);
        isScrolling = true;

        return true;
    }


    private Runnable flingRunnable = new Runnable() {
        @Override
        public void run() {
            if (mOverScroller.computeScrollOffset()) {
                getDependencyView().setTranslationY(mOverScroller.getCurrY());
                mHandler.post(this);
            } else {
                isScrolling = false;
            }
        }
    };

實現黏性滑動的程式碼,如果提供了速度的話使用速度來滑動,否則使用預設速度來滑動,在計算出需要滑動的剩餘距離後,通過Scroller 配合 Handler 來實現該效果