Webview+AppbarLayout上下滑动冲突

问题描述

AppBarLayout是material包里面提供的容器组件,可用于实现MD风格的页面滑动动效。

在开发场景中,我们经常使用CoordinatorLayout+AppBarLayout来实现页面头部的折叠和吸顶效果。在滑动过程中,View的行为随offset变化,View之间存在关联变化。

关于AppBarLayout的使用和嵌套滑动原理,可以见参考文档前三篇。

当AppBarLayout和Webview共同使用时,会出现一个问题,AppBarLayout无法折叠了,Webview只能在较小的固定区域内上下滑动,可以理解为两者上下滑动冲突了。

原因分析

使用AppBarLayout,我们一般是通过addOffsetChangedListener来监听嵌套滑动的位移的,从而修改AppBarLayout的appbarState,或者做一些透明度等视觉上的修改。但在这个场景下,offset始终为0,说明滑动事件并没有那我们预期处理。

滑动冲突问题的本质,是滑动事件的分发和消费不对。

在AppBarLayout中,是通过Behavior实现嵌套滑动的:若Behavior所附属的View存在可以滑动的子View,而且正在滑动的View足够大去滑动的话,就接受嵌套滑动事件.
那么问题就变成了,我们的WebView没有实现嵌套滑动机制。

解决方案

实现嵌套滑动机制,有两种方式:

  1. Android官方的例子中用NestedScrollView可以实现嵌套滑动,那么直接将WebView作为NestedScrollView直接子View即可实现WebView的嵌套滑动。
  2. 重写Webview,自己实现嵌套滑动逻辑。
    - 实现NestedScrollingChild嵌套滑动接口
    - 重写onTouchEvent方法

方式2在实现过程中,发现在WebView的惯性滑动下,滑动到顶也无法将AppBarLayout拉下来,触顶后必须再下滑一次才能将AppBarLayout拉下来,因此处理这个问题,需要额外增加一些处理逻辑.

添加一个速度追踪器(VelocityTracker)记录滑动速度,然后在ACTION_UP触摸事件出现时,获得这个速度,在ViewConfiguration中获得最大的滑动速度,然后再调用OverScroller来计算惯性滑动的距离,并且调用View的刷新从而实现调用computeScroll方法,然后在computeScroll方法中根据OverScroller计算的应该滑动的距离滑动到指定位置.

核心代码

private final int[] mScrollConsumed = new int[2];
private final int[] mScrollOffset = new int[2];
private int mLastMotionY;
private VelocityTracker mVelocityTracker;
private int mMinimumVelocity;
private int mMaximumVelocity;
private OverScroller mScroller;
private int mLastScrollerY;
private final NestedScrollingChildHelper mChildHelper;

// 初始化
public NestedScrollWebView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mChildHelper = new NestedScrollingChildHelper(this);
    setNestedScrollingEnabled(true);

    mScroller = new OverScroller(getContext());

    final ViewConfiguration configuration = ViewConfiguration.get(getContext());
    mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
    mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
}
private void initVelocityTrackerIfNotExists() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    public void fling(int velocityY) {
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
        mScroller.fling(getScrollX(), getScrollY(), // start
                0, velocityY, // velocities
                0, 0, // x
                Integer.MIN_VALUE, Integer.MAX_VALUE, // y
                0, 0); // overscroll
        mLastScrollerY = getScrollY();
        ViewCompat.postInvalidateOnAnimation(this);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            final int x = mScroller.getCurrX();
            final int y = mScroller.getCurrY();
            Log.d(TAG, "computeScroll: y : " + y);
            int dy = y - mLastScrollerY;
            if (dy != 0) {
                int scrollY = getScrollY();
                int dyUnConsumed = 0;
                int consumedY = dy;
                if (scrollY == 0) {
                    dyUnConsumed = dy;
                    consumedY = 0;
                } else if (scrollY + dy < 0) {
                    dyUnConsumed = dy + scrollY;
                    consumedY = -scrollY;
                }

                if (!dispatchNestedScroll(0, consumedY, 0, dyUnConsumed, null,
                        ViewCompat.TYPE_NON_TOUCH)) {

                }
            }

            // Finally update the scroll positions and post an invalidation
            mLastScrollerY = y;
            ViewCompat.postInvalidateOnAnimation(this);
        } else {
            // We can't scroll any more, so stop any indirect scrolling
            if (hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
                stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
            }
            // and reset the scroller y
            mLastScrollerY = 0;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        initVelocityTrackerIfNotExists();

        MotionEvent vtev = MotionEvent.obtain(event);

        final int actionMasked = event.getAction();

        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionY = (int) event.getRawY();
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                mVelocityTracker.addMovement(vtev);
                mScroller.computeScrollOffset();
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_UP:
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity();
                if (Math.abs(initialVelocity) > mMinimumVelocity) {
                    fling(-initialVelocity);
                }
            case MotionEvent.ACTION_CANCEL:
                stopNestedScroll();
                recycleVelocityTracker();
                break;
            case MotionEvent.ACTION_MOVE:
                final int y = (int) event.getRawY();
                int deltaY = mLastMotionY - y;
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                    vtev.offsetLocation(0, mScrollConsumed[1]);
                }
                mLastMotionY = y;

                int scrollY = getScrollY();

                int dyUnconsumed = 0;
                if (scrollY == 0) {
                    dyUnconsumed = deltaY;
                } else if (scrollY + deltaY < 0) {
                    dyUnconsumed = deltaY + scrollY;
                    vtev.offsetLocation(0, -dyUnconsumed);
                }
                mVelocityTracker.addMovement(vtev);
                boolean result = super.onTouchEvent(vtev);
                if (dispatchNestedScroll(0, deltaY - dyUnconsumed, 0, dyUnconsumed, mScrollOffset)) {

                }
                return result;
            default:

                break;
        }
        return super.onTouchEvent(vtev);
    }

    // NestedScrollingChild 嵌套滑动接口
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }

    @Override
    public boolean startNestedScroll(int axes, int type) {
        return mChildHelper.startNestedScroll(axes, type);
    }

    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }

    @Override
    public void stopNestedScroll(int type) {
        mChildHelper.stopNestedScroll(type);
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return mChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean hasNestedScrollingParent(int type) {
        return mChildHelper.hasNestedScrollingParent(type);
    }

    @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 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) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
                                           int type) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
    }

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

参考文档

基于AppBarLayout实现二级吸顶&踩坑记录
Android NestedScroll嵌套滑动机制解析
​Android CoordinatorLayout和Behavior解析
WebView和AppBarLayout嵌套滑动联动无效分析及解决办法

你可能感兴趣的:(Android,H5,webview,android,AppbarLayout,滑动冲突)