在上一篇文章中,为了实现手指上滑时, FAB下滑隐藏, 手指下滑时, FAB上滑显示, 我们对FloatingActionButton.Behavior进行了扩展, 都是新建一个类, 再继承FloatingActionButton.Behavior, 然后再补充代码的形式进行的.
如果手指滑动时, 隐藏显示的不是FAB, 而是一个底部布局呢? 那是不是要另外再写一个Behavior ?
其实是不用这么麻烦的, FloatingActionButton.Behavior是FAB专用的Behavior, 如果这个Behavior不是为它专门定制的, 完全可以指定泛型
CoordinatorLayout.Behavior<View>
我们更改一下继承关系, 继承改为CoordinatorLayout.Behavior<View>, 同时, 将内部参数FloatingActionButton child改为View child. 另外, 为避免让人误以为这个Behavior只针对FloatingActionButton, 将名称也改成了SlideBehaviorForBottomLayout.
public class SlideBehaviorForBottomLayout extends CoordinatorLayout.Behavior<View> {
int offsetY = 0;
AnimatorUtils animatorUtils;
public SlideBehaviorForBottomLayout(Context context, AttributeSet attrs) {
......
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child,@NonNull View directTargetChild,
@NonNull View target, int axes, int type) {
//如果NSChild传过来的axes是竖直方向SCROLL_AXIS_VERTICAL则返回true
if (axes == ViewCompat.SCROLL_AXIS_VERTICAL) {
// 获取控件左上角到父控件底部的偏移量
offsetY = coordinatorLayout.getMeasuredHeight() - child.getTop();
return true;
}
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
}
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child, @NonNull View target,
int dx, int dy, @NonNull int[] consumed, int type) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
// dy > 0 ,表示手指上滑,页面上拉, 查看更多内容,child要下滑隐藏
if (dy > 0) {
animatorUtils.startHindAnimator(child, offsetY);
return;
}
// dy < 0 ,表示手指下滑, 页面下拉, 查看前面的内容, child上滑显示
if (dy < 0) {
animatorUtils.startShowAnimator(child, 0);
return;
}
}
......
}
效果和之前一模一样.
既然如此, 那我们将FAB按钮改为一个底部布局BottomLayout来看看.
layout_bottom.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:layout_gravity="bottom"
android:background="?attr/colorPrimary"
app:layout_behavior=".SlideBehaviorForBottomLayout">
<ImageView
android:id="@+id/left_img_comment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:src="@drawable/ic_comment_white_24dp" />
<ImageView
android:id="@+id/btn_submit_in_btm_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_done_white_24dp"
/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:maxLines="1"
android:lines="1"
android:textSize="14sp"
android:background="@drawable/searchview_edit_bg"
android:layout_toLeftOf="@id/btn_submit_in_btm_layout"
android:layout_toRightOf="@id/left_img_comment" />
</RelativeLayout>
注意: 直接在RelativiLayout上申明了Behavior.
app:layout_behavior=".SlideBehaviorForBottomLayout"
在布局中将该底部布局引入. 同时将原有FAB的Behavior取消, 并将锚点挂接在Appbar上.
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout ...>
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar" ...>
<android.support.design.widget.CollapsingToolbarLayout ...>
<ImageView ... />
<android.support.v7.widget.Toolbar... />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
...
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<TextView... />
</android.support.v4.widget.NestedScrollView>
<include layout="@layout/layout_bottom" />
<android.support.design.widget.FloatingActionButton
...
app:layout_anchor="@id/app_bar"
app:layout_anchorGravity="bottom|end"/>
</android.support.design.widget.CoordinatorLayout>
我们一个代码没写, 只是把原有Behavior赋予给了底部布局layout_bottom, 来看看下滑隐藏上滑显示是否正常.
就是这么魔性.
滚动方向
先看下onNestedPreScroll方法. 上面代码中, 在onNestedPreScroll对滚动方向进行了简单描述
dy > 0 ,表示手指上滑,页面上拉, 查看更多内容,child要下滑隐藏
dy < 0 ,表示手指下滑, 页面下拉, 查看前面的内容, child上滑显示
我们在方法里加一个log来看看是不是.
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child, @NonNull View target, int dx, int dy,
@NonNull int[] consumed, int type) {
Log.i(TAG, "onNestedPreScroll: dy = " + dy);
...
}
来看下log
可以看到, 手指稍微上滑一点, 就得到多个正值的dy, 手指下滑, 得到多个负值的dy.
感觉Google为了顺滑性考虑, 将手指滑动的行程做了分解, 如100px的上滑行程, 可能被分成了若干段, 根据滑动速度, 每段可能2px, 也可能20px, 然后加起来就是这个行程的长度. 我们改下代码来看log. 代码是将上滑行程和下滑行程分开计算.
static float distUP = 0;
static float distDown = 0;
public void onNestedPreScroll(...) {
if(dy > 0 ){
distDown = 0;
distUP += dy;
Log.i(TAG, "onNestedPreScroll: distUP = " + distUP);
}
if(dy < 0 ){
distUP = 0;
distDown += dy;
Log.i(TAG, "onNestedPreScroll: distDown = " + distDown);
}
...
}
好像印证了我的猜想哦. 同理, dx只不过是x方向的数值而已.
再来看另外一个方法onNestedScroll, 这就不一一试试了, 直接从代码和动图中看结论.
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child, @NonNull View target,
int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
//1-dyConsumed > 0 && dyUnconsumed = 0 手指上滑,页面往上拉
//2-dyConsumed = 0 && dyUnconsumed > 0 页面往上拉到顶了, 手指还在往上滑
//3-dyConsumed < 0 && dyUnconsumed = 0 手指下滑,页面往下拉
//4-dyConsumed = 0 && dyUnconsumed < 0 页面往下拉到底了, 手指还在往下滑
if (dyConsumed > 0 && dyUnconsumed == 0) {
Log.i(TAG, "上拉");
} else if (dyConsumed == 0 && dyUnconsumed > 0) {
Log.i(TAG, "上拉到顶了, 还拉啥啊");
} else if (dyConsumed < 0 && dyUnconsumed == 0) {
Log.i(TAG, "下拉");
} else if (dyConsumed == 0 && dyUnconsumed < 0) {
Log.i(TAG, "下拉到底了, 还拉啥啊");
}
...
}
什么是消费滚动数值
在上一篇记录中, 我对onNestedPreScroll等系列方法进行了简单描述. 刚开始接触"消费滚动数值"的时候, 感觉一头雾水, 就不能说人话吗? 我来写个简单的例子做个说明.
先看下效果
被隐藏的View暂时用一个ImageView来替代, 可以替换成复杂的ViewPager或其它Layout等. 我们来完成这个例子,
1-布局
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout ...>
<ImageView
android:id="@+id/img_advertisement"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop"
app:layout_behavior=".MyADBehavior"
android:src="@drawable/lunbo08" />
<android.support.v4.widget.NestedScrollView
android:id="@+id/dy_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:lineSpacingMultiplier="2"
android:text="@string/large_text"
android:textSize="16sp" />
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
关键是给ImageView赋予了一个Behavior
app:layout_behavior=".MyADBehavior"
来看下这个Behavior. 我们先只给一个构造方法看看.
public class MyADBehavior extends CoordinatorLayout.Behavior {
public MyADBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
看效果图, 上部的图片不见了.
这是因为CoordinatorLayout本质上是个FrameLayout, ImageView被后来放置的NestedScrollView盖住了. 所以需要在代码中将NestedScrollView往下移动一定距离. 幸好Google给我们留了一个方法:
2-onLayoutChild
public class MyADBehavior extends CoordinatorLayout.Behavior {
private static final String TAG = "kkk";
private ImageView adImg;
private float imgHeight;
public MyADBehavior(Context context, AttributeSet attrs) {...}
//初始化界面时, 让NestedScrollView置于的ImageView下方
@Override
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
adImg = parent.findViewById(R.id.img_advertisement);
imgHeight = adImg.getMeasuredHeight();
// 注意:target其实就是NestedScrollView,放置的起始位置(0,0)
View target = parent.findViewById(R.id.dy_scroll_view);
// 将target移动到ImageView的下方
target.setTranslationY(imgHeight);
return super.onLayoutChild(parent, child, layoutDirection);
}
}
来看下效果
可以看到, 上部图片和它之间没有发生什么关系, NestedScrollView自己滑自己的.
3-onStartNestedScroll
ImageView对NestedScrollView说, 我开始关注你的纵向滑动了. 但此时两者还是相对独立的, ImageView能做的仅仅是观察到NestedScrollView的滑动而已.
//根据target和nestedScrollAxes决定此view是否要与target配合进行嵌套滚动,
//并返回true(要与target配合进行嵌套滚动)或false(不与target配合进行嵌套滚动)。
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull View child, @NonNull View directTargetChild,
@NonNull View target, int axes, int type) {
return axes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
4-onNestedPreScroll
当 NestedScrollView即将被滑动时调用. 在这里你可以为CoordinatorLayout下的子View进行一些设置, 如将target进行平移, 或者给child设置透明度:
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View target, int dx, int dy,
@NonNull int[] consumed, int type) {
float translationY = target.getTranslationY();
float offsetY ;
//还未平移到顶的情况下
if (translationY > 0) {
offsetY = translationY - dy;
//手指上滑, dy>0, offset减小, target向上平移,
//如果手指上滑过快, dy数值会过大, 造成offset<0,需要控制其平移到顶即可
if(offsetY < 0){
offsetY = 0;
}
//手指下滑, dy<0, offset加大, target向下平移,
//如果手指下滑过快, dy数值会过大, 造成offset>imgHeight, 需要控制其平移到底即可
if (offsetY > imgHeight) {
offsetY = imgHeight;
}
//target平移
target.setTranslationY(offsetY);
//child透明度变化
float alpha = (float) (0.5 + 0.5 * offsetY / imgHeight);
child.setAlpha(alpha);
}
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
可以看下效果.
- NestedScrollView往上平移逐渐盖住ImageView, 往下平移又逐渐显现ImageView
- 随着NestedScrollView的平移, ImageView的透明度逐渐变化
细心的朋友可能观察到了, 随着NestedScrollView往上平移, 里面的Text文字也在往上或往下滚动.
不是说好的只让平移, 不让滚动的吗, 这是因为没有设置消费滚动数值.
//还未平移到顶的情况下
if (translationY > 0) {
offsetY = translationY - dy;
...
target.setTranslationY(offsetY);
...
child.setAlpha(alpha);
consumed[1] = dy;
}
好, 来看下完全消费掉滚动数据的情况.
可以看到, 随着平移, 文字并没有滚动. 就是说, 确实NestedScrollView的滚动数据被消费掉了.
上面代码是一次性全部消费掉滚动数据, 其实还可以按比例消费, 如将消费比例改为如下:
consumed[1] = (int) (0.5* dy);
NestedScrollView在平移的同时, 还是接收到了大约一半的滚动数据, 安排内部文本做相应的滚动.
还有更细心的朋友可能再次发现了, 我没有一次让NestedScrollView移动到顶部, 那么来试一下.
好吧, 到顶就下不来了. 这是因为代码中只针对"translationY > 0" 做了预设置, 而平移到最顶端时"translationY == 0" , 那么改下代码看看.
if (translationY >= 0) {
offsetY = translationY - dy;
...
target.setTranslationY(offsetY);
...
child.setAlpha(alpha);
consumed[1] = dy;
}
好了, 可以拉下来了. 但在最顶端的情况下, 还往上推, 怎么推也推不上去. 这是因为此时已经出了onNestedPreScroll方法管理的范畴. 需要用到onNestedScroll来处理越界后的滚动.
6-onNestedScroll
上面onNestedPreScroll方法, 将代码回到更改前:
//还未平移到顶的情况下
if (translationY > 0) {
offsetY = translationY - dy;
...
target.setTranslationY(offsetY);
...
child.setAlpha(alpha);
consumed[1] = dy;
}
再看一下效果
可以看到, 往上推还是能让文字滚动的, 往下拉也能让文字滚动, 只是滚动到底后不能将整个NestedScrollView平移下来.
面篇幅中我们知道, 在onNestedScroll方法中:
- dyConsumed > 0 && dyUnconsumed = 0 手指上滑,页面往上拉
- dyConsumed = 0 && dyUnconsumed > 0 页面往上拉到顶了, 手指还在往上滑
- dyConsumed < 0 && dyUnconsumed = 0 手指下滑,页面往下拉
- dyConsumed = 0 && dyUnconsumed < 0 页面往下拉到底了, 手指还在往下滑
要想让NestedScrollView往下平移, 需要在滚动到底后对dyUnconsumed进行判断,
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
float translationY = target.getTranslationY();
float offsetY = translationY - dyUnconsumed;
if (dyUnconsumed < 0) {
target.setTranslationY(offsetY);
}
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, type);
}
其实可以看到, 只要越过顶部界限, 滚动处理又交回到了onNestedPreScroll()方法来处理了, 可以看到ImageView的透明度在变化.
7-补充
对NestedScrol机制做个小结.
- onNestedPreScroll: 当子View在处理滑动事件之前, 先告诉自己的父View是否需要先处理这次滑动事件, 父View通过onNestedPreScroll接收该事件, 父View安排处理这个滑动事件, 处理完后, 通过consumed[?]= ?? 告诉子View它处理了多少滑动距离. 剩下的滑动事件交给子View自己来处理
- onNestedScroll: 子View处理完剩下的滑动事件后, 告诉父View, 我处理了dx/dyConsumed这么多滑动距离, 父View你要不要也处理一下? 父View通过onNestedScroll接收到该滑动事件, 安排处理(或不处理)这个滑动事件. 父View在这个方法里还监听越界行为.
是不是找到了一点自定义Behavior的感觉了呢? 后面我们继续深入, 来做一些复杂的联动设计.