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

在这里插入图片描述

平台

测试平台:

  • 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;
            }
        }
    
    /**
         * <p>ScrollabilityCache holds various fields used by a View when scrolling
         * is supported. This avoids keeping too many unused fields in most
         * instances of View.</p>
         */
        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:

在这里插入图片描述

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

解决方法

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/366120.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Oracle云】OCI DevOps Services 教程 (3) - 创建自动化部署流水线【CD流水线】

欢迎来到【Oracle云】OCI DevOps Services教程的第三部分&#xff01;在这一部分中&#xff0c;我们将介绍如何在OCI上创建自动化部署流水线&#xff0c;也称为持续部署&#xff08;CD&#xff09;流水线。OCI DevOps Services提供了一套标准的构建工具&#xff0c;帮助我们简化…

解决WARNING: IPv4 forwarding is disabled. Networking will not work的具体操作步骤

IPv4转发禁用警告&#xff1a;网络无法正常工作 在使用网络连接的过程中&#xff0c;我们可能遇到警告消息“WARNING: IPv4 forwarding is disabled. Networking will not work”&#xff08;警告&#xff1a;IPv4转发已禁用&#xff0c;网络将无法正常工作&#xff09;。这个…

2.2号作业

一 1.A 2. A 3. A 4. D 5. D 6. B 7. A 8. C 9. A 10. 二 1. 42 2. cerr 3. cout <<"\n" 4. 5 5. 0 6. 7. 20 8. 1.50 9. ssi2 i…

UE4学习笔记 FPS游戏制作2 制作第一人称控制器

文章目录 章节目标前置概念Rotator与Vector&#xff1a;roll与yaw与pitch 添加按键输入蓝图结构区域1区域2区域3区域4 章节目标 本章节将实现FPS基础移动 前置概念 Rotator与Vector&#xff1a; Vector是用向量表示方向&#xff0c;UE中玩家的正前方是本地坐标系的(1,0,0)&…

ARM架构可视化ROS消息方案部署

ARM架构可视化ROS消息方案部署 三种方案, 1. webviz 2. foxglove 3. rosviz 注: web要用firefox, chromimum用不了, 可能是因为取消了时间同步机制的原因 先说三种方案的优劣, webviz 延迟比较高, 但是部署相对简单, foxglove 部署比较费劲, 但是效果不错, 延迟低, 本文会尽…

k8s kubeadm部署安装详解

目录 kubeadm部署流程简述 环境准备 步骤简述 关闭 防火墙规则、selinux、swap交换 修改主机名 配置节点之间的主机名解析 调整内核参数 所有节点安装docker 安装依赖组件 配置Docker 所有节点安装kubeadm&#xff0c;kubelet和kubectl 定义kubernetes源并指定版本…

带大家做一个,易上手的家常芹菜炒腊肉

起锅 放入腊肉 加入清水 水包裹住腊肉即可 盖上盖子煮15分钟 一把芹菜 芹菜切段 清水洗干净 15分钟后 捞出腊肉 将腊肉切片 一块生姜 两瓣蒜 将蒜切片 单独装起来 生姜切片 和三个干辣椒装一起 起锅 下腊肉 不要放油 腊肉够油了 翻炒开后 倒入生姜 干辣椒 翻炒个一分…

STM32CAN2进入bus off 模式

工作遇到的问题记录 无人机CAN2整个进不了中断&#xff0c;通过查看寄存器判定出CAN节点进入了bus off mode 为何进入bus off &#xff0c;最后通过示波器看到整个CAN2总线波形就不对&#xff0c;总线出现了错误 Busoff的产生是一定是因为节点自身识别到自己发送错误&#xff…

Modbus协议学习第七篇之libmodbus库API介绍(modbus_write_bits等)

写在前面 在第六篇中我们介绍了基于libmodbus库的演示代码&#xff0c;那本篇博客就详细介绍一下第六篇的代码中使用的基于该库的API函数。另各位读者&#xff0c;Modbus相关知识受众较少&#xff0c;如果觉得我的专栏文章有帮助&#xff0c;请一定点个赞&#xff0c;在此跪谢&…

oracle19C 密码包含特殊字符@ 导致ORA-12154

oracle 19C 密码包含特殊字符 出现登录失败&#xff0c;针对此问题一次说个明白 ORA-12154: TNS:could not resolve the connect identifier specified Oracle 19c之前密码是可以包含特殊字符&#xff0c;但是如果包含特殊字符需要双引号 比如oracle11g 正常 如果密码包含特殊…

