当前位置: 首页 > news >正文

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: 上图中的滚动条默认情况下并没有显示出来.

解决方法

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

相关文章:

  • LeetCode 热题 100 | 链表(上)
  • 解决Docker AList本地挂载失效的问题。
  • 免费电视TV盒子软件,好用的免费电视盒子软件大全,免费电视盒子APP大全,2024最新整理
  • 影院购票|电影院订票选座小程序|基于微信小程序的电影院购票系统设计与实现(源码+数据库+文档)
  • npm 以组织为单位发布依赖包(@username/package-name、@org-name/package-name)
  • 【全网最全】2024美赛ABCDEF题思路模型全解(后续会更新)
  • go语言标准库flag命令行参数解析
  • HDFS Federation前世今生
  • ChatGPT炸裂了
  • Mac安装及配置MySql及图形化工具MySQLworkbench安装
  • 贪吃蛇/链表实现(C/C++)
  • Django_基本增删改查
  • Leetcode—2950. 可整除子串的数量【中等】Plus(前缀和题型)
  • 面试经典 150 题 -- 滑动窗口 (总结)
  • 异步解耦之RabbitMQ(四)_消息持久化及ACK机制
  • [分享]iOS开发 - 实现UITableView Plain SectionView和table不停留一起滑动
  • Angular数据绑定机制
  • Apache的80端口被占用以及访问时报错403
  • ECS应用管理最佳实践
  • FastReport在线报表设计器工作原理
  • JavaScript 事件——“事件类型”中“HTML5事件”的注意要点
  • Java比较器对数组,集合排序
  • Java基本数据类型之Number
  • mongo索引构建
  • React-redux的原理以及使用
  • SpiderData 2019年2月25日 DApp数据排行榜
  • SpringBoot 实战 (三) | 配置文件详解
  • SpringCloud(第 039 篇)链接Mysql数据库,通过JpaRepository编写数据库访问
  • windows-nginx-https-本地配置
  • 阿里云前端周刊 - 第 26 期
  • 当SetTimeout遇到了字符串
  • 道格拉斯-普克 抽稀算法 附javascript实现
  • 前端技术周刊 2018-12-10:前端自动化测试
  • 时间复杂度与空间复杂度分析
  • 在Mac OS X上安装 Ruby运行环境
  • Nginx惊现漏洞 百万网站面临“拖库”风险
  • PostgreSQL之连接数修改
  • 好程序员大数据教程Hadoop全分布安装(非HA)
  • ​中南建设2022年半年报“韧”字当头,经营性现金流持续为正​
  • # Swust 12th acm 邀请赛# [ A ] A+B problem [题解]
  • #微信小程序:微信小程序常见的配置传旨
  • #我与Java虚拟机的故事#连载02:“小蓝”陪伴的日日夜夜
  • $forceUpdate()函数
  • ()、[]、{}、(())、[[]]命令替换
  • (1)Nginx简介和安装教程
  • (3)llvm ir转换过程
  • (多级缓存)多级缓存
  • (翻译)Quartz官方教程——第一课:Quartz入门
  • (附源码)springboot猪场管理系统 毕业设计 160901
  • (附源码)ssm旅游企业财务管理系统 毕业设计 102100
  • (十七)Flask之大型项目目录结构示例【二扣蓝图】
  • (转)Groupon前传:从10个月的失败作品修改,1个月找到成功
  • .NET 4.0中的泛型协变和反变
  • .NET 6 Mysql Canal (CDC 增量同步,捕获变更数据) 案例版
  • .NET BackgroundWorker