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