Redis的bitmap使用不当,我内存爆了

背景 最近发现Redis的内存持续暴涨&#xff0c; 涨的有点吓人&#xff0c;机器都快扛不住了&#xff0c;不得不进行Redis内存可视化分析&#xff0c;发现大量的String类型的大key 经分析&#xff0c;最近上线了页面UV的统计&#xff0c;那目前如何做的呢&#xff1f; 通过访…

【iOS ARKit】2D肢体动作捕捉

人体肢体动作捕捉在动漫影视制作、游戏CG 动画、实时模型驱动中有着广泛的应用&#xff0c;利用 ARKit&#xff0c;无须额外的硬件设备即可实现 2D和3D人体一系列关节和骨骼的动态捕捉&#xff0c;由于移动AR 的便携性及低成本&#xff0c;必将促进相关产业的发展。 ARBody Tr…

图片热区功能

一、需求描述及效果图 1.需求描述&#xff1a; 根据后端返回的坐标及人员信息&#xff0c;在图片上的相应位置添加图片热区功能&#xff0c;点击可展示出对应的人员信息。 图片可进行缩放 2.示例&#xff1a; &#xff08;定位是随便写的&#xff0c;仅做示例&#xff09; …

【Algorithms 4】算法(第4版)学习笔记 03 - 1.3 背包、队列和栈

文章目录 前言参考目录学习笔记0&#xff1a;预热1&#xff1a;栈1.1&#xff1a;栈的链表实现1.1.1 代码实现1.2&#xff1a;栈的数组实现1.2.1&#xff1a;定容栈1.2.2&#xff1a;可调整大小数组1.2.3&#xff1a;代码实现1.3&#xff1a;链表与数组的取舍2&#xff1a;队列…

MySQL原理(一)架构组成之逻辑模块(1)组成

总的来说&#xff0c;MySQL可以看成是二层架构&#xff0c;第一层我们通常叫做SQL Layer&#xff0c;在MySQL数据库系统处理底层数据之前的所有工作都是在这一层完成的&#xff0c;包括权限判断&#xff0c;sql解析&#xff0c;执行计划优化&#xff0c;query cache的处理等等&…

算法——A/算法通识

目录 一、复杂度分析 A/时间复杂度 B/空间复杂度 C/分析技巧 二、枚举分析 A/枚举算法介绍 B/解空间的类型 C/循环枚举解空间 三、模拟算法 四、递归 A/递归介绍 递归的两个关键要素&#xff1a; B/递归如何实现 C/递归和循环的比较 一、复杂度分析 A/时间复杂度…

AVL树

文章目录 AVL树平衡因子 AVL树结点的定义AVL树类和函数接口AVL树插入元素最小不平衡子树旋转 AVL树的验证参考源码 AVL树是对普通二叉搜索树的一种优化。当二叉搜索树插入的元素是有序的时候或者接近有序的时候&#xff0c;二叉搜索树的性能会大大降低。二叉搜索树可能会变成一…

中二少年工具箱(PC端)简介

同学们可以私信我加入学习群&#xff01; 正文开始 简介一、功能模块1.node版本管理工具 总结 简介 中二少年开发的中二少年工具箱&#xff0c;相信博主&#xff0c;功能不孬。 辅助自己开发工作&#xff0c;帮助新人快速入门&#xff0c;提供交互式文档辅助学习……如果还不…

LDRA Testbed软件静态分析_Jenkins持续集成_(2)配置邮件自动发送静态分析结果

系列文章目录 LDRA Testbed软件静态分析_操作指南 LDRA Testbed软件静态分析_自动提取静态分析数据生成文档 LDRA Testbed软件静态分析_Jenkins持续集成_(1)自动进行静态分析的环境搭建 LDRA Testbed软件静态分析_Jenkins持续集成_(2)配置邮件自动发送静态分析结果 LDRA Testb…

arcgis自定义dem高程实现地形抬高 - 操作矢量,转tin、adf(tif),cesiumlab切高程服务

这次记录分享一下arcgis自定义高程全过程 /(ㄒoㄒ)/~~ 我的场景&#xff1a;前端实现地面抬高效果 自定义高程实现地形抬高 一、数据处理 - arcgis操作矢量1、准备工作&#xff08;可选&#xff09;2、绘制外围矢量&#xff08;可选&#xff09;3、操作矢量数据 二、创建tin - …