Android進(jìn)階NestedScroll嵌套滑動(dòng)機(jī)制實(shí)現(xiàn)吸頂效果詳解
引言
在上一篇文章Android進(jìn)階寶典 -- 事件沖突怎么解決?先從Android事件分發(fā)機(jī)制開始說起中,我們?cè)敿?xì)地介紹了Android事件分發(fā)機(jī)制,其實(shí)只要頁面結(jié)構(gòu)復(fù)雜,聯(lián)動(dòng)眾多就會(huì)產(chǎn)生事件沖突,處理不得當(dāng)就是bug,e.g. 我畫了一張很丑的圖

其實(shí)這種交互形式在很多電商、支付平臺(tái)都非常常見,頁面整體是可滑動(dòng)的(scrollable),當(dāng)頁面整體往上滑時(shí),是外部滑動(dòng)組件,e.g. NestedScrollView,當(dāng)TabBar滑動(dòng)到頂部的時(shí)候吸頂,緊接著ListView自身特性繼續(xù)往上滑。
其實(shí)這種效果,系統(tǒng)已經(jīng)幫我們實(shí)現(xiàn)好了,尤其是像NestScrollView;如果我們?cè)谧远xView的時(shí)候,沒有系統(tǒng)能力的加持,會(huì)有問題嗎?如果熟悉Android事件分發(fā)機(jī)制,因?yàn)檎w上滑的時(shí)候,外部組件消費(fèi)了DOWM事件和MOVE事件,等到Tabbar吸頂之后,再次滑動(dòng)ListView的時(shí)候,因?yàn)槭录荚谕獠繑r截,此時(shí) mFirstTouchTarget還是父容器,沒有機(jī)會(huì)讓父容器取消事件再轉(zhuǎn)換到ListView,導(dǎo)致ListView不可滑動(dòng)。
那么我們只有松開手,再次滑動(dòng)ListView,讓DOWN事件傳遞到ListView當(dāng)中,這樣列表會(huì)繼續(xù)滑動(dòng),顯得沒有那么順滑,從用戶體驗(yàn)上來說是不可接受的。
1 自定義滑動(dòng)布局,實(shí)現(xiàn)吸頂效果
首先我們?nèi)绻胍獙?shí)現(xiàn)這個(gè)效果,其實(shí)辦法有很多,CoordinateLayout就是其中之一,但是如果我們想要自定義一個(gè)可滑動(dòng)的布局,而且還需要實(shí)現(xiàn)Tabbar的吸頂效果,我們需要注意兩點(diǎn):
(1)在頭部沒有被移出屏幕的時(shí)候,事件需要被外部攔截,只能滑動(dòng)外部布局,ListView不可滑動(dòng);
(2)當(dāng)頭部被移出到屏幕之外時(shí),事件需要被ListView消費(fèi)(繼續(xù)上滑時(shí)),如果下滑時(shí)則是同樣會(huì)先把頭部拉出來然后才可以滑動(dòng)ListView
1.1 滑動(dòng)容器實(shí)現(xiàn)
因?yàn)槲覀冎?,要控制view移動(dòng),可以調(diào)用scrollBy或者scrollTo兩個(gè)方法,其中兩個(gè)方法的區(qū)別在于,前者是滑動(dòng)的相對(duì)上一次的距離,而后者是滑動(dòng)到具體位置。
class MyNestScrollView @JvmOverloads constructor(
val mContext: Context,
val attributeSet: AttributeSet? = null,
val flag: Int = 0
) : LinearLayout(mContext, attributeSet, flag) {
private var mTouchSlop = 0
private var startY = 0f
init {
mTouchSlop = ViewConfiguration.get(context).scaledPagingTouchSlop
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
/**什么時(shí)候攔截事件呢,當(dāng)頭部還沒有消失的時(shí)候*/
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
Log.e("TAG", "MyNestScrollView ACTION_DOWN")
startY = event.y
}
MotionEvent.ACTION_MOVE -> {
Log.e("TAG", "MyNestScrollView ACTION_MOVE")
val endY = event.y
if (abs(endY - startY) > mTouchSlop) {
//滑動(dòng)了
scrollBy(0, (startY - endY).toInt())
}
startY = endY
}
}
return super.onTouchEvent(event)
}
override fun scrollTo(x: Int, y: Int) {
var finalY = 0
if (y < 0) {
} else {
finalY = y
}
super.scrollTo(x, finalY)
}
}
所以在事件消費(fèi)的時(shí)候,會(huì)調(diào)用scrollBy,來進(jìn)行頁面的滑動(dòng),如果我們看scrollBy的源碼,會(huì)明白最終調(diào)用就是通過scrollTo實(shí)現(xiàn)的,只不過是在上次pos的基礎(chǔ)上進(jìn)行累計(jì)。
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
所以這里重寫了scrollTo方法,來判斷y(縱向)滑動(dòng)的位置,因?yàn)楫?dāng)y小于0的時(shí)候,按照Android的坐標(biāo)系,我們知道如果一直往下滑,那么△Y(豎直方向滑動(dòng)距離) < 0,如果一直向下滑,最終totalY也會(huì)小于0,所以這里也是做一次邊界的處理。
接下來我們需要處理下吸頂效果,所以我們需要知道,頂部View的高度,以便控制滑動(dòng)的距離,也是一次邊界處理。
override fun scrollTo(x: Int, y: Int) {
var finalY = 0
if (y < 0) {
} else {
finalY = y
}
if (y > mTopViewHeight) {
finalY = mTopViewHeight
}
super.scrollTo(x, finalY)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
//頂部view是第一個(gè)View
mTopViewHeight = getChildAt(0).measuredHeight
}
所以這里需要和我們寫的布局相對(duì)應(yīng),頂部view是容器中第一個(gè)子View,通過在onSizeChanged或者onMeasure中獲取第一個(gè)子View的高度,在滑動(dòng)時(shí),如果滑動(dòng)的距離超過 mTopViewHeight(頂部View的高度),那么滑動(dòng)時(shí)也就不會(huì)再繼續(xù)滑動(dòng)了,這樣就實(shí)現(xiàn)了TabBar的吸頂效果。
基礎(chǔ)工作完成了,接下來我們完成需要注意的第一點(diǎn),先看下面的圖:

