Magnifier.java revision 6e44808890cac7809555b9ff63ff8e88e644b562
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 final int startX = Math.max(viewVisibleRegion.left, Math.min( 165 mCenterZoomCoords.x - mBitmapWidth / 2, 166 viewVisibleRegion.right - mBitmapWidth)); 167 final int startY = mCenterZoomCoords.y - mBitmapHeight / 2; 168 169 if (xPosInView != mPrevPosInView.x || yPosInView != mPrevPosInView.y) { 170 if (mWindow == null) { 171 synchronized (mLock) { 172 mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(), 173 getValidViewSurface(), 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 // Update the content shown in the magnifier. 208 performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y, 209 false /* update window position */); 210 } 211 } 212 213 /** 214 * @return The width of the magnifier window, in pixels. 215 */ 216 public int getWidth() { 217 return mWindowWidth; 218 } 219 220 /** 221 * @return The height of the magnifier window, in pixels. 222 */ 223 public int getHeight() { 224 return mWindowHeight; 225 } 226 227 /** 228 * @return The zoom applied to the magnified view region copied to the magnifier window. 229 * If the zoom is x and the magnifier window size is (width, height), the original size 230 * of the content copied in the magnifier will be (width / x, height / x). 231 */ 232 public float getZoom() { 233 return mZoom; 234 } 235 236 @Nullable 237 private Surface getValidViewSurface() { 238 // TODO: deduplicate this against the first part of #performPixelCopy 239 final Surface surface; 240 if (mView instanceof SurfaceView) { 241 surface = ((SurfaceView) mView).getHolder().getSurface(); 242 } else if (mView.getViewRootImpl() != null) { 243 surface = mView.getViewRootImpl().mSurface; 244 } else { 245 surface = null; 246 } 247 248 return (surface != null && surface.isValid()) ? surface : null; 249 } 250 251 private void configureCoordinates(final float xPosInView, final float yPosInView) { 252 // Compute the coordinates of the center of the content going to be displayed in the 253 // magnifier. These are relative to the surface the content is copied from. 254 final float posX; 255 final float posY; 256 if (mView instanceof SurfaceView) { 257 // No offset required if the backing Surface matches the size of the SurfaceView. 258 posX = xPosInView; 259 posY = yPosInView; 260 } else { 261 mView.getLocationInSurface(mViewCoordinatesInSurface); 262 posX = xPosInView + mViewCoordinatesInSurface[0]; 263 posY = yPosInView + mViewCoordinatesInSurface[1]; 264 } 265 mCenterZoomCoords.x = Math.round(posX); 266 mCenterZoomCoords.y = Math.round(posY); 267 268 // Compute the position of the magnifier window. Again, this has to be relative to the 269 // surface of the magnified view, as this surface is the parent of the magnifier surface. 270 final int verticalOffset = mView.getContext().getResources().getDimensionPixelSize( 271 R.dimen.magnifier_offset); 272 mWindowCoords.x = mCenterZoomCoords.x - mWindowWidth / 2; 273 mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 - verticalOffset; 274 } 275 276 private void performPixelCopy(final int startXInSurface, final int startYInSurface, 277 final boolean updateWindowPosition) { 278 // Get the view surface where the content will be copied from. 279 final Surface surface; 280 final int surfaceWidth; 281 final int surfaceHeight; 282 if (mView instanceof SurfaceView) { 283 final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder(); 284 surface = surfaceHolder.getSurface(); 285 surfaceWidth = surfaceHolder.getSurfaceFrame().right; 286 surfaceHeight = surfaceHolder.getSurfaceFrame().bottom; 287 } else if (mView.getViewRootImpl() != null) { 288 final ViewRootImpl viewRootImpl = mView.getViewRootImpl(); 289 surface = viewRootImpl.mSurface; 290 surfaceWidth = viewRootImpl.getWidth(); 291 surfaceHeight = viewRootImpl.getHeight(); 292 } else { 293 surface = null; 294 surfaceWidth = NONEXISTENT_PREVIOUS_CONFIG_VALUE; 295 surfaceHeight = NONEXISTENT_PREVIOUS_CONFIG_VALUE; 296 } 297 298 if (surface == null || !surface.isValid()) { 299 return; 300 } 301 302 // Clamp copy coordinates inside the surface to avoid displaying distorted content. 303 final int clampedStartXInSurface = Math.max(0, 304 Math.min(startXInSurface, surfaceWidth - mBitmapWidth)); 305 final int clampedStartYInSurface = Math.max(0, 306 Math.min(startYInSurface, surfaceHeight - mBitmapHeight)); 307 308 // Clamp window coordinates inside the parent surface, to avoid displaying 309 // the magnifier out of screen or overlapping with system insets. 310 final Rect insets = mView.getRootWindowInsets().getSystemWindowInsets(); 311 final int windowCoordsX = Math.max(insets.left, 312 Math.min(surfaceWidth - mWindowWidth - insets.right, mWindowCoords.x)); 313 final int windowCoordsY = Math.max(insets.top, 314 Math.min(surfaceHeight - mWindowHeight - insets.bottom, mWindowCoords.y)); 315 316 // Perform the pixel copy. 317 mPixelCopyRequestRect.set(clampedStartXInSurface, 318 clampedStartYInSurface, 319 clampedStartXInSurface + mBitmapWidth, 320 clampedStartYInSurface + mBitmapHeight); 321 final InternalPopupWindow currentWindowInstance = mWindow; 322 final Bitmap bitmap = 323 Bitmap.createBitmap(mBitmapWidth, mBitmapHeight, Bitmap.Config.ARGB_8888); 324 PixelCopy.request(surface, mPixelCopyRequestRect, bitmap, 325 result -> { 326 synchronized (mLock) { 327 if (mWindow != currentWindowInstance) { 328 // The magnifier was dismissed (and maybe shown again) in the meantime. 329 return; 330 } 331 if (updateWindowPosition) { 332 // TODO: pull the position update outside #performPixelCopy 333 mWindow.setContentPositionForNextDraw(windowCoordsX, windowCoordsY); 334 } 335 mWindow.updateContent(bitmap); 336 } 337 }, 338 sPixelCopyHandlerThread.getThreadHandler()); 339 mPrevStartCoordsInSurface.x = startXInSurface; 340 mPrevStartCoordsInSurface.y = startYInSurface; 341 } 342 343 /** 344 * Magnifier's own implementation of PopupWindow-similar floating window. 345 * This exists to ensure frame-synchronization between window position updates and window 346 * content updates. By using a PopupWindow, these events would happen in different frames, 347 * producing a shakiness effect for the magnifier content. 348 */ 349 private static class InternalPopupWindow { 350 // The alpha set on the magnifier's content, which defines how 351 // prominent the white background is. 352 private static final int CONTENT_BITMAP_ALPHA = 242; 353 354 // Display associated to the view the magnifier is attached to. 355 private final Display mDisplay; 356 // The size of the content of the magnifier. 357 private final int mContentWidth; 358 private final int mContentHeight; 359 // The size of the allocated surface. 360 private final int mSurfaceWidth; 361 private final int mSurfaceHeight; 362 // The insets of the content inside the allocated surface. 363 private final int mOffsetX; 364 private final int mOffsetY; 365 // The surface we allocate for the magnifier content + shadow. 366 private final SurfaceSession mSurfaceSession; 367 private final SurfaceControl mSurfaceControl; 368 private final Surface mSurface; 369 // The renderer used for the allocated surface. 370 private final ThreadedRenderer.SimpleRenderer mRenderer; 371 // The RenderNode used to draw the magnifier content in the surface. 372 private final RenderNode mBitmapRenderNode; 373 // The job that will be post'd to apply the pending magnifier updates to the surface. 374 private final Runnable mMagnifierUpdater; 375 // The handler where the magnifier updater jobs will be post'd. 376 private final Handler mHandler; 377 // The callback to be run after the next draw. Only used for testing. 378 private Callback mCallback; 379 380 // Members below describe the state of the magnifier. Reads/writes to them 381 // have to be synchronized between the UI thread and the thread that handles 382 // the pixel copy results. This is the purpose of mLock. 383 private final Object mLock; 384 // Whether a magnifier frame draw is currently pending in the UI thread queue. 385 private boolean mFrameDrawScheduled; 386 // The content bitmap. 387 private Bitmap mBitmap; 388 // Whether the next draw will be the first one for the current instance. 389 private boolean mFirstDraw = true; 390 // The window position in the parent surface. Might be applied during the next draw, 391 // when mPendingWindowPositionUpdate is true. 392 private int mWindowPositionX; 393 private int mWindowPositionY; 394 private boolean mPendingWindowPositionUpdate; 395 396 InternalPopupWindow(final Context context, final Display display, 397 final Surface parentSurface, 398 final int width, final int height, final float elevation, final float cornerRadius, 399 final Handler handler, final Object lock, final Callback callback) { 400 mDisplay = display; 401 mLock = lock; 402 mCallback = callback; 403 404 mContentWidth = width; 405 mContentHeight = height; 406 mOffsetX = (int) (0.1f * width); 407 mOffsetY = (int) (0.1f * height); 408 // Setup the surface we will use for drawing the content and shadow. 409 mSurfaceWidth = mContentWidth + 2 * mOffsetX; 410 mSurfaceHeight = mContentHeight + 2 * mOffsetY; 411 mSurfaceSession = new SurfaceSession(parentSurface); 412 mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession) 413 .setFormat(PixelFormat.TRANSLUCENT) 414 .setSize(mSurfaceWidth, mSurfaceHeight) 415 .setName("magnifier surface") 416 .setFlags(SurfaceControl.HIDDEN) 417 .build(); 418 mSurface = new Surface(); 419 mSurface.copyFrom(mSurfaceControl); 420 421 // Setup the RenderNode tree. The root has only one child, which contains the bitmap. 422 mRenderer = new ThreadedRenderer.SimpleRenderer( 423 context, 424 "magnifier renderer", 425 mSurface 426 ); 427 mBitmapRenderNode = createRenderNodeForBitmap( 428 "magnifier content", 429 elevation, 430 cornerRadius 431 ); 432 433 final DisplayListCanvas canvas = mRenderer.getRootNode().start(width, height); 434 try { 435 canvas.insertReorderBarrier(); 436 canvas.drawRenderNode(mBitmapRenderNode); 437 canvas.insertInorderBarrier(); 438 } finally { 439 mRenderer.getRootNode().end(canvas); 440 } 441 442 // Initialize the update job and the handler where this will be post'd. 443 mHandler = handler; 444 mMagnifierUpdater = this::doDraw; 445 mFrameDrawScheduled = false; 446 } 447 448 private RenderNode createRenderNodeForBitmap(final String name, 449 final float elevation, final float cornerRadius) { 450 final RenderNode bitmapRenderNode = RenderNode.create(name, null); 451 452 // Define the position of the bitmap in the parent render node. The surface regions 453 // outside the bitmap are used to draw elevation. 454 bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY, 455 mOffsetX + mContentWidth, mOffsetY + mContentHeight); 456 bitmapRenderNode.setElevation(elevation); 457 458 final Outline outline = new Outline(); 459 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius); 460 outline.setAlpha(1.0f); 461 bitmapRenderNode.setOutline(outline); 462 bitmapRenderNode.setClipToOutline(true); 463 464 // Create a dummy draw, which will be replaced later with real drawing. 465 final DisplayListCanvas canvas = bitmapRenderNode.start(mContentWidth, mContentHeight); 466 try { 467 canvas.drawColor(0xFF00FF00); 468 } finally { 469 bitmapRenderNode.end(canvas); 470 } 471 472 return bitmapRenderNode; 473 } 474 475 /** 476 * Sets the position of the magnifier content relative to the parent surface. 477 * The position update will happen in the same frame with the next draw. 478 * The method has to be called in a context that holds {@link #mLock}. 479 * 480 * @param contentX the x coordinate of the content 481 * @param contentY the y coordinate of the content 482 */ 483 public void setContentPositionForNextDraw(final int contentX, final int contentY) { 484 mWindowPositionX = contentX - mOffsetX; 485 mWindowPositionY = contentY - mOffsetY; 486 mPendingWindowPositionUpdate = true; 487 requestUpdate(); 488 } 489 490 /** 491 * Sets the content that should be displayed in the magnifier. 492 * The update happens immediately, and possibly triggers a pending window movement set 493 * by {@link #setContentPositionForNextDraw(int, int)}. 494 * The method has to be called in a context that holds {@link #mLock}. 495 * 496 * @param bitmap the content bitmap 497 */ 498 public void updateContent(final @NonNull Bitmap bitmap) { 499 if (mBitmap != null) { 500 mBitmap.recycle(); 501 } 502 mBitmap = bitmap; 503 requestUpdate(); 504 } 505 506 private void requestUpdate() { 507 if (mFrameDrawScheduled) { 508 return; 509 } 510 final Message request = Message.obtain(mHandler, mMagnifierUpdater); 511 request.setAsynchronous(true); 512 request.sendToTarget(); 513 mFrameDrawScheduled = true; 514 } 515 516 /** 517 * Destroys this instance. 518 */ 519 public void destroy() { 520 synchronized (mLock) { 521 mRenderer.destroy(); 522 mSurface.destroy(); 523 mSurfaceControl.destroy(); 524 mSurfaceSession.kill(); 525 mBitmapRenderNode.destroy(); 526 mHandler.removeCallbacks(mMagnifierUpdater); 527 if (mBitmap != null) { 528 mBitmap.recycle(); 529 } 530 } 531 } 532 533 private void doDraw() { 534 final ThreadedRenderer.FrameDrawingCallback callback; 535 536 // Draw the current bitmap to the surface, and prepare the callback which updates the 537 // surface position. These have to be in the same synchronized block, in order to 538 // guarantee the consistency between the bitmap content and the surface position. 539 synchronized (mLock) { 540 if (!mSurface.isValid()) { 541 // Probably #destroy() was called for the current instance, so we skip the draw. 542 return; 543 } 544 545 final DisplayListCanvas canvas = 546 mBitmapRenderNode.start(mContentWidth, mContentHeight); 547 try { 548 canvas.drawColor(Color.WHITE); 549 550 final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 551 final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight); 552 final Paint paint = new Paint(); 553 paint.setFilterBitmap(true); 554 paint.setAlpha(CONTENT_BITMAP_ALPHA); 555 canvas.drawBitmap(mBitmap, srcRect, dstRect, paint); 556 } finally { 557 mBitmapRenderNode.end(canvas); 558 } 559 560 if (mPendingWindowPositionUpdate || mFirstDraw) { 561 // If the window has to be shown or moved, defer this until the next draw. 562 final boolean firstDraw = mFirstDraw; 563 mFirstDraw = false; 564 final boolean updateWindowPosition = mPendingWindowPositionUpdate; 565 mPendingWindowPositionUpdate = false; 566 final int pendingX = mWindowPositionX; 567 final int pendingY = mWindowPositionY; 568 569 callback = frame -> { 570 synchronized (mLock) { 571 if (!mSurface.isValid()) { 572 return; 573 } 574 mRenderer.setLightCenter(mDisplay, pendingX, pendingY); 575 // Show or move the window at the content draw frame. 576 SurfaceControl.openTransaction(); 577 mSurfaceControl.deferTransactionUntil(mSurface, frame); 578 if (updateWindowPosition) { 579 mSurfaceControl.setPosition(pendingX, pendingY); 580 } 581 if (firstDraw) { 582 mSurfaceControl.show(); 583 } 584 SurfaceControl.closeTransaction(); 585 } 586 }; 587 } else { 588 callback = null; 589 } 590 591 mFrameDrawScheduled = false; 592 } 593 594 mRenderer.draw(callback); 595 if (mCallback != null) { 596 mCallback.onOperationComplete(); 597 } 598 } 599 } 600 601 // The rest of the file consists of test APIs. 602 603 /** 604 * See {@link #setOnOperationCompleteCallback(Callback)}. 605 */ 606 @TestApi 607 private Callback mCallback; 608 609 /** 610 * Sets a callback which will be invoked at the end of the next 611 * {@link #show(float, float)} or {@link #update()} operation. 612 * 613 * @hide 614 */ 615 @TestApi 616 public void setOnOperationCompleteCallback(final Callback callback) { 617 mCallback = callback; 618 if (mWindow != null) { 619 mWindow.mCallback = callback; 620 } 621 } 622 623 /** 624 * @return the content being currently displayed in the magnifier, as bitmap 625 * 626 * @hide 627 */ 628 @TestApi 629 public @Nullable Bitmap getContent() { 630 if (mWindow == null) { 631 return null; 632 } 633 synchronized (mWindow.mLock) { 634 return Bitmap.createScaledBitmap(mWindow.mBitmap, mWindowWidth, mWindowHeight, true); 635 } 636 } 637 638 /** 639 * @return the position of the magnifier window relative to the screen 640 * 641 * @hide 642 */ 643 @TestApi 644 public Rect getWindowPositionOnScreen() { 645 final int[] viewLocationOnScreen = new int[2]; 646 mView.getLocationOnScreen(viewLocationOnScreen); 647 final int[] viewLocationInSurface = new int[2]; 648 mView.getLocationInSurface(viewLocationInSurface); 649 650 final int left = mWindowCoords.x + viewLocationOnScreen[0] - viewLocationInSurface[0]; 651 final int top = mWindowCoords.y + viewLocationOnScreen[1] - viewLocationInSurface[1]; 652 return new Rect(left, top, left + mWindowWidth, top + mWindowHeight); 653 } 654 655 /** 656 * @return the size of the magnifier window in dp 657 * 658 * @hide 659 */ 660 @TestApi 661 public static PointF getMagnifierDefaultSize() { 662 final Resources resources = Resources.getSystem(); 663 final float density = resources.getDisplayMetrics().density; 664 final PointF size = new PointF(); 665 size.x = resources.getDimension(R.dimen.magnifier_width) / density; 666 size.y = resources.getDimension(R.dimen.magnifier_height) / density; 667 return size; 668 } 669 670 /** 671 * @hide 672 */ 673 @TestApi 674 public interface Callback { 675 /** 676 * Callback called after the drawing for a magnifier update has happened. 677 */ 678 void onOperationComplete(); 679 } 680} 681