目標
- 2個recycleview同時放在一個頁面內(nèi),可以完成平滑滾動
- 頂部recycleview有固定高度,為gridLayout布局
- 底部recycleview占用剩余高度,為LinearLayoutManager布局
目前沒有直接可用的布局可以完成我們的需求,我們基于CoordinatorLayout做一些簡單的定制來完成我們的需求,如果想完成定制,那么需要我們理解嵌入式滑動的原理,下面我們會從三個方面來進行講解
- 繪制原理
- 事件分發(fā)機制
- 嵌入式滑動處理
最終效果圖

android的繪制原理
- 跟繪制相關(guān)的三個核心方法
- onMeasure
- onLayout
- onDraw
onMeasure
當計劃在界面繪制一個View時,我們需要知道,視圖的大小,onMeasure會提供給我們一個機會來決定我們繪制view的大小,我們可以直接設(shè)定這個大小,也可以設(shè)置一個依賴值,由父類根據(jù)父類的大小來動態(tài)決定子空間大小,我們一旦自己設(shè)置了固定的大小,那么需要在這里調(diào)用setMeasuredDimension方法,明確告訴父容器我們的設(shè)置
依賴值
- ViewGroup.MATCH_PARENT
- ViewGroup.WRAP_CONTENT
MATCH_PARENT 表示,我們需要父容器有多大,我們盡可能占據(jù)多大
WRAP_CONTENT 表示,只要能夠顯示出我們的內(nèi)容,就可以了。其他位置由父容器另外安排
getMeasureHeight 和getHeight的區(qū)別
- getMeasureHeight是計算出來的高度
- getHeight是最后繪制的高度
- 有可能不同,因為后面還有動畫,或者直接設(shè)置來改變
- 在onMeasure后。getMeasureHeight是有值的
- 在onDraw后,getHeight才有值
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
onLayout
上面我們一旦知道我們的大小,那么就需要確定我們的位置,在這個回調(diào)中,父容器提供一個機會給我們來確定自己容器的位置,我們可以根據(jù)父容器提供的上下左右來確定我們的位置,也可以自己設(shè)置我們理想中的上下左右位置。
座標系
- 原點左上角
- 寬是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
這個方法調(diào)用時,就是根據(jù)我們前面通過onMeasure和onLayout完成的對視圖大小和位置的計算來完成最終的繪制。我們可以在這塊來決定繪制的顏色,也可以修改我們繪制的大小和位置。
Canvas
- Canvas是無限大的
- 屏幕只是畫布的可見區(qū)域
- 我們可以繪制在屏幕外部
- 如果需要看到屏幕外部的內(nèi)容,我們需要滑動屏幕來完成,不過為了優(yōu)化。我們常常是在屏幕外不會去做繪制的
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
事件分發(fā)機制
- 事件分發(fā)的三個方法
- dispatchTouchEvent
- onInterceptTouchEvent
- onTouchEvent
dispatchTouchEvent
- View
- ViewGroup
- Activity
這個方法是事件分發(fā)的入口,所有的方法都從這個入口進入,然后向子視圖或者自己的其他方法傳遞,在這個方法內(nèi)的攔截會直接影響性能和后面的回調(diào)處理
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
onInterceptTouchEvent
- ViewGroup
- Activity
這個方法只存在可以添加子視圖的容器類中,因為這個方法主要是做攔截處理的。如果方法返回true,那么就開始攔截,會把事件轉(zhuǎn)到自己的onTouchEvent中,
而不會向子視圖傳遞,如果返回false,那么不會攔截,會繼續(xù)傳遞和處理
@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);
}
嵌入式滑動的定制處理
上面的繪制和事件分發(fā)邏輯都比較簡潔,清晰,隨著業(yè)務(wù)發(fā)展,可能需要更多更負責的頁面,比如在一個頁面滑動時,需要修改其他視圖的布局或者滑動。那么Android提供給了我們CoordernateLayout布局來完成,同時提供了ViewBehavior來自定義嵌入式滑動的布局和滑動
- ViewBehavior
- NestedScrollingParent
- NestedScrollingChild
目前很多android默認的視圖已經(jīng)實現(xiàn)了NestedScrollingParent和NestedScrollingChild接口來完成了嵌入式滑動的處理,我們可以直接使用,不過如果使用效果無法滿足我們的需求,還是需要通過ViewBehavior來定制我們處理。
如果我們用到的視圖不支持嵌入式滑動,我們需要自己來實現(xiàn)NestedScrollingParent和NestedScrollingChild接口來完成嵌入式滑動。
原理
容器A支持NestedScrollingParent, 增加了支持NestedScrollingChild接口的視圖B和視圖C,那么在B滑動時,如何影響視圖C的布局和滑動呢
以前的滑動處理
- 如果滑動發(fā)生在B,那么事件分發(fā)由A開始
- 如果A要攔截,那么A就會處理事件
- 如果A不攔截,那么就交給B來處理自己的事件
嵌入式滑動
- 如果滑動發(fā)生在B,同時B支持NestedScrollingChild,事件分發(fā)還在是A開始
- 如果容器A內(nèi)的子視圖有包含NestedScrollingChild或者有ViewBehavior。那么就要分發(fā)touch事件給這個視圖,比如A中的另外一個視圖C
- 視圖C會根據(jù)定制的ViewBehavior來確定是否要響應(yīng)這個滑動。
需求解決
我們的目標是容器A中有容器B和容器C,先添加B,再添加C,B為頂部視圖,C為底部視圖,B和C都是RecycleView,他們是支持滑動的,A我們可以使用CoordernateLayout,B和C使用嵌入式滑動來處理事件
我們需要解決幾個問題
- 布局問題,需要B和C平鋪在A中,默認是覆蓋,后面的覆蓋前面的
- 滑動問題,在C上滑動時,整體布局上移動,知道B移除屏幕,C開始處理自己的滑動
布局問題
- layoutDependsOn
- onLayoutChild
C在layoutDependsOn回調(diào)中設(shè)置對B的依賴。那么B繪制完,C會被觸發(fā),onLayoutChild回調(diào)中我們下移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 會來確定是否消費,消費多少,同時返回消費剩余內(nèi)容
@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;
}
}
回調(diào)介紹
- 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;
}