Android輔助功能---全局手勢放大
在Android的輔助功能中,存在一個點擊三次屏幕觸發屏幕放大功能。
這個功能的使用頻率實在是低...但是為什麼會想記錄一下這個功能的實現原理。第一,在處理性能問題的時候遇到了相關代碼;其次其實現的原理還是具有部分啟發性質的。主要還是研究啟發部分:
1、如何實現手勢攔截
2、全局放大的原理(主要在system_server中存在雙編舞者協作實現),如下圖所示在啟動手勢放大過程中systrace抓取到下面的現象:
一、手勢攔截
在設置中打開放大手勢的開關,會設置Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED這個屬性值,AccessiblityContentObserver中的onChange會處理這個值的變化:
@Overriden4456 public void onChange(boolean selfChange, Uri uri) {n4457 synchronized (mLock) {n4458 // Profiles share the accessibility state of the parent. Therefore,n4459 // we are checking for changes only the parent settings.n4460 UserState userState = getCurrentUserStateLocked();n4461n4462 // If the automation service is suppressing, we will update when it dies.n4463 if (userState.isUiAutomationSuppressingOtherServices()) {n4464 return;n4465 }n4466n4467 if (mTouchExplorationEnabledUri.equals(uri)) {n4468 if (readTouchExplorationEnabledSettingLocked(userState)) {n4469 onUserStateChangedLocked(userState);n4470 }n4471 } else if (mDisplayMagnificationEnabledUri.equals(uri)) {n4472 if (readDisplayMagnificationEnabledSettingLocked(userState)) {n4479 onUserStateChangedLocked(userState);n4480 }n4481 }n
在onUserStateChangedLocked中會調用updateMagnificationLocked以及scheduleUpdateInputFilter去更新當前系統狀態:
updateMagnificationLocked是用於建立wms和輔助功能服務的聯繫
scheduleUpdateInputFilter是用於在輸入層面建立手勢攔截,往Input流程中加入inputfilter
1820 private void updateMagnificationLocked(UserState userState) {n1821 if (userState.mUserId != mCurrentUserId) {n1822 return;n1823 }n1824n1825 if (userState.mIsDisplayMagnificationEnabled ||n1826 userHasListeningMagnificationServicesLocked(userState)) {n1827 // Initialize the magnification controller if necessaryn1828 getMagnificationController();n //核心在於放大控制器的註冊n1829 mMagnificationController.register();n1830 } else if (mMagnificationController != null) {n //當關閉此功能的時候會調用反註冊n1831 mMagnificationController.unregister();n1832 }n1833 }n
實際上就是通過MagnificationController去註冊。
55/**n56 * This class is used to control and query the state of display magnificationn57 * from the accessibility manager and related classes. It is responsible forn58 * holding the current state of magnification and animation, and it handlesn59 * communication between the accessibility manager and window manager.n60 */n61class MagnificationControllern
從對這個類的描述可以看出,它是為了控制和查詢當前屏幕的放大狀態;其次用於輔助服務和WMS之間的通信工作。這些具體的含義還是放到代碼中去一一解釋。
首先看看他的register函數:
public void register() {n130 synchronized (mLock) {n131 if (!mRegistered) {n //step1、註冊廣播監聽亮滅屏事件n132 mScreenStateObserver.register();n //step2、註冊WMS中的回調(與WMS之間通信)n133 mWindowStateObserver.register();n //step3、使能跟動畫相關的函數(雖然這個類名字有點奇怪,但還是能猜到是跟動畫相關的)n134 mSpecAnimationBridge.setEnabled(true);n135 // Obtain initial state.n136 mWindowStateObserver.getMagnificationRegion(mMagnificationRegion);n137 mMagnificationRegion.getBounds(mMagnificationBounds);n138 mRegistered = true;n139 }n140 }n141 }n
step1就略過從step2開始看它是如何跟wms進行交互的。
/**n957 * This class handles the screen magnification when accessibility is enabled.n958 */n959 private static class WindowStateObservern960 implements WindowManagerInternal.MagnificationCallbacks {n......n975n976 public WindowStateObserver(Context context, MagnificationController controller) {n977 mController = controller;n978 mWindowManager = LocalServices.getService(WindowManagerInternal.class);n979 mHandler = new CallbackHandler(context);n980 }n981n982 public void register() {n987 mWindowManager.setMagnificationCallbacks(this);n990 }n991n
WindowStateObserver實現了介面MagnificationCallbacks,這個介面是wms用於通知放大控制器當前wms端有了哪些變化的:
/**n51 * Callbacks for contextual changes that affect the screen magnificationn52 * feature.n53 */n54 public interface MagnificationCallbacks {n55n56 /**n57 * Called when the region where magnification operates changes. Note that this isnt then58 * entire screen. For example, IMEs are not magnified.n *這種情況在放大的情況下點開了輸入法,輸入法界面是不能夠被放大的,但是由於其佔用了一定的屏幕空間,就會導致放大的區域變小,wms就會回調註冊的該方法n59 *n60 * @param magnificationRegion the current magnification regionn61 */n62 public void onMagnificationRegionChanged(Region magnificationRegion);n63n64 /**n65 * Called when an application requests a rectangle on the screen to allown66 * the client to apply the appropriate pan and scale.n67 *n68 * @param left The rectangle left.n69 * @param top The rectangle top.n70 * @param right The rectangle right.n71 * @param bottom The rectangle bottom.n72 */n73 public void onRectangleOnScreenRequested(int left, int top, int right, int bottom);n74n75 /**n76 * Notifies that the rotation changed.n77 *n78 * @param rotation The current rotation.n79 */n80 public void onRotationChanged(int rotation);n81n82 /**n83 * Notifies that the context of the user changed. For example, an applicationn84 * was started.n *context發生變化(個人理解為當前Activity發生了切換)n85 */n86 public void onUserContextChanged();n87 }n
通過註冊WindowStateObserver到WMS,就建立wms和AccessibilityMS的溝通了。
回到前面的step3,使能SpecAnimationBridge,從下面這個類的注釋可以看出它有兩個功能
/**n727 * Class responsible for animating spec on the main thread and sending specn728 * updates to the window manager.n729 */n730 private static class SpecAnimationBridge {n
1:將放大相關的參數發送給wms
2:在主線程上管理動畫:一般而言system_server中只有android.display這條線程有編舞者用來做系統窗口的動畫,這裡的SpecAnimationBridge就會使用UI線程來創建編舞者,完成放大的動畫操作
回到建立手勢攔截上,scheduleUpdateInputFilter函數就是完成插入一個inputfilter到input流程中
1383 private void scheduleUpdateInputFilter(UserState userState) {n1384 mMainHandler.obtainMessage(MainHandler.MSG_UPDATE_INPUT_FILTER, userState).sendToTarget();n1385 }n1386n1387 private void updateInputFilter(UserState userState) {n1388 boolean setInputFilter = false;n1389 AccessibilityInputFilter inputFilter = null;n1390 synchronized (mLock) {n1391 int flags = 0;n......n1412 if (flags != 0) {n1413 if (!mHasInputFilter) {n1414 mHasInputFilter = true;n1415 if (mInputFilter == null) {n1416 mInputFilter = new AccessibilityInputFilter(mContext,n1417 AccessibilityManagerService.this);n1418 }n1419 inputFilter = mInputFilter;n1420 setInputFilter = true;n1421 }n1422 mInputFilter.setUserAndEnabledFeatures(userState.mUserId, flags);n1423 } else {n......n1430 }n1431 }n1432 if (setInputFilter) {n1433 mWindowManagerService.setInputFilter(inputFilter);n1434 }n1435 }n
先拋開一些細節,主要的原理就是創建一個AccessibilityInputFilter(其基類是InputFilter),並根據對應的輔助功能設置其flag,然後通過setInputFilter設置到wms中去。
在android.view包下存在一個InputFilter用於做輸入事件的攔截,但是這個API是設定為hide的,APP當然是不能去使用的。 可以進入如下的鏈接閱讀以下這個類的描述
InputFilter.java
通過inputFilter的注釋可以得到有幾個要點:
1、當前系統中只能install一個inputfilter
2、inputfilter的作用域在傳遞給APP之前
3、event流必須是內部一致的,也就是必須是down-up-down-up這樣的序列而不能是down-down-up-up這樣
4、當有事件達到時會回調public void onInputEvent(InputEvent event, int policyFlags)這個函數進行處理
這裡插入介紹一個InputFilterHost類,在InputFilter不處理當前Event的時候通過InputFilterHost的sendInputEvent將輸入事件再次注入到native層的InputManagerService中,然後走正常的input流程
/**n2226 * Hosting interface for input filters to call back into the input manager.n2227 */n2228 private final class InputFilterHost extends IInputFilterHost.Stub {n2229 private boolean mDisconnected;n2230n2231 public void disconnectLocked() {n2232 mDisconnected = true;n2233 }n2234n2235 @Overriden2236 public void sendInputEvent(InputEvent event, int policyFlags) {n2237 if (event == null) {n2238 throw new IllegalArgumentException("event must not be null");n2239 }n2240n2241 synchronized (mInputFilterLock) {n2242 if (!mDisconnected) {n2243 nativeInjectInputEvent(mPtr, event, Display.DEFAULT_DISPLAY, 0, 0,n2244 InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0,n2245 policyFlags | WindowManagerPolicy.FLAG_FILTERED);n2246 }n2247 }n2248 }n2249 }n
到此基本上對InputFilter有個大致的概念了。到此打開手勢開關之後,主要就做了兩件事:
1、創建MagnificationController跟wms和AccessibilityMS建立溝通
2、往InputManagerService插入InputFilter
那接下來看看AccessibilityInputFilter是具體怎麼實作出過濾手勢的。基類雖然簡單但是這個類的實現還是比較複雜的。
還是從基礎的流程開始,因為當有事件進來的話會回調onInputEvent,AccessibilityInputFilter的onInputEvent方法
173 @Overriden174 public void onInputEvent(InputEvent event, int policyFlags) {nn //1、mEventHandler為空n180 if (mEventHandler == null) {n181 super.onInputEvent(event, policyFlags);n182 return;n183 }n184 //2、EventStreamState為空n185 EventStreamState state = getEventStreamState(event);n186 if (state == null) {n187 super.onInputEvent(event, policyFlags);n188 return;n189 }n190 //3、如果這個event沒有標記為傳遞給用戶n191 int eventSource = event.getSource();n192 if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) {n193 state.reset();n194 mEventHandler.clearEvents(eventSource);n195 super.onInputEvent(event, policyFlags);n196 return;n197 }n198 //4、如果設備的deviceID發生變化n199 if (state.updateDeviceId(event.getDeviceId())) {n200 mEventHandler.clearEvents(eventSource);n201 }n202 //5、如果設備ID無效n203 if (!state.deviceIdValid()) {n204 super.onInputEvent(event, policyFlags);n205 return;n206 }n207n208 if (event instanceof MotionEvent) {n //6、需要添加該filter的時候的flag滿足會影響滑動事件n209 if ((mEnabledFeatures & FEATURES_AFFECTING_MOTION_EVENTS) != 0) {n210 MotionEvent motionEvent = (MotionEvent) event;n211 processMotionEvent(state, motionEvent, policyFlags);n212 return;n213 } else {n214 super.onInputEvent(event, policyFlags);n215 }n216 } else if (event instanceof KeyEvent) {n217 ......n219 }n220 }n
如上面代碼所示,很多不滿足條件的情況下,就會通過super.onInputEvent(event, policyFlags)交給inputfilter處理,也就是交給inputfilterhost重新注入到輸入的流程中去。
這裡有兩個比較陌生的東西:mEventHandler和state,這兩個先不詳細解釋,後面再做介紹
最後如果正常的話則會調用processMotionEvent處理
252 private void processMotionEvent(EventStreamState state, MotionEvent event, int policyFlags) {n253 if (!state.shouldProcessScroll() && event.getActionMasked() == MotionEvent.ACTION_SCROLL) {n254 super.onInputEvent(event, policyFlags);n255 return;n256 }n257n258 if (!state.shouldProcessMotionEvent(event)) {n259 return;n260 }n261n262 batchMotionEvent(event, policyFlags);n263 }n
然後會call到batchMotionEvent:
277 private void batchMotionEvent(MotionEvent event, int policyFlags) {n278 if (DEBUG) {n279 Slog.i(TAG, "Batching event: " + event + ", policyFlags: " + policyFlags);n280 }n //step1、如果當前時間隊列為空,則以此事件作為隊頭,然後申請一次編舞者的input處理(這裡還是第一次看到編舞者的input回調的實例)n281 if (mEventQueue == null) {n282 mEventQueue = MotionEventHolder.obtain(event, policyFlags);n283 scheduleProcessBatchedEvents();n284 return;n285 }n //step2、看當前的event是否跟隊頭的event是相同屬性的,如果是相同屬性則可以批量處理。例如那種手指移動的事件,對於這種手勢想檢測那種手指移動畫出來的幾何圖形估計就不太可能n //後面會研究下針對幾何圖形的檢測有什麼辦法n286 if (mEventQueue.event.addBatch(event)) {n287 return;n288 }n //step3、如果上面兩種情況都不是,則把當前這次的事件串到事件隊列中去n289 MotionEventHolder holder = MotionEventHolder.obtain(event, policyFlags);n290 holder.next = mEventQueue;n291 mEventQueue.previous = holder;n292 mEventQueue = holder;n293 }n
這裡的MotionEventHolder類就是每個Event的容器,一個容器中只放一個event,由靜態變數的對象池進行管理,用於節省創建對象的開銷 ;mEventQueue是則是這個輸入隊列的隊頭
看到上面的step1的時候肯定會有一個疑問,就是為啥只建立一個隊頭就需要馬上去請求處理。例如點擊三次觸發放大這種情況,那麼隊頭只有一個ACTION_DOWN的時候就會去觸發處理了,明明你離攢夠6個事件還差的遠
下面這個runnable就是post到編舞者上類型為input的回調
private final Runnable mProcessBatchedEventsRunnable = new Runnable() {n94 @Overriden95 public void run() {n96 final long frameTimeNanos = mChoreographer.getFrameTimeNanos();n97 if (DEBUG) {n98 Slog.i(TAG, "Begin batch processing for frame: " + frameTimeNanos);n99 }n //這個函數對隊列中的event事件進行處理n100 processBatchedEvents(frameTimeNanos);n101 if (DEBUG) {n102 Slog.i(TAG, "End batch processing.");n103 }n //如果之前的processBatchedEvents對隊列中的事件沒有完全消化,則我們就繼續等待,請求下一次編舞者到來的時候能否處理完n //所以針對之前的只有一個action_down的情況,肯定是處理不掉需要繼續等待的n104 if (mEventQueue != null) {n105 scheduleProcessBatchedEvents();n106 }n107 }n108 };n
那關鍵就是processBatchedEvents是依據什麼規則來消耗當前的事件隊列了
295 private void processBatchedEvents(long frameNanos) {n296 MotionEventHolder current = mEventQueue;n297 if (current == null) {n298 return;n299 }n //因為每次來的新的event都是放在隊頭,所以每次解析的時候,先要逐漸往後退,讓current指向隊尾,也就是最早的事件n300 while (current.next != null) {n301 current = current.next;n302 }n303 while (true) {n //跳出死循環的條件1:隊列消耗完畢n304 if (current == null) {n305 mEventQueue = null;n306 break;n307 }nn //event的事件時間如果晚於當前編舞者執行的事件,則該輪迴調不處理n308 if (current.event.getEventTimeNano() >= frameNanos) {n309 // Finished with this choreographer frame. Do the rest on the next one.n310 current.next = null;n311 break;n312 }n //這裡感覺是依次將事件灌入到handleMotionEvent函數中,如果灌入的事件序列滿足某個模式則會馬上觸發n //例如三次點擊事件的down-up-down-up-down-up檢測到了則會觸發放大n313 handleMotionEvent(current.event, current.policyFlags);n314 MotionEventHolder prior = current;n315 current = current.previous;n316 prior.recycle();n317 }n318 }n
再看handleMotionEvent的處理
private void handleMotionEvent(MotionEvent event, int policyFlags) {n321 if (DEBUG) {n322 Slog.i(TAG, "Handling batched event: " + event + ", policyFlags: " + policyFlags);n323 }n324 // Since we do batch processing it is possible that by the time then325 // next batch is processed the event handle had been set to null.n326 if (mEventHandler != null) {n327 mPm.userActivity(event.getEventTime(), false);n328 MotionEvent transformedEvent = MotionEvent.obtain(event);n329 mEventHandler.onMotionEvent(transformedEvent, event, policyFlags);n330 transformedEvent.recycle();n331 }n332 }n
這裡又有了之前提到的mEventHandler;這個的類型是EventStreamTransformation,從名字也能看出這個類是將事件流進行轉換,系統中有很多這個的實現體; 從下面這個addFirstEventHander來看,EventStreamTransformation也是鏈式排列通過onMotionEvent對事件鏈表進行處理
/**n426 * Adds an event handler to the event handler chain. The handler is added at the beginning ofn427 * the chain.n428 *n429 * @param handler The handler to be added to the event handlers list.n430 */n431 private void addFirstEventHandler(EventStreamTransformation handler) {n432 if (mEventHandler != null) {n433 handler.setNext(mEventHandler);n434 } else {n435 handler.setNext(this);n436 }n437 mEventHandler = handler;n438 }n
我們就只看跟放大手勢相關的EventStreamTransformation
class MagnificationGestureHandler implements EventStreamTransformation {n
其onMotionEvent的實現如下:
148 @Overriden149 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {n //如果當前的event不是來自於觸摸屏則交由下個EventStreamTransformation處理n150 if (!event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {n151 if (mNext != null) {n152 mNext.onMotionEvent(event, rawEvent, policyFlags);n153 }n154 return;n155 }n //mDetectControlGestures這個變數代表是否檢測控制手勢,如果這個為false則會直接return掉(這。。。不適用為何還要把這個插進去?)n156 if (!mDetectControlGestures) {n157 if (mNext != null) {n158 dispatchTransformedEvent(event, rawEvent, policyFlags);n159 }n160 return;n161 }n //這裡先刷新一下檢測狀態,後面根據狀態做處理n162 mMagnifiedContentInteractionStateHandler.onMotionEvent(event, rawEvent, policyFlags);n163 switch (mCurrentState) {n164 case STATE_DELEGATING: {n165 handleMotionEventStateDelegating(event, rawEvent, policyFlags);n166 }n167 break;n168 case STATE_DETECTING: {n169 mDetectingStateHandler.onMotionEvent(event, rawEvent, policyFlags);n170 }n171 break;n172 case STATE_VIEWPORT_DRAGGING: {n173 mStateViewportDraggingHandler.onMotionEvent(event, rawEvent, policyFlags);n174 }n175 break;n176 case STATE_MAGNIFIED_INTERACTION: {n177 // mMagnifiedContentInteractionStateHandler handles events onlyn178 // if this is the current state since it uses ScaleGestureDetecotrn179 // and a GestureDetector which need well formed event stream.n180 }n181 break;n182 default: {n183 throw new IllegalStateException("Unknown state: " + mCurrentState);n184 }n185 }n186 }n
上面提到的幾個STATE因為沒有注釋,還沒有完全理清其含義,這個留在以後討論手勢的實現裡面再進一步確認
mMagnifiedContentInteractionStateHandler的類型為下面這個,看定義也是比較麻煩,就先不管無關細節
353 /**n354 * This class determines if the user is performing a scale or pan gesture.n * 這個類的主要作用就是在已經放大的基礎上,處理用戶的滑動和縮放操作n355 */n356 private final class MagnifiedContentInteractionStateHandler extends SimpleOnGestureListenern357 implements OnScaleGestureListener, MotionEventHandler {n
其onMotionEvent實現如下,當有觸摸事件進來的時候:
380 @Overriden381 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {n382 //step1:先由放大手勢檢測器處理n mScaleGestureDetector.onTouchEvent(event);n //step2:再由姿勢檢測器處理滑動操作(因為這個姿勢檢測器只實現了onScroll操作)n383 mGestureDetector.onTouchEvent(event);n //step3:如果當前的狀態非STATE_MAGNIFIED_INTERACTION就直接returnn //從這裡我們可以猜測出來STATE_MAGNIFIED_INTERACTION對應的就是開啟了放大的狀態,且沒有正在拖動和縮放的過程中n384 if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {n385 return;n386 }n387 if (event.getActionMasked() == MotionEvent.ACTION_UP) {n388 clear();n389 mMagnificationController.persistScale();n390 if (mPreviousState == STATE_VIEWPORT_DRAGGING) {n391 transitionToState(STATE_VIEWPORT_DRAGGING);n392 } else {n393 transitionToState(STATE_DETECTING);n394 }n395 }n396 }n
step1和step2都是利用Android的API提供的手勢工具類處理對縮放手勢和滑動手勢的處理:
滾動手勢,應該對應到的是兩指觸摸的那種滑動
430 @Overriden431 public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX,n432 float distanceY) {n433 if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {n434 return true;n435 }n436 if (DEBUG_PANNING) {n437 Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceXn438 + " scrollY: " + distanceY);n439 }n440 mMagnificationController.offsetMagnifiedRegion(distanceX, distanceY,n441 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);n442 return true;n443 }n
最終會通知MagnificationController對縮放區域做偏移
類似的縮放操作是通過setScale去對縮放區域進行放大和縮小
413 @Overriden414 public boolean onScale(ScaleGestureDetector detector) {n......n446n447 final float pivotX = detector.getFocusX();n448 final float pivotY = detector.getFocusY();n449 mMagnificationController.setScale(scale, pivotX, pivotY, false,n450 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);n451 return true;n452 }n
MagnificationController的偏移和縮放最終都是通過其設置的動畫以及其和wms的一些交互實現的,這個會在第二部分中介紹到
了解了縮放和縮放後的拖動操作的具體實現的位置,那麼還有一個三擊屏幕開啟的手勢還沒有提到實現的位置
當檢測到放大手勢時,會通過DetectingStateHandler的onActionTap來觸發屏幕放大的操作
private final class DetectingStateHandler implements MotionEventHandlern
這個類主要就是用於檢測三擊屏幕的手勢這塊就先略過以後討論,當檢測到三擊屏幕之後會調用下面的函數
private void onActionTap(MotionEvent up, int policyFlags) {n773 if (DEBUG_DETECTING) {n774 Slog.i(LOG_TAG, "onActionTap()");n775 }n776n777 if (!mMagnificationController.isMagnifying()) {n778 final float targetScale = mMagnificationController.getPersistedScale();n779 final float scale = MathUtils.constrain(targetScale, MIN_SCALE, MAX_SCALE);n780 mMagnificationController.setScaleAndCenter(scale, up.getX(), up.getY(), true,n781 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);n782 } else {n783 mMagnificationController.reset(true);n784 }n785 }n
通過MagnificcationController的setScaleAndCenter去設定縮放的幅度和中心點
二、屏幕放大
緊接上面的MagnificationController.setScaleAndCenter,前三個參數指定了放大的倍數以及放大的中心點
469 public boolean setScaleAndCenter(n470 float scale, float centerX, float centerY, boolean animate, int id) {n471 synchronized (mLock) {n472 if (!mRegistered) {n473 return false;n474 }n475 return setScaleAndCenterLocked(scale, centerX, centerY, animate, id);n476 }n477 }n478n479 private boolean setScaleAndCenterLocked(float scale, float centerX, float centerY,n480 boolean animate, int id) {n //step1、先更新放大的參數信息(放大倍數和中心點)n481 final boolean changed = updateMagnificationSpecLocked(scale, centerX, centerY);n //step2、通過參數動畫橋來更新當前的顯示狀態n482 mSpecAnimationBridge.updateSentSpec(mCurrentMagnificationSpec, animate);n483 if (isMagnifying() && (id != INVALID_ID)) {n484 mIdOfLastServiceToMagnify = id;n485 }n486 return changed;n487 }n
通過SpecAnimationBridge的updateSentSpec來啟動放大的操作
808 public void updateSentSpec(MagnificationSpec spec, boolean animate) {n809 if (Thread.currentThread().getId() == mMainThreadId) {n810 // Already on the main thread, dont bother proxying.n811 updateSentSpecInternal(spec, animate);n812 } else {n813 mHandler.obtainMessage(ACTION_UPDATE_SPEC,n814 animate ? 1 : 0, 0, spec).sendToTarget();n815 }n816 }n
無論caller是否是主線程最終會在主線程上調用到下面函數
818 /**n819 * Updates the sent spec.n820 */n821 private void updateSentSpecInternal(MagnificationSpec spec, boolean animate) {n822 if (mTransformationAnimator.isRunning()) {n823 mTransformationAnimator.cancel();n824 }n825n826 // If the current and sent specs dont match, update the sent spec.n827 synchronized (mLock) {n828 final boolean changed = !mSentMagnificationSpec.equals(spec);n829 if (changed) {n830 if (animate) {n831 animateMagnificationSpecLocked(spec);n832 } else {n833 setMagnificationSpecLocked(spec);n834 }n835 }n836 }n837 }n
會先判斷當前是否有動畫在執行,如果正在執行則取消掉;判斷當前更新的Spec跟之前的Spec是否相等,如果發生了改變,然後根據是否需要執行動畫選擇按照新的Spec運行動畫或者僅僅設置一個新的Spec。
當然從前面的代碼來看這個動畫肯定是需要執行的,所以來看下animateMagnificationSpecLocked函數
839 private void animateMagnificationSpecLocked(MagnificationSpec toSpec) {n840 mTransformationAnimator.setObjectValues(mSentMagnificationSpec, toSpec);n841 mTransformationAnimator.start();n842 }n
這個動畫就是由TransformationAnimator完成的,其作為一個屬性動畫定義為
762 final MagnificationSpecProperty property = new MagnificationSpecProperty();n763 final MagnificationSpecEvaluator evaluator = new MagnificationSpecEvaluator();n764 final long animationDuration = context.getResources().getInteger(n765 R.integer.config_longAnimTime);n766 mTransformationAnimator = ObjectAnimator.ofObject(this, property, evaluator,n767 mSentMagnificationSpec);n768 mTransformationAnimator.setDuration(animationDuration);n769 mTransformationAnimator.setInterpolator(new DecelerateInterpolator(2.5f));n
創建屬性動畫的第二個參數property的定義如下,當動畫間隔的時長到的時候會回調其set操作
872 private static class MagnificationSpecPropertyn873 extends Property<SpecAnimationBridge, MagnificationSpec> {n874 public MagnificationSpecProperty() {n875 super(MagnificationSpec.class, "spec");n876 }n877n878 @Overriden879 public MagnificationSpec get(SpecAnimationBridge object) {n880 synchronized (object.mLock) {n881 return object.mSentMagnificationSpec;n882 }n883 }n884n885 @Overriden886 public void set(SpecAnimationBridge object, MagnificationSpec value) {n887 synchronized (object.mLock) {n888 object.setMagnificationSpecLocked(value);n889 }n890 }n891 }n
會調用SpecAnimationBridge的setMagnificationSpecLocked操作去更新放大動畫
844 private void setMagnificationSpecLocked(MagnificationSpec spec) {n845 if (mEnabled) {n846 if (DEBUG_SET_MAGNIFICATION_SPEC) {n847 Slog.i(LOG_TAG, "Sending: " + spec);n848 }n849 //step1、根據當前動畫更新放大Specn850 mSentMagnificationSpec.setTo(spec);n //step2、然後通過WindowManager去更新Spec以及觸發動畫n851 mWindowManager.setMagnificationSpec(spec);n852 }n853 }n
然後call到AccessibilityController的setMagnificationSpecLocked函數,AccessibilityController這個類文件在/frameworks/base/services/core/java/com/android/server/wm下,說明他應該是屬於wms的東西
123 public void setMagnificationSpecLocked(MagnificationSpec spec) {n124 if (mDisplayMagnifier != null) {n125 mDisplayMagnifier.setMagnificationSpecLocked(spec);n126 }n127 if (mWindowsForAccessibilityObserver != null) {n128 mWindowsForAccessibilityObserver.scheduleComputeChangedWindowsLocked();n129 }n130 }n
最核心的在調用DisplayMagnifier的setMagnificationSpecLocked
275 public void setMagnificationSpecLocked(MagnificationSpec spec) {n //step1、更新視口的縮放參數n276 mMagnifedViewport.updateMagnificationSpecLocked(spec);n //step2、計算放大的視口的邊框n277 mMagnifedViewport.recomputeBoundsLocked();n //step3、觸發WindowManager的窗口切換動畫n278 mWindowManagerService.scheduleAnimationLocked();n279 }n
在這個時候我們就可以得到這個放大的流程中存在兩個編舞者協同工作的結論了 如下圖所示:
在System Server的主線程中有一個跟放大參數更新相關的屬性動畫在利用主線程的編舞者更新參數,其次在android.display線程上用於組織系統窗口動畫的編舞者負責實際去更新放大界面的對應的Surface
來到WindowAnimator的animateLocked
/** Locked on mService.mWindowMap. */n809 private void animateLocked(long frameTimeNs) {n......n //step1、為每個窗口準備Surfacen878 for (int j = 0; j < N; j++) {n879 windows.get(j).mWinAnimator.prepareSurfaceLocked(true);n880 }n......n889n890 for (int i = 0; i < numDisplays; i++) {n891 final int displayId = mDisplayContentsAnimators.keyAt(i);n892n893 testTokenMayBeDrawnLocked(displayId);n894n895 final ScreenRotationAnimation screenRotationAnimation =n896 mDisplayContentsAnimators.valueAt(i).mScreenRotationAnimation;n897 if (screenRotationAnimation != null) {n898 screenRotationAnimation.updateSurfacesInTransaction();n899 }n900n901 orAnimating(mService.getDisplayContentLocked(displayId).animateDimLayers());n902 orAnimating(mService.getDisplayContentLocked(displayId).getDockedDividerController()n903 .animate(mCurrentTime));n904 //TODO (multidisplay): Magnification is supported only for the default display.n //step2、繪製放大後顯示的邊框n905 if (mService.mAccessibilityController != nulln906 && displayId == Display.DEFAULT_DISPLAY) {n907 mService.mAccessibilityController.drawMagnifiedRegionBorderIfNeededLocked();n908 }n909 }n......n990 }n
如上代碼中截取的step1和step2,在之前的系統窗口動畫的流程中並不是很起眼,但是這兩個地方對放大這個功能確實核心的步驟;
step1、
652 void prepareSurfaceLocked(final boolean recoveringMemory) {n1653 final WindowState w = mWin;n1654 if (!hasS urface()) {n1655 if (w.mOrientationChanging) {n1656 if (DEBUG_ORIENTATION) {n1657 Slog.v(TAG, "Orientation change skips hidden " + w);n1658 }n1659 w.mOrientationChanging = false;n1660 }n1661 return;n1662 }n1663n......n1674n1675 boolean displayed = false;n1676 //這裡就會根據放大參數得到當前的窗口的Frame大小n1677 computeShownFrameLocked();n1678n1679 setSurfaceBoundariesLocked(recoveringMemory);n
在computeShownFrameLocked函數中,會先去獲取放大參數,然後再對該窗口進行apply;其中applyMagnificationSpec會對當前的窗口Surface的矩陣進行變換
1149 if (mService.mAccessibilityController != null && displayId == DEFAULT_DISPLAY) {n1150 MagnificationSpec spec = mService.mAccessibilityControllern1151 .getMagnificationSpecForWindowLocked(mWin);n1152 applyMagnificationSpec(spec, tmpMatrix);n1153 }n
setSurfaceBoundariesLocked函數會通過SurfaceControl去設定到SurfaceFlinger中去,代碼比較長,就不貼了
step2、就是計算和繪製文章開頭放大那張圖的橘黃色的邊框
主要就是計算當前邊框的範圍,主要因為有些Window是規定不支持縮放的,例如虛擬導航欄和輸入法窗口。這部分主要涉及的是Region的子交並補的操作,值得去看下這些數學相關的計算思路
此外還想說明這個邊框是繪製在一個獨立的Layer上的,名字叫:Magnification Overlay,可以通過dumpsys SurfaceFlinger查看當前系統中是否存在該layer
其創建是通過ViewPort的構造創建的:
public ViewportWindow(Context context) {n705 SurfaceControl surfaceControl = null;n706 try {n707 mWindowManager.getDefaultDisplay().getRealSize(mTempPoint);n708 surfaceControl = new SurfaceControl(mWindowManagerService.mFxSession,n709 SURFACE_TITLE, mTempPoint.x, mTempPoint.y, PixelFormat.TRANSLUCENT,n710 SurfaceControl.HIDDEN);n711 } catch (OutOfResourcesException oore) {n712 /* ignore */n713 }n714 mSurfaceControl = surfaceControl;n715 mSurfaceControl.setLayerStack(mWindowManager.getDefaultDisplay()n716 .getLayerStack());n717 mSurfaceControl.setLayer(mWindowManagerService.mPolicy.getWindowLayerFromTypeLw(n718 WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY)n719 * WindowManagerService.TYPE_LAYER_MULTIPLIER);n720 mSurfaceControl.setPosition(0, 0);n721 mSurface.copyFrom(mSurfaceControl);n722n......n736 }n
總結:Input的高級進階應該就是手勢檢測了,手勢檢測確實設計起來需要比較高的精細度,需要考慮比較完整,設計狀態機,這個還需更深入研究下;其次這種利用雙編舞者在UI執行屬性動畫,在Display線程去改變Surface屬性的做法是值得效仿的,可以完成一些特殊需求的,可以多思考下這個的點做些挖掘。
推薦閱讀:
※如何評價不久之前發布的xposed for Android N?
※Android studio用真機調試時logcat一直輸出日誌?
※在eclipse中安裝不上genymotion插件,求解?
※關於安卓5.0的升級問題,為啥手機rom升級就是那麼蛋疼?
※知乎安卓客戶端關注和取消關注的這個按鈕點擊特效是怎麼實現的?