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)只需要重寫(xiě)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í)候可以重寫(xiě)ViewPager的onMeasure,重新測(cè)量ViewPager的寬高。也可以換用ViewPager2
public class CustomViewPager extends ViewPager { ... @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //重寫(xiě)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吸頂,只需要重寫(xiě)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); }
具體的事件傳遞流程如下圖:
因此我們可以重寫(xiě)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-09Android獲取SDcard目錄及創(chuàng)建文件夾的方法
今天小編就為大家分享一篇Android獲取SDcard目錄及創(chuàng)建文件夾的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08Android開(kāi)發(fā)之完成登陸界面的數(shù)據(jù)保存回顯操作實(shí)例
這篇文章主要介紹了Android開(kāi)發(fā)之完成登陸界面的數(shù)據(jù)保存回顯操作實(shí)現(xiàn)方法,結(jié)合完整實(shí)例形式較為詳細(xì)的分析了Android針對(duì)登錄數(shù)據(jù)的保存及回顯操作技巧,需要的朋友可以參考下2015-12-12Android Studio中生成aar文件及本地方式使用aar文件的方法
這篇文章給大家講解Android Studio中生成aar文件以及本地方式使用aar文件的方法,也就是說(shuō) *.jar 與 *.aar 的生成與*.aar導(dǎo)入項(xiàng)目方法,本文給大家介紹的非常詳細(xì),需要的朋友參考下吧2017-12-12RecyclerView實(shí)現(xiàn)查看更多及收起
這篇文章主要為大家詳細(xì)介紹了RecyclerView實(shí)現(xiàn)查看更多及收起,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01Flutter實(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-04Jetpack?Compose?實(shí)現(xiàn)一個(gè)圖片選擇框架功能
這篇文章主要介紹了Jetpack?Compose?實(shí)現(xiàn)一個(gè)圖片選擇框架,本文通過(guò)實(shí)例代碼圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06