NestedScrolling机制之CoordinatorLay
在上一讲中我们讲了NestedScrolling机制,其实android很多有些常用的控件都是支持NestedScrolling机制的,如RecyclerView,NestedScrollView等,
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2{}
public class NestedScrollView extends FrameLayout implements NestedScrollingParent2,NestedScrollingChild2, ScrollingView{}
这些控件内部用的就是我们上一讲的东西,通过上一讲的内容其实我们已经可以实现很复杂的ui效果了,那个这一讲讲什么呢,就是CoordinatorLayout,CoordinatorLayout.Behavior这个相当于NestedScrolling机制的运用和封装。
简单来说CoordinatorLayout像一个容易,包含所有子View,协调其子View之间的动作的一个父View,而Behavior是用来给CoordinatorLayout里的子View实现交互的。
单单说概念可能大家都理解不深,接下来就讲我写的类似美团外卖骨架的demo吧。
看效果图先吧:
这种效果假如不用CoordinatorLayout其实还是有点难麻烦的,不过有了CoordinatorLayout就简单了,首先我们看一下布局文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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">
<com.jack.meituangoodsdetails.view.GoodDetailsView
android:id="@+id/goods_details_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.jack.meituangoodsdetails.view.GoodDetailsView>
<com.jack.meituangoodsdetails.view.GoodsListView
android:id="@+id/goods_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/goods_list_behavior">
</com.jack.meituangoodsdetails.view.GoodsListView>
<com.jack.meituangoodsdetails.view.GoodsTitleView
android:id="@+id/goods_title_view"
android:layout_width="match_parent"
android:layout_height="50dp">
</com.jack.meituangoodsdetails.view.GoodsTitleView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
从上面的布局文件可以看出,CoordinatorLayout包含着3个自定义的Viewr然后就没了,其中GoodDetailsView是图片和下面商品详情的View,GoodsTitleView如其名字那样是界面的头部的View,
GoodsListView就是给我们滑动的View了。在这布局里,我们看到一个比较特殊的东西app:layout_behavior="@string/goods_list_behavior",这是什么呢?
其实这是CoordinatorLayout父View绑定一个叫goods_list_behavior的子View,有个这个就完成了父View和子View的关联,那么goods_list_behavior又指向那个类呢?看字符串资源文件
<string name="goods_list_behavior">com.jack.meituangoodsdetails.hehavior.GoodsListBehavior</string>
可以是指向一个叫GoodsListBehavior的类,这也是这个UI交互的核心,所有的UI交互都在这个类完成,代码如下:
public class GoodsListBehavior extends CoordinatorLayout.Behavior<GoodsListView> {
private CoordinatorLayout parentView;
private GoodDetailsView detailsView;
private GoodsTitleView titleView;
private GoodsListView goodView;
private Context context;
private Scroller scroller;
private int duration=1000;
private Handler handler;
private int pagingTouchSlop;
private int verticalPagingTouch;
//商品界面的中心
int centerGoodView;
//商品界面离顶部的间隔
int goodViewTop;
public GoodsListBehavior(Context context, AttributeSet attrs){
super(context,attrs);
this.context=context;
this.pagingTouchSlop=DensityUtils.dp2px(context,5);
this.scroller=new Scroller(context);
this.handler=new Handler();
}
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, @NonNull View dependency) {
this.goodView=child;
this.parentView=parent;
if(dependency instanceof GoodsTitleView){
titleView=(GoodsTitleView) dependency;
return true;
}
if(dependency instanceof GoodDetailsView){
detailsView=(GoodDetailsView) dependency;
detailsView.expandBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startScroll((int)goodView.getTranslationY(),goodViewTop-parentView.getHeight());
}
});
return true;
}
return false;
}
@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, int layoutDirection) {
CoordinatorLayout.LayoutParams layoutParams=(CoordinatorLayout.LayoutParams)child.getLayoutParams();
if(layoutParams.height==CoordinatorLayout.LayoutParams.MATCH_PARENT){
layoutParams.height=parent.getHeight()-titleView.getHeight();
child.setLayoutParams(layoutParams);
goodViewTop=titleView.getHeight()+ DensityUtils.dp2px(context,160);
child.setTranslationY(goodViewTop);
return true;
}
return super.onLayoutChild(parent, child, layoutDirection);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull GoodsListView child,
@NonNull View directTargetChild,
@NonNull View target, int axes, int type) {
handler.removeCallbacks(flingRunnable);
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL)!=0;
}
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child,
@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
//防止左右误滑
verticalPagingTouch+=dy;
if(goodView.viewPager.isScrollable()&&Math.abs(verticalPagingTouch)>pagingTouchSlop){
goodView.viewPager.setScrollable(false);
}
if(dy>0){
//向上滑
if(child.getTranslationY()<=titleView.getHeight()){
child.setTranslationY(titleView.getHeight());
}else{
child.setTranslationY(child.getTranslationY()-dy);
consumed[1]=dy;
}
}else{
//向下滑
if(((GoodsListFragment) child.getFragment().get(child.viewPager.getCurrentItem())).isScrollAble()){
child.setTranslationY(child.getTranslationY()-dy);
}
}
if(child.getTranslationY()>=goodViewTop){
detailsView.updateView(dy);
titleView.checkView();
} else{
titleView.updateView(dy);
}
}
@Override
public void onStopNestedScroll(@NonNull CoordinatorLayout parent,
@NonNull GoodsListView child,
@NonNull View target, int type) {
verticalPagingTouch = 0;
goodView.viewPager.setScrollable(true);
centerGoodView=(parent.getHeight()+goodViewTop)/2;
if(child.getTranslationY()>goodViewTop&&child.getTranslationY()<centerGoodView){
//恢复
startScroll((int)child.getTranslationY(),(int)(goodViewTop-child.getTranslationY()));
}else if(child.getTranslationY()>centerGoodView){
//隐藏
startScroll((int)child.getTranslationY(),(int)(parent.getHeight()-child.getTranslationY()));
}
}
@Override
public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child, @NonNull View target, float velocityX, float velocityY, boolean consumed) {
if(velocityY<0){
//向下
startScroll((int)child.getTranslationY(),(int)(coordinatorLayout.getHeight()-child.getTranslationY()));
}else{
//向上
if(goodView.getTranslationY()<goodViewTop){
startScroll((int)child.getTranslationY(),(int)(titleView.getHeight()-child.getTranslationY()));
}else{
startScroll((int)child.getTranslationY(),(int)(goodViewTop-child.getTranslationY()));
}
}
return true;
}
public void startScroll(int startY,int dy){
scroller.startScroll(0,startY,0,dy,duration);
this.handler.post(flingRunnable);
}
Runnable flingRunnable=new Runnable() {
@Override
public void run() {
if(scroller.computeScrollOffset()){
goodView.setTranslationY(scroller.getCurrY());
if(goodView.getTranslationY()>=goodViewTop){
detailsView.updateView(scroller.getStartY()-scroller.getFinalY());
}else{
titleView.updateView(scroller.getStartY()-scroller.getFinalY());
}
handler.post(flingRunnable);
}
}
};
}
看上去代码还是有点多,首先要形成与父View的关联GoodsListBehavior必须继承GoodsListBehavior,这样子View一滑动才可以回调相应的NestedScrolling机制的一些方法,在这里我们看几个方法:
/**
* 开始滑动的时候调用一次,手松开的时候调用一次
* 返回true代表获取滑动事件,其他的scroll事件就会被触发
* coordinatorLayout
* child 使用此Behavior的View
* directTargetChild 是target或是target的parent
* target 处理滑动事件的view
* axes 垂直滚动2 横向滚动1
* type 滑动类型touch 0手指按下 1手指松开
*/
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull GoodsListView child,
@NonNull View directTargetChild,
@NonNull View target, int axes, int type);
/**
* 页面滑动的时候调用
* coordinatorLayout 同上
* child 同上
* target 同上
* dxConsumed 水平滑动的实时距离
* dyConsumed 竖直滑动的实时距离
* dxUnconsumed view处于滚动状态,但是并不是由target消耗的滚动时候触发,这个是水平滚动的实时距离
* dyUnconsumed view处于滚动状态,但是并不是由target消耗的滚动时候触发,这个是竖直滚动的实时距离
* type 同上
*/
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child,@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type);
//手指松开时,调用一次,滑动停止时调用一次
public void onStopNestedScroll(@NonNull CoordinatorLayout parent,
@NonNull GoodsListView child,
@NonNull View target, int type);
/**
* 滑动时手指松开如果还继续滑动的时候调用一次
* coordinatorLayout 同上
* child 同上
* target 同上
* velocityX 水平加速度
* velocityY 竖直加速度
* consumed 同上 false不拦截 true则不会有惯性滑动,需要自己处理
*/
public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull GoodsListView child, @NonNull View target, float velocityX, float velocityY, boolean consumed);
是不是和我们上一讲中的NestedScrollingParent回调方法很像,其实说白了CoordinatorLayout内部还是用NestedScrolling机制实现的。因为这个方法比较常用,所以我就讲这几个方法,m没出现的暂时不讲。除了上面几个,还有如下:
/**
* 指定依赖的View,在这里指定依赖的View之后,
* @param parent
* @param child 使用该Behavior的View
* @param dependency 依赖的View
* @return 当指定的View是我们需要的View时,返回true
*/
boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, @NonNull View dependency);
确定使用Behavior的View要依赖的View的类型,在这里,我做的最多的是初始化各个View,如GoodDetailsView,GoodsTitleView,GoodsListView,CoordinatorLayout分别对应detailsView,titleView,goodView,parentView。
/**
* CoordinatorLayout绘制child的时候调用
* parent 同上
* child 同上
* CoordinatorLayout布局解析的方法 0=ltr 1=rtl,因为有些国家是从左向右显示的
**/
boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull GoodsListView child, int layoutDirection);
确定使用Behavior的View位置,这一步确定各个子View的初始位置,具体无非通过计算得到各个View的位置再移动,代码很简单已给。
onStartNestedScroll():当(axes & ViewCompat.SCROLL_AXIS_VERTICAL)!=0既表示竖直滑动嵌套滑动就开始了,最主要的作用就是确定滑动的方向。
onNestedPreScroll():当我们滑动时候就会不断的调用这个方法,这也是我们实现各种效果的关键,我在这里做的最主要的就是各种滑动动画效果的实现,而效果无非就是放大,缩小,透明度,View的移动等。
onStopNestedScroll():看名字就知道了,当停止滑动时调用的方法,主要是执行当滑到一般停止时要怎么恢复还是隐藏商品列表的判断
onNestedFling(): 当手指快速一划时所触发的方法,在代码中结合着Scroller,onNestedFling赋一个结束值给Scroller,Scroller会不断产生中间值直到结束为止。而我们拿到这些中间中间值进行动画处理。
这个就是各个方法的功能和职责,也是整个整个功能的骨架,共同支撑了整个交互的执行,而具体的细节请看源码。