Android深入探究自定義View之嵌套滑動(dòng)的實(shí)現(xiàn)
本文主要探討以下幾個(gè)問(wèn)題:
- 嵌套滑動(dòng)設(shè)計(jì)目的
- 嵌套滑動(dòng)的實(shí)現(xiàn)
- 嵌套滑動(dòng)與事件分發(fā)機(jī)制
嵌套滑動(dòng)設(shè)計(jì)目的
不知道大家有沒(méi)有注意過(guò)淘寶APP首頁(yè)的二級(jí)聯(lián)動(dòng),滑動(dòng)的商品的時(shí)候上面類別也會(huì)滑動(dòng),滑動(dòng)過(guò)程中類別模塊停了商品還能繼續(xù)滑動(dòng)。也就是說(shuō)滑動(dòng)的是view,ViewGroup也會(huì)跟著滑動(dòng)。如果用事件分發(fā)機(jī)制處理也能處理,但會(huì)及其麻煩。那用NestedScroll會(huì)咋樣?
嵌套滑動(dòng)的實(shí)現(xiàn)
假設(shè)布局如下

RecyclerView 實(shí)現(xiàn)了 NestedScrollingChild 接口,NestedScrollView 實(shí)現(xiàn)了 NestedScrollingParent,這是實(shí)現(xiàn)嵌套布局的基礎(chǔ)
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3 public class NestedScrollView extends FrameLayout implements NestedScrollingParent3, NestedScrollingChild3, ScrollingView
滑動(dòng)屏幕時(shí) RecyclerView 收到滑動(dòng)事件,在 ACTION_DOWN 時(shí)
// RecyclerView.java onTouchEvent函數(shù)
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
//
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
}
break;
繼續(xù)深入
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;
}
遞歸尋找NestedScrollingParent,然后回調(diào) onStartNestedScroll 和 onNestedScrollAccepted 。onStartNestedScroll 決定了當(dāng)前控件是否能接收到其內(nèi)部View(非并非是直接子View)滑動(dòng)時(shí)的參數(shù);按下時(shí)確定其嵌套的父布局以及是否能收到后續(xù)事件。再看ACTION_MOVE事件
case MotionEvent.ACTION_MOVE: {
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
} break;
ACTION_MOVE 中調(diào)用了 dispatchNestedPreScroll 。dispatchNestedPreScroll 中會(huì)回調(diào) onNestedPreScroll 方法,內(nèi)部的 scrollByInternal 中還會(huì)回調(diào) onNestedScroll 方法
整個(gè)流程如下

