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