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