WebView嵌套在ScrollView引起的滑动问题,以及事件分发情况

本文分析了当WebView嵌套在ScrollView中导致的滑动问题,由于ScrollView会拦截并处理滑动事件,使得WebView无法正常滚动。详细解释了Touch事件的分发和消费机制,并提供了两种解决方案:1. 自定义ScrollView,当检测到双指缩放时,不拦截事件;2. 自定义WebView,主动请求父ViewGroup不要拦截事件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前提知识

Touch事件的分发和消费机制
Note: 如果onTouchEvent中返回true, 则表示消费了这个事件,这个事件就不会再传递下去了,如果子View return true, 那么父ViewGroup的onTouchEvent就不会执行。

原因分析

ScrollView的onInterceptTouchEvent源码:

 public boolean onInterceptTouchEvent(MotionEvent ev) {
        /**
        * 从这里可以看出当前Event为MOVE,且mIsBeingDragged(滑动)为true时,就拦截
        * Event. 记住是MOVE.
        */
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }

        //省略代码

        switch (action & MotionEvent.ACTION_MASK) {

            //省略代码        

            case MotionEvent.ACTION_DOWN: {
                final int y = (int) ev.getY();
                if (!inChild((int) ev.getX(), (int) y)) {
                    mIsBeingDragged = false;
                    recycleVelocityTracker();
                    break;
                }

                /*
                 * Remember location of down touch.
                 * ACTION_DOWN always refers to pointer index 0.
                 */
                mLastMotionY = y;
                mActivePointerId = ev.getPointerId(0);

                initOrResetVelocityTracker();
                mVelocityTracker.addMovement(ev);
                /*
                * If being flinged and user touches the screen, initiate drag;
                * otherwise don't.  mScroller.isFinished should be false when
                * being flinged.
                */
                mIsBeingDragged = !mScroller.isFinished();
                //从这里可以看出在DOWN事件时,是不拦截DOWN事件的,此DOWN事件会传递
                //到子View中,这样子View就可调用onTouchEvent,不过此时是DOWN事件
                if (mIsBeingDragged && mScrollStrictSpan == null) {
                    mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
                }
                //初始化Scroll
                startNestedScroll(SCROLL_AXIS_VERTICAL);
                break;
            }

            //省略代码
        }

        /*
        * The only time we want to intercept motion events is if we are in the
        * drag mode.
        */
        return mIsBeingDragged;
    }

ScrollView的onTouchEvent源码:.

public boolean onTouchEvent(MotionEvent ev) {
        //省略代码
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }
        vtev.offsetLocation(0, mNestedYOffset);

        switch (actionMasked) {
            //省略代码
            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)) {
                    deltaY -= mScrollConsumed[1];
                    vtev.offsetLocation(0, mScrollOffset[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) {
                    // Scroll to follow the motion event
                    mLastMotionY = y - mScrollOffset[1];

                    final int oldY = mScrollY;
                    final int range = getScrollRange();
                    final int overscrollMode = getOverScrollMode();
                    boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

                    // Calling overScrollBy will call onOverScrolled, which
                    // calls onScrollChanged if applicable.
                    //可以看到在MOVE时,会调用滑动overScrollBy
                    if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
                            && !hasNestedScrollingParent()) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }

                    final int scrolledDeltaY = mScrollY - oldY;
                    final int unconsumedY = deltaY - scrolledDeltaY;
                    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                        mLastMotionY -= mScrollOffset[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    } else if (canOverscroll) {
                        final int pulledToY = oldY + deltaY;
                        if (pulledToY < 0) {
                            mEdgeGlowTop.onPull((float) deltaY / getHeight(),
                                    ev.getX(activePointerIndex) / getWidth());
                            if (!mEdgeGlowBottom.isFinished()) {
                                mEdgeGlowBottom.onRelease();
                            }
                        } else if (pulledToY > range) {
                            mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
                                    1.f - ev.getX(activePointerIndex) / getWidth());
                            if (!mEdgeGlowTop.isFinished()) {
                                mEdgeGlowTop.onRelease();
                            }
                        }
                        if (mEdgeGlowTop != null
                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
                            postInvalidateOnAnimation();
                        }
                    }
                }
                break;
            //省略代码
        return true;
    }

如果把WebView嵌套在ScrollView中,滑动时,函数调用情况为:ScrollView的onInterceptTouchEvent获取到DOWN事件,并传递DOWN事件给WebView, 随后MOVE事件来到,现在经过ScrollView的onInterceptTouchEvent,发现被拦截了,MOVE事件也就不会传递到WebView中,那么调用ScrollView的onTouchEvent,在onTouchEvent中进行实际的scroll动作。总的来说,在Scroll时,就是ScrollView总是响应,WebView不会执行Scroll,这样造成的现象是,一旦WebView中网页放大,那么就无法通过scroll来查看WebView中整个放大后的内容,只能看到当前已经放大的屏幕大小的内容,因此scroll动作在WebView中没有执行,而是在ScrollView中执行了。

解决方案

1.WebView中的Scale动作会被ScrollView影响,解决方案:
自定义一个ScrollView,在onInterceptTouchEvent中检测到当前有两个手指,就是Scale,就不拦截,让WebView来处理这个动作。

public class PoorPriorityScrollView extends ScrollView {


    public PoorPriorityScrollView(Context context) {
        this(context, null);
    }

    public PoorPriorityScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PoorPriorityScrollView(Context context, AttributeSet attrs,
            int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getPointerCount() > 1) {
            return false;
        }

        return super.onInterceptTouchEvent(ev);
    }

}
  1. 自定义WebView,在onTouchEvent中调用requestDisallowInterceptTouchEvent,请求父ViewGroup不要拦截事件:
public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
                //首先DOWN事件,无论怎样都会传递到WebView中,这时
                //可以调用requestDisallowInterceptTouchEvent,让Scroll
                //View不拦截MOVE事件
        case MotionEvent.ACTION_DOWN:
                getParent().getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
        //在MOVE事件中,我们确定两种情况Scroll是需要ScrollView来执行
        //如果WebView中的内容滑到顶部,这时就由ScrollView来执行
        //Scroll动作。如果WebView中的内容滑到底部,这时就由
        //ScrollView来执行Scroll动作。其他情况Scroll动作都由WebView
        //来执行。
                boolean scroll = true;
                if (isTop()) {     //是否滑到顶部
                    scroll = false;
                } else if (isBottom()){    //是否滑到底部
                    scroll = false;
                }
                getParent().getParent().requestDisallowInterceptTouchEvent(scroll);

            break;
        case MotionEvent.ACTION_UP:
             getParent().getParent().requestDisallowInterceptTouchEvent(false);
        }

        return super.onTouchEvent(event);
    }

    private boolean isBottom() {
        float htmlHeight = getContentHeight() * getScale();
        float measuredHeight = getMeasuredHeight();
        float currentheight = getHeight() + getScrollY();
        Log.d("xuchun", htmlHeight + ", " + measuredHeight + ", " + getHeight() + ", " + getScrollY());
        return htmlHeight == currentheight;
    }

    private boolean isTop() {
        return getScrollY() == 0;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值