Android 触摸模式(Touch Mode)

本文详细解释了在非触屏及触屏设备上,焦点与触摸模式的概念及其工作原理。探讨了不同交互模式下控件如何获取焦点,以及触摸模式对用户界面的影响。

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

什么是焦点?

在非触屏手机时代或电脑上,我们通常需要用键盘、 鼠标、轨迹球(trackball)与界面进行交互,当交互的时候必须使目标控件获得焦点(比如高亮起来),这样用户才会注意到是什么控件接受输入。而如果是在触屏时代,用户可以直接用手指点击控件,这个时候就没必要将目标高亮了(即获取焦点)。这也就是接下来我们要讲的触摸模式(Touch Mode)。

触摸模式

当用户使用方向键或轨迹球导航用户界面时,必须聚焦到可操作项目上(如按钮),以便用户看到将接受输入的对象。 但是,如果设备具有触摸功能且用户开始通过触摸界面与之交互,则不再需要突出显示项目或聚焦到特定视图对象上。 因此,有一种交互模式称为“触摸模式”(Touch Mode)

触摸模式是用户和手机进行交互时view层次结构的一个状态。代表了最近一次的交互是否是通过触摸屏发生的,因为在Android设备上还存在别的交互方式,比如键盘、等等。

对于支持触摸功能的设备,当用户触摸屏幕时,设备会立即进入触摸模式。无论何时,只要用户点击方向键或滚动轨迹球,设备就会退出触摸模式并找到一个视图使其获得焦点。 现在,用户可在不触摸屏幕的情况下继续与用户界面交互。

整个系统(所有窗口和 Activity)都将保持触摸模式状态。要查询当前状态,您可以调用View#isInTouchMode() 来检查设备目前是否处于触摸模式。

焦点和触摸模式

在触摸模式下,没有焦点,没有选择。同样,任何已聚焦的控件当进入触摸模式时都变为未聚焦状态。而当使用轨迹球和键盘时,就会立即离开触摸模式,控件就会变成聚焦的状态。

现在我们知道了焦点不可以存在于触摸模式了吧,但是这并不完全正确,焦点其实是可以一种特殊的方式存在于触摸模式中的,我们称之为聚焦( focusable)。这种特殊的模式是专门为可接收文字输入的控件创建的,比如EditText。这就是为什么用户可以直接输入文字到文本框中,而不必先用手指选择其文本框的原因。

在触摸模式中,任何控件只要是可聚焦(focusable )的状态,当用户点击其控件时,该控件就会接收到其焦点。如果是不可聚焦的,点击控件将不会接收到焦点。如下所示,当用户点击EditText时,EditText会接收到焦点:

image_1b45qt521k4j1uje2us11bdr479.png-51.6kB

对于支持触摸功能的设备,当用户触摸屏幕时,设备会立即进入触摸模式。 自此以后,只有 isFocusableInTouchMode() 为 true 的视图才可聚焦,如文本编辑小部件EditText。其他可触摸的视图(如按钮Button)在用户触摸时不会获得焦点;按下时它们只是触发点击侦听器。

在触摸模式下,只有少部分的控件默认是可聚焦的状态,例如EditText。可以通过setFocusableInTouchMode或xml中android:focusableInTouchMode设置控件是否可聚焦。

setFocusableInTouchMode 和 setFocusable

很多人对这两个方法有疑问其实很简单:

  • setFocusable:设置控件是否能获取焦点。可以通过isFocusable()获取其状态。
  • setFocusableInTouchMode:在触摸模式下,设置控件是否允许聚焦。可以通过isFocusableInTouchMode() 获取其状态。

在使用键盘或轨迹球的情况下,只有setFocusable为true的控件,才可以获取焦点(选中时高亮)。而在触摸模式下,setFocusable为true,并无法保证控件可以获取焦点。setFocusable为true只能保证在非触摸模式下,该控件可以允许获取焦点。如果想在在触摸模式中,改变控件是否允许聚焦,请使用setFocusableInTouchMode进行更改。

从上面我们也可以看出,不管是否在触摸模式下,控件获取焦点的前提是isFocusable()为true。而在触摸模式下,只有isFocusable()和isFocusableInTouchMode()都为ture的情况下,控件才允许聚焦。

下面通过设置setFocusableInTouchMode和setFocusable的先后顺序,查看控件的焦点状态:

button.setFocusableInTouchMode(false);
button.setFocusable(true);
Log.d("cryc","isFocusable  "+button.isFocusable()+" isFocusableInTouchMode "+ button.isFocusableInTouchMode());
//isFocusable  true  isFocusableInTouchMode false 

