1 /* 2 * Copyright (C) 2017 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 17 package android.widget; 18 19 import android.annotation.FloatRange; 20 import android.annotation.IntDef; 21 import android.annotation.IntRange; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.Px; 25 import android.annotation.TestApi; 26 import android.annotation.UiThread; 27 import android.content.Context; 28 import android.content.res.Resources; 29 import android.content.res.TypedArray; 30 import android.graphics.Bitmap; 31 import android.graphics.Canvas; 32 import android.graphics.Color; 33 import android.graphics.Insets; 34 import android.graphics.Outline; 35 import android.graphics.Paint; 36 import android.graphics.PixelFormat; 37 import android.graphics.Point; 38 import android.graphics.PointF; 39 import android.graphics.RecordingCanvas; 40 import android.graphics.Rect; 41 import android.graphics.RenderNode; 42 import android.graphics.drawable.ColorDrawable; 43 import android.graphics.drawable.Drawable; 44 import android.os.Handler; 45 import android.os.HandlerThread; 46 import android.os.Message; 47 import android.util.Log; 48 import android.view.ContextThemeWrapper; 49 import android.view.Display; 50 import android.view.PixelCopy; 51 import android.view.Surface; 52 import android.view.SurfaceControl; 53 import android.view.SurfaceHolder; 54 import android.view.SurfaceSession; 55 import android.view.SurfaceView; 56 import android.view.ThreadedRenderer; 57 import android.view.View; 58 import android.view.ViewRootImpl; 59 60 import com.android.internal.R; 61 import com.android.internal.util.Preconditions; 62 63 import java.lang.annotation.Retention; 64 import java.lang.annotation.RetentionPolicy; 65 66 /** 67 * Android magnifier widget. Can be used by any view which is attached to a window. 68 */ 69 @UiThread 70 public final class Magnifier { 71 private static final String TAG = "Magnifier"; 72 // Use this to specify that a previous configuration value does not exist. 73 private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1; 74 // The callbacks of the pixel copy requests will be invoked on 75 // the Handler of this Thread when the copy is finished. 76 private static final HandlerThread sPixelCopyHandlerThread = 77 new HandlerThread("magnifier pixel copy result handler"); 78 79 // The view to which this magnifier is attached. 80 private final View mView; 81 // The coordinates of the view in the surface. 82 private final int[] mViewCoordinatesInSurface; 83 // The window containing the magnifier. 84 private InternalPopupWindow mWindow; 85 // The width of the window containing the magnifier. 86 private final int mWindowWidth; 87 // The height of the window containing the magnifier. 88 private final int mWindowHeight; 89 // The zoom applied to the view region copied to the magnifier view. 90 private float mZoom; 91 // The width of the content that will be copied to the magnifier. 92 private int mSourceWidth; 93 // The height of the content that will be copied to the magnifier. 94 private int mSourceHeight; 95 // Whether the zoom of the magnifier or the view position have changed since last content copy. 96 private boolean mDirtyState; 97 // The elevation of the window containing the magnifier. 98 private final float mWindowElevation; 99 // The corner radius of the window containing the magnifier. 100 private final float mWindowCornerRadius; 101 // The overlay to be drawn on the top of the magnifier content. 102 private final Drawable mOverlay; 103 // The horizontal offset between the source and window coords when #show(float, float) is used. 104 private final int mDefaultHorizontalSourceToMagnifierOffset; 105 // The vertical offset between the source and window coords when #show(float, float) is used. 106 private final int mDefaultVerticalSourceToMagnifierOffset; 107 // Whether the area where the magnifier can be positioned will be clipped to the main window 108 // and within system insets. 109 private final boolean mClippingEnabled; 110 // The behavior of the left bound of the rectangle where the content can be copied from. 111 private @SourceBound int mLeftContentBound; 112 // The behavior of the top bound of the rectangle where the content can be copied from. 113 private @SourceBound int mTopContentBound; 114 // The behavior of the right bound of the rectangle where the content can be copied from. 115 private @SourceBound int mRightContentBound; 116 // The behavior of the bottom bound of the rectangle where the content can be copied from. 117 private @SourceBound int mBottomContentBound; 118 // The parent surface for the magnifier surface. 119 private SurfaceInfo mParentSurface; 120 // The surface where the content will be copied from. 121 private SurfaceInfo mContentCopySurface; 122 // The center coordinates of the window containing the magnifier. 123 private final Point mWindowCoords = new Point(); 124 // The center coordinates of the content to be magnified, 125 // clamped inside the visible region of the magnified view. 126 private final Point mClampedCenterZoomCoords = new Point(); 127 // Variables holding previous states, used for detecting redundant calls and invalidation. 128 private final Point mPrevStartCoordsInSurface = new Point( 129 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE); 130 private final PointF mPrevShowSourceCoords = new PointF( 131 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE); 132 private final PointF mPrevShowWindowCoords = new PointF( 133 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE); 134 // Rectangle defining the view surface area we pixel copy content from. 135 private final Rect mPixelCopyRequestRect = new Rect(); 136 // Lock to synchronize between the UI thread and the thread that handles pixel copy results. 137 // Only sync mWindow writes from UI thread with mWindow reads from sPixelCopyHandlerThread. 138 private final Object mLock = new Object(); 139 140 /** 141 * Initializes a magnifier. 142 * 143 * @param view the view for which this magnifier is attached 144 * 145 * @deprecated Please use {@link Builder} instead 146 */ 147 @Deprecated Magnifier(@onNull View view)148 public Magnifier(@NonNull View view) { 149 this(createBuilderWithOldMagnifierDefaults(view)); 150 } 151 createBuilderWithOldMagnifierDefaults(final View view)152 static Builder createBuilderWithOldMagnifierDefaults(final View view) { 153 final Builder params = new Builder(view); 154 final Context context = view.getContext(); 155 final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Magnifier, 156 R.attr.magnifierStyle, 0); 157 params.mWidth = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierWidth, 0); 158 params.mHeight = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHeight, 0); 159 params.mElevation = a.getDimension(R.styleable.Magnifier_magnifierElevation, 0); 160 params.mCornerRadius = getDeviceDefaultDialogCornerRadius(context); 161 params.mZoom = a.getFloat(R.styleable.Magnifier_magnifierZoom, 0); 162 params.mHorizontalDefaultSourceToMagnifierOffset = 163 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHorizontalOffset, 0); 164 params.mVerticalDefaultSourceToMagnifierOffset = 165 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierVerticalOffset, 0); 166 params.mOverlay = new ColorDrawable(a.getColor( 167 R.styleable.Magnifier_magnifierColorOverlay, Color.TRANSPARENT)); 168 a.recycle(); 169 params.mClippingEnabled = true; 170 params.mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE; 171 params.mTopContentBound = SOURCE_BOUND_MAX_IN_SURFACE; 172 params.mRightContentBound = SOURCE_BOUND_MAX_VISIBLE; 173 params.mBottomContentBound = SOURCE_BOUND_MAX_IN_SURFACE; 174 return params; 175 } 176 177 /** 178 * Returns the device default theme dialog corner radius attribute. 179 * We retrieve this from the device default theme to avoid 180 * using the values set in the custom application themes. 181 */ getDeviceDefaultDialogCornerRadius(final Context context)182 private static float getDeviceDefaultDialogCornerRadius(final Context context) { 183 final Context deviceDefaultContext = 184 new ContextThemeWrapper(context, R.style.Theme_DeviceDefault); 185 final TypedArray ta = deviceDefaultContext.obtainStyledAttributes( 186 new int[]{android.R.attr.dialogCornerRadius}); 187 final float dialogCornerRadius = ta.getDimension(0, 0); 188 ta.recycle(); 189 return dialogCornerRadius; 190 } 191 Magnifier(@onNull Builder params)192 private Magnifier(@NonNull Builder params) { 193 // Copy params from builder. 194 mView = params.mView; 195 mWindowWidth = params.mWidth; 196 mWindowHeight = params.mHeight; 197 mZoom = params.mZoom; 198 mSourceWidth = Math.round(mWindowWidth / mZoom); 199 mSourceHeight = Math.round(mWindowHeight / mZoom); 200 mWindowElevation = params.mElevation; 201 mWindowCornerRadius = params.mCornerRadius; 202 mOverlay = params.mOverlay; 203 mDefaultHorizontalSourceToMagnifierOffset = 204 params.mHorizontalDefaultSourceToMagnifierOffset; 205 mDefaultVerticalSourceToMagnifierOffset = 206 params.mVerticalDefaultSourceToMagnifierOffset; 207 mClippingEnabled = params.mClippingEnabled; 208 mLeftContentBound = params.mLeftContentBound; 209 mTopContentBound = params.mTopContentBound; 210 mRightContentBound = params.mRightContentBound; 211 mBottomContentBound = params.mBottomContentBound; 212 // The view's surface coordinates will not be updated until the magnifier is first shown. 213 mViewCoordinatesInSurface = new int[2]; 214 } 215 216 static { sPixelCopyHandlerThread.start()217 sPixelCopyHandlerThread.start(); 218 } 219 220 /** 221 * Shows the magnifier on the screen. The method takes the coordinates of the center 222 * of the content source going to be magnified and copied to the magnifier. The coordinates 223 * are relative to the top left corner of the magnified view. The magnifier will be 224 * positioned such that its center will be at the default offset from the center of the source. 225 * The default offset can be specified using the method 226 * {@link Builder#setDefaultSourceToMagnifierOffset(int, int)}. If the offset should 227 * be different across calls to this method, you should consider to use method 228 * {@link #show(float, float, float, float)} instead. 229 * 230 * @param sourceCenterX horizontal coordinate of the source center, relative to the view 231 * @param sourceCenterY vertical coordinate of the source center, relative to the view 232 * 233 * @see Builder#setDefaultSourceToMagnifierOffset(int, int) 234 * @see Builder#getDefaultHorizontalSourceToMagnifierOffset() 235 * @see Builder#getDefaultVerticalSourceToMagnifierOffset() 236 * @see #show(float, float, float, float) 237 */ show(@loatRangefrom = 0) float sourceCenterX, @FloatRange(from = 0) float sourceCenterY)238 public void show(@FloatRange(from = 0) float sourceCenterX, 239 @FloatRange(from = 0) float sourceCenterY) { 240 show(sourceCenterX, sourceCenterY, 241 sourceCenterX + mDefaultHorizontalSourceToMagnifierOffset, 242 sourceCenterY + mDefaultVerticalSourceToMagnifierOffset); 243 } 244 245 /** 246 * Shows the magnifier on the screen at a position that is independent from its content 247 * position. The first two arguments represent the coordinates of the center of the 248 * content source going to be magnified and copied to the magnifier. The last two arguments 249 * represent the coordinates of the center of the magnifier itself. All four coordinates 250 * are relative to the top left corner of the magnified view. If you consider using this 251 * method such that the offset between the source center and the magnifier center coordinates 252 * remains constant, you should consider using method {@link #show(float, float)} instead. 253 * 254 * @param sourceCenterX horizontal coordinate of the source center relative to the view 255 * @param sourceCenterY vertical coordinate of the source center, relative to the view 256 * @param magnifierCenterX horizontal coordinate of the magnifier center, relative to the view 257 * @param magnifierCenterY vertical coordinate of the magnifier center, relative to the view 258 */ show(@loatRangefrom = 0) float sourceCenterX, @FloatRange(from = 0) float sourceCenterY, float magnifierCenterX, float magnifierCenterY)259 public void show(@FloatRange(from = 0) float sourceCenterX, 260 @FloatRange(from = 0) float sourceCenterY, 261 float magnifierCenterX, float magnifierCenterY) { 262 263 obtainSurfaces(); 264 obtainContentCoordinates(sourceCenterX, sourceCenterY); 265 obtainWindowCoordinates(magnifierCenterX, magnifierCenterY); 266 267 final int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2; 268 final int startY = mClampedCenterZoomCoords.y - mSourceHeight / 2; 269 if (sourceCenterX != mPrevShowSourceCoords.x || sourceCenterY != mPrevShowSourceCoords.y 270 || mDirtyState) { 271 if (mWindow == null) { 272 synchronized (mLock) { 273 mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(), 274 mParentSurface.mSurfaceControl, mWindowWidth, mWindowHeight, 275 mWindowElevation, mWindowCornerRadius, 276 mOverlay != null ? mOverlay : new ColorDrawable(Color.TRANSPARENT), 277 Handler.getMain() /* draw the magnifier on the UI thread */, mLock, 278 mCallback); 279 } 280 } 281 performPixelCopy(startX, startY, true /* update window position */); 282 } else if (magnifierCenterX != mPrevShowWindowCoords.x 283 || magnifierCenterY != mPrevShowWindowCoords.y) { 284 final Point windowCoords = getCurrentClampedWindowCoordinates(); 285 final InternalPopupWindow currentWindowInstance = mWindow; 286 sPixelCopyHandlerThread.getThreadHandler().post(() -> { 287 synchronized (mLock) { 288 if (mWindow != currentWindowInstance) { 289 // The magnifier was dismissed (and maybe shown again) in the meantime. 290 return; 291 } 292 mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y); 293 } 294 }); 295 } 296 mPrevShowSourceCoords.x = sourceCenterX; 297 mPrevShowSourceCoords.y = sourceCenterY; 298 mPrevShowWindowCoords.x = magnifierCenterX; 299 mPrevShowWindowCoords.y = magnifierCenterY; 300 } 301 302 /** 303 * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op. 304 */ dismiss()305 public void dismiss() { 306 if (mWindow != null) { 307 synchronized (mLock) { 308 mWindow.destroy(); 309 mWindow = null; 310 } 311 mPrevShowSourceCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE; 312 mPrevShowSourceCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE; 313 mPrevShowWindowCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE; 314 mPrevShowWindowCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE; 315 mPrevStartCoordsInSurface.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE; 316 mPrevStartCoordsInSurface.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE; 317 } 318 } 319 320 /** 321 * Asks the magnifier to update its content. It uses the previous coordinates passed to 322 * {@link #show(float, float)} or {@link #show(float, float, float, float)}. The 323 * method only has effect if the magnifier is currently showing. 324 */ update()325 public void update() { 326 if (mWindow != null) { 327 obtainSurfaces(); 328 if (!mDirtyState) { 329 // Update the content shown in the magnifier. 330 performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y, 331 false /* update window position */); 332 } else { 333 // If for example the zoom has changed, we cannot use the same top left 334 // coordinates as before, so just #show again to have them recomputed. 335 show(mPrevShowSourceCoords.x, mPrevShowSourceCoords.y, 336 mPrevShowWindowCoords.x, mPrevShowWindowCoords.y); 337 } 338 } 339 } 340 341 /** 342 * @return the width of the magnifier window, in pixels 343 * @see Magnifier.Builder#setSize(int, int) 344 */ 345 @Px getWidth()346 public int getWidth() { 347 return mWindowWidth; 348 } 349 350 /** 351 * @return the height of the magnifier window, in pixels 352 * @see Magnifier.Builder#setSize(int, int) 353 */ 354 @Px getHeight()355 public int getHeight() { 356 return mWindowHeight; 357 } 358 359 /** 360 * @return the initial width of the content magnified and copied to the magnifier, in pixels 361 * @see Magnifier.Builder#setSize(int, int) 362 * @see Magnifier.Builder#setInitialZoom(float) 363 */ 364 @Px getSourceWidth()365 public int getSourceWidth() { 366 return mSourceWidth; 367 } 368 369 /** 370 * @return the initial height of the content magnified and copied to the magnifier, in pixels 371 * @see Magnifier.Builder#setSize(int, int) 372 * @see Magnifier.Builder#setInitialZoom(float) 373 */ 374 @Px getSourceHeight()375 public int getSourceHeight() { 376 return mSourceHeight; 377 } 378 379 /** 380 * Sets the zoom to be applied to the chosen content before being copied to the magnifier popup. 381 * The change will become effective at the next #show or #update call. 382 * @param zoom the zoom to be set 383 */ setZoom(@loatRangefrom = 0f) float zoom)384 public void setZoom(@FloatRange(from = 0f) float zoom) { 385 Preconditions.checkArgumentPositive(zoom, "Zoom should be positive"); 386 mZoom = zoom; 387 mSourceWidth = Math.round(mWindowWidth / mZoom); 388 mSourceHeight = Math.round(mWindowHeight / mZoom); 389 mDirtyState = true; 390 } 391 392 /** 393 * Returns the zoom to be applied to the magnified view region copied to the magnifier. 394 * If the zoom is x and the magnifier window size is (width, height), the original size 395 * of the content being magnified will be (width / x, height / x). 396 * @return the zoom applied to the content 397 * @see Magnifier.Builder#setInitialZoom(float) 398 */ getZoom()399 public float getZoom() { 400 return mZoom; 401 } 402 403 /** 404 * @return the elevation set for the magnifier window, in pixels 405 * @see Magnifier.Builder#setElevation(float) 406 */ 407 @Px getElevation()408 public float getElevation() { 409 return mWindowElevation; 410 } 411 412 /** 413 * @return the corner radius of the magnifier window, in pixels 414 * @see Magnifier.Builder#setCornerRadius(float) 415 */ 416 @Px getCornerRadius()417 public float getCornerRadius() { 418 return mWindowCornerRadius; 419 } 420 421 /** 422 * Returns the horizontal offset, in pixels, to be applied to the source center position 423 * to obtain the magnifier center position when {@link #show(float, float)} is called. 424 * The value is ignored when {@link #show(float, float, float, float)} is used instead. 425 * 426 * @return the default horizontal offset between the source center and the magnifier 427 * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int) 428 * @see Magnifier#show(float, float) 429 */ 430 @Px getDefaultHorizontalSourceToMagnifierOffset()431 public int getDefaultHorizontalSourceToMagnifierOffset() { 432 return mDefaultHorizontalSourceToMagnifierOffset; 433 } 434 435 /** 436 * Returns the vertical offset, in pixels, to be applied to the source center position 437 * to obtain the magnifier center position when {@link #show(float, float)} is called. 438 * The value is ignored when {@link #show(float, float, float, float)} is used instead. 439 * 440 * @return the default vertical offset between the source center and the magnifier 441 * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int) 442 * @see Magnifier#show(float, float) 443 */ 444 @Px getDefaultVerticalSourceToMagnifierOffset()445 public int getDefaultVerticalSourceToMagnifierOffset() { 446 return mDefaultVerticalSourceToMagnifierOffset; 447 } 448 449 /** 450 * Returns the overlay to be drawn on the top of the magnifier, or 451 * {@code null} if no overlay should be drawn. 452 * @return the overlay 453 * @see Magnifier.Builder#setOverlay(Drawable) 454 */ 455 @Nullable getOverlay()456 public Drawable getOverlay() { 457 return mOverlay; 458 } 459 460 /** 461 * Returns whether the magnifier position will be adjusted such that the magnifier will be 462 * fully within the bounds of the main application window, by also avoiding any overlap 463 * with system insets (such as the one corresponding to the status bar) i.e. whether the 464 * area where the magnifier can be positioned will be clipped to the main application window 465 * and the system insets. 466 * @return whether the magnifier position will be adjusted 467 * @see Magnifier.Builder#setClippingEnabled(boolean) 468 */ isClippingEnabled()469 public boolean isClippingEnabled() { 470 return mClippingEnabled; 471 } 472 473 /** 474 * Returns the top left coordinates of the magnifier, relative to the main application 475 * window. They will be determined by the coordinates of the last {@link #show(float, float)} 476 * or {@link #show(float, float, float, float)} call, adjusted to take into account any 477 * potential clamping behavior. The method can be used immediately after a #show 478 * call to find out where the magnifier will be positioned. However, the position of the 479 * magnifier will not be updated visually in the same frame, due to the async nature of 480 * the content copying and of the magnifier rendering. 481 * The method will return {@code null} if #show has not yet been called, or if the last 482 * operation performed was a #dismiss. 483 * 484 * @return the top left coordinates of the magnifier 485 */ 486 @Nullable getPosition()487 public Point getPosition() { 488 if (mWindow == null) { 489 return null; 490 } 491 final Point position = getCurrentClampedWindowCoordinates(); 492 position.offset(-mParentSurface.mInsets.left, -mParentSurface.mInsets.top); 493 return new Point(position); 494 } 495 496 /** 497 * Returns the top left coordinates of the magnifier source (i.e. the view region going to 498 * be magnified and copied to the magnifier), relative to the window or surface the content 499 * is copied from. The content will be copied: 500 * - if the magnified view is a {@link SurfaceView}, from the surface backing it 501 * - otherwise, from the surface backing the main application window, and the coordinates 502 * returned will be relative to the main application window 503 * The method will return {@code null} if #show has not yet been called, or if the last 504 * operation performed was a #dismiss. 505 * 506 * @return the top left coordinates of the magnifier source 507 */ 508 @Nullable getSourcePosition()509 public Point getSourcePosition() { 510 if (mWindow == null) { 511 return null; 512 } 513 final Point position = new Point(mPixelCopyRequestRect.left, mPixelCopyRequestRect.top); 514 position.offset(-mContentCopySurface.mInsets.left, -mContentCopySurface.mInsets.top); 515 return new Point(position); 516 } 517 518 /** 519 * Retrieves the surfaces used by the magnifier: 520 * - a parent surface for the magnifier surface. This will usually be the main app window. 521 * - a surface where the magnified content will be copied from. This will be the main app 522 * window unless the magnified view is a SurfaceView, in which case its backing surface 523 * will be used. 524 */ obtainSurfaces()525 private void obtainSurfaces() { 526 // Get the main window surface. 527 SurfaceInfo validMainWindowSurface = SurfaceInfo.NULL; 528 if (mView.getViewRootImpl() != null) { 529 final ViewRootImpl viewRootImpl = mView.getViewRootImpl(); 530 final Surface mainWindowSurface = viewRootImpl.mSurface; 531 if (mainWindowSurface != null && mainWindowSurface.isValid()) { 532 final Rect surfaceInsets = viewRootImpl.mWindowAttributes.surfaceInsets; 533 final int surfaceWidth = 534 viewRootImpl.getWidth() + surfaceInsets.left + surfaceInsets.right; 535 final int surfaceHeight = 536 viewRootImpl.getHeight() + surfaceInsets.top + surfaceInsets.bottom; 537 validMainWindowSurface = 538 new SurfaceInfo(viewRootImpl.getSurfaceControl(), mainWindowSurface, 539 surfaceWidth, surfaceHeight, surfaceInsets, true); 540 } 541 } 542 // Get the surface backing the magnified view, if it is a SurfaceView. 543 SurfaceInfo validSurfaceViewSurface = SurfaceInfo.NULL; 544 if (mView instanceof SurfaceView) { 545 final SurfaceControl sc = ((SurfaceView) mView).getSurfaceControl(); 546 final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder(); 547 final Surface surfaceViewSurface = surfaceHolder.getSurface(); 548 549 if (sc != null && sc.isValid()) { 550 final Rect surfaceFrame = surfaceHolder.getSurfaceFrame(); 551 validSurfaceViewSurface = new SurfaceInfo(sc, surfaceViewSurface, 552 surfaceFrame.right, surfaceFrame.bottom, new Rect(), false); 553 } 554 } 555 556 // Choose the parent surface for the magnifier and the source surface for the content. 557 mParentSurface = validMainWindowSurface != SurfaceInfo.NULL 558 ? validMainWindowSurface : validSurfaceViewSurface; 559 mContentCopySurface = mView instanceof SurfaceView 560 ? validSurfaceViewSurface : validMainWindowSurface; 561 } 562 563 /** 564 * Computes the coordinates of the center of the content going to be displayed in the 565 * magnifier. These are relative to the surface the content is copied from. 566 */ obtainContentCoordinates(final float xPosInView, final float yPosInView)567 private void obtainContentCoordinates(final float xPosInView, final float yPosInView) { 568 final int prevViewXInSurface = mViewCoordinatesInSurface[0]; 569 final int prevViewYInSurface = mViewCoordinatesInSurface[1]; 570 mView.getLocationInSurface(mViewCoordinatesInSurface); 571 if (mViewCoordinatesInSurface[0] != prevViewXInSurface 572 || mViewCoordinatesInSurface[1] != prevViewYInSurface) { 573 mDirtyState = true; 574 } 575 576 final int zoomCenterX; 577 final int zoomCenterY; 578 if (mView instanceof SurfaceView) { 579 // No offset required if the backing Surface matches the size of the SurfaceView. 580 zoomCenterX = Math.round(xPosInView); 581 zoomCenterY = Math.round(yPosInView); 582 } else { 583 zoomCenterX = Math.round(xPosInView + mViewCoordinatesInSurface[0]); 584 zoomCenterY = Math.round(yPosInView + mViewCoordinatesInSurface[1]); 585 } 586 587 final Rect[] bounds = new Rect[2]; // [MAX_IN_SURFACE, MAX_VISIBLE] 588 // Obtain the surface bounds rectangle. 589 final Rect surfaceBounds = new Rect(0, 0, 590 mContentCopySurface.mWidth, mContentCopySurface.mHeight); 591 bounds[0] = surfaceBounds; 592 // Obtain the visible view region rectangle. 593 final Rect viewVisibleRegion = new Rect(); 594 mView.getGlobalVisibleRect(viewVisibleRegion); 595 if (mView.getViewRootImpl() != null) { 596 // Clamping coordinates relative to the surface, not to the window. 597 final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets; 598 viewVisibleRegion.offset(surfaceInsets.left, surfaceInsets.top); 599 } 600 if (mView instanceof SurfaceView) { 601 // If we copy content from a SurfaceView, clamp coordinates relative to it. 602 viewVisibleRegion.offset(-mViewCoordinatesInSurface[0], -mViewCoordinatesInSurface[1]); 603 } 604 bounds[1] = viewVisibleRegion; 605 606 // Aggregate the above to obtain the bounds where the content copy will be restricted. 607 int resolvedLeft = Integer.MIN_VALUE; 608 for (int i = mLeftContentBound; i >= 0; --i) { 609 resolvedLeft = Math.max(resolvedLeft, bounds[i].left); 610 } 611 int resolvedTop = Integer.MIN_VALUE; 612 for (int i = mTopContentBound; i >= 0; --i) { 613 resolvedTop = Math.max(resolvedTop, bounds[i].top); 614 } 615 int resolvedRight = Integer.MAX_VALUE; 616 for (int i = mRightContentBound; i >= 0; --i) { 617 resolvedRight = Math.min(resolvedRight, bounds[i].right); 618 } 619 int resolvedBottom = Integer.MAX_VALUE; 620 for (int i = mBottomContentBound; i >= 0; --i) { 621 resolvedBottom = Math.min(resolvedBottom, bounds[i].bottom); 622 } 623 // Adjust <left-right> and <top-bottom> pairs of bounds to make sense. 624 resolvedLeft = Math.min(resolvedLeft, mContentCopySurface.mWidth - mSourceWidth); 625 resolvedTop = Math.min(resolvedTop, mContentCopySurface.mHeight - mSourceHeight); 626 if (resolvedLeft < 0 || resolvedTop < 0) { 627 Log.e(TAG, "Magnifier's content is copied from a surface smaller than" 628 + "the content requested size. The magnifier will be dismissed."); 629 } 630 resolvedRight = Math.max(resolvedRight, resolvedLeft + mSourceWidth); 631 resolvedBottom = Math.max(resolvedBottom, resolvedTop + mSourceHeight); 632 633 // Finally compute the coordinates of the source center. 634 mClampedCenterZoomCoords.x = Math.max(resolvedLeft + mSourceWidth / 2, Math.min( 635 zoomCenterX, resolvedRight - mSourceWidth / 2)); 636 mClampedCenterZoomCoords.y = Math.max(resolvedTop + mSourceHeight / 2, Math.min( 637 zoomCenterY, resolvedBottom - mSourceHeight / 2)); 638 } 639 640 /** 641 * Computes the coordinates of the top left corner of the magnifier window. 642 * These are relative to the surface the magnifier window is attached to. 643 */ obtainWindowCoordinates(final float xWindowPos, final float yWindowPos)644 private void obtainWindowCoordinates(final float xWindowPos, final float yWindowPos) { 645 final int windowCenterX; 646 final int windowCenterY; 647 if (mView instanceof SurfaceView) { 648 // No offset required if the backing Surface matches the size of the SurfaceView. 649 windowCenterX = Math.round(xWindowPos); 650 windowCenterY = Math.round(yWindowPos); 651 } else { 652 windowCenterX = Math.round(xWindowPos + mViewCoordinatesInSurface[0]); 653 windowCenterY = Math.round(yWindowPos + mViewCoordinatesInSurface[1]); 654 } 655 656 mWindowCoords.x = windowCenterX - mWindowWidth / 2; 657 mWindowCoords.y = windowCenterY - mWindowHeight / 2; 658 if (mParentSurface != mContentCopySurface) { 659 mWindowCoords.x += mViewCoordinatesInSurface[0]; 660 mWindowCoords.y += mViewCoordinatesInSurface[1]; 661 } 662 } 663 performPixelCopy(final int startXInSurface, final int startYInSurface, final boolean updateWindowPosition)664 private void performPixelCopy(final int startXInSurface, final int startYInSurface, 665 final boolean updateWindowPosition) { 666 if (mContentCopySurface.mSurface == null || !mContentCopySurface.mSurface.isValid()) { 667 onPixelCopyFailed(); 668 return; 669 } 670 671 // Clamp window coordinates inside the parent surface, to avoid displaying 672 // the magnifier out of screen or overlapping with system insets. 673 final Point windowCoords = getCurrentClampedWindowCoordinates(); 674 675 // Perform the pixel copy. 676 mPixelCopyRequestRect.set(startXInSurface, 677 startYInSurface, 678 startXInSurface + mSourceWidth, 679 startYInSurface + mSourceHeight); 680 final InternalPopupWindow currentWindowInstance = mWindow; 681 final Bitmap bitmap = 682 Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ARGB_8888); 683 PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap, 684 result -> { 685 if (result != PixelCopy.SUCCESS) { 686 onPixelCopyFailed(); 687 return; 688 } 689 synchronized (mLock) { 690 if (mWindow != currentWindowInstance) { 691 // The magnifier was dismissed (and maybe shown again) in the meantime. 692 return; 693 } 694 if (updateWindowPosition) { 695 // TODO: pull the position update outside #performPixelCopy 696 mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y); 697 } 698 mWindow.updateContent(bitmap); 699 } 700 }, 701 sPixelCopyHandlerThread.getThreadHandler()); 702 mPrevStartCoordsInSurface.x = startXInSurface; 703 mPrevStartCoordsInSurface.y = startYInSurface; 704 mDirtyState = false; 705 } 706 onPixelCopyFailed()707 private void onPixelCopyFailed() { 708 Log.e(TAG, "Magnifier failed to copy content from the view Surface. It will be dismissed."); 709 // Post to make sure #dismiss is done on the main thread. 710 Handler.getMain().postAtFrontOfQueue(() -> { 711 dismiss(); 712 if (mCallback != null) { 713 mCallback.onOperationComplete(); 714 } 715 }); 716 } 717 718 /** 719 * Clamp window coordinates inside the surface the magnifier is attached to, to avoid 720 * displaying the magnifier out of screen or overlapping with system insets. 721 * @return the current window coordinates, after they are clamped inside the parent surface 722 */ getCurrentClampedWindowCoordinates()723 private Point getCurrentClampedWindowCoordinates() { 724 if (!mClippingEnabled) { 725 // No position adjustment should be done, so return the raw coordinates. 726 return new Point(mWindowCoords); 727 } 728 729 final Rect windowBounds; 730 if (mParentSurface.mIsMainWindowSurface) { 731 final Insets systemInsets = mView.getRootWindowInsets().getSystemWindowInsets(); 732 windowBounds = new Rect( 733 systemInsets.left + mParentSurface.mInsets.left, 734 systemInsets.top + mParentSurface.mInsets.top, 735 mParentSurface.mWidth - systemInsets.right - mParentSurface.mInsets.right, 736 mParentSurface.mHeight - systemInsets.bottom 737 - mParentSurface.mInsets.bottom 738 ); 739 } else { 740 windowBounds = new Rect(0, 0, mParentSurface.mWidth, mParentSurface.mHeight); 741 } 742 final int windowCoordsX = Math.max(windowBounds.left, 743 Math.min(windowBounds.right - mWindowWidth, mWindowCoords.x)); 744 final int windowCoordsY = Math.max(windowBounds.top, 745 Math.min(windowBounds.bottom - mWindowHeight, mWindowCoords.y)); 746 return new Point(windowCoordsX, windowCoordsY); 747 } 748 749 /** 750 * Contains a surface and metadata corresponding to it. 751 */ 752 private static class SurfaceInfo { 753 public static final SurfaceInfo NULL = new SurfaceInfo(null, null, 0, 0, null, false); 754 755 private Surface mSurface; 756 private SurfaceControl mSurfaceControl; 757 private int mWidth; 758 private int mHeight; 759 private Rect mInsets; 760 private boolean mIsMainWindowSurface; 761 SurfaceInfo(final SurfaceControl surfaceControl, final Surface surface, final int width, final int height, final Rect insets, final boolean isMainWindowSurface)762 SurfaceInfo(final SurfaceControl surfaceControl, final Surface surface, 763 final int width, final int height, final Rect insets, 764 final boolean isMainWindowSurface) { 765 mSurfaceControl = surfaceControl; 766 mSurface = surface; 767 mWidth = width; 768 mHeight = height; 769 mInsets = insets; 770 mIsMainWindowSurface = isMainWindowSurface; 771 } 772 } 773 774 /** 775 * Magnifier's own implementation of PopupWindow-similar floating window. 776 * This exists to ensure frame-synchronization between window position updates and window 777 * content updates. By using a PopupWindow, these events would happen in different frames, 778 * producing a shakiness effect for the magnifier content. 779 */ 780 private static class InternalPopupWindow { 781 // The z of the magnifier surface, defining its z order in the list of 782 // siblings having the same parent surface (usually the main app surface). 783 private static final int SURFACE_Z = 5; 784 785 // Display associated to the view the magnifier is attached to. 786 private final Display mDisplay; 787 // The size of the content of the magnifier. 788 private final int mContentWidth; 789 private final int mContentHeight; 790 // The size of the allocated surface. 791 private final int mSurfaceWidth; 792 private final int mSurfaceHeight; 793 // The insets of the content inside the allocated surface. 794 private final int mOffsetX; 795 private final int mOffsetY; 796 // The overlay to be drawn on the top of the content. 797 private final Drawable mOverlay; 798 // The surface we allocate for the magnifier content + shadow. 799 private final SurfaceSession mSurfaceSession; 800 private final SurfaceControl mSurfaceControl; 801 private final Surface mSurface; 802 // The renderer used for the allocated surface. 803 private final ThreadedRenderer.SimpleRenderer mRenderer; 804 // The RenderNode used to draw the magnifier content in the surface. 805 private final RenderNode mBitmapRenderNode; 806 // The RenderNode used to draw the overlay over the magnifier content. 807 private final RenderNode mOverlayRenderNode; 808 // The job that will be post'd to apply the pending magnifier updates to the surface. 809 private final Runnable mMagnifierUpdater; 810 // The handler where the magnifier updater jobs will be post'd. 811 private final Handler mHandler; 812 // The callback to be run after the next draw. 813 private Callback mCallback; 814 // The position of the magnifier content when the last draw was requested. 815 private int mLastDrawContentPositionX; 816 private int mLastDrawContentPositionY; 817 818 // Members below describe the state of the magnifier. Reads/writes to them 819 // have to be synchronized between the UI thread and the thread that handles 820 // the pixel copy results. This is the purpose of mLock. 821 private final Object mLock; 822 // Whether a magnifier frame draw is currently pending in the UI thread queue. 823 private boolean mFrameDrawScheduled; 824 // The content bitmap, as returned by pixel copy. 825 private Bitmap mBitmap; 826 // Whether the next draw will be the first one for the current instance. 827 private boolean mFirstDraw = true; 828 // The window position in the parent surface. Might be applied during the next draw, 829 // when mPendingWindowPositionUpdate is true. 830 private int mWindowPositionX; 831 private int mWindowPositionY; 832 private boolean mPendingWindowPositionUpdate; 833 834 // The current content of the magnifier. It is mBitmap + mOverlay, only used for testing. 835 private Bitmap mCurrentContent; 836 InternalPopupWindow(final Context context, final Display display, final SurfaceControl parentSurfaceControl, final int width, final int height, final float elevation, final float cornerRadius, final Drawable overlay, final Handler handler, final Object lock, final Callback callback)837 InternalPopupWindow(final Context context, final Display display, 838 final SurfaceControl parentSurfaceControl, final int width, final int height, 839 final float elevation, final float cornerRadius, final Drawable overlay, 840 final Handler handler, final Object lock, final Callback callback) { 841 mDisplay = display; 842 mOverlay = overlay; 843 mLock = lock; 844 mCallback = callback; 845 846 mContentWidth = width; 847 mContentHeight = height; 848 mOffsetX = (int) (1.05f * elevation); 849 mOffsetY = (int) (1.05f * elevation); 850 // Setup the surface we will use for drawing the content and shadow. 851 mSurfaceWidth = mContentWidth + 2 * mOffsetX; 852 mSurfaceHeight = mContentHeight + 2 * mOffsetY; 853 mSurfaceSession = new SurfaceSession(); 854 mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession) 855 .setFormat(PixelFormat.TRANSLUCENT) 856 .setBufferSize(mSurfaceWidth, mSurfaceHeight) 857 .setName("magnifier surface") 858 .setFlags(SurfaceControl.HIDDEN) 859 .setParent(parentSurfaceControl) 860 .build(); 861 mSurface = new Surface(); 862 mSurface.copyFrom(mSurfaceControl); 863 864 // Setup the RenderNode tree. The root has two children, one containing the bitmap 865 // and one containing the overlay. We use a separate render node for the overlay 866 // to avoid drawing this as the same rate we do for content. 867 mRenderer = new ThreadedRenderer.SimpleRenderer( 868 context, 869 "magnifier renderer", 870 mSurface 871 ); 872 mBitmapRenderNode = createRenderNodeForBitmap( 873 "magnifier content", 874 elevation, 875 cornerRadius 876 ); 877 mOverlayRenderNode = createRenderNodeForOverlay( 878 "magnifier overlay", 879 cornerRadius 880 ); 881 setupOverlay(); 882 883 final RecordingCanvas canvas = mRenderer.getRootNode().beginRecording(width, height); 884 try { 885 canvas.insertReorderBarrier(); 886 canvas.drawRenderNode(mBitmapRenderNode); 887 canvas.insertInorderBarrier(); 888 canvas.drawRenderNode(mOverlayRenderNode); 889 canvas.insertInorderBarrier(); 890 } finally { 891 mRenderer.getRootNode().endRecording(); 892 } 893 if (mCallback != null) { 894 mCurrentContent = 895 Bitmap.createBitmap(mContentWidth, mContentHeight, Bitmap.Config.ARGB_8888); 896 updateCurrentContentForTesting(); 897 } 898 899 // Initialize the update job and the handler where this will be post'd. 900 mHandler = handler; 901 mMagnifierUpdater = this::doDraw; 902 mFrameDrawScheduled = false; 903 } 904 createRenderNodeForBitmap(final String name, final float elevation, final float cornerRadius)905 private RenderNode createRenderNodeForBitmap(final String name, 906 final float elevation, final float cornerRadius) { 907 final RenderNode bitmapRenderNode = RenderNode.create(name, null); 908 909 // Define the position of the bitmap in the parent render node. The surface regions 910 // outside the bitmap are used to draw elevation. 911 bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY, 912 mOffsetX + mContentWidth, mOffsetY + mContentHeight); 913 bitmapRenderNode.setElevation(elevation); 914 915 final Outline outline = new Outline(); 916 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius); 917 outline.setAlpha(1.0f); 918 bitmapRenderNode.setOutline(outline); 919 bitmapRenderNode.setClipToOutline(true); 920 921 // Create a placeholder draw, which will be replaced later with real drawing. 922 final RecordingCanvas canvas = bitmapRenderNode.beginRecording( 923 mContentWidth, mContentHeight); 924 try { 925 canvas.drawColor(0xFF00FF00); 926 } finally { 927 bitmapRenderNode.endRecording(); 928 } 929 930 return bitmapRenderNode; 931 } 932 createRenderNodeForOverlay(final String name, final float cornerRadius)933 private RenderNode createRenderNodeForOverlay(final String name, final float cornerRadius) { 934 final RenderNode overlayRenderNode = RenderNode.create(name, null); 935 936 // Define the position of the overlay in the parent render node. 937 // This coincides with the position of the content. 938 overlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY, 939 mOffsetX + mContentWidth, mOffsetY + mContentHeight); 940 941 final Outline outline = new Outline(); 942 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius); 943 outline.setAlpha(1.0f); 944 overlayRenderNode.setOutline(outline); 945 overlayRenderNode.setClipToOutline(true); 946 947 return overlayRenderNode; 948 } 949 setupOverlay()950 private void setupOverlay() { 951 drawOverlay(); 952 953 mOverlay.setCallback(new Drawable.Callback() { 954 @Override 955 public void invalidateDrawable(Drawable who) { 956 // When the overlay drawable is invalidated, redraw it to the render node. 957 drawOverlay(); 958 if (mCallback != null) { 959 updateCurrentContentForTesting(); 960 } 961 } 962 963 @Override 964 public void scheduleDrawable(Drawable who, Runnable what, long when) { 965 Handler.getMain().postAtTime(what, who, when); 966 } 967 968 @Override 969 public void unscheduleDrawable(Drawable who, Runnable what) { 970 Handler.getMain().removeCallbacks(what, who); 971 } 972 }); 973 } 974 drawOverlay()975 private void drawOverlay() { 976 // Draw the drawable to the render node. This happens once during 977 // initialization and whenever the overlay drawable is invalidated. 978 final RecordingCanvas canvas = 979 mOverlayRenderNode.beginRecording(mContentWidth, mContentHeight); 980 try { 981 mOverlay.setBounds(0, 0, mContentWidth, mContentHeight); 982 mOverlay.draw(canvas); 983 } finally { 984 mOverlayRenderNode.endRecording(); 985 } 986 } 987 988 /** 989 * Sets the position of the magnifier content relative to the parent surface. 990 * The position update will happen in the same frame with the next draw. 991 * The method has to be called in a context that holds {@link #mLock}. 992 * 993 * @param contentX the x coordinate of the content 994 * @param contentY the y coordinate of the content 995 */ setContentPositionForNextDraw(final int contentX, final int contentY)996 public void setContentPositionForNextDraw(final int contentX, final int contentY) { 997 mWindowPositionX = contentX - mOffsetX; 998 mWindowPositionY = contentY - mOffsetY; 999 mPendingWindowPositionUpdate = true; 1000 requestUpdate(); 1001 } 1002 1003 /** 1004 * Sets the content that should be displayed in the magnifier. 1005 * The update happens immediately, and possibly triggers a pending window movement set 1006 * by {@link #setContentPositionForNextDraw(int, int)}. 1007 * The method has to be called in a context that holds {@link #mLock}. 1008 * 1009 * @param bitmap the content bitmap 1010 */ updateContent(final @NonNull Bitmap bitmap)1011 public void updateContent(final @NonNull Bitmap bitmap) { 1012 if (mBitmap != null) { 1013 mBitmap.recycle(); 1014 } 1015 mBitmap = bitmap; 1016 requestUpdate(); 1017 } 1018 requestUpdate()1019 private void requestUpdate() { 1020 if (mFrameDrawScheduled) { 1021 return; 1022 } 1023 final Message request = Message.obtain(mHandler, mMagnifierUpdater); 1024 request.setAsynchronous(true); 1025 request.sendToTarget(); 1026 mFrameDrawScheduled = true; 1027 } 1028 1029 /** 1030 * Destroys this instance. The method has to be called in a context holding {@link #mLock}. 1031 */ destroy()1032 public void destroy() { 1033 // Destroy the renderer. This will not proceed until pending frame callbacks complete. 1034 mRenderer.destroy(); 1035 mSurface.destroy(); 1036 new SurfaceControl.Transaction().remove(mSurfaceControl).apply(); 1037 mSurfaceSession.kill(); 1038 mHandler.removeCallbacks(mMagnifierUpdater); 1039 if (mBitmap != null) { 1040 mBitmap.recycle(); 1041 } 1042 } 1043 doDraw()1044 private void doDraw() { 1045 final ThreadedRenderer.FrameDrawingCallback callback; 1046 1047 // Draw the current bitmap to the surface, and prepare the callback which updates the 1048 // surface position. These have to be in the same synchronized block, in order to 1049 // guarantee the consistency between the bitmap content and the surface position. 1050 synchronized (mLock) { 1051 if (!mSurface.isValid()) { 1052 // Probably #destroy() was called for the current instance, so we skip the draw. 1053 return; 1054 } 1055 1056 final RecordingCanvas canvas = 1057 mBitmapRenderNode.beginRecording(mContentWidth, mContentHeight); 1058 try { 1059 final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 1060 final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight); 1061 final Paint paint = new Paint(); 1062 paint.setFilterBitmap(true); 1063 canvas.drawBitmap(mBitmap, srcRect, dstRect, paint); 1064 } finally { 1065 mBitmapRenderNode.endRecording(); 1066 } 1067 1068 if (mPendingWindowPositionUpdate || mFirstDraw) { 1069 // If the window has to be shown or moved, defer this until the next draw. 1070 final boolean firstDraw = mFirstDraw; 1071 mFirstDraw = false; 1072 final boolean updateWindowPosition = mPendingWindowPositionUpdate; 1073 mPendingWindowPositionUpdate = false; 1074 final int pendingX = mWindowPositionX; 1075 final int pendingY = mWindowPositionY; 1076 1077 callback = frame -> { 1078 if (!mSurface.isValid()) { 1079 return; 1080 } 1081 // Show or move the window at the content draw frame. 1082 SurfaceControl.openTransaction(); 1083 mSurfaceControl.deferTransactionUntil(mSurface, frame); 1084 if (updateWindowPosition) { 1085 mSurfaceControl.setPosition(pendingX, pendingY); 1086 } 1087 if (firstDraw) { 1088 mSurfaceControl.setLayer(SURFACE_Z); 1089 mSurfaceControl.show(); 1090 } 1091 SurfaceControl.closeTransaction(); 1092 }; 1093 mRenderer.setLightCenter(mDisplay, pendingX, pendingY); 1094 } else { 1095 callback = null; 1096 } 1097 1098 mLastDrawContentPositionX = mWindowPositionX + mOffsetX; 1099 mLastDrawContentPositionY = mWindowPositionY + mOffsetY; 1100 mFrameDrawScheduled = false; 1101 } 1102 1103 mRenderer.draw(callback); 1104 if (mCallback != null) { 1105 // The current content bitmap is only used in testing, so, for performance, 1106 // we only want to update it when running tests. For this, we check that 1107 // mCallback is not null, as it can only be set from a @TestApi. 1108 updateCurrentContentForTesting(); 1109 mCallback.onOperationComplete(); 1110 } 1111 } 1112 1113 /** 1114 * Updates mCurrentContent, which reproduces what is currently supposed to be 1115 * drawn in the magnifier. mCurrentContent is only used for testing, so this method 1116 * should only be called otherwise. 1117 */ updateCurrentContentForTesting()1118 private void updateCurrentContentForTesting() { 1119 final Canvas canvas = new Canvas(mCurrentContent); 1120 final Rect bounds = new Rect(0, 0, mContentWidth, mContentHeight); 1121 if (mBitmap != null && !mBitmap.isRecycled()) { 1122 final Rect originalBounds = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 1123 canvas.drawBitmap(mBitmap, originalBounds, bounds, null); 1124 } 1125 mOverlay.setBounds(bounds); 1126 mOverlay.draw(canvas); 1127 } 1128 } 1129 1130 /** 1131 * Builder class for {@link Magnifier} objects. 1132 */ 1133 public static final class Builder { 1134 private @NonNull View mView; 1135 private @Px @IntRange(from = 0) int mWidth; 1136 private @Px @IntRange(from = 0) int mHeight; 1137 private float mZoom; 1138 private @FloatRange(from = 0f) float mElevation; 1139 private @FloatRange(from = 0f) float mCornerRadius; 1140 private @Nullable Drawable mOverlay; 1141 private int mHorizontalDefaultSourceToMagnifierOffset; 1142 private int mVerticalDefaultSourceToMagnifierOffset; 1143 private boolean mClippingEnabled; 1144 private @SourceBound int mLeftContentBound; 1145 private @SourceBound int mTopContentBound; 1146 private @SourceBound int mRightContentBound; 1147 private @SourceBound int mBottomContentBound; 1148 1149 /** 1150 * Construct a new builder for {@link Magnifier} objects. 1151 * @param view the view this magnifier is attached to 1152 */ Builder(@onNull View view)1153 public Builder(@NonNull View view) { 1154 mView = Preconditions.checkNotNull(view); 1155 applyDefaults(); 1156 } 1157 applyDefaults()1158 private void applyDefaults() { 1159 final Resources resources = mView.getContext().getResources(); 1160 mWidth = resources.getDimensionPixelSize(R.dimen.default_magnifier_width); 1161 mHeight = resources.getDimensionPixelSize(R.dimen.default_magnifier_height); 1162 mElevation = resources.getDimension(R.dimen.default_magnifier_elevation); 1163 mCornerRadius = resources.getDimension(R.dimen.default_magnifier_corner_radius); 1164 mZoom = resources.getFloat(R.dimen.default_magnifier_zoom); 1165 mHorizontalDefaultSourceToMagnifierOffset = 1166 resources.getDimensionPixelSize(R.dimen.default_magnifier_horizontal_offset); 1167 mVerticalDefaultSourceToMagnifierOffset = 1168 resources.getDimensionPixelSize(R.dimen.default_magnifier_vertical_offset); 1169 mOverlay = new ColorDrawable(resources.getColor( 1170 R.color.default_magnifier_color_overlay, null)); 1171 mClippingEnabled = true; 1172 mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE; 1173 mTopContentBound = SOURCE_BOUND_MAX_VISIBLE; 1174 mRightContentBound = SOURCE_BOUND_MAX_VISIBLE; 1175 mBottomContentBound = SOURCE_BOUND_MAX_VISIBLE; 1176 } 1177 1178 /** 1179 * Sets the size of the magnifier window, in pixels. Defaults to (100dp, 48dp). 1180 * Note that the size of the content being magnified and copied to the magnifier 1181 * will be computed as (window width / zoom, window height / zoom). 1182 * @param width the window width to be set 1183 * @param height the window height to be set 1184 */ 1185 @NonNull setSize(@x @ntRangefrom = 0) int width, @Px @IntRange(from = 0) int height)1186 public Builder setSize(@Px @IntRange(from = 0) int width, 1187 @Px @IntRange(from = 0) int height) { 1188 Preconditions.checkArgumentPositive(width, "Width should be positive"); 1189 Preconditions.checkArgumentPositive(height, "Height should be positive"); 1190 mWidth = width; 1191 mHeight = height; 1192 return this; 1193 } 1194 1195 /** 1196 * Sets the zoom to be applied to the chosen content before being copied to the magnifier. 1197 * A content of size (content_width, content_height) will be magnified to 1198 * (content_width * zoom, content_height * zoom), which will coincide with the size 1199 * of the magnifier. A zoom of 1 will translate to no magnification (the content will 1200 * be just copied to the magnifier with no scaling). The zoom defaults to 1.25. 1201 * Note that the zoom can also be changed after the instance is built, using the 1202 * {@link Magnifier#setZoom(float)} method. 1203 * @param zoom the zoom to be set 1204 */ 1205 @NonNull setInitialZoom(@loatRangefrom = 0f) float zoom)1206 public Builder setInitialZoom(@FloatRange(from = 0f) float zoom) { 1207 Preconditions.checkArgumentPositive(zoom, "Zoom should be positive"); 1208 mZoom = zoom; 1209 return this; 1210 } 1211 1212 /** 1213 * Sets the elevation of the magnifier window, in pixels. Defaults to 4dp. 1214 * @param elevation the elevation to be set 1215 */ 1216 @NonNull setElevation(@x @loatRangefrom = 0) float elevation)1217 public Builder setElevation(@Px @FloatRange(from = 0) float elevation) { 1218 Preconditions.checkArgumentNonNegative(elevation, "Elevation should be non-negative"); 1219 mElevation = elevation; 1220 return this; 1221 } 1222 1223 /** 1224 * Sets the corner radius of the magnifier window, in pixels. Defaults to 2dp. 1225 * @param cornerRadius the corner radius to be set 1226 */ 1227 @NonNull setCornerRadius(@x @loatRangefrom = 0) float cornerRadius)1228 public Builder setCornerRadius(@Px @FloatRange(from = 0) float cornerRadius) { 1229 Preconditions.checkArgumentNonNegative(cornerRadius, 1230 "Corner radius should be non-negative"); 1231 mCornerRadius = cornerRadius; 1232 return this; 1233 } 1234 1235 /** 1236 * Sets an overlay that will be drawn on the top of the magnifier. 1237 * In general, the overlay should not be opaque, in order to let the magnified 1238 * content be partially visible in the magnifier. The default overlay is {@code null} 1239 * (no overlay). As an example, TextView applies a white {@link ColorDrawable} 1240 * overlay with 5% alpha, aiming to make the magnifier distinguishable when shown in dark 1241 * application regions. To disable the overlay, the parameter should be set 1242 * to {@code null}. If not null, the overlay will be automatically redrawn 1243 * when the drawable is invalidated. To achieve this, the magnifier will set a new 1244 * {@link android.graphics.drawable.Drawable.Callback} for the overlay drawable, 1245 * so keep in mind that any existing one set by the application will be lost. 1246 * @param overlay the overlay to be drawn on top 1247 */ 1248 @NonNull setOverlay(@ullable Drawable overlay)1249 public Builder setOverlay(@Nullable Drawable overlay) { 1250 mOverlay = overlay; 1251 return this; 1252 } 1253 1254 /** 1255 * Sets an offset that should be added to the content source center to obtain 1256 * the position of the magnifier window, when the {@link #show(float, float)} 1257 * method is called. The offset is ignored when {@link #show(float, float, float, float)} 1258 * is used. The offset can be negative. It defaults to (0dp, 0dp). 1259 * @param horizontalOffset the horizontal component of the offset 1260 * @param verticalOffset the vertical component of the offset 1261 */ 1262 @NonNull setDefaultSourceToMagnifierOffset(@x int horizontalOffset, @Px int verticalOffset)1263 public Builder setDefaultSourceToMagnifierOffset(@Px int horizontalOffset, 1264 @Px int verticalOffset) { 1265 mHorizontalDefaultSourceToMagnifierOffset = horizontalOffset; 1266 mVerticalDefaultSourceToMagnifierOffset = verticalOffset; 1267 return this; 1268 } 1269 1270 /** 1271 * Defines the behavior of the magnifier when it is requested to position outside the 1272 * surface of the main application window. The default value is {@code true}, which means 1273 * that the position will be adjusted such that the magnifier will be fully within the 1274 * bounds of the main application window, while also avoiding any overlap with system insets 1275 * (such as the one corresponding to the status bar). If this flag is set to {@code false}, 1276 * the area where the magnifier can be positioned will no longer be clipped, so the 1277 * magnifier will be able to extend outside the main application window boundaries (and also 1278 * overlap the system insets). This can be useful if you require a custom behavior, but it 1279 * should be handled with care, when passing coordinates to {@link #show(float, float)}; 1280 * note that: 1281 * <ul> 1282 * <li>in a multiwindow context, if the magnifier crosses the boundary between the two 1283 * windows, it will not be able to show over the window of the other application</li> 1284 * <li>if the magnifier overlaps the status bar, there is no guarantee about which one 1285 * will be displayed on top. This should be handled with care.</li> 1286 * </ul> 1287 * @param clip whether the magnifier position will be adjusted 1288 */ 1289 @NonNull setClippingEnabled(boolean clip)1290 public Builder setClippingEnabled(boolean clip) { 1291 mClippingEnabled = clip; 1292 return this; 1293 } 1294 1295 /** 1296 * Defines the bounds of the rectangle where the magnifier will be able to copy its content 1297 * from. The content will always be copied from the {@link Surface} of the main application 1298 * window unless the magnified view is a {@link SurfaceView}, in which case its backing 1299 * surface will be used. Each bound can have a different behavior, with the options being: 1300 * <ul> 1301 * <li>{@link #SOURCE_BOUND_MAX_VISIBLE}, which extends the bound as much as possible 1302 * while remaining in the visible region of the magnified view, as given by 1303 * {@link android.view.View#getGlobalVisibleRect(Rect)}. For example, this will take into 1304 * account the case when the view is contained in a scrollable container, and the 1305 * magnifier will refuse to copy content outside of the visible view region</li> 1306 * <li>{@link #SOURCE_BOUND_MAX_IN_SURFACE}, which extends the bound as much 1307 * as possible while remaining inside the surface the content is copied from.</li> 1308 * </ul> 1309 * Note that if either of the first three options is used, the bound will be compared to 1310 * the bound of the surface (i.e. as if {@link #SOURCE_BOUND_MAX_IN_SURFACE} was used), 1311 * and the more restrictive one will be chosen. In other words, no attempt to copy content 1312 * from outside the surface will be permitted. If two opposite bounds are not well-behaved 1313 * (i.e. left + sourceWidth > right or top + sourceHeight > bottom), the left and top 1314 * bounds will have priority and the others will be extended accordingly. If the pairs 1315 * obtained this way still remain out of bounds, the smallest possible offset will be added 1316 * to the pairs to bring them inside the surface bounds. If this is impossible 1317 * (i.e. the surface is too small for the size of the content we try to copy on either 1318 * dimension), an error will be logged and the magnifier content will look distorted. 1319 * The default values assumed by the builder for the source bounds are 1320 * left: {@link #SOURCE_BOUND_MAX_VISIBLE}, top: {@link #SOURCE_BOUND_MAX_IN_SURFACE}, 1321 * right: {@link #SOURCE_BOUND_MAX_VISIBLE}, bottom: {@link #SOURCE_BOUND_MAX_IN_SURFACE}. 1322 * @param left the left bound for content copy 1323 * @param top the top bound for content copy 1324 * @param right the right bound for content copy 1325 * @param bottom the bottom bound for content copy 1326 */ 1327 @NonNull setSourceBounds(@ourceBound int left, @SourceBound int top, @SourceBound int right, @SourceBound int bottom)1328 public Builder setSourceBounds(@SourceBound int left, @SourceBound int top, 1329 @SourceBound int right, @SourceBound int bottom) { 1330 mLeftContentBound = left; 1331 mTopContentBound = top; 1332 mRightContentBound = right; 1333 mBottomContentBound = bottom; 1334 return this; 1335 } 1336 1337 /** 1338 * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}. 1339 */ build()1340 public @NonNull Magnifier build() { 1341 return new Magnifier(this); 1342 } 1343 } 1344 1345 /** 1346 * A source bound that will extend as much as possible, while remaining within the surface 1347 * the content is copied from. 1348 */ 1349 public static final int SOURCE_BOUND_MAX_IN_SURFACE = 0; 1350 1351 /** 1352 * A source bound that will extend as much as possible, while remaining within the 1353 * visible region of the magnified view, as determined by 1354 * {@link View#getGlobalVisibleRect(Rect)}. 1355 */ 1356 public static final int SOURCE_BOUND_MAX_VISIBLE = 1; 1357 1358 1359 /** 1360 * Used to describe the {@link Surface} rectangle where the magnifier's content is allowed 1361 * to be copied from. For more details, see method 1362 * {@link Magnifier.Builder#setSourceBounds(int, int, int, int)} 1363 * 1364 * @hide 1365 */ 1366 @IntDef({SOURCE_BOUND_MAX_IN_SURFACE, SOURCE_BOUND_MAX_VISIBLE}) 1367 @Retention(RetentionPolicy.SOURCE) 1368 public @interface SourceBound {} 1369 1370 // The rest of the file consists of test APIs and methods relevant for tests. 1371 1372 /** 1373 * See {@link #setOnOperationCompleteCallback(Callback)}. 1374 */ 1375 @TestApi 1376 private Callback mCallback; 1377 1378 /** 1379 * Sets a callback which will be invoked at the end of the next 1380 * {@link #show(float, float)} or {@link #update()} operation. 1381 * 1382 * @hide 1383 */ 1384 @TestApi setOnOperationCompleteCallback(final Callback callback)1385 public void setOnOperationCompleteCallback(final Callback callback) { 1386 mCallback = callback; 1387 if (mWindow != null) { 1388 mWindow.mCallback = callback; 1389 } 1390 } 1391 1392 /** 1393 * @return the drawing being currently displayed in the magnifier, as bitmap 1394 * 1395 * @hide 1396 */ 1397 @TestApi getContent()1398 public @Nullable Bitmap getContent() { 1399 if (mWindow == null) { 1400 return null; 1401 } 1402 synchronized (mWindow.mLock) { 1403 return mWindow.mCurrentContent; 1404 } 1405 } 1406 1407 /** 1408 * Returns a bitmap containing the content that was magnified and drew to the 1409 * magnifier, at its original size, without the overlay applied. 1410 * @return the content that is magnified, as bitmap 1411 * 1412 * @hide 1413 */ 1414 @TestApi getOriginalContent()1415 public @Nullable Bitmap getOriginalContent() { 1416 if (mWindow == null) { 1417 return null; 1418 } 1419 synchronized (mWindow.mLock) { 1420 return Bitmap.createBitmap(mWindow.mBitmap); 1421 } 1422 } 1423 1424 /** 1425 * @return the size of the magnifier window in dp 1426 * 1427 * @hide 1428 */ 1429 @TestApi getMagnifierDefaultSize()1430 public static PointF getMagnifierDefaultSize() { 1431 final Resources resources = Resources.getSystem(); 1432 final float density = resources.getDisplayMetrics().density; 1433 final PointF size = new PointF(); 1434 size.x = resources.getDimension(R.dimen.default_magnifier_width) / density; 1435 size.y = resources.getDimension(R.dimen.default_magnifier_height) / density; 1436 return size; 1437 } 1438 1439 /** 1440 * @hide 1441 */ 1442 @TestApi 1443 public interface Callback { 1444 /** 1445 * Callback called after the drawing for a magnifier update has happened. 1446 */ onOperationComplete()1447 void onOperationComplete(); 1448 } 1449 } 1450