Android使用ScrollView导致鼠标点击事件无效

Android使用ScrollView导致鼠标点击事件无效_第1张图片

平台

测试平台:

  • RK3288 + Android8.1
  • RK3588 + Android 12

问题

 首先, 这个问题的前提是, 使用的输入设备是**鼠标**, 普通的触摸屏并不会出现这个问题. 大致的流程是APP的UI布局中采用ScrollView作为根容器, 之后添加各类子控件, 在一起准备就绪后, 使用鼠标进行功能测试, 出现无法点击控件触发事件响应.
<ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <LinearLayout
            style="@style/settingsItems">
            <TextView style="@style/TV"
                android:text="XXX"
                android:layout_weight="1"
                />
            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="BTN"/>
        LinearLayout>
        <LinearLayout
            style="@style/settingsItems">
            <TextView style="@style/TV"
                android:text="XXX"
                android:layout_weight="1"
                />
            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="BTN"/>
        LinearLayout>
				
    LinearLayout>
ScrollView>

分析

最先从onInterceptTouchEvent函数入手, 各个层级的LOG大致分析, 由下往上发现控件BUTTON中根本没有捕获到MotionEvent, 那原因只可能是父控件自己捕获了而不下发. 兜兜转转最终来到了ScrollView.

通过重写onInterceptHoverEvent 判断是否时间已被捕获

  @Override
	public boolean onInterceptHoverEvent(MotionEvent event) {
		boolean b = super.onInterceptHoverEvent(event);
		Logger.d(TAG, "onInterceptHoverEvent " + b);
		return b;
	}

从输出的LOG可以看出来, 当使用鼠标的时候, TRUE 和 FALSE 均有可能出现(在后面排查是才发现这和控件处的位置有关), 当TRUE是, 说明事件由ScrollView处理了, 子控件自然就接收不到事件下发.

