本篇文章介绍使用CoordinatorLayout的自定义Behavior来实现如下的效果
本例效果和上篇文章的效果类似, 因此建议先阅读上篇文章
自定义Behavior之ToolBar上滑TabLayout颜色渐变
首先我们来分析下整个例子需要实现哪些效果:
NestedScrollView上滑和下滑时覆盖背景
ImageView跟随NestedScrollView放大缩小与位移
滑动时会有黏性效果
滑动距离超过中间值后放开会自动滑向想要的方向
滑动距离未超过中间值放开则会自动回弹
我们的例子中重写了Behavior的几个重要方法:
这些方法具体的说明可以参考:CoordinatorLayout自定义Behavior的简单总结
本例使用了两个自定义behavior, NestedScrollView不设依赖, 仅改变自己的位置, imageView将NestedScrollView作为依赖视图,通过获取NestedScrollView位置的改变, 计算出一个百分比值, 利用这个百分比值来调整imageView的位置以及大小
首先继承Behavior, 在构造函数内做初始化工作
public class ContentScrollBehavior extends CoordinatorLayout.Behavior<View> {
private static final String TAG = ContentScrollBehavior.class.getSimpleName();
private WeakReference mChildView;
private OverScroller mOverScroller;
private Handler mHandler;
// 依赖视图起始y坐标
private float mDependencyOriginalY;
// 依赖视图最终y坐标
private float mDependencyFinalY;
private boolean isScrolling = false;
public ContentScrollBehavior(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
mOverScroller = new OverScroller(context);
mHandler = new Handler();
mDependencyOriginalY = context.getResources().getDimensionPixelOffset(R.dimen.content_height);
mDependencyFinalY = context.getResources().getDimensionPixelOffset(R.dimen.content_offset);
}
.....
}
mDependencyOriginalY和mDependencyFinalY是NestedScrollView的初始Y位置和最终Y位置
@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());
child.setTranslationY(mDependencyOriginalY);
mChildView = new WeakReference<>(child);
return true;
}
return super.onLayoutChild(parent, child, layoutDirection);
}
在onLayoutChild里设置NestedScrollView的初始位置
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
onStartNestedScroll在用户按下手指的时候回调,该方法在返回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);
}
onNestedScrollAccepted方法里我们可以做一些准备工作,比如让之前的滑动动画结束
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View get, int dx, int dy, int[] consumed) {
//上滑操作dy值大于0,因此小于0直接分发给onNestedScroll
if (dy <0) {
return;
}
//上滑后的位置不小于mDependencyFinalY(100dp)
float transY = child.getTranslationY() - dy;
if (transY > 0 && transY >= mDependencyFinalY) {
child.setTranslationY(transY);
consumed[1] = dy;
}
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
//上滑到底部并越界后dyUnconsumed值大于0直接忽略
if (dyUnconsumed > 0) {
return;
}
//下滑后的位置不大于mDependencyOriginalY(250dp)
float transY = child.getTranslationY() - dyUnconsumed;
if (transY > 0 && transY <= mDependencyOriginalY) {
child.setTranslationY(transY);
}
}
由于onNestedPreScroll方法会优先于onNestedScroll之前调用,因此我们可以将上滑动作分配到onNestedPreScroll,下滑动作分配到onNestedScroll,我们来分析下这样实现的原理:
最后解释下为什么要分别分配到两个方法中,因为子视图是NestedScrollView, 内部还包含了一个子视图(textview), 如果NestedScrollView还没消费完移动事件,而NestedScrollView内的子视图又可以向下滚动,这时我们就不能决定是让NestedScrollView位移还是NestedScrollView内的子视图滚动了,只有让NestedScrollView先消费完移动事件才能保证唯一性
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
return onUserStopDragging(velocityY, child);
}
用户松开手指并且会发生惯性滚动之前调用,在这个方法内我们可以实现快速上滑或者快速下滑的操作
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
if (!isScrolling) {
onUserStopDragging(800, child);
}
}
用户松开手指如果不发生惯性滚动,就会执行该方法,这里我们可以用来实现黏性滑动的效果
private boolean onUserStopDragging(float velocity, View child) {
float translateY = child.getTranslationY();
float minHeaderTranslate = mDependencyFinalY;
float maxHeaderTranslate = mDependencyOriginalY;
float midHeaderTranslate = maxHeaderTranslate - minHeaderTranslate;
//中间态对比参数
float y = translateY - maxHeaderTranslate;
float ym = y + midHeaderTranslate;
if (translateY == mDependencyFinalY || translateY == mDependencyOriginalY) {
return false;
}
//在这里计算有没有超过中间态
boolean targetState; // Flag indicates whether to expand the content.
if (Math.abs(velocity) <= 800) {
//y范围(-450~0) ym范围(0~450),取绝对值比大小来判断滑动距离有没有超过1/2
targetState = Math.abs(y) >= Math.abs(ym);
velocity = 800; // Limit velocity's minimum value.
} else {
targetState = velocity > 0;
}
//根据targetState判断,超过中间态自动滑动剩余距离,没有则回到原处
float targetTranslateY = targetState ? minHeaderTranslate : maxHeaderTranslate;
//根据targetTranslateY的值来减去translateY来计算dy
mOverScroller.startScroll(0, (int) translateY, 0, (int) (targetTranslateY - translateY), (int) (1000000 / Math.abs(velocity)));
mHandler.post(flingRunnable);
isScrolling = true;
return true;
}
private Runnable flingRunnable = new Runnable() {
@Override
public void run() {
if (mOverScroller.computeScrollOffset()) {
getChildView().setTranslationY(mOverScroller.getCurrY());
mHandler.post(this);
} else {
isScrolling = false;
}
}
};
实现黏性滑动的代码, 如果提供了速度的话使用速度来滑动, 否则使用默认速度来滑动, 如果手指滑动如果超过中间值的话滑动到目标位置, 否则滑回原来的位置, 在计算出需要滑动的剩余距离后, 通过Scroller 配合 Handler 来实现该效果
最后贴一下xml布局
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/scrolling_header"
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="@mipmap/banner3"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="350dp"
android:paddingBottom="60dp"
android:text="想告别起床气,快去定个闹铃吧!"
android:textColor="@android:color/white"
app:layout_anchor="@id/scrollView"
app:layout_anchorGravity="top|center_horizontal"
/>
<android.support.v4.widget.NestedScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
app:layout_behavior="@string/content_behavior">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/large_text"/>
android.support.v4.widget.NestedScrollView>
<ImageView
android:id="@+id/add_alarm_clock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/add_alarm_clock_button_pressed"
app:layout_anchor="@id/scrolling_header"
app:layout_anchorGravity="center"
app:layout_behavior="@string/floating_image_behavoir"
/>
android.support.design.widget.CoordinatorLayout>
代码示例:
MaterialDesignFeatures
参考:
http://www.jianshu.com/p/7f50faa65622
http://www.jianshu.com/p/82d18b0d18f4
1