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