NestScrollView嵌套R(shí)ecyclerView實(shí)現(xiàn)淘寶首頁(yè)滑動(dòng)效果
一.概述
本文主要實(shí)現(xiàn)淘寶首頁(yè)嵌套滑動(dòng),中間tab吸頂效果,以及介紹NestScrollView嵌套R(shí)ecyclerView處理滑動(dòng)沖突的方法,淘寶首頁(yè)的效果圖如下:

二.開(kāi)搞
首先我們通過(guò)一張圖來(lái)分析下頁(yè)面的布局結(jié)構(gòu):

先把最基礎(chǔ)的頁(yè)面搭出來(lái),禁用Recycler滑動(dòng)只需要重寫onInterceptTouchEvent、onTouchEvent返回值都設(shè)為false即可:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activiy.ViewPagerActivity"
android:background="#f2f2f2">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.aykj.nestscrolldemo.widget.NoScrollRecyclerView
android:id="@+id/top_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="#e0e0e0"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="#e0e0e0"/>
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
public class ViewPagerActivity extends AppCompatActivity {
private List<String> topDatas = new ArrayList<>();
private List<String> tabTitles = new ArrayList<>();
ActivityViewPagerBinding viewBinding;
private RecyclerAdapter topAdapter;
private DividerItemDecoration divider;
private TabFragmentAdapter pagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewBinding = ActivityViewPagerBinding.inflate(LayoutInflater.from(this));
setContentView(viewBinding.getRoot());
initDatas();
initView();
}
private void initDatas() {
topDatas.clear();
for(int i=0; i<5; i++) {
topDatas.add("top item " + (i + 1));
}
tabTitles.clear();
tabTitles.add("tab1");
tabTitles.add("tab2");
tabTitles.add("tab3");
}
private void initView() {
//init topRecycler
divider = new DividerItemDecoration(this, LinearLayout.VERTICAL);
divider.setDrawable(new ColorDrawable(Color.parseColor("#ffe0e0e0")));
viewBinding.topRecyclerView.setLayoutManager(new LinearLayoutManager(this));
viewBinding.topRecyclerView.addItemDecoration(divider);
topAdapter = new RecyclerAdapter(this, topDatas);
viewBinding.topRecyclerView.setAdapter(topAdapter);
//initTabs with ViewPager
pagerAdapter = new TabFragmentAdapter(getSupportFragmentManager(), tabTitles);
viewBinding.viewPager.setAdapter(pagerAdapter);
viewBinding.tabView.setupWithViewPager(viewBinding.viewPager);
viewBinding.tabView.setTabMode(TabLayout.MODE_FIXED);
}
}

可以看到ViewPager沒(méi)有正常顯示出來(lái),這個(gè)時(shí)候可以重寫ViewPager的onMeasure,重新測(cè)量ViewPager的寬高。也可以換用ViewPager2
public class CustomViewPager extends ViewPager {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//重寫ViewPager的onMeasure
int width = 0;
int height = 0;
for(int i=0; i<getChildCount(); i++) {
View childView = getChildAt(0);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
width = Math.max(width, childView.getMeasuredWidth());
height = Math.max(height, childView.getMeasuredHeight());
}
height += getPaddingTop() + getPaddingBottom();
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}

從上面的效果圖可以看到,ViewPager能正常顯示出來(lái)了,但是在RecyclerView上滑動(dòng)的時(shí)候發(fā)現(xiàn),RecyclerView滑動(dòng)完了之后,ScrollView才會(huì)滑動(dòng),并且ScrollView只滑動(dòng)了一小段距離,這是因?yàn)槭紫萐crollView是不支持嵌套滑動(dòng)的
ScrollView內(nèi)部的第一個(gè)子View中所有子View的高度 = 頂部的RecyclerView高度 + TabLayout高度 + 底部RecyclerView中所有可見(jiàn)Item的高度
這個(gè)高度只比ScrollView的高度大一點(diǎn)點(diǎn)導(dǎo)致的。為了實(shí)現(xiàn)嵌套滑動(dòng)需要使用NestedScrollView,接下來(lái)把ScrollView替換成NestedScrollView:

整個(gè)頁(yè)面可以滑完,看起來(lái)就像是兩個(gè)Scroll被合并成一個(gè)了,如果單單只是實(shí)現(xiàn)上面的界面效果,我們完全可以使用一個(gè)RecyclerView即可,但是Tab沒(méi)有吸頂,這是因?yàn)?
ScrollView內(nèi)部的第一個(gè)子View中所有子View的高度 = 頂部的RecyclerView高度 + TabLayout高度 + 底部RecyclerView中所有Item的高度
要實(shí)現(xiàn)Tab吸頂,只需要重寫NestedScrollView的onMeasue方法,將TabLayout的高度和ViewPager的高度之和設(shè)置為NestedScrollView的高度:

