讲一个 Android 嵌套滑动踩坑的真实经历
/ 今日科技快讯 /
昨日,易车网发布一份公告,声明公司董事会已收到一份初步的非约束性私有化提议。这份私有化收购要约来*腾讯及Hammer Capital(黑马资本)组成的买方团体,提议以每股ADS(美国存托股票)16美元的现金价格收购尚未持有的所有股份。其中,腾讯目前拥有易车网约7.81%的股份,Hammer Capital未持有任何股份。声明发出后,截至9月13日美东时间收盘,易车网股价上涨8.73%,报每股14.95美元。
/ 作者简介 /
愉快的中秋假期结束啦,新的一周重新开始,很高兴又跟大家见面!
本篇文章来自琼珶和予的投稿,分享了他踩坑嵌套滑动的经历,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
琼珶和予的博客地址:
https://juejin.im/user/5d5b50f66fb9a06b155dbfb7
/ 前言 /
本来认为自己对嵌套滑动的理解和应用还是不错的,但是最近做了一个跟手动画的需求,使用嵌套滑动发现了这里有了很多的坑,本文来根据自身的踩坑经历和经验来总结使用嵌套滑动的注意项。
本文不会介绍嵌套滑动的基本使用,不了解的同学可以参考我的文章:Android 源码分析 - 嵌套滑动机制的实现原理。
Android 源码分析 - 嵌套滑动机制的实现原理
https://www.jianshu.com/p/cb3779d36118
同时,本文嵌套滑动皆以RecyclerView为例。
/ 正文 /
1. 不要在onInterceptTouchEvent方法里面拦截事件
如果你有一个ViewGroup作为RecyclerView的父布局,这个ViewGroup主要来处理一些嵌套滑动的逻辑,比如说使用系统的SwipeRefreshLayout来做下拉刷新。如果这个ViewGroup不可能有父布局处理嵌套滑动,那么是否重写onInterceptTouchEvent可以自身需求来定,比如说SwipeRefreshLayout就重写了。
但是如果你的业务场景可能还会有ViewGroup来处理嵌套滑动,作为关系链中间的View千万不要重写onInterceptTouchEvent。
可能有对此有疑惑,现在我以一个具体的场景来解释具体的原因,假设有如下一个场景:
整个事件传递的流程是:首先由RecyclerView产生嵌套滑动的事件,然后提交给SwipeRefreshLayout尝试着处理, SwipeRefreshLayout收到事件之后,发现还有父View可能会处理,然后在提交给ViewGroup,ViewGroup根据自身条件选择消费一定的距离,然后又返回给SwipeRefreshLayout,SwipeRefreshLayout在根据自身条件选择消费,最后RecyclerView在消费。整个事件传递和消费的流程如下:
这里存在一种特殊情况,如果中间的SwipeRefreshLayout重写了onInterceptTouchEvent方法,导致事件不能传递到RecyclerView,从而导致了嵌套滑动的机制不能触发。有人可能有人疑问: SwipeRefreshLayout自己想拦截事件,并且处理事件,这难道有问题吗?
针对这个问题,我想说的是,正常情况下是没有问题的,但是如果ViewGroup必须跟手变化,只有ViewGroup跟手变化到最终态才能让 SwipeRefreshLayout下拉或者RecyclerView滑动,这种情况下,不走嵌套滑动的逻辑根本没法实现。
可能有人会提出相应的解决方法:我重写ViewGroup的onInterceptTouchEvent方法来拦截事件,然后消费事件不行吗?针对于这种解决方法,我想问的是,如果一次滑动产生10px的有效距离,而ViewGroup只能消费其中的5px,剩下的5px怎么办呢?根据情况传递到子View中去或者不消费?首先不消费是肯定不行的,否则就会显得滑动不灵敏,其次如果传递到子View中去,这也太麻烦了嘛。
像这种情况,我们最好的解决方法就是所有的滑动走嵌套滑动的逻辑,因为嵌套滑动本身自己支持消费部分距离的功能,而不用我们去特殊处理。
解释了在什么情况下不要重写onInterceptTouchEvent方法之后,我们现在来解释一下系统的SwipeRefreshLayout为什么要重写onInterceptTouchEvent。
-
Google爸爸默认为SwipeRefreshLayout已经嵌套滑动关系链上最后一个View了,SwipeRefreshLayout不可能再有父View处理嵌套滑动。
-
重写onInterceptTouchEvent可以为SwipeRefreshLayout增加一个新特性--就是不用依赖子View就可以实现下拉刷新。也是说,我们在xml布局中直接添加一个SwipeRefreshLayout,不用给它添加子View就能下拉刷新。这也是嵌套滑动的弊端,必须得有一个View来产生嵌套滑动。
2. 不要私自在dispatchTouchEvent的ACTION_CANCEL时机或者ACTION_UP时机调用stopNestedScroll方法
ViewParent parent = getNestedScrollingParentForType(type);
if (parent != null) {
ViewParentCompat.onStopNestedScroll(parent, mView, type);
setNestedScrollingParentForType(type, null);
}
}
2. TYPE_NON_TOUCH,表示手指松开之后还在滑动。
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
这是因为,在整个嵌套滑动关系链中,RecyclerView只可能是最底层的View,也就是只能产生嵌套滑动,不可能作为关系中间的一员。这一点,我们可以从RecyclerView继承的接口加以证明,RecyclerView只实现了NestedScrollingChild接口,而没有实现NestedScrollingParent接口。
一旦一个View实现了NestedScrollingParent接口,不能在ACTION_CANCEL或者ACTION_UP时调用stopNestedScroll方法。说到底就是,谁是startNestedScroll的源头,谁才有资格调用stopNestedScroll。
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
}
break;
//----------------------------------------------------------------------------
private void resetTouch() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
stopNestedScroll(TYPE_TOUCH);
releaseGlows();
}
// ·······
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
// ······
}
return false;
}
3. 慎重重写onStartNestedScroll方法
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ViewCompat.ScrollAxis int axes, @ViewCompat.NestedScrollType int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && type == ViewCompat.TYPE_TOUCH;
}
// ······
// 1. 分发fling
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
// ······
if (canScroll) {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontal) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertical) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
// 2. 建立type为TYPE_NON_TOUCH的传递链
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
-
分发fling事件。如果我们在处理嵌套滑动,很少会自己处理fling事件,所以dispatchNestedPreFling方法通常返回为false,从而进入了if的判断语句中。
-
通过startNestedScroll方法建立type为TYPE_NON_TOUCH的嵌套滑动传递链。由于,我们在上层View中没有对type进行判断,所以最终的传递链中会有我们的ViewGroup。
&& (overscrollX != 0 || overscrollY != 0)) {
final int vel = (int) scroller.getCurrVelocity();
int velX = 0;
if (overscrollX != x) {
velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0;
}
int velY = 0;
if (overscrollY != y) {
velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0;
}
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
absorbGlows(velX, velY);
}
if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0)
&& (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) {
scroller.abortAnimation();
}
}
本文地址:https://blog.csdn.net/c10WTiybQ1Ye3/article/details/100907599