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

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