這篇文章主要介紹了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進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。