Android嵌套滑动的分析与实践

【一】传统事件分发

1.1 传统事件分发流程

Activity:

public boolean dispatchTouchEvent(MotionEvent event)

public boolean onTouchEvent(MotionEvent event) 

ViewGroup:

public boolean dispatchTouchEvent(MotionEvent event)

public boolean onInterceptTouchEvent(MotionEvent ev)

public boolean onTouchEvent(MotionEvent event) 

View:

public boolean dispatchTouchEvent(MotionEvent event)

public boolean onTouchEvent(MotionEvent event) 

伪代码表示dispatchTouchEventonInterceptTouchEventonTouchEvent三者之间的关系:

public boolean dispatchTouchEvent (MotionEvent ev){
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    } else {
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

用图来表示事件传递的过程:

事件传递.png

1.2 传统事件滑动冲突

来看ViewGroup的分发(PS:本文中的源码是基于Android API 24分析的~):

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

------other code------

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        intercepted = false;
    }
} else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}
------other code------
handled = super.dispatchTouchEvent(transformedEvent);
}

ViewGroup不拦截事件并由ViewGroup的子View处理事件时,mFirstTouchTarget会被赋值并指向子View,上面的代码可以分成下面几种情况:
1、如果MotionEvent.ACTION_DOWN事件被ViewGroup拦截,那么mFirstTouchTarget==null,那么后续的ACTION_MOVEACTION_UP事件都不会往子View中传递了,而会走ViewGrouponTouchEvent方法。

2、当mFirstTouchTarget != null时,即子View处理后续ACTION_MOVEACTION_UP事件时,子View中可以通过设置getParent().requestDisallowInterceptTouchEvent(true),该设置会影响上述代码中的disallowIntercept变量,从而使ViewGroup不拦截事件,将事件传递到子View中去

下面分析一下当遇到滑动冲突的时候一般的解决方法:

1.2.1外部拦截法

外部拦截法即在父ViewGrouponInterceptTouchEvent中去做处理,伪代码如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean isIntercept = false;
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isIntercept = false;
            break;
        case MotionEvent.ACTION_MOVE:
            if (ViewGroup拦截ACTION_MOVE条件) {
                isIntercept = true;
            } else {
                isIntercept = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            isIntercept = false;
            break;
    }
    return isIntercept;
}

这里注意除非父ViewGroup要处理所有事件,否则一定不能拦截ACTION_DOWN,因为一旦拦截了ACTION_DOWN事件,后续的MOVEUP事件将都不能传递到子View

1.2.2内部拦截法

内部拦截法是指父ViewGroup不拦截任何事件,所有事件都传递到子View中,通过getParent().requestDisallowInterceptTouchEvent(boolean)来控制后续事件让不让父ViewGroup去拦截。来看requestDisallowInterceptTouchEvent的源码:

@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

主要是修改标志位mGroupFlags,这个标志位也是在父ViewGroupdispatchTouchEvent中控制是否走onInterceptTouchEvent()拦截方法的,disallowIntercepttrue时,父ViewGroup不会再走onInterceptTouchEvent()拦截事件;反之会走onInterceptTouchEvent()方法,默认是false

内部拦截法的使用举例:

子ViewdispatchTouchEvent中:

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            if (父ViewGroup需要拦截并处理事件) {
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return super.dispatchTouchEvent(event);
}

父ViewGrouponInterceptTouchEvent中:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return ev.getAction() != MotionEvent.ACTION_DOWN;
}

内部拦截法同样不能拦截ACTION_DOWN事件,否则事件不能传递到子View中。

1.3 传统嵌套滑动冲突

在开始介绍滑动冲突之前,先介绍一下测量规格MeasureSpecMeasureSpec参与了View的测量过程,子ViewMeasureSpec的创建是由父ViewMeasureSpec子View自身的LayoutParams共同决定的,MeasureSpec的组成:

MeasureSpec是一个32位的int值,高2位是specMode,低30位是specSize,如下:

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

