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.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.TestApi; 23 import android.annotation.UiThread; 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.content.res.TypedArray; 27 import android.graphics.Bitmap; 28 import android.graphics.Color; 29 import android.graphics.Outline; 30 import android.graphics.Paint; 31 import android.graphics.PixelFormat; 32 import android.graphics.Point; 33 import android.graphics.PointF; 34 import android.graphics.Rect; 35 import android.os.Handler; 36 import android.os.HandlerThread; 37 import android.os.Message; 38 import android.view.ContextThemeWrapper; 39 import android.view.Display; 40 import android.view.DisplayListCanvas; 41 import android.view.PixelCopy; 42 import android.view.RenderNode; 43 import android.view.Surface; 44 import android.view.SurfaceControl; 45 import android.view.SurfaceHolder; 46 import android.view.SurfaceSession; 47 import android.view.SurfaceView; 48 import android.view.ThreadedRenderer; 49 import android.view.View; 50 import android.view.ViewRootImpl; 51 52 import com.android.internal.R; 53 import com.android.internal.util.Preconditions; 54 55 /** 56 * Android magnifier widget. Can be used by any view which is attached to a window. 57 */ 58 @UiThread 59 public final class Magnifier { 60 // Use this to specify that a previous configuration value does not exist. 61 private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1; 62 // The callbacks of the pixel copy requests will be invoked on 63 // the Handler of this Thread when the copy is finished. 64 private static final HandlerThread sPixelCopyHandlerThread = 65 new HandlerThread("magnifier pixel copy result handler"); 66 67 // The view to which this magnifier is attached. 68 private final View mView; 69 // The coordinates of the view in the surface. 70 private final int[] mViewCoordinatesInSurface; 71 // The window containing the magnifier. 72 private InternalPopupWindow mWindow; 73 // The width of the window containing the magnifier. 74 private final int mWindowWidth; 75 // The height of the window containing the magnifier. 76 private final int mWindowHeight; 77 // The zoom applied to the view region copied to the magnifier window. 78 private final float mZoom; 79 // The width of the bitmaps where the magnifier content is copied. 80 private final int mBitmapWidth; 81 // The height of the bitmaps where the magnifier content is copied. 82 private final int mBitmapHeight; 83 // The elevation of the window containing the magnifier. 84 private final float mWindowElevation; 85 // The corner radius of the window containing the magnifier. 86 private final float mWindowCornerRadius; 87 // The parent surface for the magnifier surface. 88 private SurfaceInfo mParentSurface; 89 // The surface where the content will be copied from. 90 private SurfaceInfo mContentCopySurface; 91 // The center coordinates of the window containing the magnifier. 92 private final Point mWindowCoords = new Point(); 93 // The center coordinates of the content to be magnified, 94 // which can potentially contain a region outside the magnified view. 95 private final Point mCenterZoomCoords = new Point(); 96 // The center coordinates of the content to be magnified, 97 // clamped inside the visible region of the magnified view. 98 private final Point mClampedCenterZoomCoords = new Point(); 99 // Variables holding previous states, used for detecting redundant calls and invalidation. 100 private final Point mPrevStartCoordsInSurface = new Point( 101 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE); 102 private final PointF mPrevPosInView = new PointF( 103 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE); 104 // Rectangle defining the view surface area we pixel copy content from. 105 private final Rect mPixelCopyRequestRect = new Rect(); 106 // Lock to synchronize between the UI thread and the thread that handles pixel copy results. 107 // Only sync mWindow writes from UI thread with mWindow reads from sPixelCopyHandlerThread. 108 private final Object mLock = new Object(); 109 110 /** 111 * Initializes a magnifier. 112 * 113 * @param view the view for which this magnifier is attached 114 */ 115 public Magnifier(@NonNull View view) { 116 mView = Preconditions.checkNotNull(view); 117 final Context context = mView.getContext(); 118 mWindowWidth = context.getResources().getDimensionPixelSize(R.dimen.magnifier_width); 119 mWindowHeight = context.getResources().getDimensionPixelSize(R.dimen.magnifier_height); 120 mWindowElevation = context.getResources().getDimension(R.dimen.magnifier_elevation); 121 mWindowCornerRadius = getDeviceDefaultDialogCornerRadius(); 122 mZoom = context.getResources().getFloat(R.dimen.magnifier_zoom_scale); 123 mBitmapWidth = Math.round(mWindowWidth / mZoom); 124 mBitmapHeight = Math.round(mWindowHeight / mZoom); 125 // The view's surface coordinates will not be updated until the magnifier is first shown. 126 mViewCoordinatesInSurface = new int[2]; 127 } 128 129 static { 130 sPixelCopyHandlerThread.start(); 131 } 132 133 /** 134 * Returns the device default theme dialog corner radius attribute. 135 * We retrieve this from the device default theme to avoid 136 * using the values set in the custom application themes. 137 */ 138 private float getDeviceDefaultDialogCornerRadius() { 139 final Context deviceDefaultContext = 140 new ContextThemeWrapper(mView.getContext(), R.style.Theme_DeviceDefault); 141 final TypedArray ta = deviceDefaultContext.obtainStyledAttributes( 142 new int[]{android.R.attr.dialogCornerRadius}); 143 final float dialogCornerRadius = ta.getDimension(0, 0); 144 ta.recycle(); 145 return dialogCornerRadius; 146 } 147 148 /** 149 * Shows the magnifier on the screen. 150 * 151 * @param xPosInView horizontal coordinate of the center point of the magnifier source relative 152 * to the view. The lower end is clamped to 0 and the higher end is clamped to the view 153 * width. 154 * @param yPosInView vertical coordinate of the center point of the magnifier source 155 * relative to the view. The lower end is clamped to 0 and the higher end is clamped to 156 * the view height. 157 */ 158 public void show(@FloatRange(from = 0) float xPosInView, 159 @FloatRange(from = 0) float yPosInView) { 160 xPosInView = Math.max(0, Math.min(xPosInView, mView.getWidth())); 161 yPosInView = Math.max(0, Math.min(yPosInView, mView.getHeight())); 162 163 obtainSurfaces(); 164 obtainContentCoordinates(xPosInView, yPosInView); 165 obtainWindowCoordinates(); 166 167 final int startX = mClampedCenterZoomCoords.x - mBitmapWidth / 2; 168 final int startY = mClampedCenterZoomCoords.y - mBitmapHeight / 2; 169 if (xPosInView != mPrevPosInView.x || yPosInView != mPrevPosInView.y) { 170 if (mWindow == null) { 171 synchronized (mLock) { 172 mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(), 173 mParentSurface.mSurface, 174 mWindowWidth, mWindowHeight, mWindowElevation, mWindowCornerRadius, 175 Handler.getMain() /* draw the magnifier on the UI thread */, mLock, 176 mCallback); 177 } 178 } 179 performPixelCopy(startX, startY, true /* update window position */); 180 mPrevPosInView.x = xPosInView; 181 mPrevPosInView.y = yPosInView; 182 } 183 } 184 185 /** 186 * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op. 187 */ 188 public void dismiss() { 189 if (mWindow != null) { 190 synchronized (mLock) { 191 mWindow.destroy(); 192 mWindow = null; 193 } 194 mPrevPosInView.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE; 195 mPrevPosInView.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE; 196 mPrevStartCoordsInSurface.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE; 197 mPrevStartCoordsInSurface.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE; 198 } 199 } 200 201 /** 202 * Forces the magnifier to update its content. It uses the previous coordinates passed to 203 * {@link #show(float, float)}. This only happens if the magnifier is currently showing. 204 */ 205 public void update() { 206 if (mWindow != null) { 207 obtainSurfaces(); 208 // Update the content shown in the magnifier. 209 performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y, 210 false /* update window position */); 211 } 212 } 213 214 /** 215 * @return The width of the magnifier window, in pixels. 216 */ 217 public int getWidth() { 218 return mWindowWidth; 219 } 220 221 /** 222 * @return The height of the magnifier window, in pixels. 223 */ 224 public int getHeight() { 225 return mWindowHeight; 226 } 227 228 /** 229 * @return The zoom applied to the magnified view region copied to the magnifier window. 230 * If the zoom is x and the magnifier window size is (width, height), the original size 231 * of the content copied in the magnifier will be (width / x, height / x). 232 */ 233 public float getZoom() { 234 return mZoom; 235 } 236 237 /** 238 * @hide 239 * 240 * @return The top left coordinates of the magnifier, relative to the parent window. 241 */ 242 @Nullable 243 public Point getWindowCoords() { 244 if (mWindow == null) { 245 return null; 246 } 247 final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets; 248 return new Point(mWindow.mLastDrawContentPositionX - surfaceInsets.left, 249 mWindow.mLastDrawContentPositionY - surfaceInsets.top); 250 } 251 252 /** 253 * Retrieves the surfaces used by the magnifier: 254 * - a parent surface for the magnifier surface. This will usually be the main app window. 255 * - a surface where the magnified content will be copied from. This will be the main app 256 * window unless the magnified view is a SurfaceView, in which case its backing surface 257 * will be used. 258 */ 259 private void obtainSurfaces() { 260 // Get the main window surface. 261 SurfaceInfo validMainWindowSurface = SurfaceInfo.NULL; 262 if (mView.getViewRootImpl() != null) { 263 final ViewRootImpl viewRootImpl = mView.getViewRootImpl(); 264 final Surface mainWindowSurface = viewRootImpl.mSurface; 265 if (mainWindowSurface != null && mainWindowSurface.isValid()) { 266 final Rect surfaceInsets = viewRootImpl.mWindowAttributes.surfaceInsets; 267 final int surfaceWidth = 268 viewRootImpl.getWidth() + surfaceInsets.left + surfaceInsets.right; 269 final int surfaceHeight = 270 viewRootImpl.getHeight() + surfaceInsets.top + surfaceInsets.bottom; 271 validMainWindowSurface = 272 new SurfaceInfo(mainWindowSurface, surfaceWidth, surfaceHeight, true); 273 } 274 } 275 // Get the surface backing the magnified view, if it is a SurfaceView. 276 SurfaceInfo validSurfaceViewSurface = SurfaceInfo.NULL; 277 if (mView instanceof SurfaceView) { 278 final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder(); 279 final Surface surfaceViewSurface = surfaceHolder.getSurface(); 280 if (surfaceViewSurface != null && surfaceViewSurface.isValid()) { 281 final Rect surfaceFrame = surfaceHolder.getSurfaceFrame(); 282 validSurfaceViewSurface = new SurfaceInfo(surfaceViewSurface, 283 surfaceFrame.right, surfaceFrame.bottom, false); 284 } 285 } 286 287 // Choose the parent surface for the magnifier and the source surface for the content. 288 mParentSurface = validMainWindowSurface != SurfaceInfo.NULL 289 ? validMainWindowSurface : validSurfaceViewSurface; 290 mContentCopySurface = mView instanceof SurfaceView 291 ? validSurfaceViewSurface : validMainWindowSurface; 292 } 293 294 /** 295 * Computes the coordinates of the center of the content going to be displayed in the 296 * magnifier. These are relative to the surface the content is copied from. 297 */ 298 private void obtainContentCoordinates(final float xPosInView, final float yPosInView) { 299 final float posX; 300 final float posY; 301 mView.getLocationInSurface(mViewCoordinatesInSurface); 302 if (mView instanceof SurfaceView) { 303 // No offset required if the backing Surface matches the size of the SurfaceView. 304 posX = xPosInView; 305 posY = yPosInView; 306 } else { 307 posX = xPosInView + mViewCoordinatesInSurface[0]; 308 posY = yPosInView + mViewCoordinatesInSurface[1]; 309 } 310 mCenterZoomCoords.x = Math.round(posX); 311 mCenterZoomCoords.y = Math.round(posY); 312 313 // Clamp the x location to avoid magnifying content which does not belong 314 // to the magnified view. This will not take into account overlapping views. 315 final Rect viewVisibleRegion = new Rect(); 316 mView.getGlobalVisibleRect(viewVisibleRegion); 317 if (mView.getViewRootImpl() != null) { 318 // Clamping coordinates relative to the surface, not to the window. 319 final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets; 320 viewVisibleRegion.offset(surfaceInsets.left, surfaceInsets.top); 321 } 322 if (mView instanceof SurfaceView) { 323 // If we copy content from a SurfaceView, clamp coordinates relative to it. 324 viewVisibleRegion.offset(-mViewCoordinatesInSurface[0], -mViewCoordinatesInSurface[1]); 325 } 326 mClampedCenterZoomCoords.x = Math.max(viewVisibleRegion.left + mBitmapWidth / 2, Math.min( 327 mCenterZoomCoords.x, viewVisibleRegion.right - mBitmapWidth / 2)); 328 mClampedCenterZoomCoords.y = mCenterZoomCoords.y; 329 } 330 331 private void obtainWindowCoordinates() { 332 // Compute the position of the magnifier window. Again, this has to be relative to the 333 // surface of the magnified view, as this surface is the parent of the magnifier surface. 334 final int verticalOffset = mView.getContext().getResources().getDimensionPixelSize( 335 R.dimen.magnifier_offset); 336 mWindowCoords.x = mCenterZoomCoords.x - mWindowWidth / 2; 337 mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 - verticalOffset; 338 if (mParentSurface != mContentCopySurface) { 339 mWindowCoords.x += mViewCoordinatesInSurface[0]; 340 mWindowCoords.y += mViewCoordinatesInSurface[1]; 341 } 342 } 343 344 private void performPixelCopy(final int startXInSurface, final int startYInSurface, 345 final boolean updateWindowPosition) { 346 if (mContentCopySurface.mSurface == null || !mContentCopySurface.mSurface.isValid()) { 347 return; 348 } 349 // Clamp copy coordinates inside the surface to avoid displaying distorted content. 350 final int clampedStartXInSurface = Math.max(0, 351 Math.min(startXInSurface, mContentCopySurface.mWidth - mBitmapWidth)); 352 final int clampedStartYInSurface = Math.max(0, 353 Math.min(startYInSurface, mContentCopySurface.mHeight - mBitmapHeight)); 354 355 // Clamp window coordinates inside the parent surface, to avoid displaying 356 // the magnifier out of screen or overlapping with system insets. 357 final Rect windowBounds; 358 if (mParentSurface.mIsMainWindowSurface) { 359 final Rect systemInsets = mView.getRootWindowInsets().getSystemWindowInsets(); 360 windowBounds = new Rect(systemInsets.left, systemInsets.top, 361 mParentSurface.mWidth - systemInsets.right, 362 mParentSurface.mHeight - systemInsets.bottom); 363 } else { 364 windowBounds = new Rect(0, 0, mParentSurface.mWidth, mParentSurface.mHeight); 365 } 366 final int windowCoordsX = Math.max(windowBounds.left, 367 Math.min(windowBounds.right - mWindowWidth, mWindowCoords.x)); 368 final int windowCoordsY = Math.max(windowBounds.top, 369 Math.min(windowBounds.bottom - mWindowHeight, mWindowCoords.y)); 370 371 // Perform the pixel copy. 372 mPixelCopyRequestRect.set(clampedStartXInSurface, 373 clampedStartYInSurface, 374 clampedStartXInSurface + mBitmapWidth, 375 clampedStartYInSurface + mBitmapHeight); 376 final InternalPopupWindow currentWindowInstance = mWindow; 377 final Bitmap bitmap = 378 Bitmap.createBitmap(mBitmapWidth, mBitmapHeight, Bitmap.Config.ARGB_8888); 379 PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap, 380 result -> { 381 synchronized (mLock) { 382 if (mWindow != currentWindowInstance) { 383 // The magnifier was dismissed (and maybe shown again) in the meantime. 384 return; 385 } 386 if (updateWindowPosition) { 387 // TODO: pull the position update outside #performPixelCopy 388 mWindow.setContentPositionForNextDraw(windowCoordsX, windowCoordsY); 389 } 390 mWindow.updateContent(bitmap); 391 } 392 }, 393 sPixelCopyHandlerThread.getThreadHandler()); 394 mPrevStartCoordsInSurface.x = startXInSurface; 395 mPrevStartCoordsInSurface.y = startYInSurface; 396 } 397 398 /** 399 * Contains a surface and metadata corresponding to it. 400 */ 401 private static class SurfaceInfo { 402 public static final SurfaceInfo NULL = new SurfaceInfo(null, 0, 0, false); 403 404 private Surface mSurface; 405 private int mWidth; 406 private int mHeight; 407 private boolean mIsMainWindowSurface; 408 409 SurfaceInfo(final Surface surface, final int width, final int height, 410 final boolean isMainWindowSurface) { 411 mSurface = surface; 412 mWidth = width; 413 mHeight = height; 414 mIsMainWindowSurface = isMainWindowSurface; 415 } 416 } 417 418 /** 419 * Magnifier's own implementation of PopupWindow-similar floating window. 420 * This exists to ensure frame-synchronization between window position updates and window 421 * content updates. By using a PopupWindow, these events would happen in different frames, 422 * producing a shakiness effect for the magnifier content. 423 */ 424 private static class InternalPopupWindow { 425 // The alpha set on the magnifier's content, which defines how 426 // prominent the white background is. 427 private static final int CONTENT_BITMAP_ALPHA = 242; 428 // The z of the magnifier surface, defining its z order in the list of 429 // siblings having the same parent surface (usually the main app surface). 430 private static final int SURFACE_Z = 5; 431 432 // Display associated to the view the magnifier is attached to. 433 private final Display mDisplay; 434 // The size of the content of the magnifier. 435 private final int mContentWidth; 436 private final int mContentHeight; 437 // The size of the allocated surface. 438 private final int mSurfaceWidth; 439 private final int mSurfaceHeight; 440 // The insets of the content inside the allocated surface. 441 private final int mOffsetX; 442 private final int mOffsetY; 443 // The surface we allocate for the magnifier content + shadow. 444 private final SurfaceSession mSurfaceSession; 445 private final SurfaceControl mSurfaceControl; 446 private final Surface mSurface; 447 // The renderer used for the allocated surface. 448 private final ThreadedRenderer.SimpleRenderer mRenderer; 449 // The RenderNode used to draw the magnifier content in the surface. 450 private final RenderNode mBitmapRenderNode; 451 // The job that will be post'd to apply the pending magnifier updates to the surface. 452 private final Runnable mMagnifierUpdater; 453 // The handler where the magnifier updater jobs will be post'd. 454 private final Handler mHandler; 455 // The callback to be run after the next draw. 456 private Callback mCallback; 457 // The position of the magnifier content when the last draw was requested. 458 private int mLastDrawContentPositionX; 459 private int mLastDrawContentPositionY; 460 461 // Members below describe the state of the magnifier. Reads/writes to them 462 // have to be synchronized between the UI thread and the thread that handles 463 // the pixel copy results. This is the purpose of mLock. 464 private final Object mLock; 465 // Whether a magnifier frame draw is currently pending in the UI thread queue. 466 private boolean mFrameDrawScheduled; 467 // The content bitmap. 468 private Bitmap mBitmap; 469 // Whether the next draw will be the first one for the current instance. 470 private boolean mFirstDraw = true; 471 // The window position in the parent surface. Might be applied during the next draw, 472 // when mPendingWindowPositionUpdate is true. 473 private int mWindowPositionX; 474 private int mWindowPositionY; 475 private boolean mPendingWindowPositionUpdate; 476 477 // The lock used to synchronize the UI and render threads when a #destroy 478 // is performed on the UI thread and a frame callback on the render thread. 479 // When both mLock and mDestroyLock need to be held at the same time, 480 // mDestroyLock should be acquired before mLock in order to avoid deadlocks. 481 private final Object mDestroyLock = new Object(); 482 483 InternalPopupWindow(final Context context, final Display display, 484 final Surface parentSurface, 485 final int width, final int height, final float elevation, final float cornerRadius, 486 final Handler handler, final Object lock, final Callback callback) { 487 mDisplay = display; 488 mLock = lock; 489 mCallback = callback; 490 491 mContentWidth = width; 492 mContentHeight = height; 493 mOffsetX = (int) (0.1f * width); 494 mOffsetY = (int) (0.1f * height); 495 // Setup the surface we will use for drawing the content and shadow. 496 mSurfaceWidth = mContentWidth + 2 * mOffsetX; 497 mSurfaceHeight = mContentHeight + 2 * mOffsetY; 498 mSurfaceSession = new SurfaceSession(parentSurface); 499 mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession) 500 .setFormat(PixelFormat.TRANSLUCENT) 501 .setSize(mSurfaceWidth, mSurfaceHeight) 502 .setName("magnifier surface") 503 .setFlags(SurfaceControl.HIDDEN) 504 .build(); 505 mSurface = new Surface(); 506 mSurface.copyFrom(mSurfaceControl); 507 508 // Setup the RenderNode tree. The root has only one child, which contains the bitmap. 509 mRenderer = new ThreadedRenderer.SimpleRenderer( 510 context, 511 "magnifier renderer", 512 mSurface 513 ); 514 mBitmapRenderNode = createRenderNodeForBitmap( 515 "magnifier content", 516 elevation, 517 cornerRadius 518 ); 519 520 final DisplayListCanvas canvas = mRenderer.getRootNode().start(width, height); 521 try { 522 canvas.insertReorderBarrier(); 523 canvas.drawRenderNode(mBitmapRenderNode); 524 canvas.insertInorderBarrier(); 525 } finally { 526 mRenderer.getRootNode().end(canvas); 527 } 528 529 // Initialize the update job and the handler where this will be post'd. 530 mHandler = handler; 531 mMagnifierUpdater = this::doDraw; 532 mFrameDrawScheduled = false; 533 } 534 535 private RenderNode createRenderNodeForBitmap(final String name, 536 final float elevation, final float cornerRadius) { 537 final RenderNode bitmapRenderNode = RenderNode.create(name, null); 538 539 // Define the position of the bitmap in the parent render node. The surface regions 540 // outside the bitmap are used to draw elevation. 541 bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY, 542 mOffsetX + mContentWidth, mOffsetY + mContentHeight); 543 bitmapRenderNode.setElevation(elevation); 544 545 final Outline outline = new Outline(); 546 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius); 547 outline.setAlpha(1.0f); 548 bitmapRenderNode.setOutline(outline); 549 bitmapRenderNode.setClipToOutline(true); 550 551 // Create a dummy draw, which will be replaced later with real drawing. 552 final DisplayListCanvas canvas = bitmapRenderNode.start(mContentWidth, mContentHeight); 553 try { 554 canvas.drawColor(0xFF00FF00); 555 } finally { 556 bitmapRenderNode.end(canvas); 557 } 558 559 return bitmapRenderNode; 560 } 561 562 /** 563 * Sets the position of the magnifier content relative to the parent surface. 564 * The position update will happen in the same frame with the next draw. 565 * The method has to be called in a context that holds {@link #mLock}. 566 * 567 * @param contentX the x coordinate of the content 568 * @param contentY the y coordinate of the content 569 */ 570 public void setContentPositionForNextDraw(final int contentX, final int contentY) { 571 mWindowPositionX = contentX - mOffsetX; 572 mWindowPositionY = contentY - mOffsetY; 573 mPendingWindowPositionUpdate = true; 574 requestUpdate(); 575 } 576 577 /** 578 * Sets the content that should be displayed in the magnifier. 579 * The update happens immediately, and possibly triggers a pending window movement set 580 * by {@link #setContentPositionForNextDraw(int, int)}. 581 * The method has to be called in a context that holds {@link #mLock}. 582 * 583 * @param bitmap the content bitmap 584 */ 585 public void updateContent(final @NonNull Bitmap bitmap) { 586 if (mBitmap != null) { 587 mBitmap.recycle(); 588 } 589 mBitmap = bitmap; 590 requestUpdate(); 591 } 592 593 private void requestUpdate() { 594 if (mFrameDrawScheduled) { 595 return; 596 } 597 final Message request = Message.obtain(mHandler, mMagnifierUpdater); 598 request.setAsynchronous(true); 599 request.sendToTarget(); 600 mFrameDrawScheduled = true; 601 } 602 603 /** 604 * Destroys this instance. 605 */ 606 public void destroy() { 607 synchronized (mDestroyLock) { 608 mSurface.destroy(); 609 } 610 synchronized (mLock) { 611 mRenderer.destroy(); 612 mSurfaceControl.destroy(); 613 mSurfaceSession.kill(); 614 mBitmapRenderNode.destroy(); 615 mHandler.removeCallbacks(mMagnifierUpdater); 616 if (mBitmap != null) { 617 mBitmap.recycle(); 618 } 619 } 620 } 621 622 private void doDraw() { 623 final ThreadedRenderer.FrameDrawingCallback callback; 624 625 // Draw the current bitmap to the surface, and prepare the callback which updates the 626 // surface position. These have to be in the same synchronized block, in order to 627 // guarantee the consistency between the bitmap content and the surface position. 628 synchronized (mLock) { 629 if (!mSurface.isValid()) { 630 // Probably #destroy() was called for the current instance, so we skip the draw. 631 return; 632 } 633 634 final DisplayListCanvas canvas = 635 mBitmapRenderNode.start(mContentWidth, mContentHeight); 636 try { 637 canvas.drawColor(Color.WHITE); 638 639 final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 640 final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight); 641 final Paint paint = new Paint(); 642 paint.setFilterBitmap(true); 643 paint.setAlpha(CONTENT_BITMAP_ALPHA); 644 canvas.drawBitmap(mBitmap, srcRect, dstRect, paint); 645 } finally { 646 mBitmapRenderNode.end(canvas); 647 } 648 649 if (mPendingWindowPositionUpdate || mFirstDraw) { 650 // If the window has to be shown or moved, defer this until the next draw. 651 final boolean firstDraw = mFirstDraw; 652 mFirstDraw = false; 653 final boolean updateWindowPosition = mPendingWindowPositionUpdate; 654 mPendingWindowPositionUpdate = false; 655 final int pendingX = mWindowPositionX; 656 final int pendingY = mWindowPositionY; 657 658 callback = frame -> { 659 synchronized (mDestroyLock) { 660 if (!mSurface.isValid()) { 661 return; 662 } 663 synchronized (mLock) { 664 mRenderer.setLightCenter(mDisplay, pendingX, pendingY); 665 // Show or move the window at the content draw frame. 666 SurfaceControl.openTransaction(); 667 mSurfaceControl.deferTransactionUntil(mSurface, frame); 668 if (updateWindowPosition) { 669 mSurfaceControl.setPosition(pendingX, pendingY); 670 } 671 if (firstDraw) { 672 mSurfaceControl.setLayer(SURFACE_Z); 673 mSurfaceControl.show(); 674 } 675 SurfaceControl.closeTransaction(); 676 } 677 } 678 }; 679 } else { 680 callback = null; 681 } 682 683 mLastDrawContentPositionX = mWindowPositionX + mOffsetX; 684 mLastDrawContentPositionY = mWindowPositionY + mOffsetY; 685 mFrameDrawScheduled = false; 686 } 687 688 mRenderer.draw(callback); 689 if (mCallback != null) { 690 mCallback.onOperationComplete(); 691 } 692 } 693 } 694 695 // The rest of the file consists of test APIs. 696 697 /** 698 * See {@link #setOnOperationCompleteCallback(Callback)}. 699 */ 700 @TestApi 701 private Callback mCallback; 702 703 /** 704 * Sets a callback which will be invoked at the end of the next 705 * {@link #show(float, float)} or {@link #update()} operation. 706 * 707 * @hide 708 */ 709 @TestApi 710 public void setOnOperationCompleteCallback(final Callback callback) { 711 mCallback = callback; 712 if (mWindow != null) { 713 mWindow.mCallback = callback; 714 } 715 } 716 717 /** 718 * @return the content being currently displayed in the magnifier, as bitmap 719 * 720 * @hide 721 */ 722 @TestApi 723 public @Nullable Bitmap getContent() { 724 if (mWindow == null) { 725 return null; 726 } 727 synchronized (mWindow.mLock) { 728 return Bitmap.createScaledBitmap(mWindow.mBitmap, mWindowWidth, mWindowHeight, true); 729 } 730 } 731 732 /** 733 * @return the position of the magnifier window relative to the screen 734 * 735 * @hide 736 */ 737 @TestApi 738 public Rect getWindowPositionOnScreen() { 739 final int[] viewLocationOnScreen = new int[2]; 740 mView.getLocationOnScreen(viewLocationOnScreen); 741 final int[] viewLocationInSurface = new int[2]; 742 mView.getLocationInSurface(viewLocationInSurface); 743 744 final int left = mWindowCoords.x + viewLocationOnScreen[0] - viewLocationInSurface[0]; 745 final int top = mWindowCoords.y + viewLocationOnScreen[1] - viewLocationInSurface[1]; 746 return new Rect(left, top, left + mWindowWidth, top + mWindowHeight); 747 } 748 749 /** 750 * @return the size of the magnifier window in dp 751 * 752 * @hide 753 */ 754 @TestApi 755 public static PointF getMagnifierDefaultSize() { 756 final Resources resources = Resources.getSystem(); 757 final float density = resources.getDisplayMetrics().density; 758 final PointF size = new PointF(); 759 size.x = resources.getDimension(R.dimen.magnifier_width) / density; 760 size.y = resources.getDimension(R.dimen.magnifier_height) / density; 761 return size; 762 } 763 764 /** 765 * @hide 766 */ 767 @TestApi 768 public interface Callback { 769 /** 770 * Callback called after the drawing for a magnifier update has happened. 771 */ 772 void onOperationComplete(); 773 } 774 } 775