您好,登錄后才能下訂單哦!
這篇文章主要介紹了Android嵌套滾動和協調滾如何實現的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Android嵌套滾動和協調滾如何實現文章都會有所收獲,下面我們一起來看看吧。
什么叫嵌套滾動?什么叫協調滾動?
只要是涉及到滾動那必然父容器和子容器,按照原理來說子容器先滾動,當子容器滾不動了再讓父容器滾動,或者先讓父容器滾動,父容器滾不動了再讓子容器滾動,這種就叫嵌套滾動。代表為 NestedScrollView 。
如果只是子容器滾動,父容器中的其他控件在子容器滾動過程中做一些布局,透明度,動畫等操作,這種叫協調滾動。代表為 CoordinatorLayout 。
這里我們從嵌套滾動的實現方式開始講起。
最近看到一些文章又開始講 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>
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>
關于“Android嵌套滾動和協調滾如何實現”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Android嵌套滾動和協調滾如何實現”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。