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