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