button.setFocusableInTouchMode(true);
button.setFocusable(false);
Log.d("cryc","isFocusable  "+button.isFocusable()+" isFocusableInTouchMode "+ button.isFocusableInTouchMode());
//isFocusable  false isFocusableInTouchMode false

button.setFocusable(true);
button.setFocusableInTouchMode(false);
Log.d("cryc","isFocusable  "+button.isFocusable()+" isFocusableInTouchMode "+ button.isFocusableInTouchMode());
//isFocusable  true isFocusableInTouchMode false

button.setFocusable(false);
button.setFocusableInTouchMode(true);
Log.d("cryc","isFocusable  "+button.isFocusable()+" isFocusableInTouchMode "+ button.isFocusableInTouchMode());
//isFocusable  true isFocusableInTouchMode true

通过上面我们发现如下的规律:
setFocusableInTouchMode为true,会使isFocusable也变为true,而setFocusableInTouchMode为false并不影响isFocusable。
setFocusable为false,会使isFocusableInTouchMode变为false,而setFocusable为true并不影响isFocusableInTouchMode。

下面我列出了各种常用控件的默认初始状态:

控件FocusableFocusableInTouchModeClickableLongClickable
Viewfalsefalsefalsefalse
TextViewfalsefalsefalsefalse
EditTexttruetruetruetrue
Buttontruefalsetruefalse
ImageButtontruefalsetruefalse
ImageViewfalsefalsefalsefalse
CheckBoxtruefalsetruefalse
RadioButtontruefalsetruefalse
ProgressBarfalsefalsefalsefalse
LinearLayoutfalsefalsefalsefalse
RelativeLayoutfalsefalsefalsefalse
其他Layout都几乎一样falsefalsefalsefalse

从上面我们可以看出,大部分的控件FocusableInTouchMode属性都为false。只有类似EditText这种控件才为true,因为EditText需要提供在没用户点击的条件下,弹出一个软键盘进行输入的功能。

控件可聚焦的注意点

我们现在有如下的布局,设置如下的两个Button都是可聚焦的:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.mytesttwo.MainActivity">
    <Button
        android:layout_width="match_parent"
        android:text="one"
        android:focusableInTouchMode="true"
        android:id="@+id/btnOne"
        android:layout_height="wrap_content" />
    <Button
        android:layout_width="match_parent"
        android:text="two"
        android:focusableInTouchMode="true"
        android:id="@+id/btnTwo"
        android:layout_height="wrap_content" />
</LinearLayout>

并设置了两个button点击和焦点的监听事件:

btnOne.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                  Log.d("cryc","btnOne onClick");
            }
        });
        btnOne.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                Log.d("cryc","btnOne onFocusChange "+ hasFocus);
            }
        });
        btnTwo.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("cryc","btnTwo onClick");
            }
        });
        btnTwo.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                Log.d("cryc","btnTwo onFocusChange "+hasFocus);
            }
        });

当首次进后,应用界面是下面这样的
image_1b45rk6cod691q12nsfqae1t1r13.png-39.3kB
这时候焦点在按钮one上,使按钮的背景变成了橘黄色,Log也打印出了信息。

btnOne onFocusChange true 

这时候当我们点击按钮two的时候,焦点切换到按钮two上:

image_1b45rlg4kov1u291r5617gg12vq1t.png-35.1kB

btnOne onFocusChange false
btnTwo onFocusChange true

注意Log打印出的信息,当用户点击button two的时候,点击事件并没有响应,只有焦点事件响应了。这时候有的开发者会觉得很奇怪?通常我们不是点击一下就可以响应按钮的点击事件吗? 因为当前的按钮处于可聚焦的状态,要让按钮响应事件必须点击两下:

  • 第一下,使焦点聚焦于button上。
  • 第二下,才是真正响应点击的事件。

从上面我们也可以看出有时候设置focusableInTouchMode为true真的不能乱用,不然可能导致上面的情况发生。

在默认拥有可聚焦属性的EditText也是如此,当进入带有EditText控件的页面时,EditText会自动获取焦点(黄色边框+光标闪烁),但此时输入法软键盘是不会自动弹起的(除非你特殊设置),也是需要用户点击EditText才会弹起输入法:
image_1b45rpb4fjvvc0g772gil1hjn2n.png-20.8kB
java EditTextOne onFocusChange true
但是当我们已经处在页面中时,用户只要点击一次第二个的输入框时,软键盘就会弹起。不要以为第二个的输入框接收了点击事件(onClick),它接收的还是焦点事件,之所以会弹起,因为系统内部做了处理。

EditTextOne  onFocusChange false
EditTextTwo  onFocusChange true

