Android使用ScrollView导致鼠标点击事件无效
平台
测试平台:
- RK3288 + Android8.1
- RK3588 + Android 12
问题
首先, 这个问题的前提是, 使用的输入设备是**鼠标**, 普通的触摸屏并不会出现这个问题. 大致的流程是APP的UI布局中采用ScrollView作为根容器, 之后添加各类子控件, 在一起准备就绪后, 使用鼠标进行功能测试, 出现无法点击控件触发事件响应.
<ScrollViewxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><LinearLayoutstyle="@style/settingsItems"><TextView style="@style/TV"android:text="XXX"android:layout_weight="1"/><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="BTN"/></LinearLayout><LinearLayoutstyle="@style/settingsItems"><TextView style="@style/TV"android:text="XXX"android:layout_weight="1"/><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="BTN"/></LinearLayout><!--可以写多几个--></LinearLayout>
</ScrollView>
分析
最先从onInterceptTouchEvent
函数入手, 各个层级的LOG大致分析, 由下往上发现控件BUTTON中根本没有捕获到MotionEvent, 那原因只可能是父控件自己捕获了而不下发. 兜兜转转最终来到了ScrollView.
通过重写onInterceptHoverEvent 判断是否时间已被捕获
@Overridepublic 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// transparentint nextFrame = (int) now;int framesCount = 0;Interpolator interpolator = scrollBarInterpolator;// Start opaqueinterpolator.setKeyFrame(framesCount++, nextFrame, OPAQUE);// End transparentnextFrame += scrollBarFadeDuration;interpolator.setKeyFrame(framesCount, nextFrame, TRANSPARENT);state = FADING;// Kick off the fade animationhost.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: 上图中的滚动条默认情况下并没有显示出来.
解决方法
- 修改XML中ScrollView的属性
android:scrollbars="none”
- 避免需要输入的控件显示在ScrollBar的下方, 考虑给子控件加个padding或margin
- 自定义ScrollView, 优化
onInterceptHoverEvent
函数
相关文章:

Android使用ScrollView导致鼠标点击事件无效
平台 测试平台: RK3288 Android8.1RK3588 Android 12 问题 首先, 这个问题的前提是, 使用的输入设备是**鼠标**, 普通的触摸屏并不会出现这个问题. 大致的流程是APP的UI布局中采用ScrollView作为根容器, 之后添加各类子控件, 在一起准备就绪后, 使用鼠标进行功能测试, 出现…...

【开源】SpringBoot框架开发大学计算机课程管理平台
目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 实验课程档案模块2.2 实验资源模块2.3 学生实验模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 实验课程档案表3.2.2 实验资源表3.2.3 学生实验表 四、系统展示五、核心代码5.1 一键生成实验5.2 提交实验5.3 批阅实…...

Mac Shift切换输入法 - shift切换中英文 - Karabiner-Elements
转载自 https://www.jianshu.com/p/677ae7d9beda...

智慧港口:山海鲸可视化引领未来
随着疫情的结束,全球贸易迎来新的春天,港口作为物流枢纽的地位日益凸显。然而,传统港口的运营和管理方式已无法满足现代物流的需求。为了提高港口运营效率,降低成本,智慧港口的概念应运而生。作为山海鲸可视化的开发者…...

Linux 网络编程 + 笔记
协议:一组规则 分层模型结构: OSI七层模型:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层TCP/IP 4层模型:链路层/网络接口层、网络层、传输层、应用层 应用层:http、ftp、nfs、ssh、telnet、传输层&am…...
顺序表应用3:元素位置互换之移位算法
顺序表应用3:元素位置互换之移位算法 Description 一个长度为len(1<len<1000000)的顺序表,数据元素的类型为整型,将该表分成两半,前一半有m个元素,后一半有len-m个元素(1<m<len),借…...
Luogu P6066 [USACO05JAN] Watchcow S 题解 欧拉回路
题目链接:Luogu P6066 [USACO05JAN] Watchcow S 欧拉回路 题目描述: 给定一张无向图,输出任意一条从一号结点出发的欧拉回路(欧拉回路指每条无向边来回经过且只经过一次),给定的图保证这样的欧拉回路存在。…...

