Android嵌套滾動和協調滾動的多種實現方法
Android的嵌套滾動的幾種實現方式
很多 Android 開發者雖然做瞭幾年的開發,但是可能還是對滾動的幾種方式不是很瞭解,本系列也不會涉及到底層滾動原理,隻是探討一下 Android 佈局滾動的幾種方式。
什麼叫嵌套滾動?什麼叫協調滾動?
隻要是涉及到滾動那必然父容器和子容器,按照原理來說子容器先滾動,當子容器滾不動瞭再讓父容器滾動,或者先讓父容器滾動,父容器滾不動瞭再讓子容器滾動,這種就叫嵌套滾動。代表為 NestedScrollView 。
如果隻是子容器滾動,父容器中的其他控件在子容器滾動過程中做一些佈局,透明度,動畫等操作,這種叫協調滾動。代表為 CoordinatorLayout 。
這裡我們從嵌套滾動的實現方式開始講起。(不細講原理,本文隻探討實現的方式與步驟!)
一、嵌套滾動 NestedScrollingParent/Child
最近看到一些文章又開始講 NestedScrollingParent/Child
的嵌套滾動瞭,這…屬實是懷舊瞭。
依稀記得大概是2017年左右吧,谷歌出瞭一個 NestedScrollingParent/Child
嵌套滾動,當時應該是很轟動的。Android 開發者真的苦於嵌套滾動久矣。
NestedScrolling
機制能夠讓父view和子view在滾動時進行配合,其基本流程如下:
- 當子view開始滾動之前,可以通知父view,讓其先於自己進行滾動;
- 子view自己進行滾動
- 子view滾動之後,還可以通知父view繼續滾動
要實現這樣的交互,父View需要實現 NestedScrollingParent
接口,而子View需要實現 NestedScrollingChild
接口。
作為一個可以嵌入 NestedScrollingChild
的父 View,需要實現 NestedScrollingParent
,這個接口方法和 NestedScrollingChild
大致有一一對應的關系。同樣,也有一個 NestedScrollingParentHelper 輔助類來默默的幫助你實現和 Child 交互的邏輯。滑動動作是 Child 主動發起,Parent 就收滑動回調並作出響應。
-
從上面的 Child 分析可知,滑動開始的調用 startNestedScroll(),Parent 收到 onStartNestedScroll() 回調,決定是否需要配合 Child 一起進行處理滑動,如果需要配合,還會回調 onNestedScrollAccepted()。
-
每次滑動前,Child 先詢問 Parent 是否需要滑動,即 dispatchNestedPreScroll(),這就回調到 Parent 的 onNestedPreScroll(),Parent 可以在這個回調中“劫持”掉 Child 的滑動,也就是先於 Child 滑動。
-
Child 滑動以後,會調用 onNestedScroll(),回調到 Parent 的 onNestedScroll(),這裡就是 Child 滑動後,剩下的給 Parent 處理,也就是 後於 Child 滑動。
-
最後,滑動結束,調用 onStopNestedScroll() 表示本次處理結束。
更詳細的教程大傢可以看看鴻洋的文章。
這裡我做一個簡單的示例,後面的效果都是基於這個佈局實現。
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實現文本佈局置頂效果:
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(); } //在此可以判斷參數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個為輸入參數,最後一個是輸出參數 @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
來實現
因此,我們可以簡單的把 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的方式,自己處理事件與監聽。
使用自定義ViewGroup的方式,添加全部的佈局,並測量與排版,並且對事件做攔截處理。內部是如LinearLayout的垂直佈局,實現瞭 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實現的滾動" /> <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" //可以實現吸頂效果 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>
效果:
總結
其實嵌套滾動要實現類似的效果,方式還有很多種,如自定義的ViewPager,自定義ListView,或者RecyclerView加上頭佈局也能實現類似的效果。這裡我隻展示瞭基於 ScrollingView 自行滾動的方式。
嵌套的滾動主要方式就是這些,這些簡單的效果我們用協調滾動,如 CoordinatorLayout
也能實現同樣的效果。後面會講一些協調滾動的實現由幾種方式。
到此這篇關於Android嵌套滾動和協調滾動的多種實現方法的文章就介紹到這瞭,更多相關Android嵌套滾動與協調滾動內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Android實現背景圖片輪播
- Android seekbar實現可拖動進度條
- Android移動應用開發指南之六種佈局詳解
- Android實現左側滑動菜單
- Android文本視圖TextView實現跑馬燈效果