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