那系统内部如何做处理的呢?我认为可能是在onTouchEvent方法中做处理,注意了只要有触摸控件的操作都会触发onTouchEvent方法,当手指接触控件的那一刻先响应的是onTouchEvent方法,然后触发的才是onFocusChange方法或onClick方法。

注意:要想让控件不触发onTouchEvent方法,设置控件disable是没有效果的,控件的CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE这三个状态必须都为false才行。还有一个方法就是可以通过父控件进行截获输入事件。

在项目中,一进入一个页面, EditText默认就会自动获取焦点。那么如何取消这个默认行为呢?有人在EditText的父级控件中找一个,设置成可聚焦的状态:android:focusableInTouchMode=”true” 把EditText的焦点进行截获,使焦点转移到父控件上:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:focusableInTouchMode="true"
   >
    <EditText
       android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

虽然解决了当前的问题,但是这里却有个隐含的风险,在有些低版本的系统中,当你把焦点转移到父控件的身上时,如果从后台切换到前台,会导致整个界面发送抖动。而这个bug通常很少人会得出原因,因此在转移焦点的时候要特别注意。

由于设置了focusableInTouchMode属性后会引起和android正常交互行为的不一致,所以android建议我们保守地使用这个属性,在你确定要用它之前最好三思而后行。

参考:
http://android-developers.blogspot.com/2008/12/touch-mode.html
https://developer.android.com/guide/topics/ui/ui-events.html
http://jakend.iteye.com/blog/764521
http://www.cnblogs.com/xiaoweiz/p/3833079.html

