前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview

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

作者头像
程序员徐公
发布于 2022-10-05 09:19:51
发布于 2022-10-05 09:19:51
1.2K00
代码可运行
举报
运行总次数:0
代码可运行

本文首发我的微信公众号徐公,收录于 Github·AndroidGuide,这里有 Android 进阶成长知识体系, 希望我们能够一起学习进步

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

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

前言

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

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

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

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

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

NestedScrolling 机制深入解析

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

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

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

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

NestedScrolling 机制深入解析

一步步带你读懂 CoordinatorLayout 源码

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

ViewPager,ScrollView 嵌套ViewPager滑动冲突解决

自定义 behavior - 完美仿 QQ 浏览器首页,美团商家详情页

原理实现

废话不多说,今天,让我们一起来看看 WebView 怎样实现嵌套滑动。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dJ5GuQrF-1663672759559)(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)

原理简述

我们知道,嵌套滑动目前主要有几个接口 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 接口,并重写相应的方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 方法,进行活动
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
   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 的时候,计算一下垂直方向的滑动速度,并进行分发

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 方法,处理惯性滑动

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 在更新 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 中进行拦截

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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)

X5 webView 兼容

当我将代码搬到 x5 webview 的时候,这时候进行滑动,发现无法联动了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class NestedWebView extends com.tencent.smtt.sdk.WebView implements NestedScrollingChild3

原因分析

这是什么原因呢?

我们点进去 X5 webView 里面的代码,发现 webView 是继承 FrameLayout,而不是继承系统 WebView。

因此我们直接 extends com.tencent.smtt.sdk.WebView,对触摸事件进行拦截,实际上是对 FrameLayout 进行拦截处理,而不是对里面的 WebView 进行拦截处理,那肯定达不到嵌套滑动。

解决方案

我们先来看一下 X5 webView 的 View Tree 结构,因为 X5 webView 代码是混淆的,我们想要通过代码直接看出他的 View Tree,是不太方便的。

于是,我们可以通过代码,将 x5 webView viewTree 结构打印出来

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
webView = view.findViewById<WebView>(R.id.webview)
val childCount = webView.childCount
Log.i(TAG, "onViewCreated: webView  is $webView, childCount is $childCount")

for (i in 0 until childCount) {
    Log.i(TAG, "x5 webView: childView[$i]  is ${webView.getChildAt(i)}")
}

运行以上代码,得到以下结果

可以看到 X5 WebView 应该就是在 WebView 的基础之上包了一层 FrameLayout。

那我们对没有办法拿到里面的 TencentWebViewProxy$InnerWebView 对象,其实是有的。他在里面有一个 getView 的方法。

拿到这个对象之后,我们有办法进行拦截处理嘛,像 onTouchEvent, onInterceptTouchEvent 方法?

我们在官方文档中 X5 webview 常见问题 找到这样的描述

3.10 如何重写TBS WebView 的屏幕事件(例如 overScrollBy) 需 setWebViewCallbackClient 和 setWebViewClientExtension 参考代码示例 http://res.imtt.qq.com/tbs/BrowserActivity.zip

通过代码跟踪&调试,我们发现了 WebViewCallBackClient 的接口

当 X5 里面的 webview 进行滑动的时候,会调用相应的方法。那么,我们这时候就可以依样画葫芦,将上面 NestedWebView 的代码逻辑搬下来。

重写 onTouchEvent, onInterceptTouchEvent, computeScroll 这几个关键方法。

这样就实现了嵌套滑动。

具体的代码可以见 nestedwebview

总结

  1. 借助 NestedScrool 机制,要实现嵌套滑动其实还是蛮简单的,基本按照模板代码魔改一下就好了,要学会举一反三。
  2. 如果要实现一些自定义的效果,那么我们可以通过 Behavior 来实现,具体的可以参照 自定义 behavior - 完美仿 QQ 浏览器首页,美团商家详情页

参考博客

NestedWebView working properly with ScrollingViewBehavior

X5 WebView 官网

源码地址

