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