Android嵌套滾動(dòng)和協(xié)調(diào)滾動(dòng)的多種實(shí)現(xiàn)方法
Android的嵌套滾動(dòng)的幾種實(shí)現(xiàn)方式
很多 Android 開(kāi)發(fā)者雖然做了幾年的開(kāi)發(fā),但是可能還是對(duì)滾動(dòng)的幾種方式不是很了解,本系列也不會(huì)涉及到底層滾動(dòng)原理,只是探討一下 Android 布局滾動(dòng)的幾種方式。
什么叫嵌套滾動(dòng)?什么叫協(xié)調(diào)滾動(dòng)?
只要是涉及到滾動(dòng)那必然父容器和子容器,按照原理來(lái)說(shuō)子容器先滾動(dòng),當(dāng)子容器滾不動(dòng)了再讓父容器滾動(dòng),或者先讓父容器滾動(dòng),父容器滾不動(dòng)了再讓子容器滾動(dòng),這種就叫嵌套滾動(dòng)。代表為 NestedScrollView 。
如果只是子容器滾動(dòng),父容器中的其他控件在子容器滾動(dòng)過(guò)程中做一些布局,透明度,動(dòng)畫等操作,這種叫協(xié)調(diào)滾動(dòng)。代表為 CoordinatorLayout 。
這里我們從嵌套滾動(dòng)的實(shí)現(xiàn)方式開(kāi)始講起。(不細(xì)講原理,本文只探討實(shí)現(xiàn)的方式與步驟?。?/p>
一、嵌套滾動(dòng) NestedScrollingParent/Child
最近看到一些文章又開(kāi)始講 NestedScrollingParent/Child
的嵌套滾動(dòng)了,這...屬實(shí)是懷舊了。
依稀記得大概是2017年左右吧,谷歌出了一個(gè) NestedScrollingParent/Child
嵌套滾動(dòng),當(dāng)時(shí)應(yīng)該是很轟動(dòng)的。Android 開(kāi)發(fā)者真的苦于嵌套滾動(dòng)久矣。
NestedScrolling
機(jī)制能夠讓父view和子view在滾動(dòng)時(shí)進(jìn)行配合,其基本流程如下:
- 當(dāng)子view開(kāi)始滾動(dòng)之前,可以通知父view,讓其先于自己進(jìn)行滾動(dòng);
- 子view自己進(jìn)行滾動(dòng)
- 子view滾動(dòng)之后,還可以通知父view繼續(xù)滾動(dòng)
要實(shí)現(xiàn)這樣的交互,父View需要實(shí)現(xiàn) NestedScrollingParent
接口,而子View需要實(shí)現(xiàn) NestedScrollingChild
接口。
作為一個(gè)可以嵌入 NestedScrollingChild
的父 View,需要實(shí)現(xiàn) NestedScrollingParent
,這個(gè)接口方法和 NestedScrollingChild
大致有一一對(duì)應(yīng)的關(guān)系。同樣,也有一個(gè) NestedScrollingParentHelper 輔助類來(lái)默默的幫助你實(shí)現(xiàn)和 Child 交互的邏輯。滑動(dòng)動(dòng)作是 Child 主動(dòng)發(fā)起,Parent 就收滑動(dòng)回調(diào)并作出響應(yīng)。
從上面的 Child 分析可知,滑動(dòng)開(kāi)始的調(diào)用 startNestedScroll(),Parent 收到 onStartNestedScroll() 回調(diào),決定是否需要配合 Child 一起進(jìn)行處理滑動(dòng),如果需要配合,還會(huì)回調(diào) onNestedScrollAccepted()。
每次滑動(dòng)前,Child 先詢問(wèn) Parent 是否需要滑動(dòng),即 dispatchNestedPreScroll(),這就回調(diào)到 Parent 的 onNestedPreScroll(),Parent 可以在這個(gè)回調(diào)中“劫持”掉 Child 的滑動(dòng),也就是先于 Child 滑動(dòng)。
Child 滑動(dòng)以后,會(huì)調(diào)用 onNestedScroll(),回調(diào)到 Parent 的 onNestedScroll(),這里就是 Child 滑動(dòng)后,剩下的給 Parent 處理,也就是 后于 Child 滑動(dòng)。
最后,滑動(dòng)結(jié)束,調(diào)用 onStopNestedScroll() 表示本次處理結(jié)束。
更詳細(xì)的教程大家可以看看鴻洋的文章。
這里我做一個(gè)簡(jiǎn)單的示例,后面的效果都是基于這個(gè)布局實(shí)現(xiàn)。
public class MyNestedScrollChild extends LinearLayout implements NestedScrollingChild { private NestedScrollingChildHelper mScrollingChildHelper; private final int[] offset = new int[2]; private final int[] consumed = new int[2]; private int lastY; private int mShowHeight; public MyNestedScrollChild(Context context) { super(context); } public MyNestedScrollChild(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //第一次測(cè)量,因?yàn)椴季治募懈叨仁莣rap_content,因此測(cè)量模式為ATMOST,即高度不能超過(guò)父控件的剩余空間 super.onMeasure(widthMeasureSpec, heightMeasureSpec); mShowHeight = getMeasuredHeight(); //第二次測(cè)量,對(duì)高度沒(méi)有任何限制,那么測(cè)量出來(lái)的就是完全展示內(nèi)容所需要的高度 heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override public boolean onTouchEvent(MotionEvent e) { switch (e.getAction()) { case MotionEvent.ACTION_DOWN: lastY = (int) e.getRawY(); break; case MotionEvent.ACTION_MOVE: int y = (int) (e.getRawY()); int dy = y - lastY; lastY = y; if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) //如果找到了支持嵌套滾動(dòng)的父類 && dispatchNestedPreScroll(0, dy, consumed, offset)) {//父類進(jìn)行了一部分滾動(dòng) int remain = dy - consumed[1];//獲取滾動(dòng)的剩余距離 if (remain != 0) { scrollBy(0, -remain); } } else { scrollBy(0, -dy); } } return true; } //scrollBy內(nèi)部會(huì)調(diào)用scrollTo //限制滾動(dòng)范圍 @Override public void scrollTo(int x, int y) { int MaxY = getMeasuredHeight() - mShowHeight; if (y > MaxY) { y = MaxY; } if (y < 0) { y = 0; } super.scrollTo(x, y); } private NestedScrollingChildHelper getScrollingChildHelper() { if (mScrollingChildHelper == null) { mScrollingChildHelper = new NestedScrollingChildHelper(this); mScrollingChildHelper.setNestedScrollingEnabled(true); } return mScrollingChildHelper; } @Override public void setNestedScrollingEnabled(boolean enabled) { getScrollingChildHelper().setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return getScrollingChildHelper().isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return getScrollingChildHelper().startNestedScroll(axes); } @Override public void stopNestedScroll() { getScrollingChildHelper().stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return getScrollingChildHelper().hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY); } }
定義Parent實(shí)現(xiàn)文本布局置頂效果:
public class MyNestedScrollParent extends LinearLayout implements NestedScrollingParent { private ImageView img; private TextView tv; private MyNestedScrollChild nsc; private NestedScrollingParentHelper mParentHelper; private int imgHeight; private int tvHeight; public MyNestedScrollParent(Context context) { super(context); init(); } public MyNestedScrollParent(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { mParentHelper = new NestedScrollingParentHelper(this); } //獲取子view @Override protected void onFinishInflate() { img = (ImageView) getChildAt(0); tv = (TextView) getChildAt(1); nsc = (MyNestedScrollChild) getChildAt(2); img.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (imgHeight <= 0) { imgHeight = img.getMeasuredHeight(); } } }); tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (tvHeight <= 0) { tvHeight = tv.getMeasuredHeight(); } } }); super.onFinishInflate(); } //在此可以判斷參數(shù)target是哪一個(gè)子view以及滾動(dòng)的方向,然后決定是否要配合其進(jìn)行嵌套滾動(dòng) @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { if (target instanceof MyNestedScrollChild) { return true; } return false; } @Override public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); } @Override public void onStopNestedScroll(View target) { mParentHelper.onStopNestedScroll(target); } //先于child滾動(dòng) //前3個(gè)為輸入?yún)?shù),最后一個(gè)是輸出參數(shù) @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { if (showImg(dy) || hideImg(dy)) {//如果需要顯示或隱藏圖片,即需要自己(parent)滾動(dòng) scrollBy(0, -dy);//滾動(dòng) consumed[1] = dy;//告訴child我消費(fèi)了多少 } } //后于child滾動(dòng) @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { } //返回值:是否消費(fèi)了fling @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return false; } //返回值:是否消費(fèi)了fling @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return false; } @Override public int getNestedScrollAxes() { return mParentHelper.getNestedScrollAxes(); } //-------------------------------------------------- //下拉的時(shí)候是否要向下滾動(dòng)以顯示圖片 public boolean showImg(int dy) { if (dy > 0) { if (getScrollY() > 0 && nsc.getScrollY() == 0) { return true; } } return false; } //上拉的時(shí)候,是否要向上滾動(dòng),隱藏圖片 public boolean hideImg(int dy) { if (dy < 0) { if (getScrollY() < imgHeight) { return true; } } return false; } //scrollBy內(nèi)部會(huì)調(diào)用scrollTo //限制滾動(dòng)范圍 @Override public void scrollTo(int x, int y) { if (y < 0) { y = 0; } if (y > imgHeight) { y = imgHeight; } super.scrollTo(x, y); } }
頁(yè)面的布局如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white"> <com.guadou.lib_baselib.view.titlebar.EasyTitleBar android:layout_width="match_parent" android:layout_height="wrap_content" app:Easy_title="NestedParent/Child的滾動(dòng)" /> <com.guadou.kt_demo.demo.demo8_recyclerview.scroll8.MyNestedScrollParent android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ImageView android:layout_width="match_parent" android:layout_height="150dp" android:contentDescription="我是測(cè)試的圖片" android:src="@mipmap/ic_launcher" /> <TextView android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:background="#ccc" android:text="我是測(cè)試的分割線" /> <com.guadou.kt_demo.demo.demo8_recyclerview.scroll8.MyNestedScrollChild android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/scroll_content" /> </com.guadou.kt_demo.demo.demo8_recyclerview.scroll8.MyNestedScrollChild> </com.guadou.kt_demo.demo.demo8_recyclerview.scroll8.MyNestedScrollParent> </LinearLayout>
看看效果:
二、嵌套滾動(dòng) NestedScrollView
NestedScrollingParent/Child
的定義也太過(guò)復(fù)雜了吧,如果只是一些簡(jiǎn)單的效果如 ScrollView 嵌套 LinearLayout 這樣的簡(jiǎn)單效果,我們直接可以使用 NestedScrollView
來(lái)實(shí)現(xiàn)
因此,我們可以簡(jiǎn)單的把 NestedScrollView 類比為 ScrollView,其作用就是作為控件父布局,從而具備嵌套滑動(dòng)功能。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:background="@color/white" android:orientation="vertical"> <com.guadou.lib_baselib.view.titlebar.EasyTitleBar android:layout_width="match_parent" android:layout_height="wrap_content" app:Easy_title="NestedScrollView的滾動(dòng)" /> <androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:layout_width="match_parent" android:layout_height="150dp" android:contentDescription="我是測(cè)試的圖片" android:src="@mipmap/ic_launcher" /> <TextView android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:background="#ccc" android:text="我是測(cè)試的分割線" /> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/scroll_content" /> </ScrollView> </LinearLayout> </androidx.core.widget.NestedScrollView> </LinearLayout>
效果:
三、嵌套滾動(dòng)-自定義布局
除了使用官方提供的方式,我們還能使用自定義View的方式,自己處理事件與監(jiān)聽(tīng)。
使用自定義ViewGroup的方式,添加全部的布局,并測(cè)量與排版,并且對(duì)事件做攔截處理。內(nèi)部是如LinearLayout的垂直布局,實(shí)現(xiàn)了 ScrollingView
支持滾動(dòng),并處理滾動(dòng)。有源碼,大概2800行代碼,這里就不方便貼出來(lái)了。
如何使用:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:background="@color/white" android:orientation="vertical"> <com.guadou.lib_baselib.view.titlebar.EasyTitleBar android:layout_width="match_parent" android:layout_height="wrap_content" app:Easy_title="自定義View實(shí)現(xiàn)的滾動(dòng)" /> <com.guadou.kt_demo.demo.demo8_recyclerview.scroll10.ConsecutiveScrollerLayout android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical"> <ImageView android:layout_width="match_parent" android:layout_height="150dp" android:contentDescription="我是測(cè)試的圖片" android:src="@mipmap/ic_launcher" /> <TextView app:layout_isSticky="true" //可以實(shí)現(xiàn)吸頂效果 android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:background="#ccc" android:text="我是測(cè)試的分割線" /> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/scroll_content" /> </ScrollView> </com.guadou.kt_demo.demo.demo8_recyclerview.scroll10.ConsecutiveScrollerLayout> </LinearLayout>
效果:
總結(jié)
其實(shí)嵌套滾動(dòng)要實(shí)現(xiàn)類似的效果,方式還有很多種,如自定義的ViewPager,自定義ListView,或者RecyclerView加上頭布局也能實(shí)現(xiàn)類似的效果。這里我只展示了基于 ScrollingView 自行滾動(dòng)的方式。
嵌套的滾動(dòng)主要方式就是這些,這些簡(jiǎn)單的效果我們用協(xié)調(diào)滾動(dòng),如 CoordinatorLayout
也能實(shí)現(xiàn)同樣的效果。后面會(huì)講一些協(xié)調(diào)滾動(dòng)的實(shí)現(xiàn)由幾種方式。
到此這篇關(guān)于Android嵌套滾動(dòng)和協(xié)調(diào)滾動(dòng)的多種實(shí)現(xiàn)方法的文章就介紹到這了,更多相關(guān)Android嵌套滾動(dòng)與協(xié)調(diào)滾動(dòng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Android studio3.6的JNI教程之ncnn之語(yǔ)義分割ENet
這篇文章主要介紹了基于Android studio3.6的JNI教程之ncnn之語(yǔ)義分割ENet的相關(guān)知識(shí),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2020-03-03淺談Android獲取ImageView上的圖片,和一個(gè)有可能遇到的問(wèn)題
下面小編就為大家?guī)?lái)一篇淺談Android獲取ImageView上的圖片,和一個(gè)有可能遇到的問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04Android AutoCompleteTextView控件使用實(shí)例
AutoCompleteTextView這個(gè)控件用于輸入框的自動(dòng)完成提示,非常適合搜索框等。它本質(zhì)上是個(gè)EditText,實(shí)際上它也是從EditText繼承的,使用起來(lái)也十分簡(jiǎn)單2014-04-04Android小程序?qū)崿F(xiàn)個(gè)人信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Android小程序?qū)崿F(xiàn)個(gè)人信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05Android Broadcast原理分析之registerReceiver詳解
這篇文章主要介紹了Android Broadcast原理分析之registerReceiver詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08Android創(chuàng)建外部lib庫(kù)及自定義View的圖文教程
這篇文章主要給大家介紹了關(guān)于Android創(chuàng)建外部lib庫(kù)及自定義View的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11Kotlin類的繼承實(shí)現(xiàn)詳細(xì)介紹
這篇文章主要介紹了Kotlin類的繼承,在Java中類的繼承默認(rèn)是繼承父類的方法和參數(shù)的,但是在kotlin中默認(rèn)是不繼承的,那么我們接下來(lái)來(lái)驗(yàn)證2022-09-09Android Studio中引入Lambda表達(dá)式的方法
這篇文章主要給大家介紹了在Android Studio中引入Lambda表達(dá)式的方法,文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家具有一定的參考價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-03-03Android學(xué)習(xí)筆記(一)環(huán)境安裝及第一個(gè)hello world
最近在學(xué)習(xí)安卓開(kāi)發(fā),記錄下環(huán)境安裝和第一個(gè)hello world的誕生過(guò)程,希望對(duì)大家有所幫助2014-07-07解決Android studio xml界面無(wú)法預(yù)覽問(wèn)題
這篇文章主要介紹了解決Android studio xml界面無(wú)法預(yù)覽問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03