上一篇文章 【使用篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
已经讲解了如何实现嵌套滑动,这篇文章,让我们一起来看他的实现原理。废话不多说,开始进入正文。
前言
讲解之前,先简单说一下嵌套滑动的一些概念。(熟悉这个的哥们可以直接跳过这个)
说到嵌套滑动,大家应该都不陌生。他是 Google 在 5.0 之后推出来的 NestedScroll 机制。
可能初学者会有这样的疑问?想比较于传统的事件分发机制,NetstedScroll 机制有什么优点。
在传统的事件分发机制 中,一旦某个 View 或者 ViewGroup 消费了事件,就很难将事件交给父 View 进行共同处理。而 NestedScrolling 机制很好地帮助我们解决了这一问题。我们只需要按照规范实现相应的接口即可,子 View 实现 NestedScrollingChild,父 View 实现 NestedScrollingParent ,通过 NestedScrollingChildHelper 或者 NestedScrollingParentHelper 完成交互。
如果对于 NestedScrolling 机制不了解的,可以看我几年前写的这篇文章。
他结合 CoordinatorLayout 可以实现很多炫酷的效果,比如吸顶效果等。
有兴趣的话可以看这些文章。
自定义Behavior —— 仿知乎,FloatActionButton隐藏与展示
ViewPager,ScrollView 嵌套ViewPager滑动冲突解决
自定义 behavior - 完美仿 QQ 浏览器首页,美团商家详情页
原理实现
废话不多说,今天,让我们一起来看看 WebView 怎样实现嵌套滑动。
原理简述
我们知道,嵌套滑动目前主要有几个接口 NestedScrollingChild,NestedScrollingParent 。
对于一个 ACTION_MOVE 动作
- scrolling child 在滑动之前,会通过 NestedScrollingChildHelper 查找是否有响应的 scrolling parent,如果有的话,会先询问scrolling parent 是否需要先于scrolling child 滑动,如果需要的话,scrolling parent 进行相应的滑动,并消费一定的距离;
- 接着scrolling child 进行相应的滑动,并消耗一定的距离值 dx,dy
- scrolling child 滑动完之后,询问scrolling parent 是否还需要继续进行滑动,需要的话,进行相应的处理。
- 滑动结束之后,Scrolling child 会停止滑动,并通过 NestedScrollingChildHelper 通知相应的 Scrolling Parent 停止滑动。
- 手指抬起的时候(Action_up) 的时候,根据滑动速度,计算是否相应 fling
而我们的 WebView 如果要实现嵌套滑动,那就可以借助这套机制。
实现
第一步,实现 NestedScroolChild3 接口,并重写相应的方法
public class NestedWebView extends WebView implements NestedScrollingChild3 { public NestedWebView(Context context) { this(context, null); } public NestedWebView(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.webViewStyle); } public NestedWebView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setOverScrollMode(WebView.OVER_SCROLL_NEVER); initScrollView(); mChildHelper = new NestedScrollingChildHelper(this); setNestedScrollingEnabled(true); } // 省略 }
第二步:
- 在 ACTION_DOWN 的时候,先调用 startNestedScroll 方法,告诉 NestedScrollParent,说我要滑动了
- 接着,在 ACTION_MOVE 的时候,调用 dispatchNestedPreScroll 方法,让 NestedScrollParent 有机会可以提前滑动,接着调用自身的 dispatchNestedScroll 方法,进行活动
public boolean onTouchEvent(MotionEvent ev) { initVelocityTrackerIfNotExists(); MotionEvent vtev = MotionEvent.obtain(ev); final int actionMasked = ev.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } vtev.offsetLocation(0, mNestedYOffset); switch (actionMasked) { case MotionEvent.ACTION_DOWN: if ((mIsBeingDragged = !mScroller.isFinished())) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } if (!mScroller.isFinished()) { abortAnimatedScroll(); } mLastMotionY = (int) ev.getY(); mActivePointerId = ev.getPointerId(0); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); break; case MotionEvent.ACTION_MOVE: final int activePointerIndex = ev.findPointerIndex(mActivePointerId); if (activePointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); break; } final int y = (int) ev.getY(activePointerIndex); int deltaY = mLastMotionY - y; if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) { deltaY -= mScrollConsumed[1]; mNestedYOffset += mScrollOffset[1]; } if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } mIsBeingDragged = true; if (deltaY > 0) { deltaY -= mTouchSlop; } else { deltaY += mTouchSlop; } } if (mIsBeingDragged) { mLastMotionY = y - mScrollOffset[1]; final int oldY = getScrollY(); final int range = getScrollRange(); // Calling overScrollByCompat will call onOverScrolled, which // calls onScrollChanged if applicable. if (overScrollByCompat(0, deltaY, 0, oldY, 0, range, 0, 0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) { mVelocityTracker.clear(); } final int scrolledDeltaY = getScrollY() - oldY; final int unconsumedY = deltaY - scrolledDeltaY; mScrollConsumed[1] = 0; dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset, ViewCompat.TYPE_TOUCH, mScrollConsumed); mLastMotionY -= mScrollOffset[1]; mNestedYOffset += mScrollOffset[1]; } break;
第三步:在 ACTION_UP 的时候,计算一下垂直方向的滑动速度,并进行分发
case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); if ((Math.abs(initialVelocity) > mMinimumVelocity)) { if (!dispatchNestedPreFling(0, -initialVelocity)) { dispatchNestedFling(0, -initialVelocity, true); fling(-initialVelocity); } } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } mActivePointerId = INVALID_POINTER; endDrag(); break;
同时重写 computeScroll 方法,处理惯性滑动
// 在更新 mScrollX 和 mScrollY 的时候会调用 public void computeScroll() { if (mScroller.isFinished()) { return; } mScroller.computeScrollOffset(); final int y = mScroller.getCurrY(); int unconsumed = y - mLastScrollerY; mLastScrollerY = y; // Nested Scrolling Pre Pass mScrollConsumed[1] = 0; dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH); unconsumed -= mScrollConsumed[1]; if (unconsumed != 0) { // Internal Scroll final int oldScrollY = getScrollY(); overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, getScrollRange(), 0, 0, false); final int scrolledByMe = getScrollY() - oldScrollY; unconsumed -= scrolledByMe; // Nested Scrolling Post Pass mScrollConsumed[1] = 0; dispatchNestedScroll(0, 0, 0, unconsumed, mScrollOffset, ViewCompat.TYPE_NON_TOUCH, mScrollConsumed); unconsumed -= mScrollConsumed[1]; } if (unconsumed != 0) { abortAnimatedScroll(); } // 判断是否滑动完成,没有完成的话,继续滑动 mScroller if (!mScroller.isFinished()) { ViewCompat.postInvalidateOnAnimation(this); } }
最后,为了确保 onTouchEvent 能够收到触摸事件,我们在 onInterceptTouchEvent 中进行拦截
public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { // most common return true; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: final int y = (int) ev.getY(pointerIndex); final int yDiff = Math.abs(y - mLastMotionY); // 判断一下滑动距离并且是竖直方向的滑动 if (yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { // 代表药进行拦截 mIsBeingDragged = true; mLastMotionY = y; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); mNestedYOffset = 0; // 请求父类不要拦截事件 final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } break; case MotionEvent.ACTION_DOWN: mLastMotionY = (int) ev.getY(); mActivePointerId = ev.getPointerId(0); initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); mScroller.computeScrollOffset(); mIsBeingDragged = !mScroller.isFinished(); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); break; return mIsBeingDragged; }
处理完之后,我们的 webview 就实现了 NestedScrol 机制,可以进行嵌套滑动了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLoWwHcf-1663672759560)(https://raw.githubusercontent.com/gdutxiaoxu/blog_image/master/22/04/webview%20%E5%B5%8C%E5%A5%97%E6%BB%91%E5%8A%A8.gif)]