<think>我们正在解决Android 10上无障碍服务在触摸探索模式下无法获取控件ID的问题。根据之前的讨论,我们知道需要正确配置无障碍服务以启用触摸探索模式Touch Exploration)才能获取控件的位置和ID。以下步骤将指导你如何完整设置: ### 1. 无障碍服务配置(XML) 在`res/xml`目录下创建(或修改)无障碍服务配置文件(例如`accessibility_service_config.xml`),并确保包含以下关键标志: ```xml <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/accessibility_service_description" android:accessibilityEventTypes="typeAllMask" android:accessibilityFlags="flagRetrieveInteractiveWindows|flagRequestTouchExplorationMode" android:canRetrieveWindowContent="true" android:packageNames="com.example.targetapp" /> ``` - `flagRequestTouchExplorationMode`:这是关键,它请求系统启用触摸探索模式,从而允许服务获取控件的位置信息[^1]。 - `canRetrieveWindowContent`:必须设为`true`以获取窗口内容。 ### 2. 服务实现(Java) 在服务代码中,我们需要在`onServiceConnected()`中设置服务信息,并明确启用触摸探索模式标志: ```java public class MyAccessibilityService extends AccessibilityService { @Override protected void onServiceConnected() { super.onServiceConnected(); // 创建新的配置信息 AccessibilityServiceInfo info = new AccessibilityServiceInfo(); info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; info.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK; // 关键:设置标志以启用触摸探索 info.flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS | AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS | AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE; info.setCapabilities(AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT); setServiceInfo(info); } @Override public void onAccessibilityEvent(AccessibilityEvent event) { AccessibilityNodeInfo rootNode = getRootInActiveWindow(); if (rootNode != null) { processNode(rootNode); rootNode.recycle(); } } private void processNode(AccessibilityNodeInfo node) { if (node == null) return; // 获取控件ID(资源名称,如:com.example.app:id/button1) String viewId = node.getViewIdResourceName(); Rect bounds = new Rect(); node.getBoundsInScreen(bounds); // 打印日志(实际开发中可替换为其他处理) Log.i("Accessibility", "View ID: " + viewId + ", Bounds: " + bounds); // 遍历子节点 for (int i = 0; i < node.getChildCount(); i++) { AccessibilityNodeInfo child = node.getChild(i); if (child != null) { processNode(child); child.recycle(); } } } } ``` ### 3. 清单文件声明 在`AndroidManifest.xml`中注册服务并声明所需权限: ```xml <service android:name=".MyAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="true"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service> ``` ### 4. 启用服务与保活策略 - 引导用户开启服务:在应用启动时检查服务是否启用,若未启用则跳转到无障碍设置页面(参考引用[3]): ```java public static void checkAccessibilityEnabled(Context context) { if (!isAccessibilityServiceEnabled(context, MyAccessibilityService.class)) { Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); context.startActivity(intent); Toast.makeText(context, "请开启无障碍服务", Toast.LENGTH_LONG).show(); } } private static boolean isAccessibilityServiceEnabled(Context context, Class<?> serviceClass) { String serviceName = new ComponentName(context, serviceClass).flattenToString(); int enabled = Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED, 0); if (enabled != 1) return false; String enabledServices = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); return enabledServices != null && enabledServices.contains(serviceName); } ``` - 服务保活:通过前台服务或绑定其他组件防止系统回收(注意Android 10后台限制)[^3]。 ### 5. 测试验证 - 使用`adb shell dumpsys accessibility`检查服务状态。 - 在设备上启用你的无障碍服务(而非TalkBack),然后测试是否能够获取控件ID和位置。 ### 原理说明 在Android 10中,系统默认不向无障碍服务提供控件位置信息(除非服务声明需要触摸探索模式)。通过`FLAG_REQUEST_TOUCH_EXPLORATION_MODE`标志,系统会为服务模拟触摸探索行为,从而解锁位置数据[^1][^4]。 §§相关问题§§ 1. 如何为自定义无障碍服务添加手势操作支持? 2. Android 11及以上版本的无障碍开发有哪些API变更? 3. 如何解决跨应用获取控件信息时的权限限制问题? [^1]: Android触摸事件与无障碍功能的优化 [^2]: Android 16系统源码_无障碍辅助(一)认识无障碍服务 [^3]: Android无障碍服务 [^4]: Android无障碍开发入门</think>### 解决 Android 10 无障碍服务触摸探索模式无法获取控件 ID 的完整方案 在 Android 10 及以上版本中,需要特殊配置才能在不开启 TalkBack 的情况下获取控件 ID 和位置信息。以下是完整的解决方案: #### 1. 无障碍服务配置(关键设置) 在 `res/xml/accessibility_service.xml` 中添加以下配置: ```xml <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/service_description" android:accessibilityEventTypes="typeAllMask" android:accessibilityFlags="flagRetrieveInteractiveWindows|flagRequestTouchExplorationMode" android:canRetrieveWindowContent="true" android:packageNames="目标包名" android:canRequestTouchExplorationMode="true" /> ``` **关键参数说明**: - `flagRequestTouchExplorationMode`:启用触摸探索模式(核心) - `canRetrieveWindowContent="true"`:允许获取控件内容 - `canRequestTouchExplorationMode="true"`:显式请求触摸探索权限 - `packageNames`:指定需要监控的应用包名(可多个) #### 2. Java 服务实现(带触摸探索模式) ```java public class MyAccessibilityService extends AccessibilityService { @Override protected void onServiceConnected() { super.onServiceConnected(); // 强制启用触摸探索模式 AccessibilityServiceInfo info = new AccessibilityServiceInfo(); info.flags = AccessibilityServiceInfo.DEFAULT | AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE | AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; info.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK; setServiceInfo(info); } @Override public void onAccessibilityEvent(AccessibilityEvent event) { AccessibilityNodeInfo rootNode = getRootInActiveWindow(); if (rootNode == null) return; // 获取控件ID和位置 traverseNodes(rootNode); rootNode.recycle(); } private void traverseNodes(AccessibilityNodeInfo node) { if (node == null) return; // 获取控件ID(核心) String viewId = node.getViewIdResourceName(); // 获取位置信息 Rect bounds = new Rect(); node.getBoundsInScreen(bounds); // 打印调试信息 Log.d("Accessibility", "ID: " + viewId + " | Position: " + bounds); // 递归处理子节点 for (int i = 0; i < node.getChildCount(); i++) { AccessibilityNodeInfo child = node.getChild(i); if (child != null) { traverseNodes(child); child.recycle(); } } } } ``` #### 3. AndroidManifest 声明 ```xml <service android:name=".MyAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="true"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service" /> </service> ``` #### 4. 触摸探索模式强制启用方案 如果上述配置仍无效,添加以下代码强制激活触摸探索: ```java // 在 onServiceConnected() 中添加 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { AccessibilityServiceInfo info = getServiceInfo(); info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE; setServiceInfo(info); } ``` #### 5. 常见问题排查 1. **服务未启用**: ```java // 检查服务状态 public static boolean isAccessibilityEnabled(Context context) { String serviceName = context.getPackageName() + "/.MyAccessibilityService"; int enabled = Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED, 0); if (enabled == 1) { String services = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); return services != null && services.contains(serviceName); } return false; } ``` 2. **省电模式限制**: - 进入设置 → 电池优化 → 选择应用 → 设置为"不允许" 3. **调试命令**: ```bash adb shell dumpsys accessibility adb logcat -s AccessibilityNodeInfo ``` #### 技术原理 Android 10 加强了隐私保护,默认限制获取控件位置信息。通过 `FLAG_REQUEST_TOUCH_EXPLORATION_MODE` 标志,系统会将你的服务视为类似 TalkBack 的辅助工具,从而解锁位置信息获取能力。此标志会激活以下功能: 1. 启用触摸事件映射 2. 激活节点位置计算 3. 解锁隐私敏感信息[^1][^4]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值