blob: 2924dd9bf954db3ce3e2e5d3581429dec5c3b44b [file] [log] [blame]
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01001/*
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
Andrei Stingaceanud6644dc2017-11-21 14:53:38 +000014 * limitations under the License.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010015 */
16
Andrei Stingaceanud6644dc2017-11-21 14:53:38 +000017package android.widget;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010018
19import android.annotation.FloatRange;
Mihai Popa3e1aed12018-08-03 18:25:52 +010020import android.annotation.IntDef;
Mihai Popa469aba82018-07-18 14:52:26 +010021import android.annotation.IntRange;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010022import android.annotation.NonNull;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000023import android.annotation.Nullable;
Mihai Popa469aba82018-07-18 14:52:26 +010024import android.annotation.Px;
Mihai Popa137b5842018-01-30 15:03:22 +000025import android.annotation.TestApi;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010026import android.annotation.UiThread;
27import android.content.Context;
Mihai Popa137b5842018-01-30 15:03:22 +000028import android.content.res.Resources;
Mihai Popafb4b6b82018-03-01 16:08:14 +000029import android.content.res.TypedArray;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010030import android.graphics.Bitmap;
Mihai Popa1ddabb22018-08-06 11:55:54 +010031import android.graphics.Canvas;
Mihai Popad870b882018-02-27 14:25:52 +000032import android.graphics.Color;
Adrian Roos60f59292018-08-24 16:29:06 +020033import android.graphics.Insets;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000034import android.graphics.Outline;
35import android.graphics.Paint;
36import android.graphics.PixelFormat;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010037import android.graphics.Point;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +000038import android.graphics.PointF;
John Reck32f140aa62018-10-04 15:08:24 -070039import android.graphics.RecordingCanvas;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010040import android.graphics.Rect;
John Reck32f140aa62018-10-04 15:08:24 -070041import android.graphics.RenderNode;
Mihai Popa1ddabb22018-08-06 11:55:54 +010042import android.graphics.drawable.ColorDrawable;
43import android.graphics.drawable.Drawable;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010044import android.os.Handler;
Mihai Popa8b789102018-02-15 12:06:59 +000045import android.os.HandlerThread;
46import android.os.Message;
Mihai Popa3e1aed12018-08-03 18:25:52 +010047import android.util.Log;
Mihai Popafb4b6b82018-03-01 16:08:14 +000048import android.view.ContextThemeWrapper;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000049import android.view.Display;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010050import android.view.PixelCopy;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +000051import android.view.Surface;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000052import android.view.SurfaceControl;
Mihai Popa3589c2c2018-01-25 19:26:30 +000053import android.view.SurfaceHolder;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000054import android.view.SurfaceSession;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +000055import android.view.SurfaceView;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000056import android.view.ThreadedRenderer;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010057import android.view.View;
Mihai Popa3589c2c2018-01-25 19:26:30 +000058import android.view.ViewRootImpl;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010059
Mihai Popafb4b6b82018-03-01 16:08:14 +000060import com.android.internal.R;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010061import com.android.internal.util.Preconditions;
62
Mihai Popa3e1aed12018-08-03 18:25:52 +010063import java.lang.annotation.Retention;
64import java.lang.annotation.RetentionPolicy;
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +000065import java.util.Objects;
Mihai Popa3e1aed12018-08-03 18:25:52 +010066
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010067/**
Andrei Stingaceanud6644dc2017-11-21 14:53:38 +000068 * Android magnifier widget. Can be used by any view which is attached to a window.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010069 */
Andrei Stingaceanud6644dc2017-11-21 14:53:38 +000070@UiThread
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010071public final class Magnifier {
Mihai Popa3e1aed12018-08-03 18:25:52 +010072 private static final String TAG = "Magnifier";
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +000073 // Use this to specify that a previous configuration value does not exist.
74 private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
Mihai Popa8b789102018-02-15 12:06:59 +000075 // The callbacks of the pixel copy requests will be invoked on
76 // the Handler of this Thread when the copy is finished.
77 private static final HandlerThread sPixelCopyHandlerThread =
78 new HandlerThread("magnifier pixel copy result handler");
79
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +000080 // The view to which this magnifier is attached.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010081 private final View mView;
Mihai Popa1d1ed0c2018-01-12 12:38:12 +000082 // The coordinates of the view in the surface.
83 private final int[] mViewCoordinatesInSurface;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010084 // The window containing the magnifier.
Mihai Popa4bcd4d402018-02-07 17:13:51 +000085 private InternalPopupWindow mWindow;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010086 // The width of the window containing the magnifier.
87 private final int mWindowWidth;
88 // The height of the window containing the magnifier.
89 private final int mWindowHeight;
Mihai Popa469aba82018-07-18 14:52:26 +010090 // The zoom applied to the view region copied to the magnifier view.
Mihai Popabeeaf552018-07-19 15:50:43 +010091 private float mZoom;
Mihai Popa7433a072018-07-18 12:18:34 +010092 // The width of the content that will be copied to the magnifier.
Mihai Popabeeaf552018-07-19 15:50:43 +010093 private int mSourceWidth;
Mihai Popa7433a072018-07-18 12:18:34 +010094 // The height of the content that will be copied to the magnifier.
Mihai Popabeeaf552018-07-19 15:50:43 +010095 private int mSourceHeight;
Mihai Popa3e1aed12018-08-03 18:25:52 +010096 // Whether the zoom of the magnifier or the view position have changed since last content copy.
97 private boolean mDirtyState;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000098 // The elevation of the window containing the magnifier.
99 private final float mWindowElevation;
Mihai Popafb4b6b82018-03-01 16:08:14 +0000100 // The corner radius of the window containing the magnifier.
101 private final float mWindowCornerRadius;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100102 // The overlay to be drawn on the top of the magnifier content.
103 private final Drawable mOverlay;
Mihai Popa469aba82018-07-18 14:52:26 +0100104 // The horizontal offset between the source and window coords when #show(float, float) is used.
105 private final int mDefaultHorizontalSourceToMagnifierOffset;
106 // The vertical offset between the source and window coords when #show(float, float) is used.
107 private final int mDefaultVerticalSourceToMagnifierOffset;
Mihai Popabecda342018-12-13 17:12:37 +0000108 // Whether the area where the magnifier can be positioned will be clipped to the main window
109 // and within system insets.
110 private final boolean mClippingEnabled;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100111 // The behavior of the left bound of the rectangle where the content can be copied from.
112 private @SourceBound int mLeftContentBound;
113 // The behavior of the top bound of the rectangle where the content can be copied from.
114 private @SourceBound int mTopContentBound;
115 // The behavior of the right bound of the rectangle where the content can be copied from.
116 private @SourceBound int mRightContentBound;
117 // The behavior of the bottom bound of the rectangle where the content can be copied from.
118 private @SourceBound int mBottomContentBound;
Mihai Popaf2980682018-04-30 19:08:57 +0100119 // The parent surface for the magnifier surface.
120 private SurfaceInfo mParentSurface;
121 // The surface where the content will be copied from.
122 private SurfaceInfo mContentCopySurface;
123 // The center coordinates of the window containing the magnifier.
124 private final Point mWindowCoords = new Point();
125 // The center coordinates of the content to be magnified,
Mihai Popaf2980682018-04-30 19:08:57 +0100126 // clamped inside the visible region of the magnified view.
127 private final Point mClampedCenterZoomCoords = new Point();
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000128 // Variables holding previous states, used for detecting redundant calls and invalidation.
129 private final Point mPrevStartCoordsInSurface = new Point(
130 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
Mihai Popa7433a072018-07-18 12:18:34 +0100131 private final PointF mPrevShowSourceCoords = new PointF(
132 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
133 private final PointF mPrevShowWindowCoords = new PointF(
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000134 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000135 // Rectangle defining the view surface area we pixel copy content from.
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000136 private final Rect mPixelCopyRequestRect = new Rect();
Mihai Popa39a71332018-02-22 19:30:24 +0000137 // Lock to synchronize between the UI thread and the thread that handles pixel copy results.
138 // Only sync mWindow writes from UI thread with mWindow reads from sPixelCopyHandlerThread.
139 private final Object mLock = new Object();
Bin Chend2631412019-05-10 16:05:05 +0800140 // The lock used to synchronize the UI and render threads when a #dismiss is performed.
141 private final Object mDestroyLock = new Object();
Andrei Stingaceanu15af5612017-10-13 12:53:23 +0100142
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100143 /**
144 * Initializes a magnifier.
145 *
146 * @param view the view for which this magnifier is attached
Mihai Popa469aba82018-07-18 14:52:26 +0100147 *
Mihai Popab6ca9092018-09-24 21:14:50 +0100148 * @deprecated Please use {@link Builder} instead
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100149 */
Mihai Popab6ca9092018-09-24 21:14:50 +0100150 @Deprecated
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100151 public Magnifier(@NonNull View view) {
Mihai Popac6950292018-11-15 21:32:42 +0000152 this(createBuilderWithOldMagnifierDefaults(view));
153 }
154
155 static Builder createBuilderWithOldMagnifierDefaults(final View view) {
156 final Builder params = new Builder(view);
157 final Context context = view.getContext();
158 final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Magnifier,
159 R.attr.magnifierStyle, 0);
160 params.mWidth = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierWidth, 0);
161 params.mHeight = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHeight, 0);
162 params.mElevation = a.getDimension(R.styleable.Magnifier_magnifierElevation, 0);
163 params.mCornerRadius = getDeviceDefaultDialogCornerRadius(context);
164 params.mZoom = a.getFloat(R.styleable.Magnifier_magnifierZoom, 0);
165 params.mHorizontalDefaultSourceToMagnifierOffset =
166 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHorizontalOffset, 0);
167 params.mVerticalDefaultSourceToMagnifierOffset =
168 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierVerticalOffset, 0);
169 params.mOverlay = new ColorDrawable(a.getColor(
170 R.styleable.Magnifier_magnifierColorOverlay, Color.TRANSPARENT));
171 a.recycle();
Mihai Popabecda342018-12-13 17:12:37 +0000172 params.mClippingEnabled = true;
Mihai Popac6950292018-11-15 21:32:42 +0000173 params.mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
174 params.mTopContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
175 params.mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
176 params.mBottomContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
177 return params;
178 }
179
180 /**
181 * Returns the device default theme dialog corner radius attribute.
182 * We retrieve this from the device default theme to avoid
183 * using the values set in the custom application themes.
184 */
185 private static float getDeviceDefaultDialogCornerRadius(final Context context) {
186 final Context deviceDefaultContext =
187 new ContextThemeWrapper(context, R.style.Theme_DeviceDefault);
188 final TypedArray ta = deviceDefaultContext.obtainStyledAttributes(
189 new int[]{android.R.attr.dialogCornerRadius});
190 final float dialogCornerRadius = ta.getDimension(0, 0);
191 ta.recycle();
192 return dialogCornerRadius;
Mihai Popa469aba82018-07-18 14:52:26 +0100193 }
194
195 private Magnifier(@NonNull Builder params) {
196 // Copy params from builder.
197 mView = params.mView;
198 mWindowWidth = params.mWidth;
199 mWindowHeight = params.mHeight;
200 mZoom = params.mZoom;
Mihai Popa7433a072018-07-18 12:18:34 +0100201 mSourceWidth = Math.round(mWindowWidth / mZoom);
202 mSourceHeight = Math.round(mWindowHeight / mZoom);
Mihai Popa469aba82018-07-18 14:52:26 +0100203 mWindowElevation = params.mElevation;
204 mWindowCornerRadius = params.mCornerRadius;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100205 mOverlay = params.mOverlay;
Mihai Popa469aba82018-07-18 14:52:26 +0100206 mDefaultHorizontalSourceToMagnifierOffset =
207 params.mHorizontalDefaultSourceToMagnifierOffset;
208 mDefaultVerticalSourceToMagnifierOffset =
209 params.mVerticalDefaultSourceToMagnifierOffset;
Mihai Popabecda342018-12-13 17:12:37 +0000210 mClippingEnabled = params.mClippingEnabled;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100211 mLeftContentBound = params.mLeftContentBound;
212 mTopContentBound = params.mTopContentBound;
213 mRightContentBound = params.mRightContentBound;
214 mBottomContentBound = params.mBottomContentBound;
Mihai Popa1d1ed0c2018-01-12 12:38:12 +0000215 // The view's surface coordinates will not be updated until the magnifier is first shown.
216 mViewCoordinatesInSurface = new int[2];
Mihai Popa8b789102018-02-15 12:06:59 +0000217 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100218
Mihai Popa8b789102018-02-15 12:06:59 +0000219 static {
220 sPixelCopyHandlerThread.start();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100221 }
222
223 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100224 * Shows the magnifier on the screen. The method takes the coordinates of the center
225 * of the content source going to be magnified and copied to the magnifier. The coordinates
226 * are relative to the top left corner of the magnified view. The magnifier will be
227 * positioned such that its center will be at the default offset from the center of the source.
228 * The default offset can be specified using the method
229 * {@link Builder#setDefaultSourceToMagnifierOffset(int, int)}. If the offset should
230 * be different across calls to this method, you should consider to use method
231 * {@link #show(float, float, float, float)} instead.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100232 *
Mihai Popac2e0bee2018-07-19 12:18:30 +0100233 * @param sourceCenterX horizontal coordinate of the source center, relative to the view
234 * @param sourceCenterY vertical coordinate of the source center, relative to the view
235 *
236 * @see Builder#setDefaultSourceToMagnifierOffset(int, int)
237 * @see Builder#getDefaultHorizontalSourceToMagnifierOffset()
238 * @see Builder#getDefaultVerticalSourceToMagnifierOffset()
239 * @see #show(float, float, float, float)
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100240 */
Mihai Popa7433a072018-07-18 12:18:34 +0100241 public void show(@FloatRange(from = 0) float sourceCenterX,
242 @FloatRange(from = 0) float sourceCenterY) {
Mihai Popa469aba82018-07-18 14:52:26 +0100243 show(sourceCenterX, sourceCenterY,
244 sourceCenterX + mDefaultHorizontalSourceToMagnifierOffset,
245 sourceCenterY + mDefaultVerticalSourceToMagnifierOffset);
Mihai Popa7433a072018-07-18 12:18:34 +0100246 }
247
248 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100249 * Shows the magnifier on the screen at a position that is independent from its content
250 * position. The first two arguments represent the coordinates of the center of the
251 * content source going to be magnified and copied to the magnifier. The last two arguments
252 * represent the coordinates of the center of the magnifier itself. All four coordinates
253 * are relative to the top left corner of the magnified view. If you consider using this
254 * method such that the offset between the source center and the magnifier center coordinates
255 * remains constant, you should consider using method {@link #show(float, float)} instead.
Mihai Popa7433a072018-07-18 12:18:34 +0100256 *
Mihai Popac2e0bee2018-07-19 12:18:30 +0100257 * @param sourceCenterX horizontal coordinate of the source center relative to the view
258 * @param sourceCenterY vertical coordinate of the source center, relative to the view
259 * @param magnifierCenterX horizontal coordinate of the magnifier center, relative to the view
260 * @param magnifierCenterY vertical coordinate of the magnifier center, relative to the view
Mihai Popa7433a072018-07-18 12:18:34 +0100261 */
262 public void show(@FloatRange(from = 0) float sourceCenterX,
263 @FloatRange(from = 0) float sourceCenterY,
264 float magnifierCenterX, float magnifierCenterY) {
Andrei Stingaceanu451f9472017-10-13 16:41:28 +0100265
Mihai Popaf2980682018-04-30 19:08:57 +0100266 obtainSurfaces();
Mihai Popa7433a072018-07-18 12:18:34 +0100267 obtainContentCoordinates(sourceCenterX, sourceCenterY);
268 obtainWindowCoordinates(magnifierCenterX, magnifierCenterY);
Andrei Stingaceanuca189fe2017-10-19 17:02:22 +0100269
Mihai Popa7433a072018-07-18 12:18:34 +0100270 final int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2;
271 final int startY = mClampedCenterZoomCoords.y - mSourceHeight / 2;
Mihai Popabeeaf552018-07-19 15:50:43 +0100272 if (sourceCenterX != mPrevShowSourceCoords.x || sourceCenterY != mPrevShowSourceCoords.y
Mihai Popa3e1aed12018-08-03 18:25:52 +0100273 || mDirtyState) {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000274 if (mWindow == null) {
Mihai Popa39a71332018-02-22 19:30:24 +0000275 synchronized (mLock) {
276 mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(),
Robert Carr5fea55b2018-12-10 13:05:52 -0800277 mParentSurface.mSurfaceControl, mWindowWidth, mWindowHeight,
Mihai Popa1ddabb22018-08-06 11:55:54 +0100278 mWindowElevation, mWindowCornerRadius,
279 mOverlay != null ? mOverlay : new ColorDrawable(Color.TRANSPARENT),
Mihai Popa39a71332018-02-22 19:30:24 +0000280 Handler.getMain() /* draw the magnifier on the UI thread */, mLock,
Mihai Popaafee43b2019-06-17 12:34:50 +0100281 mCallback);
Mihai Popa39a71332018-02-22 19:30:24 +0000282 }
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000283 }
284 performPixelCopy(startX, startY, true /* update window position */);
Mihai Popa7433a072018-07-18 12:18:34 +0100285 } else if (magnifierCenterX != mPrevShowWindowCoords.x
286 || magnifierCenterY != mPrevShowWindowCoords.y) {
287 final Point windowCoords = getCurrentClampedWindowCoordinates();
288 final InternalPopupWindow currentWindowInstance = mWindow;
289 sPixelCopyHandlerThread.getThreadHandler().post(() -> {
Mihai Popa7433a072018-07-18 12:18:34 +0100290 synchronized (mLock) {
Mihai Popaddcd54812018-09-03 17:25:54 +0100291 if (mWindow != currentWindowInstance) {
292 // The magnifier was dismissed (and maybe shown again) in the meantime.
293 return;
294 }
Mihai Popa7433a072018-07-18 12:18:34 +0100295 mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
296 }
297 });
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100298 }
Mihai Popa7433a072018-07-18 12:18:34 +0100299 mPrevShowSourceCoords.x = sourceCenterX;
300 mPrevShowSourceCoords.y = sourceCenterY;
301 mPrevShowWindowCoords.x = magnifierCenterX;
302 mPrevShowWindowCoords.y = magnifierCenterY;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100303 }
304
305 /**
Andrei Stingaceanud6644dc2017-11-21 14:53:38 +0000306 * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100307 */
308 public void dismiss() {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000309 if (mWindow != null) {
Mihai Popaafee43b2019-06-17 12:34:50 +0100310 synchronized (mLock) {
311 mWindow.destroy();
312 mWindow = null;
Mihai Popa39a71332018-02-22 19:30:24 +0000313 }
Mihai Popa7433a072018-07-18 12:18:34 +0100314 mPrevShowSourceCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
315 mPrevShowSourceCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
316 mPrevShowWindowCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
317 mPrevShowWindowCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
Mihai Popa953b1342018-03-21 18:05:13 +0000318 mPrevStartCoordsInSurface.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
319 mPrevStartCoordsInSurface.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000320 }
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000321 }
Andrei Stingaceanu15af5612017-10-13 12:53:23 +0100322
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000323 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100324 * Asks the magnifier to update its content. It uses the previous coordinates passed to
325 * {@link #show(float, float)} or {@link #show(float, float, float, float)}. The
326 * method only has effect if the magnifier is currently showing.
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000327 */
328 public void update() {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000329 if (mWindow != null) {
Mihai Popaf2980682018-04-30 19:08:57 +0100330 obtainSurfaces();
Mihai Popa3e1aed12018-08-03 18:25:52 +0100331 if (!mDirtyState) {
Mihai Popabeeaf552018-07-19 15:50:43 +0100332 // Update the content shown in the magnifier.
333 performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y,
334 false /* update window position */);
335 } else {
Mihai Popa3e1aed12018-08-03 18:25:52 +0100336 // If for example the zoom has changed, we cannot use the same top left
337 // coordinates as before, so just #show again to have them recomputed.
Mihai Popabeeaf552018-07-19 15:50:43 +0100338 show(mPrevShowSourceCoords.x, mPrevShowSourceCoords.y,
339 mPrevShowWindowCoords.x, mPrevShowWindowCoords.y);
340 }
Andrei Stingaceanu15af5612017-10-13 12:53:23 +0100341 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100342 }
343
Mihai Popa17ea3052018-03-06 14:24:07 +0000344 /**
Mihai Popa469aba82018-07-18 14:52:26 +0100345 * @return the width of the magnifier window, in pixels
Mihai Popac2e0bee2018-07-19 12:18:30 +0100346 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa17ea3052018-03-06 14:24:07 +0000347 */
Mihai Popac2e0bee2018-07-19 12:18:30 +0100348 @Px
Mihai Popa17ea3052018-03-06 14:24:07 +0000349 public int getWidth() {
350 return mWindowWidth;
351 }
352
353 /**
Mihai Popa469aba82018-07-18 14:52:26 +0100354 * @return the height of the magnifier window, in pixels
Mihai Popac2e0bee2018-07-19 12:18:30 +0100355 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa17ea3052018-03-06 14:24:07 +0000356 */
Mihai Popac2e0bee2018-07-19 12:18:30 +0100357 @Px
Mihai Popa17ea3052018-03-06 14:24:07 +0000358 public int getHeight() {
359 return mWindowHeight;
360 }
361
362 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100363 * @return the initial width of the content magnified and copied to the magnifier, in pixels
364 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa27cf08f2019-01-10 19:59:29 +0000365 * @see Magnifier.Builder#setInitialZoom(float)
Mihai Popac2e0bee2018-07-19 12:18:30 +0100366 */
367 @Px
368 public int getSourceWidth() {
369 return mSourceWidth;
370 }
371
372 /**
373 * @return the initial height of the content magnified and copied to the magnifier, in pixels
374 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa27cf08f2019-01-10 19:59:29 +0000375 * @see Magnifier.Builder#setInitialZoom(float)
Mihai Popac2e0bee2018-07-19 12:18:30 +0100376 */
377 @Px
378 public int getSourceHeight() {
379 return mSourceHeight;
380 }
381
382 /**
Mihai Popabeeaf552018-07-19 15:50:43 +0100383 * Sets the zoom to be applied to the chosen content before being copied to the magnifier popup.
Mihai Popa3e1aed12018-08-03 18:25:52 +0100384 * The change will become effective at the next #show or #update call.
Mihai Popabeeaf552018-07-19 15:50:43 +0100385 * @param zoom the zoom to be set
386 */
387 public void setZoom(@FloatRange(from = 0f) float zoom) {
388 Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
389 mZoom = zoom;
390 mSourceWidth = Math.round(mWindowWidth / mZoom);
391 mSourceHeight = Math.round(mWindowHeight / mZoom);
Mihai Popa3e1aed12018-08-03 18:25:52 +0100392 mDirtyState = true;
Mihai Popabeeaf552018-07-19 15:50:43 +0100393 }
394
395 /**
Mihai Popa469aba82018-07-18 14:52:26 +0100396 * Returns the zoom to be applied to the magnified view region copied to the magnifier.
Mihai Popa17ea3052018-03-06 14:24:07 +0000397 * If the zoom is x and the magnifier window size is (width, height), the original size
Mihai Popa469aba82018-07-18 14:52:26 +0100398 * of the content being magnified will be (width / x, height / x).
399 * @return the zoom applied to the content
Mihai Popa27cf08f2019-01-10 19:59:29 +0000400 * @see Magnifier.Builder#setInitialZoom(float)
Mihai Popa17ea3052018-03-06 14:24:07 +0000401 */
402 public float getZoom() {
403 return mZoom;
404 }
405
Mihai Popa63ee7f12018-04-05 12:01:53 +0100406 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100407 * @return the elevation set for the magnifier window, in pixels
408 * @see Magnifier.Builder#setElevation(float)
409 */
410 @Px
411 public float getElevation() {
412 return mWindowElevation;
413 }
414
415 /**
416 * @return the corner radius of the magnifier window, in pixels
417 * @see Magnifier.Builder#setCornerRadius(float)
418 */
419 @Px
420 public float getCornerRadius() {
421 return mWindowCornerRadius;
422 }
423
424 /**
425 * Returns the horizontal offset, in pixels, to be applied to the source center position
426 * to obtain the magnifier center position when {@link #show(float, float)} is called.
427 * The value is ignored when {@link #show(float, float, float, float)} is used instead.
Mihai Popa0450a162018-04-27 13:09:12 +0100428 *
Mihai Popac2e0bee2018-07-19 12:18:30 +0100429 * @return the default horizontal offset between the source center and the magnifier
430 * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
431 * @see Magnifier#show(float, float)
432 */
433 @Px
434 public int getDefaultHorizontalSourceToMagnifierOffset() {
435 return mDefaultHorizontalSourceToMagnifierOffset;
436 }
437
438 /**
439 * Returns the vertical offset, in pixels, to be applied to the source center position
440 * to obtain the magnifier center position when {@link #show(float, float)} is called.
441 * The value is ignored when {@link #show(float, float, float, float)} is used instead.
442 *
443 * @return the default vertical offset between the source center and the magnifier
444 * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
445 * @see Magnifier#show(float, float)
446 */
447 @Px
448 public int getDefaultVerticalSourceToMagnifierOffset() {
449 return mDefaultVerticalSourceToMagnifierOffset;
450 }
451
452 /**
Mihai Popabecda342018-12-13 17:12:37 +0000453 * Returns the overlay to be drawn on the top of the magnifier, or
Mihai Popa1ddabb22018-08-06 11:55:54 +0100454 * {@code null} if no overlay should be drawn.
455 * @return the overlay
456 * @see Magnifier.Builder#setOverlay(Drawable)
457 */
458 @Nullable
459 public Drawable getOverlay() {
460 return mOverlay;
461 }
462
463 /**
Mihai Popa1903cab2018-08-01 14:33:12 +0100464 * Returns whether the magnifier position will be adjusted such that the magnifier will be
Mihai Popabecda342018-12-13 17:12:37 +0000465 * fully within the bounds of the main application window, by also avoiding any overlap
466 * with system insets (such as the one corresponding to the status bar) i.e. whether the
467 * area where the magnifier can be positioned will be clipped to the main application window
468 * and the system insets.
Mihai Popa1903cab2018-08-01 14:33:12 +0100469 * @return whether the magnifier position will be adjusted
Mihai Popabecda342018-12-13 17:12:37 +0000470 * @see Magnifier.Builder#setClippingEnabled(boolean)
Mihai Popa1903cab2018-08-01 14:33:12 +0100471 */
Mihai Popabecda342018-12-13 17:12:37 +0000472 public boolean isClippingEnabled() {
473 return mClippingEnabled;
Mihai Popa1903cab2018-08-01 14:33:12 +0100474 }
475
476 /**
Mihai Popaef456972019-02-18 15:17:09 +0000477 * Returns the top left coordinates of the magnifier, relative to the main application
478 * window. They will be determined by the coordinates of the last {@link #show(float, float)}
479 * or {@link #show(float, float, float, float)} call, adjusted to take into account any
480 * potential clamping behavior. The method can be used immediately after a #show
481 * call to find out where the magnifier will be positioned. However, the position of the
482 * magnifier will not be updated visually in the same frame, due to the async nature of
483 * the content copying and of the magnifier rendering.
Mihai Popac2e0bee2018-07-19 12:18:30 +0100484 * The method will return {@code null} if #show has not yet been called, or if the last
485 * operation performed was a #dismiss.
486 *
487 * @return the top left coordinates of the magnifier
Mihai Popa63ee7f12018-04-05 12:01:53 +0100488 */
489 @Nullable
Mihai Popac2e0bee2018-07-19 12:18:30 +0100490 public Point getPosition() {
Mihai Popa63ee7f12018-04-05 12:01:53 +0100491 if (mWindow == null) {
492 return null;
493 }
Mihai Popaef456972019-02-18 15:17:09 +0000494 final Point position = getCurrentClampedWindowCoordinates();
495 position.offset(-mParentSurface.mInsets.left, -mParentSurface.mInsets.top);
496 return new Point(position);
Mihai Popac2e0bee2018-07-19 12:18:30 +0100497 }
498
499 /**
500 * Returns the top left coordinates of the magnifier source (i.e. the view region going to
Mihai Popaef456972019-02-18 15:17:09 +0000501 * be magnified and copied to the magnifier), relative to the window or surface the content
502 * is copied from. The content will be copied:
Mihai Popac2e0bee2018-07-19 12:18:30 +0100503 * - if the magnified view is a {@link SurfaceView}, from the surface backing it
Mihai Popaef456972019-02-18 15:17:09 +0000504 * - otherwise, from the surface backing the main application window, and the coordinates
505 * returned will be relative to the main application window
Mihai Popac2e0bee2018-07-19 12:18:30 +0100506 * The method will return {@code null} if #show has not yet been called, or if the last
507 * operation performed was a #dismiss.
508 *
509 * @return the top left coordinates of the magnifier source
510 */
511 @Nullable
512 public Point getSourcePosition() {
513 if (mWindow == null) {
514 return null;
515 }
Mihai Popaef456972019-02-18 15:17:09 +0000516 final Point position = new Point(mPixelCopyRequestRect.left, mPixelCopyRequestRect.top);
517 position.offset(-mContentCopySurface.mInsets.left, -mContentCopySurface.mInsets.top);
518 return new Point(position);
Mihai Popa63ee7f12018-04-05 12:01:53 +0100519 }
520
Mihai Popaf2980682018-04-30 19:08:57 +0100521 /**
522 * Retrieves the surfaces used by the magnifier:
523 * - a parent surface for the magnifier surface. This will usually be the main app window.
524 * - a surface where the magnified content will be copied from. This will be the main app
525 * window unless the magnified view is a SurfaceView, in which case its backing surface
526 * will be used.
527 */
528 private void obtainSurfaces() {
529 // Get the main window surface.
530 SurfaceInfo validMainWindowSurface = SurfaceInfo.NULL;
Mihai Popa819e90d2018-04-16 14:27:05 +0100531 if (mView.getViewRootImpl() != null) {
Mihai Popaf2980682018-04-30 19:08:57 +0100532 final ViewRootImpl viewRootImpl = mView.getViewRootImpl();
533 final Surface mainWindowSurface = viewRootImpl.mSurface;
Mihai Popa819e90d2018-04-16 14:27:05 +0100534 if (mainWindowSurface != null && mainWindowSurface.isValid()) {
Mihai Popaf2980682018-04-30 19:08:57 +0100535 final Rect surfaceInsets = viewRootImpl.mWindowAttributes.surfaceInsets;
536 final int surfaceWidth =
537 viewRootImpl.getWidth() + surfaceInsets.left + surfaceInsets.right;
538 final int surfaceHeight =
539 viewRootImpl.getHeight() + surfaceInsets.top + surfaceInsets.bottom;
540 validMainWindowSurface =
Robert Carr5fea55b2018-12-10 13:05:52 -0800541 new SurfaceInfo(viewRootImpl.getSurfaceControl(), mainWindowSurface,
Mihai Popaef456972019-02-18 15:17:09 +0000542 surfaceWidth, surfaceHeight, surfaceInsets, true);
Mihai Popa819e90d2018-04-16 14:27:05 +0100543 }
Mihai Popa17ea3052018-03-06 14:24:07 +0000544 }
Mihai Popaf2980682018-04-30 19:08:57 +0100545 // Get the surface backing the magnified view, if it is a SurfaceView.
546 SurfaceInfo validSurfaceViewSurface = SurfaceInfo.NULL;
Mihai Popa819e90d2018-04-16 14:27:05 +0100547 if (mView instanceof SurfaceView) {
Robert Carr5fea55b2018-12-10 13:05:52 -0800548 final SurfaceControl sc = ((SurfaceView) mView).getSurfaceControl();
Mihai Popaf2980682018-04-30 19:08:57 +0100549 final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder();
550 final Surface surfaceViewSurface = surfaceHolder.getSurface();
Robert Carr5fea55b2018-12-10 13:05:52 -0800551
552 if (sc != null && sc.isValid()) {
Mihai Popaf2980682018-04-30 19:08:57 +0100553 final Rect surfaceFrame = surfaceHolder.getSurfaceFrame();
Robert Carr5fea55b2018-12-10 13:05:52 -0800554 validSurfaceViewSurface = new SurfaceInfo(sc, surfaceViewSurface,
Mihai Popaef456972019-02-18 15:17:09 +0000555 surfaceFrame.right, surfaceFrame.bottom, new Rect(), false);
Mihai Popa819e90d2018-04-16 14:27:05 +0100556 }
557 }
Mihai Popaf2980682018-04-30 19:08:57 +0100558
559 // Choose the parent surface for the magnifier and the source surface for the content.
560 mParentSurface = validMainWindowSurface != SurfaceInfo.NULL
561 ? validMainWindowSurface : validSurfaceViewSurface;
562 mContentCopySurface = mView instanceof SurfaceView
563 ? validSurfaceViewSurface : validMainWindowSurface;
Mihai Popa17ea3052018-03-06 14:24:07 +0000564 }
565
Mihai Popaf2980682018-04-30 19:08:57 +0100566 /**
567 * Computes the coordinates of the center of the content going to be displayed in the
568 * magnifier. These are relative to the surface the content is copied from.
569 */
570 private void obtainContentCoordinates(final float xPosInView, final float yPosInView) {
Mihai Popa3e1aed12018-08-03 18:25:52 +0100571 final int prevViewXInSurface = mViewCoordinatesInSurface[0];
572 final int prevViewYInSurface = mViewCoordinatesInSurface[1];
Mihai Popa819e90d2018-04-16 14:27:05 +0100573 mView.getLocationInSurface(mViewCoordinatesInSurface);
Mihai Popa3e1aed12018-08-03 18:25:52 +0100574 if (mViewCoordinatesInSurface[0] != prevViewXInSurface
575 || mViewCoordinatesInSurface[1] != prevViewYInSurface) {
576 mDirtyState = true;
577 }
578
Mihai Popa7433a072018-07-18 12:18:34 +0100579 final int zoomCenterX;
580 final int zoomCenterY;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000581 if (mView instanceof SurfaceView) {
582 // No offset required if the backing Surface matches the size of the SurfaceView.
Mihai Popa7433a072018-07-18 12:18:34 +0100583 zoomCenterX = Math.round(xPosInView);
584 zoomCenterY = Math.round(yPosInView);
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000585 } else {
Mihai Popa7433a072018-07-18 12:18:34 +0100586 zoomCenterX = Math.round(xPosInView + mViewCoordinatesInSurface[0]);
587 zoomCenterY = Math.round(yPosInView + mViewCoordinatesInSurface[1]);
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000588 }
589
Mihai Popa520e44752019-01-29 21:26:26 +0000590 final Rect[] bounds = new Rect[2]; // [MAX_IN_SURFACE, MAX_VISIBLE]
Mihai Popa3e1aed12018-08-03 18:25:52 +0100591 // Obtain the surface bounds rectangle.
592 final Rect surfaceBounds = new Rect(0, 0,
593 mContentCopySurface.mWidth, mContentCopySurface.mHeight);
594 bounds[0] = surfaceBounds;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100595 // Obtain the visible view region rectangle.
Mihai Popaf2980682018-04-30 19:08:57 +0100596 final Rect viewVisibleRegion = new Rect();
597 mView.getGlobalVisibleRect(viewVisibleRegion);
598 if (mView.getViewRootImpl() != null) {
599 // Clamping coordinates relative to the surface, not to the window.
600 final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets;
601 viewVisibleRegion.offset(surfaceInsets.left, surfaceInsets.top);
602 }
603 if (mView instanceof SurfaceView) {
604 // If we copy content from a SurfaceView, clamp coordinates relative to it.
605 viewVisibleRegion.offset(-mViewCoordinatesInSurface[0], -mViewCoordinatesInSurface[1]);
606 }
Mihai Popa520e44752019-01-29 21:26:26 +0000607 bounds[1] = viewVisibleRegion;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100608
609 // Aggregate the above to obtain the bounds where the content copy will be restricted.
610 int resolvedLeft = Integer.MIN_VALUE;
611 for (int i = mLeftContentBound; i >= 0; --i) {
612 resolvedLeft = Math.max(resolvedLeft, bounds[i].left);
613 }
614 int resolvedTop = Integer.MIN_VALUE;
615 for (int i = mTopContentBound; i >= 0; --i) {
616 resolvedTop = Math.max(resolvedTop, bounds[i].top);
617 }
618 int resolvedRight = Integer.MAX_VALUE;
619 for (int i = mRightContentBound; i >= 0; --i) {
620 resolvedRight = Math.min(resolvedRight, bounds[i].right);
621 }
622 int resolvedBottom = Integer.MAX_VALUE;
623 for (int i = mBottomContentBound; i >= 0; --i) {
624 resolvedBottom = Math.min(resolvedBottom, bounds[i].bottom);
625 }
626 // Adjust <left-right> and <top-bottom> pairs of bounds to make sense.
627 resolvedLeft = Math.min(resolvedLeft, mContentCopySurface.mWidth - mSourceWidth);
628 resolvedTop = Math.min(resolvedTop, mContentCopySurface.mHeight - mSourceHeight);
629 if (resolvedLeft < 0 || resolvedTop < 0) {
630 Log.e(TAG, "Magnifier's content is copied from a surface smaller than"
Mihai Popa3aa46b22019-05-07 22:29:52 +0100631 + "the content requested size. The magnifier will be dismissed.");
Mihai Popa3e1aed12018-08-03 18:25:52 +0100632 }
633 resolvedRight = Math.max(resolvedRight, resolvedLeft + mSourceWidth);
634 resolvedBottom = Math.max(resolvedBottom, resolvedTop + mSourceHeight);
635
636 // Finally compute the coordinates of the source center.
637 mClampedCenterZoomCoords.x = Math.max(resolvedLeft + mSourceWidth / 2, Math.min(
638 zoomCenterX, resolvedRight - mSourceWidth / 2));
639 mClampedCenterZoomCoords.y = Math.max(resolvedTop + mSourceHeight / 2, Math.min(
640 zoomCenterY, resolvedBottom - mSourceHeight / 2));
Mihai Popaf2980682018-04-30 19:08:57 +0100641 }
642
Mihai Popa7433a072018-07-18 12:18:34 +0100643 /**
644 * Computes the coordinates of the top left corner of the magnifier window.
645 * These are relative to the surface the magnifier window is attached to.
646 */
647 private void obtainWindowCoordinates(final float xWindowPos, final float yWindowPos) {
648 final int windowCenterX;
649 final int windowCenterY;
650 if (mView instanceof SurfaceView) {
651 // No offset required if the backing Surface matches the size of the SurfaceView.
652 windowCenterX = Math.round(xWindowPos);
653 windowCenterY = Math.round(yWindowPos);
654 } else {
655 windowCenterX = Math.round(xWindowPos + mViewCoordinatesInSurface[0]);
656 windowCenterY = Math.round(yWindowPos + mViewCoordinatesInSurface[1]);
657 }
658
659 mWindowCoords.x = windowCenterX - mWindowWidth / 2;
660 mWindowCoords.y = windowCenterY - mWindowHeight / 2;
Mihai Popaf2980682018-04-30 19:08:57 +0100661 if (mParentSurface != mContentCopySurface) {
662 mWindowCoords.x += mViewCoordinatesInSurface[0];
663 mWindowCoords.y += mViewCoordinatesInSurface[1];
Mihai Popa819e90d2018-04-16 14:27:05 +0100664 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100665 }
666
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000667 private void performPixelCopy(final int startXInSurface, final int startYInSurface,
668 final boolean updateWindowPosition) {
Mihai Popaf2980682018-04-30 19:08:57 +0100669 if (mContentCopySurface.mSurface == null || !mContentCopySurface.mSurface.isValid()) {
Mihai Popa3aa46b22019-05-07 22:29:52 +0100670 onPixelCopyFailed();
Mihai Popa3589c2c2018-01-25 19:26:30 +0000671 return;
672 }
Mihai Popa3e1aed12018-08-03 18:25:52 +0100673
Mihai Popa953b1342018-03-21 18:05:13 +0000674 // Clamp window coordinates inside the parent surface, to avoid displaying
675 // the magnifier out of screen or overlapping with system insets.
Mihai Popa7433a072018-07-18 12:18:34 +0100676 final Point windowCoords = getCurrentClampedWindowCoordinates();
Mihai Popa3589c2c2018-01-25 19:26:30 +0000677
678 // Perform the pixel copy.
Mihai Popa3e1aed12018-08-03 18:25:52 +0100679 mPixelCopyRequestRect.set(startXInSurface,
680 startYInSurface,
681 startXInSurface + mSourceWidth,
682 startYInSurface + mSourceHeight);
Mihai Popa39a71332018-02-22 19:30:24 +0000683 final InternalPopupWindow currentWindowInstance = mWindow;
Mihai Popa8b789102018-02-15 12:06:59 +0000684 final Bitmap bitmap =
Mihai Popa7433a072018-07-18 12:18:34 +0100685 Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ARGB_8888);
Mihai Popaf2980682018-04-30 19:08:57 +0100686 PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap,
Mihai Popa3589c2c2018-01-25 19:26:30 +0000687 result -> {
Mihai Popa3aa46b22019-05-07 22:29:52 +0100688 if (result != PixelCopy.SUCCESS) {
689 onPixelCopyFailed();
690 return;
691 }
Mihai Popa39a71332018-02-22 19:30:24 +0000692 synchronized (mLock) {
693 if (mWindow != currentWindowInstance) {
694 // The magnifier was dismissed (and maybe shown again) in the meantime.
695 return;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000696 }
Mihai Popa39a71332018-02-22 19:30:24 +0000697 if (updateWindowPosition) {
698 // TODO: pull the position update outside #performPixelCopy
Mihai Popa7433a072018-07-18 12:18:34 +0100699 mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
Mihai Popa39a71332018-02-22 19:30:24 +0000700 }
701 mWindow.updateContent(bitmap);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000702 }
Mihai Popa3589c2c2018-01-25 19:26:30 +0000703 },
Mihai Popa8b789102018-02-15 12:06:59 +0000704 sPixelCopyHandlerThread.getThreadHandler());
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000705 mPrevStartCoordsInSurface.x = startXInSurface;
706 mPrevStartCoordsInSurface.y = startYInSurface;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100707 mDirtyState = false;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000708 }
709
Mihai Popa3aa46b22019-05-07 22:29:52 +0100710 private void onPixelCopyFailed() {
711 Log.e(TAG, "Magnifier failed to copy content from the view Surface. It will be dismissed.");
712 // Post to make sure #dismiss is done on the main thread.
713 Handler.getMain().postAtFrontOfQueue(() -> {
714 dismiss();
715 if (mCallback != null) {
716 mCallback.onOperationComplete();
717 }
718 });
719 }
720
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000721 /**
Mihai Popa7433a072018-07-18 12:18:34 +0100722 * Clamp window coordinates inside the surface the magnifier is attached to, to avoid
723 * displaying the magnifier out of screen or overlapping with system insets.
724 * @return the current window coordinates, after they are clamped inside the parent surface
725 */
726 private Point getCurrentClampedWindowCoordinates() {
Mihai Popabecda342018-12-13 17:12:37 +0000727 if (!mClippingEnabled) {
Mihai Popa1903cab2018-08-01 14:33:12 +0100728 // No position adjustment should be done, so return the raw coordinates.
729 return new Point(mWindowCoords);
730 }
731
Mihai Popa7433a072018-07-18 12:18:34 +0100732 final Rect windowBounds;
733 if (mParentSurface.mIsMainWindowSurface) {
Adrian Roos60f59292018-08-24 16:29:06 +0200734 final Insets systemInsets = mView.getRootWindowInsets().getSystemWindowInsets();
Mihai Popaef456972019-02-18 15:17:09 +0000735 windowBounds = new Rect(
736 systemInsets.left + mParentSurface.mInsets.left,
737 systemInsets.top + mParentSurface.mInsets.top,
738 mParentSurface.mWidth - systemInsets.right - mParentSurface.mInsets.right,
739 mParentSurface.mHeight - systemInsets.bottom
740 - mParentSurface.mInsets.bottom
741 );
Mihai Popa7433a072018-07-18 12:18:34 +0100742 } else {
743 windowBounds = new Rect(0, 0, mParentSurface.mWidth, mParentSurface.mHeight);
744 }
745 final int windowCoordsX = Math.max(windowBounds.left,
746 Math.min(windowBounds.right - mWindowWidth, mWindowCoords.x));
747 final int windowCoordsY = Math.max(windowBounds.top,
748 Math.min(windowBounds.bottom - mWindowHeight, mWindowCoords.y));
749 return new Point(windowCoordsX, windowCoordsY);
750 }
751
752 /**
Mihai Popaf2980682018-04-30 19:08:57 +0100753 * Contains a surface and metadata corresponding to it.
754 */
755 private static class SurfaceInfo {
Mihai Popaef456972019-02-18 15:17:09 +0000756 public static final SurfaceInfo NULL = new SurfaceInfo(null, null, 0, 0, null, false);
Mihai Popaf2980682018-04-30 19:08:57 +0100757
758 private Surface mSurface;
Robert Carr5fea55b2018-12-10 13:05:52 -0800759 private SurfaceControl mSurfaceControl;
Mihai Popaf2980682018-04-30 19:08:57 +0100760 private int mWidth;
761 private int mHeight;
Mihai Popaef456972019-02-18 15:17:09 +0000762 private Rect mInsets;
Mihai Popaf2980682018-04-30 19:08:57 +0100763 private boolean mIsMainWindowSurface;
764
Robert Carr5fea55b2018-12-10 13:05:52 -0800765 SurfaceInfo(final SurfaceControl surfaceControl, final Surface surface,
Mihai Popaef456972019-02-18 15:17:09 +0000766 final int width, final int height, final Rect insets,
Mihai Popaf2980682018-04-30 19:08:57 +0100767 final boolean isMainWindowSurface) {
Robert Carr5fea55b2018-12-10 13:05:52 -0800768 mSurfaceControl = surfaceControl;
Mihai Popaf2980682018-04-30 19:08:57 +0100769 mSurface = surface;
770 mWidth = width;
771 mHeight = height;
Mihai Popaef456972019-02-18 15:17:09 +0000772 mInsets = insets;
Mihai Popaf2980682018-04-30 19:08:57 +0100773 mIsMainWindowSurface = isMainWindowSurface;
774 }
775 }
776
777 /**
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000778 * Magnifier's own implementation of PopupWindow-similar floating window.
779 * This exists to ensure frame-synchronization between window position updates and window
780 * content updates. By using a PopupWindow, these events would happen in different frames,
781 * producing a shakiness effect for the magnifier content.
782 */
783 private static class InternalPopupWindow {
Mihai Popa819e90d2018-04-16 14:27:05 +0100784 // The z of the magnifier surface, defining its z order in the list of
785 // siblings having the same parent surface (usually the main app surface).
786 private static final int SURFACE_Z = 5;
Mihai Popad870b882018-02-27 14:25:52 +0000787
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000788 // Display associated to the view the magnifier is attached to.
789 private final Display mDisplay;
790 // The size of the content of the magnifier.
791 private final int mContentWidth;
792 private final int mContentHeight;
793 // The size of the allocated surface.
794 private final int mSurfaceWidth;
795 private final int mSurfaceHeight;
796 // The insets of the content inside the allocated surface.
797 private final int mOffsetX;
798 private final int mOffsetY;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100799 // The overlay to be drawn on the top of the content.
800 private final Drawable mOverlay;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000801 // The surface we allocate for the magnifier content + shadow.
802 private final SurfaceSession mSurfaceSession;
803 private final SurfaceControl mSurfaceControl;
Robert Carr64660be2019-08-26 16:20:55 -0700804 private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000805 private final Surface mSurface;
806 // The renderer used for the allocated surface.
807 private final ThreadedRenderer.SimpleRenderer mRenderer;
808 // The RenderNode used to draw the magnifier content in the surface.
809 private final RenderNode mBitmapRenderNode;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100810 // The RenderNode used to draw the overlay over the magnifier content.
811 private final RenderNode mOverlayRenderNode;
Mihai Popa8b789102018-02-15 12:06:59 +0000812 // The job that will be post'd to apply the pending magnifier updates to the surface.
813 private final Runnable mMagnifierUpdater;
814 // The handler where the magnifier updater jobs will be post'd.
815 private final Handler mHandler;
Mihai Popa63ee7f12018-04-05 12:01:53 +0100816 // The callback to be run after the next draw.
Mihai Popa2ba5d8e2018-02-20 18:50:20 +0000817 private Callback mCallback;
Mihai Popa63ee7f12018-04-05 12:01:53 +0100818 // The position of the magnifier content when the last draw was requested.
819 private int mLastDrawContentPositionX;
820 private int mLastDrawContentPositionY;
Mihai Popa8b789102018-02-15 12:06:59 +0000821
822 // Members below describe the state of the magnifier. Reads/writes to them
823 // have to be synchronized between the UI thread and the thread that handles
824 // the pixel copy results. This is the purpose of mLock.
Mihai Popa39a71332018-02-22 19:30:24 +0000825 private final Object mLock;
Mihai Popa8b789102018-02-15 12:06:59 +0000826 // Whether a magnifier frame draw is currently pending in the UI thread queue.
827 private boolean mFrameDrawScheduled;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100828 // The content bitmap, as returned by pixel copy.
Mihai Popa8b789102018-02-15 12:06:59 +0000829 private Bitmap mBitmap;
830 // Whether the next draw will be the first one for the current instance.
831 private boolean mFirstDraw = true;
832 // The window position in the parent surface. Might be applied during the next draw,
833 // when mPendingWindowPositionUpdate is true.
834 private int mWindowPositionX;
835 private int mWindowPositionY;
836 private boolean mPendingWindowPositionUpdate;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000837
Mihai Popa1ddabb22018-08-06 11:55:54 +0100838 // The current content of the magnifier. It is mBitmap + mOverlay, only used for testing.
839 private Bitmap mCurrentContent;
840
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000841 InternalPopupWindow(final Context context, final Display display,
Robert Carr5fea55b2018-12-10 13:05:52 -0800842 final SurfaceControl parentSurfaceControl, final int width, final int height,
Mihai Popa1ddabb22018-08-06 11:55:54 +0100843 final float elevation, final float cornerRadius, final Drawable overlay,
Mihai Popaafee43b2019-06-17 12:34:50 +0100844 final Handler handler, final Object lock, final Callback callback) {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000845 mDisplay = display;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100846 mOverlay = overlay;
Mihai Popa39a71332018-02-22 19:30:24 +0000847 mLock = lock;
Mihai Popa2ba5d8e2018-02-20 18:50:20 +0000848 mCallback = callback;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000849
850 mContentWidth = width;
851 mContentHeight = height;
Mihai Popabd391f92019-01-08 20:07:01 +0000852 mOffsetX = (int) (1.05f * elevation);
853 mOffsetY = (int) (1.05f * elevation);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000854 // Setup the surface we will use for drawing the content and shadow.
855 mSurfaceWidth = mContentWidth + 2 * mOffsetX;
856 mSurfaceHeight = mContentHeight + 2 * mOffsetY;
Robert Carr5fea55b2018-12-10 13:05:52 -0800857 mSurfaceSession = new SurfaceSession();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000858 mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
859 .setFormat(PixelFormat.TRANSLUCENT)
Vishnu Naire86bd982018-11-28 13:23:17 -0800860 .setBufferSize(mSurfaceWidth, mSurfaceHeight)
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000861 .setName("magnifier surface")
862 .setFlags(SurfaceControl.HIDDEN)
Robert Carr5fea55b2018-12-10 13:05:52 -0800863 .setParent(parentSurfaceControl)
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000864 .build();
865 mSurface = new Surface();
866 mSurface.copyFrom(mSurfaceControl);
867
Mihai Popa1ddabb22018-08-06 11:55:54 +0100868 // Setup the RenderNode tree. The root has two children, one containing the bitmap
869 // and one containing the overlay. We use a separate render node for the overlay
870 // to avoid drawing this as the same rate we do for content.
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000871 mRenderer = new ThreadedRenderer.SimpleRenderer(
872 context,
873 "magnifier renderer",
874 mSurface
875 );
876 mBitmapRenderNode = createRenderNodeForBitmap(
877 "magnifier content",
Mihai Popafb4b6b82018-03-01 16:08:14 +0000878 elevation,
879 cornerRadius
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000880 );
Mihai Popa1ddabb22018-08-06 11:55:54 +0100881 mOverlayRenderNode = createRenderNodeForOverlay(
882 "magnifier overlay",
883 cornerRadius
884 );
885 setupOverlay();
Mihai Popa8b789102018-02-15 12:06:59 +0000886
John Recke57475e2019-02-20 17:39:52 -0800887 final RecordingCanvas canvas = mRenderer.getRootNode().beginRecording(width, height);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000888 try {
889 canvas.insertReorderBarrier();
890 canvas.drawRenderNode(mBitmapRenderNode);
891 canvas.insertInorderBarrier();
Mihai Popa1ddabb22018-08-06 11:55:54 +0100892 canvas.drawRenderNode(mOverlayRenderNode);
893 canvas.insertInorderBarrier();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000894 } finally {
John Recke57475e2019-02-20 17:39:52 -0800895 mRenderer.getRootNode().endRecording();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000896 }
Mihai Popa1ddabb22018-08-06 11:55:54 +0100897 if (mCallback != null) {
898 mCurrentContent =
899 Bitmap.createBitmap(mContentWidth, mContentHeight, Bitmap.Config.ARGB_8888);
900 updateCurrentContentForTesting();
901 }
Mihai Popa8b789102018-02-15 12:06:59 +0000902
903 // Initialize the update job and the handler where this will be post'd.
904 mHandler = handler;
905 mMagnifierUpdater = this::doDraw;
906 mFrameDrawScheduled = false;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000907 }
908
Mihai Popafb4b6b82018-03-01 16:08:14 +0000909 private RenderNode createRenderNodeForBitmap(final String name,
910 final float elevation, final float cornerRadius) {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000911 final RenderNode bitmapRenderNode = RenderNode.create(name, null);
912
913 // Define the position of the bitmap in the parent render node. The surface regions
914 // outside the bitmap are used to draw elevation.
915 bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
916 mOffsetX + mContentWidth, mOffsetY + mContentHeight);
917 bitmapRenderNode.setElevation(elevation);
918
919 final Outline outline = new Outline();
Mihai Popafb4b6b82018-03-01 16:08:14 +0000920 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000921 outline.setAlpha(1.0f);
922 bitmapRenderNode.setOutline(outline);
923 bitmapRenderNode.setClipToOutline(true);
924
925 // Create a dummy draw, which will be replaced later with real drawing.
John Recke57475e2019-02-20 17:39:52 -0800926 final RecordingCanvas canvas = bitmapRenderNode.beginRecording(
927 mContentWidth, mContentHeight);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000928 try {
929 canvas.drawColor(0xFF00FF00);
930 } finally {
John Recke57475e2019-02-20 17:39:52 -0800931 bitmapRenderNode.endRecording();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000932 }
933
934 return bitmapRenderNode;
935 }
936
Mihai Popa1ddabb22018-08-06 11:55:54 +0100937 private RenderNode createRenderNodeForOverlay(final String name, final float cornerRadius) {
938 final RenderNode overlayRenderNode = RenderNode.create(name, null);
939
940 // Define the position of the overlay in the parent render node.
941 // This coincides with the position of the content.
942 overlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
943 mOffsetX + mContentWidth, mOffsetY + mContentHeight);
944
945 final Outline outline = new Outline();
946 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
947 outline.setAlpha(1.0f);
948 overlayRenderNode.setOutline(outline);
949 overlayRenderNode.setClipToOutline(true);
950
951 return overlayRenderNode;
952 }
953
954 private void setupOverlay() {
955 drawOverlay();
956
957 mOverlay.setCallback(new Drawable.Callback() {
958 @Override
959 public void invalidateDrawable(Drawable who) {
960 // When the overlay drawable is invalidated, redraw it to the render node.
961 drawOverlay();
962 if (mCallback != null) {
963 updateCurrentContentForTesting();
964 }
965 }
966
967 @Override
968 public void scheduleDrawable(Drawable who, Runnable what, long when) {
969 Handler.getMain().postAtTime(what, who, when);
970 }
971
972 @Override
973 public void unscheduleDrawable(Drawable who, Runnable what) {
974 Handler.getMain().removeCallbacks(what, who);
975 }
976 });
977 }
978
979 private void drawOverlay() {
980 // Draw the drawable to the render node. This happens once during
981 // initialization and whenever the overlay drawable is invalidated.
982 final RecordingCanvas canvas =
John Recke57475e2019-02-20 17:39:52 -0800983 mOverlayRenderNode.beginRecording(mContentWidth, mContentHeight);
Mihai Popa1ddabb22018-08-06 11:55:54 +0100984 try {
985 mOverlay.setBounds(0, 0, mContentWidth, mContentHeight);
986 mOverlay.draw(canvas);
987 } finally {
988 mOverlayRenderNode.endRecording();
989 }
990 }
991
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000992 /**
993 * Sets the position of the magnifier content relative to the parent surface.
994 * The position update will happen in the same frame with the next draw.
Mihai Popa8b789102018-02-15 12:06:59 +0000995 * The method has to be called in a context that holds {@link #mLock}.
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000996 *
997 * @param contentX the x coordinate of the content
998 * @param contentY the y coordinate of the content
999 */
1000 public void setContentPositionForNextDraw(final int contentX, final int contentY) {
1001 mWindowPositionX = contentX - mOffsetX;
1002 mWindowPositionY = contentY - mOffsetY;
1003 mPendingWindowPositionUpdate = true;
Mihai Popa8b789102018-02-15 12:06:59 +00001004 requestUpdate();
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001005 }
1006
1007 /**
1008 * Sets the content that should be displayed in the magnifier.
1009 * The update happens immediately, and possibly triggers a pending window movement set
1010 * by {@link #setContentPositionForNextDraw(int, int)}.
Mihai Popa8b789102018-02-15 12:06:59 +00001011 * The method has to be called in a context that holds {@link #mLock}.
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001012 *
1013 * @param bitmap the content bitmap
1014 */
Mihai Popa8b789102018-02-15 12:06:59 +00001015 public void updateContent(final @NonNull Bitmap bitmap) {
1016 if (mBitmap != null) {
1017 mBitmap.recycle();
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001018 }
Mihai Popa8b789102018-02-15 12:06:59 +00001019 mBitmap = bitmap;
1020 requestUpdate();
1021 }
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001022
Mihai Popa8b789102018-02-15 12:06:59 +00001023 private void requestUpdate() {
1024 if (mFrameDrawScheduled) {
1025 return;
1026 }
1027 final Message request = Message.obtain(mHandler, mMagnifierUpdater);
1028 request.setAsynchronous(true);
1029 request.sendToTarget();
1030 mFrameDrawScheduled = true;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001031 }
1032
1033 /**
Mihai Popaafee43b2019-06-17 12:34:50 +01001034 * Destroys this instance. The method has to be called in a context holding {@link #mLock}.
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001035 */
1036 public void destroy() {
Mihai Popaafee43b2019-06-17 12:34:50 +01001037 // Destroy the renderer. This will not proceed until pending frame callbacks complete.
1038 mRenderer.destroy();
1039 mSurface.destroy();
chaviw9f6171e2019-06-07 16:33:50 -07001040 new SurfaceControl.Transaction().remove(mSurfaceControl).apply();
Mihai Popaafee43b2019-06-17 12:34:50 +01001041 mSurfaceSession.kill();
1042 mHandler.removeCallbacks(mMagnifierUpdater);
1043 if (mBitmap != null) {
1044 mBitmap.recycle();
Mihai Popa8b789102018-02-15 12:06:59 +00001045 }
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001046 }
1047
1048 private void doDraw() {
1049 final ThreadedRenderer.FrameDrawingCallback callback;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001050
Mihai Popa8b789102018-02-15 12:06:59 +00001051 // Draw the current bitmap to the surface, and prepare the callback which updates the
1052 // surface position. These have to be in the same synchronized block, in order to
1053 // guarantee the consistency between the bitmap content and the surface position.
1054 synchronized (mLock) {
1055 if (!mSurface.isValid()) {
1056 // Probably #destroy() was called for the current instance, so we skip the draw.
1057 return;
1058 }
1059
John Reck32f140aa62018-10-04 15:08:24 -07001060 final RecordingCanvas canvas =
John Recke57475e2019-02-20 17:39:52 -08001061 mBitmapRenderNode.beginRecording(mContentWidth, mContentHeight);
Mihai Popa8b789102018-02-15 12:06:59 +00001062 try {
1063 final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
1064 final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
1065 final Paint paint = new Paint();
1066 paint.setFilterBitmap(true);
1067 canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
1068 } finally {
John Recke57475e2019-02-20 17:39:52 -08001069 mBitmapRenderNode.endRecording();
Mihai Popa8b789102018-02-15 12:06:59 +00001070 }
1071
1072 if (mPendingWindowPositionUpdate || mFirstDraw) {
1073 // If the window has to be shown or moved, defer this until the next draw.
1074 final boolean firstDraw = mFirstDraw;
1075 mFirstDraw = false;
1076 final boolean updateWindowPosition = mPendingWindowPositionUpdate;
1077 mPendingWindowPositionUpdate = false;
1078 final int pendingX = mWindowPositionX;
1079 final int pendingY = mWindowPositionY;
1080
1081 callback = frame -> {
Mihai Popaafee43b2019-06-17 12:34:50 +01001082 if (!mSurface.isValid()) {
1083 return;
Mihai Popa8b789102018-02-15 12:06:59 +00001084 }
Mihai Popaafee43b2019-06-17 12:34:50 +01001085 // Show or move the window at the content draw frame.
Robert Carr64660be2019-08-26 16:20:55 -07001086 mTransaction.deferTransactionUntilSurface(mSurfaceControl, mSurface, frame);
Mihai Popaafee43b2019-06-17 12:34:50 +01001087 if (updateWindowPosition) {
Robert Carr64660be2019-08-26 16:20:55 -07001088 mTransaction.setPosition(mSurfaceControl, pendingX, pendingY);
Mihai Popaafee43b2019-06-17 12:34:50 +01001089 }
1090 if (firstDraw) {
Robert Carr64660be2019-08-26 16:20:55 -07001091 mTransaction.setLayer(mSurfaceControl, SURFACE_Z)
1092 .show(mSurfaceControl);
1093
Mihai Popaafee43b2019-06-17 12:34:50 +01001094 }
Robert Carr64660be2019-08-26 16:20:55 -07001095 mTransaction.apply();
Mihai Popa8b789102018-02-15 12:06:59 +00001096 };
Mihai Popa6bf0b2f62019-02-26 18:01:37 +00001097 mRenderer.setLightCenter(mDisplay, pendingX, pendingY);
Mihai Popa8b789102018-02-15 12:06:59 +00001098 } else {
1099 callback = null;
1100 }
1101
Mihai Popa63ee7f12018-04-05 12:01:53 +01001102 mLastDrawContentPositionX = mWindowPositionX + mOffsetX;
1103 mLastDrawContentPositionY = mWindowPositionY + mOffsetY;
Mihai Popa8b789102018-02-15 12:06:59 +00001104 mFrameDrawScheduled = false;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001105 }
1106
1107 mRenderer.draw(callback);
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001108 if (mCallback != null) {
Mihai Popa1ddabb22018-08-06 11:55:54 +01001109 // The current content bitmap is only used in testing, so, for performance,
1110 // we only want to update it when running tests. For this, we check that
1111 // mCallback is not null, as it can only be set from a @TestApi.
1112 updateCurrentContentForTesting();
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001113 mCallback.onOperationComplete();
1114 }
1115 }
Mihai Popa1ddabb22018-08-06 11:55:54 +01001116
1117 /**
1118 * Updates mCurrentContent, which reproduces what is currently supposed to be
1119 * drawn in the magnifier. mCurrentContent is only used for testing, so this method
1120 * should only be called otherwise.
1121 */
1122 private void updateCurrentContentForTesting() {
1123 final Canvas canvas = new Canvas(mCurrentContent);
1124 final Rect bounds = new Rect(0, 0, mContentWidth, mContentHeight);
1125 if (mBitmap != null && !mBitmap.isRecycled()) {
1126 final Rect originalBounds = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
1127 canvas.drawBitmap(mBitmap, originalBounds, bounds, null);
1128 }
1129 mOverlay.setBounds(bounds);
1130 mOverlay.draw(canvas);
1131 }
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001132 }
1133
Mihai Popa469aba82018-07-18 14:52:26 +01001134 /**
1135 * Builder class for {@link Magnifier} objects.
1136 */
Mihai Popa53fd6962019-03-05 20:37:35 +00001137 public static final class Builder {
Mihai Popa469aba82018-07-18 14:52:26 +01001138 private @NonNull View mView;
1139 private @Px @IntRange(from = 0) int mWidth;
1140 private @Px @IntRange(from = 0) int mHeight;
1141 private float mZoom;
1142 private @FloatRange(from = 0f) float mElevation;
1143 private @FloatRange(from = 0f) float mCornerRadius;
Mihai Popa1ddabb22018-08-06 11:55:54 +01001144 private @Nullable Drawable mOverlay;
Mihai Popa469aba82018-07-18 14:52:26 +01001145 private int mHorizontalDefaultSourceToMagnifierOffset;
1146 private int mVerticalDefaultSourceToMagnifierOffset;
Mihai Popabecda342018-12-13 17:12:37 +00001147 private boolean mClippingEnabled;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001148 private @SourceBound int mLeftContentBound;
1149 private @SourceBound int mTopContentBound;
1150 private @SourceBound int mRightContentBound;
1151 private @SourceBound int mBottomContentBound;
Mihai Popa469aba82018-07-18 14:52:26 +01001152
1153 /**
1154 * Construct a new builder for {@link Magnifier} objects.
1155 * @param view the view this magnifier is attached to
1156 */
1157 public Builder(@NonNull View view) {
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +00001158 mView = Objects.requireNonNull(view);
Mihai Popa469aba82018-07-18 14:52:26 +01001159 applyDefaults();
1160 }
1161
1162 private void applyDefaults() {
Mihai Popac6950292018-11-15 21:32:42 +00001163 final Resources resources = mView.getContext().getResources();
1164 mWidth = resources.getDimensionPixelSize(R.dimen.default_magnifier_width);
1165 mHeight = resources.getDimensionPixelSize(R.dimen.default_magnifier_height);
1166 mElevation = resources.getDimension(R.dimen.default_magnifier_elevation);
1167 mCornerRadius = resources.getDimension(R.dimen.default_magnifier_corner_radius);
1168 mZoom = resources.getFloat(R.dimen.default_magnifier_zoom);
Mihai Popa469aba82018-07-18 14:52:26 +01001169 mHorizontalDefaultSourceToMagnifierOffset =
Mihai Popac6950292018-11-15 21:32:42 +00001170 resources.getDimensionPixelSize(R.dimen.default_magnifier_horizontal_offset);
Mihai Popa469aba82018-07-18 14:52:26 +01001171 mVerticalDefaultSourceToMagnifierOffset =
Mihai Popac6950292018-11-15 21:32:42 +00001172 resources.getDimensionPixelSize(R.dimen.default_magnifier_vertical_offset);
1173 mOverlay = new ColorDrawable(resources.getColor(
1174 R.color.default_magnifier_color_overlay, null));
Mihai Popabecda342018-12-13 17:12:37 +00001175 mClippingEnabled = true;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001176 mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
Mihai Popac6950292018-11-15 21:32:42 +00001177 mTopContentBound = SOURCE_BOUND_MAX_VISIBLE;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001178 mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
Mihai Popac6950292018-11-15 21:32:42 +00001179 mBottomContentBound = SOURCE_BOUND_MAX_VISIBLE;
Mihai Popa469aba82018-07-18 14:52:26 +01001180 }
1181
1182 /**
1183 * Sets the size of the magnifier window, in pixels. Defaults to (100dp, 48dp).
1184 * Note that the size of the content being magnified and copied to the magnifier
1185 * will be computed as (window width / zoom, window height / zoom).
1186 * @param width the window width to be set
1187 * @param height the window height to be set
1188 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001189 @NonNull
Mihai Popa469aba82018-07-18 14:52:26 +01001190 public Builder setSize(@Px @IntRange(from = 0) int width,
1191 @Px @IntRange(from = 0) int height) {
1192 Preconditions.checkArgumentPositive(width, "Width should be positive");
1193 Preconditions.checkArgumentPositive(height, "Height should be positive");
1194 mWidth = width;
1195 mHeight = height;
1196 return this;
1197 }
1198
1199 /**
1200 * Sets the zoom to be applied to the chosen content before being copied to the magnifier.
1201 * A content of size (content_width, content_height) will be magnified to
1202 * (content_width * zoom, content_height * zoom), which will coincide with the size
1203 * of the magnifier. A zoom of 1 will translate to no magnification (the content will
1204 * be just copied to the magnifier with no scaling). The zoom defaults to 1.25.
Mihai Popa27cf08f2019-01-10 19:59:29 +00001205 * Note that the zoom can also be changed after the instance is built, using the
1206 * {@link Magnifier#setZoom(float)} method.
Mihai Popa469aba82018-07-18 14:52:26 +01001207 * @param zoom the zoom to be set
1208 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001209 @NonNull
Mihai Popa27cf08f2019-01-10 19:59:29 +00001210 public Builder setInitialZoom(@FloatRange(from = 0f) float zoom) {
Mihai Popa469aba82018-07-18 14:52:26 +01001211 Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
1212 mZoom = zoom;
1213 return this;
1214 }
1215
1216 /**
1217 * Sets the elevation of the magnifier window, in pixels. Defaults to 4dp.
1218 * @param elevation the elevation to be set
1219 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001220 @NonNull
Mihai Popa469aba82018-07-18 14:52:26 +01001221 public Builder setElevation(@Px @FloatRange(from = 0) float elevation) {
1222 Preconditions.checkArgumentNonNegative(elevation, "Elevation should be non-negative");
1223 mElevation = elevation;
1224 return this;
1225 }
1226
1227 /**
Mihai Popac6950292018-11-15 21:32:42 +00001228 * Sets the corner radius of the magnifier window, in pixels. Defaults to 2dp.
Mihai Popa469aba82018-07-18 14:52:26 +01001229 * @param cornerRadius the corner radius to be set
1230 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001231 @NonNull
Mihai Popa469aba82018-07-18 14:52:26 +01001232 public Builder setCornerRadius(@Px @FloatRange(from = 0) float cornerRadius) {
1233 Preconditions.checkArgumentNonNegative(cornerRadius,
1234 "Corner radius should be non-negative");
1235 mCornerRadius = cornerRadius;
1236 return this;
1237 }
1238
1239 /**
Mihai Popabecda342018-12-13 17:12:37 +00001240 * Sets an overlay that will be drawn on the top of the magnifier.
1241 * In general, the overlay should not be opaque, in order to let the magnified
1242 * content be partially visible in the magnifier. The default overlay is {@code null}
1243 * (no overlay). As an example, TextView applies a white {@link ColorDrawable}
1244 * overlay with 5% alpha, aiming to make the magnifier distinguishable when shown in dark
Mihai Popac6950292018-11-15 21:32:42 +00001245 * application regions. To disable the overlay, the parameter should be set
1246 * to {@code null}. If not null, the overlay will be automatically redrawn
Mihai Popa1ddabb22018-08-06 11:55:54 +01001247 * when the drawable is invalidated. To achieve this, the magnifier will set a new
1248 * {@link android.graphics.drawable.Drawable.Callback} for the overlay drawable,
1249 * so keep in mind that any existing one set by the application will be lost.
1250 * @param overlay the overlay to be drawn on top
1251 */
1252 @NonNull
1253 public Builder setOverlay(@Nullable Drawable overlay) {
1254 mOverlay = overlay;
1255 return this;
1256 }
1257
1258 /**
1259 * Sets an offset that should be added to the content source center to obtain
Mihai Popa469aba82018-07-18 14:52:26 +01001260 * the position of the magnifier window, when the {@link #show(float, float)}
1261 * method is called. The offset is ignored when {@link #show(float, float, float, float)}
Mihai Popac6950292018-11-15 21:32:42 +00001262 * is used. The offset can be negative. It defaults to (0dp, 0dp).
Mihai Popa469aba82018-07-18 14:52:26 +01001263 * @param horizontalOffset the horizontal component of the offset
1264 * @param verticalOffset the vertical component of the offset
1265 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001266 @NonNull
Mihai Popa469aba82018-07-18 14:52:26 +01001267 public Builder setDefaultSourceToMagnifierOffset(@Px int horizontalOffset,
1268 @Px int verticalOffset) {
1269 mHorizontalDefaultSourceToMagnifierOffset = horizontalOffset;
1270 mVerticalDefaultSourceToMagnifierOffset = verticalOffset;
1271 return this;
1272 }
1273
1274 /**
Mihai Popa1903cab2018-08-01 14:33:12 +01001275 * Defines the behavior of the magnifier when it is requested to position outside the
1276 * surface of the main application window. The default value is {@code true}, which means
1277 * that the position will be adjusted such that the magnifier will be fully within the
Mihai Popabecda342018-12-13 17:12:37 +00001278 * bounds of the main application window, while also avoiding any overlap with system insets
1279 * (such as the one corresponding to the status bar). If this flag is set to {@code false},
1280 * the area where the magnifier can be positioned will no longer be clipped, so the
1281 * magnifier will be able to extend outside the main application window boundaries (and also
1282 * overlap the system insets). This can be useful if you require a custom behavior, but it
1283 * should be handled with care, when passing coordinates to {@link #show(float, float)};
1284 * note that:
Mihai Popa1903cab2018-08-01 14:33:12 +01001285 * <ul>
1286 * <li>in a multiwindow context, if the magnifier crosses the boundary between the two
1287 * windows, it will not be able to show over the window of the other application</li>
1288 * <li>if the magnifier overlaps the status bar, there is no guarantee about which one
1289 * will be displayed on top. This should be handled with care.</li>
1290 * </ul>
Mihai Popabecda342018-12-13 17:12:37 +00001291 * @param clip whether the magnifier position will be adjusted
Mihai Popa1903cab2018-08-01 14:33:12 +01001292 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001293 @NonNull
Mihai Popabecda342018-12-13 17:12:37 +00001294 public Builder setClippingEnabled(boolean clip) {
1295 mClippingEnabled = clip;
Mihai Popa1903cab2018-08-01 14:33:12 +01001296 return this;
1297 }
1298
1299 /**
Mihai Popa3e1aed12018-08-03 18:25:52 +01001300 * Defines the bounds of the rectangle where the magnifier will be able to copy its content
1301 * from. The content will always be copied from the {@link Surface} of the main application
1302 * window unless the magnified view is a {@link SurfaceView}, in which case its backing
1303 * surface will be used. Each bound can have a different behavior, with the options being:
1304 * <ul>
1305 * <li>{@link #SOURCE_BOUND_MAX_VISIBLE}, which extends the bound as much as possible
1306 * while remaining in the visible region of the magnified view, as given by
1307 * {@link android.view.View#getGlobalVisibleRect(Rect)}. For example, this will take into
1308 * account the case when the view is contained in a scrollable container, and the
1309 * magnifier will refuse to copy content outside of the visible view region</li>
Mihai Popa3e1aed12018-08-03 18:25:52 +01001310 * <li>{@link #SOURCE_BOUND_MAX_IN_SURFACE}, which extends the bound as much
1311 * as possible while remaining inside the surface the content is copied from.</li>
1312 * </ul>
1313 * Note that if either of the first three options is used, the bound will be compared to
1314 * the bound of the surface (i.e. as if {@link #SOURCE_BOUND_MAX_IN_SURFACE} was used),
1315 * and the more restrictive one will be chosen. In other words, no attempt to copy content
1316 * from outside the surface will be permitted. If two opposite bounds are not well-behaved
1317 * (i.e. left + sourceWidth > right or top + sourceHeight > bottom), the left and top
1318 * bounds will have priority and the others will be extended accordingly. If the pairs
1319 * obtained this way still remain out of bounds, the smallest possible offset will be added
1320 * to the pairs to bring them inside the surface bounds. If this is impossible
1321 * (i.e. the surface is too small for the size of the content we try to copy on either
1322 * dimension), an error will be logged and the magnifier content will look distorted.
1323 * The default values assumed by the builder for the source bounds are
1324 * left: {@link #SOURCE_BOUND_MAX_VISIBLE}, top: {@link #SOURCE_BOUND_MAX_IN_SURFACE},
1325 * right: {@link #SOURCE_BOUND_MAX_VISIBLE}, bottom: {@link #SOURCE_BOUND_MAX_IN_SURFACE}.
1326 * @param left the left bound for content copy
1327 * @param top the top bound for content copy
1328 * @param right the right bound for content copy
1329 * @param bottom the bottom bound for content copy
1330 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001331 @NonNull
Mihai Popa3e1aed12018-08-03 18:25:52 +01001332 public Builder setSourceBounds(@SourceBound int left, @SourceBound int top,
1333 @SourceBound int right, @SourceBound int bottom) {
1334 mLeftContentBound = left;
1335 mTopContentBound = top;
1336 mRightContentBound = right;
1337 mBottomContentBound = bottom;
1338 return this;
1339 }
1340
1341 /**
Mihai Popa469aba82018-07-18 14:52:26 +01001342 * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}.
1343 */
1344 public @NonNull Magnifier build() {
1345 return new Magnifier(this);
1346 }
1347 }
1348
Mihai Popa3e1aed12018-08-03 18:25:52 +01001349 /**
1350 * A source bound that will extend as much as possible, while remaining within the surface
1351 * the content is copied from.
1352 */
Mihai Popa3e1aed12018-08-03 18:25:52 +01001353 public static final int SOURCE_BOUND_MAX_IN_SURFACE = 0;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001354
1355 /**
1356 * A source bound that will extend as much as possible, while remaining within the
1357 * visible region of the magnified view, as determined by
1358 * {@link View#getGlobalVisibleRect(Rect)}.
1359 */
Mihai Popa520e44752019-01-29 21:26:26 +00001360 public static final int SOURCE_BOUND_MAX_VISIBLE = 1;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001361
1362
1363 /**
1364 * Used to describe the {@link Surface} rectangle where the magnifier's content is allowed
1365 * to be copied from. For more details, see method
1366 * {@link Magnifier.Builder#setSourceBounds(int, int, int, int)}
1367 *
1368 * @hide
1369 */
Mihai Popa520e44752019-01-29 21:26:26 +00001370 @IntDef({SOURCE_BOUND_MAX_IN_SURFACE, SOURCE_BOUND_MAX_VISIBLE})
Mihai Popa3e1aed12018-08-03 18:25:52 +01001371 @Retention(RetentionPolicy.SOURCE)
1372 public @interface SourceBound {}
1373
Mihai Popa1ddabb22018-08-06 11:55:54 +01001374 // The rest of the file consists of test APIs and methods relevant for tests.
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001375
1376 /**
1377 * See {@link #setOnOperationCompleteCallback(Callback)}.
1378 */
1379 @TestApi
1380 private Callback mCallback;
1381
1382 /**
1383 * Sets a callback which will be invoked at the end of the next
1384 * {@link #show(float, float)} or {@link #update()} operation.
1385 *
1386 * @hide
1387 */
1388 @TestApi
1389 public void setOnOperationCompleteCallback(final Callback callback) {
1390 mCallback = callback;
1391 if (mWindow != null) {
1392 mWindow.mCallback = callback;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001393 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01001394 }
Mihai Popa137b5842018-01-30 15:03:22 +00001395
1396 /**
Mihai Popa1ddabb22018-08-06 11:55:54 +01001397 * @return the drawing being currently displayed in the magnifier, as bitmap
Mihai Popa137b5842018-01-30 15:03:22 +00001398 *
1399 * @hide
1400 */
1401 @TestApi
Mihai Popa8b789102018-02-15 12:06:59 +00001402 public @Nullable Bitmap getContent() {
1403 if (mWindow == null) {
1404 return null;
1405 }
1406 synchronized (mWindow.mLock) {
Mihai Popa1ddabb22018-08-06 11:55:54 +01001407 return mWindow.mCurrentContent;
Mihai Popa8b789102018-02-15 12:06:59 +00001408 }
Mihai Popa137b5842018-01-30 15:03:22 +00001409 }
1410
1411 /**
Mihai Popa1ddabb22018-08-06 11:55:54 +01001412 * Returns a bitmap containing the content that was magnified and drew to the
1413 * magnifier, at its original size, without the overlay applied.
1414 * @return the content that is magnified, as bitmap
Mihai Popabeeaf552018-07-19 15:50:43 +01001415 *
1416 * @hide
1417 */
1418 @TestApi
1419 public @Nullable Bitmap getOriginalContent() {
1420 if (mWindow == null) {
1421 return null;
1422 }
1423 synchronized (mWindow.mLock) {
1424 return Bitmap.createBitmap(mWindow.mBitmap);
1425 }
1426 }
1427
1428 /**
Mihai Popa137b5842018-01-30 15:03:22 +00001429 * @return the size of the magnifier window in dp
1430 *
1431 * @hide
1432 */
1433 @TestApi
1434 public static PointF getMagnifierDefaultSize() {
1435 final Resources resources = Resources.getSystem();
1436 final float density = resources.getDisplayMetrics().density;
1437 final PointF size = new PointF();
Mihai Popac6950292018-11-15 21:32:42 +00001438 size.x = resources.getDimension(R.dimen.default_magnifier_width) / density;
1439 size.y = resources.getDimension(R.dimen.default_magnifier_height) / density;
Mihai Popa137b5842018-01-30 15:03:22 +00001440 return size;
1441 }
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001442
1443 /**
1444 * @hide
1445 */
1446 @TestApi
1447 public interface Callback {
1448 /**
1449 * Callback called after the drawing for a magnifier update has happened.
1450 */
1451 void onOperationComplete();
1452 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01001453}