Android嵌套滾動和協(xié)調滾動的多種實現(xiàn)方法
Android的嵌套滾動的幾種實現(xiàn)方式
很多 Android 開發(fā)者雖然做了幾年的開發(fā),但是可能還是對滾動的幾種方式不是很了解,本系列也不會涉及到底層滾動原理,只是探討一下 Android 布局滾動的幾種方式。
什么叫嵌套滾動?什么叫協(xié)調滾動?
只要是涉及到滾動那必然父容器和子容器,按照原理來說子容器先滾動,當子容器滾不動了再讓父容器滾動,或者先讓父容器滾動,父容器滾不動了再讓子容器滾動,這種就叫嵌套滾動。代表為 NestedScrollView 。
如果只是子容器滾動,父容器中的其他控件在子容器滾動過程中做一些布局,透明度,動畫等操作,這種叫協(xié)調滾動。代表為 CoordinatorLayout 。
這里我們從嵌套滾動的實現(xiàn)方式開始講起。(不細講原理,本文只探討實現(xiàn)的方式與步驟!)
一、嵌套滾動 NestedScrollingParent/Child
最近看到一些文章又開始講 NestedScrollingParent/Child
的嵌套滾動了,這...屬實是懷舊了。
依稀記得大概是2017年左右吧,谷歌出了一個 NestedScrollingParent/Child
嵌套滾動,當時應該是很轟動的。Android 開發(fā)者真的苦于嵌套滾動久矣。
NestedScrolling
機制能夠讓父view和子view在滾動時進行配合,其基本流程如下:
- 當子view開始滾動之前,可以通知父view,讓其先于自己進行滾動;
- 子view自己進行滾動
- 子view滾動之后,還可以通知父view繼續(xù)滾動
要實現(xiàn)這樣的交互,父View需要實現(xiàn) NestedScrollingParent
接口,而子View需要實現(xiàn) NestedScrollingChild
接口。
作為一個可以嵌入 NestedScrollingChild
的父 View,需要實現(xiàn) NestedScrollingParent
,這個接口方法和 NestedScrollingChild
大致有一一對應的關系。同樣,也有一個 NestedScrollingParentHelper 輔助類來默默的幫助你實現(xiàn)和 Child 交互的邏輯。滑動動作是 Child 主動發(fā)起,Parent 就收滑動回調并作出響應。
從上面的 Child 分析可知,滑動開始的調用 startNestedScroll(),Parent 收到 onStartNestedScroll() 回調,決定是否需要配合 Child 一起進行處理滑動,如果需要配合,還會回調 onNestedScrollAccepted()。
每次滑動前,Child 先詢問 Parent 是否需要滑動,即 dispatchNestedPreScroll(),這就回調到 Parent 的 onNestedPreScroll(),Parent 可以在這個回調中“劫持”掉 Child 的滑動,也就是先于 Child 滑動。
Child 滑動以后,會調用 onNestedScroll(),回調到 Parent 的 onNestedScroll(),這里就是 Child 滑動后,剩下的給 Parent 處理,也就是 后于 Child 滑動。
最后,滑動結束,調用 onStopNestedScroll() 表示本次處理結束。
更詳細的教程大家可以看看鴻洋的文章。
這里我做一個簡單的示例,后面的效果都是基于這個布局實現(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) { //第一次測量,因為布局文件中高度是wrap_content,因此測量模式為ATMOST,即高度不能超過父控件的剩余空間 super.onMeasure(widthMeasureSpec, heightMeasureSpec); mShowHeight = getMeasuredHeight(); //第二次測量,對高度沒有任何限制,那么測量出來的就是完全展示內容所需要的高度 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) //如果找到了支持嵌套滾動的父類 && dispatchNestedPreScroll(0, dy, consumed, offset)) {//父類進行了一部分滾動 int remain = dy - consumed[1];//獲取滾動的剩余距離 if (remain != 0) { scrollBy(0, -remain); } } else { scrollBy(0, -dy); } } return true; } //scrollBy內部會調用scrollTo //限制滾動范圍 @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實現(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是哪一個子view以及滾動的方向,然后決定是否要配合其進行嵌套滾動 @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滾動 //前3個為輸入參數(shù),最后一個是輸出參數(shù) @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { if (showImg(dy) || hideImg(dy)) {//如果需要顯示或隱藏圖片,即需要自己(parent)滾動 scrollBy(0, -dy);//滾動 consumed[1] = dy;//告訴child我消費了多少 } } //后于child滾動 @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { } //返回值:是否消費了fling @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return false; } //返回值:是否消費了fling @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return false; } @Override public int getNestedScrollAxes() { return mParentHelper.getNestedScrollAxes(); } //-------------------------------------------------- //下拉的時候是否要向下滾動以顯示圖片 public boolean showImg(int dy) { if (dy > 0) { if (getScrollY() > 0 && nsc.getScrollY() == 0) { return true; } } return false; } //上拉的時候,是否要向上滾動,隱藏圖片 public boolean hideImg(int dy) { if (dy < 0) { if (getScrollY() < imgHeight) { return true; } } return false; } //scrollBy內部會調用scrollTo //限制滾動范圍 @Override public void scrollTo(int x, int y) { if (y < 0) { y = 0; } if (y > imgHeight) { y = imgHeight; } super.scrollTo(x, 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的滾動" /> <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="我是測試的圖片" android:src="@mipmap/ic_launcher" /> <TextView android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:background="#ccc" android:text="我是測試的分割線" /> <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>
看看效果:
二、嵌套滾動 NestedScrollView
NestedScrollingParent/Child
的定義也太過復雜了吧,如果只是一些簡單的效果如 ScrollView 嵌套 LinearLayout 這樣的簡單效果,我們直接可以使用 NestedScrollView
來實現(xiàn)
因此,我們可以簡單的把 NestedScrollView 類比為 ScrollView,其作用就是作為控件父布局,從而具備嵌套滑動功能。
<?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的滾動" /> <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="我是測試的圖片" android:src="@mipmap/ic_launcher" /> <TextView android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:background="#ccc" android:text="我是測試的分割線" /> <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>
效果:
三、嵌套滾動-自定義布局
除了使用官方提供的方式,我們還能使用自定義View的方式,自己處理事件與監(jiān)聽。
使用自定義ViewGroup的方式,添加全部的布局,并測量與排版,并且對事件做攔截處理。內部是如LinearLayout的垂直布局,實現(xiàn)了 ScrollingView
支持滾動,并處理滾動。有源碼,大概2800行代碼,這里就不方便貼出來了。
如何使用:
<?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實現(xiàn)的滾動" /> <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="我是測試的圖片" android:src="@mipmap/ic_launcher" /> <TextView app:layout_isSticky="true" //可以實現(xiàn)吸頂效果 android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:background="#ccc" android:text="我是測試的分割線" /> <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>
效果:
總結
其實嵌套滾動要實現(xiàn)類似的效果,方式還有很多種,如自定義的ViewPager,自定義ListView,或者RecyclerView加上頭布局也能實現(xiàn)類似的效果。這里我只展示了基于 ScrollingView 自行滾動的方式。
嵌套的滾動主要方式就是這些,這些簡單的效果我們用協(xié)調滾動,如 CoordinatorLayout
也能實現(xiàn)同樣的效果。后面會講一些協(xié)調滾動的實現(xiàn)由幾種方式。
到此這篇關于Android嵌套滾動和協(xié)調滾動的多種實現(xiàn)方法的文章就介紹到這了,更多相關Android嵌套滾動與協(xié)調滾動內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
基于Android studio3.6的JNI教程之ncnn之語義分割ENet
這篇文章主要介紹了基于Android studio3.6的JNI教程之ncnn之語義分割ENet的相關知識,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值 ,需要的朋友可以參考下2020-03-03淺談Android獲取ImageView上的圖片,和一個有可能遇到的問題
下面小編就為大家?guī)硪黄獪\談Android獲取ImageView上的圖片,和一個有可能遇到的問題。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-04-04Android AutoCompleteTextView控件使用實例
AutoCompleteTextView這個控件用于輸入框的自動完成提示,非常適合搜索框等。它本質上是個EditText,實際上它也是從EditText繼承的,使用起來也十分簡單2014-04-04Android小程序實現(xiàn)個人信息管理系統(tǒng)
這篇文章主要為大家詳細介紹了Android小程序實現(xiàn)個人信息管理系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-05-05Android Broadcast原理分析之registerReceiver詳解
這篇文章主要介紹了Android Broadcast原理分析之registerReceiver詳解,本篇文章通過簡要的案例,講解了該項技術的了解與使用,以下就是詳細內容,需要的朋友可以參考下2021-08-08Android創(chuàng)建外部lib庫及自定義View的圖文教程
這篇文章主要給大家介紹了關于Android創(chuàng)建外部lib庫及自定義View的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2018-11-11Android學習筆記(一)環(huán)境安裝及第一個hello world
最近在學習安卓開發(fā),記錄下環(huán)境安裝和第一個hello world的誕生過程,希望對大家有所幫助2014-07-07