當(dāng)我們上滑的時(shí)候,頭部是準(zhǔn)備逐漸隱藏的,所以這里會(huì)有幾個(gè)條件,首先 mStartX - nowX > 0 而且 scrollY < mTopViewHeight,而且此時(shí)scrollY是大于0的
/**
* 頭部View逐漸消失
* @param dy 手指滑動(dòng)的相對(duì)距離 dy >0 上滑 dy < 0 下滑
*/
private fun isViewHidden(dy: Int): Boolean {
return dy > 0 && scrollY < mTopViewHeight
}
當(dāng)我們向下滑動(dòng)的時(shí)候,此時(shí) mStartX - nowX < 0,因?yàn)榇藭r(shí)頭部隱藏了,所以ScrollY > 0,而且此時(shí)是能夠滑動(dòng)的,如果到了下面這個(gè)邊界條件(不會(huì)有這種情況發(fā)生,因此在滑動(dòng)時(shí)做了邊界處理),此時(shí)scrollY < 0

private fun isViewShow(dy: Int):Boolean{
return dy < 0 && scrollY > 0 && !canScrollVertically(-1)
}
此時(shí)還有一個(gè)條件,就是canScrollVertically,這個(gè)相信伙伴們也很熟悉,意味著當(dāng)前View是能夠往下滑動(dòng)的,如果返回了false,那么就是不能繼續(xù)往下滑動(dòng)了。
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var intercepted = false
/**什么時(shí)候攔截事件呢,當(dāng)頭部還沒有消失的時(shí)候*/
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
startY = ev.rawY
}
MotionEvent.ACTION_MOVE -> {
val endY = ev.rawY
if (abs(startY - endY) > mTouchSlop) {
if (isViewHidden((startY - endY).toInt())
|| isViewShow((startY - endY).toInt())
) {
Log.e("TAG","此時(shí)就需要攔截,外部進(jìn)行消費(fèi)事件")
//此時(shí)就需要攔截,外部進(jìn)行消費(fèi)事件
intercepted = true
}
}
startY = endY
}
}
return intercepted
}
所以在外部攔截的時(shí)候,通過判斷這兩種狀態(tài),如果滿足其中一個(gè)條件就會(huì)攔截事件完全由外部容器處理,這樣就完成了吸頂效果的處理。

