Android嵌套滚动与协调滚动的几种实现方式
Posted bug樱樱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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>
看看效果:
[图片上传失败…(image-ea904b-1655281923803)]
二、嵌套滚动 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嵌套滚动与协调滚动的几种实现方式的主要内容,如果未能解决你的问题,请参考以下文章
Recyclerview 滚动在嵌套滚动视图中的片段中不起作用
为啥通过 AJAX 无限滚动加载的内容的高度在加载后没有正确测量?
Android 嵌套滚动NestedScrollView+TabLayout+ViewPager+Fragment+RecyclerView 实现京东美团首页效果Tab页滚动到顶部时自动吸附