public class StickyScrollLayout extends NestedScrollView {
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int count = getChildCount();
if(count == 1) {
View firstChild = getChildAt(0);
if(firstChild != null && firstChild instanceof ViewGroup) {
int childCount = ((ViewGroup) firstChild).getChildCount();
if(childCount > 1) {
topView = ((ViewGroup) firstChild).getChildAt(0);
contentView = ((ViewGroup) firstChild).getChildAt(1);
}
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if(contentView != null) {
ViewGroup.LayoutParams contentLayoutParams = contentView.getLayoutParams();
contentLayoutParams.height = getMeasuredHeight();
contentView.setLayoutParams(contentLayoutParams);
}
}
}

此時(shí)TabLayout可以吸頂了
三.處理嵌套滑動(dòng)
從上圖中可以看出,當(dāng)我們?cè)赗ecyclerView上向上滑動(dòng)時(shí),需要等RecyclerView滑動(dòng)完,外部的NestedScrollView才開(kāi)始滑動(dòng),而我們希望NestedScrollView中頂部的RecyclerView滑完之后,底部的RecyclerView才開(kāi)始滑動(dòng),這是為什么呢?
查看NestedScrollView和RecyclerView的源碼,可以知道NestedScrollView和RecyclerView分別實(shí)現(xiàn)了NestedScrollingParent3,NestedScrollingChild3接口,分別用來(lái)表示嵌套滑動(dòng)的父View、嵌套滑動(dòng)的子View,當(dāng)我們的手指在RecyclerView上滑動(dòng)時(shí),滑動(dòng)事件會(huì)從上往下分發(fā)至RecyclerView的onTouchEvent中,RecyclerView會(huì)依次響應(yīng)ACTION_DOWN、ACTION_MOVE、ACTION_UP
RecyclerView在處理ACTION_DOWN時(shí)的關(guān)鍵代碼如下:
public boolean onTouchEvent(MotionEvent e) {
switch (action) {
case MotionEvent.ACTION_DOWN: {
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
}
return true;
}
當(dāng)手指按下屏幕時(shí)會(huì)調(diào)用其作為NestedScrollingChild的實(shí)現(xiàn)方法startNestedScroll,在startNestedScroll的具體實(shí)現(xiàn)中,會(huì)一級(jí)一級(jí)的往上查找是否有NestedScrollingParent,如果有,會(huì)調(diào)用NestedScrollingParent的onStartNestedScroll方法通知它我即將要開(kāi)始滑動(dòng)了,然后NestedScrollingParent會(huì)調(diào)用onNestedScrollAccepted繼續(xù)傳遞給上層的NestedScrollingParent,此處的NestedScrollingParent整好由NestedScrollView來(lái)充當(dāng),而NestedScrollView的上層已經(jīng)找不到NestedScrollingParent了,時(shí)間傳給NestedScrollView之后就中斷了。
緊接著處理一系列的ACTION_MOVE:
public boolean onTouchEvent(MotionEvent e) {
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// Scroll has initiated, prevent parents from intercepting
getParent().requestDisallowInterceptTouchEvent(true);
}
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
} break;
}
return true;
}
RecyclerView接收到ACTION_MOVE后,首先會(huì)調(diào)用其作為NestedScrollingChild的實(shí)現(xiàn)方法dispatchNestedPreScroll,在dispatchNestedPreScroll的具體實(shí)現(xiàn)中,會(huì)一級(jí)一級(jí)的往上查找是否有NestedScrollingParent,如果有,會(huì)調(diào)用NestedScrollingParent的dispatchNestedPreScroll,緊接著調(diào)用NestedScrollView的onNestedPreScroll,來(lái)告訴NestedScrollView我即將要滑動(dòng) xxx 距離,你需不需要滑動(dòng),在NestedScrollView的onNestedPreScroll方法中并不會(huì)去響應(yīng)滑動(dòng),又會(huì)把自己作為一個(gè)NestedScrollingChild,把事件繼續(xù)往上傳遞,而在NestedScrollView的上層已經(jīng)沒(méi)有可以處理嵌套滑動(dòng)的NestedScrollingParent了
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
int type) {
dispatchNestedPreScroll(dx, dy, consumed, null, type);
}
具體的事件傳遞流程如下圖:

因此我們可以重寫NestedScrollView的onNestedPreScroll方法來(lái)使NestedScrollView滑動(dòng)
public class StickyNestedScrollLayout extends NestedScrollView {
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int count = getChildCount();
if(count == 1) {
View firstChild = getChildAt(0);
if(firstChild != null && firstChild instanceof ViewGroup) {
int childCount = ((ViewGroup) firstChild).getChildCount();
if(childCount > 1) {
topView = ((ViewGroup) firstChild).getChildAt(0);
contentView = ((ViewGroup) firstChild).getChildAt(1);
}
}
}
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
boolean topIsShow = getScrollY() >=0 && getScrollY() < topView.getHeight();
if(topIsShow) {
scrollBy(0, dy);
} else {
super.onNestedPreScroll(target, dx, dy, consumed, type);
}
}
}

此時(shí)NestedScrollView能滑動(dòng)了,但是NestedScrollView滑動(dòng)的同時(shí),RecyclerView也會(huì)跟著滑動(dòng),這是為什么呢?
在RecyclerView的dispatchNestedPreScroll方法具體實(shí)現(xiàn)中,有這樣一段代碼
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
//consumed[0]、consumed[1]的值仍為0
return consumed[0] != 0 || consumed[1] != 0;//返回false
}
}
return false;
}
再結(jié)合RecyclerView的ACTION_MOVE來(lái)看:
public boolean onTouchEvent(MotionEvent e) {
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
//dispatchNestedPreScroll返回了false,此處的if語(yǔ)句不會(huì)執(zhí)行,因此RecyclerView也會(huì)滑動(dòng)
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// Scroll has initiated, prevent parents from intercepting
getParent().requestDisallowInterceptTouchEvent(true);
}
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
} break;
}
return true;
}
因此,我們,在NestedScrollView的onNestedPreScroll方法中,處理完滑動(dòng)后,通過(guò)consumed告訴RecyclerView我滑動(dòng)了多少,這樣
RecyclerView會(huì)重新設(shè)置dx、dy的值,因此RecyclerView就不會(huì)跟著滑動(dòng)了
public class StickyNestedScrollLayout extends NestedScrollView {
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int count = getChildCount();
if(count == 1) {
View firstChild = getChildAt(0);
if(firstChild != null && firstChild instanceof ViewGroup) {
int childCount = ((ViewGroup) firstChild).getChildCount();
if(childCount > 1) {
topView = ((ViewGroup) firstChild).getChildAt(0);
contentView = ((ViewGroup) firstChild).getChildAt(1);
}
}
}
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
boolean topIsShow = getScrollY() >=0 && getScrollY() < topView.getHeight();
if(topIsShow) {
scrollBy(0, dy);
//告訴RecyclerView,我滑動(dòng)了多少距離
consumed[1] = dy;
} else {
super.onNestedPreScroll(target, dx, dy, consumed, type);
}
}
}

