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

2020-04-20  本文已影响0人  willimes

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

   }

}

上一篇下一篇

猜你喜欢

热点阅读