1.2 嵌套滑動(dòng)機(jī)制完成交互優(yōu)化
通過上面的gif,我們看效果貌似還可以,但是有一個(gè)問題就是,當(dāng)完成吸頂之后,ListView并不能跟隨手指繼續(xù)向上滑動(dòng),而是需要松開手指之后,再次滑動(dòng)即可,其實(shí)我們從Android事件分發(fā)機(jī)制中就能夠知道,此時(shí)mFirstTouchTarget == 父容器,此時(shí)再次上滑并沒有給父容器Cancel的機(jī)會(huì),所以才導(dǎo)致事件沒有被ListView接收。
因?yàn)閭鹘y(tǒng)的事件沖突解決方案,會(huì)導(dǎo)致滑動(dòng)不流暢,此時(shí)就需要嵌套滑動(dòng)機(jī)制解決這個(gè)問題。在前面我們提到過,NestedScrollView其實(shí)就是已經(jīng)處理過嵌套滑動(dòng)了,所以我們前面去看一下NestedScrollView到底干了什么事?
public class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
NestedScrollingChild3, ScrollingView
我們看到,NestedScrollView是實(shí)現(xiàn)了NestedScrollingParent3、NestedScrollingChild3等接口,挺有意思的,這幾個(gè)接口貌似都是根據(jù)數(shù)字做了升級(jí),既然有3,那么必然有1和2,所以我們看下這幾個(gè)接口的作用。
1.2.1 NestedScrollingParent接口和NestedScrollingChild接口
對(duì)于NestedScrollingParent接口,如果可滑動(dòng)的ViewGroup,e.g. 我們?cè)?.1中定義的容器作為父View,那么就需要實(shí)現(xiàn)這個(gè)接口;如果是作為可滑動(dòng)的子View,那么就需要實(shí)現(xiàn)NestedScrollingChild接口,因?yàn)槲覀冊(cè)谧远x控件的時(shí)候,它既可能作為子View也可能作為父View,因此這倆接口都需要實(shí)現(xiàn)。
public interface NestedScrollingChild {
/**
* Enable or disable nested scrolling for this view.
*
* 啟動(dòng)或者禁用嵌套滑動(dòng),如果返回ture,那么說明當(dāng)前布局存在嵌套滑動(dòng)的場(chǎng)景,反之沒有
* 使用場(chǎng)景:NestedScrollingParent嵌套NestedScrollingChild
* 在此接口中的方法,都是交給NestedScrollingChildHelper代理類實(shí)現(xiàn)
*/
void setNestedScrollingEnabled(boolean enabled);
/**
* Returns true if nested scrolling is enabled for this view.
* 其實(shí)就是返回setNestedScrollingEnabled中設(shè)置的值
*/
boolean isNestedScrollingEnabled();
/**
* Begin a nestable scroll operation along the given axes.
* 表示view開始滾動(dòng)了,一般是在ACTION_DOWN中調(diào)用,如果返回true則表示父布局支持嵌套滾動(dòng)。
* 一般也是直接代理給NestedScrollingChildHelper的同名方法即可。這個(gè)時(shí)候正常情況會(huì)觸發(fā)Parent的onStartNestedScroll()方法
*/
boolean startNestedScroll(@ScrollAxis int axes);
/**
* Stop a nested scroll in progress.
* 停止嵌套滾動(dòng),一般在UP或者CANCEL事件中執(zhí)行,告訴父容器已經(jīng)停止了嵌套滑動(dòng)
*/
void stopNestedScroll();
/**
* Returns true if this view has a nested scrolling parent.
* 判斷當(dāng)前View是否存在嵌套滑動(dòng)的Parent
*/
boolean hasNestedScrollingParent();
/**
* 當(dāng)前View消費(fèi)滑動(dòng)事件之后,滾動(dòng)一段距離之后,把剩余的距離回調(diào)給父容器,父容器知道當(dāng)前剩余距離
* dxConsumed:x軸滾動(dòng)的距離
* dyConsumed:y軸滾動(dòng)的距離
* dxUnconsumed:x軸未消費(fèi)的距離
* dyUnconsumed:y軸未消費(fèi)的距離
* 這個(gè)方法是嵌套滑動(dòng)的時(shí)候調(diào)用才有用,返回值 true分發(fā)成功;false 分發(fā)失敗
*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
/**
* Dispatch one step of a nested scroll in progress before this view consumes any portion of it.
* 在子View消費(fèi)滑動(dòng)距離之前,將滑動(dòng)距離傳遞給父容器,相當(dāng)于把消費(fèi)權(quán)交給parent
* dx:當(dāng)前水平方向滑動(dòng)的距離
* dy:當(dāng)前垂直方向滑動(dòng)的距離
* consumed:輸出參數(shù),會(huì)將Parent消費(fèi)掉的距離封裝進(jìn)該參數(shù)consumed[0]代表水平方向,consumed[1]代表垂直方向
* @return true:代表Parent消費(fèi)了滾動(dòng)距離
*/
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow);
/**
* Dispatch one step of a nested scroll in progress.
* 處理慣性事件,與dispatchNestedScroll類似,也是在消費(fèi)事件之后,將消費(fèi)和未消費(fèi)的距離都傳遞給父容器
*/
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
* Dispatch a fling to a nested scrolling parent before it is processed by this view.
* 與dispatchNestedPreScroll類似,在消費(fèi)之前首先會(huì)傳遞給父容器,把優(yōu)先處理權(quán)交給父容器
*/
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
public interface NestedScrollingParent {
/**
* React to a descendant view initiating a nestable scroll operation, claiming the
* nested scroll operation if appropriate.
* 當(dāng)子View調(diào)用startNestedScroll方法的時(shí)候,父容器會(huì)在這個(gè)方法中獲取回調(diào)
*/
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
/**
* React to the successful claiming of a nested scroll operation.
* 在onStartNestedScroll調(diào)用之后,就緊接著調(diào)用這個(gè)方法
*/
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
/**
* React to a nested scroll operation ending.
* 當(dāng)子View調(diào)用 stopNestedScroll方法的時(shí)候回調(diào)
*/
void onStopNestedScroll(@NonNull View target);
/**
* React to a nested scroll in progress.
*
*/
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
/**
* React to a nested scroll in progress before the target view consumes a portion of the scroll.
* 在子View調(diào)用dispatchNestedPreScroll之后,這個(gè)方法拿到了回調(diào)
*
*/
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
/**
* Request a fling from a nested scroll.
*
*/
boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);
/**
* React to a nested fling before the target view consumes it.
*
*/
boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
/**
* Return the current axes of nested scrolling for this NestedScrollingParent.
* 返回當(dāng)前滑動(dòng)的方向
*/
@ScrollAxis
int getNestedScrollAxes();
}
通過這兩個(gè)接口,我們大概就能夠明白,其實(shí)嵌套滑動(dòng)機(jī)制完全是子View在做主導(dǎo),通過子View能夠決定Parent是否能夠優(yōu)先消費(fèi)事件(dispatchNestedPreScroll),所以我們先從子View開始,開啟嵌套滑動(dòng)之旅。
1.2.2 預(yù)滾動(dòng)階段實(shí)現(xiàn)
在這個(gè)示例中,需要與parent嵌套滑動(dòng)的就是RecyclerView,所以RecyclerView就需要實(shí)現(xiàn)child接口。前面我們看到child接口好多方法,該怎么調(diào)用呢?其實(shí)這個(gè)接口中大部分的方法都可以交給一個(gè)helper代理類實(shí)現(xiàn),e.g. NestedScrollingChildHelper.
因?yàn)樗械那短谆瑒?dòng)都是由子View主導(dǎo),所以我們先看子View消費(fèi)事件,也就是onTouchEvent中,如果當(dāng)手指按下的時(shí)候,首先獲取滑動(dòng)的是x軸還是y軸,這里我們就認(rèn)為是豎向滑動(dòng),然后調(diào)用NestedScrollingChild的startNestedScroll方法,這個(gè)方法就代表開始滑動(dòng)了。
override fun onTouchEvent(e: MotionEvent?): Boolean {
when(e?.action){
MotionEvent.ACTION_DOWN->{
mStartX = e.y.toInt()
//子View開始嵌套滑動(dòng)
var axis = ViewCompat.SCROLL_AXIS_NONE
axis = axis or ViewCompat.SCROLL_AXIS_VERTICAL
nestedScrollingChildHelper.startNestedScroll(axis)
}
MotionEvent.ACTION_MOVE->{
}
}
return super.onTouchEvent(e)
}
我們看下startNestedScroll內(nèi)部的源碼:
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
從源碼中 我們可以看到,首先如果有嵌套滑動(dòng)的父容器,直接返回true,此時(shí)代表嵌套滑動(dòng)成功;
public boolean hasNestedScrollingParent(@NestedScrollType int type) {
return getNestedScrollingParentForType(type) != null;
private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
switch (type) {
case TYPE_TOUCH:
return mNestedScrollingParentTouch;
case TYPE_NON_TOUCH:
return mNestedScrollingParentNonTouch;
}
return null;
}
在判斷的時(shí)候,會(huì)判斷mNestedScrollingParentTouch是否為空,因?yàn)榈谝淮芜M(jìn)來的時(shí)候肯定是空的,所以會(huì)繼續(xù)往下走;如果支持嵌套滑動(dòng),那么就會(huì)進(jìn)入到while循環(huán)中。
核心代碼1:
while (p != null) {
//---------- 判斷條件1 -------------//
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
首先調(diào)用ViewParentCompat的onStartNestedScroll方法如下:
public static boolean onStartNestedScroll(@NonNull ViewParent parent, @NonNull View child,
@NonNull View target, int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
if (Build.VERSION.SDK_INT >= 21) {
try {
return Api21Impl.onStartNestedScroll(parent, child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onStartNestedScroll", e);
}
} else if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
}
return false;
}
其實(shí)在這個(gè)方法中,就是判斷parent是否實(shí)現(xiàn)了NestedScrollingParent(2 3)接口,如果實(shí)現(xiàn)了此接口,那么返回值就是parent中onStartNestedScroll的返回值。
這里需要注意的是,如果parent中onStartNestedScroll的返回值為false,那么就不會(huì)進(jìn)入代碼塊的條件判斷,所以在實(shí)現(xiàn)parent接口的時(shí)候,onStartNestedScroll需要返回true。進(jìn)入代碼塊中調(diào)用setNestedScrollingParentForType方法,將父容器給mNestedScrollingParentTouch賦值,那么此時(shí)hasNestedScrollingParent方法就返回true,不需要遍歷View層級(jí)了。
private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
switch (type) {
case TYPE_TOUCH:
mNestedScrollingParentTouch = p;
break;
case TYPE_NON_TOUCH:
mNestedScrollingParentNonTouch = p;
break;
}
}
然后又緊接著調(diào)用了parent的onNestedScrollAccepted方法,這兩者一前一后,這樣預(yù)滾動(dòng)階段就算是完成了。
在父容器中,預(yù)滾動(dòng)節(jié)點(diǎn)就需要處理這兩個(gè)回調(diào)即可,關(guān)鍵在于onStartNestedScroll的返回值。
override fun onStartNestedScroll(child: View, target: View, axes: Int): Boolean {
Log.e("TAG","onStartNestedScroll")
//這里需要return true,否則在子View中分發(fā)事件就不會(huì)成功
return true
}
override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
Log.e("TAG","onNestedScrollAccepted")
}
1.2.3 滾動(dòng)階段實(shí)現(xiàn)
然后MOVE事件來了,這個(gè)時(shí)候我們需要記住,即便是滑動(dòng)了子View,但是子View依然是需要將事件扔給父類,這里就需要調(diào)用dispatchNestedPreScroll方法,這里在1.2.1中介紹過,需要跟dispatchNestedScroll區(qū)分,dispatchNestedPreScroll是在子View消費(fèi)事件之前就交給父類優(yōu)先處理。
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
//這里不為空了
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
consumed = getTempNestedScrollConsumed();
}
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
//-------- 由父容器是否消費(fèi)決定返回值 -------//
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
在子View調(diào)用dispatchNestedPreScroll方法時(shí),需要傳入四個(gè)參數(shù),這里我們?cè)俅卧敿?xì)介紹一下:
dx、dy指的是x軸和y軸滑動(dòng)的距離;
consumed在子View調(diào)用時(shí),其實(shí)只需要傳入一個(gè)空數(shù)組即可,具體的賦值是需要在父容器中進(jìn)行,父view消費(fèi)了多少距離,就傳入多少,consumed[0]代表x軸,consumed[1]代表y軸;
看上面的源碼,當(dāng)dx或者dy不為0的時(shí)候,說明有滑動(dòng)了,那么此時(shí)就會(huì)做一些初始化的配置,把consumed數(shù)組清空,然后會(huì)調(diào)用父容器的onNestedPreScroll方法,父容器決定是否消費(fèi)這個(gè)事件,因?yàn)樵诟溉萜髦袝?huì)對(duì)consumed數(shù)組進(jìn)行復(fù)制,所以這個(gè)方法的返回值代表著父容器是否消費(fèi)過事件;如果消費(fèi)過,那么就返回true,沒有消費(fèi)過,那么就返回false.
所以我們先看父容器的處理:
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
Log.e("TAG", "onNestedPreScroll")
//父容器什么時(shí)候 消費(fèi)呢?
if (isViewShow(dy) || isViewHidden(dy)) {
//假設(shè)這個(gè)時(shí)候把事件全消費(fèi)了
consumed[1] = dy
scrollBy(0, dy)
}
}
其實(shí)我們這里就是直接將之前在onTouchEvent中的處理邏輯放在了onNestedPreScroll中,如果在上拉或者下滑時(shí),首先頭部優(yōu)先,假設(shè)父容器把距離全部消費(fèi),這個(gè)時(shí)候給consumed[1]賦值為dy。
MotionEvent.ACTION_MOVE -> {
val endY = e.y.toInt()
val endX = e.x.toInt()
var dx = mStartX - endX
var dy = mStartY - endY
//進(jìn)行事件分發(fā),優(yōu)先給parent
if (dispatchNestedPreScroll(dx, dy, cosumed, null)) {
//如果父容器消費(fèi)過事件,這個(gè)時(shí)候,cosumed有值了,我們只關(guān)心dy
dy -= cosumed[1]
if (dy == 0) {
//代表父容器全給消費(fèi)了
return true
}
} else {
//如果沒有消費(fèi)事件,那么就子view消費(fèi)吧
smoothScrollBy(dx, dy)
}
}
再來看子View,這里是在MOVE事件中進(jìn)行事件分發(fā),調(diào)用dispatchNestedPreScroll方法,判斷如果父容器有事件消費(fèi),看消費(fèi)了多少,剩下的就是子View消費(fèi);如果父容器沒有消費(fèi),dispatchNestedPreScroll返回了false,那么子View自行處理事件。
所以如果子View使用的是RecyclerView,那么在父容器做完處理之后,其實(shí)就能夠?qū)崿F(xiàn)嵌套滑動(dòng)吸頂?shù)耐昝佬Ч?,為什么呢?是因?yàn)镽ecyclerView本來就實(shí)現(xiàn)了parent接口,所以如果在自定義子View(可滑動(dòng))時(shí),子View處理的這部分代碼就需要特別關(guān)心。
1.2.4 滾動(dòng)結(jié)束
在手指抬起之后,調(diào)用stopNestedScroll方法。
MotionEvent.ACTION_UP->{
nestedScrollingChildHelper.stopNestedScroll()
}
從源碼中看,其實(shí)就是回到父容器的onStopNestedScroll方法,然后將滑動(dòng)的標(biāo)志位(mNestedScrollingParentTouch)置為空,在下次按下的時(shí)候,重新初始化。
public void stopNestedScroll(@NestedScrollType int type) {
ViewParent parent = getNestedScrollingParentForType(type);
if (parent != null) {
ViewParentCompat.onStopNestedScroll(parent, mView, type);
setNestedScrollingParentForType(type, null);
}
}以上就是Android進(jìn)階NestedScroll嵌套滑動(dòng)機(jī)制實(shí)現(xiàn)吸頂效果詳解的詳細(xì)內(nèi)容,更多關(guān)于Android NestedScroll吸頂?shù)馁Y料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android小程序?qū)崿F(xiàn)選項(xiàng)菜單
這篇文章主要為大家詳細(xì)介紹了Android小程序?qū)崿F(xiàn)選項(xiàng)菜單,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05
Android編程實(shí)現(xiàn)簡(jiǎn)單設(shè)置按鈕顏色的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)簡(jiǎn)單設(shè)置按鈕顏色的方法,涉及Android控件布局與屬性設(shè)置相關(guān)操作技巧,需要的朋友可以參考下2017-03-03
Android TextWatcher三個(gè)回調(diào)以及監(jiān)聽EditText的輸入案例詳解
這篇文章主要介紹了Android TextWatcher三個(gè)回調(diào)以及監(jiān)聽EditText的輸入案例詳解,本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
Android NDK生成及連接靜態(tài)庫與動(dòng)態(tài)庫的方法
這篇文章主要介紹了Android NDK生成及連接靜態(tài)庫與動(dòng)態(tài)庫的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08
Android:Field can be converted to a local varible.的解決辦法
這篇文章主要介紹了Android:Field can be converted to a local varible.的解決辦法的相關(guān)資料,希望通過本文能幫助到大家,讓大家遇到這樣的問題輕松解決,需要的朋友可以參考下2017-10-10
Android獲取應(yīng)用程序大小和緩存的實(shí)例代碼
這篇文章主要介紹了Android獲取應(yīng)用程序大小和緩存的實(shí)例代碼的相關(guān)資料,非常不錯(cuò)具有參考借鑒價(jià)值,需要的朋友可以參考下2016-10-10
Android中代碼運(yùn)行指定Apk的簡(jiǎn)單方法
這篇文章主要介紹了Android中代碼運(yùn)行指定Apk的簡(jiǎn)單方法,有需要的朋友可以參考一下2014-01-01
Android 網(wǎng)絡(luò)請(qǐng)求框架解析之okhttp與okio
HTTP是現(xiàn)代應(yīng)用常用的一種交換數(shù)據(jù)和媒體的網(wǎng)絡(luò)方式,高效地使用HTTP能讓資源加載更快,節(jié)省帶寬,OkHttp是一個(gè)高效的HTTP客戶端,下面這篇文章主要給大家介紹了關(guān)于OkHttp如何用于安卓網(wǎng)絡(luò)請(qǐng)求,需要的朋友可以參考下2021-10-10
Android實(shí)現(xiàn)從緩存中讀取圖片與異步加載功能類
這篇文章主要介紹了Android實(shí)現(xiàn)從緩存中讀取圖片與異步加載功能類,涉及Android針對(duì)緩存的操作及圖片異步加載相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-08-08
Android 判斷ip地址合法實(shí)現(xiàn)代碼
這篇文章主要介紹了Android 判斷ip地址合法實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-06-06

