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