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