nestedwebview, 可以帮忙给个 star 哦。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-09-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
iOS底层—OC对象的本质与isa
我们已经学习了对象的初始化、内存对齐等内容。这篇文章将深入学习探究对象的本质、对isa进行分析。
CC老师
2022/01/11
5730
iOS底层—OC对象的本质与isa
Objective-C内存管理原理探究(一)
MelonTeam
2018/01/04
1.1K0
Objective-C内存管理原理探究(一)
Swift底层-对象&结构&属性
pushq %rbp //很明显,往下读pushq movl 参数入栈和传递
Wilbur-L
2021/03/07
1.1K0
Swift底层-对象&结构&属性
iOS-Swift 结构体与类
在 Swift 的标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分。比如 Bool、Int、Double、 String、Array、Dictionary 等常见类型都是结构体。
CC老师
2022/01/14
1.4K0
iOS-Swift 结构体与类
面试驱动技术之 - isa && 元类 && 函数调用
以MNPerson为例,里面的成员变量有不同类型是,比如int、double、NSString 类型,假如在C/C++ 中用数组存储,显然是不太合理的
小蠢驴打代码
2019/01/28
9410
面试驱动技术之 - isa && 元类 && 函数调用
OC底层探索04-探索对象内存大小OC底层探索04-探索对象内存大小
所有的类在OC中最终都会编译为objc_object(在这个问题中可以看做父类),其中包含一个isa指针,所以需要再加上8字节。
用户8893176
2021/08/09
7000
OC底层探索04-探索对象内存大小OC底层探索04-探索对象内存大小
OC底层探索03-常用的alloc,init,new到底做了什么?OC底层探索03-常用的alloc,init,new到底做了什么?
想要一探alloc是如何申请了内存空间的,就需要使用上篇中提到的objc源码了。废话不多说,打开源码,加上断点,一步步开始调试: 此处有两种可能,简述流程省略代码:
用户8893176
2021/08/09
8340
OC底层探索03-常用的alloc,init,new到底做了什么?OC底层探索03-常用的alloc,init,new到底做了什么?
OC底层探索08-基于objc4-781类结构分析OC底层探索08-基于objc4-781类结构分析
提到缓存那么cache面缓存的是什么呢:属性还方法?其实很好猜测,平时开发中使用最多的就是方法,因为只有方法才会引起变化。下面会通过两种方式进行验证这个猜测。
用户8893176
2021/08/09
3060
OC底层探索08-基于objc4-781类结构分析OC底层探索08-基于objc4-781类结构分析
OC类的原理探究(二)——方法的缓存
上面的代码中,如果我们覆写了该类的allocWithZone方法,那么就会走到第31行的逻辑;不过一般而言我们是不会自己去覆写allocWithZone方法的,所以一般都会走第8~28行的逻辑。
拉维
2021/03/10
5510
OC类的原理探究(二)——方法的缓存
OC底层探索25-深入浅出BlockOC底层探索25-深入浅出Block
在2.2中出现了值拷贝,而且在block中也不允许使用修改捕获参数,想过要block内部修改就需要__block声明变量。
用户8893176
2021/08/09
5330
OC底层探索25-深入浅出BlockOC底层探索25-深入浅出Block
OC底层探索19-weak和assign区别浅谈OC底层探索19-weak和assign区别浅谈
weak 只可以修饰对象。如果修饰基本数据类型,编译器会报错-“Property with ‘weak’ attribute must be of object type”。 assign 可修饰对象,和基本数据类型。当需要修饰对象类型时,MRC时代使用unsafe_unretained。当然,unsafe_unretained也可能产生野指针,所以它名字是"unsafe_”。
用户8893176
2021/08/09
9830
OC底层探索19-weak和assign区别浅谈OC底层探索19-weak和assign区别浅谈
【iOS底层】 类的结构分析
这里要注意的是,在new版本的源码中,objc_class继承自objc_object,在之前的旧版本中,isa指针直接定义在objc_class中,其中OC中的NSObject在编译到底层的时候都会转变成相应的结构体objc_object
CC老师
2022/01/11
3530
OC底层探索13-基于objc4-818的cache_t结构探索OC底层探索13-基于objc4-818的cache_t结构探索
在OC底层探索09-cache_t实现原理探索中已经对cache缓存的机制做了介绍,但是这文章是基于objc4-781来探索的。技术更新的太快了,在objc4-818中cache_t的结构又发生了很大的变化。与此同时缓存的过程中也有一些小的优化。
用户8893176
2021/08/09
3300
OC底层探索13-基于objc4-818的cache_t结构探索OC底层探索13-基于objc4-818的cache_t结构探索
OC对象原理(二)
上面的代码中,如果我们覆写了该类的allocWithZone方法,那么就会走到第31行的逻辑;不过一般而言我们是不会自己去覆写allocWithZone方法的,所以一般都会走第8~28行的逻辑。
拉维
2021/10/20
7470
OC底层探索09-cache_t实现原理探索OC底层探索09-cache_t实现原理探索
在OC底层探索06-isa本身藏了多少信息你知道吗?分析了isa。 在OC底层探索08-基于objc4-781类结构分析中分析了bits;
用户8893176
2021/08/09
4120
OC底层探索09-cache_t实现原理探索OC底层探索09-cache_t实现原理探索
swift底层探索 07 -内存管理(refCount&weak&unowned)swift底层探索 07 -内存管理(refCount&weak&unowned)
提到内存管理在iOS开发中,就不得不提ARC(自动引用技术)。本文主要讨论的就是ARC在swift中是如何存储、计算,以及循环引用是如何解决的。 [toc]
用户8893176
2021/08/09
1.1K0
swift底层探索 07 -内存管理(refCount&weak&unowned)swift底层探索 07 -内存管理(refCount&weak&unowned)
swift底层探索 06 - 指针简单使用swift底层探索 06 - 指针简单使用
如果在lldb中需要获取值类型的地址,直接使用po、p、v都是无法获取地址的,只能转为指针后才可以获取,如图一。
用户8893176
2021/08/09
7170
swift底层探索 06 - 指针简单使用swift底层探索 06 - 指针简单使用
ios 底层原理 : 类与类结构分析
类的分析 类的分析主要是分析 isa 的走向与继承关系 准备 创建两个类 1.继承自 NSObject 的 LGPerson @interface LGPerson : NSObject { NSString *hobby; } @property(nonatomic,copy)NSString * lg_name; - (void)sayHello; + (void)sayBye; @end @implementation LGPerson - (void)sayHello { } +
conanma
2021/10/28
6470
iOS-底层原理36:内存优化(一) 野指针探测
下面是Mach异常 与 UNIX信号 的转换关系代码,来自 xnu 中的 bsd/uxkern/ux_exception.c
conanma
2021/10/28
2.4K0
IOS底层原理之NSObject的结构
在OC程序中,我们知道NSObject是“万物之源”,所有的类的都继承自NSObject,我们疑惑的是在OC的底层NSObject是什么样的?
CC老师
2022/01/12
7090
IOS底层原理之NSObject的结构
推荐阅读
相关推荐
iOS底层—OC对象的本质与isa
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验