自定义Behavior之ToolBar上滑TabLayout颜色渐变

来源:互联网 发布:游戏编程图书 编辑:程序博客网 时间:2024/05/21 09:39

本篇文章介绍使用CoordinatorLayout的自定义Behavior来实现如下的效果

这里写图片描述

分析本例效果

首先我们来分析下整个例子需要实现哪些效果:

  • ToolBar的上滑和下滑
  • TabLayout跟随ToolBar上移和下移
  • TabLayout颜色会跟随距离的变化发生渐变
  • 滑动时会有黏性效果
    • 滑动距离超过中间值后放开会自动滑向想要的方向
    • 滑动距离未超过中间值放开则会自动回弹

本例需要的几个重要方法介绍

我们的例子中重写了Behavior的几个重要方法:

  • layoutDependsOn
  • onDependentViewChanged
  • onLayoutChild
  • onStartNestedScroll
  • onNestedPreScroll
  • onNestedScroll
  • onStopNestedScroll
  • onNestedScrollAccepted
  • onNestedPreFling
  • onStartNestedScroll
    这些方法具体的说明可以参考:CoordinatorLayout自定义Behavior的简单总结

自定义 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 来实现该效果

代码示例:
MaterialDesignFeatures

参考:
http://www.jianshu.com/p/7f50faa65622

0 0