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