目标
- 2个recycleview同时放在一个页面内,可以完成平滑滚动
- 顶部recycleview有固定高度,为gridLayout布局
- 底部recycleview占用剩余高度,为LinearLayoutManager布局
目前没有直接可用的布局可以完成我们的需求,我们基于CoordinatorLayout做一些简单的定制来完成我们的需求,如果想完成定制,那么需要我们理解嵌入式滑动的原理,下面我们会从三个方面来进行讲解
- 绘制原理
- 事件分发机制
- 嵌入式滑动处理
最终效果图
android的绘制原理
- 跟绘制相关的三个核心方法
- onMeasure
- onLayout
- onDraw
onMeasure
当计划在界面绘制一个View时,我们需要知道,视图的大小,onMeasure会提供给我们一个机会来决定我们绘制view的大小,我们可以直接设定这个大小,也可以设置一个依赖值,由父类根据父类的大小来动态决定子空间大小,我们一旦自己设置了固定的大小,那么需要在这里调用setMeasuredDimension方法,明确告诉父容器我们的设置
依赖值
- ViewGroup.MATCH_PARENT
- ViewGroup.WRAP_CONTENT
MATCH_PARENT 表示,我们需要父容器有多大,我们尽可能占据多大
WRAP_CONTENT 表示,只要能够显示出我们的内容,就可以了。其他位置由父容器另外安排
getMeasureHeight 和getHeight的区别
- getMeasureHeight是计算出来的高度
- getHeight是最后绘制的高度
- 有可能不同,因为后面还有动画,或者直接设置来改变
- 在onMeasure后。getMeasureHeight是有值的
- 在onDraw后,getHeight才有值
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
onLayout
上面我们一旦知道我们的大小,那么就需要确定我们的位置,在这个回调中,父容器提供一个机会给我们来确定自己容器的位置,我们可以根据父容器提供的上下左右来确定我们的位置,也可以自己设置我们理想中的上下左右位置。
座标系
- 原点左上角
- 宽是x轴
- 高是y轴
- 视图的位置由左上角的点(x1,y1)和右下角的点(x2,y2)的位置来决定
- 上 y1
- 左 x1
- 右 x2
- 下 y2
- 高度 y2-y1
- 宽度 x2-x1
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
onDraw
这个方法调用时,就是根据我们前面通过onMeasure和onLayout完成的对视图大小和位置的计算来完成最终的绘制。我们可以在这块来决定绘制的颜色,也可以修改我们绘制的大小和位置。
Canvas
- Canvas是无限大的
- 屏幕只是画布的可见区域
- 我们可以绘制在屏幕外部
- 如果需要看到屏幕外部的内容,我们需要滑动屏幕来完成,不过为了优化。我们常常是在屏幕外不会去做绘制的
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
事件分发机制
- 事件分发的三个方法
- dispatchTouchEvent
- onInterceptTouchEvent
- onTouchEvent
dispatchTouchEvent
- View
- ViewGroup
- Activity
这个方法是事件分发的入口,所有的方法都从这个入口进入,然后向子视图或者自己的其他方法传递,在这个方法内的拦截会直接影响性能和后面的回调处理
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
onInterceptTouchEvent
- ViewGroup
- Activity
这个方法只存在可以添加子视图的容器类中,因为这个方法主要是做拦截处理的。如果方法返回true,那么就开始拦截,会把事件转到自己的onTouchEvent中,
而不会向子视图传递,如果返回false,那么不会拦截,会继续传递和处理
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
onTouchEvent
- ViewGroup
- View
- Activity
这个方法是事件的处理方法,可以在这里写具体的处理逻辑。返回true说明自己会处理,返回false,说明自己不会处理
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
嵌入式滑动的定制处理
上面的绘制和事件分发逻辑都比较简洁,清晰,随着业务发展,可能需要更多更负责的页面,比如在一个页面滑动时,需要修改其他视图的布局或者滑动。那么Android提供给了我们CoordernateLayout布局来完成,同时提供了ViewBehavior来自定义嵌入式滑动的布局和滑动
- ViewBehavior
- NestedScrollingParent
- NestedScrollingChild
目前很多android默认的视图已经实现了NestedScrollingParent和NestedScrollingChild接口来完成了嵌入式滑动的处理,我们可以直接使用,不过如果使用效果无法满足我们的需求,还是需要通过ViewBehavior来定制我们处理。
如果我们用到的视图不支持嵌入式滑动,我们需要自己来实现NestedScrollingParent和NestedScrollingChild接口来完成嵌入式滑动。
原理
容器A支持NestedScrollingParent, 增加了支持NestedScrollingChild接口的视图B和视图C,那么在B滑动时,如何影响视图C的布局和滑动呢
以前的滑动处理
- 如果滑动发生在B,那么事件分发由A开始
- 如果A要拦截,那么A就会处理事件
- 如果A不拦截,那么就交给B来处理自己的事件
嵌入式滑动
- 如果滑动发生在B,同时B支持NestedScrollingChild,事件分发还在是A开始
- 如果容器A内的子视图有包含NestedScrollingChild或者有ViewBehavior。那么就要分发touch事件给这个视图,比如A中的另外一个视图C
- 视图C会根据定制的ViewBehavior来确定是否要响应这个滑动。
需求解决
我们的目标是容器A中有容器B和容器C,先添加B,再添加C,B为顶部视图,C为底部视图,B和C都是RecycleView,他们是支持滑动的,A我们可以使用CoordernateLayout,B和C使用嵌入式滑动来处理事件
我们需要解决几个问题
- 布局问题,需要B和C平铺在A中,默认是覆盖,后面的覆盖前面的
- 滑动问题,在C上滑动时,整体布局上移动,知道B移除屏幕,C开始处理自己的滑动
布局问题
- layoutDependsOn
- onLayoutChild
C在layoutDependsOn回调中设置对B的依赖。那么B绘制完,C会被触发,onLayoutChild回调中我们下移C到B下面。确保B和C按线性排列
@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {
if(isLayout) {
return super.onLayoutChild(parent, child, layoutDirection);
}else {
isLayout = true;
}
int height = parent.getContext().getResources().getDimensionPixelOffset(R.dimen.home_top_container_height);
Log.e(TAG,"main.method:onLayoutChild,id:"+R.id.main_container+",child.id:"+child.getId()+",height:"+height);
child.setTranslationY(height);
return super.onLayoutChild(parent, child, layoutDirection);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
boolean flag = (dependency.getId() == R.id.home_top_container);
Log.e(TAG,"flag:"+flag+",child.id:"+child.getId());
return flag;
}
滑动问题
在B中增加滑动处理,当C中有滑动时,首先父容器会查找是否有当前的其他容器会消费这个事件,如果会消费,会让这个容器来处理事件,直到处理完毕,没有其他容器消费,再交给C来处理。
- onStartNestedScroll 来确定消费的方向
- onNestedPreScroll 会来确定是否消费,消费多少,同时返回消费剩余内容
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
Log.e(TAG,"top.method:onStartNestedScroll,child.id:"+child.getId()+",target.id:"+target.getId());
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
Log.e(TAG,"top.method:onNestedPreScroll,child.id:"+child.getId()+",target.id:"+target.getId());
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
if (target instanceof RecyclerView) {
RecyclerView list = (RecyclerView) target;
// 列表第一个全部可见Item的位置
int pos = ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
if (pos == 0 && pos < lastPosition) {
downReach = true;
}
// 整体可以滑动,否则RecyclerView消费滑动事件
if (canScroll(child, dy) && pos == 0) {
float finalY = child.getTranslationY() - dy;
if (finalY < -child.getHeight()) {
finalY = -child.getHeight();
upReach = true;
} else if (finalY > 0) {
finalY = 0;
}
child.setTranslationY(finalY);
// 让CoordinatorLayout消费滑动事件
consumed[1] = dy;
}
lastPosition = pos;
}
}
回调介绍
- onLayoutChild
- layoutDependsOn
- onDependentViewChanged
- onNestedPreScroll
- onStartNestedScroll
package com.p.b.ui.behavior;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.p.b.R;
import com.y.b.tools.Log;
import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
public class MainViewBehavior extends CoordinatorLayout.Behavior<View>{
private static final String TAG = "TopViewBehavior";
private float deltaY;
public MainViewBehavior() {
}
public MainViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
boolean flag = (dependency.getId() == R.id.home_top_container);
Log.e(TAG,"flag:"+flag+",child.id:"+child.getId());
return flag;
}
@Override
public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull MotionEvent ev) {
return super.onInterceptTouchEvent(parent, child, ev);
}
boolean isLayout = false;
@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {
if(isLayout) {
return super.onLayoutChild(parent, child, layoutDirection);
}else {
isLayout = true;
}
int height = parent.getContext().getResources().getDimensionPixelOffset(R.dimen.home_top_container_height);
Log.e(TAG,"main.method:onLayoutChild,id:"+R.id.main_container+",child.id:"+child.getId()+",height:"+height);
child.setTranslationY(height);
return super.onLayoutChild(parent, child, layoutDirection);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
//计算列表y坐标,最小为0
float y = dependency.getHeight() + dependency.getTranslationY();
Log.e(TAG,"main.method:onDependentViewChanged,child.id:"+child.getId()+",dependency.id:"+dependency.getId()+",y:"+y+",de.height:"+dependency.getHeight()+",tranY:"+dependency.getTranslationY());
if (y <= 0) {
y = 0;
}
child.setY(y);
return true;
}}
package com.p.b.ui.behavior;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.p.b.R;
import com.y.b.tools.Log;
import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class TopViewBehavior extends CoordinatorLayout.Behavior<View>{
private static final String TAG = "TopViewBehavior";
private float deltaY;
public TopViewBehavior() {
}
public TopViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// boolean flag = (dependency.getId() == R.id.main_container);
// Log.e(TAG,"flag:"+flag+",child.id:"+child.getId());
return false;
}
@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {
// Log.e(TAG,"id:"+R.id.home_main_container+",child.id:"+child.getId());
// child.setTranslationY(600);
return false;
}
// 界面整体向上滑动,达到列表可滑动的临界点
private boolean upReach;
// 列表向上滑动后,再向下滑动,达到界面整体可滑动的临界点
private boolean downReach;
// 列表上一个全部可见的item位置
private int lastPosition = -1;
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
Log.e(TAG,"top.method:onStartNestedScroll,child.id:"+child.getId()+",ev:"+ev);
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downReach = false;
upReach = false;
break;
}
return super.onInterceptTouchEvent(parent, child, ev);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
Log.e(TAG,"top.method:onStartNestedScroll,child.id:"+child.getId()+",target.id:"+target.getId());
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
Log.e(TAG,"top.method:onNestedPreScroll,child.id:"+child.getId()+",target.id:"+target.getId());
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
if (target instanceof RecyclerView) {
RecyclerView list = (RecyclerView) target;
// 列表第一个全部可见Item的位置
int pos = ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
if (pos == 0 && pos < lastPosition) {
downReach = true;
}
// 整体可以滑动,否则RecyclerView消费滑动事件
if (canScroll(child, dy) && pos == 0) {
float finalY = child.getTranslationY() - dy;
if (finalY < -child.getHeight()) {
finalY = -child.getHeight();
upReach = true;
} else if (finalY > 0) {
finalY = 0;
}
child.setTranslationY(finalY);
// 让CoordinatorLayout消费滑动事件
consumed[1] = dy;
}
lastPosition = pos;
}
}
private boolean canScroll(View child, float scrollY) {
if (scrollY > 0 && child.getTranslationY() == -child.getHeight() && !upReach) {
return false;
}
if (downReach) {
return false;
}
return true;
}