ScreenMagnifier.java revision 36e614c110dad174dea6017eb701f55339aee200
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.server.accessibility; 18 19import android.animation.Animator; 20import android.animation.Animator.AnimatorListener; 21import android.animation.ObjectAnimator; 22import android.animation.TypeEvaluator; 23import android.animation.ValueAnimator; 24import android.content.BroadcastReceiver; 25import android.content.Context; 26import android.content.Intent; 27import android.content.IntentFilter; 28import android.graphics.Canvas; 29import android.graphics.Color; 30import android.graphics.PixelFormat; 31import android.graphics.PointF; 32import android.graphics.PorterDuff.Mode; 33import android.graphics.Rect; 34import android.graphics.drawable.Drawable; 35import android.hardware.display.DisplayManager; 36import android.hardware.display.DisplayManager.DisplayListener; 37import android.os.AsyncTask; 38import android.os.Handler; 39import android.os.Message; 40import android.os.RemoteException; 41import android.os.ServiceManager; 42import android.provider.Settings; 43import android.util.MathUtils; 44import android.util.Property; 45import android.util.Slog; 46import android.view.Display; 47import android.view.DisplayInfo; 48import android.view.Gravity; 49import android.view.IDisplayContentChangeListener; 50import android.view.IWindowManager; 51import android.view.MotionEvent; 52import android.view.MotionEvent.PointerCoords; 53import android.view.MotionEvent.PointerProperties; 54import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener; 55import android.view.Surface; 56import android.view.View; 57import android.view.ViewConfiguration; 58import android.view.ViewGroup; 59import android.view.WindowInfo; 60import android.view.WindowManager; 61import android.view.WindowManagerPolicy; 62import android.view.accessibility.AccessibilityEvent; 63import android.view.animation.DecelerateInterpolator; 64import android.view.animation.Interpolator; 65 66import com.android.internal.R; 67import com.android.internal.os.SomeArgs; 68 69import java.util.ArrayList; 70 71/** 72 * This class handles the screen magnification when accessibility is enabled. 73 * The behavior is as follows: 74 * 75 * 1. Triple tap toggles permanent screen magnification which is magnifying 76 * the area around the location of the triple tap. One can think of the 77 * location of the triple tap as the center of the magnified viewport. 78 * For example, a triple tap when not magnified would magnify the screen 79 * and leave it in a magnified state. A triple tapping when magnified would 80 * clear magnification and leave the screen in a not magnified state. 81 * 82 * 2. Triple tap and hold would magnify the screen if not magnified and enable 83 * viewport dragging mode until the finger goes up. One can think of this 84 * mode as a way to move the magnified viewport since the area around the 85 * moving finger will be magnified to fit the screen. For example, if the 86 * screen was not magnified and the user triple taps and holds the screen 87 * would magnify and the viewport will follow the user's finger. When the 88 * finger goes up the screen will clear zoom out. If the same user interaction 89 * is performed when the screen is magnified, the viewport movement will 90 * be the same but when the finger goes up the screen will stay magnified. 91 * In other words, the initial magnified state is sticky. 92 * 93 * 3. Pinching with any number of additional fingers when viewport dragging 94 * is enabled, i.e. the user triple tapped and holds, would adjust the 95 * magnification scale which will become the current default magnification 96 * scale. The next time the user magnifies the same magnification scale 97 * would be used. 98 * 99 * 4. When in a permanent magnified state the user can use two or more fingers 100 * to pan the viewport. Note that in this mode the content is panned as 101 * opposed to the viewport dragging mode in which the viewport is moved. 102 * 103 * 5. When in a permanent magnified state the user can use three or more 104 * fingers to change the magnification scale which will become the current 105 * default magnification scale. The next time the user magnifies the same 106 * magnification scale would be used. 107 * 108 * 6. The magnification scale will be persisted in settings and in the cloud. 109 */ 110public final class ScreenMagnifier implements EventStreamTransformation { 111 112 private static final boolean DEBUG_STATE_TRANSITIONS = false; 113 private static final boolean DEBUG_DETECTING = false; 114 private static final boolean DEBUG_TRANSFORMATION = false; 115 private static final boolean DEBUG_PANNING = false; 116 private static final boolean DEBUG_SCALING = false; 117 private static final boolean DEBUG_VIEWPORT_WINDOW = false; 118 private static final boolean DEBUG_WINDOW_TRANSITIONS = false; 119 private static final boolean DEBUG_ROTATION = false; 120 private static final boolean DEBUG_GESTURE_DETECTOR = false; 121 private static final boolean DEBUG_MAGNIFICATION_CONTROLLER = false; 122 123 private static final String LOG_TAG = ScreenMagnifier.class.getSimpleName(); 124 125 private static final int STATE_DELEGATING = 1; 126 private static final int STATE_DETECTING = 2; 127 private static final int STATE_VIEWPORT_DRAGGING = 3; 128 private static final int STATE_MAGNIFIED_INTERACTION = 4; 129 130 private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f; 131 private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1; 132 private static final float DEFAULT_WINDOW_ANIMATION_SCALE = 1.0f; 133 134 private static final int MULTI_TAP_TIME_SLOP_ADJUSTMENT = 50; 135 136 private final IWindowManager mWindowManagerService = IWindowManager.Stub.asInterface( 137 ServiceManager.getService("window")); 138 private final WindowManager mWindowManager; 139 private final DisplayProvider mDisplayProvider; 140 141 private final DetectingStateHandler mDetectingStateHandler = new DetectingStateHandler(); 142 private final GestureDetector mGestureDetector; 143 private final StateViewportDraggingHandler mStateViewportDraggingHandler = 144 new StateViewportDraggingHandler(); 145 146 private final Interpolator mInterpolator = new DecelerateInterpolator(2.5f); 147 148 private final MagnificationController mMagnificationController; 149 private final DisplayContentObserver mDisplayContentObserver; 150 private final ScreenStateObserver mScreenStateObserver; 151 private final Viewport mViewport; 152 153 private final int mTapTimeSlop = ViewConfiguration.getTapTimeout(); 154 private final int mMultiTapTimeSlop = 155 ViewConfiguration.getDoubleTapTimeout() - MULTI_TAP_TIME_SLOP_ADJUSTMENT; 156 private final int mTapDistanceSlop; 157 private final int mMultiTapDistanceSlop; 158 159 private final int mShortAnimationDuration; 160 private final int mLongAnimationDuration; 161 private final float mWindowAnimationScale; 162 163 private final Context mContext; 164 165 private EventStreamTransformation mNext; 166 167 private int mCurrentState; 168 private int mPreviousState; 169 private boolean mTranslationEnabledBeforePan; 170 171 private PointerCoords[] mTempPointerCoords; 172 private PointerProperties[] mTempPointerProperties; 173 174 public ScreenMagnifier(Context context) { 175 mContext = context; 176 mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 177 178 mShortAnimationDuration = context.getResources().getInteger( 179 com.android.internal.R.integer.config_shortAnimTime); 180 mLongAnimationDuration = context.getResources().getInteger( 181 com.android.internal.R.integer.config_longAnimTime); 182 mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 183 mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); 184 mWindowAnimationScale = Settings.System.getFloat(context.getContentResolver(), 185 Settings.System.WINDOW_ANIMATION_SCALE, DEFAULT_WINDOW_ANIMATION_SCALE); 186 187 mMagnificationController = new MagnificationController(mShortAnimationDuration); 188 mDisplayProvider = new DisplayProvider(context, mWindowManager); 189 mViewport = new Viewport(mContext, mWindowManager, mWindowManagerService, 190 mDisplayProvider, mInterpolator, mShortAnimationDuration); 191 mDisplayContentObserver = new DisplayContentObserver(mContext, mViewport, 192 mMagnificationController, mWindowManagerService, mDisplayProvider, 193 mLongAnimationDuration, mWindowAnimationScale); 194 mScreenStateObserver = new ScreenStateObserver(mContext, mViewport, 195 mMagnificationController); 196 197 mGestureDetector = new GestureDetector(context); 198 199 transitionToState(STATE_DETECTING); 200 } 201 202 @Override 203 public void onMotionEvent(MotionEvent event, int policyFlags) { 204 mGestureDetector.onMotionEvent(event); 205 switch (mCurrentState) { 206 case STATE_DELEGATING: { 207 handleMotionEventStateDelegating(event, policyFlags); 208 } break; 209 case STATE_DETECTING: { 210 mDetectingStateHandler.onMotionEvent(event, policyFlags); 211 } break; 212 case STATE_VIEWPORT_DRAGGING: { 213 mStateViewportDraggingHandler.onMotionEvent(event, policyFlags); 214 } break; 215 case STATE_MAGNIFIED_INTERACTION: { 216 // Handled by the gesture detector. Since the detector 217 // needs all touch events to work properly we cannot 218 // call it only for this state. 219 } break; 220 default: { 221 throw new IllegalStateException("Unknown state: " + mCurrentState); 222 } 223 } 224 } 225 226 @Override 227 public void onAccessibilityEvent(AccessibilityEvent event) { 228 if (mNext != null) { 229 mNext.onAccessibilityEvent(event); 230 } 231 } 232 233 @Override 234 public void setNext(EventStreamTransformation next) { 235 mNext = next; 236 } 237 238 @Override 239 public void clear() { 240 mCurrentState = STATE_DETECTING; 241 mDetectingStateHandler.clear(); 242 mStateViewportDraggingHandler.clear(); 243 mGestureDetector.clear(); 244 if (mNext != null) { 245 mNext.clear(); 246 } 247 } 248 249 @Override 250 public void onDestroy() { 251 mMagnificationController.setScaleAndMagnifiedRegionCenter(1.0f, 252 0, 0, true); 253 mViewport.setFrameShown(false, true); 254 mDisplayProvider.destroy(); 255 mDisplayContentObserver.destroy(); 256 mScreenStateObserver.destroy(); 257 } 258 259 private void handleMotionEventStateDelegating(MotionEvent event, int policyFlags) { 260 if (event.getActionMasked() == MotionEvent.ACTION_UP) { 261 if (mDetectingStateHandler.mDelayedEventQueue == null) { 262 transitionToState(STATE_DETECTING); 263 } 264 } 265 if (mNext != null) { 266 // If the event is within the magnified portion of the screen we have 267 // to change its location to be where the user thinks he is poking the 268 // UI which may have been magnified and panned. 269 final float eventX = event.getX(); 270 final float eventY = event.getY(); 271 if (mMagnificationController.isMagnifying() 272 && mViewport.getBounds().contains((int) eventX, (int) eventY)) { 273 final float scale = mMagnificationController.getScale(); 274 final float scaledOffsetX = mMagnificationController.getScaledOffsetX(); 275 final float scaledOffsetY = mMagnificationController.getScaledOffsetY(); 276 final int pointerCount = event.getPointerCount(); 277 PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount); 278 PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount); 279 for (int i = 0; i < pointerCount; i++) { 280 event.getPointerCoords(i, coords[i]); 281 coords[i].x = (coords[i].x - scaledOffsetX) / scale; 282 coords[i].y = (coords[i].y - scaledOffsetY) / scale; 283 event.getPointerProperties(i, properties[i]); 284 } 285 event = MotionEvent.obtain(event.getDownTime(), 286 event.getEventTime(), event.getAction(), pointerCount, properties, 287 coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(), 288 event.getFlags()); 289 } 290 mNext.onMotionEvent(event, policyFlags); 291 } 292 } 293 294 private PointerCoords[] getTempPointerCoordsWithMinSize(int size) { 295 final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0; 296 if (oldSize < size) { 297 PointerCoords[] oldTempPointerCoords = mTempPointerCoords; 298 mTempPointerCoords = new PointerCoords[size]; 299 if (oldTempPointerCoords != null) { 300 System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize); 301 } 302 } 303 for (int i = oldSize; i < size; i++) { 304 mTempPointerCoords[i] = new PointerCoords(); 305 } 306 return mTempPointerCoords; 307 } 308 309 private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) { 310 final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0; 311 if (oldSize < size) { 312 PointerProperties[] oldTempPointerProperties = mTempPointerProperties; 313 mTempPointerProperties = new PointerProperties[size]; 314 if (oldTempPointerProperties != null) { 315 System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize); 316 } 317 } 318 for (int i = oldSize; i < size; i++) { 319 mTempPointerProperties[i] = new PointerProperties(); 320 } 321 return mTempPointerProperties; 322 } 323 324 private void transitionToState(int state) { 325 if (DEBUG_STATE_TRANSITIONS) { 326 switch (state) { 327 case STATE_DELEGATING: { 328 Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING"); 329 } break; 330 case STATE_DETECTING: { 331 Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING"); 332 } break; 333 case STATE_VIEWPORT_DRAGGING: { 334 Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING"); 335 } break; 336 case STATE_MAGNIFIED_INTERACTION: { 337 Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION"); 338 } break; 339 default: { 340 throw new IllegalArgumentException("Unknown state: " + state); 341 } 342 } 343 } 344 mPreviousState = mCurrentState; 345 mCurrentState = state; 346 } 347 348 private final class GestureDetector implements OnScaleGestureListener { 349 private static final float MIN_SCALE = 1.3f; 350 private static final float MAX_SCALE = 5.0f; 351 352 private static final float DETECT_SCALING_THRESHOLD = 0.30f; 353 private static final int DETECT_PANNING_THRESHOLD_DIP = 30; 354 355 private final float mScaledDetectPanningThreshold; 356 357 private final ScaleGestureDetector mScaleGestureDetector; 358 359 private final PointF mPrevFocus = new PointF(Float.NaN, Float.NaN); 360 private final PointF mInitialFocus = new PointF(Float.NaN, Float.NaN); 361 362 private float mCurrScale = Float.NaN; 363 private float mCurrScaleFactor = 1.0f; 364 private float mPrevScaleFactor = 1.0f; 365 private float mCurrPan; 366 private float mPrevPan; 367 368 private float mScaleFocusX = Float.NaN; 369 private float mScaleFocusY = Float.NaN; 370 371 private boolean mScaling; 372 private boolean mPanning; 373 374 public GestureDetector(Context context) { 375 final float density = context.getResources().getDisplayMetrics().density; 376 mScaledDetectPanningThreshold = DETECT_PANNING_THRESHOLD_DIP * density; 377 mScaleGestureDetector = new ScaleGestureDetector(this); 378 } 379 380 public void onMotionEvent(MotionEvent event) { 381 mScaleGestureDetector.onTouchEvent(event); 382 switch (mCurrentState) { 383 case STATE_DETECTING: 384 case STATE_DELEGATING: 385 case STATE_VIEWPORT_DRAGGING: { 386 return; 387 } 388 } 389 if (event.getActionMasked() == MotionEvent.ACTION_UP) { 390 clear(); 391 final float scale = mMagnificationController.getScale(); 392 if (scale != getPersistedScale()) { 393 persistScale(scale); 394 } 395 if (mPreviousState == STATE_VIEWPORT_DRAGGING) { 396 transitionToState(STATE_VIEWPORT_DRAGGING); 397 } else { 398 transitionToState(STATE_DETECTING); 399 } 400 } 401 } 402 403 @Override 404 public boolean onScale(ScaleGestureDetector detector) { 405 switch (mCurrentState) { 406 case STATE_DETECTING: 407 case STATE_DELEGATING: 408 case STATE_VIEWPORT_DRAGGING: { 409 return true; 410 } 411 case STATE_MAGNIFIED_INTERACTION: { 412 mCurrScaleFactor = mScaleGestureDetector.getScaleFactor(); 413 final float scaleDelta = Math.abs(1.0f - mCurrScaleFactor * mPrevScaleFactor); 414 if (DEBUG_GESTURE_DETECTOR) { 415 Slog.i(LOG_TAG, "scaleDelta: " + scaleDelta); 416 } 417 if (!mScaling && scaleDelta > DETECT_SCALING_THRESHOLD) { 418 mScaling = true; 419 clearContextualState(); 420 return true; 421 } 422 if (mScaling) { 423 performScale(detector); 424 } 425 mCurrPan = (float) MathUtils.dist( 426 mScaleGestureDetector.getFocusX(), 427 mScaleGestureDetector.getFocusY(), 428 mInitialFocus.x, mInitialFocus.y); 429 final float panDelta = mCurrPan + mPrevPan; 430 if (DEBUG_GESTURE_DETECTOR) { 431 Slog.i(LOG_TAG, "panDelta: " + panDelta); 432 } 433 if (!mPanning && panDelta > mScaledDetectPanningThreshold) { 434 mPanning = true; 435 clearContextualState(); 436 return true; 437 } 438 if (mPanning) { 439 performPan(detector); 440 } 441 } break; 442 } 443 return false; 444 } 445 446 @Override 447 public boolean onScaleBegin(ScaleGestureDetector detector) { 448 mPrevScaleFactor *= mCurrScaleFactor; 449 mCurrScale = Float.NaN; 450 mPrevPan += mCurrPan; 451 mPrevFocus.x = mInitialFocus.x = detector.getFocusX(); 452 mPrevFocus.y = mInitialFocus.y = detector.getFocusY(); 453 return true; 454 } 455 456 @Override 457 public void onScaleEnd(ScaleGestureDetector detector) { 458 clearContextualState(); 459 } 460 461 public void clear() { 462 clearContextualState(); 463 mScaling = false; 464 mPanning = false; 465 } 466 467 private void clearContextualState() { 468 mCurrScaleFactor = 1.0f; 469 mPrevScaleFactor = 1.0f; 470 mPrevPan = 0; 471 mCurrPan = 0; 472 mInitialFocus.set(Float.NaN, Float.NaN); 473 mPrevFocus.set(Float.NaN, Float.NaN); 474 mCurrScale = Float.NaN; 475 mScaleFocusX = Float.NaN; 476 mScaleFocusY = Float.NaN; 477 } 478 479 private void performPan(ScaleGestureDetector detector) { 480 if (Float.compare(mPrevFocus.x, Float.NaN) == 0 481 && Float.compare(mPrevFocus.y, Float.NaN) == 0) { 482 mPrevFocus.set(detector.getFocusX(), detector.getFocusY()); 483 return; 484 } 485 final float scale = mMagnificationController.getScale(); 486 final float scrollX = (detector.getFocusX() - mPrevFocus.x) / scale; 487 final float scrollY = (detector.getFocusY() - mPrevFocus.y) / scale; 488 final float centerX = mMagnificationController.getMagnifiedRegionCenterX() 489 - scrollX; 490 final float centerY = mMagnificationController.getMagnifiedRegionCenterY() 491 - scrollY; 492 if (DEBUG_PANNING) { 493 Slog.i(LOG_TAG, "Panned content by scrollX: " + scrollX 494 + " scrollY: " + scrollY); 495 } 496 mMagnificationController.setMagnifiedRegionCenter(centerX, centerY, false); 497 mPrevFocus.set(detector.getFocusX(), detector.getFocusY()); 498 } 499 500 private void performScale(ScaleGestureDetector detector) { 501 if (Float.compare(mCurrScale, Float.NaN) == 0) { 502 mCurrScale = mMagnificationController.getScale(); 503 return; 504 } 505 final float totalScaleFactor = mPrevScaleFactor * detector.getScaleFactor(); 506 final float newScale = mCurrScale * totalScaleFactor; 507 final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE), 508 MAX_SCALE); 509 if (DEBUG_SCALING) { 510 Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale); 511 } 512 if (Float.compare(mScaleFocusX, Float.NaN) == 0 513 && Float.compare(mScaleFocusY, Float.NaN) == 0) { 514 mScaleFocusX = detector.getFocusX(); 515 mScaleFocusY = detector.getFocusY(); 516 } 517 mMagnificationController.setScale(normalizedNewScale, mScaleFocusX, 518 mScaleFocusY, false); 519 } 520 } 521 522 private final class StateViewportDraggingHandler { 523 private boolean mLastMoveOutsideMagnifiedRegion; 524 525 private void onMotionEvent(MotionEvent event, int policyFlags) { 526 final int action = event.getActionMasked(); 527 switch (action) { 528 case MotionEvent.ACTION_DOWN: { 529 throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN"); 530 } 531 case MotionEvent.ACTION_POINTER_DOWN: { 532 clear(); 533 transitionToState(STATE_MAGNIFIED_INTERACTION); 534 } break; 535 case MotionEvent.ACTION_MOVE: { 536 if (event.getPointerCount() != 1) { 537 throw new IllegalStateException("Should have one pointer down."); 538 } 539 final float eventX = event.getX(); 540 final float eventY = event.getY(); 541 if (mViewport.getBounds().contains((int) eventX, (int) eventY)) { 542 if (mLastMoveOutsideMagnifiedRegion) { 543 mLastMoveOutsideMagnifiedRegion = false; 544 mMagnificationController.setMagnifiedRegionCenter(eventX, 545 eventY, true); 546 } else { 547 mMagnificationController.setMagnifiedRegionCenter(eventX, 548 eventY, false); 549 } 550 } else { 551 mLastMoveOutsideMagnifiedRegion = true; 552 } 553 } break; 554 case MotionEvent.ACTION_UP: { 555 if (!mTranslationEnabledBeforePan) { 556 mMagnificationController.reset(true); 557 mViewport.setFrameShown(false, true); 558 } 559 clear(); 560 transitionToState(STATE_DETECTING); 561 } break; 562 case MotionEvent.ACTION_POINTER_UP: { 563 throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP"); 564 } 565 } 566 } 567 568 public void clear() { 569 mLastMoveOutsideMagnifiedRegion = false; 570 } 571 } 572 573 private final class DetectingStateHandler { 574 575 private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1; 576 577 private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2; 578 579 private static final int ACTION_TAP_COUNT = 3; 580 581 private MotionEventInfo mDelayedEventQueue; 582 583 private MotionEvent mLastDownEvent; 584 private MotionEvent mLastTapUpEvent; 585 private int mTapCount; 586 587 private final Handler mHandler = new Handler() { 588 @Override 589 public void handleMessage(Message message) { 590 final int type = message.what; 591 switch (type) { 592 case MESSAGE_ON_ACTION_TAP_AND_HOLD: { 593 MotionEvent event = (MotionEvent) message.obj; 594 final int policyFlags = message.arg1; 595 onActionTapAndHold(event, policyFlags); 596 } break; 597 case MESSAGE_TRANSITION_TO_DELEGATING_STATE: { 598 transitionToState(STATE_DELEGATING); 599 sendDelayedMotionEvents(); 600 clear(); 601 } break; 602 default: { 603 throw new IllegalArgumentException("Unknown message type: " + type); 604 } 605 } 606 } 607 }; 608 609 public void onMotionEvent(MotionEvent event, int policyFlags) { 610 cacheDelayedMotionEvent(event, policyFlags); 611 final int action = event.getActionMasked(); 612 switch (action) { 613 case MotionEvent.ACTION_DOWN: { 614 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 615 if (!mViewport.getBounds().contains((int) event.getX(), 616 (int) event.getY())) { 617 transitionToDelegatingStateAndClear(); 618 return; 619 } 620 if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null 621 && GestureUtils.isMultiTap(mLastDownEvent, event, 622 mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { 623 Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD, 624 policyFlags, 0, event); 625 mHandler.sendMessageDelayed(message, 626 ViewConfiguration.getLongPressTimeout()); 627 } else if (mTapCount < ACTION_TAP_COUNT) { 628 Message message = mHandler.obtainMessage( 629 MESSAGE_TRANSITION_TO_DELEGATING_STATE); 630 mHandler.sendMessageDelayed(message, mMultiTapTimeSlop); 631 } 632 clearLastDownEvent(); 633 mLastDownEvent = MotionEvent.obtain(event); 634 } break; 635 case MotionEvent.ACTION_POINTER_DOWN: { 636 if (mMagnificationController.isMagnifying()) { 637 transitionToState(STATE_MAGNIFIED_INTERACTION); 638 clear(); 639 } else { 640 transitionToDelegatingStateAndClear(); 641 } 642 } break; 643 case MotionEvent.ACTION_MOVE: { 644 if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) { 645 final double distance = GestureUtils.computeDistance(mLastDownEvent, 646 event, 0); 647 if (Math.abs(distance) > mTapDistanceSlop) { 648 transitionToDelegatingStateAndClear(); 649 } 650 } 651 } break; 652 case MotionEvent.ACTION_UP: { 653 if (mLastDownEvent == null) { 654 return; 655 } 656 mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); 657 if (!mViewport.getBounds().contains((int) event.getX(), (int) event.getY())) { 658 transitionToDelegatingStateAndClear(); 659 return; 660 } 661 if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop, 662 mTapDistanceSlop, 0)) { 663 transitionToDelegatingStateAndClear(); 664 return; 665 } 666 if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent, 667 event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { 668 transitionToDelegatingStateAndClear(); 669 return; 670 } 671 mTapCount++; 672 if (DEBUG_DETECTING) { 673 Slog.i(LOG_TAG, "Tap count:" + mTapCount); 674 } 675 if (mTapCount == ACTION_TAP_COUNT) { 676 clear(); 677 onActionTap(event, policyFlags); 678 return; 679 } 680 clearLastTapUpEvent(); 681 mLastTapUpEvent = MotionEvent.obtain(event); 682 } break; 683 case MotionEvent.ACTION_POINTER_UP: { 684 /* do nothing */ 685 } break; 686 } 687 } 688 689 public void clear() { 690 mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); 691 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 692 clearTapDetectionState(); 693 clearDelayedMotionEvents(); 694 } 695 696 private void clearTapDetectionState() { 697 mTapCount = 0; 698 clearLastTapUpEvent(); 699 clearLastDownEvent(); 700 } 701 702 private void clearLastTapUpEvent() { 703 if (mLastTapUpEvent != null) { 704 mLastTapUpEvent.recycle(); 705 mLastTapUpEvent = null; 706 } 707 } 708 709 private void clearLastDownEvent() { 710 if (mLastDownEvent != null) { 711 mLastDownEvent.recycle(); 712 mLastDownEvent = null; 713 } 714 } 715 716 private void cacheDelayedMotionEvent(MotionEvent event, int policyFlags) { 717 MotionEventInfo info = MotionEventInfo.obtain(event, policyFlags); 718 if (mDelayedEventQueue == null) { 719 mDelayedEventQueue = info; 720 } else { 721 MotionEventInfo tail = mDelayedEventQueue; 722 while (tail.mNext != null) { 723 tail = tail.mNext; 724 } 725 tail.mNext = info; 726 } 727 } 728 729 private void sendDelayedMotionEvents() { 730 while (mDelayedEventQueue != null) { 731 MotionEventInfo info = mDelayedEventQueue; 732 mDelayedEventQueue = info.mNext; 733 ScreenMagnifier.this.onMotionEvent(info.mEvent, info.mPolicyFlags); 734 info.recycle(); 735 } 736 } 737 738 private void clearDelayedMotionEvents() { 739 while (mDelayedEventQueue != null) { 740 MotionEventInfo info = mDelayedEventQueue; 741 mDelayedEventQueue = info.mNext; 742 info.recycle(); 743 } 744 } 745 746 private void transitionToDelegatingStateAndClear() { 747 transitionToState(STATE_DELEGATING); 748 sendDelayedMotionEvents(); 749 clear(); 750 } 751 752 private void onActionTap(MotionEvent up, int policyFlags) { 753 if (DEBUG_DETECTING) { 754 Slog.i(LOG_TAG, "onActionTap()"); 755 } 756 if (!mMagnificationController.isMagnifying()) { 757 mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(), 758 up.getX(), up.getY(), true); 759 mViewport.setFrameShown(true, true); 760 } else { 761 mMagnificationController.reset(true); 762 mViewport.setFrameShown(false, true); 763 } 764 } 765 766 private void onActionTapAndHold(MotionEvent down, int policyFlags) { 767 if (DEBUG_DETECTING) { 768 Slog.i(LOG_TAG, "onActionTapAndHold()"); 769 } 770 clear(); 771 mTranslationEnabledBeforePan = mMagnificationController.isMagnifying(); 772 mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(), 773 down.getX(), down.getY(), true); 774 mViewport.setFrameShown(true, true); 775 transitionToState(STATE_VIEWPORT_DRAGGING); 776 } 777 } 778 779 private void persistScale(final float scale) { 780 new AsyncTask<Void, Void, Void>() { 781 @Override 782 protected Void doInBackground(Void... params) { 783 Settings.Secure.putFloat(mContext.getContentResolver(), 784 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale); 785 return null; 786 } 787 }.execute(); 788 } 789 790 private float getPersistedScale() { 791 return Settings.Secure.getFloat(mContext.getContentResolver(), 792 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 793 DEFAULT_MAGNIFICATION_SCALE); 794 } 795 796 private static boolean isScreenMagnificationAutoUpdateEnabled(Context context) { 797 return (Settings.Secure.getInt(context.getContentResolver(), 798 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE, 799 DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1); 800 } 801 802 private static final class MotionEventInfo { 803 804 private static final int MAX_POOL_SIZE = 10; 805 806 private static final Object sLock = new Object(); 807 private static MotionEventInfo sPool; 808 private static int sPoolSize; 809 810 private MotionEventInfo mNext; 811 private boolean mInPool; 812 813 public MotionEvent mEvent; 814 public int mPolicyFlags; 815 816 public static MotionEventInfo obtain(MotionEvent event, int policyFlags) { 817 synchronized (sLock) { 818 MotionEventInfo info; 819 if (sPoolSize > 0) { 820 sPoolSize--; 821 info = sPool; 822 sPool = info.mNext; 823 info.mNext = null; 824 info.mInPool = false; 825 } else { 826 info = new MotionEventInfo(); 827 } 828 info.initialize(event, policyFlags); 829 return info; 830 } 831 } 832 833 private void initialize(MotionEvent event, int policyFlags) { 834 mEvent = MotionEvent.obtain(event); 835 mPolicyFlags = policyFlags; 836 } 837 838 public void recycle() { 839 synchronized (sLock) { 840 if (mInPool) { 841 throw new IllegalStateException("Already recycled."); 842 } 843 clear(); 844 if (sPoolSize < MAX_POOL_SIZE) { 845 sPoolSize++; 846 mNext = sPool; 847 sPool = this; 848 mInPool = true; 849 } 850 } 851 } 852 853 private void clear() { 854 mEvent.recycle(); 855 mEvent = null; 856 mPolicyFlags = 0; 857 } 858 } 859 860 private static final class ScreenStateObserver extends BroadcastReceiver { 861 862 private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1; 863 864 private final Handler mHandler = new Handler() { 865 @Override 866 public void handleMessage(Message message) { 867 switch (message.what) { 868 case MESSAGE_ON_SCREEN_STATE_CHANGE: { 869 String action = (String) message.obj; 870 handleOnScreenStateChange(action); 871 } break; 872 } 873 } 874 }; 875 876 private final Context mContext; 877 private final Viewport mViewport; 878 private final MagnificationController mMagnificationController; 879 880 public ScreenStateObserver(Context context, Viewport viewport, 881 MagnificationController magnificationController) { 882 mContext = context; 883 mViewport = viewport; 884 mMagnificationController = magnificationController; 885 mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF)); 886 } 887 888 public void destroy() { 889 mContext.unregisterReceiver(this); 890 } 891 892 @Override 893 public void onReceive(Context context, Intent intent) { 894 mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE, 895 intent.getAction()).sendToTarget(); 896 } 897 898 private void handleOnScreenStateChange(String action) { 899 if (action.equals(Intent.ACTION_SCREEN_OFF) 900 && mMagnificationController.isMagnifying() 901 && isScreenMagnificationAutoUpdateEnabled(mContext)) { 902 mMagnificationController.reset(false); 903 mViewport.setFrameShown(false, false); 904 } 905 } 906 } 907 908 private static final class DisplayContentObserver { 909 910 private static final int MESSAGE_SHOW_VIEWPORT_FRAME = 1; 911 private static final int MESSAGE_RECOMPUTE_VIEWPORT_BOUNDS = 2; 912 private static final int MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED = 3; 913 private static final int MESSAGE_ON_WINDOW_TRANSITION = 4; 914 private static final int MESSAGE_ON_ROTATION_CHANGED = 5; 915 916 private final Handler mHandler = new MyHandler(); 917 918 private final Rect mTempRect = new Rect(); 919 920 private final IDisplayContentChangeListener mDisplayContentChangeListener; 921 922 private final Context mContext; 923 private final Viewport mViewport; 924 private final MagnificationController mMagnificationController; 925 private final IWindowManager mWindowManagerService; 926 private final DisplayProvider mDisplayProvider; 927 private final long mLongAnimationDuration; 928 private final float mWindowAnimationScale; 929 930 public DisplayContentObserver(Context context, Viewport viewport, 931 MagnificationController magnificationController, 932 IWindowManager windowManagerService, DisplayProvider displayProvider, 933 long longAnimationDuration, float windowAnimationScale) { 934 mContext = context; 935 mViewport = viewport; 936 mMagnificationController = magnificationController; 937 mWindowManagerService = windowManagerService; 938 mDisplayProvider = displayProvider; 939 mLongAnimationDuration = longAnimationDuration; 940 mWindowAnimationScale = windowAnimationScale; 941 942 mDisplayContentChangeListener = new IDisplayContentChangeListener.Stub() { 943 @Override 944 public void onWindowTransition(int displayId, int transition, WindowInfo info) { 945 mHandler.obtainMessage(MESSAGE_ON_WINDOW_TRANSITION, transition, 0, 946 WindowInfo.obtain(info)).sendToTarget(); 947 } 948 949 @Override 950 public void onRectangleOnScreenRequested(int dsiplayId, Rect rectangle, 951 boolean immediate) { 952 SomeArgs args = SomeArgs.obtain(); 953 args.argi1 = rectangle.left; 954 args.argi2 = rectangle.top; 955 args.argi3 = rectangle.right; 956 args.argi4 = rectangle.bottom; 957 mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, 0, 958 immediate ? 1 : 0, args).sendToTarget(); 959 } 960 961 @Override 962 public void onRotationChanged(int rotation) throws RemoteException { 963 mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0) 964 .sendToTarget(); 965 } 966 }; 967 968 try { 969 mWindowManagerService.addDisplayContentChangeListener( 970 mDisplayProvider.getDisplay().getDisplayId(), 971 mDisplayContentChangeListener); 972 } catch (RemoteException re) { 973 /* ignore */ 974 } 975 } 976 977 public void destroy() { 978 try { 979 mWindowManagerService.removeDisplayContentChangeListener( 980 mDisplayProvider.getDisplay().getDisplayId(), 981 mDisplayContentChangeListener); 982 } catch (RemoteException re) { 983 /* ignore*/ 984 } 985 } 986 987 private void handleOnRotationChanged(int rotation) { 988 if (DEBUG_ROTATION) { 989 Slog.i(LOG_TAG, "Rotation: " + rotationToString(rotation)); 990 } 991 resetMagnificationIfNeeded(); 992 mViewport.setFrameShown(false, false); 993 mViewport.rotationChanged(); 994 mViewport.recomputeBounds(false); 995 if (mMagnificationController.isMagnifying()) { 996 final long delay = (long) (2 * mLongAnimationDuration * mWindowAnimationScale); 997 Message message = mHandler.obtainMessage(MESSAGE_SHOW_VIEWPORT_FRAME); 998 mHandler.sendMessageDelayed(message, delay); 999 } 1000 } 1001 1002 private void handleOnWindowTransition(int transition, WindowInfo info) { 1003 if (DEBUG_WINDOW_TRANSITIONS) { 1004 Slog.i(LOG_TAG, "Window transitioning: " 1005 + windowTransitionToString(transition)); 1006 } 1007 try { 1008 final boolean magnifying = mMagnificationController.isMagnifying(); 1009 if (magnifying) { 1010 switch (transition) { 1011 case WindowManagerPolicy.TRANSIT_ACTIVITY_OPEN: 1012 case WindowManagerPolicy.TRANSIT_TASK_OPEN: 1013 case WindowManagerPolicy.TRANSIT_TASK_TO_FRONT: 1014 case WindowManagerPolicy.TRANSIT_WALLPAPER_OPEN: 1015 case WindowManagerPolicy.TRANSIT_WALLPAPER_CLOSE: 1016 case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_OPEN: { 1017 resetMagnificationIfNeeded(); 1018 } 1019 } 1020 } 1021 if (info.type == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR 1022 || info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD 1023 || info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG) { 1024 switch (transition) { 1025 case WindowManagerPolicy.TRANSIT_ENTER: 1026 case WindowManagerPolicy.TRANSIT_SHOW: 1027 case WindowManagerPolicy.TRANSIT_EXIT: 1028 case WindowManagerPolicy.TRANSIT_HIDE: { 1029 mViewport.recomputeBounds(mMagnificationController.isMagnifying()); 1030 } break; 1031 } 1032 } else { 1033 switch (transition) { 1034 case WindowManagerPolicy.TRANSIT_ENTER: 1035 case WindowManagerPolicy.TRANSIT_SHOW: { 1036 if (!magnifying || !isScreenMagnificationAutoUpdateEnabled(mContext)) { 1037 break; 1038 } 1039 final int type = info.type; 1040 switch (type) { 1041 // TODO: Are these all the windows we want to make 1042 // visible when they appear on the screen? 1043 // Do we need to take some of them out? 1044 case WindowManager.LayoutParams.TYPE_APPLICATION_PANEL: 1045 case WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA: 1046 case WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL: 1047 case WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG: 1048 case WindowManager.LayoutParams.TYPE_SEARCH_BAR: 1049 case WindowManager.LayoutParams.TYPE_PHONE: 1050 case WindowManager.LayoutParams.TYPE_SYSTEM_ALERT: 1051 case WindowManager.LayoutParams.TYPE_TOAST: 1052 case WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY: 1053 case WindowManager.LayoutParams.TYPE_PRIORITY_PHONE: 1054 case WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG: 1055 case WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG: 1056 case WindowManager.LayoutParams.TYPE_SYSTEM_ERROR: 1057 case WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY: 1058 case WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL: { 1059 Rect magnifiedRegionBounds = mMagnificationController 1060 .getMagnifiedRegionBounds(); 1061 Rect touchableRegion = info.touchableRegion; 1062 if (!magnifiedRegionBounds.intersect(touchableRegion)) { 1063 ensureRectangleInMagnifiedRegionBounds( 1064 magnifiedRegionBounds, touchableRegion); 1065 } 1066 } break; 1067 } break; 1068 } 1069 } 1070 } 1071 } finally { 1072 if (info != null) { 1073 info.recycle(); 1074 } 1075 } 1076 } 1077 1078 private void handleOnRectangleOnScreenRequested(Rect rectangle, boolean immediate) { 1079 if (!mMagnificationController.isMagnifying()) { 1080 return; 1081 } 1082 Rect magnifiedRegionBounds = mMagnificationController.getMagnifiedRegionBounds(); 1083 if (magnifiedRegionBounds.contains(rectangle)) { 1084 return; 1085 } 1086 ensureRectangleInMagnifiedRegionBounds(magnifiedRegionBounds, rectangle); 1087 } 1088 1089 private void ensureRectangleInMagnifiedRegionBounds(Rect magnifiedRegionBounds, 1090 Rect rectangle) { 1091 if (!Rect.intersects(rectangle, mViewport.getBounds())) { 1092 return; 1093 } 1094 final float scrollX; 1095 final float scrollY; 1096 if (rectangle.width() > magnifiedRegionBounds.width()) { 1097 scrollX = rectangle.left - magnifiedRegionBounds.left; 1098 } else if (rectangle.left < magnifiedRegionBounds.left) { 1099 scrollX = rectangle.left - magnifiedRegionBounds.left; 1100 } else if (rectangle.right > magnifiedRegionBounds.right) { 1101 scrollX = rectangle.right - magnifiedRegionBounds.right; 1102 } else { 1103 scrollX = 0; 1104 } 1105 if (rectangle.height() > magnifiedRegionBounds.height()) { 1106 scrollY = rectangle.top - magnifiedRegionBounds.top; 1107 } else if (rectangle.top < magnifiedRegionBounds.top) { 1108 scrollY = rectangle.top - magnifiedRegionBounds.top; 1109 } else if (rectangle.bottom > magnifiedRegionBounds.bottom) { 1110 scrollY = rectangle.bottom - magnifiedRegionBounds.bottom; 1111 } else { 1112 scrollY = 0; 1113 } 1114 final float viewportCenterX = mMagnificationController.getMagnifiedRegionCenterX() 1115 + scrollX; 1116 final float viewportCenterY = mMagnificationController.getMagnifiedRegionCenterY() 1117 + scrollY; 1118 mMagnificationController.setMagnifiedRegionCenter(viewportCenterX, viewportCenterY, 1119 true); 1120 } 1121 1122 private void resetMagnificationIfNeeded() { 1123 if (mMagnificationController.isMagnifying() 1124 && isScreenMagnificationAutoUpdateEnabled(mContext)) { 1125 mMagnificationController.reset(true); 1126 mViewport.setFrameShown(false, true); 1127 } 1128 } 1129 1130 private String windowTransitionToString(int transition) { 1131 switch (transition) { 1132 case WindowManagerPolicy.TRANSIT_UNSET: { 1133 return "TRANSIT_UNSET"; 1134 } 1135 case WindowManagerPolicy.TRANSIT_NONE: { 1136 return "TRANSIT_NONE"; 1137 } 1138 case WindowManagerPolicy.TRANSIT_ENTER: { 1139 return "TRANSIT_ENTER"; 1140 } 1141 case WindowManagerPolicy.TRANSIT_EXIT: { 1142 return "TRANSIT_EXIT"; 1143 } 1144 case WindowManagerPolicy.TRANSIT_SHOW: { 1145 return "TRANSIT_SHOW"; 1146 } 1147 case WindowManagerPolicy.TRANSIT_EXIT_MASK: { 1148 return "TRANSIT_EXIT_MASK"; 1149 } 1150 case WindowManagerPolicy.TRANSIT_PREVIEW_DONE: { 1151 return "TRANSIT_PREVIEW_DONE"; 1152 } 1153 case WindowManagerPolicy.TRANSIT_ACTIVITY_OPEN: { 1154 return "TRANSIT_ACTIVITY_OPEN"; 1155 } 1156 case WindowManagerPolicy.TRANSIT_ACTIVITY_CLOSE: { 1157 return "TRANSIT_ACTIVITY_CLOSE"; 1158 } 1159 case WindowManagerPolicy.TRANSIT_TASK_OPEN: { 1160 return "TRANSIT_TASK_OPEN"; 1161 } 1162 case WindowManagerPolicy.TRANSIT_TASK_CLOSE: { 1163 return "TRANSIT_TASK_CLOSE"; 1164 } 1165 case WindowManagerPolicy.TRANSIT_TASK_TO_FRONT: { 1166 return "TRANSIT_TASK_TO_FRONT"; 1167 } 1168 case WindowManagerPolicy.TRANSIT_TASK_TO_BACK: { 1169 return "TRANSIT_TASK_TO_BACK"; 1170 } 1171 case WindowManagerPolicy.TRANSIT_WALLPAPER_CLOSE: { 1172 return "TRANSIT_WALLPAPER_CLOSE"; 1173 } 1174 case WindowManagerPolicy.TRANSIT_WALLPAPER_OPEN: { 1175 return "TRANSIT_WALLPAPER_OPEN"; 1176 } 1177 case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_OPEN: { 1178 return "TRANSIT_WALLPAPER_INTRA_OPEN"; 1179 } 1180 case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_CLOSE: { 1181 return "TRANSIT_WALLPAPER_INTRA_CLOSE"; 1182 } 1183 default: { 1184 return "<UNKNOWN>"; 1185 } 1186 } 1187 } 1188 1189 private String rotationToString(int rotation) { 1190 switch (rotation) { 1191 case Surface.ROTATION_0: { 1192 return "ROTATION_0"; 1193 } 1194 case Surface.ROTATION_90: { 1195 return "ROATATION_90"; 1196 } 1197 case Surface.ROTATION_180: { 1198 return "ROATATION_180"; 1199 } 1200 case Surface.ROTATION_270: { 1201 return "ROATATION_270"; 1202 } 1203 default: { 1204 throw new IllegalArgumentException("Invalid rotation: " 1205 + rotation); 1206 } 1207 } 1208 } 1209 1210 private final class MyHandler extends Handler { 1211 @Override 1212 public void handleMessage(Message message) { 1213 final int action = message.what; 1214 switch (action) { 1215 case MESSAGE_SHOW_VIEWPORT_FRAME: { 1216 mViewport.setFrameShown(true, true); 1217 } break; 1218 case MESSAGE_RECOMPUTE_VIEWPORT_BOUNDS: { 1219 final boolean animate = message.arg1 == 1; 1220 mViewport.recomputeBounds(animate); 1221 } break; 1222 case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: { 1223 SomeArgs args = (SomeArgs) message.obj; 1224 try { 1225 mTempRect.set(args.argi1, args.argi2, args.argi3, args.argi4); 1226 final boolean immediate = (message.arg1 == 1); 1227 handleOnRectangleOnScreenRequested(mTempRect, immediate); 1228 } finally { 1229 args.recycle(); 1230 } 1231 } break; 1232 case MESSAGE_ON_WINDOW_TRANSITION: { 1233 final int transition = message.arg1; 1234 WindowInfo info = (WindowInfo) message.obj; 1235 handleOnWindowTransition(transition, info); 1236 } break; 1237 case MESSAGE_ON_ROTATION_CHANGED: { 1238 final int rotation = message.arg1; 1239 handleOnRotationChanged(rotation); 1240 } break; 1241 default: { 1242 throw new IllegalArgumentException("Unknown message: " + action); 1243 } 1244 } 1245 } 1246 } 1247 } 1248 1249 private final class MagnificationController { 1250 1251 private static final String PROPERTY_NAME_ACCESSIBILITY_TRANSFORMATION = 1252 "accessibilityTransformation"; 1253 1254 private final MagnificationSpec mSentMagnificationSpec = new MagnificationSpec(); 1255 1256 private final MagnificationSpec mCurrentMagnificationSpec = new MagnificationSpec(); 1257 1258 private final Rect mTempRect = new Rect(); 1259 1260 private final ValueAnimator mTransformationAnimator; 1261 1262 public MagnificationController(int animationDuration) { 1263 Property<MagnificationController, MagnificationSpec> property = 1264 Property.of(MagnificationController.class, MagnificationSpec.class, 1265 PROPERTY_NAME_ACCESSIBILITY_TRANSFORMATION); 1266 TypeEvaluator<MagnificationSpec> evaluator = new TypeEvaluator<MagnificationSpec>() { 1267 private final MagnificationSpec mTempTransformationSpec = new MagnificationSpec(); 1268 @Override 1269 public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec, 1270 MagnificationSpec toSpec) { 1271 MagnificationSpec result = mTempTransformationSpec; 1272 result.mScale = fromSpec.mScale 1273 + (toSpec.mScale - fromSpec.mScale) * fraction; 1274 result.mMagnifiedRegionCenterX = fromSpec.mMagnifiedRegionCenterX 1275 + (toSpec.mMagnifiedRegionCenterX - fromSpec.mMagnifiedRegionCenterX) 1276 * fraction; 1277 result.mMagnifiedRegionCenterY = fromSpec.mMagnifiedRegionCenterY 1278 + (toSpec.mMagnifiedRegionCenterY - fromSpec.mMagnifiedRegionCenterY) 1279 * fraction; 1280 result.mScaledOffsetX = fromSpec.mScaledOffsetX 1281 + (toSpec.mScaledOffsetX - fromSpec.mScaledOffsetX) 1282 * fraction; 1283 result.mScaledOffsetY = fromSpec.mScaledOffsetY 1284 + (toSpec.mScaledOffsetY - fromSpec.mScaledOffsetY) 1285 * fraction; 1286 return result; 1287 } 1288 }; 1289 mTransformationAnimator = ObjectAnimator.ofObject(this, property, 1290 evaluator, mSentMagnificationSpec, mCurrentMagnificationSpec); 1291 mTransformationAnimator.setDuration((long) (animationDuration)); 1292 mTransformationAnimator.setInterpolator(mInterpolator); 1293 } 1294 1295 public boolean isMagnifying() { 1296 return mCurrentMagnificationSpec.mScale > 1.0f; 1297 } 1298 1299 public void reset(boolean animate) { 1300 if (mTransformationAnimator.isRunning()) { 1301 mTransformationAnimator.cancel(); 1302 } 1303 mCurrentMagnificationSpec.reset(); 1304 if (animate) { 1305 animateAccessibilityTranformation(mSentMagnificationSpec, 1306 mCurrentMagnificationSpec); 1307 } else { 1308 setAccessibilityTransformation(mCurrentMagnificationSpec); 1309 } 1310 } 1311 1312 public Rect getMagnifiedRegionBounds() { 1313 mTempRect.set(mViewport.getBounds()); 1314 mTempRect.offset((int) -mCurrentMagnificationSpec.mScaledOffsetX, 1315 (int) -mCurrentMagnificationSpec.mScaledOffsetY); 1316 mTempRect.scale(1.0f / mCurrentMagnificationSpec.mScale); 1317 return mTempRect; 1318 } 1319 1320 public float getScale() { 1321 return mCurrentMagnificationSpec.mScale; 1322 } 1323 1324 public float getMagnifiedRegionCenterX() { 1325 return mCurrentMagnificationSpec.mMagnifiedRegionCenterX; 1326 } 1327 1328 public float getMagnifiedRegionCenterY() { 1329 return mCurrentMagnificationSpec.mMagnifiedRegionCenterY; 1330 } 1331 1332 public float getScaledOffsetX() { 1333 return mCurrentMagnificationSpec.mScaledOffsetX; 1334 } 1335 1336 public float getScaledOffsetY() { 1337 return mCurrentMagnificationSpec.mScaledOffsetY; 1338 } 1339 1340 public void setScale(float scale, float pivotX, float pivotY, boolean animate) { 1341 MagnificationSpec spec = mCurrentMagnificationSpec; 1342 final float oldScale = spec.mScale; 1343 final float oldCenterX = spec.mMagnifiedRegionCenterX; 1344 final float oldCenterY = spec.mMagnifiedRegionCenterY; 1345 final float normPivotX = (-spec.mScaledOffsetX + pivotX) / oldScale; 1346 final float normPivotY = (-spec.mScaledOffsetY + pivotY) / oldScale; 1347 final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale); 1348 final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale); 1349 final float centerX = normPivotX + offsetX; 1350 final float centerY = normPivotY + offsetY; 1351 setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate); 1352 } 1353 1354 public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) { 1355 setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.mScale, centerX, centerY, 1356 animate); 1357 } 1358 1359 public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY, 1360 boolean animate) { 1361 if (Float.compare(mCurrentMagnificationSpec.mScale, scale) == 0 1362 && Float.compare(mCurrentMagnificationSpec.mMagnifiedRegionCenterX, 1363 centerX) == 0 1364 && Float.compare(mCurrentMagnificationSpec.mMagnifiedRegionCenterY, 1365 centerY) == 0) { 1366 return; 1367 } 1368 if (mTransformationAnimator.isRunning()) { 1369 mTransformationAnimator.cancel(); 1370 } 1371 if (DEBUG_MAGNIFICATION_CONTROLLER) { 1372 Slog.i(LOG_TAG, "scale: " + scale + " centerX: " + centerX 1373 + " centerY: " + centerY); 1374 } 1375 mCurrentMagnificationSpec.initialize(scale, centerX, centerY); 1376 if (animate) { 1377 animateAccessibilityTranformation(mSentMagnificationSpec, 1378 mCurrentMagnificationSpec); 1379 } else { 1380 setAccessibilityTransformation(mCurrentMagnificationSpec); 1381 } 1382 } 1383 1384 private void animateAccessibilityTranformation(MagnificationSpec fromSpec, 1385 MagnificationSpec toSpec) { 1386 mTransformationAnimator.setObjectValues(fromSpec, toSpec); 1387 mTransformationAnimator.start(); 1388 } 1389 1390 @SuppressWarnings("unused") 1391 // Called from an animator. 1392 public MagnificationSpec getAccessibilityTransformation() { 1393 return mSentMagnificationSpec; 1394 } 1395 1396 public void setAccessibilityTransformation(MagnificationSpec transformation) { 1397 if (DEBUG_TRANSFORMATION) { 1398 Slog.i(LOG_TAG, "Transformation scale: " + transformation.mScale 1399 + " offsetX: " + transformation.mScaledOffsetX 1400 + " offsetY: " + transformation.mScaledOffsetY); 1401 } 1402 try { 1403 mSentMagnificationSpec.updateFrom(transformation); 1404 mWindowManagerService.magnifyDisplay(mDisplayProvider.getDisplay().getDisplayId(), 1405 transformation.mScale, transformation.mScaledOffsetX, 1406 transformation.mScaledOffsetY); 1407 } catch (RemoteException re) { 1408 /* ignore */ 1409 } 1410 } 1411 1412 private class MagnificationSpec { 1413 1414 private static final float DEFAULT_SCALE = 1.0f; 1415 1416 public float mScale = DEFAULT_SCALE; 1417 1418 public float mMagnifiedRegionCenterX; 1419 1420 public float mMagnifiedRegionCenterY; 1421 1422 public float mScaledOffsetX; 1423 1424 public float mScaledOffsetY; 1425 1426 public void initialize(float scale, float magnifiedRegionCenterX, 1427 float magnifiedRegionCenterY) { 1428 mScale = scale; 1429 1430 final int viewportWidth = mViewport.getBounds().width(); 1431 final int viewportHeight = mViewport.getBounds().height(); 1432 final float minMagnifiedRegionCenterX = (viewportWidth / 2) / scale; 1433 final float minMagnifiedRegionCenterY = (viewportHeight / 2) / scale; 1434 final float maxMagnifiedRegionCenterX = viewportWidth - minMagnifiedRegionCenterX; 1435 final float maxMagnifiedRegionCenterY = viewportHeight - minMagnifiedRegionCenterY; 1436 1437 mMagnifiedRegionCenterX = Math.min(Math.max(magnifiedRegionCenterX, 1438 minMagnifiedRegionCenterX), maxMagnifiedRegionCenterX); 1439 mMagnifiedRegionCenterY = Math.min(Math.max(magnifiedRegionCenterY, 1440 minMagnifiedRegionCenterY), maxMagnifiedRegionCenterY); 1441 1442 mScaledOffsetX = -(mMagnifiedRegionCenterX * scale - viewportWidth / 2); 1443 mScaledOffsetY = -(mMagnifiedRegionCenterY * scale - viewportHeight / 2); 1444 } 1445 1446 public void updateFrom(MagnificationSpec other) { 1447 mScale = other.mScale; 1448 mMagnifiedRegionCenterX = other.mMagnifiedRegionCenterX; 1449 mMagnifiedRegionCenterY = other.mMagnifiedRegionCenterY; 1450 mScaledOffsetX = other.mScaledOffsetX; 1451 mScaledOffsetY = other.mScaledOffsetY; 1452 } 1453 1454 public void reset() { 1455 mScale = DEFAULT_SCALE; 1456 mMagnifiedRegionCenterX = 0; 1457 mMagnifiedRegionCenterY = 0; 1458 mScaledOffsetX = 0; 1459 mScaledOffsetY = 0; 1460 } 1461 } 1462 } 1463 1464 private static final class Viewport { 1465 1466 private static final String PROPERTY_NAME_ALPHA = "alpha"; 1467 1468 private static final String PROPERTY_NAME_BOUNDS = "bounds"; 1469 1470 private static final int MIN_ALPHA = 0; 1471 1472 private static final int MAX_ALPHA = 255; 1473 1474 private final ArrayList<WindowInfo> mTempWindowInfoList = new ArrayList<WindowInfo>(); 1475 1476 private final Rect mTempRect = new Rect(); 1477 1478 private final IWindowManager mWindowManagerService; 1479 private final DisplayProvider mDisplayProvider; 1480 1481 private final ViewportWindow mViewportFrame; 1482 1483 private final ValueAnimator mResizeFrameAnimator; 1484 1485 private final ValueAnimator mShowHideFrameAnimator; 1486 1487 public Viewport(Context context, WindowManager windowManager, 1488 IWindowManager windowManagerService, DisplayProvider displayInfoProvider, 1489 Interpolator animationInterpolator, long animationDuration) { 1490 mWindowManagerService = windowManagerService; 1491 mDisplayProvider = displayInfoProvider; 1492 mViewportFrame = new ViewportWindow(context, windowManager, displayInfoProvider); 1493 1494 mShowHideFrameAnimator = ObjectAnimator.ofInt(mViewportFrame, PROPERTY_NAME_ALPHA, 1495 MIN_ALPHA, MAX_ALPHA); 1496 mShowHideFrameAnimator.setInterpolator(animationInterpolator); 1497 mShowHideFrameAnimator.setDuration(animationDuration); 1498 mShowHideFrameAnimator.addListener(new AnimatorListener() { 1499 @Override 1500 public void onAnimationEnd(Animator animation) { 1501 if (mShowHideFrameAnimator.getAnimatedValue().equals(MIN_ALPHA)) { 1502 mViewportFrame.hide(); 1503 } 1504 } 1505 @Override 1506 public void onAnimationStart(Animator animation) { 1507 /* do nothing - stub */ 1508 } 1509 @Override 1510 public void onAnimationCancel(Animator animation) { 1511 /* do nothing - stub */ 1512 } 1513 @Override 1514 public void onAnimationRepeat(Animator animation) { 1515 /* do nothing - stub */ 1516 } 1517 }); 1518 1519 Property<ViewportWindow, Rect> property = Property.of(ViewportWindow.class, 1520 Rect.class, PROPERTY_NAME_BOUNDS); 1521 TypeEvaluator<Rect> evaluator = new TypeEvaluator<Rect>() { 1522 private final Rect mReusableResultRect = new Rect(); 1523 @Override 1524 public Rect evaluate(float fraction, Rect fromFrame, Rect toFrame) { 1525 Rect result = mReusableResultRect; 1526 result.left = (int) (fromFrame.left 1527 + (toFrame.left - fromFrame.left) * fraction); 1528 result.top = (int) (fromFrame.top 1529 + (toFrame.top - fromFrame.top) * fraction); 1530 result.right = (int) (fromFrame.right 1531 + (toFrame.right - fromFrame.right) * fraction); 1532 result.bottom = (int) (fromFrame.bottom 1533 + (toFrame.bottom - fromFrame.bottom) * fraction); 1534 return result; 1535 } 1536 }; 1537 mResizeFrameAnimator = ObjectAnimator.ofObject(mViewportFrame, property, 1538 evaluator, mViewportFrame.mBounds, mViewportFrame.mBounds); 1539 mResizeFrameAnimator.setDuration((long) (animationDuration)); 1540 mResizeFrameAnimator.setInterpolator(animationInterpolator); 1541 1542 recomputeBounds(false); 1543 } 1544 1545 public void recomputeBounds(boolean animate) { 1546 Rect frame = mTempRect; 1547 frame.set(0, 0, mDisplayProvider.getDisplayInfo().logicalWidth, 1548 mDisplayProvider.getDisplayInfo().logicalHeight); 1549 ArrayList<WindowInfo> infos = mTempWindowInfoList; 1550 infos.clear(); 1551 try { 1552 mWindowManagerService.getVisibleWindowsForDisplay( 1553 mDisplayProvider.getDisplay().getDisplayId(), infos); 1554 final int windowCount = infos.size(); 1555 for (int i = 0; i < windowCount; i++) { 1556 WindowInfo info = infos.get(i); 1557 if (info.type == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR 1558 || info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD 1559 || info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG) { 1560 subtract(frame, info.touchableRegion); 1561 } 1562 info.recycle(); 1563 } 1564 } catch (RemoteException re) { 1565 /* ignore */ 1566 } finally { 1567 infos.clear(); 1568 } 1569 resize(frame, animate); 1570 } 1571 1572 public void rotationChanged() { 1573 mViewportFrame.rotationChanged(); 1574 } 1575 1576 public Rect getBounds() { 1577 return mViewportFrame.getBounds(); 1578 } 1579 1580 public void setFrameShown(boolean shown, boolean animate) { 1581 if (mViewportFrame.isShown() == shown) { 1582 return; 1583 } 1584 if (animate) { 1585 if (mShowHideFrameAnimator.isRunning()) { 1586 mShowHideFrameAnimator.reverse(); 1587 } else { 1588 if (shown) { 1589 mViewportFrame.show(); 1590 mShowHideFrameAnimator.start(); 1591 } else { 1592 mShowHideFrameAnimator.reverse(); 1593 } 1594 } 1595 } else { 1596 mShowHideFrameAnimator.cancel(); 1597 if (shown) { 1598 mViewportFrame.show(); 1599 } else { 1600 mViewportFrame.hide(); 1601 } 1602 } 1603 } 1604 1605 private void resize(Rect bounds, boolean animate) { 1606 if (mViewportFrame.getBounds().equals(bounds)) { 1607 return; 1608 } 1609 if (animate) { 1610 if (mResizeFrameAnimator.isRunning()) { 1611 mResizeFrameAnimator.cancel(); 1612 } 1613 mResizeFrameAnimator.setObjectValues(mViewportFrame.mBounds, bounds); 1614 mResizeFrameAnimator.start(); 1615 } else { 1616 mViewportFrame.setBounds(bounds); 1617 } 1618 } 1619 1620 private boolean subtract(Rect lhs, Rect rhs) { 1621 if (lhs.right < rhs.left || lhs.left > rhs.right 1622 || lhs.bottom < rhs.top || lhs.top > rhs.bottom) { 1623 return false; 1624 } 1625 if (lhs.left < rhs.left) { 1626 lhs.right = rhs.left; 1627 } 1628 if (lhs.top < rhs.top) { 1629 lhs.bottom = rhs.top; 1630 } 1631 if (lhs.right > rhs.right) { 1632 lhs.left = rhs.right; 1633 } 1634 if (lhs.bottom > rhs.bottom) { 1635 lhs.top = rhs.bottom; 1636 } 1637 return true; 1638 } 1639 1640 private static final class ViewportWindow { 1641 private static final String WINDOW_TITLE = "Magnification Overlay"; 1642 1643 private final WindowManager mWindowManager; 1644 private final DisplayProvider mDisplayProvider; 1645 1646 private final ContentView mWindowContent; 1647 private final WindowManager.LayoutParams mWindowParams; 1648 1649 private final Rect mBounds = new Rect(); 1650 private boolean mShown; 1651 private int mAlpha; 1652 1653 public ViewportWindow(Context context, WindowManager windowManager, 1654 DisplayProvider displayProvider) { 1655 mWindowManager = windowManager; 1656 mDisplayProvider = displayProvider; 1657 1658 ViewGroup.LayoutParams contentParams = new ViewGroup.LayoutParams( 1659 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 1660 mWindowContent = new ContentView(context); 1661 mWindowContent.setLayoutParams(contentParams); 1662 mWindowContent.setBackgroundColor(R.color.transparent); 1663 1664 mWindowParams = new WindowManager.LayoutParams( 1665 WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY); 1666 mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 1667 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 1668 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 1669 mWindowParams.setTitle(WINDOW_TITLE); 1670 mWindowParams.gravity = Gravity.CENTER; 1671 mWindowParams.width = displayProvider.getDisplayInfo().logicalWidth; 1672 mWindowParams.height = displayProvider.getDisplayInfo().logicalHeight; 1673 mWindowParams.format = PixelFormat.TRANSLUCENT; 1674 } 1675 1676 public boolean isShown() { 1677 return mShown; 1678 } 1679 1680 public void show() { 1681 if (mShown) { 1682 return; 1683 } 1684 mShown = true; 1685 mWindowManager.addView(mWindowContent, mWindowParams); 1686 if (DEBUG_VIEWPORT_WINDOW) { 1687 Slog.i(LOG_TAG, "ViewportWindow shown."); 1688 } 1689 } 1690 1691 public void hide() { 1692 if (!mShown) { 1693 return; 1694 } 1695 mShown = false; 1696 mWindowManager.removeView(mWindowContent); 1697 if (DEBUG_VIEWPORT_WINDOW) { 1698 Slog.i(LOG_TAG, "ViewportWindow hidden."); 1699 } 1700 } 1701 1702 @SuppressWarnings("unused") 1703 // Called reflectively from an animator. 1704 public int getAlpha() { 1705 return mAlpha; 1706 } 1707 1708 @SuppressWarnings("unused") 1709 // Called reflectively from an animator. 1710 public void setAlpha(int alpha) { 1711 if (mAlpha == alpha) { 1712 return; 1713 } 1714 mAlpha = alpha; 1715 if (mShown) { 1716 mWindowContent.invalidate(); 1717 } 1718 if (DEBUG_VIEWPORT_WINDOW) { 1719 Slog.i(LOG_TAG, "ViewportFrame set alpha: " + alpha); 1720 } 1721 } 1722 1723 public Rect getBounds() { 1724 return mBounds; 1725 } 1726 1727 public void rotationChanged() { 1728 mWindowParams.width = mDisplayProvider.getDisplayInfo().logicalWidth; 1729 mWindowParams.height = mDisplayProvider.getDisplayInfo().logicalHeight; 1730 if (mShown) { 1731 mWindowManager.updateViewLayout(mWindowContent, mWindowParams); 1732 } 1733 } 1734 1735 public void setBounds(Rect bounds) { 1736 if (mBounds.equals(bounds)) { 1737 return; 1738 } 1739 mBounds.set(bounds); 1740 if (mShown) { 1741 mWindowContent.invalidate(); 1742 } 1743 if (DEBUG_VIEWPORT_WINDOW) { 1744 Slog.i(LOG_TAG, "ViewportFrame set bounds: " + bounds); 1745 } 1746 } 1747 1748 private final class ContentView extends View { 1749 private final Drawable mHighlightFrame; 1750 1751 public ContentView(Context context) { 1752 super(context); 1753 mHighlightFrame = context.getResources().getDrawable( 1754 R.drawable.magnified_region_frame); 1755 } 1756 1757 @Override 1758 public void onDraw(Canvas canvas) { 1759 canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR); 1760 mHighlightFrame.setBounds(mBounds); 1761 mHighlightFrame.setAlpha(mAlpha); 1762 mHighlightFrame.draw(canvas); 1763 } 1764 } 1765 } 1766 } 1767 1768 private static class DisplayProvider implements DisplayListener { 1769 private final WindowManager mWindowManager; 1770 private final DisplayManager mDisplayManager; 1771 private final Display mDefaultDisplay; 1772 private final DisplayInfo mDefaultDisplayInfo = new DisplayInfo(); 1773 1774 public DisplayProvider(Context context, WindowManager windowManager) { 1775 mWindowManager = windowManager; 1776 mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); 1777 mDefaultDisplay = mWindowManager.getDefaultDisplay(); 1778 mDisplayManager.registerDisplayListener(this, null); 1779 updateDisplayInfo(); 1780 } 1781 1782 public DisplayInfo getDisplayInfo() { 1783 return mDefaultDisplayInfo; 1784 } 1785 1786 public Display getDisplay() { 1787 return mDefaultDisplay; 1788 } 1789 1790 private void updateDisplayInfo() { 1791 if (!mDefaultDisplay.getDisplayInfo(mDefaultDisplayInfo)) { 1792 Slog.e(LOG_TAG, "Default display is not valid."); 1793 } 1794 } 1795 1796 public void destroy() { 1797 mDisplayManager.unregisterDisplayListener(this); 1798 } 1799 1800 @Override 1801 public void onDisplayAdded(int displayId) { 1802 /* do noting */ 1803 } 1804 1805 @Override 1806 public void onDisplayRemoved(int displayId) { 1807 // Having no default display 1808 } 1809 1810 @Override 1811 public void onDisplayChanged(int displayId) { 1812 updateDisplayInfo(); 1813 } 1814 } 1815 1816 /** 1817 * The listener for receiving notifications when gestures occur. 1818 * If you want to listen for all the different gestures then implement 1819 * this interface. If you only want to listen for a subset it might 1820 * be easier to extend {@link SimpleOnScaleGestureListener}. 1821 * 1822 * An application will receive events in the following order: 1823 * <ul> 1824 * <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} 1825 * <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} 1826 * <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)} 1827 * </ul> 1828 */ 1829 interface OnScaleGestureListener { 1830 /** 1831 * Responds to scaling events for a gesture in progress. 1832 * Reported by pointer motion. 1833 * 1834 * @param detector The detector reporting the event - use this to 1835 * retrieve extended info about event state. 1836 * @return Whether or not the detector should consider this event 1837 * as handled. If an event was not handled, the detector 1838 * will continue to accumulate movement until an event is 1839 * handled. This can be useful if an application, for example, 1840 * only wants to update scaling factors if the change is 1841 * greater than 0.01. 1842 */ 1843 public boolean onScale(ScaleGestureDetector detector); 1844 1845 /** 1846 * Responds to the beginning of a scaling gesture. Reported by 1847 * new pointers going down. 1848 * 1849 * @param detector The detector reporting the event - use this to 1850 * retrieve extended info about event state. 1851 * @return Whether or not the detector should continue recognizing 1852 * this gesture. For example, if a gesture is beginning 1853 * with a focal point outside of a region where it makes 1854 * sense, onScaleBegin() may return false to ignore the 1855 * rest of the gesture. 1856 */ 1857 public boolean onScaleBegin(ScaleGestureDetector detector); 1858 1859 /** 1860 * Responds to the end of a scale gesture. Reported by existing 1861 * pointers going up. 1862 * 1863 * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} 1864 * and {@link ScaleGestureDetector#getFocusY()} will return the location 1865 * of the pointer remaining on the screen. 1866 * 1867 * @param detector The detector reporting the event - use this to 1868 * retrieve extended info about event state. 1869 */ 1870 public void onScaleEnd(ScaleGestureDetector detector); 1871 } 1872 1873 class ScaleGestureDetector { 1874 1875 private final MinCircleFinder mMinCircleFinder = new MinCircleFinder(); 1876 1877 private final OnScaleGestureListener mListener; 1878 1879 private float mFocusX; 1880 private float mFocusY; 1881 1882 private float mCurrSpan; 1883 private float mPrevSpan; 1884 private float mCurrSpanX; 1885 private float mCurrSpanY; 1886 private float mPrevSpanX; 1887 private float mPrevSpanY; 1888 private long mCurrTime; 1889 private long mPrevTime; 1890 private boolean mInProgress; 1891 1892 public ScaleGestureDetector(OnScaleGestureListener listener) { 1893 mListener = listener; 1894 } 1895 1896 /** 1897 * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener} 1898 * when appropriate. 1899 * 1900 * <p>Applications should pass a complete and consistent event stream to this method. 1901 * A complete and consistent event stream involves all MotionEvents from the initial 1902 * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p> 1903 * 1904 * @param event The event to process 1905 * @return true if the event was processed and the detector wants to receive the 1906 * rest of the MotionEvents in this event stream. 1907 */ 1908 public boolean onTouchEvent(MotionEvent event) { 1909 boolean streamEnded = false; 1910 boolean contextChanged = false; 1911 int excludedPtrIdx = -1; 1912 final int action = event.getActionMasked(); 1913 switch (action) { 1914 case MotionEvent.ACTION_DOWN: 1915 case MotionEvent.ACTION_POINTER_DOWN: { 1916 contextChanged = true; 1917 } break; 1918 case MotionEvent.ACTION_POINTER_UP: { 1919 contextChanged = true; 1920 excludedPtrIdx = event.getActionIndex(); 1921 } break; 1922 case MotionEvent.ACTION_UP: 1923 case MotionEvent.ACTION_CANCEL: { 1924 streamEnded = true; 1925 } break; 1926 } 1927 1928 if (mInProgress && (contextChanged || streamEnded)) { 1929 mListener.onScaleEnd(this); 1930 mInProgress = false; 1931 mPrevSpan = 0; 1932 mPrevSpanX = 0; 1933 mPrevSpanY = 0; 1934 return true; 1935 } 1936 1937 final long currTime = mCurrTime; 1938 1939 mFocusX = 0; 1940 mFocusY = 0; 1941 mCurrSpan = 0; 1942 mCurrSpanX = 0; 1943 mCurrSpanY = 0; 1944 mCurrTime = 0; 1945 mPrevTime = 0; 1946 1947 if (!streamEnded) { 1948 MinCircleFinder.Circle circle = 1949 mMinCircleFinder.computeMinCircleAroundPointers(event); 1950 mFocusX = circle.centerX; 1951 mFocusY = circle.centerY; 1952 1953 double sumSlope = 0; 1954 final int pointerCount = event.getPointerCount(); 1955 for (int i = 0; i < pointerCount; i++) { 1956 if (i == excludedPtrIdx) { 1957 continue; 1958 } 1959 float x = event.getX(i) - mFocusX; 1960 float y = event.getY(i) - mFocusY; 1961 if (x == 0) { 1962 x += 0.1f; 1963 } 1964 sumSlope += y / x; 1965 } 1966 final double avgSlope = sumSlope 1967 / ((excludedPtrIdx < 0) ? pointerCount : pointerCount - 1); 1968 1969 double angle = Math.atan(avgSlope); 1970 mCurrSpan = 2 * circle.radius; 1971 mCurrSpanX = (float) Math.abs((Math.cos(angle) * mCurrSpan)); 1972 mCurrSpanY = (float) Math.abs((Math.sin(angle) * mCurrSpan)); 1973 } 1974 1975 if (contextChanged || mPrevSpan == 0 || mPrevSpanX == 0 || mPrevSpanY == 0) { 1976 mPrevSpan = mCurrSpan; 1977 mPrevSpanX = mCurrSpanX; 1978 mPrevSpanY = mCurrSpanY; 1979 } 1980 1981 if (!mInProgress && mCurrSpan != 0 && !streamEnded) { 1982 mInProgress = mListener.onScaleBegin(this); 1983 } 1984 1985 if (mInProgress) { 1986 mPrevTime = (currTime != 0) ? currTime : event.getEventTime(); 1987 mCurrTime = event.getEventTime(); 1988 if (mCurrSpan == 0) { 1989 mListener.onScaleEnd(this); 1990 mInProgress = false; 1991 } else { 1992 if (mListener.onScale(this)) { 1993 mPrevSpanX = mCurrSpanX; 1994 mPrevSpanY = mCurrSpanY; 1995 mPrevSpan = mCurrSpan; 1996 } 1997 } 1998 } 1999 2000 return true; 2001 } 2002 2003 /** 2004 * Returns {@code true} if a scale gesture is in progress. 2005 */ 2006 public boolean isInProgress() { 2007 return mInProgress; 2008 } 2009 2010 /** 2011 * Get the X coordinate of the current gesture's focal point. 2012 * If a gesture is in progress, the focal point is between 2013 * each of the pointers forming the gesture. 2014 * 2015 * If {@link #isInProgress()} would return false, the result of this 2016 * function is undefined. 2017 * 2018 * @return X coordinate of the focal point in pixels. 2019 */ 2020 public float getFocusX() { 2021 return mFocusX; 2022 } 2023 2024 /** 2025 * Get the Y coordinate of the current gesture's focal point. 2026 * If a gesture is in progress, the focal point is between 2027 * each of the pointers forming the gesture. 2028 * 2029 * If {@link #isInProgress()} would return false, the result of this 2030 * function is undefined. 2031 * 2032 * @return Y coordinate of the focal point in pixels. 2033 */ 2034 public float getFocusY() { 2035 return mFocusY; 2036 } 2037 2038 /** 2039 * Return the average distance between each of the pointers forming the 2040 * gesture in progress through the focal point. 2041 * 2042 * @return Distance between pointers in pixels. 2043 */ 2044 public float getCurrentSpan() { 2045 return mCurrSpan; 2046 } 2047 2048 /** 2049 * Return the average X distance between each of the pointers forming the 2050 * gesture in progress through the focal point. 2051 * 2052 * @return Distance between pointers in pixels. 2053 */ 2054 public float getCurrentSpanX() { 2055 return mCurrSpanX; 2056 } 2057 2058 /** 2059 * Return the average Y distance between each of the pointers forming the 2060 * gesture in progress through the focal point. 2061 * 2062 * @return Distance between pointers in pixels. 2063 */ 2064 public float getCurrentSpanY() { 2065 return mCurrSpanY; 2066 } 2067 2068 /** 2069 * Return the previous average distance between each of the pointers forming the 2070 * gesture in progress through the focal point. 2071 * 2072 * @return Previous distance between pointers in pixels. 2073 */ 2074 public float getPreviousSpan() { 2075 return mPrevSpan; 2076 } 2077 2078 /** 2079 * Return the previous average X distance between each of the pointers forming the 2080 * gesture in progress through the focal point. 2081 * 2082 * @return Previous distance between pointers in pixels. 2083 */ 2084 public float getPreviousSpanX() { 2085 return mPrevSpanX; 2086 } 2087 2088 /** 2089 * Return the previous average Y distance between each of the pointers forming the 2090 * gesture in progress through the focal point. 2091 * 2092 * @return Previous distance between pointers in pixels. 2093 */ 2094 public float getPreviousSpanY() { 2095 return mPrevSpanY; 2096 } 2097 2098 /** 2099 * Return the scaling factor from the previous scale event to the current 2100 * event. This value is defined as 2101 * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}). 2102 * 2103 * @return The current scaling factor. 2104 */ 2105 public float getScaleFactor() { 2106 return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1; 2107 } 2108 2109 /** 2110 * Return the time difference in milliseconds between the previous 2111 * accepted scaling event and the current scaling event. 2112 * 2113 * @return Time difference since the last scaling event in milliseconds. 2114 */ 2115 public long getTimeDelta() { 2116 return mCurrTime - mPrevTime; 2117 } 2118 2119 /** 2120 * Return the event time of the current event being processed. 2121 * 2122 * @return Current event time in milliseconds. 2123 */ 2124 public long getEventTime() { 2125 return mCurrTime; 2126 } 2127 } 2128 2129 private static final class MinCircleFinder { 2130 private final ArrayList<PointHolder> mPoints = new ArrayList<PointHolder>(); 2131 private final ArrayList<PointHolder> sBoundary = new ArrayList<PointHolder>(); 2132 private final Circle mMinCircle = new Circle(); 2133 2134 /** 2135 * Finds the minimal circle that contains all pointers of a motion event. 2136 * 2137 * @param event A motion event. 2138 * @return The minimal circle. 2139 */ 2140 public Circle computeMinCircleAroundPointers(MotionEvent event) { 2141 ArrayList<PointHolder> points = mPoints; 2142 points.clear(); 2143 final int pointerCount = event.getPointerCount(); 2144 for (int i = 0; i < pointerCount; i++) { 2145 PointHolder point = PointHolder.obtain(event.getX(i), event.getY(i)); 2146 points.add(point); 2147 } 2148 ArrayList<PointHolder> boundary = sBoundary; 2149 boundary.clear(); 2150 computeMinCircleAroundPointsRecursive(points, boundary, mMinCircle); 2151 for (int i = points.size() - 1; i >= 0; i--) { 2152 points.remove(i).recycle(); 2153 } 2154 boundary.clear(); 2155 return mMinCircle; 2156 } 2157 2158 private static void computeMinCircleAroundPointsRecursive(ArrayList<PointHolder> points, 2159 ArrayList<PointHolder> boundary, Circle outCircle) { 2160 if (points.isEmpty()) { 2161 if (boundary.size() == 0) { 2162 outCircle.initialize(); 2163 } else if (boundary.size() == 1) { 2164 outCircle.initialize(boundary.get(0).mData, boundary.get(0).mData); 2165 } else if (boundary.size() == 2) { 2166 outCircle.initialize(boundary.get(0).mData, boundary.get(1).mData); 2167 } else if (boundary.size() == 3) { 2168 outCircle.initialize(boundary.get(0).mData, boundary.get(1).mData, 2169 boundary.get(2).mData); 2170 } 2171 return; 2172 } 2173 PointHolder point = points.remove(points.size() - 1); 2174 computeMinCircleAroundPointsRecursive(points, boundary, outCircle); 2175 if (!outCircle.contains(point.mData)) { 2176 boundary.add(point); 2177 computeMinCircleAroundPointsRecursive(points, boundary, outCircle); 2178 boundary.remove(point); 2179 } 2180 points.add(point); 2181 } 2182 2183 private static final class PointHolder { 2184 private static final int MAX_POOL_SIZE = 20; 2185 private static PointHolder sPool; 2186 private static int sPoolSize; 2187 2188 private PointHolder mNext; 2189 private boolean mIsInPool; 2190 2191 private final PointF mData = new PointF(); 2192 2193 public static PointHolder obtain(float x, float y) { 2194 PointHolder holder; 2195 if (sPoolSize > 0) { 2196 sPoolSize--; 2197 holder = sPool; 2198 sPool = sPool.mNext; 2199 holder.mNext = null; 2200 holder.mIsInPool = false; 2201 } else { 2202 holder = new PointHolder(); 2203 } 2204 holder.mData.set(x, y); 2205 return holder; 2206 } 2207 2208 public void recycle() { 2209 if (mIsInPool) { 2210 throw new IllegalStateException("Already recycled."); 2211 } 2212 clear(); 2213 if (sPoolSize < MAX_POOL_SIZE) { 2214 sPoolSize++; 2215 mNext = sPool; 2216 sPool = this; 2217 mIsInPool = true; 2218 } 2219 } 2220 2221 private void clear() { 2222 mData.set(0, 0); 2223 } 2224 } 2225 2226 public static final class Circle { 2227 public float centerX; 2228 public float centerY; 2229 public float radius; 2230 2231 private void initialize() { 2232 centerX = 0; 2233 centerY = 0; 2234 radius = 0; 2235 } 2236 2237 private void initialize(PointF first, PointF second, PointF third) { 2238 if (!hasLineWithInfiniteSlope(first, second, third)) { 2239 initializeInternal(first, second, third); 2240 } else if (!hasLineWithInfiniteSlope(first, third, second)) { 2241 initializeInternal(first, third, second); 2242 } else if (!hasLineWithInfiniteSlope(second, first, third)) { 2243 initializeInternal(second, first, third); 2244 } else if (!hasLineWithInfiniteSlope(second, third, first)) { 2245 initializeInternal(second, third, first); 2246 } else if (!hasLineWithInfiniteSlope(third, first, second)) { 2247 initializeInternal(third, first, second); 2248 } else if (!hasLineWithInfiniteSlope(third, second, first)) { 2249 initializeInternal(third, second, first); 2250 } else { 2251 initialize(); 2252 } 2253 } 2254 2255 private void initialize(PointF first, PointF second) { 2256 radius = (float) (Math.hypot(second.x - first.x, second.y - first.y) / 2); 2257 centerX = (float) (second.x + first.x) / 2; 2258 centerY = (float) (second.y + first.y) / 2; 2259 } 2260 2261 public boolean contains(PointF point) { 2262 return (int) (Math.hypot(point.x - centerX, point.y - centerY)) <= radius; 2263 } 2264 2265 private void initializeInternal(PointF first, PointF second, PointF third) { 2266 final float x1 = first.x; 2267 final float y1 = first.y; 2268 final float x2 = second.x; 2269 final float y2 = second.y; 2270 final float x3 = third.x; 2271 final float y3 = third.y; 2272 2273 final float sl1 = (y2 - y1) / (x2 - x1); 2274 final float sl2 = (y3 - y2) / (x3 - x2); 2275 2276 centerX = (int) ((sl1 * sl2 * (y1 - y3) + sl2 * (x1 + x2) - sl1 * (x2 + x3)) 2277 / (2 * (sl2 - sl1))); 2278 centerY = (int) (-1 / sl1 * (centerX - (x1 + x2) / 2) + (y1 + y2) / 2); 2279 radius = (int) Math.hypot(x1 - centerX, y1 - centerY); 2280 } 2281 2282 private boolean hasLineWithInfiniteSlope(PointF first, PointF second, PointF third) { 2283 return (second.x - first.x == 0 || third.x - second.x == 0 2284 || second.y - first.y == 0 || third.y - second.y == 0); 2285 } 2286 2287 @Override 2288 public String toString() { 2289 return "cetner: [" + centerX + ", " + centerY + "] radius: " + radius; 2290 } 2291 } 2292 } 2293} 2294