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)畫(huà)等操作,這種叫協(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)來(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)的父類(lèi)
&& dispatchNestedPreScroll(0, dy, consumed, offset)) {//父類(lèi)進(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 類(lèi)比為 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)類(lèi)似的效果,方式還有很多種,如自定義的ViewPager,自定義ListView,或者RecyclerView加上頭布局也能實(shí)現(xiàn)類(lèi)似的效果。這里我只展示了基于 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自定義View實(shí)現(xiàn)九宮格圖形解鎖(Kotlin版)
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)九宮格圖形解鎖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
在Android Studio中設(shè)置Button透明度的方法詳解
本文將介紹在Android Studio中如何設(shè)置Button的透明度,首先,我們將展示實(shí)現(xiàn)該功能的整個(gè)流程,并使用表格列出每個(gè)步驟,然后,我們將詳細(xì)說(shuō)明每個(gè)步驟需要做什么,并提供相應(yīng)的代碼和注釋,需要的朋友可以參考下2023-09-09
C#之Android手機(jī)App開(kāi)發(fā)
這篇文章主要為大家詳細(xì)介紹了C#之Android手機(jī)App開(kāi)發(fā),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-06-06
Android 自定義SeekBar動(dòng)態(tài)改變硬件音量大小實(shí)現(xiàn)和音量鍵的同步(推薦)
這篇文章主要介紹了 Android 自定義SeekBar動(dòng)態(tài)改變硬件音量大小實(shí)現(xiàn)和音量鍵的同步效果,整段代碼簡(jiǎn)單易懂,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-01-01
Android開(kāi)發(fā)之圓角矩形創(chuàng)建工具RoundRect類(lèi)定義與用法分析
這篇文章主要介紹了Android開(kāi)發(fā)之圓角矩形創(chuàng)建工具RoundRect類(lèi)定義與用法,結(jié)合完整實(shí)例形式分析了Android圓角矩形工具類(lèi)的定義與簡(jiǎn)單使用技巧,需要的朋友可以參考下2018-01-01
Android自定義控件實(shí)現(xiàn)可多選課程日歷CalendarView
這篇文章主要為大家詳細(xì)介紹了Android自定義控件實(shí)現(xiàn)可多選課程日歷CalendarView,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05
快速解決設(shè)置Android 23.0以上版本對(duì)SD卡的讀寫(xiě)權(quán)限無(wú)效的問(wèn)題
今天小編就為大家分享一篇快速解決設(shè)置Android 23.0以上版本對(duì)SD卡的讀寫(xiě)權(quán)限無(wú)效的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08
Android編程實(shí)現(xiàn)在Activity中操作刷新另外一個(gè)Activity數(shù)據(jù)列表的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)在Activity中操作刷新另外一個(gè)Activity數(shù)據(jù)列表的方法,結(jié)合具體實(shí)例形式分析了2種常用的Activity交互實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-06-06

