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