前奏:
在你的手機更多設置或者高級設置中,我們會發現有個無障礙的功能,很多人不知道這個功能具體是干嘛的,其實這個功能是為了增強用戶界面以幫助殘障人士,或者可能暫時無法與設備充分交互的人們
它的具體實現是通過AccessibilityService服務運行在后臺中,通過AccessibilityEvent接收指定事件的回調。
例如:焦點改變了,一個按鈕被點擊,等等。這樣的服務可以選擇請求活動窗口的內容的能力。簡單的說AccessibilityService就是一個后臺監控服務,當你監控的內容發生改變時,就會調用后臺服務的回調方法
AccessibilityService使用
1.1創建服務類
編寫自己的Service類,重寫onServiceConnected()方法、onAccessibilityEvent()方法和onInterrupt()方法
/**
* Created by zxn on 2019/4/29.
*/
public class PlugAccessibilityService extends AccessibilityService {
/**
* 當啟動服務的時候就會被調用,系統成功綁定該服務時被觸發,也就是當你在設置中開啟相應的服務,
* 系統成功的綁定了該服務時會觸發,通常我們可以在這里做一些初始化操作
*/
@Override
protected void onServiceConnected() {
super.onServiceConnected();
}
/**
* 通過系統監聽窗口變化的回調,sendAccessibiliyEvent()不斷的發送AccessibilityEvent到此處
*
* @param event
*/
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int eventType = event.getEventType();
//根據時間回調類型進行處理.
}
/**
* 中斷服務時的回調.
*/
@Override
public void onInterrupt() {
}
/**
* 查找擁有特定焦點類型的控件
*
* @param focus
* @return
*/
@Override
public AccessibilityNodeInfo findFocus(int focus) {
return super.findFocus(focus);
}
/**
* 如果配置能夠獲取窗口內容,則會返回當前活動窗口的根結點
*
* @return
*/
@Override
public AccessibilityNodeInfo getRootInActiveWindow() {
return super.getRootInActiveWindow();
}
/**
* 獲取系統服務
*
* @param name
* @return
*/
@Override
public Object getSystemService(String name) {
return super.getSystemService(name);
}
/**
* 如果允許服務監聽按鍵操作,該方法是按鍵事件的回調,需要注意,這個過程發生了系統處理按鍵事件之前
*
* @param event
* @return
*/
@Override
protected boolean onKeyEvent(KeyEvent event) {
return super.onKeyEvent(event);
}
}
1.1.1AccessibilityService中常用的方法的介紹
findFocus(int focus)
查找擁有特定焦點類型的控件getRootInActiveWindow()
如果配置能夠獲取窗口內容,則會返回當前活動窗口的根結點final void disableSelf()
禁用當前服務,也就是在服務可以通過該方法停止運行final AccessibilityServiceInfo getServiceInfo()
獲取當前服務的配置信息onAccessibilityEvent(AccessibilityEvent event)
有關AccessibilityEvent事件的回調函數,系統通過sendAccessibiliyEvent()不斷的發送AccessibilityEvent到此處performGlobalAction(int action)
執行全局操作,比如返回,回到主頁,打開最近等操作setServiceInfo(AccessibilityServiceInfo info)
設置當前服務的配置信息getSystemService(String name)
獲取系統服務onKeyEvent(KeyEvent event)
如果允許服務監聽按鍵操作,該方法是按鍵事件的回調,需要注意,這個過程發生了系統處理按鍵事件之前onServiceConnected()
系統成功綁定該服務時被觸發,也就是當你在設置中開啟相應的服務,系統成功的綁定了該服務時會觸發,通常我們可以在這里做一些初始化操作onInterrupt()
服務中斷時的回調
1.2 聲明服務
在manifests中配置該服務信息
<service
android:name=".PlugAccessibilityService"
android:enabled="true"
android:exported="true"
android:label="@string/plug_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
</intent-filter>
</service>
注意:任何一個信息配置錯誤,都會使該服務無反應
-
android:label:在無障礙列表中顯示該服務的名字
image.png
- android:permission:需要指定BIND_ACCESSIBILITY_SERVICE權限,這是4.0以上的系統要求的
- intent-filter:這個name是固定不變的
1.3 配置服務參數
配置服務參數是指:配置用來接受指定類型的事件,監聽指定package,檢索窗口內容,獲取事件類型的時間等等。其配置服務參數有兩種方法:
方法一:安卓4.0之后可以通過meta-data標簽指定xml文件進行配置
方法二:通過代碼動態配置參數
1.3.1 方法一
在原先的manifests中增加meta-data標簽指定xml文件
<service
android:name=".PlugAccessibilityService"
android:enabled="true"
android:exported="true"
android:label="@string/plug_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config"
/>
</service>
接下來是accessibility_service_config文件的配置
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackAllMask"
android:accessibilityFlags="flagRequestFilterKeyEvents"
android:canRequestFilterKeyEvents="true"
android:canRetrieveWindowContent="true"
android:notificationTimeout="100" />
下面是對xml參數的介紹
accessibilityEventTypes:表示該服務對界面中的哪些變化感興趣,即哪些事件通知,比如窗口打開,滑動,焦點變化,長按等。具體的值可以在AccessibilityEvent類中查到,如typeAllMask表示接受所有的事件通知
accessibilityFeedbackType:表示反饋方式,比如是語音播放,還是震動
canRetrieveWindowContent:表示該服務能否訪問活動窗口中的內容。也就是如果你希望在服務中獲取窗體內容,則需要設置其值為true
description:對該無障礙功能的描述,具體體現在下圖
notificationTimeout:接受事件的時間間隔,通常將其設置為100即可
packageNames:表示對該服務是用來監聽哪個包的產生的事件
1.3.2 方法二
通過代碼為我們的AccessibilityService配置AccessibilityServiceInfo信息
public void setAccessibilityServiceInfo() {
String[] packageNames = {"com.tencent.mm"};
AccessibilityServiceInfo accessibilityServiceInfo = new AccessibilityServiceInfo();
//相應時間的類型,(長安,點擊,滑動)
accessibilityServiceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
//反饋給用戶的類型,這里是語音
accessibilityServiceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
//過濾的包名
accessibilityServiceInfo.packageNames = packageNames;
setServiceInfo(accessibilityServiceInfo);
}
在這里涉及到了AccessibilityServiceInfo類,AccessibilityServiceInfo類被用于配置AccessibilityService信息,該類中包含了大量用于配置的常量字段及用來xml屬性,常見的有:accessibilityEventTypes,canRequestFilterKeyEvents,packageNames等等
1.4 啟動服務
這里我們需要在無障礙功能里面手動打開該項功能,否則無法繼續進行,通過
下面代碼可以打開系統的無障礙功能列表
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);
1.5 處理事件信息
由于我們監聽了事件的通知欄和界面等信息,當我們指定packageNames的通知欄或者界面發生變化時,會通過onAccessibilityEvent回調我們的事件,接著進行事件的處理
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int eventType = event.getEventType();
//根據時間回調類型進行處理.
switch (eventType) {
//通知欄變化時
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
break;
case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
//當窗口狀態發生改變時.
break;
}
}
當我們微信收到通知時,狀態欄會有一條推送信息到達,這個時候就會被TYPE_NOTIFICATION_STATE_CHANGED監聽,執行里面的內容,當我們切換微信界面時,或者使用微信時,這個時候就會被TYPE_WINDOW_STATE_CHANGED監聽,執行里面的內容
AccessibilityEvent的方法
getEventType():事件類型
getSource():獲取事件源對應的結點信息
getClassName():獲取事件源對應類的類型,比如點擊事件是有某個Button產生的,那么此時獲取的就是Button的完整類名
getText():獲取事件源的文本信息,比如事件是有TextView發出的,此時獲取的就是TextView的text屬性。如果該事件源是樹結構,那么此時獲取的是這個樹上所有具有text屬性的值的集合
isEnabled():事件源(對應的界面控件)是否處在可用狀態
getItemCount():如果事件源是樹結構,將返回該樹根節點下子節點的數量
1.6 獲取節點信息
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
1.6.2 獲取指定子節點(控件節點)
//通過文本找到對應節點集合.
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
List<AccessibilityNodeInfo> textList = nodeInfo.findAccessibilityNodeInfosByText("");
//通過空間id找到對應的節點集合.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
List<AccessibilityNodeInfo> textIdList = nodeInfo.findAccessibilityNodeInfosByViewId("000");
}
1.7 模擬節點點擊
當我們獲取了節點信息之后,對控件節點進行模擬點擊、長按等操作,AccessibilityNodeInfo類提供了performAction()方法讓我們執行模擬操作,具體操作可看官方文檔介紹,這里列舉常用的操作
//模擬點擊
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
//模擬長安
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
//模擬獲取焦點
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
//模擬粘貼.
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_PASTE);
監聽控件插件實現
2.1 原理分析
2.2 注意事項
1,每個版本的按鈕ID都是不一樣的,在我們的程序中是需要去修改按鈕ID,以達到版本的適配
2,在獲取控件ID的時候,注意其布局是否可點擊,否則獲取不可點擊的控件,會使程序無反應
2.3 獲取控件ID
當我們手機接入USB線時,在Android Device Monitor中的選擇設備并開啟Dump View Hierarchy for UI Automator工具,通過它可以獲取控件信息
monitor的安裝目錄.
C:\ZxnProgram\Android\Sdk\tools\monitor.bat
2.4 代碼實現
科邁贏錢
包名:
com.pos.kmretailpos.mini
id:
com.pos.kmretailpos.mini:id/tv_waipay
com.pos.kmretailpos.mini:id/et_ys
科邁贏錢
包名:
com.pos.kmretailpos.mini
id:
com.pos.kmretailpos.mini:id/tv_waipay
com.pos.kmretailpos.mini:id/et_ys
AccessibilityEvent時間類型:
/**
* View被點擊--->1
* Represents the event of clicking on a {@link android.view.View} like
* {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc.
*/
public static final int TYPE_VIEW_CLICKED = 0x00000001;
/**
* View被長按
* Represents the event of long clicking on a {@link android.view.View} like
* {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc.
*/
public static final int TYPE_VIEW_LONG_CLICKED = 0x00000002;
/**
* View被選中
* Represents the event of selecting an item usually in the context of an
* {@link android.widget.AdapterView}.
*/
public static final int TYPE_VIEW_SELECTED = 0x00000004;
/**
* View獲得焦點
* Represents the event of setting input focus of a {@link android.view.View}.
*/
public static final int TYPE_VIEW_FOCUSED = 0x00000008;
/**
* View文本變化
* Represents the event of changing the text of an {@link android.widget.EditText}.
*/
public static final int TYPE_VIEW_TEXT_CHANGED = 0x00000010;
/**
* 監聽窗口狀態變化,比如打開一個popupWindow,dialog,Activity切換等等.打開了一個PopupWindow,Menu或Dialog(--->32)
* Represents the event of a change to a visually distinct section of the user interface.
* These events should only be dispatched from {@link android.view.View}s that have
* accessibility pane titles, and replaces {@link #TYPE_WINDOW_CONTENT_CHANGED} for those
* sources. Details about the change are available from {@link #getContentChangeTypes()}.
*/
public static final int TYPE_WINDOW_STATE_CHANGED = 0x00000020;
/**
* Notification通知變化
* Represents the event showing a {@link android.app.Notification}.
*/
public static final int TYPE_NOTIFICATION_STATE_CHANGED = 0x00000040;
/**
* 一個View進入懸停
* Represents the event of a hover enter over a {@link android.view.View}.
*/
public static final int TYPE_VIEW_HOVER_ENTER = 0x00000080;
/**
* 一個View退出懸停
* Represents the event of a hover exit over a {@link android.view.View}.
*/
public static final int TYPE_VIEW_HOVER_EXIT = 0x00000100;
/**
* 觸摸瀏覽事件開始
* Represents the event of starting a touch exploration gesture.
*/
public static final int TYPE_TOUCH_EXPLORATION_GESTURE_START = 0x00000200;
/**
* 觸摸瀏覽事件完成
* Represents the event of ending a touch exploration gesture.
*/
public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400;
/**
* 窗口的內容發生變化,或子樹根布局發生變化(--->2048)
* Represents the event of changing the content of a window and more
* specifically the sub-tree rooted at the event's source.
*/
public static final int TYPE_WINDOW_CONTENT_CHANGED = 0x00000800;
/**
* View滾動
* Represents the event of scrolling a view. This event type is generally not sent directly.
* @see View#onScrollChanged(int, int, int, int)
*/
public static final int TYPE_VIEW_SCROLLED = 0x00001000;
/**
* Edittext文字選中發生改變事件
* Represents the event of changing the selection in an {@link android.widget.EditText}.
*/
public static final int TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000;
/**
* 應用產生一個通知事件
* Represents the event of an application making an announcement.
*/
public static final int TYPE_ANNOUNCEMENT = 0x00004000;
/**
* 獲得無障礙焦點事件
* Represents the event of gaining accessibility focus.
*/
public static final int TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000;
/**
* 無障礙焦點事件清除
* Represents the event of clearing accessibility focus.
*/
public static final int TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000;
/**
* 在給定的移動粒度下遍歷視圖文本的事件
* Represents the event of traversing the text of a view at a given movement granularity.
*/
public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 0x00020000;
/**
* 開始手勢監測
* Represents the event of beginning gesture detection.
*/
public static final int TYPE_GESTURE_DETECTION_START = 0x00040000;
/**
* 結束手勢監測
* Represents the event of ending gesture detection.
*/
public static final int TYPE_GESTURE_DETECTION_END = 0x00080000;
/**
* 觸摸屏幕事件開始
* Represents the event of the user starting to touch the screen.
*/
public static final int TYPE_TOUCH_INTERACTION_START = 0x00100000;
/**
* 觸摸屏幕事件結束
* Represents the event of the user ending to touch the screen.
*/
public static final int TYPE_TOUCH_INTERACTION_END = 0x00200000;
/**
* 屏幕上的窗口變化事件,需要API 21+
* Represents the event change in the system windows shown on the screen. This event type should
* only be dispatched by the system.
*/
public static final int TYPE_WINDOWS_CHANGED = 0x00400000;
/**
* View中的上下文點擊事件
* Represents the event of a context click on a {@link android.view.View}.
*/
public static final int TYPE_VIEW_CONTEXT_CLICKED = 0x00800000;
/**
* 輔助用戶讀取當前屏幕事件
* Represents the event of the assistant currently reading the users screen context.
*/
public static final int TYPE_ASSIST_READING_CONTEXT = 0x01000000;
/**
* Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
* The type of change is not defined.
*/
public static final int CONTENT_CHANGE_TYPE_UNDEFINED = 0x00000000;
/**
* Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
* One or more content changes occurred in the the subtree rooted at the source node,
* or the subtree's structure changed when a node was added or removed.
*/
public static final int CONTENT_CHANGE_TYPE_SUBTREE = 0x00000001;
/**
* Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
* The node's text changed.
*/
public static final int CONTENT_CHANGE_TYPE_TEXT = 0x00000002;
/**
* Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
* The node's content description changed.
*/
public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004;
/**
* Change type for {@link #TYPE_WINDOW_STATE_CHANGED} event:
* The node's pane title changed.
*/
public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 0x00000008;
/**
* Change type for {@link #TYPE_WINDOW_STATE_CHANGED} event:
* The node has a pane title, and either just appeared or just was assigned a title when it
* had none before.
*/
public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 0x00000010;
/**
*
* Change type for {@link #TYPE_WINDOW_STATE_CHANGED} event:
* Can mean one of two slightly different things. The primary meaning is that the node has
* a pane title, and was removed from the node hierarchy. It will also be sent if the pane
* title is set to {@code null} after it contained a title.
* No source will be returned if the node is no longer on the screen. To make the change more
* clear for the user, the first entry in {@link #getText()} will return the value that would
* have been returned by {@code getSource().getPaneTitle()}.
*/
public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 0x00000020;
/**
* 一個窗口被添加了
* Change type for {@link #TYPE_WINDOWS_CHANGED} event:
* The window was added.
*/
public static final int WINDOWS_CHANGE_ADDED = 0x00000001;
/**
* 一個被窗口移除了.
* Change type for {@link #TYPE_WINDOWS_CHANGED} event:
* A window was removed.
*/
public static final int WINDOWS_CHANGE_REMOVED = 0x00000002;
/**
* Change type for {@link #TYPE_WINDOWS_CHANGED} event:
* The window's title changed.
*/
public static final int WINDOWS_CHANGE_TITLE = 0x00000004;
/**
* Change type for {@link #TYPE_WINDOWS_CHANGED} event:
* The window's bounds changed.
*/
public static final int WINDOWS_CHANGE_BOUNDS = 0x00000008;
/**
* Change type for {@link #TYPE_WINDOWS_CHANGED} event:
* The window's layer changed.
*/
public static final int WINDOWS_CHANGE_LAYER = 0x00000010;
/**
* Change type for {@link #TYPE_WINDOWS_CHANGED} event:
* The window's {@link AccessibilityWindowInfo#isActive()} changed.
*/
public static final int WINDOWS_CHANGE_ACTIVE = 0x00000020;
/**
* Change type for {@link #TYPE_WINDOWS_CHANGED} event:
* The window's {@link AccessibilityWindowInfo#isFocused()} changed.
*/
public static final int WINDOWS_CHANGE_FOCUSED = 0x00000040;
/**
* Change type for {@link #TYPE_WINDOWS_CHANGED} event:
* The window's {@link AccessibilityWindowInfo#isAccessibilityFocused()} changed.
*/
public static final int WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED = 0x00000080;
/**
* Change type for {@link #TYPE_WINDOWS_CHANGED} event:
* The window's parent changed.
*/
public static final int WINDOWS_CHANGE_PARENT = 0x00000100;
/**
* Change type for {@link #TYPE_WINDOWS_CHANGED} event:
* The window's children changed.
*/
public static final int WINDOWS_CHANGE_CHILDREN = 0x00000200;
/**
*
* Change type for {@link #TYPE_WINDOWS_CHANGED} event:
* The window either entered or exited picture-in-picture mode.
*/
public static final int WINDOWS_CHANGE_PIP = 0x00000400;