• 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", /*updateDestinationFrame*/ true);
1025             mBBQ.update(mBbqSurfaceControl,
1026                     surfaceWidth, surfaceHeight, PixelFormat.TRANSLUCENT);
1027             mSurface = mBBQ.createSurface();
1028 
1029             // Setup the RenderNode tree. The root has two children, one containing the bitmap
1030             // and one containing the overlay. We use a separate render node for the overlay
1031             // to avoid drawing this as the same rate we do for content.
1032             mRenderer = new ThreadedRenderer.SimpleRenderer(
1033                     context,
1034                     "magnifier renderer",
1035                     mSurface
1036             );
1037             mBitmapRenderNode = createRenderNodeForBitmap(
1038                     "magnifier content",
1039                     elevation,
1040                     cornerRadius
1041             );
1042             mOverlayRenderNode = createRenderNodeForOverlay(
1043                     "magnifier overlay",
1044                     cornerRadius
1045             );
1046             setupOverlay();
1047 
1048             final RecordingCanvas canvas = mRenderer.getRootNode().beginRecording(width, height);
1049             try {
1050                 canvas.enableZ();
1051                 canvas.drawRenderNode(mBitmapRenderNode);
1052                 canvas.disableZ();
1053                 canvas.drawRenderNode(mOverlayRenderNode);
1054                 canvas.disableZ();
1055             } finally {
1056                 mRenderer.getRootNode().endRecording();
1057             }
1058             if (mCallback != null) {
1059                 mCurrentContent =
1060                         Bitmap.createBitmap(mContentWidth, mContentHeight, Bitmap.Config.ARGB_8888);
1061                 updateCurrentContentForTesting();
1062             }
1063 
1064             // Initialize the update job and the handler where this will be post'd.
1065             mHandler = handler;
1066             mMagnifierUpdater = this::doDraw;
1067             mFrameDrawScheduled = false;
1068             mIsFishEyeStyle = isFishEyeStyle;
1069 
1070             if (mIsFishEyeStyle) {
1071                 createMeshMatrixForFishEyeEffect();
1072             }
1073         }
1074 
1075         /**
1076          * Updates the factors of content which may resize the window.
1077          * @param contentHeight the new height of content.
1078          * @param zoom the new zoom factor.
1079          */
updateContentFactors(final int contentHeight, final float zoom)1080         private void updateContentFactors(final int contentHeight, final float zoom) {
1081             if (mContentHeight == contentHeight && mZoom == zoom) {
1082               return;
1083             }
1084             if (mContentHeight < contentHeight) {
1085                 // Grows the surface height as necessary.
1086                 mBBQ.update(mBbqSurfaceControl, mContentWidth, contentHeight,
1087                     PixelFormat.TRANSLUCENT);
1088                 mRenderer.setSurface(mSurface);
1089 
1090                 final Outline outline = new Outline();
1091                 outline.setRoundRect(0, 0, mContentWidth, contentHeight, 0);
1092                 outline.setAlpha(1.0f);
1093 
1094                 mBitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
1095                         mOffsetX + mContentWidth, mOffsetY + contentHeight);
1096                 mBitmapRenderNode.setOutline(outline);
1097 
1098                 mOverlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
1099                         mOffsetX + mContentWidth, mOffsetY + contentHeight);
1100                 mOverlayRenderNode.setOutline(outline);
1101 
1102                 final RecordingCanvas canvas =
1103                         mRenderer.getRootNode().beginRecording(mContentWidth, contentHeight);
1104                 try {
1105                     canvas.enableZ();
1106                     canvas.drawRenderNode(mBitmapRenderNode);
1107                     canvas.disableZ();
1108                     canvas.drawRenderNode(mOverlayRenderNode);
1109                     canvas.disableZ();
1110                 } finally {
1111                     mRenderer.getRootNode().endRecording();
1112                 }
1113             }
1114             mContentHeight = contentHeight;
1115             mZoom = zoom;
1116             fillMeshMatrix();
1117         }
1118 
createMeshMatrixForFishEyeEffect()1119         private void createMeshMatrixForFishEyeEffect() {
1120             mMeshWidth = 1;
1121             mMeshHeight = 6;
1122             mMeshLeft = new float[2 * (mMeshWidth + 1) * (mMeshHeight + 1)];
1123             mMeshRight = new float[2 * (mMeshWidth + 1) * (mMeshHeight + 1)];
1124             fillMeshMatrix();
1125         }
1126 
fillMeshMatrix()1127         private void fillMeshMatrix() {
1128             mMeshWidth = 1;
1129             mMeshHeight = 6;
1130             final float w = mContentWidth;
1131             final float h = mContentHeight;
1132             final float h0 = h / mZoom;
1133             final float dh = h - h0;
1134             for (int i = 0; i < 2 * (mMeshWidth + 1) * (mMeshHeight + 1); i += 2) {
1135                 // Calculates X value.
1136                 final int colIndex = i % (2 * (mMeshWidth + 1)) / 2;
1137                 mMeshLeft[i] = (float) colIndex * mRamp / mMeshWidth;
1138                 mMeshRight[i] = w - mRamp + colIndex * mRamp / mMeshWidth;
1139 
1140                 // Calculates Y value.
1141                 final int rowIndex = i / 2 / (mMeshWidth + 1);
1142                 final float hl = h0 + dh * colIndex / mMeshWidth;
1143                 final float yl = (h - hl) / 2;
1144                 mMeshLeft[i + 1] = yl + hl * rowIndex / mMeshHeight;
1145                 final float hr = h - dh * colIndex / mMeshWidth;
1146                 final float yr = (h - hr) / 2;
1147                 mMeshRight[i + 1] = yr + hr * rowIndex / mMeshHeight;
1148             }
1149         }
1150 
createRenderNodeForBitmap(final String name, final float elevation, final float cornerRadius)1151         private RenderNode createRenderNodeForBitmap(final String name,
1152                 final float elevation, final float cornerRadius) {
1153             final RenderNode bitmapRenderNode = RenderNode.create(name, null);
1154 
1155             // Define the position of the bitmap in the parent render node. The surface regions
1156             // outside the bitmap are used to draw elevation.
1157             bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
1158                     mOffsetX + mContentWidth, mOffsetY + mContentHeight);
1159             bitmapRenderNode.setElevation(elevation);
1160 
1161             final Outline outline = new Outline();
1162             outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
1163             outline.setAlpha(1.0f);
1164             bitmapRenderNode.setOutline(outline);
1165             bitmapRenderNode.setClipToOutline(true);
1166 
1167             // Create a placeholder draw, which will be replaced later with real drawing.
1168             final RecordingCanvas canvas = bitmapRenderNode.beginRecording(
1169                     mContentWidth, mContentHeight);
1170             try {
1171                 canvas.drawColor(0xFF00FF00);
1172             } finally {
1173                 bitmapRenderNode.endRecording();
1174             }
1175 
1176             return bitmapRenderNode;
1177         }
1178 
createRenderNodeForOverlay(final String name, final float cornerRadius)1179         private RenderNode createRenderNodeForOverlay(final String name, final float cornerRadius) {
1180             final RenderNode overlayRenderNode = RenderNode.create(name, null);
1181 
1182             // Define the position of the overlay in the parent render node.
1183             // This coincides with the position of the content.
1184             overlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
1185                     mOffsetX + mContentWidth, mOffsetY + mContentHeight);
1186 
1187             final Outline outline = new Outline();
1188             outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
1189             outline.setAlpha(1.0f);
1190             overlayRenderNode.setOutline(outline);
1191             overlayRenderNode.setClipToOutline(true);
1192 
1193             return overlayRenderNode;
1194         }
1195 
setupOverlay()1196         private void setupOverlay() {
1197             drawOverlay();
1198 
1199             mOverlay.setCallback(new Drawable.Callback() {
1200                 @Override
1201                 public void invalidateDrawable(Drawable who) {
1202                     // When the overlay drawable is invalidated, redraw it to the render node.
1203                     drawOverlay();
1204                     if (mCallback != null) {
1205                         updateCurrentContentForTesting();
1206                     }
1207                 }
1208 
1209                 @Override
1210                 public void scheduleDrawable(Drawable who, Runnable what, long when) {
1211                     Handler.getMain().postAtTime(what, who, when);
1212                 }
1213 
1214                 @Override
1215                 public void unscheduleDrawable(Drawable who, Runnable what) {
1216                     Handler.getMain().removeCallbacks(what, who);
1217                 }
1218             });
1219         }
1220 
drawOverlay()1221         private void drawOverlay() {
1222             // Draw the drawable to the render node. This happens once during
1223             // initialization and whenever the overlay drawable is invalidated.
1224             final RecordingCanvas canvas =
1225                     mOverlayRenderNode.beginRecording(mContentWidth, mContentHeight);
1226             try {
1227                 mOverlay.setBounds(0, 0, mContentWidth, mContentHeight);
1228                 mOverlay.draw(canvas);
1229             } finally {
1230                 mOverlayRenderNode.endRecording();
1231             }
1232         }
1233 
1234         /**
1235          * Sets the position of the magnifier content relative to the parent surface.
1236          * The position update will happen in the same frame with the next draw.
1237          * The method has to be called in a context that holds {@link #mLock}.
1238          *
1239          * @param contentX the x coordinate of the content
1240          * @param contentY the y coordinate of the content
1241          */
setContentPositionForNextDraw(final int contentX, final int contentY)1242         public void setContentPositionForNextDraw(final int contentX, final int contentY) {
1243             mWindowPositionX = contentX - mOffsetX;
1244             mWindowPositionY = contentY - mOffsetY;
1245             mPendingWindowPositionUpdate = true;
1246             requestUpdate();
1247         }
1248 
1249         /**
1250          * Sets the content that should be displayed in the magnifier.
1251          * The update happens immediately, and possibly triggers a pending window movement set
1252          * by {@link #setContentPositionForNextDraw(int, int)}.
1253          * The method has to be called in a context that holds {@link #mLock}.
1254          *
1255          * @param bitmap the content bitmap
1256          */
updateContent(final @NonNull Bitmap bitmap)1257         public void updateContent(final @NonNull Bitmap bitmap) {
1258             if (mBitmap != null) {
1259                 mBitmap.recycle();
1260             }
1261             mBitmap = bitmap;
1262             requestUpdate();
1263         }
1264 
requestUpdate()1265         private void requestUpdate() {
1266             if (mFrameDrawScheduled) {
1267                 return;
1268             }
1269             final Message request = Message.obtain(mHandler, mMagnifierUpdater);
1270             request.setAsynchronous(true);
1271             request.sendToTarget();
1272             mFrameDrawScheduled = true;
1273         }
1274 
1275         /**
1276          * Destroys this instance. The method has to be called in a context holding {@link #mLock}.
1277          */
destroy()1278         public void destroy() {
1279             // Destroy the renderer. This will not proceed until pending frame callbacks complete.
1280             mRenderer.destroy();
1281             mSurface.destroy();
1282             mBBQ.destroy();
1283             new SurfaceControl.Transaction()
1284                     .remove(mSurfaceControl)
1285                     .remove(mBbqSurfaceControl)
1286                     .apply();
1287             mSurfaceSession.kill();
1288             mHandler.removeCallbacks(mMagnifierUpdater);
1289             if (mBitmap != null) {
1290                 mBitmap.recycle();
1291             }
1292             mOverlay.setCallback(null);
1293         }
1294 
doDraw()1295         private void doDraw() {
1296             final ThreadedRenderer.FrameDrawingCallback callback;
1297 
1298             // Draw the current bitmap to the surface, and prepare the callback which updates the
1299             // surface position. These have to be in the same synchronized block, in order to
1300             // guarantee the consistency between the bitmap content and the surface position.
1301             synchronized (mLock) {
1302                 if (!mSurface.isValid()) {
1303                     // Probably #destroy() was called for the current instance, so we skip the draw.
1304                     return;
1305                 }
1306 
1307                 final RecordingCanvas canvas =
1308                         mBitmapRenderNode.beginRecording(mContentWidth, mContentHeight);
1309                 try {
1310                     final int w = mBitmap.getWidth();
1311                     final int h = mBitmap.getHeight();
1312                     final Paint paint = new Paint();
1313                     paint.setFilterBitmap(true);
1314                     if (mIsFishEyeStyle) {
1315                         final int margin =
1316                             (int)((mContentWidth - (mContentWidth - 2 * mRamp) / mZoom) / 2);
1317 
1318                         // Draws the middle part.
1319                         final Rect srcRect = new Rect(margin, 0, w - margin, h);
1320                         final Rect dstRect = new Rect(
1321                             mRamp, 0, mContentWidth - mRamp, mContentHeight);
1322                         canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
1323 
1324                         // Draws the left/right parts with mesh matrixes.
1325                         canvas.drawBitmapMesh(
1326                                 Bitmap.createBitmap(mBitmap, 0, 0, margin, h),
1327                                 mMeshWidth, mMeshHeight, mMeshLeft, 0, null, 0, paint);
1328                         canvas.drawBitmapMesh(
1329                                 Bitmap.createBitmap(mBitmap, w - margin, 0, margin, h),
1330                                 mMeshWidth, mMeshHeight, mMeshRight, 0, null, 0, paint);
1331                     } else {
1332                         final Rect srcRect = new Rect(0, 0, w, h);
1333                         final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
1334                         canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
1335                     }
1336                 } finally {
1337                     mBitmapRenderNode.endRecording();
1338                 }
1339                 if (mPendingWindowPositionUpdate || mFirstDraw) {
1340                     // If the window has to be shown or moved, defer this until the next draw.
1341                     final boolean firstDraw = mFirstDraw;
1342                     mFirstDraw = false;
1343                     final boolean updateWindowPosition = mPendingWindowPositionUpdate;
1344                     mPendingWindowPositionUpdate = false;
1345                     final int pendingX = mWindowPositionX;
1346                     final int pendingY = mWindowPositionY;
1347 
1348                     callback = frame -> {
1349                         if (!mSurface.isValid()) {
1350                             return;
1351                         }
1352                         if (updateWindowPosition) {
1353                             mTransaction.setPosition(mSurfaceControl, pendingX, pendingY);
1354                         }
1355                         if (firstDraw) {
1356                             mTransaction.setLayer(mSurfaceControl, SURFACE_Z)
1357                                 .show(mSurfaceControl);
1358 
1359                         }
1360                         // Show or move the window at the content draw frame.
1361                         mBBQ.mergeWithNextTransaction(mTransaction, frame);
1362                     };
1363                     if (!mIsFishEyeStyle) {
1364                         // The new style magnifier doesn't need the light/shadow.
1365                         mRenderer.setLightCenter(mDisplay, pendingX, pendingY);
1366                     }
1367                 } else {
1368                     callback = null;
1369                 }
1370 
1371                 mFrameDrawScheduled = false;
1372             }
1373 
1374             mRenderer.draw(callback);
1375             if (mCallback != null) {
1376                 // The current content bitmap is only used in testing, so, for performance,
1377                 // we only want to update it when running tests. For this, we check that
1378                 // mCallback is not null, as it can only be set from a @TestApi.
1379                 updateCurrentContentForTesting();
1380                 mCallback.onOperationComplete();
1381             }
1382         }
1383 
1384         /**
1385          * Updates mCurrentContent, which reproduces what is currently supposed to be
1386          * drawn in the magnifier. mCurrentContent is only used for testing, so this method
1387          * should only be called otherwise.
1388          */
updateCurrentContentForTesting()1389         private void updateCurrentContentForTesting() {
1390             final Canvas canvas = new Canvas(mCurrentContent);
1391             final Rect bounds = new Rect(0, 0, mContentWidth, mContentHeight);
1392             if (mBitmap != null && !mBitmap.isRecycled()) {
1393                 final Rect originalBounds = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
1394                 canvas.drawBitmap(mBitmap, originalBounds, bounds, null);
1395             }
1396             mOverlay.setBounds(bounds);
1397             mOverlay.draw(canvas);
1398         }
1399     }
1400 
1401     /**
1402      * Builder class for {@link Magnifier} objects.
1403      */
1404     public static final class Builder {
1405         private @NonNull View mView;
1406         private @Px @IntRange(from = 0) int mWidth;
1407         private @Px @IntRange(from = 0) int mHeight;
1408         private float mZoom;
1409         private @FloatRange(from = 0f) float mElevation;
1410         private @FloatRange(from = 0f) float mCornerRadius;
1411         private @Nullable Drawable mOverlay;
1412         private int mHorizontalDefaultSourceToMagnifierOffset;
1413         private int mVerticalDefaultSourceToMagnifierOffset;
1414         private boolean mClippingEnabled;
1415         private @SourceBound int mLeftContentBound;
1416         private @SourceBound int mTopContentBound;
1417         private @SourceBound int mRightContentBound;
1418         private @SourceBound int  mBottomContentBound;
1419         private boolean mIsFishEyeStyle;
1420         private int mSourceWidth;
1421         private int mSourceHeight;
1422 
1423         /**
1424          * Construct a new builder for {@link Magnifier} objects.
1425          * @param view the view this magnifier is attached to
1426          */
Builder(@onNull View view)1427         public Builder(@NonNull View view) {
1428             mView = Objects.requireNonNull(view);
1429             applyDefaults();
1430         }
1431 
applyDefaults()1432         private void applyDefaults() {
1433             final Resources resources = mView.getContext().getResources();
1434             mWidth = resources.getDimensionPixelSize(R.dimen.default_magnifier_width);
1435             mHeight = resources.getDimensionPixelSize(R.dimen.default_magnifier_height);
1436             mElevation = resources.getDimension(R.dimen.default_magnifier_elevation);
1437             mCornerRadius = resources.getDimension(R.dimen.default_magnifier_corner_radius);
1438             mZoom = resources.getFloat(R.dimen.default_magnifier_zoom);
1439             mHorizontalDefaultSourceToMagnifierOffset =
1440                     resources.getDimensionPixelSize(R.dimen.default_magnifier_horizontal_offset);
1441             mVerticalDefaultSourceToMagnifierOffset =
1442                     resources.getDimensionPixelSize(R.dimen.default_magnifier_vertical_offset);
1443             mOverlay = new ColorDrawable(resources.getColor(
1444                     R.color.default_magnifier_color_overlay, null));
1445             mClippingEnabled = true;
1446             mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
1447             mTopContentBound = SOURCE_BOUND_MAX_VISIBLE;
1448             mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
1449             mBottomContentBound = SOURCE_BOUND_MAX_VISIBLE;
1450             mIsFishEyeStyle = false;
1451         }
1452 
1453         /**
1454          * Sets the size of the magnifier window, in pixels. Defaults to (100dp, 48dp).
1455          * Note that the size of the content being magnified and copied to the magnifier
1456          * will be computed as (window width / zoom, window height / zoom).
1457          * @param width the window width to be set
1458          * @param height the window height to be set
1459          */
1460         @NonNull
setSize(@x @ntRangefrom = 0) int width, @Px @IntRange(from = 0) int height)1461         public Builder setSize(@Px @IntRange(from = 0) int width,
1462                 @Px @IntRange(from = 0) int height) {
1463             Preconditions.checkArgumentPositive(width, "Width should be positive");
1464             Preconditions.checkArgumentPositive(height, "Height should be positive");
1465             mWidth = width;
1466             mHeight = height;
1467             return this;
1468         }
1469 
1470         /**
1471          * Sets the zoom to be applied to the chosen content before being copied to the magnifier.
1472          * A content of size (content_width, content_height) will be magnified to
1473          * (content_width * zoom, content_height * zoom), which will coincide with the size
1474          * of the magnifier. A zoom of 1 will translate to no magnification (the content will
1475          * be just copied to the magnifier with no scaling). The zoom defaults to 1.25.
1476          * Note that the zoom can also be changed after the instance is built, using the
1477          * {@link Magnifier#setZoom(float)} method.
1478          * @param zoom the zoom to be set
1479          */
1480         @NonNull
setInitialZoom(@loatRangefrom = 0f) float zoom)1481         public Builder setInitialZoom(@FloatRange(from = 0f) float zoom) {
1482             Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
1483             mZoom = zoom;
1484             return this;
1485         }
1486 
1487         /**
1488          * Sets the elevation of the magnifier window, in pixels. Defaults to 4dp.
1489          * @param elevation the elevation to be set
1490          */
1491         @NonNull
setElevation(@x @loatRangefrom = 0) float elevation)1492         public Builder setElevation(@Px @FloatRange(from = 0) float elevation) {
1493             Preconditions.checkArgumentNonNegative(elevation, "Elevation should be non-negative");
1494             mElevation = elevation;
1495             return this;
1496         }
1497 
1498         /**
1499          * Sets the corner radius of the magnifier window, in pixels. Defaults to 2dp.
1500          * @param cornerRadius the corner radius to be set
1501          */
1502         @NonNull
setCornerRadius(@x @loatRangefrom = 0) float cornerRadius)1503         public Builder setCornerRadius(@Px @FloatRange(from = 0) float cornerRadius) {
1504             Preconditions.checkArgumentNonNegative(cornerRadius,
1505                     "Corner radius should be non-negative");
1506             mCornerRadius = cornerRadius;
1507             return this;
1508         }
1509 
1510         /**
1511          * Sets an overlay that will be drawn on the top of the magnifier.
1512          * In general, the overlay should not be opaque, in order to let the magnified
1513          * content be partially visible in the magnifier. The default overlay is {@code null}
1514          * (no overlay). As an example, TextView applies a white {@link ColorDrawable}
1515          * overlay with 5% alpha, aiming to make the magnifier distinguishable when shown in dark
1516          * application regions. To disable the overlay, the parameter should be set
1517          * to {@code null}. If not null, the overlay will be automatically redrawn
1518          * when the drawable is invalidated. To achieve this, the magnifier will set a new
1519          * {@link android.graphics.drawable.Drawable.Callback} for the overlay drawable,
1520          * so keep in mind that any existing one set by the application will be lost.
1521          * @param overlay the overlay to be drawn on top
1522          */
1523         @NonNull
setOverlay(@ullable Drawable overlay)1524         public Builder setOverlay(@Nullable Drawable overlay) {
1525             mOverlay = overlay;
1526             return this;
1527         }
1528 
1529         /**
1530          * Sets an offset that should be added to the content source center to obtain
1531          * the position of the magnifier window, when the {@link #show(float, float)}
1532          * method is called. The offset is ignored when {@link #show(float, float, float, float)}
1533          * is used. The offset can be negative. It defaults to (0dp, 0dp).
1534          * @param horizontalOffset the horizontal component of the offset
1535          * @param verticalOffset the vertical component of the offset
1536          */
1537         @NonNull
setDefaultSourceToMagnifierOffset(@x int horizontalOffset, @Px int verticalOffset)1538         public Builder setDefaultSourceToMagnifierOffset(@Px int horizontalOffset,
1539                 @Px int verticalOffset) {
1540             mHorizontalDefaultSourceToMagnifierOffset = horizontalOffset;
1541             mVerticalDefaultSourceToMagnifierOffset = verticalOffset;
1542             return this;
1543         }
1544 
1545         /**
1546          * Defines the behavior of the magnifier when it is requested to position outside the
1547          * surface of the main application window. The default value is {@code true}, which means
1548          * that the position will be adjusted such that the magnifier will be fully within the
1549          * bounds of the main application window, while also avoiding any overlap with system insets
1550          * (such as the one corresponding to the status bar). If this flag is set to {@code false},
1551          * the area where the magnifier can be positioned will no longer be clipped, so the
1552          * magnifier will be able to extend outside the main application window boundaries (and also
1553          * overlap the system insets). This can be useful if you require a custom behavior, but it
1554          * should be handled with care, when passing coordinates to {@link #show(float, float)};
1555          * note that:
1556          * <ul>
1557          *   <li>in a multiwindow context, if the magnifier crosses the boundary between the two
1558          *   windows, it will not be able to show over the window of the other application</li>
1559          *   <li>if the magnifier overlaps the status bar, there is no guarantee about which one
1560          *   will be displayed on top. This should be handled with care.</li>
1561          * </ul>
1562          * @param clip whether the magnifier position will be adjusted
1563          */
1564         @NonNull
setClippingEnabled(boolean clip)1565         public Builder setClippingEnabled(boolean clip) {
1566             mClippingEnabled = clip;
1567             return this;
1568         }
1569 
1570         /**
1571          * Defines the bounds of the rectangle where the magnifier will be able to copy its content
1572          * from. The content will always be copied from the {@link Surface} of the main application
1573          * window unless the magnified view is a {@link SurfaceView}, in which case its backing
1574          * surface will be used. Each bound can have a different behavior, with the options being:
1575          * <ul>
1576          *   <li>{@link #SOURCE_BOUND_MAX_VISIBLE}, which extends the bound as much as possible
1577          *   while remaining in the visible region of the magnified view, as given by
1578          *   {@link android.view.View#getGlobalVisibleRect(Rect)}. For example, this will take into
1579          *   account the case when the view is contained in a scrollable container, and the
1580          *   magnifier will refuse to copy content outside of the visible view region</li>
1581          *   <li>{@link #SOURCE_BOUND_MAX_IN_SURFACE}, which extends the bound as much
1582          *   as possible while remaining inside the surface the content is copied from.</li>
1583          * </ul>
1584          * Note that if either of the first three options is used, the bound will be compared to
1585          * the bound of the surface (i.e. as if {@link #SOURCE_BOUND_MAX_IN_SURFACE} was used),
1586          * and the more restrictive one will be chosen. In other words, no attempt to copy content
1587          * from outside the surface will be permitted. If two opposite bounds are not well-behaved
1588          * (i.e. left + sourceWidth > right or top + sourceHeight > bottom), the left and top
1589          * bounds will have priority and the others will be extended accordingly. If the pairs
1590          * obtained this way still remain out of bounds, the smallest possible offset will be added
1591          * to the pairs to bring them inside the surface bounds. If this is impossible
1592          * (i.e. the surface is too small for the size of the content we try to copy on either
1593          * dimension), an error will be logged and the magnifier content will look distorted.
1594          * The default values assumed by the builder for the source bounds are
1595          * left: {@link #SOURCE_BOUND_MAX_VISIBLE}, top: {@link #SOURCE_BOUND_MAX_IN_SURFACE},
1596          * right: {@link #SOURCE_BOUND_MAX_VISIBLE}, bottom: {@link #SOURCE_BOUND_MAX_IN_SURFACE}.
1597          * @param left the left bound for content copy
1598          * @param top the top bound for content copy
1599          * @param right the right bound for content copy
1600          * @param bottom the bottom bound for content copy
1601          */
1602         @NonNull
setSourceBounds(@ourceBound int left, @SourceBound int top, @SourceBound int right, @SourceBound int bottom)1603         public Builder setSourceBounds(@SourceBound int left, @SourceBound int top,
1604                 @SourceBound int right, @SourceBound int bottom) {
1605             mLeftContentBound = left;
1606             mTopContentBound = top;
1607             mRightContentBound = right;
1608             mBottomContentBound = bottom;
1609             return this;
1610         }
1611 
1612         /**
1613          * Sets the source width/height.
1614          */
1615         @NonNull
setSourceSize(int width, int height)1616         Builder setSourceSize(int width, int height) {
1617             mSourceWidth = width;
1618             mSourceHeight = height;
1619             return this;
1620         }
1621 
1622         /**
1623          * Sets the magnifier as the new fish-eye style.
1624          */
1625         @NonNull
setFishEyeStyle()1626         Builder setFishEyeStyle() {
1627             mIsFishEyeStyle = true;
1628             return this;
1629         }
1630 
1631         /**
1632          * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}.
1633          */
build()1634         public @NonNull Magnifier build() {
1635             return new Magnifier(this);
1636         }
1637     }
1638 
1639     /**
1640      * A source bound that will extend as much as possible, while remaining within the surface
1641      * the content is copied from.
1642      */
1643     public static final int SOURCE_BOUND_MAX_IN_SURFACE = 0;
1644 
1645     /**
1646      * A source bound that will extend as much as possible, while remaining within the
1647      * visible region of the magnified view, as determined by
1648      * {@link View#getGlobalVisibleRect(Rect)}.
1649      */
1650     public static final int SOURCE_BOUND_MAX_VISIBLE = 1;
1651 
1652 
1653     /**
1654      * Used to describe the {@link Surface} rectangle where the magnifier's content is allowed
1655      * to be copied from. For more details, see method
1656      * {@link Magnifier.Builder#setSourceBounds(int, int, int, int)}
1657      *
1658      * @hide
1659      */
1660     @IntDef({SOURCE_BOUND_MAX_IN_SURFACE, SOURCE_BOUND_MAX_VISIBLE})
1661     @Retention(RetentionPolicy.SOURCE)
1662     public @interface SourceBound {}
1663 
1664     // The rest of the file consists of test APIs and methods relevant for tests.
1665 
1666     /**
1667      * See {@link #setOnOperationCompleteCallback(Callback)}.
1668      */
1669     @TestApi
1670     private Callback mCallback;
1671 
1672     /**
1673      * Sets a callback which will be invoked at the end of the next
1674      * {@link #show(float, float)} or {@link #update()} operation.
1675      *
1676      * @hide
1677      */
1678     @TestApi
setOnOperationCompleteCallback(final Callback callback)1679     public void setOnOperationCompleteCallback(final Callback callback) {
1680         mCallback = callback;
1681         if (mWindow != null) {
1682             mWindow.mCallback = callback;
1683         }
1684     }
1685 
1686     /**
1687      * @return the drawing being currently displayed in the magnifier, as bitmap
1688      *
1689      * @hide
1690      */
1691     @TestApi
getContent()1692     public @Nullable Bitmap getContent() {
1693         if (mWindow == null) {
1694             return null;
1695         }
1696         synchronized (mWindow.mLock) {
1697             return mWindow.mCurrentContent;
1698         }
1699     }
1700 
1701     /**
1702      * Returns a bitmap containing the content that was magnified and drew to the
1703      * magnifier, at its original size, without the overlay applied.
1704      * @return the content that is magnified, as bitmap
1705      *
1706      * @hide
1707      */
1708     @TestApi
getOriginalContent()1709     public @Nullable Bitmap getOriginalContent() {
1710         if (mWindow == null) {
1711             return null;
1712         }
1713         synchronized (mWindow.mLock) {
1714             return Bitmap.createBitmap(mWindow.mBitmap);
1715         }
1716     }
1717 
1718     /**
1719      * @return the size of the magnifier window in dp
1720      *
1721      * @hide
1722      */
1723     @TestApi
getMagnifierDefaultSize()1724     public static PointF getMagnifierDefaultSize() {
1725         final Resources resources = Resources.getSystem();
1726         final float density = resources.getDisplayMetrics().density;
1727         final PointF size = new PointF();
1728         size.x = resources.getDimension(R.dimen.default_magnifier_width) / density;
1729         size.y = resources.getDimension(R.dimen.default_magnifier_height) / density;
1730         return size;
1731     }
1732 
1733     /**
1734      * @hide
1735      */
1736     @TestApi
1737     public interface Callback {
1738         /**
1739          * Callback called after the drawing for a magnifier update has happened.
1740          */
onOperationComplete()1741         void onOperationComplete();
1742     }
1743 }
1744