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

本篇文章介绍使用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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值