本文首发我的微信公众号 徐公 ,收录于 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…)]
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/Br…
通过代码跟踪 & 调试,咱们发现了 WebViewCallBackClient 的接口
当 X5 外面的 webview 进行滑动的时候,会调用相应的办法。那么,咱们这时候就能够如法炮制,将下面 NestedWebView 的代码逻辑搬下来。
重写 onTouchEvent
, onInterceptTouchEvent
, computeScroll
这几个要害办法。
这样就实现了嵌套滑动。
具体的代码能够见 nestedwebview
总结
- 借助 NestedScrool 机制,要实现嵌套滑动其实还是蛮简略的,根本依照模板代码魔改一下就好了,要学会触类旁通。
- 如果要实现一些自定义的成果,那么咱们能够通过 Behavior 来实现,具体的能够参照 自定义 behavior – 完满仿 QQ 浏览器首页,美团商家详情页
参考博客
NestedWebView working properly with ScrollingViewBehavior
X5 WebView 官网
源码地址
nestedwebview, 能够帮忙给个 star 哦。
如果感觉对你有所帮忙的话,能够关注我我的微信公众号 徐公 ,这里有 Android 进阶成长常识体系, 心愿咱们可能一起学习提高,关注公众号 徐公,一起建设外围竞争力