总结】Android辅助功能(一)-AccessibilityEvent的分发
目前關于輔助功能的使用的文章很多,但鮮有分析其具體實現的,本文基于Andoird 7.1.0_r7源碼分析一下輔助事件是怎么分發的,只涉及事件的分發和輔助App的接收,之后有機會再講一講獲取AccessibilityNodeInfo、進行操作等等的源碼流程。
文中“目標App”指的是發出輔助事件的App,“輔助App”指的是擁有輔助功能的App。
1. 【目標App】 View.sendAccessibilityEvent(int eventType)
我們看View的源碼可以看到在很多地方調用了sendAccessibilityEvent(int eventType)的方法,例如:
在View獲取到焦點時,調用了sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); 當View被點擊時,調用了sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED)使用過輔助功能的同學對這些Event應該很熟悉,這些就是我們在寫輔助App時定義的想要接收的輔助事件的類型,Android為我們定義了一系列輔助事件,這里舉幾個比較常用的事件:
TYPE_VIEW_CLICKED // 當View被點擊時發送此事件。 TYPE_VIEW_LONG_CLICKED // 當View被長按時發送此事件。 TYPE_VIEW_FOCUSED // 當View獲取到焦點時發送此事件。 TYPE_WINDOW_STATE_CHANGED // 當Window發生變化時發送此事件。 TYPE_VIEW_SCROLLED // 當View滑動時發送此事件。所以說,sendAccessibilityEvent(int eventType)就是我們的起點,我們來看一看這個方法。View實現了AccessibilityEventSource接口,這個方法就來自于AccessibilityEventSource接口。
public void sendAccessibilityEvent(int eventType) {if (mAccessibilityDelegate != null) {// AccessibilityDelegate是用來增強輔助功能的,一般情況下不用考慮。mAccessibilityDelegate.sendAccessibilityEvent(this, eventType);} else {sendAccessibilityEventInternal(eventType);} }2. 【目標App】 View.sendAccessibilityEventInternal(int eventType) -> View.sendAccessibilityEventUnchecked(AccessibilityEvent event)
這2個方法都比較短,就放在一起說了。sendAccessibilityEventInternal(int eventType)會檢查當前輔助服務是否開啟,至少有一個輔助App被開啟了才會返回true。如果當前開啟了,會把eventType轉成AccessibilityEvent,這就是我們在AccessibilityService中收到的AccessibilityEvent,之后調用了sendAccessibilityEventUnchecked(AccessibilityEvent event),進而調用了sendAccessibilityEventUncheckedInternal(AccessibilityEvent event)。
public void sendAccessibilityEventInternal(int eventType) {if (AccessibilityManager.getInstance(mContext).isEnabled()) {sendAccessibilityEventUnchecked(AccessibilityEvent.obtain(eventType));} }public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {if (mAccessibilityDelegate != null) {// AccessibilityDelegate是用來增強輔助功能的,一般情況下不用考慮。mAccessibilityDelegate.sendAccessibilityEventUnchecked(this, event);} else {sendAccessibilityEventUncheckedInternal(event);} }3. 【目標App】 View.sendAccessibilityEventUncheckedInternal(AccessibilityEvent event)
此時會先判斷當前View及所有的Parent是否可見,如果不可見則不會分發當前的AccessibilityEvent。onInitializeAccessibilityEvent(event)做了一些初始化工作,例如給AccessibilityEvent設置source、className、packageName等等信息。
系統定義了一個叫POPULATING_ACCESSIBILITY_EVENT_TYPES的常量,包括了AccessibilityEvent.TYPE_VIEW_CLICKED等等一系列Event,當發送的EventType是這些中的一個時,目標App可以通過重寫dispatchPopulateAccessibilityEvent(AccessibilityEvent event)或onPopulateAccessibilityEvent(AccessibilityEvent event)方法對將要發送的AccessibilityEvent進行修改。
之后會調用getParent().requestSendAccessibilityEvent(this, event)發給Parent View去處理。
public void sendAccessibilityEventUncheckedInternal(AccessibilityEvent event) {// 判斷View是否可見if (!isShown()) {return;}// 設置AccessibilityEvent的一些信息onInitializeAccessibilityEvent(event);if ((event.getEventType() & POPULATING_ACCESSIBILITY_EVENT_TYPES) != 0) {// 目標App可通過此方法修改AccessibilityEventdispatchPopulateAccessibilityEvent(event);}// In the beginning we called #isShown(), so we know that getParent() is not null.getParent().requestSendAccessibilityEvent(this, event); }private static final int POPULATING_ACCESSIBILITY_EVENT_TYPES =AccessibilityEvent.TYPE_VIEW_CLICKED| AccessibilityEvent.TYPE_VIEW_LONG_CLICKED| AccessibilityEvent.TYPE_VIEW_SELECTED| AccessibilityEvent.TYPE_VIEW_FOCUSED| AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED| AccessibilityEvent.TYPE_VIEW_HOVER_ENTER| AccessibilityEvent.TYPE_VIEW_HOVER_EXIT| AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED| AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED| AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED| AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY;4. 【目標App】ViewGroup.requestSendAccessibilityEvent(View child, AccessibilityEvent event)
對于一個View來說,它的Parent View就是ViewGroup,這里會遞歸調用Parent View的requestSendAccessibilityEvent方法,值得注意的是onRequestSendAccessibilityEvent(View child, AccessibilityEvent event)方法,官方的注釋說是“當子View請求發送一個AccessibilityEvent時調用,給父View一個增加事件的機會。” 但我覺得更大的用處是可以通過重寫這個方法阻止事件的發送。
我們知道正常情況下,最終我們會調用DecorView的requestSendAccessibilityEvent(View child, AccessibilityEvent event),而DecorView的Parent是ViewRootImpl,所以說最終會調用ViewRootImpl的requestSendAccessibilityEvent(View child, AccessibilityEvent event)方法。
@Override public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {ViewParent parent = mParent;if (parent == null) {return false;}// 自定義View可以重寫這個方法阻止事件的發送。final boolean propagate = onRequestSendAccessibilityEvent(child, event);if (!propagate) {return false;}return parent.requestSendAccessibilityEvent(this, event); }5. 【目標App】ViewRootImpl.requestSendAccessibilityEvent(View child, AccessibilityEvent event)
該方法對幾個特殊的EventType進行了處理,在此我們先不關注,之后調用AccessibilityManager的sendAccessibilityEvent(AccessibilityEvent event)方法。
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {if (mView == null || mStopped || mPausedForTransition) {return false;}final int eventType = event.getEventType();switch (eventType) {// 對某些eventType進行了特殊處理,在此省略}mAccessibilityManager.sendAccessibilityEvent(event);return true; }6. 【目標App】AccessibilityManager.sendAccessibilityEvent(AccessibilityEvent event)
這里再次檢查了輔助功能當前是否開啟,之后就通過Binder進入AccessibilityManagerService【下文簡稱AMS,額,不要以為是ActivityManagerService】的世界了。
public void sendAccessibilityEvent(AccessibilityEvent event) {final IAccessibilityManager service;final int userId;synchronized (mLock) {service = getServiceLocked();if (service == null) {return;}if (!mIsEnabled) {Looper myLooper = Looper.myLooper();if (myLooper == Looper.getMainLooper()) {throw new IllegalStateException("Accessibility off. Did you forget to check that?");} else {Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled");return;}}userId = mUserId;}boolean doRecycle = false;try {event.setEventTime(SystemClock.uptimeMillis());long identityToken = Binder.clearCallingIdentity();// 向AccessibilityManagerService發送AccessibilityEventdoRecycle = service.sendAccessibilityEvent(event, userId);Binder.restoreCallingIdentity(identityToken);if (DEBUG) {Log.i(LOG_TAG, event + " sent");}} catch (RemoteException re) {Log.e(LOG_TAG, "Error during sending " + event + " ", re);} finally {if (doRecycle) {event.recycle();}}7. 【AMS】 AccessibilityManagerService.sendAccessibilityEvent(AccessibilityEvent event, int userId)
在進行了一些檢查和準備工作后,最后調用notifyAccessibilityServicesDelayedLocked(AccessibilityEvent event, boolean isDefault)準備開始分發。
@Override public boolean sendAccessibilityEvent(AccessibilityEvent event, int userId) {synchronized (mLock) {final int resolvedUserId = mSecurityPolicy.resolveCallingUserIdEnforcingPermissionsLocked(userId);if (resolvedUserId != mCurrentUserId) {return true; // yes, recycle the event}if (mSecurityPolicy.canDispatchAccessibilityEventLocked(event)) {mSecurityPolicy.updateActiveAndAccessibilityFocusedWindowLocked(event.getWindowId(),event.getSourceNodeId(), event.getEventType(), event.getAction());mSecurityPolicy.updateEventSourceLocked(event);// 開始分發AccessibilityEventnotifyAccessibilityServicesDelayedLocked(event, false);notifyAccessibilityServicesDelayedLocked(event, true);}if (mHasInputFilter && mInputFilter != null) {mMainHandler.obtainMessage(MainHandler.MSG_SEND_ACCESSIBILITY_EVENT_TO_INPUT_FILTER,AccessibilityEvent.obtain(event)).sendToTarget();}event.recycle();}return (OWN_PROCESS_ID != Binder.getCallingPid()); }8. 【AMS】 AccessibilityManagerService.notifyAccessibilityServicesDelayedLocked(AccessibilityEvent event, boolean isDefault)
UserState是AccessibilityManagerService一個內部類,在這個類里保存了一個用戶當前安裝了的、開啟了的、已經建立連接的AccessibilityService列表等等信息。在初始化、安裝/卸載應用、切換用戶、開關輔助功能等等操作時,系統會對UserState的信息進行更新。mBoundServices中保存的就是當前已經啟動了的Service列表,Service類也是AccessibilityManagerService的一個內部類,里面儲存了從輔助App讀取到的配置信息,即我們在輔助App的xml里配置的內容,并且Service類還會負責與各個AccessibilityService建立連接、進行通訊,管理著AccessibilityService的生命周期。此時會調用每個Service的notifyAccessibilityEvent(AccessibilityEvent event)進行事件的分發。
其中canDispatchEventToServiceLocked(Service service, AccessibilityEvent event)方法是用于判斷該Service是否可以接收當前的AccessibilityEvent,即根據輔助App配置的需要接收的EventType和packageName等信息進行判斷。
private void notifyAccessibilityServicesDelayedLocked(AccessibilityEvent event, boolean isDefault) {try {UserState state = getCurrentUserStateLocked();for (int i = 0, count = state.mBoundServices.size(); i < count; i++) {Service service = state.mBoundServices.get(i);if (service.mIsDefault == isDefault) {// 輔助App接收該packageName和該EventType時才會分發if (canDispatchEventToServiceLocked(service, event)) {service.notifyAccessibilityEvent(event);}}}} catch (IndexOutOfBoundsException oobe) {// An out of bounds exception can happen if services are going away// as the for loop is running. If that happens, just bail because// there are no more services to notify.} }9. 【AMS】 AccessibilityManagerService.Service.notifyAccessibilityEvent(AccessibilityEvent event)
利用Service里定義的Handler把事件發出去,在handleMessage中進而調用了notifyAccessibilityEventInternal(int eventType, AccessibilityEvent event)方法。
public void notifyAccessibilityEvent(AccessibilityEvent event) {synchronized (mLock) {final int eventType = event.getEventType();// 復制當前的AccessibilityEventAccessibilityEvent newEvent = AccessibilityEvent.obtain(event);Message message;if ((mNotificationTimeout > 0)&& (eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)) {// Allow at most one pending eventfinal AccessibilityEvent oldEvent = mPendingEvents.get(eventType);mPendingEvents.put(eventType, newEvent);if (oldEvent != null) {mEventDispatchHandler.removeMessages(eventType);oldEvent.recycle();}message = mEventDispatchHandler.obtainMessage(eventType);} else {// Send all messages, bypassing mPendingEventsmessage = mEventDispatchHandler.obtainMessage(eventType, newEvent);}mEventDispatchHandler.sendMessageDelayed(message, mNotificationTimeout);}}public Handler mEventDispatchHandler = new Handler(mMainHandler.getLooper()) {@Overridepublic void handleMessage(Message message) {final int eventType = message.what;AccessibilityEvent event = (AccessibilityEvent) message.obj;notifyAccessibilityEventInternal(eventType, event);}};10. 【AMS】 AccessibilityManagerService.Service.notifyAccessibilityEventInternal(int eventType, AccessibilityEvent event)
該方法中的IAccessibilityServiceClient是AccessibilityService中的內部類IAccessibilityServiceClientWrapper,通過Binder調用了其onAccessibilityEvent(AccessibilityEvent event)方法。之后我們便轉入了輔助App也就是接收輔助事件的App中。
private void notifyAccessibilityEventInternal(int eventType, AccessibilityEvent event) {IAccessibilityServiceClient listener;synchronized (mLock) {listener = mServiceInterface;// If the service died/was disabled while the message for dispatching// the accessibility event was propagating the listener may be null.if (listener == null) {return;}if (event == null) {event = mPendingEvents.get(eventType);if (event == null) {return;}mPendingEvents.remove(eventType);}if (mSecurityPolicy.canRetrieveWindowContentLocked(this)) {event.setConnectionId(mId);} else {event.setSource(null);}event.setSealed(true);}try {// 分發給輔助Applistener.onAccessibilityEvent(event);if (DEBUG) {Slog.i(LOG_TAG, "Event " + event + " sent to " + listener);}} catch (RemoteException re) {Slog.e(LOG_TAG, "Error during sending " + event + " to " + listener, re);} finally {event.recycle();}}11. 【輔助App】 AccessibilityService.IAccessibilityServiceClientWrapper.onAccessibilityEvent(AccessibilityEvent event)
此時通過mCaller發送了message code為DO_ON_ACCESSIBILITY_EVENT的Message,mCaller是IAccessibilityServiceClientWrapper中持有的一個HandlerCaller,在IAccessibilityServiceClientWrapper的構造方法中通過mCaller = new HandlerCaller(context, looper, this, true /asyncHandler/)創建,其中第三個參數即HandlerCaller的Callback,因此最終會回調IAccessibilityServiceClientWrapper的executeMessage方法。
在此我們只看message code為DO_ON_ACCESSIBILITY_EVENT的實現,可以看到最后調用的是mCallback.onAccessibilityEvent(event),這個mCallback是什么呢?在AccessibilityService里定義了一個接口Callbacks,IAccessibilityServiceClientWrapper中持有的這個Callbacks是由其構造方法傳入的參數。而IAccessibilityServiceClientWrapper是在AccessibilityService的onBind(Intent intent)方法中生成了,其中Callbacks的onAccessibilityEvent(AccessibilityEvent event)方法實現非常簡單,直接調用了AccessibilityService.this.onAccessibilityEvent(event),這個也就是我們在輔助App中重寫的onAccessibilityEvent(AccessibilityEvent event)方法了。
public void onAccessibilityEvent(AccessibilityEvent event) {Message message = mCaller.obtainMessageO(DO_ON_ACCESSIBILITY_EVENT, event);mCaller.sendMessage(message); }public void executeMessage(Message message) {switch (message.what) {case DO_ON_ACCESSIBILITY_EVENT: {AccessibilityEvent event = (AccessibilityEvent) message.obj;if (event != null) {// 如果是設計UI方面的eventType會對一些緩存進行更新AccessibilityInteractionClient.getInstance().onAccessibilityEvent(event);mCallback.onAccessibilityEvent(event);// Make sure the event is recycled.try {event.recycle();} catch (IllegalStateException ise) {/* ignore - best effort */}}} return;...// 其他實現省略}}@Override public void onAccessibilityEvent(AccessibilityEvent event) {AccessibilityService.this.onAccessibilityEvent(event); }到此,AccessibilityEvent便由目標App經由AccessibilityManagerService發送到了輔助App上,如果用圖展示的話大致如下(圖中省去了部分Handler的流程):
點擊查看大圖
補遺
目標App與AccessibilityManagerService的通訊
其實目標App與AccessibilityManagerService之間除了在發送AccessibilityEvent時進行了通訊外,在第一次連接獲取輔助服務開關狀態以及開關狀態發生變化時都會進行通訊。判斷輔助服務是否開啟的邏輯如下:
public boolean isEnabled() {synchronized (mLock) {IAccessibilityManager service = getServiceLocked();if (service == null) {return false;}return mIsEnabled;} }private IAccessibilityManager getServiceLocked() {if (mService == null) {tryConnectToServiceLocked(null);}return mService; }private void tryConnectToServiceLocked(IAccessibilityManager service) {if (service == null) {IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE);if (iBinder == null) {return;}service = IAccessibilityManager.Stub.asInterface(iBinder);}try {// 向AccessibilityManagerService添加client時會返回當前開關狀態final int stateFlags = service.addClient(mClient, mUserId);setStateLocked(stateFlags);mService = service;} catch (RemoteException re) {Log.e(LOG_TAG, "AccessibilityManagerService is dead", re);} }AccessibilityManager中用mIsEnabled變量標識當前輔助功能是否開啟,如果當前已經和AccessibilityManagerService建立了聯系則直接返回該標識,如果沒有會嘗試和AccessibilityManagerService聯系,調用AccessibilityManagerService.addClient(mClient, mUserId)方法就能得到當前輔助功能的開關狀態,之后通過setStateLocked(stateFlags)給mIsEnabled變量賦值。
private void setStateLocked(int stateFlags) {final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0;final boolean touchExplorationEnabled =(stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0;final boolean highTextContrastEnabled =(stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0;final boolean wasEnabled = mIsEnabled;final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled;final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled;// Ensure listeners get current state from isZzzEnabled() calls.mIsEnabled = enabled;mIsTouchExplorationEnabled = touchExplorationEnabled;mIsHighTextContrastEnabled = highTextContrastEnabled;if (wasEnabled != enabled) {mHandler.sendEmptyMessage(MyHandler.MSG_NOTIFY_ACCESSIBILITY_STATE_CHANGED);}if (wasTouchExplorationEnabled != touchExplorationEnabled) {mHandler.sendEmptyMessage(MyHandler.MSG_NOTIFY_EXPLORATION_STATE_CHANGED);}if (wasHighTextContrastEnabled != highTextContrastEnabled) {mHandler.sendEmptyMessage(MyHandler.MSG_NOTIFY_HIGH_TEXT_CONTRAST_STATE_CHANGED);} }除此之外還可以看到我們可以向AccessibilityManager注冊一些AccessibilityStateChangeListener,當開關狀態發生變化時我們能拿到相應的回調。
在調用AccessibilityManagerService.addClient(mClient, mUserId)時,目標App就向AccessibilityManagerService注冊了自己,mClient代碼如下:
private final IAccessibilityManagerClient.Stub mClient =new IAccessibilityManagerClient.Stub() {public void setState(int state) {mHandler.obtainMessage(MyHandler.MSG_SET_STATE, state, 0).sendToTarget();} };當輔助功能開關變化時,AccessibilityManagerService會調用每個client的setState(int state)方法,通過Handler又調用了setStateLocked(state)方法修改了開關狀態。
第7步AccessibilityManagerService進行了哪些檢查和準備
系統不允許后臺用戶發送AccessibilityEvent,所以首先會檢查處理后的UserId是否和當前UserId一樣。實際使用中,多用戶的情況并不多,所以我們基本無需考慮UserId的問題。
public int resolveCallingUserIdEnforcingPermissionsLocked(int userId) {final int callingUid = Binder.getCallingUid();if (callingUid == 0|| callingUid == Process.SYSTEM_UID|| callingUid == Process.SHELL_UID) {if (userId == UserHandle.USER_CURRENT|| userId == UserHandle.USER_CURRENT_OR_SELF) {return mCurrentUserId;}return resolveProfileParentLocked(userId);}final int callingUserId = UserHandle.getUserId(callingUid);if (callingUserId == userId) {return resolveProfileParentLocked(userId);}final int callingUserParentId = resolveProfileParentLocked(callingUserId);if (callingUserParentId == mCurrentUserId &&(userId == UserHandle.USER_CURRENT|| userId == UserHandle.USER_CURRENT_OR_SELF)) {return mCurrentUserId;}if (!hasPermission(Manifest.permission.INTERACT_ACROSS_USERS)&& !hasPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL)) {throw new SecurityException("Call from user " + callingUserId + " as user "+ userId + " without permission INTERACT_ACROSS_USERS or "+ "INTERACT_ACROSS_USERS_FULL not allowed.");}if (userId == UserHandle.USER_CURRENT|| userId == UserHandle.USER_CURRENT_OR_SELF) {return mCurrentUserId;}throw new IllegalArgumentException("Calling user can be changed to only "+ "UserHandle.USER_CURRENT or UserHandle.USER_CURRENT_OR_SELF.");}private int resolveProfileParentLocked(int userId) {if (userId != mCurrentUserId) {final long identity = Binder.clearCallingIdentity();try {UserInfo parent = mUserManager.getProfileParent(userId);if (parent != null) {return parent.getUserHandle().getIdentifier();}} finally {Binder.restoreCallingIdentity(identity);}}return userId;}之后會檢查這個AccessibilityEvent能不能分發,見下面的代碼,一部分EventType是必定可以分發的,其他的EventType會再檢查Window的情況。
private boolean canDispatchAccessibilityEventLocked(AccessibilityEvent event) {final int eventType = event.getEventType();switch (eventType) {case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:case AccessibilityEvent.TYPE_ANNOUNCEMENT:case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END:case AccessibilityEvent.TYPE_GESTURE_DETECTION_START:case AccessibilityEvent.TYPE_GESTURE_DETECTION_END:case AccessibilityEvent.TYPE_TOUCH_INTERACTION_START:case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT:case AccessibilityEvent.TYPE_ASSIST_READING_CONTEXT:case AccessibilityEvent.TYPE_WINDOWS_CHANGED: {return true;}default: {return isRetrievalAllowingWindow(event.getWindowId());}}}private boolean isRetrievalAllowingWindow(int windowId) {// The system gets to interact with any window it wants.if (Binder.getCallingUid() == Process.SYSTEM_UID) {return true;}if (windowId == mActiveWindowId) {return true;}return findWindowById(windowId) != null;}這2項檢查通過之后,就準備分發事件了,updateActiveAndAccessibilityFocusedWindowLocked方法主要更新了一些跟Window相關的東西,而updateEventSourceLocked方法則是會把不在RETRIEVAL_ALLOWING_EVENT_TYPES之中的AccessibilityEvent的source置為null。
private static final int RETRIEVAL_ALLOWING_EVENT_TYPES =AccessibilityEvent.TYPE_VIEW_CLICKED| AccessibilityEvent.TYPE_VIEW_FOCUSED| AccessibilityEvent.TYPE_VIEW_HOVER_ENTER| AccessibilityEvent.TYPE_VIEW_HOVER_EXIT| AccessibilityEvent.TYPE_VIEW_LONG_CLICKED| AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED| AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED| AccessibilityEvent.TYPE_VIEW_SELECTED| AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED| AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED| AccessibilityEvent.TYPE_VIEW_SCROLLED| AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED| AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED| AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY; 原文地址: https://darkness463.github.io/2017/04/17/accessibility-event/
總結
以上是生活随笔為你收集整理的总结】Android辅助功能(一)-AccessibilityEvent的分发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android AOP之字节码插桩
- 下一篇: android sina oauth2.