四.實(shí)現(xiàn)慣性滑動(dòng)
實(shí)現(xiàn)思路:
記錄父控件慣性滑動(dòng)的速度判斷NestedScrollView是否滾動(dòng)到底部,若滾動(dòng)到底部,判斷子控件是否需要繼續(xù)滾動(dòng)滾動(dòng)將慣性滑動(dòng)的速度轉(zhuǎn)化成距離,計(jì)算子控件應(yīng)滑的距離 = 慣性距離 - 父控件已滑動(dòng)距離,并將子控件應(yīng)滑的距離轉(zhuǎn)化成速度交給子控件進(jìn)行慣性滑動(dòng)
1.記錄父控件慣性滑動(dòng)的速度
public void fling(int velocityY) {
super.fling(velocityY);
if (velocityY <= 0) {
mVelocityY = 0;
} else {
mVelocityY = velocityY;
}
}
2.判斷NestedScrollView是否滾動(dòng)到底部,若滾動(dòng)到底部,判斷子控件是否需要繼續(xù)滾動(dòng)
@Override
protected void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY);
/*
* scrollY == 0 即還未滾動(dòng)
* scrollY == getChildAt(0).getMeasuredHeight() - v.getMeasuredHeight()即滾動(dòng)到底部了
*/
//判斷NestedScrollView是否滾動(dòng)到底部,若滾動(dòng)到底部,判斷子控件是否需要繼續(xù)滾動(dòng)
if (scrollY == getChildAt(0).getMeasuredHeight() - this.getMeasuredHeight()) {
dispatchChildFling();
}
//累計(jì)自身滾動(dòng)的距離
mConsumedY += scrollY - oldScrollY;
}
3.將慣性滑動(dòng)的速度轉(zhuǎn)化成距離,計(jì)算子控件應(yīng)滑的距離 = 慣性距離 - 父控件已滑動(dòng)距離,并將子控件應(yīng)滑的距離轉(zhuǎn)化成速度交給子控件進(jìn)行慣性滑動(dòng)
private void dispatchChildFling() {
if(mFlingHelper == null) {
mFlingHelper = new FlingHelper(getContext());
}
if (mVelocityY != 0) {
//將慣性滑動(dòng)速度轉(zhuǎn)化成距離
double distance = mFlingHelper.getSplineFlingDistance(mVelocityY);
//計(jì)算子控件應(yīng)該滑動(dòng)的距離 = 慣性滑動(dòng)距離 - 已滑距離
if (distance > mConsumedY) {
RecyclerView recyclerView = getChildRecyclerView(mContentView);
if (recyclerView != null) {
//將剩余滑動(dòng)距離轉(zhuǎn)化成速度交給子控件進(jìn)行慣性滑動(dòng)
int velocityY = mFlingHelper.getVelocityByDistance(distance - mConsumedY);
recyclerView.fling(0, velocityY);
}
}
}
mConsumedY = 0;
mVelocityY = 0;
}
//遞歸獲取子控件RecyclerView
private RecyclerView getChildRecyclerView(ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View view = viewGroup.getChildAt(i);
if (view instanceof RecyclerView && Objects.requireNonNull(((RecyclerView) view).getLayoutManager()).canScrollVertically()) {
return (RecyclerView) view;
} else if (viewGroup.getChildAt(i) instanceof ViewGroup) {
RecyclerView childRecyclerView = getChildRecyclerView((ViewGroup) viewGroup.getChildAt(i));
if (childRecyclerView != null && Objects.requireNonNull((childRecyclerView).getLayoutManager()).canScrollVertically()) {
return childRecyclerView;
}
}
}
return null;
}

到此這篇關(guān)于NestScrollView嵌套R(shí)ecyclerView實(shí)現(xiàn)淘寶首頁(yè)滑動(dòng)效果的文章就介紹到這了,更多相關(guān)NestScrollView嵌套R(shí)ecyclerView內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android 程序申請(qǐng)權(quán)限注意事項(xiàng)
本主要介紹Android 程序申請(qǐng)權(quán)限注意事項(xiàng),這里整理了相關(guān)資料,并詳細(xì)說(shuō)明如何避免開(kāi)發(fā)的程序支持設(shè)備減少,有需要的小伙伴可以參考下2016-09-09
Android獲取SDcard目錄及創(chuàng)建文件夾的方法
今天小編就為大家分享一篇Android獲取SDcard目錄及創(chuàng)建文件夾的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08
Android開(kāi)發(fā)之完成登陸界面的數(shù)據(jù)保存回顯操作實(shí)例
這篇文章主要介紹了Android開(kāi)發(fā)之完成登陸界面的數(shù)據(jù)保存回顯操作實(shí)現(xiàn)方法,結(jié)合完整實(shí)例形式較為詳細(xì)的分析了Android針對(duì)登錄數(shù)據(jù)的保存及回顯操作技巧,需要的朋友可以參考下2015-12-12
Android Studio中生成aar文件及本地方式使用aar文件的方法
這篇文章給大家講解Android Studio中生成aar文件以及本地方式使用aar文件的方法,也就是說(shuō) *.jar 與 *.aar 的生成與*.aar導(dǎo)入項(xiàng)目方法,本文給大家介紹的非常詳細(xì),需要的朋友參考下吧2017-12-12
RecyclerView實(shí)現(xiàn)查看更多及收起
這篇文章主要為大家詳細(xì)介紹了RecyclerView實(shí)現(xiàn)查看更多及收起,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01
Flutter實(shí)現(xiàn)自定義下拉選擇框的示例詳解
在一些列表頁(yè)面中,我們經(jīng)常會(huì)有上方篩選項(xiàng)的的需求,點(diǎn)擊出現(xiàn)一個(gè)下拉菜單,而在Flutter中,并沒(méi)有現(xiàn)成的這樣的組件,所以最好我們可以自己做一個(gè)。本文將利用Flutter實(shí)現(xiàn)自定義下拉選擇框,需要的可以參考一下2022-04-04
Jetpack?Compose?實(shí)現(xiàn)一個(gè)圖片選擇框架功能
這篇文章主要介紹了Jetpack?Compose?實(shí)現(xiàn)一個(gè)圖片選擇框架,本文通過(guò)實(shí)例代碼圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06

