美文网首页
WebView嵌套能力的技术方案与实现总结

WebView嵌套能力的技术方案与实现总结

作者: willimes | 来源:发表于2020-04-20 14:02 被阅读0次

    1、嵌套滚动机制简介

    与嵌套滚动机制有关的接口或者辅助类主要有如下截图几个类

    按照类别可以分为两大类别:NestedScrollingChildXXX、NestedScrollingParentXXX接口行为类别;NestedScrollingChildHelper、NestedScrollingParentHelper嵌套委派辅助类。官方这么设计,将嵌套滚动逻辑处理单独剥离,没有依附在具体的子View和父View,嵌套逻辑都抽离在NestedScrollingParentHelper、NestedScrollingChildHelper委派类里面。换句话说,Helper类封装了嵌套滚动传动逻辑。

    嵌套滚动一般是由嵌套子View(也就是实现了NestedScrollingChildXXX系列接口的一方)发起的,父嵌套布局被动接受。结合事件拦截分发以及嵌套滚动时对应的接口流程,我将嵌套滚动链路大致梳理了如下几个步骤(嵌套子View假设是一个ViewGroup,实际使用过程中,这种场景占据绝大部分):

    以子view child为例,以上嵌套流程里面的嵌套调用方法都是NestedScrollingChildXXX接口里面声明的方法,方法对应的具体执行逻辑都是委托给NestedScrollingChildHelper辅助类去实现的。

    注意一个概念,嵌套并不需要要求直接父子View节点嵌套,中间可以隔着很多层,嵌套子View会去找最近的嵌套父View,这是什么意思呢?这里我们可以看下开始嵌套滚动时的源码入口startNestedScroll(int axes, int type),实际执行者是NestedScrollingChildHelper里面的startNestedScroll(axes, type)方法,贴出源码。

    如图中源码所示,执行开始嵌套滚动的代码,会直接遍历到最近的具有嵌套滚动能力的父View。

    2、WebView实现为嵌套子View

    依据第一小节介绍的嵌套滚动机制,那么如何将一个view实现为嵌套子View的大致思路还是清晰的。先把最简单的流程环节先串完,那就是先实现NestedScrollingChildXXX接口,并将对应的调用方法委托给NestedScrollingChildHelper辅助类去调用自己同签名方法去执行。这一步大部分人应该都没有问题。下面就是如何在事件分发事件流程里面,如何控制嵌套流程以及处理Parent与Child在边界处的嵌套事件传递与嵌套事件消耗问题。

    这里其实存在一定的经验技巧,因为google本身提供了一些嵌套子View API,比如RecyclerView、NestedScrollView。这些API其实就是很好的源码实现参考对象。需要注意的地方就是,虽然这些API都实现了嵌套滚动机制,但是要考虑到不同的View本身的特性,对于细节需要个性化处理(这里有个前提,就是对参考的API源码整体有个把控,尽量选取源码比较清晰,结构简单的去参考,作者本人经过阅读,选取了更为简单的NestedScrollView源码实现作为参考)。当然,自己理清思路去处理细节也是可以的(头铁的大神们,膜拜)。

    言归正传,那么WebView具体如何实现嵌套滚动呢?

    2.1、实现嵌套接口,并委托给NestedScrollingChildHelper

    @Override

    public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,

                                    int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) {

       mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,

               offsetInWindow, type, consumed);

    }​

    // NestedScrollingChild2

    @Override

    public boolean startNestedScroll(int axes, int type) {

       return mChildHelper.startNestedScroll(axes, type);

    }

    @Override

    public void stopNestedScroll(int type) {

       mChildHelper.stopNestedScroll(type);

    }

    @Override

    public boolean hasNestedScrollingParent(int type) {

       return mChildHelper.hasNestedScrollingParent(type);

    }

    @Override

    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,

                                       int dyUnconsumed, int[] offsetInWindow, int type) {

       return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,

               offsetInWindow, type);

    }

    @Override

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,

                                          int type) {

       return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);

    }

    // NestedScrollingChild

    @Override

    public void setNestedScrollingEnabled(boolean enabled) {

       mChildHelper.setNestedScrollingEnabled(enabled);

    }

    @Override

    public boolean isNestedScrollingEnabled() {

       return mChildHelper.isNestedScrollingEnabled();

    }

    @Override

    public boolean startNestedScroll(int axes) {

       return startNestedScroll(axes, ViewCompat.TYPE_TOUCH);

    }

    @Override

    public void stopNestedScroll() {

       stopNestedScroll(ViewCompat.TYPE_TOUCH);

    }

    @Override

    public boolean hasNestedScrollingParent() {

       return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH);

    }

    @Override

    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,

                                       int dyUnconsumed, int[] offsetInWindow) {

       return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,

               offsetInWindow);

    }

    @Override

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {

       return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH);

    }

    @Override

    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {

       return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);

    }

    @Override

    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {

       return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);

    }

    2.2、嵌套滚动行为位置、嵌套滚动记录、滚动行为需要的变量定义

    private OverScroller mScroller;//滚动

    private int mTouchSlop;//判断当前move是否为拖动的判断条件

    private int mMinimumVelocity;//判断ACTION_UP后,当前是否应该执行fling的下限阈值

    private int mMaximumVelocity;//执行fling操作时,最大的速率阈值

    private boolean mIsBeingDragged = false;//标记当前行为是否为拖动行为

    private VelocityTracker mVelocityTracker;//速度追踪器

    /**

    * ID of the active pointer. This is used to retain consistency during

    * drags/flings if multiple pointers are used.

    */

    private int mActivePointerId = INVALID_POINTER;//这个用来保存多个手指交互操作的场景,多指交互也是个蛮有研究意义的点,感兴趣的可以深入了解下

    private static final int INVALID_POINTER = -1;

    /**

    * Position of the last motion event.

    */

    private int mLastMotionY;

    /**

    * Used during scrolling to retrieve the new offset within the window.

    */

    private final int[] mScrollOffset = new int[2];

    private final int[] mScrollConsumed = new int[2];

    private int mNestedYOffset;//用来修正嵌套滚动过程中Y坐标方向的实际位置

    private int mLastScrollerY;

    2.3、onTouch中如何实现嵌套流程,并不影响原有webView的特性

    @Override

    public boolean onTouchEvent(MotionEvent ev) {

       initVelocityTrackerIfNotExists();

    ​   final int actionMasked = ev.getActionMasked();

       if (actionMasked == MotionEvent.ACTION_DOWN) {

           mNestedYOffset = 0;

       }

       MotionEvent vtev = MotionEvent.obtain(ev);

       vtev.offsetLocation(0, mNestedYOffset);//修正Y方向实际位置

       switch (actionMasked) {

           case MotionEvent.ACTION_DOWN: {

               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) {

                   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 (mIsBeingDragged) {

                   mLastMotionY = y - mScrollOffset[1];

                   final int oldY = getScrollY();

                   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;

           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)) {//提前判断父View是否消耗完所有的fling消耗量,没有的话,剩余消耗量由子View去执行

                       dispatchNestedFling(0, -initialVelocity, true);//fling操作通知给父view,父view决定当前场景是否消耗

                       fling(-initialVelocity);//子View执行fling

                   }

               }

               mActivePointerId = INVALID_POINTER;

               endDrag();//结束嵌套滚动,重置资源

               break;

              //..........

       }

       if (mVelocityTracker != null) {

           mVelocityTracker.addMovement(vtev);

       }

      vtev.recycle();

    ​  return super.onTouchEvent(ev);//这里需要注意,onTouch里面具体是否消耗事件,不要屏蔽webView

       自身的逻辑,不然会影响webView本身内部的事件分发切换。

    }

    针对嵌套滚动过程中fling的操作,具体的细节在computeScroll方法里面。

    @Override

    public void computeScroll() {

       super.computeScroll();//这个方法不要去掉,保留webView原有的滚动执行逻辑

    ​   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);//在fling的过程中,将未消耗完的消耗量交给TYPE_NON_TOUCH类型的嵌套滚动处理,这里是预处理,提前计算,这里还没有开始滚动

       unconsumed -= mScrollConsumed[1];

       final int range = getScrollRange();

       if (unconsumed != 0) {

           // Internal Scroll

           final int oldScrollY = getScrollY();

           overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, range, 0, 0, false);

           final int scrolledByMe = getScrollY() - oldScrollY;

           unconsumed -= scrolledByMe;

           // Nested Scrolling Post Pass

           mScrollConsumed[1] = 0;

           dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, mScrollOffset,

                   ViewCompat.TYPE_NON_TOUCH, mScrollConsumed);//在fling的过程中,将未消耗完的消耗量交给TYPE_NON_TOUCH类型的嵌套滚动处理,这里是开始滚动

           unconsumed -= mScrollConsumed[1];

       }

    ​  if (unconsumed != 0) {//子view还是没有消耗完,停止子View的滚动操作

           abortAnimatedScroll();

       }

    ​   if (!mScroller.isFinished()) {

           ViewCompat.postInvalidateOnAnimation(this);

       }

    }

    相关文章

      网友评论

          本文标题:WebView嵌套能力的技术方案与实现总结

          本文链接:https://www.haomeiwen.com/subject/nearihtx.html