本文首发我的微信公众号徐公,收录于 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 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 嵌套滑动.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的更多相关文章

  1. 自定义tab吸顶效果一(原理)

    PS:问题:什么是吸顶,吸顶有什么作用,吸顶怎么使用? 在很多app商城中,介绍软件的时候就会使用吸顶效果, 吸顶有很多作用,一个最简单粗暴的作用就是,让用户知道此刻在浏览哪个模块,并可以选择另外的模 ...

  2. [RN] React Native 头部 滑动吸顶效果的实现

    React Native 头部 滑动吸顶效果的实现 效果如下图所示: 实现方法: 一.吸顶组件封装 StickyHeader .js import * as React from 'react'; i ...

  3. dart系列之:如丝滑般柔顺,操作文件和目录

    目录 简介 File 读取整个文件 以流的形式读取文件 随机访问 文件的写入 处理异常 总结 简介 文件操作是IO中非常常见的一种操作,那么对应dart语言来说,操作文件是不是很简单呢?实际上dart ...

  4. 透过 NestedScrollView 源码解析嵌套滑动原理

    NestedScrollView 是用于替代 ScrollView 来解决嵌套滑动过程中的滑动事件的冲突.作为开发者,你会发现很多地方会用到嵌套滑动的逻辑,比如下拉刷新页面,京东或者淘宝的各种商品页面 ...

  5. NestedScrollView嵌套RecycleView 滑动 实现上滑隐藏 下滑显示头部效果

    废了好大的劲才弄好的,记下来 方便以后查看 public class MainActivity extends AppCompatActivity { private RecyclerView mRe ...

  6. 利用轮播原理结合hammer.js实现简洁的滑屏功能

    最近有个任务,做一个非常小的h5的应用,只有2屏,需要做横向的全屏滑动切换和一些简单的动画效果,之前做这种东西用的是fullpage.js和jquery,性能不是很好,于是就想自己动手弄一个简单的东西 ...

  7. 让你的app体验更丝滑的11种方法!冲击手机应用榜单Top3指日可待

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由WeTest质量开放平台团队发表于云+社区专栏 一款app除了要有令人惊叹的功能和令人发指交互之外,在性能上也应该追求丝滑的要求,这样 ...

  8. Android NestedScrolling嵌套滑动机制

    Android NestedScrolling嵌套滑动机制 最近项目要用到官网的下拉刷新SwipeRefreshLayout,它是个容器,包裹各种控件实现下拉,不像以前自己要实现事件的拦截,都是通过对 ...

  9. UIScrollView嵌套滑动手势冲突的简易实现

    明确需求 现在有较多的商城类app有如下需求,界面上带有headerView,并且有一个barView可悬停,最下方为多个可左右滑动的tableView,具体可参考下图 另类实现 在网上关于此类需求的 ...

  10. Redis原理篇

    Redis原理篇 1.发布 订阅模式 1.1列表 的局限 ​ 前面我们说通过队列的 rpush 和 lpop 可以实现消息队列(队尾进队头出),但是消费者需要不停地调用 lpop 查看 List 中是 ...

随机推荐

  1. 数据结构与算法 | 记忆化搜索(Memorize Search)

    在本系列的文章中已经写了二叉树(Binary Tree).深搜(DFS)与广搜(BFS).哈希表(Hash Table)等等,计划接下来要写的是动态规划(Dynamic Programming,DP) ...

  2. 为什么 Django 后台管理系统那么“丑”?

    哈喽大家好,我是咸鱼 相信使用过 Django 的小伙伴都知道 Django 有一个默认的后台管理系统--Django Admin 它的 UI 很多年都没有发生过变化,现在看来显得有些"过时 ...

  3. React、Umi、RN 整体理解

    可以少去理解一些不必要的概念,而多去思考为什么会有这样的东西,它解决了什么问题,或者它的运行机制是什么? React 1. React 起源和发展(是什么?) React 是用于构建用户界面的 Jav ...

  4. HBuilderX代码提示失效解决方案

    用了一段时间后,HBuilderX的代码提示功能突然不生效了,重新下载也没有用,解决方案是重置默认配置 提示:重置配置会清除编辑器内的项目,记得先保存 解决方案 1.找到HBuilderX根目录下的r ...

  5. ABAP 生产订单长文本增强 <销售计划 、物料独立需求 长文本带入 计划订单-生产订单 >

    计划订单长文本带入生产订单 尝试在生产订单保存后 用 creat_text 函数 去创建长文本,发现前台不显示,查看 文本抬头底表 STXL 发现有值 ,用READ 函数 读取 能读. DATA:td ...

  6. 【UniApp】-uni-app-CompositionAPI应用生命周期和页面生命周期

    前言 好,经过上个章节的介绍完毕之后,了解了一下 uni-app-OptionAPI应用生命周期和页面生命周期 那么了解完了uni-app-OptionAPI应用生命周期和页面生命周期之后,这篇文章来 ...

  7. CICD实践1:环境安装篇

    一.CICD技术选型 配置管理工具 工具 需求管理工具 使用禅道 代码管理工具 使用Gitlab 编译构建工具 搭建Jenkins,使用Jenkinsfile 制品库工具 nexus 文档管理工具 C ...

  8. Head First Java学习:第十章-数字很重要

     1.Math 方法:最接近全局的方法 一种方法的行为不依靠实例变量值,方法对参数执行操作,但是操作不受实例变量状态影响,那么为了执行该方法去堆上建立对象实例比较浪费. 举例: Math mathOb ...

  9. 深入了解RC4 Drop加密技术

    一.引言 在网络安全领域,加密技术始终是重中之重.随着计算机技术的发展,加密算法也在不断更新换代.RC4(Rivest Cipher 4)加密算法因其高效.简洁的特性,在信息安全领域得到了广泛的应用. ...

  10. Go语言实现GoF设计模式:备忘录模式的实践探索

    本文分享自华为云社区<[Go实现]实践GoF的23种设计模式:备忘录模式>,作者:元闰子. 简介 相对于代理模式.工厂模式等设计模式,备忘录模式(Memento)在我们日常开发中出镜率并不 ...