【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X6 webview(一)

简介: 【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X6 webview

上一篇文章  【使用篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview

已经讲解了如何实现嵌套滑动,这篇文章,让我们一起来看他的实现原理。废话不多说,开始进入正文。


前言


讲解之前,先简单说一下嵌套滑动的一些概念。(熟悉这个的哥们可以直接跳过这个)


说到嵌套滑动,大家应该都不陌生。他是 Google 在 5.0 之后推出来的 NestedScroll 机制。


可能初学者会有这样的疑问?想比较于传统的事件分发机制,NetstedScroll 机制有什么优点。


在传统的事件分发机制 中,一旦某个 View 或者 ViewGroup 消费了事件,就很难将事件交给父 View 进行共同处理。而 NestedScrolling 机制很好地帮助我们解决了这一问题。我们只需要按照规范实现相应的接口即可,子 View 实现 NestedScrollingChild,父 View 实现 NestedScrollingParent ,通过 NestedScrollingChildHelper 或者 NestedScrollingParentHelper 完成交互。


b7a1f6f49c96f7adeaa96a45d4570bd0_ea967568f57ca28f965d800a4ec5a91a.png


如果对于 NestedScrolling 机制不了解的,可以看我几年前写的这篇文章。

NestedScrolling 机制深入解析


他结合 CoordinatorLayout 可以实现很多炫酷的效果,比如吸顶效果等。


有兴趣的话可以看这些文章。

使用CoordinatorLayout打造各种炫酷的效果

自定义Behavior —— 仿知乎,FloatActionButton隐藏与展示

NestedScrolling 机制深入解析

一步步带你读懂 CoordinatorLayout 源码

自定义 Behavior -仿新浪微博发现页的实现

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)]


相关文章
|
Android开发
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X6 webview(二)
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X6 webview
|
Android开发
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
|
Android开发
WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
194 0
WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
|
Android开发
NestedScrollView嵌套WebView导致底部有空白
NestedScrollView嵌套WebView导致底部有空白
558 0
|
存储 缓存 移动开发
WebView缓存原理分析和应用
WebView缓存原理分析和应用
681 0
|
API Android开发 数据安全/隐私保护
解决android webview 加载http url 失败 net::ERR_CLEARTEXT_NOT_PERMITTED 错误
解决android webview 加载http url 失败 net::ERR_CLEARTEXT_NOT_PERMITTED 错误
988 0
|
4月前
|
Web App开发 JavaScript 前端开发
Android端使用WebView注入一段js代码实现js调用android
Android端使用WebView注入一段js代码实现js调用android
122 0
|
5月前
|
Web App开发 移动开发 前端开发
52. 【Android教程】网页视图:WebView
52. 【Android教程】网页视图:WebView
93 1
|
Android开发 iOS开发 UED
Android webView 实现阻尼回弹效果
iOS webView默认滑动到顶部或者底部的时候,还可以继续通过手指拉扯滑动,松手后回弹;而Android webView默认是不行的,要实现跟iOS一样的效果,就需要自定义webView。
582 0
|
6月前
|
JavaScript 前端开发 Android开发
android开发,使用kotlin学习WebView(详细)
android开发,使用kotlin学习WebView(详细)
510 0