Android NestedScrolling 嵌套滚动原理解析

本文探讨了Android中复杂的可滚动控件嵌套时遇到的滑动问题,以及5.0版本后引入的NestedScrolling解决方案。通过分析NestedScrollView与ScrollView的源码,解释了NestedScrollingChild和NestedScrollingParent接口的作用,以及如何实现内部嵌套滚动。最后,通过NestedScrollView和RecyclerView的组合使用,展示了嵌套滚动的实际应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.原有问题

众所周知,android的触摸事件传递有局限性,当比较复杂的可滚动控件嵌套在一起的时候,总会有各种各样的滑动问题,这与android的触摸事件传递机制(View触摸事件机制)密不可分:

android的触摸事件传递是从上至下的递归传递,如果某次DOWN事件,有子view消费了,则之后的所有事件都只可能交由该子view处理,其父view没有机会再去处理(只能拦截);

并且很多ViewGroup,比如ViewPager、ScrollView,直接在onInterceptTouchEvent方法中,将move事件拦截,为的是交由自己处理,而没有兼顾到其子view可能的滑动;

所以原有的触摸传递问题的局限性就是:一旦子view处理事件后,父view就不能处理了,并且因此导致许多ViewGroup为了自己处理,直接拦截事件并没有交由子view处理事件,导致滑动效果的局限性很大

二.解决方案

1.实现原理

android在5.0后,对这块的问题做了很大的改善,其根本解决办法就是:既然原有问题是子view处理事件后父view就处理不了,那么就想办法在子view处理前,给父view一个处理的机会就好了。

这样有三个好处:

  1. 不影响核心的触摸事件传递机制,还是从上至下递归执行;一个view处理后不会交由其他view处理(onTouchEvent)

  2. 父view不用为了自己处理而武断的拦截事件,因为自己也会有一个处理事件的机会

  3. 可以让一次触摸事件在多层view中都应用上去,即可以实现滚动view内部嵌套滚动view的效果

2.方案设计

(1)android的support-v4包提供了两个接口来实现NestedScroll框架

NestedScrollingChild:提供了作为内部嵌套类应该实现的方法

public interface NestedScrollingChild {
   
    //是否可以嵌套滚动
    public void setNestedScrollingEnabled(boolean enabled);
	
	//自身是否支持嵌套滚动
	public boolean isNestedScrollingEnabled();

    //开始内部嵌套滚动,返回值为是否可以内部嵌套滚动,参数为滚动方向
    public boolean startNestedScroll(int axes);

    //停止内部嵌套滚动
    public void stopNestedScroll();

    //是否已经有支持其内部嵌套滚动的父view
    public boolean hasNestedScrollingParent();

    //在内部嵌套滚动时,派发给支持其嵌套滚动的parent,使其有机会做一些滚动的处理
	//参数为x/y轴已消费的和未消费的距离
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    //在内部嵌套滚动前,派发给支持其嵌套滚动的parent,使其有机会做一些滚动的预处理
	//dx,dy为可以消耗的距离,consumed为已经消耗的距离
	//返回值为支持其嵌套滚动的parent是否消费了部分距离
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    //在内部嵌套自由滑动时,派发给支持其嵌套滚动的parent,使其有机会做一些自由滑动的处理
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    //在内部嵌套自由滑动前,派发给支持其嵌套滚动的parent,使其有机会做一些自由滑动的预处理
	//返回值为支持其嵌套滚动的parent是否消费了fling事件
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

NestedScrollingParent:提供了作为支持内部嵌套滚动的view的方法

public interface NestedScrollingParent {
   
    //是否接受此次的内部嵌套滚动
	//target是想要内部滚动的view,child是包含target的parent的直接子view
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    //接受内部滚动后,做一些预处理工作
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    //停止了内部滚动
    public void onStopNestedScroll(View target);

    //内部嵌套滚动开始,根据已消费和未消费的距离参数进行应用
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    //内部嵌套滚动开始前做一些预处理,主要是根据dx,dy,将自己要消费的距离计算出来,告知target(通过consumed一层层记录实现)
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    //内部嵌套滑动开始,consumed参数为target是否消费了fling事件,parent可以根据此来做出自己的选择
	//返回值为parent自己是否消费了fling事件
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    //内部嵌套滑动开始前做一些预处理
	//返回值为parent自己是否消费此次fling事件
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);
} 

(2)android的support-v4包也提供了两个相应的Helper类来实现通用功能

NestedScrollingChildHelper:内部嵌套滚动view实现NestedScrollingChild的一些通用实现

public class NestedScrollingChildHelper {
   
    private final View mView;
    private ViewParent mNestedScrollingParent;
    private boolean mIsNestedScrollingEnabled;

    //view作为内部嵌套滚动的view
    public NestedScrollingChildHelper(View view) {
   
        mView = view;
    }

    //开始嵌套滚动的方法
    public boolean startNestedScroll(int axes) {
   
        if (hasNestedScrollingParent()) {
   
            //之前已经找到了支持嵌套滚动的parent,说明正在进行嵌套滚动,则返回true即可
            return true;
        }
        if (isNestedScrollingEnabled()) {
   
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
   
				//向上找到支持内部嵌套滚动的parent,并记录下来
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
   
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
   
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

    //派发滚动事件
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
   
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
   
			//已经消费部分距离或者还有未消费距离时,需要派发给parent进行处理
            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
   
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
   
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }
				//派发,即调用到NestedScrollingParent的onNestedScroll中进行处理
                ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
                        dyConsumed, dxUnconsumed, dyUnconsumed);

                if (offsetInWindow != null) {
   
                    mView.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值