顺着onInterceptHoverEvent往上查:

  • frameworks/base/core/java/android/view/ViewGroup.java

        public boolean onInterceptHoverEvent(MotionEvent event) {
            if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
                final int action = event.getAction();
                final float x = event.getX();
                final float y = event.getY();
                if ((action == MotionEvent.ACTION_HOVER_MOVE
                        || action == MotionEvent.ACTION_HOVER_ENTER) && isOnScrollbar(x, y)) {
                    return true;
                }
            }
            return false;
        }
    

    从代码中可以看出, 基本的判断条件都是成立的, 鼠标输入 + MOVE/ENTER时间, 最后一个是isOnScrollbar, 顾名思义输入鼠标的位置在ScrollBar 上?

  • frameworks/base/core/java/android/view/View.java

    
        boolean isOnScrollbar(float x, float y) {
            if (mScrollCache == null) {
                return false;
            }
            x += getScrollX();
            y += getScrollY();
            if (isVerticalScrollBarEnabled() && !isVerticalScrollBarHidden()) {
                final Rect touchBounds = mScrollCache.mScrollBarTouchBounds;
                getVerticalScrollBarBounds(null, touchBounds);
                if (touchBounds.contains((int) x, (int) y)) {
                    return true;
                }
            }
            if (isHorizontalScrollBarEnabled()) {
                final Rect touchBounds = mScrollCache.mScrollBarTouchBounds;
                getHorizontalScrollBarBounds(null, touchBounds);
                if (touchBounds.contains((int) x, (int) y)) {
                    return true;
                }
            }
            return false;
        }
    
        private void getVerticalScrollBarBounds(@Nullable Rect bounds, @Nullable Rect touchBounds) {
            if (mRoundScrollbarRenderer == null) {
                getStraightVerticalScrollBarBounds(bounds, touchBounds);
            } else {
                getRoundVerticalScrollBarBounds(bounds != null ? bounds : touchBounds);
            }
        }
        private void getStraightVerticalScrollBarBounds(@Nullable Rect drawBounds,
                @Nullable Rect touchBounds) {
            final Rect bounds = drawBounds != null ? drawBounds : touchBounds;
            if (bounds == null) {
                return;
            }
            final int inside = (mViewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0;
            final int size = getVerticalScrollbarWidth();
            int verticalScrollbarPosition = mVerticalScrollbarPosition;
            if (verticalScrollbarPosition == SCROLLBAR_POSITION_DEFAULT) {
                verticalScrollbarPosition = isLayoutRtl() ?
                        SCROLLBAR_POSITION_LEFT : SCROLLBAR_POSITION_RIGHT;
            }
            final int width = mRight - mLeft;
            final int height = mBottom - mTop;
            switch (verticalScrollbarPosition) {
                default:
                case SCROLLBAR_POSITION_RIGHT:
                    bounds.left = mScrollX + width - size - (mUserPaddingRight & inside);
                    break;
                case SCROLLBAR_POSITION_LEFT:
                    bounds.left = mScrollX + (mUserPaddingLeft & inside);
                    break;
            }
            bounds.top = mScrollY + (mPaddingTop & inside);
            bounds.right = bounds.left + size;
            bounds.bottom = mScrollY + height - (mUserPaddingBottom & inside);
    
            if (touchBounds == null) {
                return;
            }
            if (touchBounds != bounds) {
                touchBounds.set(bounds);
            }
            final int minTouchTarget = mScrollCache.scrollBarMinTouchTarget;
            if (touchBounds.width() < minTouchTarget) {
                final int adjust = (minTouchTarget - touchBounds.width()) / 2;
                if (verticalScrollbarPosition == SCROLLBAR_POSITION_RIGHT) {
                    touchBounds.right = Math.min(touchBounds.right + adjust, mScrollX + width);
                    touchBounds.left = touchBounds.right - minTouchTarget;
                } else {
                    touchBounds.left = Math.max(touchBounds.left + adjust, mScrollX);
                    touchBounds.right = touchBounds.left + minTouchTarget;
                }
            }
            if (touchBounds.height() < minTouchTarget) {
                final int adjust = (minTouchTarget - touchBounds.height()) / 2;
                touchBounds.top -= adjust;
                touchBounds.bottom = touchBounds.top + minTouchTarget;
            }
        }
    
    /**
         * 

    ScrollabilityCache holds various fields used by a View when scrolling * is supported. This avoids keeping too many unused fields in most * instances of View.

    */
    private static class ScrollabilityCache implements Runnable { /** * Scrollbars are not visible */ public static final int OFF = 0; /** * Scrollbars are visible */ public static final int ON = 1; /** * Scrollbars are fading away */ public static final int FADING = 2; public boolean fadeScrollBars; public int fadingEdgeLength; public int scrollBarDefaultDelayBeforeFade; public int scrollBarFadeDuration; public int scrollBarSize; public int scrollBarMinTouchTarget; public ScrollBarDrawable scrollBar; public float[] interpolatorValues; public View host; public final Paint paint; public final Matrix matrix; public Shader shader; public final Interpolator scrollBarInterpolator = new Interpolator(1, 2); private static final float[] OPAQUE = { 255 }; private static final float[] TRANSPARENT = { 0.0f }; /** * When fading should start. This time moves into the future every time * a new scroll happens. Measured based on SystemClock.uptimeMillis() */ public long fadeStartTime; /** * The current state of the scrollbars: ON, OFF, or FADING */ public int state = OFF; private int mLastColor; public final Rect mScrollBarBounds = new Rect(); public final Rect mScrollBarTouchBounds = new Rect(); public static final int NOT_DRAGGING = 0; public static final int DRAGGING_VERTICAL_SCROLL_BAR = 1; public static final int DRAGGING_HORIZONTAL_SCROLL_BAR = 2; public int mScrollBarDraggingState = NOT_DRAGGING; public float mScrollBarDraggingPos = 0; public ScrollabilityCache(ViewConfiguration configuration, View host) { fadingEdgeLength = configuration.getScaledFadingEdgeLength(); scrollBarSize = configuration.getScaledScrollBarSize(); scrollBarMinTouchTarget = configuration.getScaledMinScrollbarTouchTarget(); scrollBarDefaultDelayBeforeFade = ViewConfiguration.getScrollDefaultDelay(); scrollBarFadeDuration = ViewConfiguration.getScrollBarFadeDuration(); paint = new Paint(); matrix = new Matrix(); // use use a height of 1, and then wack the matrix each time we // actually use it. shader = new LinearGradient(0, 0, 0, 1, 0xFF000000, 0, Shader.TileMode.CLAMP); paint.setShader(shader); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); this.host = host; } public void setFadeColor(int color) { if (color != mLastColor) { mLastColor = color; if (color != 0) { shader = new LinearGradient(0, 0, 0, 1, color | 0xFF000000, color & 0x00FFFFFF, Shader.TileMode.CLAMP); paint.setShader(shader); // Restore the default transfer mode (src_over) paint.setXfermode(null); } else { shader = new LinearGradient(0, 0, 0, 1, 0xFF000000, 0, Shader.TileMode.CLAMP); paint.setShader(shader); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); } } } public void run() { long now = AnimationUtils.currentAnimationTimeMillis(); if (now >= fadeStartTime) { // the animation fades the scrollbars out by changing // the opacity (alpha) from fully opaque to fully // transparent int nextFrame = (int) now; int framesCount = 0; Interpolator interpolator = scrollBarInterpolator; // Start opaque interpolator.setKeyFrame(framesCount++, nextFrame, OPAQUE); // End transparent nextFrame += scrollBarFadeDuration; interpolator.setKeyFrame(framesCount, nextFrame, TRANSPARENT); state = FADING; // Kick off the fade animation host.invalidate(true); } } }

View中的代码有点多, 简单的来说, 就是isOnScrollbar 这个函数通过获取ScrollBar的位置大小信息判断输入的事件是否处于其捕获的范围.

通过反射调用getVerticalScrollBarBounds并输出读取的信息: touchRect=[1464,0][1512,674], 基本可以判定是滚动条的位置.


Rect touchRect = new Rect();
getVerticalScrollBarBoundsRe(null, touchRect);
Logger.d(TAG, "touchRect=" + touchRect.toShortString());

void getVerticalScrollBarBoundsRe(Rect r, Rect r2){
		try {
			@SuppressLint("SoonBlockedPrivateApi")
			Method getVerticalScrollBarBounds = View.class.getDeclaredMethod("getVerticalScrollBarBounds", Rect.class, Rect.class);
			getVerticalScrollBarBounds.setAccessible(true);
			getVerticalScrollBarBounds.invoke(this, r, r2);
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
	}

计算宽度1512 - 1464 = 48, 这个宽度默认在系统中又定义, 如果是自定义的ScrollBar则大小不一定是48, 根据configuration.getScaledMinScrollbarTouchTarget();查到源码的定义如下:

  • frameworks/base/core/java/android/view/ViewConfiguration.java

    private static final int MIN_SCROLLBAR_TOUCH_TARGET = 48;
    
    /**
     * @return the minimum size of the scrollbar thumb's touch target in pixels
     * @hide
     */
    public int getScaledMinScrollbarTouchTarget() {
        return mMinScrollbarTouchTarget;
    }
    

问题的根源如下图所示, 红色滚动条的宽度为48:

Android使用ScrollView导致鼠标点击事件无效_第2张图片

PS: 上图中的滚动条默认情况下并没有显示出来.

解决方法

  1. 修改XML中ScrollView的属性android:scrollbars="none”
  2. 避免需要输入的控件显示在ScrollBar的下方, 考虑给子控件加个padding或margin
  3. 自定义ScrollView, 优化onInterceptHoverEvent函数

你可能感兴趣的:(android,android,鼠标,ScrollView)