计算机网络_1.6.3 计算机网络体系结构分层思想举例
1.6.3 计算机网络体系结构分层思想举例 1、实例引入(用户在主机中使用浏览器访问web服务器)2、从五层原理体系结构的角度研究该实例3、练习题 笔记来源: B站 《深入浅出计算机网络》课程 本节通过一个常见的网络应用实例,来介绍计…...
图论练习1
内容:,拆点,分层,传递,带限制的最小生成树 [HNOI2015]菜肴制作 题目链接 题目大意 有个限制,号菜肴在号前完成在满足限制的条件下,按照出菜( 是为了满足的限制 ) 解题思路 由限制…...

canvas设置图形各种混合模式,类似photoshop效果
查看专栏目录 canvas实例应用100专栏,提供canvas的基础知识,高级动画,相关应用扩展等信息。canvas作为html的一部分,是图像图标地图可视化的一个重要的基础,学好了canvas,在其他的一些应用上将会起到非常重…...
谷粒商城-P19
项目结构创建&提交到码云 数据库初始化 保持docker数据库一直打开 docker update redis --restartalways 连不上了,发现配置文件错了 换了一个配置文件。 快速开发 使用开源的脚手架 人人开源 (gitee.com) 使用renren-fast作为后台开发,使用…...
Vue3入门到实战笔记02
9. watch 作用:监视数据的变化(和Vue2中的watch作用一致)特点:Vue3中的watch只能监视以下四种数据: ref定义的数据。reactive定义的数据。函数返回一个值(getter函数)。一个包含上述内容的数组…...
CDN高防IP:技术解析与相关问题解答
在使用CDN高防IP技术的过程中,可能会遇到一些问题。下面是一些常见问题和相应的解答,希望能帮助读者更好地了解和使用CDN高防IP技术。 问题一:什么是CDN高防IP技术? 解答:CDN高防IP技术是一种通过将网站流量分散到多…...

【React】react组件传参
【React】react组件传参 一、props:父组件向子组件传参1、将普通的参数作为props传递2、将jsx作为props传递(组件插槽)(1)基础功能示例(2)进阶示例 二、自定义事件:子父组件向父组件…...
Vue/html中点击复制到剪贴板
1.使用JQ实现复制功能 html <div class"tran_one tran_yi"><div class"form-group"><textarea>文本</textarea></div><div class"form-group tran_group"><div class"tran_flex tran_left tran_co…...

MtfLive直播导航PHP源码,附带系统搭建教程
将自动采集斗鱼、虎牙、触手、YY、章鱼、电视直播按分类/关键词聚合,用户选择分类,可以观看到全网该关键词下正在直播的内容。 特点 PC站和H5移动站自适应 自动缓存,避免频繁抓取数据 自定义抓取采集规则,同时支持HTML和JSON …...
day19 初始HTML
什么是HTML HTML(Hyper Text Markup Language)超文本标记语言 超文本包括:文字、图片、音频、视频、动画等 HTML5,提供了一些新的元素和一些有趣的新特性,同时也建立了一些新的规则。这些元素、特性和规则的建立&…...
从编程中理解:退一步海阔天空
编程中,“退一步海阔天空”的理念指的是在面对问题时,有时过于纠结于细节或局部优化,反而会陷入困境。这时,如果能暂时放下手中的具体工作,从更高的层面或者换个角度来审视整个系统的设计和架构,可能会发现…...

【前沿技术杂谈:开源软件】引领技术创新与商业模式的革命
【前沿技术杂谈:开源软件】引领技术创新与商业模式的革命 开源软件如何推动技术创新开源软件的开放性和协作精神促进知识共享和技术迭代推动关键技术的发展开源软件与新技术的融合 开源软件的商业模式开源软件的商业模式将开源软件与商业软件相结合 开源软件的安全风…...
c# datatable 通过反射转成泛型list
在C#中,可以使用反射来将DataTable转换为泛型列表。下面是一个示例代码,展示了如何使用反射来实现这个转换过程: using System; using System.Collections.Generic; using System.Data;public class DataConverter {public List<T> Co…...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...

学校招生小程序源码介绍
基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...

家政维修平台实战20:权限设计
目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系,主要是分成几个表,用户表我们是记录用户的基础信息,包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题,不同的角色…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...

《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...
稳定币的深度剖析与展望
一、引言 在当今数字化浪潮席卷全球的时代,加密货币作为一种新兴的金融现象,正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而,加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下,稳定…...

【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...

HashMap中的put方法执行流程(流程图)
1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中,其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下: 初始判断与哈希计算: 首先,putVal 方法会检查当前的 table(也就…...