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