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