onNestedPreScroll中,我們判斷,如果是上滑且頂部控件未完全隱藏,則消耗掉dy,即consumed[1]=dy;如果是下滑且內(nèi)部View已經(jīng)無(wú)法繼續(xù)下拉,則消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去執(zhí)行scrollBy,實(shí)際上就是我們的NestedScrollView 滑動(dòng)。
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
// 向上滑動(dòng)。若當(dāng)前topview可見(jiàn),需要將topview滑動(dòng)至不可見(jiàn)
boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight();
if (hideTop) {
scrollBy(0, dy);
// 這個(gè)是被消費(fèi)的距離,如果沒(méi)有會(huì)被重復(fù)消費(fèi)現(xiàn)象是父布局與子布局同時(shí)滑動(dòng),滑動(dòng)的距離被消費(fèi)兩次
consumed[1] = dy;
}
}
整體代碼如下
public class NestedScrollLayout extends NestedScrollView {
private View topView;
private ViewGroup contentView;
private static final String TAG = "NestedScrollLayout";
public NestedScrollLayout(Context context) {
this(context, null);
init();
}
public NestedScrollLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
init();
}
public NestedScrollLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
init();
}
public NestedScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr);
init();
}
private FlingHelper mFlingHelper;
int totalDy = 0;
/**
* 用于判斷RecyclerView是否在fling
*/
boolean isStartFling = false;
/**
* 記錄當(dāng)前滑動(dòng)的y軸加速度
*/
private int velocityY = 0;
private void init() {
mFlingHelper = new FlingHelper(getContext());
setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if (isStartFling) {
totalDy = 0;
isStartFling = false;
}
if (scrollY == 0) {
Log.e(TAG, "TOP SCROLL");
// refreshLayout.setEnabled(true);
}
if (scrollY == (getChildAt(0).getMeasuredHeight() - v.getMeasuredHeight())) {
Log.e(TAG, "BOTTOM SCROLL");
dispatchChildFling();
}
//在RecyclerView fling情況下,記錄當(dāng)前RecyclerView在y軸的偏移
totalDy += scrollY - oldScrollY;
}
});
}
private void dispatchChildFling() {
if (velocityY != 0) {
Double splineFlingDistance = mFlingHelper.getSplineFlingDistance(velocityY);
if (splineFlingDistance > totalDy) {
childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - Double.valueOf(totalDy)));
}
}
totalDy = 0;
velocityY = 0;
}
private void childFling(int velY) {
RecyclerView childRecyclerView = getChildRecyclerView(contentView);
if (childRecyclerView != null) {
childRecyclerView.fling(0, velY);
}
}
@Override
public void fling(int velocityY) {
super.fling(velocityY);
if (velocityY <= 0) {
this.velocityY = 0;
} else {
isStartFling = true;
this.velocityY = velocityY;
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
topView = ((ViewGroup) getChildAt(0)).getChildAt(0);
contentView = (ViewGroup) ((ViewGroup) getChildAt(0)).getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 調(diào)整contentView的高度為父容器高度,使之填充布局,避免父容器滾動(dòng)后出現(xiàn)空白
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams lp = contentView.getLayoutParams();
lp.height = getMeasuredHeight();
contentView.setLayoutParams(lp);
}
/**
* 解決滑動(dòng)沖突:RecyclerView在滑動(dòng)之前會(huì)問(wèn)下父布局是否需要攔截,父布局使用此方法
*/
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
Log.e("NestedScrollLayout", getScrollY()+"::onNestedPreScroll::"+topView.getMeasuredHeight()+"::dy::"+dy);
// 向上滑動(dòng)。若當(dāng)前topview可見(jiàn),需要將topview滑動(dòng)至不可見(jiàn)
boolean hideTop = dy > 0 && getScrollY() < topView.getMeasuredHeight();
if (hideTop) {
scrollBy(0, dy);
// 這個(gè)是被消費(fèi)的距離,如果沒(méi)有會(huì)被重復(fù)消費(fèi),現(xiàn)象是父布局與子布局同時(shí)滑動(dòng)
consumed[1] = dy;
}
}
private RecyclerView getChildRecyclerView(ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View view = viewGroup.getChildAt(i);
if (view instanceof RecyclerView && view.getClass() == NestedLogRecyclerView.class) {
return (RecyclerView) viewGroup.getChildAt(i);
} else if (viewGroup.getChildAt(i) instanceof ViewGroup) {
ViewGroup childRecyclerView = getChildRecyclerView((ViewGroup) viewGroup.getChildAt(i));
if (childRecyclerView instanceof RecyclerView) {
return (RecyclerView) childRecyclerView;
}
}
continue;
}
return null;
}
}
嵌套滑動(dòng)與事件分發(fā)機(jī)制
- 事件分發(fā)機(jī)制:子View首先得到事件處理權(quán),處理過(guò)程中父View可以對(duì)其攔截,但是攔截了以后就無(wú)法再還給子View(本次手勢(shì)內(nèi))。
- NestedScrolling 滑動(dòng)機(jī)制:內(nèi)部View在滾動(dòng)的時(shí)候,首先將dx,dy交給NestedScrollingParent,NestedScrollingParent可對(duì)其進(jìn)行部分消耗,剩余的部分還給內(nèi)部View。
總結(jié):嵌套布局要注意的有幾個(gè)方面
- ACTION_DOWN 時(shí)子view調(diào)用父布局的onStartNestedScroll,根據(jù)滑動(dòng)方向判斷父布局是否要收到子view的滑動(dòng)參數(shù)
- ACTION_MOVE時(shí)子view調(diào)用父布局的onNestedPreScroll函數(shù),父布局是否要滑動(dòng)已經(jīng)消費(fèi)掉自身需要的距離
- ACTION_UP時(shí),手指抬起可能還有加速度,調(diào)用父布局的onPreFling判斷是否需要消費(fèi)以及消費(fèi)剩下的再傳給子布局
到此這篇關(guān)于Android深入探究自定義View之嵌套滑動(dòng)的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Android 嵌套滑動(dòng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Flutter利用Canvas繪制精美表盤(pán)效果詳解
這篇文章主要介紹了如何利用Flutter中的Canvas繪制一個(gè)精美的表盤(pán)效果,文中的實(shí)現(xiàn)步驟講解詳細(xì),快跟隨小編一起動(dòng)手嘗試一下2022-03-03
Android游戲開(kāi)發(fā)學(xué)習(xí)①?gòu)椞∏驅(qū)崿F(xiàn)方法
這篇文章主要介紹了Android游戲開(kāi)發(fā)學(xué)習(xí)①?gòu)椞∏驅(qū)崿F(xiàn)方法,涉及Android通過(guò)物理引擎BallThread類模擬小球運(yùn)動(dòng)的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10
Android開(kāi)發(fā)解決popupWindow重疊報(bào)錯(cuò)問(wèn)題
今天小編就為大家分享一篇關(guān)于Android開(kāi)發(fā)解決popupWindow重疊報(bào)錯(cuò)問(wèn)題的文章,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-10-10
Android Studio 恢復(fù)小窗口??磕J?Docked Mode)
這篇文章主要介紹了Android Studio 恢復(fù)小窗口??磕J?Docked Mode),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-04-04
Android使用ViewPager實(shí)現(xiàn)啟動(dòng)引導(dǎo)頁(yè)
這篇文章主要為大家詳細(xì)介紹了Android使用ViewPager實(shí)現(xiàn)第一次啟動(dòng)引導(dǎo)頁(yè),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-07-07
Android自定義控件實(shí)現(xiàn)帶數(shù)值和動(dòng)畫(huà)的圓形進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了Android自定義控件實(shí)現(xiàn)帶數(shù)值和動(dòng)畫(huà)的圓形進(jìn)度條,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12
Android編程實(shí)現(xiàn)等比例顯示圖片的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)等比例顯示圖片的方法,實(shí)例分析了Android等比例縮放圖片的具體步驟與相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11
Kotlin實(shí)現(xiàn)圖片選擇器的關(guān)鍵技術(shù)點(diǎn)總結(jié)
這篇文章主要給大家介紹了關(guān)于Kotlin實(shí)現(xiàn)圖片選擇器的一些關(guān)鍵技術(shù)點(diǎn),這是一個(gè)我在學(xué)習(xí)Kotlin過(guò)程中的一個(gè)練手項(xiàng)目,非常適合學(xué)習(xí)Kotlin的時(shí)候參考,需要的朋友可以參考下2021-09-09
Kotlin協(xié)程Context應(yīng)用使用示例詳解
這篇文章主要為大家介紹了Kotlin協(xié)程Context應(yīng)用使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

