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

本文主要是介绍【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文首发我的微信公众号徐公,收录于 Github·AndroidGuide,这里有 Android 进阶成长知识体系, 希望我们能够一起学习进步,关注公众号徐公,5 年中大厂程序员,一起建立核心竞争力

上一篇文章 【使用篇】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 怎样实现嵌套滑动。

请添加图片描述

原理简述

我们知道,嵌套滑动目前主要有几个接口 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 PassmScrollConsumed[1] = 0;dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null,ViewCompat.TYPE_NON_TOUCH);unconsumed -= mScrollConsumed[1];if (unconsumed != 0) {// Internal Scrollfinal int oldScrollY = getScrollY();overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, getScrollRange(),0, 0, false);final int scrolledByMe = getScrollY() - oldScrollY;unconsumed -= scrolledByMe;// Nested Scrolling Post PassmScrollConsumed[1] = 0;dispatchNestedScroll(0, 0, 0, unconsumed, mScrollOffset,ViewCompat.TYPE_NON_TOUCH, mScrollConsumed);unconsumed -= mScrollConsumed[1];}if (unconsumed != 0) {abortAnimatedScroll();}// 判断是否滑动完成,没有完成的话,继续滑动 mScrollerif (!mScroller.isFinished()) {ViewCompat.postInvalidateOnAnimation(this);}
}

最后,为了确保 onTouchEvent 能够收到触摸事件,我们在 onInterceptTouchEvent 中进行拦截

public boolean onInterceptTouchEvent(MotionEvent ev) {final int action = ev.getAction();if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { // most commonreturn 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 的时候,这时候进行滑动,发现无法联动了。

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 结构打印出来

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 哦。

如果觉得对你有所帮助的话,可以关注我我的微信公众号徐公,这里有 Android 进阶成长知识体系, 希望我们能够一起学习进步,关注公众号徐公,一起建立核心竞争力

这篇关于【原理篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/255768

相关文章

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

MySQL双主搭建+keepalived高可用的实现

《MySQL双主搭建+keepalived高可用的实现》本文主要介绍了MySQL双主搭建+keepalived高可用的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、测试环境准备二、主从搭建1.创建复制用户2.创建复制关系3.开启复制,确认复制是否成功4.同

Java实现文件图片的预览和下载功能

《Java实现文件图片的预览和下载功能》这篇文章主要为大家详细介绍了如何使用Java实现文件图片的预览和下载功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... Java实现文件(图片)的预览和下载 @ApiOperation("访问文件") @GetMapping("

使用Sentinel自定义返回和实现区分来源方式

《使用Sentinel自定义返回和实现区分来源方式》:本文主要介绍使用Sentinel自定义返回和实现区分来源方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Sentinel自定义返回和实现区分来源1. 自定义错误返回2. 实现区分来源总结Sentinel自定

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义

opencv图像处理之指纹验证的实现

《opencv图像处理之指纹验证的实现》本文主要介绍了opencv图像处理之指纹验证的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录一、简介二、具体案例实现1. 图像显示函数2. 指纹验证函数3. 主函数4、运行结果三、总结一、