• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.IntDef;
21 import android.annotation.IntRange;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.Px;
25 import android.annotation.TestApi;
26 import android.annotation.UiThread;
27 import android.content.Context;
28 import android.content.res.Resources;
29 import android.content.res.TypedArray;
30 import android.graphics.Bitmap;
31 import android.graphics.Canvas;
32 import android.graphics.Color;
33 import android.graphics.BLASTBufferQueue;
34 import android.graphics.Insets;
35 import android.graphics.Outline;
36 import android.graphics.Paint;
37 import android.graphics.PixelFormat;
38 import android.graphics.Point;
39 import android.graphics.PointF;
40 import android.graphics.RecordingCanvas;
41 import android.graphics.Rect;
42 import android.graphics.RenderNode;
43 import android.graphics.drawable.ColorDrawable;
44 import android.graphics.drawable.Drawable;
45 import android.os.Handler;
46 import android.os.HandlerThread;
47 import android.os.Message;
48 import android.util.Log;
49 import android.util.TypedValue;
50 import android.view.ContextThemeWrapper;
51 import android.view.Display;
52 import android.view.PixelCopy;
53 import android.view.Surface;
54 import android.view.SurfaceControl;
55 import android.view.SurfaceHolder;
56 import android.view.SurfaceSession;
57 import android.view.SurfaceView;
58 import android.view.ThreadedRenderer;
59 import android.view.View;
60 import android.view.ViewRootImpl;
61 
62 import com.android.internal.R;
63 import com.android.internal.util.Preconditions;
64 
65 import java.lang.annotation.Retention;
66 import java.lang.annotation.RetentionPolicy;
67 import java.util.Objects;
68 
69 /**
70  * Android magnifier widget. Can be used by any view which is attached to a window.
71  */
72 @UiThread
73 public final class Magnifier {
74     private static final String TAG = "Magnifier";
75     // Use this to specify that a previous configuration value does not exist.
76     private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
77     // The callbacks of the pixel copy requests will be invoked on
78     // the Handler of this Thread when the copy is finished.
79     private static final HandlerThread sPixelCopyHandlerThread =
80             new HandlerThread("magnifier pixel copy result handler");
81     // The width of the ramp region in DP on the left & right sides of the fish-eye effect.
82     private static final float FISHEYE_RAMP_WIDTH = 12f;
83 
84     // The view to which this magnifier is attached.
85     private final View mView;
86     // The coordinates of the view in the surface.
87     private final int[] mViewCoordinatesInSurface;
88     // The window containing the magnifier.
89     private InternalPopupWindow mWindow;
90     // The width of the window containing the magnifier.
91     private final int mWindowWidth;
92     // The height of the window containing the magnifier.
93     private int mWindowHeight;
94     // The zoom applied to the view region copied to the magnifier view.
95     private float mZoom;
96     // The width of the content that will be copied to the magnifier.
97     private int mSourceWidth;
98     // The height of the content that will be copied to the magnifier.
99     private int mSourceHeight;
100     // Whether the zoom of the magnifier or the view position have changed since last content copy.
101     private boolean mDirtyState;
102     // The elevation of the window containing the magnifier.
103     private final float mWindowElevation;
104     // The corner radius of the window containing the magnifier.
105     private final float mWindowCornerRadius;
106     // The overlay to be drawn on the top of the magnifier content.
107     private final Drawable mOverlay;
108     // The horizontal offset between the source and window coords when #show(float, float) is used.
109     private final int mDefaultHorizontalSourceToMagnifierOffset;
110     // The vertical offset between the source and window coords when #show(float, float) is used.
111     private final int mDefaultVerticalSourceToMagnifierOffset;
112     // Whether the area where the magnifier can be positioned will be clipped to the main window
113     // and within system insets.
114     private final boolean mClippingEnabled;
115     // The behavior of the left bound of the rectangle where the content can be copied from.
116     private @SourceBound int mLeftContentBound;
117     // The behavior of the top bound of the rectangle where the content can be copied from.
118     private @SourceBound int mTopContentBound;
119     // The behavior of the right bound of the rectangle where the content can be copied from.
120     private @SourceBound int mRightContentBound;
121     // The behavior of the bottom bound of the rectangle where the content can be copied from.
122     private @SourceBound int mBottomContentBound;
123     // The parent surface for the magnifier surface.
124     private SurfaceInfo mParentSurface;
125     // The surface where the content will be copied from.
126     private SurfaceInfo mContentCopySurface;
127     // The center coordinates of the window containing the magnifier.
128     private final Point mWindowCoords = new Point();
129     // The center coordinates of the content to be magnified,
130     // clamped inside the visible region of the magnified view.
131     private final Point mClampedCenterZoomCoords = new Point();
132     // Variables holding previous states, used for detecting redundant calls and invalidation.
133     private final Point mPrevStartCoordsInSurface = new Point(
134             NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
135     private final PointF mPrevShowSourceCoords = new PointF(
136             NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
137     private final PointF mPrevShowWindowCoords = new PointF(
138             NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
139     // Rectangle defining the view surface area we pixel copy content from.
140     private final Rect mPixelCopyRequestRect = new Rect();
141     // Lock to synchronize between the UI thread and the thread that handles pixel copy results.
142     // Only sync mWindow writes from UI thread with mWindow reads from sPixelCopyHandlerThread.
143     private final Object mLock = new Object();
144 
145     // Members for new styled magnifier (Eloquent style).
146 
147     // Whether the magnifier is in new style.
148     private boolean mIsFishEyeStyle;
149     // The width of the cut region on the left edge of the pixel copy source rect.
150     private int mLeftCutWidth = 0;
151     // The width of the cut region on the right edge of the pixel copy source rect.
152     private int mRightCutWidth = 0;
153     // The width of the ramp region in pixels on the left & right sides of the fish-eye effect.
154     private final int mRamp;
155 
156     /**
157      * Initializes a magnifier.
158      *
159      * @param view the view for which this magnifier is attached
160      *
161      * @deprecated Please use {@link Builder} instead
162      */
163     @Deprecated
Magnifier(@onNull View view)164     public Magnifier(@NonNull View view) {
165         this(createBuilderWithOldMagnifierDefaults(view));
166     }
167 
createBuilderWithOldMagnifierDefaults(final View view)168     static Builder createBuilderWithOldMagnifierDefaults(final View view) {
169         final Builder params = new Builder(view);
170         final Context context = view.getContext();
171         final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Magnifier,
172                 R.attr.magnifierStyle, 0);
173         params.mWidth = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierWidth, 0);
174         params.mHeight = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHeight, 0);
175         params.mElevation = a.getDimension(R.styleable.Magnifier_magnifierElevation, 0);
176         params.mCornerRadius = getDeviceDefaultDialogCornerRadius(context);
177         params.mZoom = a.getFloat(R.styleable.Magnifier_magnifierZoom, 0);
178         params.mHorizontalDefaultSourceToMagnifierOffset =
179                 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHorizontalOffset, 0);
180         params.mVerticalDefaultSourceToMagnifierOffset =
181                 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierVerticalOffset, 0);
182         params.mOverlay = new ColorDrawable(a.getColor(
183                 R.styleable.Magnifier_magnifierColorOverlay, Color.TRANSPARENT));
184         a.recycle();
185         params.mClippingEnabled = true;
186         params.mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
187         params.mTopContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
188         params.mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
189         params.mBottomContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
190         return params;
191     }
192 
193     /**
194      * Returns the device default theme dialog corner radius attribute.
195      * We retrieve this from the device default theme to avoid
196      * using the values set in the custom application themes.
197      */
getDeviceDefaultDialogCornerRadius(final Context context)198     private static float getDeviceDefaultDialogCornerRadius(final Context context) {
199         final Context deviceDefaultContext =
200                 new ContextThemeWrapper(context, R.style.Theme_DeviceDefault);
201         final TypedArray ta = deviceDefaultContext.obtainStyledAttributes(
202                 new int[]{android.R.attr.dialogCornerRadius});
203         final float dialogCornerRadius = ta.getDimension(0, 0);
204         ta.recycle();
205         return dialogCornerRadius;
206     }
207 
Magnifier(@onNull Builder params)208     private Magnifier(@NonNull Builder params) {
209         // Copy params from builder.
210         mView = params.mView;
211         mWindowWidth = params.mWidth;
212         mWindowHeight = params.mHeight;
213         mZoom = params.mZoom;
214         mIsFishEyeStyle = params.mIsFishEyeStyle;
215         if (params.mSourceWidth > 0 && params.mSourceHeight > 0) {
216             mSourceWidth = params.mSourceWidth;
217             mSourceHeight = params.mSourceHeight;
218         } else {
219             mSourceWidth = Math.round(mWindowWidth / mZoom);
220             mSourceHeight = Math.round(mWindowHeight / mZoom);
221         }
222         mWindowElevation = params.mElevation;
223         mWindowCornerRadius = params.mCornerRadius;
224         mOverlay = params.mOverlay;
225         mDefaultHorizontalSourceToMagnifierOffset =
226                 params.mHorizontalDefaultSourceToMagnifierOffset;
227         mDefaultVerticalSourceToMagnifierOffset =
228                 params.mVerticalDefaultSourceToMagnifierOffset;
229         mClippingEnabled = params.mClippingEnabled;
230         mLeftContentBound = params.mLeftContentBound;
231         mTopContentBound = params.mTopContentBound;
232         mRightContentBound = params.mRightContentBound;
233         mBottomContentBound = params.mBottomContentBound;
234         // The view's surface coordinates will not be updated until the magnifier is first shown.
235         mViewCoordinatesInSurface = new int[2];
236         mRamp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, FISHEYE_RAMP_WIDTH,
237                 mView.getContext().getResources().getDisplayMetrics());
238     }
239 
240     static {
sPixelCopyHandlerThread.start()241         sPixelCopyHandlerThread.start();
242     }
243 
244     /**
245      * Shows the magnifier on the screen. The method takes the coordinates of the center
246      * of the content source going to be magnified and copied to the magnifier. The coordinates
247      * are relative to the top left corner of the magnified view. The magnifier will be
248      * positioned such that its center will be at the default offset from the center of the source.
249      * The default offset can be specified using the method
250      * {@link Builder#setDefaultSourceToMagnifierOffset(int, int)}. If the offset should
251      * be different across calls to this method, you should consider to use method
252      * {@link #show(float, float, float, float)} instead.
253      *
254      * @param sourceCenterX horizontal coordinate of the source center, relative to the view
255      * @param sourceCenterY vertical coordinate of the source center, relative to the view
256      *
257      * @see Builder#setDefaultSourceToMagnifierOffset(int, int)
258      * @see Builder#getDefaultHorizontalSourceToMagnifierOffset()
259      * @see Builder#getDefaultVerticalSourceToMagnifierOffset()
260      * @see #show(float, float, float, float)
261      */
show(@loatRangefrom = 0) float sourceCenterX, @FloatRange(from = 0) float sourceCenterY)262     public void show(@FloatRange(from = 0) float sourceCenterX,
263             @FloatRange(from = 0) float sourceCenterY) {
264         show(sourceCenterX, sourceCenterY,
265                 sourceCenterX + mDefaultHorizontalSourceToMagnifierOffset,
266                 sourceCenterY + mDefaultVerticalSourceToMagnifierOffset);
267     }
268 
269     private Drawable mCursorDrawable;
270     private boolean mDrawCursorEnabled;
271 
setDrawCursor(boolean enabled, Drawable cursorDrawable)272     void setDrawCursor(boolean enabled, Drawable cursorDrawable) {
273         mDrawCursorEnabled = enabled;
274         mCursorDrawable = cursorDrawable;
275     }
276 
277     /**
278      * Shows the magnifier on the screen at a position that is independent from its content
279      * position. The first two arguments represent the coordinates of the center of the
280      * content source going to be magnified and copied to the magnifier. The last two arguments
281      * represent the coordinates of the center of the magnifier itself. All four coordinates
282      * are relative to the top left corner of the magnified view. If you consider using this
283      * method such that the offset between the source center and the magnifier center coordinates
284      * remains constant, you should consider using method {@link #show(float, float)} instead.
285      *
286      * @param sourceCenterX horizontal coordinate of the source center relative to the view
287      * @param sourceCenterY vertical coordinate of the source center, relative to the view
288      * @param magnifierCenterX horizontal coordinate of the magnifier center, relative to the view
289      * @param magnifierCenterY vertical coordinate of the magnifier center, relative to the view
290      */
show(@loatRangefrom = 0) float sourceCenterX, @FloatRange(from = 0) float sourceCenterY, float magnifierCenterX, float magnifierCenterY)291     public void show(@FloatRange(from = 0) float sourceCenterX,
292             @FloatRange(from = 0) float sourceCenterY,
293             float magnifierCenterX, float magnifierCenterY) {
294 
295         obtainSurfaces();
296         obtainContentCoordinates(sourceCenterX, sourceCenterY);
297 
298         int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2;
299         final int startY = mClampedCenterZoomCoords.y - mSourceHeight / 2;
300 
301         if (mIsFishEyeStyle) {
302             // The magnifier center is the same as source center in new style.
303             magnifierCenterX = mClampedCenterZoomCoords.x - mViewCoordinatesInSurface[0];
304             magnifierCenterY = mClampedCenterZoomCoords.y - mViewCoordinatesInSurface[1];
305 
306             // PixelCopy requires the pre-magnified bounds.
307             // The below logic calculates the leftBound & rightBound for the pre-magnified bounds.
308             final float rampPre =
309                     (mSourceWidth - (mSourceWidth - 2 * mRamp) / mZoom) / 2;
310 
311             // Calculates the pre-zoomed left edge.
312             // The leftEdge moves from the left of view towards to sourceCenterX, considering the
313             // fisheye-like zooming.
314             final float x0 = sourceCenterX - mSourceWidth / 2f;
315             final float rampX0 = x0 + mRamp;
316             float leftEdge = 0;
317             if (leftEdge > rampX0) {
318                 // leftEdge is in the zoom range, the distance from leftEdge to sourceCenterX
319                 // should reduce per mZoom.
320                 leftEdge = sourceCenterX - (sourceCenterX - leftEdge) / mZoom;
321             } else if (leftEdge > x0) {
322                 // leftEdge is in the ramp range, the distance from leftEdge to rampX0 should
323                 // increase per ramp zoom (ramp / rampPre).
324                 leftEdge = x0 + rampPre - (rampX0 - leftEdge) * rampPre / mRamp;
325             }
326             int leftBound = Math.min((int) leftEdge, mView.getWidth());
327 
328             // Calculates the pre-zoomed right edge.
329             // The rightEdge moves from the right of view towards to sourceCenterX, considering the
330             // fisheye-like zooming.
331             final float x1 = sourceCenterX + mSourceWidth / 2f;
332             final float rampX1 = x1 - mRamp;
333             float rightEdge = mView.getWidth();
334             if (rightEdge < rampX1) {
335                 // rightEdge is in the zoom range, the distance from rightEdge to sourceCenterX
336                 // should reduce per mZoom.
337                 rightEdge = sourceCenterX + (rightEdge - sourceCenterX) / mZoom;
338             } else if (rightEdge < x1) {
339                 // rightEdge is in the ramp range, the distance from rightEdge to rampX1 should
340                 // increase per ramp zoom (ramp / rampPre).
341                 rightEdge = x1 - rampPre + (rightEdge - rampX1) * rampPre / mRamp;
342             }
343             int rightBound = Math.max(leftBound, (int) rightEdge);
344 
345             // Gets the startX for new style, which should be bounded by the horizontal bounds.
346             // Also calculates the left/right cut width for pixel copy.
347             leftBound = Math.max(leftBound + mViewCoordinatesInSurface[0], 0);
348             rightBound = Math.min(
349                 rightBound + mViewCoordinatesInSurface[0], mContentCopySurface.mWidth);
350             mLeftCutWidth = Math.max(0, leftBound - startX);
351             mRightCutWidth = Math.max(0, startX + mSourceWidth - rightBound);
352             startX = Math.max(startX, leftBound);
353         }
354         obtainWindowCoordinates(magnifierCenterX, magnifierCenterY);
355 
356         if (sourceCenterX != mPrevShowSourceCoords.x || sourceCenterY != mPrevShowSourceCoords.y
357                 || mDirtyState) {
358             if (mWindow == null) {
359                 synchronized (mLock) {
360                     mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(),
361                             mParentSurface.mSurfaceControl, mWindowWidth, mWindowHeight, mZoom,
362                             mRamp, mWindowElevation, mWindowCornerRadius,
363                             mOverlay != null ? mOverlay : new ColorDrawable(Color.TRANSPARENT),
364                             Handler.getMain() /* draw the magnifier on the UI thread */, mLock,
365                             mCallback, mIsFishEyeStyle);
366                 }
367             }
368             performPixelCopy(startX, startY, true /* update window position */);
369         } else if (magnifierCenterX != mPrevShowWindowCoords.x
370                 || magnifierCenterY != mPrevShowWindowCoords.y) {
371             final Point windowCoords = getCurrentClampedWindowCoordinates();
372             final InternalPopupWindow currentWindowInstance = mWindow;
373             sPixelCopyHandlerThread.getThreadHandler().post(() -> {
374                 synchronized (mLock) {
375                     if (mWindow != currentWindowInstance) {
376                         // The magnifier was dismissed (and maybe shown again) in the meantime.
377                         return;
378                     }
379                     mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
380                 }
381             });
382         }
383         mPrevShowSourceCoords.x = sourceCenterX;
384         mPrevShowSourceCoords.y = sourceCenterY;
385         mPrevShowWindowCoords.x = magnifierCenterX;
386         mPrevShowWindowCoords.y = magnifierCenterY;
387     }
388 
389     /**
390      * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op.
391      */
dismiss()392     public void dismiss() {
393         if (mWindow != null) {
394             synchronized (mLock) {
395                 mWindow.destroy();
396                 mWindow = null;
397             }
398             mPrevShowSourceCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
399             mPrevShowSourceCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
400             mPrevShowWindowCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
401             mPrevShowWindowCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
402             mPrevStartCoordsInSurface.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
403             mPrevStartCoordsInSurface.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
404         }
405     }
406 
407     /**
408      * Asks the magnifier to update its content. It uses the previous coordinates passed to
409      * {@link #show(float, float)} or {@link #show(float, float, float, float)}. The
410      * method only has effect if the magnifier is currently showing.
411      */
update()412     public void update() {
413         if (mWindow != null) {
414             obtainSurfaces();
415             if (!mDirtyState) {
416                 // Update the content shown in the magnifier.
417                 performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y,
418                         false /* update window position */);
419             } else {
420                 // If for example the zoom has changed, we cannot use the same top left
421                 // coordinates as before, so just #show again to have them recomputed.
422                 show(mPrevShowSourceCoords.x, mPrevShowSourceCoords.y,
423                         mPrevShowWindowCoords.x, mPrevShowWindowCoords.y);
424             }
425         }
426     }
427 
428     /**
429      * @return the width of the magnifier window, in pixels
430      * @see Magnifier.Builder#setSize(int, int)
431      */
432     @Px
getWidth()433     public int getWidth() {
434         return mWindowWidth;
435     }
436 
437     /**
438      * @return the height of the magnifier window, in pixels
439      * @see Magnifier.Builder#setSize(int, int)
440      */
441     @Px
getHeight()442     public int getHeight() {
443         return mWindowHeight;
444     }
445 
446     /**
447      * @return the initial width of the content magnified and copied to the magnifier, in pixels
448      * @see Magnifier.Builder#setSize(int, int)
449      * @see Magnifier.Builder#setInitialZoom(float)
450      */
451     @Px
getSourceWidth()452     public int getSourceWidth() {
453         return mSourceWidth;
454     }
455 
456     /**
457      * @return the initial height of the content magnified and copied to the magnifier, in pixels
458      * @see Magnifier.Builder#setSize(int, int)
459      * @see Magnifier.Builder#setInitialZoom(float)
460      */
461     @Px
getSourceHeight()462     public int getSourceHeight() {
463         return mSourceHeight;
464     }
465 
466     /**
467      * Sets the zoom to be applied to the chosen content before being copied to the magnifier popup.
468      * The change will become effective at the next #show or #update call.
469      * @param zoom the zoom to be set
470      */
setZoom(@loatRangefrom = 0f) float zoom)471     public void setZoom(@FloatRange(from = 0f) float zoom) {
472         Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
473         mZoom = zoom;
474         mSourceWidth = mIsFishEyeStyle ? mWindowWidth : Math.round(mWindowWidth / mZoom);
475         mSourceHeight = Math.round(mWindowHeight / mZoom);
476         mDirtyState = true;
477     }
478 
479     /**
480      * Updates the factors of source which may impact the magnifier's size.
481      * This can be called while the magnifier is showing and moving.
482      * @param sourceHeight the new source height.
483      * @param zoom the new zoom factor.
484      */
updateSourceFactors(final int sourceHeight, final float zoom)485     void updateSourceFactors(final int sourceHeight, final float zoom) {
486         mZoom = zoom;
487         mSourceHeight = sourceHeight;
488         mWindowHeight = (int) (sourceHeight * zoom);
489         if (mWindow != null) {
490             mWindow.updateContentFactors(mWindowHeight, zoom);
491         }
492     }
493 
494     /**
495      * Returns the zoom to be applied to the magnified view region copied to the magnifier.
496      * If the zoom is x and the magnifier window size is (width, height), the original size
497      * of the content being magnified will be (width / x, height / x).
498      * @return the zoom applied to the content
499      * @see Magnifier.Builder#setInitialZoom(float)
500      */
getZoom()501     public float getZoom() {
502         return mZoom;
503     }
504 
505     /**
506      * @return the elevation set for the magnifier window, in pixels
507      * @see Magnifier.Builder#setElevation(float)
508      */
509     @Px
getElevation()510     public float getElevation() {
511         return mWindowElevation;
512     }
513 
514     /**
515      * @return the corner radius of the magnifier window, in pixels
516      * @see Magnifier.Builder#setCornerRadius(float)
517      */
518     @Px
getCornerRadius()519     public float getCornerRadius() {
520         return mWindowCornerRadius;
521     }
522 
523     /**
524      * Returns the horizontal offset, in pixels, to be applied to the source center position
525      * to obtain the magnifier center position when {@link #show(float, float)} is called.
526      * The value is ignored when {@link #show(float, float, float, float)} is used instead.
527      *
528      * @return the default horizontal offset between the source center and the magnifier
529      * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
530      * @see Magnifier#show(float, float)
531      */
532     @Px
getDefaultHorizontalSourceToMagnifierOffset()533     public int getDefaultHorizontalSourceToMagnifierOffset() {
534         return mDefaultHorizontalSourceToMagnifierOffset;
535     }
536 
537     /**
538      * Returns the vertical offset, in pixels, to be applied to the source center position
539      * to obtain the magnifier center position when {@link #show(float, float)} is called.
540      * The value is ignored when {@link #show(float, float, float, float)} is used instead.
541      *
542      * @return the default vertical offset between the source center and the magnifier
543      * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
544      * @see Magnifier#show(float, float)
545      */
546     @Px
getDefaultVerticalSourceToMagnifierOffset()547     public int getDefaultVerticalSourceToMagnifierOffset() {
548         return mDefaultVerticalSourceToMagnifierOffset;
549     }
550 
551     /**
552      * Returns the overlay to be drawn on the top of the magnifier, or
553      * {@code null} if no overlay should be drawn.
554      * @return the overlay
555      * @see Magnifier.Builder#setOverlay(Drawable)
556      */
557     @Nullable
getOverlay()558     public Drawable getOverlay() {
559         return mOverlay;
560     }
561 
562     /**
563      * Returns whether the magnifier position will be adjusted such that the magnifier will be
564      * fully within the bounds of the main application window, by also avoiding any overlap
565      * with system insets (such as the one corresponding to the status bar) i.e. whether the
566      * area where the magnifier can be positioned will be clipped to the main application window
567      * and the system insets.
568      * @return whether the magnifier position will be adjusted
569      * @see Magnifier.Builder#setClippingEnabled(boolean)
570      */
isClippingEnabled()571     public boolean isClippingEnabled() {
572         return mClippingEnabled;
573     }
574 
575     /**
576      * Returns the top left coordinates of the magnifier, relative to the main application
577      * window. They will be determined by the coordinates of the last {@link #show(float, float)}
578      * or {@link #show(float, float, float, float)} call, adjusted to take into account any
579      * potential clamping behavior. The method can be used immediately after a #show
580      * call to find out where the magnifier will be positioned. However, the position of the
581      * magnifier will not be updated visually in the same frame, due to the async nature of
582      * the content copying and of the magnifier rendering.
583      * The method will return {@code null} if #show has not yet been called, or if the last
584      * operation performed was a #dismiss.
585      *
586      * @return the top left coordinates of the magnifier
587      */
588     @Nullable
getPosition()589     public Point getPosition() {
590         if (mWindow == null) {
591             return null;
592         }
593         final Point position = getCurrentClampedWindowCoordinates();
594         position.offset(-mParentSurface.mInsets.left, -mParentSurface.mInsets.top);
595         return new Point(position);
596     }
597 
598     /**
599      * Returns the top left coordinates of the magnifier source (i.e. the view region going to
600      * be magnified and copied to the magnifier), relative to the window or surface the content
601      * is copied from. The content will be copied:
602      * - if the magnified view is a {@link SurfaceView}, from the surface backing it
603      * - otherwise, from the surface backing the main application window, and the coordinates
604      *   returned will be relative to the main application window
605      * The method will return {@code null} if #show has not yet been called, or if the last
606      * operation performed was a #dismiss.
607      *
608      * @return the top left coordinates of the magnifier source
609      */
610     @Nullable
getSourcePosition()611     public Point getSourcePosition() {
612         if (mWindow == null) {
613             return null;
614         }
615         final Point position = new Point(mPixelCopyRequestRect.left, mPixelCopyRequestRect.top);
616         position.offset(-mContentCopySurface.mInsets.left, -mContentCopySurface.mInsets.top);
617         return new Point(position);
618     }
619 
620     /**
621      * Retrieves the surfaces used by the magnifier:
622      * - a parent surface for the magnifier surface. This will usually be the main app window.
623      * - a surface where the magnified content will be copied from. This will be the main app
624      *   window unless the magnified view is a SurfaceView, in which case its backing surface
625      *   will be used.
626      */
obtainSurfaces()627     private void obtainSurfaces() {
628         // Get the main window surface.
629         SurfaceInfo validMainWindowSurface = SurfaceInfo.NULL;
630         if (mView.getViewRootImpl() != null) {
631             final ViewRootImpl viewRootImpl = mView.getViewRootImpl();
632             final Surface mainWindowSurface = viewRootImpl.mSurface;
633             if (mainWindowSurface != null && mainWindowSurface.isValid()) {
634                 final Rect surfaceInsets = viewRootImpl.mWindowAttributes.surfaceInsets;
635                 final int surfaceWidth =
636                         viewRootImpl.getWidth() + surfaceInsets.left + surfaceInsets.right;
637                 final int surfaceHeight =
638                         viewRootImpl.getHeight() + surfaceInsets.top + surfaceInsets.bottom;
639                 validMainWindowSurface =
640                         new SurfaceInfo(viewRootImpl.getSurfaceControl(), mainWindowSurface,
641                                 surfaceWidth, surfaceHeight, surfaceInsets, true);
642             }
643         }
644         // Get the surface backing the magnified view, if it is a SurfaceView.
645         SurfaceInfo validSurfaceViewSurface = SurfaceInfo.NULL;
646         if (mView instanceof SurfaceView) {
647             final SurfaceControl sc = ((SurfaceView) mView).getSurfaceControl();
648             final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder();
649             final Surface surfaceViewSurface = surfaceHolder.getSurface();
650 
651             if (sc != null && sc.isValid()) {
652                 final Rect surfaceFrame = surfaceHolder.getSurfaceFrame();
653                 validSurfaceViewSurface = new SurfaceInfo(sc, surfaceViewSurface,
654                         surfaceFrame.right, surfaceFrame.bottom, new Rect(), false);
655             }
656         }
657 
658         // Choose the parent surface for the magnifier and the source surface for the content.
659         mParentSurface = validMainWindowSurface != SurfaceInfo.NULL
660                 ? validMainWindowSurface : validSurfaceViewSurface;
661         mContentCopySurface = mView instanceof SurfaceView
662                 ? validSurfaceViewSurface : validMainWindowSurface;
663     }
664 
665     /**
666      * Computes the coordinates of the center of the content going to be displayed in the
667      * magnifier. These are relative to the surface the content is copied from.
668      */
obtainContentCoordinates(final float xPosInView, final float yPosInView)669     private void obtainContentCoordinates(final float xPosInView, final float yPosInView) {
670         final int prevViewXInSurface = mViewCoordinatesInSurface[0];
671         final int prevViewYInSurface = mViewCoordinatesInSurface[1];
672         mView.getLocationInSurface(mViewCoordinatesInSurface);
673         if (mViewCoordinatesInSurface[0] != prevViewXInSurface
674                 || mViewCoordinatesInSurface[1] != prevViewYInSurface) {
675             mDirtyState = true;
676         }
677 
678         final int zoomCenterX;
679         final int zoomCenterY;
680         if (mView instanceof SurfaceView) {
681             // No offset required if the backing Surface matches the size of the SurfaceView.
682             zoomCenterX = Math.round(xPosInView);
683             zoomCenterY = Math.round(yPosInView);
684         } else {
685             zoomCenterX = Math.round(xPosInView + mViewCoordinatesInSurface[0]);
686             zoomCenterY = Math.round(yPosInView + mViewCoordinatesInSurface[1]);
687         }
688 
689         final Rect[] bounds = new Rect[2]; // [MAX_IN_SURFACE, MAX_VISIBLE]
690         // Obtain the surface bounds rectangle.
691         final Rect surfaceBounds = new Rect(0, 0,
692                 mContentCopySurface.mWidth, mContentCopySurface.mHeight);
693         bounds[0] = surfaceBounds;
694         // Obtain the visible view region rectangle.
695         final Rect viewVisibleRegion = new Rect();
696         mView.getGlobalVisibleRect(viewVisibleRegion);
697         if (mView.getViewRootImpl() != null) {
698             // Clamping coordinates relative to the surface, not to the window.
699             final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets;
700             viewVisibleRegion.offset(surfaceInsets.left, surfaceInsets.top);
701         }
702         if (mView instanceof SurfaceView) {
703             // If we copy content from a SurfaceView, clamp coordinates relative to it.
704             viewVisibleRegion.offset(-mViewCoordinatesInSurface[0], -mViewCoordinatesInSurface[1]);
705         }
706         bounds[1] = viewVisibleRegion;
707 
708         // Aggregate the above to obtain the bounds where the content copy will be restricted.
709         int resolvedLeft = Integer.MIN_VALUE;
710         for (int i = mLeftContentBound; i >= 0; --i) {
711             resolvedLeft = Math.max(resolvedLeft, bounds[i].left);
712         }
713         int resolvedTop = Integer.MIN_VALUE;
714         for (int i = mTopContentBound; i >= 0; --i) {
715             resolvedTop = Math.max(resolvedTop, bounds[i].top);
716         }
717         int resolvedRight = Integer.MAX_VALUE;
718         for (int i = mRightContentBound; i >= 0; --i) {
719             resolvedRight = Math.min(resolvedRight, bounds[i].right);
720         }
721         int resolvedBottom = Integer.MAX_VALUE;
722         for (int i = mBottomContentBound; i >= 0; --i) {
723             resolvedBottom = Math.min(resolvedBottom, bounds[i].bottom);
724         }
725         // Adjust <left-right> and <top-bottom> pairs of bounds to make sense.
726         resolvedLeft = Math.min(resolvedLeft, mContentCopySurface.mWidth - mSourceWidth);
727         resolvedTop = Math.min(resolvedTop, mContentCopySurface.mHeight - mSourceHeight);
728         if (resolvedLeft < 0 || resolvedTop < 0) {
729             Log.e(TAG, "Magnifier's content is copied from a surface smaller than"
730                     + "the content requested size. The magnifier will be dismissed.");
731         }
732         resolvedRight = Math.max(resolvedRight, resolvedLeft + mSourceWidth);
733         resolvedBottom = Math.max(resolvedBottom, resolvedTop + mSourceHeight);
734 
735         // Finally compute the coordinates of the source center.
736         mClampedCenterZoomCoords.x = mIsFishEyeStyle
737                 ? Math.max(resolvedLeft, Math.min(zoomCenterX, resolvedRight))
738                 : Math.max(resolvedLeft + mSourceWidth / 2, Math.min(
739                         zoomCenterX, resolvedRight - mSourceWidth / 2));
740         mClampedCenterZoomCoords.y = Math.max(resolvedTop + mSourceHeight / 2, Math.min(
741                 zoomCenterY, resolvedBottom - mSourceHeight / 2));
742     }
743 
744     /**
745      * Computes the coordinates of the top left corner of the magnifier window.
746      * These are relative to the surface the magnifier window is attached to.
747      */
obtainWindowCoordinates(final float xWindowPos, final float yWindowPos)748     private void obtainWindowCoordinates(final float xWindowPos, final float yWindowPos) {
749         final int windowCenterX;
750         final int windowCenterY;
751         if (mView instanceof SurfaceView) {
752             // No offset required if the backing Surface matches the size of the SurfaceView.
753             windowCenterX = Math.round(xWindowPos);
754             windowCenterY = Math.round(yWindowPos);
755         } else {
756             windowCenterX = Math.round(xWindowPos + mViewCoordinatesInSurface[0]);
757             windowCenterY = Math.round(yWindowPos + mViewCoordinatesInSurface[1]);
758         }
759 
760         mWindowCoords.x = windowCenterX - mWindowWidth / 2;
761         mWindowCoords.y = windowCenterY - mWindowHeight / 2;
762         if (mParentSurface != mContentCopySurface) {
763             mWindowCoords.x += mViewCoordinatesInSurface[0];
764             mWindowCoords.y += mViewCoordinatesInSurface[1];
765         }
766     }
767 
maybeDrawCursor(Canvas canvas)768     private void maybeDrawCursor(Canvas canvas) {
769         if (mDrawCursorEnabled) {
770             if (mCursorDrawable != null) {
771                 mCursorDrawable.setBounds(
772                         mSourceWidth / 2, 0,
773                         mSourceWidth / 2 + mCursorDrawable.getIntrinsicWidth(), mSourceHeight);
774                 mCursorDrawable.draw(canvas);
775             } else {
776                 Paint paint = new Paint();
777                 paint.setColor(Color.BLACK);  // The cursor on magnifier is by default in black.
778                 canvas.drawRect(
779                         new Rect(mSourceWidth / 2 - 1, 0, mSourceWidth / 2 + 1, mSourceHeight),
780                         paint);
781             }
782         }
783     }
784 
performPixelCopy(final int startXInSurface, final int startYInSurface, final boolean updateWindowPosition)785     private void performPixelCopy(final int startXInSurface, final int startYInSurface,
786             final boolean updateWindowPosition) {
787         if (mContentCopySurface.mSurface == null || !mContentCopySurface.mSurface.isValid()) {
788             onPixelCopyFailed();
789             return;
790         }
791 
792         // Clamp window coordinates inside the parent surface, to avoid displaying
793         // the magnifier out of screen or overlapping with system insets.
794         final Point windowCoords = getCurrentClampedWindowCoordinates();
795 
796         // Perform the pixel copy.
797         mPixelCopyRequestRect.set(startXInSurface,
798                 startYInSurface,
799                 startXInSurface + mSourceWidth - mLeftCutWidth - mRightCutWidth,
800                 startYInSurface + mSourceHeight);
801         mPrevStartCoordsInSurface.x = startXInSurface;
802         mPrevStartCoordsInSurface.y = startYInSurface;
803         mDirtyState = false;
804 
805         final InternalPopupWindow currentWindowInstance = mWindow;
806         if (mPixelCopyRequestRect.width() == 0) {
807             // If the copy rect is empty, updates an empty bitmap to the window.
808             mWindow.updateContent(
809                     Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ALPHA_8));
810             return;
811         }
812         final Bitmap bitmap =
813                 Bitmap.createBitmap(mSourceWidth - mLeftCutWidth - mRightCutWidth,
814                         mSourceHeight, Bitmap.Config.ARGB_8888);
815         PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap,
816                 result -> {
817                     if (result != PixelCopy.SUCCESS) {
818                         onPixelCopyFailed();
819                         return;
820                     }
821                     synchronized (mLock) {
822                         if (mWindow != currentWindowInstance) {
823                             // The magnifier was dismissed (and maybe shown again) in the meantime.
824                             return;
825                         }
826                         if (updateWindowPosition) {
827                             // TODO: pull the position update outside #performPixelCopy
828                             mWindow.setContentPositionForNextDraw(windowCoords.x,
829                                     windowCoords.y);
830                         }
831                         if (bitmap.getWidth() < mSourceWidth) {
832                             // When bitmap width has been cut, re-fills it with full width bitmap.
833                             // This only happens in new styled magnifier.
834                             final Bitmap newBitmap = Bitmap.createBitmap(
835                                     mSourceWidth, bitmap.getHeight(), bitmap.getConfig());
836                             final Canvas can = new Canvas(newBitmap);
837                             final Rect dstRect = new Rect(mLeftCutWidth, 0,
838                                     mSourceWidth - mRightCutWidth, bitmap.getHeight());
839                             can.drawBitmap(bitmap, null, dstRect, null);
840                             maybeDrawCursor(can);
841                             mWindow.updateContent(newBitmap);
842                         } else {
843                             maybeDrawCursor(new Canvas(bitmap));
844                             mWindow.updateContent(bitmap);
845                         }
846                     }
847                 },
848                 sPixelCopyHandlerThread.getThreadHandler());
849     }
850 
onPixelCopyFailed()851     private void onPixelCopyFailed() {
852         Log.e(TAG, "Magnifier failed to copy content from the view Surface. It will be dismissed.");
853         // Post to make sure #dismiss is done on the main thread.
854         Handler.getMain().postAtFrontOfQueue(() -> {
855             dismiss();
856             if (mCallback != null) {
857                 mCallback.onOperationComplete();
858             }
859         });
860     }
861 
862     /**
863      * Clamp window coordinates inside the surface the magnifier is attached to, to avoid
864      * displaying the magnifier out of screen or overlapping with system insets.
865      * @return the current window coordinates, after they are clamped inside the parent surface
866      */
getCurrentClampedWindowCoordinates()867     private Point getCurrentClampedWindowCoordinates() {
868         if (!mClippingEnabled) {
869             // No position adjustment should be done, so return the raw coordinates.
870             return new Point(mWindowCoords);
871         }
872 
873         final Rect windowBounds;
874         if (mParentSurface.mIsMainWindowSurface) {
875             final Insets systemInsets = mView.getRootWindowInsets().getSystemWindowInsets();
876             windowBounds = new Rect(
877                     systemInsets.left + mParentSurface.mInsets.left,
878                     systemInsets.top + mParentSurface.mInsets.top,
879                     mParentSurface.mWidth - systemInsets.right - mParentSurface.mInsets.right,
880                     mParentSurface.mHeight - systemInsets.bottom
881                             - mParentSurface.mInsets.bottom
882             );
883         } else {
884             windowBounds = new Rect(0, 0, mParentSurface.mWidth, mParentSurface.mHeight);
885         }
886         final int windowCoordsX = Math.max(windowBounds.left,
887                 Math.min(windowBounds.right - mWindowWidth, mWindowCoords.x));
888         final int windowCoordsY = Math.max(windowBounds.top,
889                 Math.min(windowBounds.bottom - mWindowHeight, mWindowCoords.y));
890         return new Point(windowCoordsX, windowCoordsY);
891     }
892 
893     /**
894      * Contains a surface and metadata corresponding to it.
895      */
896     private static class SurfaceInfo {
897         public static final SurfaceInfo NULL = new SurfaceInfo(null, null, 0, 0, null, false);
898 
899         private Surface mSurface;
900         private SurfaceControl mSurfaceControl;
901         private int mWidth;
902         private int mHeight;
903         private Rect mInsets;
904         private boolean mIsMainWindowSurface;
905 
SurfaceInfo(final SurfaceControl surfaceControl, final Surface surface, final int width, final int height, final Rect insets, final boolean isMainWindowSurface)906         SurfaceInfo(final SurfaceControl surfaceControl, final Surface surface,
907                 final int width, final int height, final Rect insets,
908                 final boolean isMainWindowSurface) {
909             mSurfaceControl = surfaceControl;
910             mSurface = surface;
911             mWidth = width;
912             mHeight = height;
913             mInsets = insets;
914             mIsMainWindowSurface = isMainWindowSurface;
915         }
916     }
917 
918     /**
919      * Magnifier's own implementation of PopupWindow-similar floating window.
920      * This exists to ensure frame-synchronization between window position updates and window
921      * content updates. By using a PopupWindow, these events would happen in different frames,
922      * producing a shakiness effect for the magnifier content.
923      */
924     private static class InternalPopupWindow {
925         // The z of the magnifier surface, defining its z order in the list of
926         // siblings having the same parent surface (usually the main app surface).
927         private static final int SURFACE_Z = 5;
928 
929         // Display associated to the view the magnifier is attached to.
930         private final Display mDisplay;
931         // The size of the content of the magnifier.
932         private final int mContentWidth;
933         private int mContentHeight;
934         // The insets of the content inside the allocated surface.
935         private final int mOffsetX;
936         private final int mOffsetY;
937         // The overlay to be drawn on the top of the content.
938         private final Drawable mOverlay;
939         // The surface we allocate for the magnifier content + shadow.
940         private final SurfaceSession mSurfaceSession;
941         private final SurfaceControl mSurfaceControl;
942         private final SurfaceControl mBbqSurfaceControl;
943         private final BLASTBufferQueue mBBQ;
944         private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
945         private final Surface mSurface;
946         // The renderer used for the allocated surface.
947         private final ThreadedRenderer.SimpleRenderer mRenderer;
948         // The RenderNode used to draw the magnifier content in the surface.
949         private final RenderNode mBitmapRenderNode;
950         // The RenderNode used to draw the overlay over the magnifier content.
951         private final RenderNode mOverlayRenderNode;
952         // The job that will be post'd to apply the pending magnifier updates to the surface.
953         private final Runnable mMagnifierUpdater;
954         // The handler where the magnifier updater jobs will be post'd.
955         private final Handler mHandler;
956         // The callback to be run after the next draw.
957         private Callback mCallback;
958 
959         // Members below describe the state of the magnifier. Reads/writes to them
960         // have to be synchronized between the UI thread and the thread that handles
961         // the pixel copy results. This is the purpose of mLock.
962         private final Object mLock;
963         // Whether a magnifier frame draw is currently pending in the UI thread queue.
964         private boolean mFrameDrawScheduled;
965         // The content bitmap, as returned by pixel copy.
966         private Bitmap mBitmap;
967         // Whether the next draw will be the first one for the current instance.
968         private boolean mFirstDraw = true;
969         // The window position in the parent surface. Might be applied during the next draw,
970         // when mPendingWindowPositionUpdate is true.
971         private int mWindowPositionX;
972         private int mWindowPositionY;
973         private boolean mPendingWindowPositionUpdate;
974 
975         // The current content of the magnifier. It is mBitmap + mOverlay, only used for testing.
976         private Bitmap mCurrentContent;
977 
978         private float mZoom;
979         // The width of the ramp region in pixels on the left & right sides of the fish-eye effect.
980         private final int mRamp;
981         // Whether is in the new magnifier style.
982         private boolean mIsFishEyeStyle;
983         // The mesh matrix for the fish-eye effect.
984         private float[] mMeshLeft;
985         private float[] mMeshRight;
986         private int mMeshWidth;
987         private int mMeshHeight;
988 
InternalPopupWindow(final Context context, final Display display, final SurfaceControl parentSurfaceControl, final int width, final int height, final float zoom, final int ramp, final float elevation, final float cornerRadius, final Drawable overlay, final Handler handler, final Object lock, final Callback callback, final boolean isFishEyeStyle)989         InternalPopupWindow(final Context context, final Display display,
990                 final SurfaceControl parentSurfaceControl, final int width, final int height,
991                 final float zoom, final int ramp, final float elevation, final float cornerRadius,
992                 final Drawable overlay, final Handler handler, final Object lock,
993                 final Callback callback, final boolean isFishEyeStyle) {
994             mDisplay = display;
995             mOverlay = overlay;
996             mLock = lock;
997             mCallback = callback;
998 
999             mContentWidth = width;
1000             mContentHeight = height;
1001             mZoom = zoom;
1002             mRamp = ramp;
1003             mOffsetX = (int) (1.05f * elevation);
1004             mOffsetY = (int) (1.05f * elevation);
1005             // Setup the surface we will use for drawing the content and shadow.
1006             final int surfaceWidth = mContentWidth + 2 * mOffsetX;
1007             final int surfaceHeight = mContentHeight + 2 * mOffsetY;
1008             mSurfaceSession = new SurfaceSession();
1009             mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
1010                     .setName("magnifier surface")
1011                     .setFlags(SurfaceControl.HIDDEN)
1012                     .setContainerLayer()
1013                     .setParent(parentSurfaceControl)
1014                     .setCallsite("InternalPopupWindow")
1015                     .build();
1016             mBbqSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
1017                     .setName("magnifier surface bbq wrapper")
1018                     .setHidden(false)
1019                     .setBLASTLayer()
1020                     .setParent(mSurfaceControl)
1021                     .setCallsite("InternalPopupWindow")
1022                     .build();
1023 
1024             mBBQ = new BLASTBufferQueue("magnifier surface", mBbqSurfaceControl,
1025                 surfaceWidth, surfaceHeight, PixelFormat.TRANSLUCENT);
1026             mSurface = mBBQ.createSurface();
1027 
1028             // Setup the RenderNode tree. The root has two children, one containing the bitmap
1029             // and one containing the overlay. We use a separate render node for the overlay
1030             // to avoid drawing this as the same rate we do for content.
1031             mRenderer = new ThreadedRenderer.SimpleRenderer(
1032                     context,
1033                     "magnifier renderer",
1034                     mSurface
1035             );
1036             mBitmapRenderNode = createRenderNodeForBitmap(
1037                     "magnifier content",
1038                     elevation,
1039                     cornerRadius
1040             );
1041             mOverlayRenderNode = createRenderNodeForOverlay(
1042                     "magnifier overlay",
1043                     cornerRadius
1044             );
1045             setupOverlay();
1046 
1047             final RecordingCanvas canvas = mRenderer.getRootNode().beginRecording(width, height);
1048             try {
1049                 canvas.enableZ();
1050                 canvas.drawRenderNode(mBitmapRenderNode);
1051                 canvas.disableZ();
1052                 canvas.drawRenderNode(mOverlayRenderNode);
1053                 canvas.disableZ();
1054             } finally {
1055                 mRenderer.getRootNode().endRecording();
1056             }
1057             if (mCallback != null) {
1058                 mCurrentContent =
1059                         Bitmap.createBitmap(mContentWidth, mContentHeight, Bitmap.Config.ARGB_8888);
1060                 updateCurrentContentForTesting();
1061             }
1062 
1063             // Initialize the update job and the handler where this will be post'd.
1064             mHandler = handler;
1065             mMagnifierUpdater = this::doDraw;
1066             mFrameDrawScheduled = false;
1067             mIsFishEyeStyle = isFishEyeStyle;
1068 
1069             if (mIsFishEyeStyle) {
1070                 createMeshMatrixForFishEyeEffect();
1071             }
1072         }
1073 
1074         /**
1075          * Updates the factors of content which may resize the window.
1076          * @param contentHeight the new height of content.
1077          * @param zoom the new zoom factor.
1078          */
updateContentFactors(final int contentHeight, final float zoom)1079         private void updateContentFactors(final int contentHeight, final float zoom) {
1080             if (mContentHeight == contentHeight && mZoom == zoom) {
1081               return;
1082             }
1083             if (mContentHeight < contentHeight) {
1084                 // Grows the surface height as necessary.
1085                 mBBQ.update(mBbqSurfaceControl, mContentWidth, contentHeight,
1086                     PixelFormat.TRANSLUCENT);
1087                 mRenderer.setSurface(mSurface);
1088 
1089                 final Outline outline = new Outline();
1090                 outline.setRoundRect(0, 0, mContentWidth, contentHeight, 0);
1091                 outline.setAlpha(1.0f);
1092 
1093                 mBitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
1094                         mOffsetX + mContentWidth, mOffsetY + contentHeight);
1095                 mBitmapRenderNode.setOutline(outline);
1096 
1097                 mOverlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
1098                         mOffsetX + mContentWidth, mOffsetY + contentHeight);
1099                 mOverlayRenderNode.setOutline(outline);
1100 
1101                 final RecordingCanvas canvas =
1102                         mRenderer.getRootNode().beginRecording(mContentWidth, contentHeight);
1103                 try {
1104                     canvas.enableZ();
1105                     canvas.drawRenderNode(mBitmapRenderNode);
1106                     canvas.disableZ();
1107                     canvas.drawRenderNode(mOverlayRenderNode);
1108                     canvas.disableZ();
1109                 } finally {
1110                     mRenderer.getRootNode().endRecording();
1111                 }
1112             }
1113             mContentHeight = contentHeight;
1114             mZoom = zoom;
1115             fillMeshMatrix();
1116         }
1117 
createMeshMatrixForFishEyeEffect()1118         private void createMeshMatrixForFishEyeEffect() {
1119             mMeshWidth = 1;
1120             mMeshHeight = 6;
1121             mMeshLeft = new float[2 * (mMeshWidth + 1) * (mMeshHeight + 1)];
1122             mMeshRight = new float[2 * (mMeshWidth + 1) * (mMeshHeight + 1)];
1123             fillMeshMatrix();
1124         }
1125 
fillMeshMatrix()1126         private void fillMeshMatrix() {
1127             mMeshWidth = 1;
1128             mMeshHeight = 6;
1129             final float w = mContentWidth;
1130             final float h = mContentHeight;
1131             final float h0 = h / mZoom;
1132             final float dh = h - h0;
1133             for (int i = 0; i < 2 * (mMeshWidth + 1) * (mMeshHeight + 1); i += 2) {
1134                 // Calculates X value.
1135                 final int colIndex = i % (2 * (mMeshWidth + 1)) / 2;
1136                 mMeshLeft[i] = (float) colIndex * mRamp / mMeshWidth;
1137                 mMeshRight[i] = w - mRamp + colIndex * mRamp / mMeshWidth;
1138 
1139                 // Calculates Y value.
1140                 final int rowIndex = i / 2 / (mMeshWidth + 1);
1141                 final float hl = h0 + dh * colIndex / mMeshWidth;
1142                 final float yl = (h - hl) / 2;
1143                 mMeshLeft[i + 1] = yl + hl * rowIndex / mMeshHeight;
1144                 final float hr = h - dh * colIndex / mMeshWidth;
1145                 final float yr = (h - hr) / 2;
1146                 mMeshRight[i + 1] = yr + hr * rowIndex / mMeshHeight;
1147             }
1148         }
1149 
createRenderNodeForBitmap(final String name, final float elevation, final float cornerRadius)1150         private RenderNode createRenderNodeForBitmap(final String name,
1151                 final float elevation, final float cornerRadius) {
1152             final RenderNode bitmapRenderNode = RenderNode.create(name, null);
1153 
1154             // Define the position of the bitmap in the parent render node. The surface regions
1155             // outside the bitmap are used to draw elevation.
1156             bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
1157                     mOffsetX + mContentWidth, mOffsetY + mContentHeight);
1158             bitmapRenderNode.setElevation(elevation);
1159 
1160             final Outline outline = new Outline();
1161             outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
1162             outline.setAlpha(1.0f);
1163             bitmapRenderNode.setOutline(outline);
1164             bitmapRenderNode.setClipToOutline(true);
1165 
1166             // Create a placeholder draw, which will be replaced later with real drawing.
1167             final RecordingCanvas canvas = bitmapRenderNode.beginRecording(
1168                     mContentWidth, mContentHeight);
1169             try {
1170                 canvas.drawColor(0xFF00FF00);
1171             } finally {
1172                 bitmapRenderNode.endRecording();
1173             }
1174 
1175             return bitmapRenderNode;
1176         }
1177 
createRenderNodeForOverlay(final String name, final float cornerRadius)1178         private RenderNode createRenderNodeForOverlay(final String name, final float cornerRadius) {
1179             final RenderNode overlayRenderNode = RenderNode.create(name, null);
1180 
1181             // Define the position of the overlay in the parent render node.
1182             // This coincides with the position of the content.
1183             overlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
1184                     mOffsetX + mContentWidth, mOffsetY + mContentHeight);
1185 
1186             final Outline outline = new Outline();
1187             outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
1188             outline.setAlpha(1.0f);
1189             overlayRenderNode.setOutline(outline);
1190             overlayRenderNode.setClipToOutline(true);
1191 
1192             return overlayRenderNode;
1193         }
1194 
setupOverlay()1195         private void setupOverlay() {
1196             drawOverlay();
1197 
1198             mOverlay.setCallback(new Drawable.Callback() {
1199                 @Override
1200                 public void invalidateDrawable(Drawable who) {
1201                     // When the overlay drawable is invalidated, redraw it to the render node.
1202                     drawOverlay();
1203                     if (mCallback != null) {
1204                         updateCurrentContentForTesting();
1205                     }
1206                 }
1207 
1208                 @Override
1209                 public void scheduleDrawable(Drawable who, Runnable what, long when) {
1210                     Handler.getMain().postAtTime(what, who, when);
1211                 }
1212 
1213                 @Override
1214                 public void unscheduleDrawable(Drawable who, Runnable what) {
1215                     Handler.getMain().removeCallbacks(what, who);
1216                 }
1217             });
1218         }
1219 
drawOverlay()1220         private void drawOverlay() {
1221             // Draw the drawable to the render node. This happens once during
1222             // initialization and whenever the overlay drawable is invalidated.
1223             final RecordingCanvas canvas =
1224                     mOverlayRenderNode.beginRecording(mContentWidth, mContentHeight);
1225             try {
1226                 mOverlay.setBounds(0, 0, mContentWidth, mContentHeight);
1227                 mOverlay.draw(canvas);
1228             } finally {
1229                 mOverlayRenderNode.endRecording();
1230             }
1231         }
1232 
1233         /**
1234          * Sets the position of the magnifier content relative to the parent surface.
1235          * The position update will happen in the same frame with the next draw.
1236          * The method has to be called in a context that holds {@link #mLock}.
1237          *
1238          * @param contentX the x coordinate of the content
1239          * @param contentY the y coordinate of the content
1240          */
setContentPositionForNextDraw(final int contentX, final int contentY)1241         public void setContentPositionForNextDraw(final int contentX, final int contentY) {
1242             mWindowPositionX = contentX - mOffsetX;
1243             mWindowPositionY = contentY - mOffsetY;
1244             mPendingWindowPositionUpdate = true;
1245             requestUpdate();
1246         }
1247 
1248         /**
1249          * Sets the content that should be displayed in the magnifier.
1250          * The update happens immediately, and possibly triggers a pending window movement set
1251          * by {@link #setContentPositionForNextDraw(int, int)}.
1252          * The method has to be called in a context that holds {@link #mLock}.
1253          *
1254          * @param bitmap the content bitmap
1255          */
updateContent(final @NonNull Bitmap bitmap)1256         public void updateContent(final @NonNull Bitmap bitmap) {
1257             if (mBitmap != null) {
1258                 mBitmap.recycle();
1259             }
1260             mBitmap = bitmap;
1261             requestUpdate();
1262         }
1263 
requestUpdate()1264         private void requestUpdate() {
1265             if (mFrameDrawScheduled) {
1266                 return;
1267             }
1268             final Message request = Message.obtain(mHandler, mMagnifierUpdater);
1269             request.setAsynchronous(true);
1270             request.sendToTarget();
1271             mFrameDrawScheduled = true;
1272         }
1273 
1274         /**
1275          * Destroys this instance. The method has to be called in a context holding {@link #mLock}.
1276          */
destroy()1277         public void destroy() {
1278             // Destroy the renderer. This will not proceed until pending frame callbacks complete.
1279             mRenderer.destroy();
1280             mSurface.destroy();
1281             mBBQ.destroy();
1282             new SurfaceControl.Transaction()
1283                     .remove(mSurfaceControl)
1284                     .remove(mBbqSurfaceControl)
1285                     .apply();
1286             mSurfaceSession.kill();
1287             mHandler.removeCallbacks(mMagnifierUpdater);
1288             if (mBitmap != null) {
1289                 mBitmap.recycle();
1290             }
1291             mOverlay.setCallback(null);
1292         }
1293 
doDraw()1294         private void doDraw() {
1295             final ThreadedRenderer.FrameDrawingCallback callback;
1296 
1297             // Draw the current bitmap to the surface, and prepare the callback which updates the
1298             // surface position. These have to be in the same synchronized block, in order to
1299             // guarantee the consistency between the bitmap content and the surface position.
1300             synchronized (mLock) {
1301                 if (!mSurface.isValid()) {
1302                     // Probably #destroy() was called for the current instance, so we skip the draw.
1303                     return;
1304                 }
1305 
1306                 final RecordingCanvas canvas =
1307                         mBitmapRenderNode.beginRecording(mContentWidth, mContentHeight);
1308                 try {
1309                     final int w = mBitmap.getWidth();
1310                     final int h = mBitmap.getHeight();
1311                     final Paint paint = new Paint();
1312                     paint.setFilterBitmap(true);
1313                     if (mIsFishEyeStyle) {
1314                         final int margin =
1315                             (int)((mContentWidth - (mContentWidth - 2 * mRamp) / mZoom) / 2);
1316 
1317                         // Draws the middle part.
1318                         final Rect srcRect = new Rect(margin, 0, w - margin, h);
1319                         final Rect dstRect = new Rect(
1320                             mRamp, 0, mContentWidth - mRamp, mContentHeight);
1321                         canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
1322 
1323                         // Draws the left/right parts with mesh matrixes.
1324                         canvas.drawBitmapMesh(
1325                                 Bitmap.createBitmap(mBitmap, 0, 0, margin, h),
1326                                 mMeshWidth, mMeshHeight, mMeshLeft, 0, null, 0, paint);
1327                         canvas.drawBitmapMesh(
1328                                 Bitmap.createBitmap(mBitmap, w - margin, 0, margin, h),
1329                                 mMeshWidth, mMeshHeight, mMeshRight, 0, null, 0, paint);
1330                     } else {
1331                         final Rect srcRect = new Rect(0, 0, w, h);
1332                         final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
1333                         canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
1334                     }
1335                 } finally {
1336                     mBitmapRenderNode.endRecording();
1337                 }
1338                 if (mPendingWindowPositionUpdate || mFirstDraw) {
1339                     // If the window has to be shown or moved, defer this until the next draw.
1340                     final boolean firstDraw = mFirstDraw;
1341                     mFirstDraw = false;
1342                     final boolean updateWindowPosition = mPendingWindowPositionUpdate;
1343                     mPendingWindowPositionUpdate = false;
1344                     final int pendingX = mWindowPositionX;
1345                     final int pendingY = mWindowPositionY;
1346 
1347                     callback = frame -> {
1348                         if (!mSurface.isValid()) {
1349                             return;
1350                         }
1351                         if (updateWindowPosition) {
1352                             mTransaction.setPosition(mSurfaceControl, pendingX, pendingY);
1353                         }
1354                         if (firstDraw) {
1355                             mTransaction.setLayer(mSurfaceControl, SURFACE_Z)
1356                                 .show(mSurfaceControl);
1357 
1358                         }
1359                         // Show or move the window at the content draw frame.
1360                         mBBQ.mergeWithNextTransaction(mTransaction, frame);
1361                     };
1362                     if (!mIsFishEyeStyle) {
1363                         // The new style magnifier doesn't need the light/shadow.
1364                         mRenderer.setLightCenter(mDisplay, pendingX, pendingY);
1365                     }
1366                 } else {
1367                     callback = null;
1368                 }
1369 
1370                 mFrameDrawScheduled = false;
1371             }
1372 
1373             mRenderer.draw(callback);
1374             if (mCallback != null) {
1375                 // The current content bitmap is only used in testing, so, for performance,
1376                 // we only want to update it when running tests. For this, we check that
1377                 // mCallback is not null, as it can only be set from a @TestApi.
1378                 updateCurrentContentForTesting();
1379                 mCallback.onOperationComplete();
1380             }
1381         }
1382 
1383         /**
1384          * Updates mCurrentContent, which reproduces what is currently supposed to be
1385          * drawn in the magnifier. mCurrentContent is only used for testing, so this method
1386          * should only be called otherwise.
1387          */
updateCurrentContentForTesting()1388         private void updateCurrentContentForTesting() {
1389             final Canvas canvas = new Canvas(mCurrentContent);
1390             final Rect bounds = new Rect(0, 0, mContentWidth, mContentHeight);
1391             if (mBitmap != null && !mBitmap.isRecycled()) {
1392                 final Rect originalBounds = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
1393                 canvas.drawBitmap(mBitmap, originalBounds, bounds, null);
1394             }
1395             mOverlay.setBounds(bounds);
1396             mOverlay.draw(canvas);
1397         }
1398     }
1399 
1400     /**
1401      * Builder class for {@link Magnifier} objects.
1402      */
1403     public static final class Builder {
1404         private @NonNull View mView;
1405         private @Px @IntRange(from = 0) int mWidth;
1406         private @Px @IntRange(from = 0) int mHeight;
1407         private float mZoom;
1408         private @FloatRange(from = 0f) float mElevation;
1409         private @FloatRange(from = 0f) float mCornerRadius;
1410         private @Nullable Drawable mOverlay;
1411         private int mHorizontalDefaultSourceToMagnifierOffset;
1412         private int mVerticalDefaultSourceToMagnifierOffset;
1413         private boolean mClippingEnabled;
1414         private @SourceBound int mLeftContentBound;
1415         private @SourceBound int mTopContentBound;
1416         private @SourceBound int mRightContentBound;
1417         private @SourceBound int  mBottomContentBound;
1418         private boolean mIsFishEyeStyle;
1419         private int mSourceWidth;
1420         private int mSourceHeight;
1421 
1422         /**
1423          * Construct a new builder for {@link Magnifier} objects.
1424          * @param view the view this magnifier is attached to
1425          */
Builder(@onNull View view)1426         public Builder(@NonNull View view) {
1427             mView = Objects.requireNonNull(view);
1428             applyDefaults();
1429         }
1430 
applyDefaults()1431         private void applyDefaults() {
1432             final Resources resources = mView.getContext().getResources();
1433             mWidth = resources.getDimensionPixelSize(R.dimen.default_magnifier_width);
1434             mHeight = resources.getDimensionPixelSize(R.dimen.default_magnifier_height);
1435             mElevation = resources.getDimension(R.dimen.default_magnifier_elevation);
1436             mCornerRadius = resources.getDimension(R.dimen.default_magnifier_corner_radius);
1437             mZoom = resources.getFloat(R.dimen.default_magnifier_zoom);
1438             mHorizontalDefaultSourceToMagnifierOffset =
1439                     resources.getDimensionPixelSize(R.dimen.default_magnifier_horizontal_offset);
1440             mVerticalDefaultSourceToMagnifierOffset =
1441                     resources.getDimensionPixelSize(R.dimen.default_magnifier_vertical_offset);
1442             mOverlay = new ColorDrawable(resources.getColor(
1443                     R.color.default_magnifier_color_overlay, null));
1444             mClippingEnabled = true;
1445             mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
1446             mTopContentBound = SOURCE_BOUND_MAX_VISIBLE;
1447             mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
1448             mBottomContentBound = SOURCE_BOUND_MAX_VISIBLE;
1449             mIsFishEyeStyle = false;
1450         }
1451 
1452         /**
1453          * Sets the size of the magnifier window, in pixels. Defaults to (100dp, 48dp).
1454          * Note that the size of the content being magnified and copied to the magnifier
1455          * will be computed as (window width / zoom, window height / zoom).
1456          * @param width the window width to be set
1457          * @param height the window height to be set
1458          */
1459         @NonNull
setSize(@x @ntRangefrom = 0) int width, @Px @IntRange(from = 0) int height)1460         public Builder setSize(@Px @IntRange(from = 0) int width,
1461                 @Px @IntRange(from = 0) int height) {
1462             Preconditions.checkArgumentPositive(width, "Width should be positive");
1463             Preconditions.checkArgumentPositive(height, "Height should be positive");
1464             mWidth = width;
1465             mHeight = height;
1466             return this;
1467         }
1468 
1469         /**
1470          * Sets the zoom to be applied to the chosen content before being copied to the magnifier.
1471          * A content of size (content_width, content_height) will be magnified to
1472          * (content_width * zoom, content_height * zoom), which will coincide with the size
1473          * of the magnifier. A zoom of 1 will translate to no magnification (the content will
1474          * be just copied to the magnifier with no scaling). The zoom defaults to 1.25.
1475          * Note that the zoom can also be changed after the instance is built, using the
1476          * {@link Magnifier#setZoom(float)} method.
1477          * @param zoom the zoom to be set
1478          */
1479         @NonNull
setInitialZoom(@loatRangefrom = 0f) float zoom)1480         public Builder setInitialZoom(@FloatRange(from = 0f) float zoom) {
1481             Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
1482             mZoom = zoom;
1483             return this;
1484         }
1485 
1486         /**
1487          * Sets the elevation of the magnifier window, in pixels. Defaults to 4dp.
1488          * @param elevation the elevation to be set
1489          */
1490         @NonNull
setElevation(@x @loatRangefrom = 0) float elevation)1491         public Builder setElevation(@Px @FloatRange(from = 0) float elevation) {
1492             Preconditions.checkArgumentNonNegative(elevation, "Elevation should be non-negative");
1493             mElevation = elevation;
1494             return this;
1495         }
1496 
1497         /**
1498          * Sets the corner radius of the magnifier window, in pixels. Defaults to 2dp.
1499          * @param cornerRadius the corner radius to be set
1500          */
1501         @NonNull
setCornerRadius(@x @loatRangefrom = 0) float cornerRadius)1502         public Builder setCornerRadius(@Px @FloatRange(from = 0) float cornerRadius) {
1503             Preconditions.checkArgumentNonNegative(cornerRadius,
1504                     "Corner radius should be non-negative");
1505             mCornerRadius = cornerRadius;
1506             return this;
1507         }
1508 
1509         /**
1510          * Sets an overlay that will be drawn on the top of the magnifier.
1511          * In general, the overlay should not be opaque, in order to let the magnified
1512          * content be partially visible in the magnifier. The default overlay is {@code null}
1513          * (no overlay). As an example, TextView applies a white {@link ColorDrawable}
1514          * overlay with 5% alpha, aiming to make the magnifier distinguishable when shown in dark
1515          * application regions. To disable the overlay, the parameter should be set
1516          * to {@code null}. If not null, the overlay will be automatically redrawn
1517          * when the drawable is invalidated. To achieve this, the magnifier will set a new
1518          * {@link android.graphics.drawable.Drawable.Callback} for the overlay drawable,
1519          * so keep in mind that any existing one set by the application will be lost.
1520          * @param overlay the overlay to be drawn on top
1521          */
1522         @NonNull
setOverlay(@ullable Drawable overlay)1523         public Builder setOverlay(@Nullable Drawable overlay) {
1524             mOverlay = overlay;
1525             return this;
1526         }
1527 
1528         /**
1529          * Sets an offset that should be added to the content source center to obtain
1530          * the position of the magnifier window, when the {@link #show(float, float)}
1531          * method is called. The offset is ignored when {@link #show(float, float, float, float)}
1532          * is used. The offset can be negative. It defaults to (0dp, 0dp).
1533          * @param horizontalOffset the horizontal component of the offset
1534          * @param verticalOffset the vertical component of the offset
1535          */
1536         @NonNull
setDefaultSourceToMagnifierOffset(@x int horizontalOffset, @Px int verticalOffset)1537         public Builder setDefaultSourceToMagnifierOffset(@Px int horizontalOffset,
1538                 @Px int verticalOffset) {
1539             mHorizontalDefaultSourceToMagnifierOffset = horizontalOffset;
1540             mVerticalDefaultSourceToMagnifierOffset = verticalOffset;
1541             return this;
1542         }
1543 
1544         /**
1545          * Defines the behavior of the magnifier when it is requested to position outside the
1546          * surface of the main application window. The default value is {@code true}, which means
1547          * that the position will be adjusted such that the magnifier will be fully within the
1548          * bounds of the main application window, while also avoiding any overlap with system insets
1549          * (such as the one corresponding to the status bar). If this flag is set to {@code false},
1550          * the area where the magnifier can be positioned will no longer be clipped, so the
1551          * magnifier will be able to extend outside the main application window boundaries (and also
1552          * overlap the system insets). This can be useful if you require a custom behavior, but it
1553          * should be handled with care, when passing coordinates to {@link #show(float, float)};
1554          * note that:
1555          * <ul>
1556          *   <li>in a multiwindow context, if the magnifier crosses the boundary between the two
1557          *   windows, it will not be able to show over the window of the other application</li>
1558          *   <li>if the magnifier overlaps the status bar, there is no guarantee about which one
1559          *   will be displayed on top. This should be handled with care.</li>
1560          * </ul>
1561          * @param clip whether the magnifier position will be adjusted
1562          */
1563         @NonNull
setClippingEnabled(boolean clip)1564         public Builder setClippingEnabled(boolean clip) {
1565             mClippingEnabled = clip;
1566             return this;
1567         }
1568 
1569         /**
1570          * Defines the bounds of the rectangle where the magnifier will be able to copy its content
1571          * from. The content will always be copied from the {@link Surface} of the main application
1572          * window unless the magnified view is a {@link SurfaceView}, in which case its backing
1573          * surface will be used. Each bound can have a different behavior, with the options being:
1574          * <ul>
1575          *   <li>{@link #SOURCE_BOUND_MAX_VISIBLE}, which extends the bound as much as possible
1576          *   while remaining in the visible region of the magnified view, as given by
1577          *   {@link android.view.View#getGlobalVisibleRect(Rect)}. For example, this will take into
1578          *   account the case when the view is contained in a scrollable container, and the
1579          *   magnifier will refuse to copy content outside of the visible view region</li>
1580          *   <li>{@link #SOURCE_BOUND_MAX_IN_SURFACE}, which extends the bound as much
1581          *   as possible while remaining inside the surface the content is copied from.</li>
1582          * </ul>
1583          * Note that if either of the first three options is used, the bound will be compared to
1584          * the bound of the surface (i.e. as if {@link #SOURCE_BOUND_MAX_IN_SURFACE} was used),
1585          * and the more restrictive one will be chosen. In other words, no attempt to copy content
1586          * from outside the surface will be permitted. If two opposite bounds are not well-behaved
1587          * (i.e. left + sourceWidth > right or top + sourceHeight > bottom), the left and top
1588          * bounds will have priority and the others will be extended accordingly. If the pairs
1589          * obtained this way still remain out of bounds, the smallest possible offset will be added
1590          * to the pairs to bring them inside the surface bounds. If this is impossible
1591          * (i.e. the surface is too small for the size of the content we try to copy on either
1592          * dimension), an error will be logged and the magnifier content will look distorted.
1593          * The default values assumed by the builder for the source bounds are
1594          * left: {@link #SOURCE_BOUND_MAX_VISIBLE}, top: {@link #SOURCE_BOUND_MAX_IN_SURFACE},
1595          * right: {@link #SOURCE_BOUND_MAX_VISIBLE}, bottom: {@link #SOURCE_BOUND_MAX_IN_SURFACE}.
1596          * @param left the left bound for content copy
1597          * @param top the top bound for content copy
1598          * @param right the right bound for content copy
1599          * @param bottom the bottom bound for content copy
1600          */
1601         @NonNull
setSourceBounds(@ourceBound int left, @SourceBound int top, @SourceBound int right, @SourceBound int bottom)1602         public Builder setSourceBounds(@SourceBound int left, @SourceBound int top,
1603                 @SourceBound int right, @SourceBound int bottom) {
1604             mLeftContentBound = left;
1605             mTopContentBound = top;
1606             mRightContentBound = right;
1607             mBottomContentBound = bottom;
1608             return this;
1609         }
1610 
1611         /**
1612          * Sets the source width/height.
1613          */
1614         @NonNull
setSourceSize(int width, int height)1615         Builder setSourceSize(int width, int height) {
1616             mSourceWidth = width;
1617             mSourceHeight = height;
1618             return this;
1619         }
1620 
1621         /**
1622          * Sets the magnifier as the new fish-eye style.
1623          */
1624         @NonNull
setFishEyeStyle()1625         Builder setFishEyeStyle() {
1626             mIsFishEyeStyle = true;
1627             return this;
1628         }
1629 
1630         /**
1631          * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}.
1632          */
build()1633         public @NonNull Magnifier build() {
1634             return new Magnifier(this);
1635         }
1636     }
1637 
1638     /**
1639      * A source bound that will extend as much as possible, while remaining within the surface
1640      * the content is copied from.
1641      */
1642     public static final int SOURCE_BOUND_MAX_IN_SURFACE = 0;
1643 
1644     /**
1645      * A source bound that will extend as much as possible, while remaining within the
1646      * visible region of the magnified view, as determined by
1647      * {@link View#getGlobalVisibleRect(Rect)}.
1648      */
1649     public static final int SOURCE_BOUND_MAX_VISIBLE = 1;
1650 
1651 
1652     /**
1653      * Used to describe the {@link Surface} rectangle where the magnifier's content is allowed
1654      * to be copied from. For more details, see method
1655      * {@link Magnifier.Builder#setSourceBounds(int, int, int, int)}
1656      *
1657      * @hide
1658      */
1659     @IntDef({SOURCE_BOUND_MAX_IN_SURFACE, SOURCE_BOUND_MAX_VISIBLE})
1660     @Retention(RetentionPolicy.SOURCE)
1661     public @interface SourceBound {}
1662 
1663     // The rest of the file consists of test APIs and methods relevant for tests.
1664 
1665     /**
1666      * See {@link #setOnOperationCompleteCallback(Callback)}.
1667      */
1668     @TestApi
1669     private Callback mCallback;
1670 
1671     /**
1672      * Sets a callback which will be invoked at the end of the next
1673      * {@link #show(float, float)} or {@link #update()} operation.
1674      *
1675      * @hide
1676      */
1677     @TestApi
setOnOperationCompleteCallback(final Callback callback)1678     public void setOnOperationCompleteCallback(final Callback callback) {
1679         mCallback = callback;
1680         if (mWindow != null) {
1681             mWindow.mCallback = callback;
1682         }
1683     }
1684 
1685     /**
1686      * @return the drawing being currently displayed in the magnifier, as bitmap
1687      *
1688      * @hide
1689      */
1690     @TestApi
getContent()1691     public @Nullable Bitmap getContent() {
1692         if (mWindow == null) {
1693             return null;
1694         }
1695         synchronized (mWindow.mLock) {
1696             return mWindow.mCurrentContent;
1697         }
1698     }
1699 
1700     /**
1701      * Returns a bitmap containing the content that was magnified and drew to the
1702      * magnifier, at its original size, without the overlay applied.
1703      * @return the content that is magnified, as bitmap
1704      *
1705      * @hide
1706      */
1707     @TestApi
getOriginalContent()1708     public @Nullable Bitmap getOriginalContent() {
1709         if (mWindow == null) {
1710             return null;
1711         }
1712         synchronized (mWindow.mLock) {
1713             return Bitmap.createBitmap(mWindow.mBitmap);
1714         }
1715     }
1716 
1717     /**
1718      * @return the size of the magnifier window in dp
1719      *
1720      * @hide
1721      */
1722     @TestApi
getMagnifierDefaultSize()1723     public static PointF getMagnifierDefaultSize() {
1724         final Resources resources = Resources.getSystem();
1725         final float density = resources.getDisplayMetrics().density;
1726         final PointF size = new PointF();
1727         size.x = resources.getDimension(R.dimen.default_magnifier_width) / density;
1728         size.y = resources.getDimension(R.dimen.default_magnifier_height) / density;
1729         return size;
1730     }
1731 
1732     /**
1733      * @hide
1734      */
1735     @TestApi
1736     public interface Callback {
1737         /**
1738          * Callback called after the drawing for a magnifier update has happened.
1739          */
onOperationComplete()1740         void onOperationComplete();
1741     }
1742 }
1743