SpecMode是测量模式,SpecSize是在某种测量模式下的测量大小,SpecMode有三个值:

  • USPECIFIED: 父View子View没有任何限制。
  • EXACTLY: 父View已经检测出View的精确大小,View的最终大小就是SpecSize指定的值。它对应于LayoutParams中的match_parent和具体数值这两种模式。
  • AT_MOST: 父View指定了一个可用大小SpecSizeView的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content
1.3.1 ScrollView+ ListView嵌套冲突

默认ListView中的onMeasure方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // Sets up mListPadding
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    -----其他代码-----
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    if (heightMode == MeasureSpec.UNSPECIFIED) {
        heightSize = mListPadding.top + mListPadding.bottom +    childHeight + getVerticalFadingEdgeLength() * 2;
    }

    if (heightMode == MeasureSpec.AT_MOST) {
        heightSize = measureHeightOfChildren(widthMeasureSpec, 0,    NO_POSITION, heightSize, -1);
    }
 }

通过源码我们知道当测量模式是UNSPECIFIED时,高度只是一个item的高度(包括上下的padding);当测量模式是AT_MOST时,高度是所有item的高度,即整个listview的高度。通过测试发现ScrollViewListView嵌套使用时,传给ListView的测量模式是UNSPECIFIED,所以只能显示一个Item的高度,那怎么显示整个ListView的高度呢?通过上面的分析我们已经知道答案了:重写ListViewonMeasure方法!

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
    super.onMeasure(widthMeasureSpec, heightSpec);
}

放弃通过父View及自身LayoutParams生成的MeasureSpec(specMode为UNSPECIFIED),重新生成一个specModeAT_MOSTMeasureSpec即可。

1.3.2 ScrollView+ ViewPager嵌套问题

来看ViewPageronMeasure源码:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // For simple implementation, our internal size is always 0.
    // We depend on the container to specify the layout size of
    // our view.  We can't really know what it is since we will be
    // adding and removing different arbitrary views and do not
    // want the layout to change as this happens.
    setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
            getDefaultSize(0, heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

通过分析源码我们知道:

测量模式specModeUNSPECIFIED时,ViewPager的高度是0;
测量模式specModeAT_MOSTEXACTLY时,ViewPager的高度直接取的父View传入的值。

通过测试发现当ScrollViewViewPager嵌套使用时,测量模式specModeUNSPECIFIED,所以默认高度是0.

【二】 传统事件分发 VS NestedScrolling

  • 传统事件分发:子View处理Touch事件时,父View可以进行拦截并在父View中处理,但是一旦父View进行拦截,后续事件都不会再往子View中传递了。
  • NestedScrolling:子View在滚动的时候,首先将dx、dy交给NestedScrollingParent进行消耗,剩余部分还给子View

【三】NestedScrolling嵌套滑动

Android5.0开始引入了NestedScrolling机制(5.0之前可以用Support V4包向前兼容),用来处理子View父View嵌套滑动时的交互机制。子View一般是可以滑动的View并且需要实现 NestedScrollingChild 接口,父View需要实现NestedScrollingParent接口。

2.1 NestedScrollingChild

public interface NestedScrollingChild {
   
    public void setNestedScrollingEnabled(boolean enabled);

    public boolean isNestedScrollingEnabled();

    public boolean startNestedScroll(int axes);

    public void stopNestedScroll();

    public boolean hasNestedScrollingParent();

    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

2.2 NestedScrollingParent

public interface NestedScrollingParent {
   
    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);

    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    public int getNestedScrollAxes();
}

2.3 两者之间的关系

NestedScroll.png

一、startNestedScroll

首先子view需要开启整个流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合处理TouchEvent

二、dispatchNestedPreScroll

在子View的onInterceptTouchEvent或者onTouch中(一般在 MontionEvent.ACTION_MOVE事件里),调用该方法通知父View滑动的距离。该方法的第三第四个参数返回父view消费掉的 scroll长度和子View的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll前调用。

三、dispatchNestedScroll

向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll后调用。

四、stopNestedScroll

结束整个流程。

更详细见:
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0603/2990.html

2.4 二个NestedScrolling 嵌套滑动例子

直接见github吧:
https://github.com/crazyqiang/AndroidStudy

或者见鸿神的博客:
https://blog.csdn.net/lmj623565791/article/details/52204039

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_小马快跑_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值