【原理篇】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)]


相关文章
|
7月前
|
Android开发
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X6 webview(二)
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X6 webview
|
7月前
|
Android开发
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
|
Android开发
WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
160 0
WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview
|
Android开发
NestedScrollView嵌套WebView导致底部有空白
NestedScrollView嵌套WebView导致底部有空白
442 0
|
Android开发 移动开发 存储
ListView嵌套webView 事件冲突解决
如图,红色部分为WebView,作为ListView头部存在,测试视频(h5)要左右滑动,ListView要上下滑动,保证视频控件和下面评论部分可以显示,但是这个时候就存在WebView横向滑动和ListView竖向滑动之间的冲突。
1342 0
|
Android开发
Android scrollview嵌套webview滑动冲突的解决方案
在Android开发中有时我们需要在scrollview中嵌套webview这时你会发现这两者的滑动事件产生了冲突导致:webview很难被滑动,即使被滑动了一点也非常不顺畅。解决方案也比较简单只需要在webview的ontouch事件中调用requestDisallowInterceptTouchEvent(true)即可。然后交给webview去处理,代码如下: mWebView.
6114 0
|
JavaScript 前端开发 Java
记一次使用 android 自带 WebView 做富文本编辑器之API、机型的兼容及各种奇葩bug的解决
转载请声明出处(http://www.cnblogs.com/linguanh/)   目录 1,测试设备介绍 2,开源项目richeditor及CrossWalk的选择 3,遇到的bug及其解决方法 4,附加功能彩蛋。
1686 0
|
6月前
|
API Android开发 数据安全/隐私保护
解决android webview 加载http url 失败 net::ERR_CLEARTEXT_NOT_PERMITTED 错误
解决android webview 加载http url 失败 net::ERR_CLEARTEXT_NOT_PERMITTED 错误
238 0
|
4月前
|
JavaScript 前端开发 Android开发
android开发,使用kotlin学习WebView(详细)
android开发,使用kotlin学习WebView(详细)
131 0
|
5月前
|
定位技术 Android开发
[√]Android webview的url scheme
[√]Android webview的url scheme
408 0