总结】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可通过此方法修改AccessibilityEvent dispatchPopulateAccessibilityEvent(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)方法。
@Overridepublic 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发送AccessibilityEvent doRecycle = 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)准备开始分发。
@Overridepublic 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); // 开始分发AccessibilityEvent notifyAccessibilityServicesDelayedLocked(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(); // 复制当前的AccessibilityEvent AccessibilityEvent newEvent = AccessibilityEvent.obtain(event); Message message; if ((mNotificationTimeout > 0) && (eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)) { // Allow at most one pending event final 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 mPendingEvents message = mEventDispatchHandler.obtainMessage(eventType, newEvent); } mEventDispatchHandler.sendMessageDelayed(message, mNotificationTimeout); } }public Handler mEventDispatchHandler = new Handler(mMainHandler.getLooper()) { @Override public 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 { // 分发给辅助App listener.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; ...// 其他实现省略 } }@Overridepublic 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(安卓)UI设计与开发】第06期:底部菜单栏(一)使用TabActivi
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)-郭霖
- 终结者:电脑显示Android手机屏幕之asm.jar工具正确的使用方法
- Android中用seekbar控件控制歌曲的进度
- 保护你的隐私,五种控制Android应用的权限的方法
- 【优才原创】Android的拖放机制
- 让Camera在portrait模式下不旋转90度
- Android(安卓)Context 到底是什么?
- Android注解框架Annotations从配置到应用