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", mBbqSurfaceControl, 1025 surfaceWidth, surfaceHeight, PixelFormat.TRANSLUCENT); 1026 mSurface = mBBQ.createSurface(); 1027 1028 // Setup the RenderNode tree. The root has two children, one containing the bitmap 1029 // and one containing the overlay. We use a separate render node for the overlay 1030 // to avoid drawing this as the same rate we do for content. 1031 mRenderer = new ThreadedRenderer.SimpleRenderer( 1032 context, 1033 "magnifier renderer", 1034 mSurface 1035 ); 1036 mBitmapRenderNode = createRenderNodeForBitmap( 1037 "magnifier content", 1038 elevation, 1039 cornerRadius 1040 ); 1041 mOverlayRenderNode = createRenderNodeForOverlay( 1042 "magnifier overlay", 1043 cornerRadius 1044 ); 1045 setupOverlay(); 1046 1047 final RecordingCanvas canvas = mRenderer.getRootNode().beginRecording(width, height); 1048 try { 1049 canvas.enableZ(); 1050 canvas.drawRenderNode(mBitmapRenderNode); 1051 canvas.disableZ(); 1052 canvas.drawRenderNode(mOverlayRenderNode); 1053 canvas.disableZ(); 1054 } finally { 1055 mRenderer.getRootNode().endRecording(); 1056 } 1057 if (mCallback != null) { 1058 mCurrentContent = 1059 Bitmap.createBitmap(mContentWidth, mContentHeight, Bitmap.Config.ARGB_8888); 1060 updateCurrentContentForTesting(); 1061 } 1062 1063 // Initialize the update job and the handler where this will be post'd. 1064 mHandler = handler; 1065 mMagnifierUpdater = this::doDraw; 1066 mFrameDrawScheduled = false; 1067 mIsFishEyeStyle = isFishEyeStyle; 1068 1069 if (mIsFishEyeStyle) { 1070 createMeshMatrixForFishEyeEffect(); 1071 } 1072 } 1073 1074 /** 1075 * Updates the factors of content which may resize the window. 1076 * @param contentHeight the new height of content. 1077 * @param zoom the new zoom factor. 1078 */ updateContentFactors(final int contentHeight, final float zoom)1079 private void updateContentFactors(final int contentHeight, final float zoom) { 1080 if (mContentHeight == contentHeight && mZoom == zoom) { 1081 return; 1082 } 1083 if (mContentHeight < contentHeight) { 1084 // Grows the surface height as necessary. 1085 mBBQ.update(mBbqSurfaceControl, mContentWidth, contentHeight, 1086 PixelFormat.TRANSLUCENT); 1087 mRenderer.setSurface(mSurface); 1088 1089 final Outline outline = new Outline(); 1090 outline.setRoundRect(0, 0, mContentWidth, contentHeight, 0); 1091 outline.setAlpha(1.0f); 1092 1093 mBitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY, 1094 mOffsetX + mContentWidth, mOffsetY + contentHeight); 1095 mBitmapRenderNode.setOutline(outline); 1096 1097 mOverlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY, 1098 mOffsetX + mContentWidth, mOffsetY + contentHeight); 1099 mOverlayRenderNode.setOutline(outline); 1100 1101 final RecordingCanvas canvas = 1102 mRenderer.getRootNode().beginRecording(mContentWidth, contentHeight); 1103 try { 1104 canvas.enableZ(); 1105 canvas.drawRenderNode(mBitmapRenderNode); 1106 canvas.disableZ(); 1107 canvas.drawRenderNode(mOverlayRenderNode); 1108 canvas.disableZ(); 1109 } finally { 1110 mRenderer.getRootNode().endRecording(); 1111 } 1112 } 1113 mContentHeight = contentHeight; 1114 mZoom = zoom; 1115 fillMeshMatrix(); 1116 } 1117 createMeshMatrixForFishEyeEffect()1118 private void createMeshMatrixForFishEyeEffect() { 1119 mMeshWidth = 1; 1120 mMeshHeight = 6; 1121 mMeshLeft = new float[2 * (mMeshWidth + 1) * (mMeshHeight + 1)]; 1122 mMeshRight = new float[2 * (mMeshWidth + 1) * (mMeshHeight + 1)]; 1123 fillMeshMatrix(); 1124 } 1125 fillMeshMatrix()1126 private void fillMeshMatrix() { 1127 mMeshWidth = 1; 1128 mMeshHeight = 6; 1129 final float w = mContentWidth; 1130 final float h = mContentHeight; 1131 final float h0 = h / mZoom; 1132 final float dh = h - h0; 1133 for (int i = 0; i < 2 * (mMeshWidth + 1) * (mMeshHeight + 1); i += 2) { 1134 // Calculates X value. 1135 final int colIndex = i % (2 * (mMeshWidth + 1)) / 2; 1136 mMeshLeft[i] = (float) colIndex * mRamp / mMeshWidth; 1137 mMeshRight[i] = w - mRamp + colIndex * mRamp / mMeshWidth; 1138 1139 // Calculates Y value. 1140 final int rowIndex = i / 2 / (mMeshWidth + 1); 1141 final float hl = h0 + dh * colIndex / mMeshWidth; 1142 final float yl = (h - hl) / 2; 1143 mMeshLeft[i + 1] = yl + hl * rowIndex / mMeshHeight; 1144 final float hr = h - dh * colIndex / mMeshWidth; 1145 final float yr = (h - hr) / 2; 1146 mMeshRight[i + 1] = yr + hr * rowIndex / mMeshHeight; 1147 } 1148 } 1149 createRenderNodeForBitmap(final String name, final float elevation, final float cornerRadius)1150 private RenderNode createRenderNodeForBitmap(final String name, 1151 final float elevation, final float cornerRadius) { 1152 final RenderNode bitmapRenderNode = RenderNode.create(name, null); 1153 1154 // Define the position of the bitmap in the parent render node. The surface regions 1155 // outside the bitmap are used to draw elevation. 1156 bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY, 1157 mOffsetX + mContentWidth, mOffsetY + mContentHeight); 1158 bitmapRenderNode.setElevation(elevation); 1159 1160 final Outline outline = new Outline(); 1161 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius); 1162 outline.setAlpha(1.0f); 1163 bitmapRenderNode.setOutline(outline); 1164 bitmapRenderNode.setClipToOutline(true); 1165 1166 // Create a placeholder draw, which will be replaced later with real drawing. 1167 final RecordingCanvas canvas = bitmapRenderNode.beginRecording( 1168 mContentWidth, mContentHeight); 1169 try { 1170 canvas.drawColor(0xFF00FF00); 1171 } finally { 1172 bitmapRenderNode.endRecording(); 1173 } 1174 1175 return bitmapRenderNode; 1176 } 1177 createRenderNodeForOverlay(final String name, final float cornerRadius)1178 private RenderNode createRenderNodeForOverlay(final String name, final float cornerRadius) { 1179 final RenderNode overlayRenderNode = RenderNode.create(name, null); 1180 1181 // Define the position of the overlay in the parent render node. 1182 // This coincides with the position of the content. 1183 overlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY, 1184 mOffsetX + mContentWidth, mOffsetY + mContentHeight); 1185 1186 final Outline outline = new Outline(); 1187 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius); 1188 outline.setAlpha(1.0f); 1189 overlayRenderNode.setOutline(outline); 1190 overlayRenderNode.setClipToOutline(true); 1191 1192 return overlayRenderNode; 1193 } 1194 setupOverlay()1195 private void setupOverlay() { 1196 drawOverlay(); 1197 1198 mOverlay.setCallback(new Drawable.Callback() { 1199 @Override 1200 public void invalidateDrawable(Drawable who) { 1201 // When the overlay drawable is invalidated, redraw it to the render node. 1202 drawOverlay(); 1203 if (mCallback != null) { 1204 updateCurrentContentForTesting(); 1205 } 1206 } 1207 1208 @Override 1209 public void scheduleDrawable(Drawable who, Runnable what, long when) { 1210 Handler.getMain().postAtTime(what, who, when); 1211 } 1212 1213 @Override 1214 public void unscheduleDrawable(Drawable who, Runnable what) { 1215 Handler.getMain().removeCallbacks(what, who); 1216 } 1217 }); 1218 } 1219 drawOverlay()1220 private void drawOverlay() { 1221 // Draw the drawable to the render node. This happens once during 1222 // initialization and whenever the overlay drawable is invalidated. 1223 final RecordingCanvas canvas = 1224 mOverlayRenderNode.beginRecording(mContentWidth, mContentHeight); 1225 try { 1226 mOverlay.setBounds(0, 0, mContentWidth, mContentHeight); 1227 mOverlay.draw(canvas); 1228 } finally { 1229 mOverlayRenderNode.endRecording(); 1230 } 1231 } 1232 1233 /** 1234 * Sets the position of the magnifier content relative to the parent surface. 1235 * The position update will happen in the same frame with the next draw. 1236 * The method has to be called in a context that holds {@link #mLock}. 1237 * 1238 * @param contentX the x coordinate of the content 1239 * @param contentY the y coordinate of the content 1240 */ setContentPositionForNextDraw(final int contentX, final int contentY)1241 public void setContentPositionForNextDraw(final int contentX, final int contentY) { 1242 mWindowPositionX = contentX - mOffsetX; 1243 mWindowPositionY = contentY - mOffsetY; 1244 mPendingWindowPositionUpdate = true; 1245 requestUpdate(); 1246 } 1247 1248 /** 1249 * Sets the content that should be displayed in the magnifier. 1250 * The update happens immediately, and possibly triggers a pending window movement set 1251 * by {@link #setContentPositionForNextDraw(int, int)}. 1252 * The method has to be called in a context that holds {@link #mLock}. 1253 * 1254 * @param bitmap the content bitmap 1255 */ updateContent(final @NonNull Bitmap bitmap)1256 public void updateContent(final @NonNull Bitmap bitmap) { 1257 if (mBitmap != null) { 1258 mBitmap.recycle(); 1259 } 1260 mBitmap = bitmap; 1261 requestUpdate(); 1262 } 1263 requestUpdate()1264 private void requestUpdate() { 1265 if (mFrameDrawScheduled) { 1266 return; 1267 } 1268 final Message request = Message.obtain(mHandler, mMagnifierUpdater); 1269 request.setAsynchronous(true); 1270 request.sendToTarget(); 1271 mFrameDrawScheduled = true; 1272 } 1273 1274 /** 1275 * Destroys this instance. The method has to be called in a context holding {@link #mLock}. 1276 */ destroy()1277 public void destroy() { 1278 // Destroy the renderer. This will not proceed until pending frame callbacks complete. 1279 mRenderer.destroy(); 1280 mSurface.destroy(); 1281 mBBQ.destroy(); 1282 new SurfaceControl.Transaction() 1283 .remove(mSurfaceControl) 1284 .remove(mBbqSurfaceControl) 1285 .apply(); 1286 mSurfaceSession.kill(); 1287 mHandler.removeCallbacks(mMagnifierUpdater); 1288 if (mBitmap != null) { 1289 mBitmap.recycle(); 1290 } 1291 mOverlay.setCallback(null); 1292 } 1293 doDraw()1294 private void doDraw() { 1295 final ThreadedRenderer.FrameDrawingCallback callback; 1296 1297 // Draw the current bitmap to the surface, and prepare the callback which updates the 1298 // surface position. These have to be in the same synchronized block, in order to 1299 // guarantee the consistency between the bitmap content and the surface position. 1300 synchronized (mLock) { 1301 if (!mSurface.isValid()) { 1302 // Probably #destroy() was called for the current instance, so we skip the draw. 1303 return; 1304 } 1305 1306 final RecordingCanvas canvas = 1307 mBitmapRenderNode.beginRecording(mContentWidth, mContentHeight); 1308 try { 1309 final int w = mBitmap.getWidth(); 1310 final int h = mBitmap.getHeight(); 1311 final Paint paint = new Paint(); 1312 paint.setFilterBitmap(true); 1313 if (mIsFishEyeStyle) { 1314 final int margin = 1315 (int)((mContentWidth - (mContentWidth - 2 * mRamp) / mZoom) / 2); 1316 1317 // Draws the middle part. 1318 final Rect srcRect = new Rect(margin, 0, w - margin, h); 1319 final Rect dstRect = new Rect( 1320 mRamp, 0, mContentWidth - mRamp, mContentHeight); 1321 canvas.drawBitmap(mBitmap, srcRect, dstRect, paint); 1322 1323 // Draws the left/right parts with mesh matrixes. 1324 canvas.drawBitmapMesh( 1325 Bitmap.createBitmap(mBitmap, 0, 0, margin, h), 1326 mMeshWidth, mMeshHeight, mMeshLeft, 0, null, 0, paint); 1327 canvas.drawBitmapMesh( 1328 Bitmap.createBitmap(mBitmap, w - margin, 0, margin, h), 1329 mMeshWidth, mMeshHeight, mMeshRight, 0, null, 0, paint); 1330 } else { 1331 final Rect srcRect = new Rect(0, 0, w, h); 1332 final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight); 1333 canvas.drawBitmap(mBitmap, srcRect, dstRect, paint); 1334 } 1335 } finally { 1336 mBitmapRenderNode.endRecording(); 1337 } 1338 if (mPendingWindowPositionUpdate || mFirstDraw) { 1339 // If the window has to be shown or moved, defer this until the next draw. 1340 final boolean firstDraw = mFirstDraw; 1341 mFirstDraw = false; 1342 final boolean updateWindowPosition = mPendingWindowPositionUpdate; 1343 mPendingWindowPositionUpdate = false; 1344 final int pendingX = mWindowPositionX; 1345 final int pendingY = mWindowPositionY; 1346 1347 callback = frame -> { 1348 if (!mSurface.isValid()) { 1349 return; 1350 } 1351 if (updateWindowPosition) { 1352 mTransaction.setPosition(mSurfaceControl, pendingX, pendingY); 1353 } 1354 if (firstDraw) { 1355 mTransaction.setLayer(mSurfaceControl, SURFACE_Z) 1356 .show(mSurfaceControl); 1357 1358 } 1359 // Show or move the window at the content draw frame. 1360 mBBQ.mergeWithNextTransaction(mTransaction, frame); 1361 }; 1362 if (!mIsFishEyeStyle) { 1363 // The new style magnifier doesn't need the light/shadow. 1364 mRenderer.setLightCenter(mDisplay, pendingX, pendingY); 1365 } 1366 } else { 1367 callback = null; 1368 } 1369 1370 mFrameDrawScheduled = false; 1371 } 1372 1373 mRenderer.draw(callback); 1374 if (mCallback != null) { 1375 // The current content bitmap is only used in testing, so, for performance, 1376 // we only want to update it when running tests. For this, we check that 1377 // mCallback is not null, as it can only be set from a @TestApi. 1378 updateCurrentContentForTesting(); 1379 mCallback.onOperationComplete(); 1380 } 1381 } 1382 1383 /** 1384 * Updates mCurrentContent, which reproduces what is currently supposed to be 1385 * drawn in the magnifier. mCurrentContent is only used for testing, so this method 1386 * should only be called otherwise. 1387 */ updateCurrentContentForTesting()1388 private void updateCurrentContentForTesting() { 1389 final Canvas canvas = new Canvas(mCurrentContent); 1390 final Rect bounds = new Rect(0, 0, mContentWidth, mContentHeight); 1391 if (mBitmap != null && !mBitmap.isRecycled()) { 1392 final Rect originalBounds = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 1393 canvas.drawBitmap(mBitmap, originalBounds, bounds, null); 1394 } 1395 mOverlay.setBounds(bounds); 1396 mOverlay.draw(canvas); 1397 } 1398 } 1399 1400 /** 1401 * Builder class for {@link Magnifier} objects. 1402 */ 1403 public static final class Builder { 1404 private @NonNull View mView; 1405 private @Px @IntRange(from = 0) int mWidth; 1406 private @Px @IntRange(from = 0) int mHeight; 1407 private float mZoom; 1408 private @FloatRange(from = 0f) float mElevation; 1409 private @FloatRange(from = 0f) float mCornerRadius; 1410 private @Nullable Drawable mOverlay; 1411 private int mHorizontalDefaultSourceToMagnifierOffset; 1412 private int mVerticalDefaultSourceToMagnifierOffset; 1413 private boolean mClippingEnabled; 1414 private @SourceBound int mLeftContentBound; 1415 private @SourceBound int mTopContentBound; 1416 private @SourceBound int mRightContentBound; 1417 private @SourceBound int mBottomContentBound; 1418 private boolean mIsFishEyeStyle; 1419 private int mSourceWidth; 1420 private int mSourceHeight; 1421 1422 /** 1423 * Construct a new builder for {@link Magnifier} objects. 1424 * @param view the view this magnifier is attached to 1425 */ Builder(@onNull View view)1426 public Builder(@NonNull View view) { 1427 mView = Objects.requireNonNull(view); 1428 applyDefaults(); 1429 } 1430 applyDefaults()1431 private void applyDefaults() { 1432 final Resources resources = mView.getContext().getResources(); 1433 mWidth = resources.getDimensionPixelSize(R.dimen.default_magnifier_width); 1434 mHeight = resources.getDimensionPixelSize(R.dimen.default_magnifier_height); 1435 mElevation = resources.getDimension(R.dimen.default_magnifier_elevation); 1436 mCornerRadius = resources.getDimension(R.dimen.default_magnifier_corner_radius); 1437 mZoom = resources.getFloat(R.dimen.default_magnifier_zoom); 1438 mHorizontalDefaultSourceToMagnifierOffset = 1439 resources.getDimensionPixelSize(R.dimen.default_magnifier_horizontal_offset); 1440 mVerticalDefaultSourceToMagnifierOffset = 1441 resources.getDimensionPixelSize(R.dimen.default_magnifier_vertical_offset); 1442 mOverlay = new ColorDrawable(resources.getColor( 1443 R.color.default_magnifier_color_overlay, null)); 1444 mClippingEnabled = true; 1445 mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE; 1446 mTopContentBound = SOURCE_BOUND_MAX_VISIBLE; 1447 mRightContentBound = SOURCE_BOUND_MAX_VISIBLE; 1448 mBottomContentBound = SOURCE_BOUND_MAX_VISIBLE; 1449 mIsFishEyeStyle = false; 1450 } 1451 1452 /** 1453 * Sets the size of the magnifier window, in pixels. Defaults to (100dp, 48dp). 1454 * Note that the size of the content being magnified and copied to the magnifier 1455 * will be computed as (window width / zoom, window height / zoom). 1456 * @param width the window width to be set 1457 * @param height the window height to be set 1458 */ 1459 @NonNull setSize(@x @ntRangefrom = 0) int width, @Px @IntRange(from = 0) int height)1460 public Builder setSize(@Px @IntRange(from = 0) int width, 1461 @Px @IntRange(from = 0) int height) { 1462 Preconditions.checkArgumentPositive(width, "Width should be positive"); 1463 Preconditions.checkArgumentPositive(height, "Height should be positive"); 1464 mWidth = width; 1465 mHeight = height; 1466 return this; 1467 } 1468 1469 /** 1470 * Sets the zoom to be applied to the chosen content before being copied to the magnifier. 1471 * A content of size (content_width, content_height) will be magnified to 1472 * (content_width * zoom, content_height * zoom), which will coincide with the size 1473 * of the magnifier. A zoom of 1 will translate to no magnification (the content will 1474 * be just copied to the magnifier with no scaling). The zoom defaults to 1.25. 1475 * Note that the zoom can also be changed after the instance is built, using the 1476 * {@link Magnifier#setZoom(float)} method. 1477 * @param zoom the zoom to be set 1478 */ 1479 @NonNull setInitialZoom(@loatRangefrom = 0f) float zoom)1480 public Builder setInitialZoom(@FloatRange(from = 0f) float zoom) { 1481 Preconditions.checkArgumentPositive(zoom, "Zoom should be positive"); 1482 mZoom = zoom; 1483 return this; 1484 } 1485 1486 /** 1487 * Sets the elevation of the magnifier window, in pixels. Defaults to 4dp. 1488 * @param elevation the elevation to be set 1489 */ 1490 @NonNull setElevation(@x @loatRangefrom = 0) float elevation)1491 public Builder setElevation(@Px @FloatRange(from = 0) float elevation) { 1492 Preconditions.checkArgumentNonNegative(elevation, "Elevation should be non-negative"); 1493 mElevation = elevation; 1494 return this; 1495 } 1496 1497 /** 1498 * Sets the corner radius of the magnifier window, in pixels. Defaults to 2dp. 1499 * @param cornerRadius the corner radius to be set 1500 */ 1501 @NonNull setCornerRadius(@x @loatRangefrom = 0) float cornerRadius)1502 public Builder setCornerRadius(@Px @FloatRange(from = 0) float cornerRadius) { 1503 Preconditions.checkArgumentNonNegative(cornerRadius, 1504 "Corner radius should be non-negative"); 1505 mCornerRadius = cornerRadius; 1506 return this; 1507 } 1508 1509 /** 1510 * Sets an overlay that will be drawn on the top of the magnifier. 1511 * In general, the overlay should not be opaque, in order to let the magnified 1512 * content be partially visible in the magnifier. The default overlay is {@code null} 1513 * (no overlay). As an example, TextView applies a white {@link ColorDrawable} 1514 * overlay with 5% alpha, aiming to make the magnifier distinguishable when shown in dark 1515 * application regions. To disable the overlay, the parameter should be set 1516 * to {@code null}. If not null, the overlay will be automatically redrawn 1517 * when the drawable is invalidated. To achieve this, the magnifier will set a new 1518 * {@link android.graphics.drawable.Drawable.Callback} for the overlay drawable, 1519 * so keep in mind that any existing one set by the application will be lost. 1520 * @param overlay the overlay to be drawn on top 1521 */ 1522 @NonNull setOverlay(@ullable Drawable overlay)1523 public Builder setOverlay(@Nullable Drawable overlay) { 1524 mOverlay = overlay; 1525 return this; 1526 } 1527 1528 /** 1529 * Sets an offset that should be added to the content source center to obtain 1530 * the position of the magnifier window, when the {@link #show(float, float)} 1531 * method is called. The offset is ignored when {@link #show(float, float, float, float)} 1532 * is used. The offset can be negative. It defaults to (0dp, 0dp). 1533 * @param horizontalOffset the horizontal component of the offset 1534 * @param verticalOffset the vertical component of the offset 1535 */ 1536 @NonNull setDefaultSourceToMagnifierOffset(@x int horizontalOffset, @Px int verticalOffset)1537 public Builder setDefaultSourceToMagnifierOffset(@Px int horizontalOffset, 1538 @Px int verticalOffset) { 1539 mHorizontalDefaultSourceToMagnifierOffset = horizontalOffset; 1540 mVerticalDefaultSourceToMagnifierOffset = verticalOffset; 1541 return this; 1542 } 1543 1544 /** 1545 * Defines the behavior of the magnifier when it is requested to position outside the 1546 * surface of the main application window. The default value is {@code true}, which means 1547 * that the position will be adjusted such that the magnifier will be fully within the 1548 * bounds of the main application window, while also avoiding any overlap with system insets 1549 * (such as the one corresponding to the status bar). If this flag is set to {@code false}, 1550 * the area where the magnifier can be positioned will no longer be clipped, so the 1551 * magnifier will be able to extend outside the main application window boundaries (and also 1552 * overlap the system insets). This can be useful if you require a custom behavior, but it 1553 * should be handled with care, when passing coordinates to {@link #show(float, float)}; 1554 * note that: 1555 * <ul> 1556 * <li>in a multiwindow context, if the magnifier crosses the boundary between the two 1557 * windows, it will not be able to show over the window of the other application</li> 1558 * <li>if the magnifier overlaps the status bar, there is no guarantee about which one 1559 * will be displayed on top. This should be handled with care.</li> 1560 * </ul> 1561 * @param clip whether the magnifier position will be adjusted 1562 */ 1563 @NonNull setClippingEnabled(boolean clip)1564 public Builder setClippingEnabled(boolean clip) { 1565 mClippingEnabled = clip; 1566 return this; 1567 } 1568 1569 /** 1570 * Defines the bounds of the rectangle where the magnifier will be able to copy its content 1571 * from. The content will always be copied from the {@link Surface} of the main application 1572 * window unless the magnified view is a {@link SurfaceView}, in which case its backing 1573 * surface will be used. Each bound can have a different behavior, with the options being: 1574 * <ul> 1575 * <li>{@link #SOURCE_BOUND_MAX_VISIBLE}, which extends the bound as much as possible 1576 * while remaining in the visible region of the magnified view, as given by 1577 * {@link android.view.View#getGlobalVisibleRect(Rect)}. For example, this will take into 1578 * account the case when the view is contained in a scrollable container, and the 1579 * magnifier will refuse to copy content outside of the visible view region</li> 1580 * <li>{@link #SOURCE_BOUND_MAX_IN_SURFACE}, which extends the bound as much 1581 * as possible while remaining inside the surface the content is copied from.</li> 1582 * </ul> 1583 * Note that if either of the first three options is used, the bound will be compared to 1584 * the bound of the surface (i.e. as if {@link #SOURCE_BOUND_MAX_IN_SURFACE} was used), 1585 * and the more restrictive one will be chosen. In other words, no attempt to copy content 1586 * from outside the surface will be permitted. If two opposite bounds are not well-behaved 1587 * (i.e. left + sourceWidth > right or top + sourceHeight > bottom), the left and top 1588 * bounds will have priority and the others will be extended accordingly. If the pairs 1589 * obtained this way still remain out of bounds, the smallest possible offset will be added 1590 * to the pairs to bring them inside the surface bounds. If this is impossible 1591 * (i.e. the surface is too small for the size of the content we try to copy on either 1592 * dimension), an error will be logged and the magnifier content will look distorted. 1593 * The default values assumed by the builder for the source bounds are 1594 * left: {@link #SOURCE_BOUND_MAX_VISIBLE}, top: {@link #SOURCE_BOUND_MAX_IN_SURFACE}, 1595 * right: {@link #SOURCE_BOUND_MAX_VISIBLE}, bottom: {@link #SOURCE_BOUND_MAX_IN_SURFACE}. 1596 * @param left the left bound for content copy 1597 * @param top the top bound for content copy 1598 * @param right the right bound for content copy 1599 * @param bottom the bottom bound for content copy 1600 */ 1601 @NonNull setSourceBounds(@ourceBound int left, @SourceBound int top, @SourceBound int right, @SourceBound int bottom)1602 public Builder setSourceBounds(@SourceBound int left, @SourceBound int top, 1603 @SourceBound int right, @SourceBound int bottom) { 1604 mLeftContentBound = left; 1605 mTopContentBound = top; 1606 mRightContentBound = right; 1607 mBottomContentBound = bottom; 1608 return this; 1609 } 1610 1611 /** 1612 * Sets the source width/height. 1613 */ 1614 @NonNull setSourceSize(int width, int height)1615 Builder setSourceSize(int width, int height) { 1616 mSourceWidth = width; 1617 mSourceHeight = height; 1618 return this; 1619 } 1620 1621 /** 1622 * Sets the magnifier as the new fish-eye style. 1623 */ 1624 @NonNull setFishEyeStyle()1625 Builder setFishEyeStyle() { 1626 mIsFishEyeStyle = true; 1627 return this; 1628 } 1629 1630 /** 1631 * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}. 1632 */ build()1633 public @NonNull Magnifier build() { 1634 return new Magnifier(this); 1635 } 1636 } 1637 1638 /** 1639 * A source bound that will extend as much as possible, while remaining within the surface 1640 * the content is copied from. 1641 */ 1642 public static final int SOURCE_BOUND_MAX_IN_SURFACE = 0; 1643 1644 /** 1645 * A source bound that will extend as much as possible, while remaining within the 1646 * visible region of the magnified view, as determined by 1647 * {@link View#getGlobalVisibleRect(Rect)}. 1648 */ 1649 public static final int SOURCE_BOUND_MAX_VISIBLE = 1; 1650 1651 1652 /** 1653 * Used to describe the {@link Surface} rectangle where the magnifier's content is allowed 1654 * to be copied from. For more details, see method 1655 * {@link Magnifier.Builder#setSourceBounds(int, int, int, int)} 1656 * 1657 * @hide 1658 */ 1659 @IntDef({SOURCE_BOUND_MAX_IN_SURFACE, SOURCE_BOUND_MAX_VISIBLE}) 1660 @Retention(RetentionPolicy.SOURCE) 1661 public @interface SourceBound {} 1662 1663 // The rest of the file consists of test APIs and methods relevant for tests. 1664 1665 /** 1666 * See {@link #setOnOperationCompleteCallback(Callback)}. 1667 */ 1668 @TestApi 1669 private Callback mCallback; 1670 1671 /** 1672 * Sets a callback which will be invoked at the end of the next 1673 * {@link #show(float, float)} or {@link #update()} operation. 1674 * 1675 * @hide 1676 */ 1677 @TestApi setOnOperationCompleteCallback(final Callback callback)1678 public void setOnOperationCompleteCallback(final Callback callback) { 1679 mCallback = callback; 1680 if (mWindow != null) { 1681 mWindow.mCallback = callback; 1682 } 1683 } 1684 1685 /** 1686 * @return the drawing being currently displayed in the magnifier, as bitmap 1687 * 1688 * @hide 1689 */ 1690 @TestApi getContent()1691 public @Nullable Bitmap getContent() { 1692 if (mWindow == null) { 1693 return null; 1694 } 1695 synchronized (mWindow.mLock) { 1696 return mWindow.mCurrentContent; 1697 } 1698 } 1699 1700 /** 1701 * Returns a bitmap containing the content that was magnified and drew to the 1702 * magnifier, at its original size, without the overlay applied. 1703 * @return the content that is magnified, as bitmap 1704 * 1705 * @hide 1706 */ 1707 @TestApi getOriginalContent()1708 public @Nullable Bitmap getOriginalContent() { 1709 if (mWindow == null) { 1710 return null; 1711 } 1712 synchronized (mWindow.mLock) { 1713 return Bitmap.createBitmap(mWindow.mBitmap); 1714 } 1715 } 1716 1717 /** 1718 * @return the size of the magnifier window in dp 1719 * 1720 * @hide 1721 */ 1722 @TestApi getMagnifierDefaultSize()1723 public static PointF getMagnifierDefaultSize() { 1724 final Resources resources = Resources.getSystem(); 1725 final float density = resources.getDisplayMetrics().density; 1726 final PointF size = new PointF(); 1727 size.x = resources.getDimension(R.dimen.default_magnifier_width) / density; 1728 size.y = resources.getDimension(R.dimen.default_magnifier_height) / density; 1729 return size; 1730 } 1731 1732 /** 1733 * @hide 1734 */ 1735 @TestApi 1736 public interface Callback { 1737 /** 1738 * Callback called after the drawing for a magnifier update has happened. 1739 */ onOperationComplete()1740 void onOperationComplete(); 1741 } 1742 } 1743