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

