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