blob: 1719015d169dba66ea42397919a79f8c5bfabbbb [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;
65
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010066/**
Andrei Stingaceanud6644dc2017-11-21 14:53:38 +000067 * Android magnifier widget. Can be used by any view which is attached to a window.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010068 */
Andrei Stingaceanud6644dc2017-11-21 14:53:38 +000069@UiThread
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010070public final class Magnifier {
Mihai Popa3e1aed12018-08-03 18:25:52 +010071 private static final String TAG = "Magnifier";
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +000072 // Use this to specify that a previous configuration value does not exist.
73 private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
Mihai Popa8b789102018-02-15 12:06:59 +000074 // The callbacks of the pixel copy requests will be invoked on
75 // the Handler of this Thread when the copy is finished.
76 private static final HandlerThread sPixelCopyHandlerThread =
77 new HandlerThread("magnifier pixel copy result handler");
78
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +000079 // The view to which this magnifier is attached.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010080 private final View mView;
Mihai Popa1d1ed0c2018-01-12 12:38:12 +000081 // The coordinates of the view in the surface.
82 private final int[] mViewCoordinatesInSurface;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010083 // The window containing the magnifier.
Mihai Popa4bcd4d402018-02-07 17:13:51 +000084 private InternalPopupWindow mWindow;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010085 // The width of the window containing the magnifier.
86 private final int mWindowWidth;
87 // The height of the window containing the magnifier.
88 private final int mWindowHeight;
Mihai Popa469aba82018-07-18 14:52:26 +010089 // The zoom applied to the view region copied to the magnifier view.
Mihai Popabeeaf552018-07-19 15:50:43 +010090 private float mZoom;
Mihai Popa7433a072018-07-18 12:18:34 +010091 // The width of the content that will be copied to the magnifier.
Mihai Popabeeaf552018-07-19 15:50:43 +010092 private int mSourceWidth;
Mihai Popa7433a072018-07-18 12:18:34 +010093 // The height of the content that will be copied to the magnifier.
Mihai Popabeeaf552018-07-19 15:50:43 +010094 private int mSourceHeight;
Mihai Popa3e1aed12018-08-03 18:25:52 +010095 // Whether the zoom of the magnifier or the view position have changed since last content copy.
96 private boolean mDirtyState;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000097 // The elevation of the window containing the magnifier.
98 private final float mWindowElevation;
Mihai Popafb4b6b82018-03-01 16:08:14 +000099 // The corner radius of the window containing the magnifier.
100 private final float mWindowCornerRadius;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100101 // The overlay to be drawn on the top of the magnifier content.
102 private final Drawable mOverlay;
Mihai Popa469aba82018-07-18 14:52:26 +0100103 // The horizontal offset between the source and window coords when #show(float, float) is used.
104 private final int mDefaultHorizontalSourceToMagnifierOffset;
105 // The vertical offset between the source and window coords when #show(float, float) is used.
106 private final int mDefaultVerticalSourceToMagnifierOffset;
Mihai Popabecda342018-12-13 17:12:37 +0000107 // Whether the area where the magnifier can be positioned will be clipped to the main window
108 // and within system insets.
109 private final boolean mClippingEnabled;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100110 // The behavior of the left bound of the rectangle where the content can be copied from.
111 private @SourceBound int mLeftContentBound;
112 // The behavior of the top bound of the rectangle where the content can be copied from.
113 private @SourceBound int mTopContentBound;
114 // The behavior of the right bound of the rectangle where the content can be copied from.
115 private @SourceBound int mRightContentBound;
116 // The behavior of the bottom bound of the rectangle where the content can be copied from.
117 private @SourceBound int mBottomContentBound;
Mihai Popaf2980682018-04-30 19:08:57 +0100118 // The parent surface for the magnifier surface.
119 private SurfaceInfo mParentSurface;
120 // The surface where the content will be copied from.
121 private SurfaceInfo mContentCopySurface;
122 // The center coordinates of the window containing the magnifier.
123 private final Point mWindowCoords = new Point();
124 // The center coordinates of the content to be magnified,
Mihai Popaf2980682018-04-30 19:08:57 +0100125 // clamped inside the visible region of the magnified view.
126 private final Point mClampedCenterZoomCoords = new Point();
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000127 // Variables holding previous states, used for detecting redundant calls and invalidation.
128 private final Point mPrevStartCoordsInSurface = new Point(
129 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
Mihai Popa7433a072018-07-18 12:18:34 +0100130 private final PointF mPrevShowSourceCoords = new PointF(
131 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
132 private final PointF mPrevShowWindowCoords = new PointF(
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000133 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000134 // Rectangle defining the view surface area we pixel copy content from.
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000135 private final Rect mPixelCopyRequestRect = new Rect();
Mihai Popa39a71332018-02-22 19:30:24 +0000136 // Lock to synchronize between the UI thread and the thread that handles pixel copy results.
137 // Only sync mWindow writes from UI thread with mWindow reads from sPixelCopyHandlerThread.
138 private final Object mLock = new Object();
Bin Chenc8beb5c2019-05-10 16:05:05 +0800139 // The lock used to synchronize the UI and render threads when a #dismiss is performed.
140 private final Object mDestroyLock = new Object();
Andrei Stingaceanu15af5612017-10-13 12:53:23 +0100141
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100142 /**
143 * Initializes a magnifier.
144 *
145 * @param view the view for which this magnifier is attached
Mihai Popa469aba82018-07-18 14:52:26 +0100146 *
Mihai Popab6ca9092018-09-24 21:14:50 +0100147 * @deprecated Please use {@link Builder} instead
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100148 */
Mihai Popab6ca9092018-09-24 21:14:50 +0100149 @Deprecated
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100150 public Magnifier(@NonNull View view) {
Mihai Popac6950292018-11-15 21:32:42 +0000151 this(createBuilderWithOldMagnifierDefaults(view));
152 }
153
154 static Builder createBuilderWithOldMagnifierDefaults(final View view) {
155 final Builder params = new Builder(view);
156 final Context context = view.getContext();
157 final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Magnifier,
158 R.attr.magnifierStyle, 0);
159 params.mWidth = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierWidth, 0);
160 params.mHeight = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHeight, 0);
161 params.mElevation = a.getDimension(R.styleable.Magnifier_magnifierElevation, 0);
162 params.mCornerRadius = getDeviceDefaultDialogCornerRadius(context);
163 params.mZoom = a.getFloat(R.styleable.Magnifier_magnifierZoom, 0);
164 params.mHorizontalDefaultSourceToMagnifierOffset =
165 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHorizontalOffset, 0);
166 params.mVerticalDefaultSourceToMagnifierOffset =
167 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierVerticalOffset, 0);
168 params.mOverlay = new ColorDrawable(a.getColor(
169 R.styleable.Magnifier_magnifierColorOverlay, Color.TRANSPARENT));
170 a.recycle();
Mihai Popabecda342018-12-13 17:12:37 +0000171 params.mClippingEnabled = true;
Mihai Popac6950292018-11-15 21:32:42 +0000172 params.mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
173 params.mTopContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
174 params.mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
175 params.mBottomContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
176 return params;
177 }
178
179 /**
180 * Returns the device default theme dialog corner radius attribute.
181 * We retrieve this from the device default theme to avoid
182 * using the values set in the custom application themes.
183 */
184 private static float getDeviceDefaultDialogCornerRadius(final Context context) {
185 final Context deviceDefaultContext =
186 new ContextThemeWrapper(context, R.style.Theme_DeviceDefault);
187 final TypedArray ta = deviceDefaultContext.obtainStyledAttributes(
188 new int[]{android.R.attr.dialogCornerRadius});
189 final float dialogCornerRadius = ta.getDimension(0, 0);
190 ta.recycle();
191 return dialogCornerRadius;
Mihai Popa469aba82018-07-18 14:52:26 +0100192 }
193
194 private Magnifier(@NonNull Builder params) {
195 // Copy params from builder.
196 mView = params.mView;
197 mWindowWidth = params.mWidth;
198 mWindowHeight = params.mHeight;
199 mZoom = params.mZoom;
Mihai Popa7433a072018-07-18 12:18:34 +0100200 mSourceWidth = Math.round(mWindowWidth / mZoom);
201 mSourceHeight = Math.round(mWindowHeight / mZoom);
Mihai Popa469aba82018-07-18 14:52:26 +0100202 mWindowElevation = params.mElevation;
203 mWindowCornerRadius = params.mCornerRadius;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100204 mOverlay = params.mOverlay;
Mihai Popa469aba82018-07-18 14:52:26 +0100205 mDefaultHorizontalSourceToMagnifierOffset =
206 params.mHorizontalDefaultSourceToMagnifierOffset;
207 mDefaultVerticalSourceToMagnifierOffset =
208 params.mVerticalDefaultSourceToMagnifierOffset;
Mihai Popabecda342018-12-13 17:12:37 +0000209 mClippingEnabled = params.mClippingEnabled;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100210 mLeftContentBound = params.mLeftContentBound;
211 mTopContentBound = params.mTopContentBound;
212 mRightContentBound = params.mRightContentBound;
213 mBottomContentBound = params.mBottomContentBound;
Mihai Popa1d1ed0c2018-01-12 12:38:12 +0000214 // The view's surface coordinates will not be updated until the magnifier is first shown.
215 mViewCoordinatesInSurface = new int[2];
Mihai Popa8b789102018-02-15 12:06:59 +0000216 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100217
Mihai Popa8b789102018-02-15 12:06:59 +0000218 static {
219 sPixelCopyHandlerThread.start();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100220 }
221
222 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100223 * Shows the magnifier on the screen. The method takes the coordinates of the center
224 * of the content source going to be magnified and copied to the magnifier. The coordinates
225 * are relative to the top left corner of the magnified view. The magnifier will be
226 * positioned such that its center will be at the default offset from the center of the source.
227 * The default offset can be specified using the method
228 * {@link Builder#setDefaultSourceToMagnifierOffset(int, int)}. If the offset should
229 * be different across calls to this method, you should consider to use method
230 * {@link #show(float, float, float, float)} instead.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100231 *
Mihai Popac2e0bee2018-07-19 12:18:30 +0100232 * @param sourceCenterX horizontal coordinate of the source center, relative to the view
233 * @param sourceCenterY vertical coordinate of the source center, relative to the view
234 *
235 * @see Builder#setDefaultSourceToMagnifierOffset(int, int)
236 * @see Builder#getDefaultHorizontalSourceToMagnifierOffset()
237 * @see Builder#getDefaultVerticalSourceToMagnifierOffset()
238 * @see #show(float, float, float, float)
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100239 */
Mihai Popa7433a072018-07-18 12:18:34 +0100240 public void show(@FloatRange(from = 0) float sourceCenterX,
241 @FloatRange(from = 0) float sourceCenterY) {
Mihai Popa469aba82018-07-18 14:52:26 +0100242 show(sourceCenterX, sourceCenterY,
243 sourceCenterX + mDefaultHorizontalSourceToMagnifierOffset,
244 sourceCenterY + mDefaultVerticalSourceToMagnifierOffset);
Mihai Popa7433a072018-07-18 12:18:34 +0100245 }
246
247 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100248 * Shows the magnifier on the screen at a position that is independent from its content
249 * position. The first two arguments represent the coordinates of the center of the
250 * content source going to be magnified and copied to the magnifier. The last two arguments
251 * represent the coordinates of the center of the magnifier itself. All four coordinates
252 * are relative to the top left corner of the magnified view. If you consider using this
253 * method such that the offset between the source center and the magnifier center coordinates
254 * remains constant, you should consider using method {@link #show(float, float)} instead.
Mihai Popa7433a072018-07-18 12:18:34 +0100255 *
Mihai Popac2e0bee2018-07-19 12:18:30 +0100256 * @param sourceCenterX horizontal coordinate of the source center relative to the view
257 * @param sourceCenterY vertical coordinate of the source center, relative to the view
258 * @param magnifierCenterX horizontal coordinate of the magnifier center, relative to the view
259 * @param magnifierCenterY vertical coordinate of the magnifier center, relative to the view
Mihai Popa7433a072018-07-18 12:18:34 +0100260 */
261 public void show(@FloatRange(from = 0) float sourceCenterX,
262 @FloatRange(from = 0) float sourceCenterY,
263 float magnifierCenterX, float magnifierCenterY) {
Andrei Stingaceanu451f9472017-10-13 16:41:28 +0100264
Mihai Popaf2980682018-04-30 19:08:57 +0100265 obtainSurfaces();
Mihai Popa7433a072018-07-18 12:18:34 +0100266 obtainContentCoordinates(sourceCenterX, sourceCenterY);
267 obtainWindowCoordinates(magnifierCenterX, magnifierCenterY);
Andrei Stingaceanuca189fe2017-10-19 17:02:22 +0100268
Mihai Popa7433a072018-07-18 12:18:34 +0100269 final int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2;
270 final int startY = mClampedCenterZoomCoords.y - mSourceHeight / 2;
Mihai Popabeeaf552018-07-19 15:50:43 +0100271 if (sourceCenterX != mPrevShowSourceCoords.x || sourceCenterY != mPrevShowSourceCoords.y
Mihai Popa3e1aed12018-08-03 18:25:52 +0100272 || mDirtyState) {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000273 if (mWindow == null) {
Mihai Popa39a71332018-02-22 19:30:24 +0000274 synchronized (mLock) {
275 mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(),
Robert Carr5fea55b2018-12-10 13:05:52 -0800276 mParentSurface.mSurfaceControl, mWindowWidth, mWindowHeight,
Mihai Popa1ddabb22018-08-06 11:55:54 +0100277 mWindowElevation, mWindowCornerRadius,
278 mOverlay != null ? mOverlay : new ColorDrawable(Color.TRANSPARENT),
Mihai Popa39a71332018-02-22 19:30:24 +0000279 Handler.getMain() /* draw the magnifier on the UI thread */, mLock,
Bin Chenc8beb5c2019-05-10 16:05:05 +0800280 mDestroyLock, mCallback);
Mihai Popa39a71332018-02-22 19:30:24 +0000281 }
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000282 }
283 performPixelCopy(startX, startY, true /* update window position */);
Mihai Popa7433a072018-07-18 12:18:34 +0100284 } else if (magnifierCenterX != mPrevShowWindowCoords.x
285 || magnifierCenterY != mPrevShowWindowCoords.y) {
286 final Point windowCoords = getCurrentClampedWindowCoordinates();
287 final InternalPopupWindow currentWindowInstance = mWindow;
288 sPixelCopyHandlerThread.getThreadHandler().post(() -> {
Mihai Popa7433a072018-07-18 12:18:34 +0100289 synchronized (mLock) {
Mihai Popaddcd54812018-09-03 17:25:54 +0100290 if (mWindow != currentWindowInstance) {
291 // The magnifier was dismissed (and maybe shown again) in the meantime.
292 return;
293 }
Mihai Popa7433a072018-07-18 12:18:34 +0100294 mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
295 }
296 });
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100297 }
Mihai Popa7433a072018-07-18 12:18:34 +0100298 mPrevShowSourceCoords.x = sourceCenterX;
299 mPrevShowSourceCoords.y = sourceCenterY;
300 mPrevShowWindowCoords.x = magnifierCenterX;
301 mPrevShowWindowCoords.y = magnifierCenterY;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100302 }
303
304 /**
Andrei Stingaceanud6644dc2017-11-21 14:53:38 +0000305 * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100306 */
307 public void dismiss() {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000308 if (mWindow != null) {
Bin Chenc8beb5c2019-05-10 16:05:05 +0800309 synchronized (mDestroyLock) {
310 synchronized (mLock) {
311 mWindow.destroy();
312 mWindow = null;
313 }
Mihai Popa39a71332018-02-22 19:30:24 +0000314 }
Mihai Popa7433a072018-07-18 12:18:34 +0100315 mPrevShowSourceCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
316 mPrevShowSourceCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
317 mPrevShowWindowCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
318 mPrevShowWindowCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
Mihai Popa953b1342018-03-21 18:05:13 +0000319 mPrevStartCoordsInSurface.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
320 mPrevStartCoordsInSurface.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000321 }
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000322 }
Andrei Stingaceanu15af5612017-10-13 12:53:23 +0100323
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000324 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100325 * Asks the magnifier to update its content. It uses the previous coordinates passed to
326 * {@link #show(float, float)} or {@link #show(float, float, float, float)}. The
327 * method only has effect if the magnifier is currently showing.
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000328 */
329 public void update() {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000330 if (mWindow != null) {
Mihai Popaf2980682018-04-30 19:08:57 +0100331 obtainSurfaces();
Mihai Popa3e1aed12018-08-03 18:25:52 +0100332 if (!mDirtyState) {
Mihai Popabeeaf552018-07-19 15:50:43 +0100333 // Update the content shown in the magnifier.
334 performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y,
335 false /* update window position */);
336 } else {
Mihai Popa3e1aed12018-08-03 18:25:52 +0100337 // If for example the zoom has changed, we cannot use the same top left
338 // coordinates as before, so just #show again to have them recomputed.
Mihai Popabeeaf552018-07-19 15:50:43 +0100339 show(mPrevShowSourceCoords.x, mPrevShowSourceCoords.y,
340 mPrevShowWindowCoords.x, mPrevShowWindowCoords.y);
341 }
Andrei Stingaceanu15af5612017-10-13 12:53:23 +0100342 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100343 }
344
Mihai Popa17ea3052018-03-06 14:24:07 +0000345 /**
Mihai Popa469aba82018-07-18 14:52:26 +0100346 * @return the width of the magnifier window, in pixels
Mihai Popac2e0bee2018-07-19 12:18:30 +0100347 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa17ea3052018-03-06 14:24:07 +0000348 */
Mihai Popac2e0bee2018-07-19 12:18:30 +0100349 @Px
Mihai Popa17ea3052018-03-06 14:24:07 +0000350 public int getWidth() {
351 return mWindowWidth;
352 }
353
354 /**
Mihai Popa469aba82018-07-18 14:52:26 +0100355 * @return the height of the magnifier window, in pixels
Mihai Popac2e0bee2018-07-19 12:18:30 +0100356 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa17ea3052018-03-06 14:24:07 +0000357 */
Mihai Popac2e0bee2018-07-19 12:18:30 +0100358 @Px
Mihai Popa17ea3052018-03-06 14:24:07 +0000359 public int getHeight() {
360 return mWindowHeight;
361 }
362
363 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100364 * @return the initial width of the content magnified and copied to the magnifier, in pixels
365 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa27cf08f2019-01-10 19:59:29 +0000366 * @see Magnifier.Builder#setInitialZoom(float)
Mihai Popac2e0bee2018-07-19 12:18:30 +0100367 */
368 @Px
369 public int getSourceWidth() {
370 return mSourceWidth;
371 }
372
373 /**
374 * @return the initial height of the content magnified and copied to the magnifier, in pixels
375 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa27cf08f2019-01-10 19:59:29 +0000376 * @see Magnifier.Builder#setInitialZoom(float)
Mihai Popac2e0bee2018-07-19 12:18:30 +0100377 */
378 @Px
379 public int getSourceHeight() {
380 return mSourceHeight;
381 }
382
383 /**
Mihai Popabeeaf552018-07-19 15:50:43 +0100384 * Sets the zoom to be applied to the chosen content before being copied to the magnifier popup.
Mihai Popa3e1aed12018-08-03 18:25:52 +0100385 * The change will become effective at the next #show or #update call.
Mihai Popabeeaf552018-07-19 15:50:43 +0100386 * @param zoom the zoom to be set
387 */
388 public void setZoom(@FloatRange(from = 0f) float zoom) {
389 Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
390 mZoom = zoom;
391 mSourceWidth = Math.round(mWindowWidth / mZoom);
392 mSourceHeight = Math.round(mWindowHeight / mZoom);
Mihai Popa3e1aed12018-08-03 18:25:52 +0100393 mDirtyState = true;
Mihai Popabeeaf552018-07-19 15:50:43 +0100394 }
395
396 /**
Mihai Popa469aba82018-07-18 14:52:26 +0100397 * Returns the zoom to be applied to the magnified view region copied to the magnifier.
Mihai Popa17ea3052018-03-06 14:24:07 +0000398 * If the zoom is x and the magnifier window size is (width, height), the original size
Mihai Popa469aba82018-07-18 14:52:26 +0100399 * of the content being magnified will be (width / x, height / x).
400 * @return the zoom applied to the content
Mihai Popa27cf08f2019-01-10 19:59:29 +0000401 * @see Magnifier.Builder#setInitialZoom(float)
Mihai Popa17ea3052018-03-06 14:24:07 +0000402 */
403 public float getZoom() {
404 return mZoom;
405 }
406
Mihai Popa63ee7f12018-04-05 12:01:53 +0100407 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100408 * @return the elevation set for the magnifier window, in pixels
409 * @see Magnifier.Builder#setElevation(float)
410 */
411 @Px
412 public float getElevation() {
413 return mWindowElevation;
414 }
415
416 /**
417 * @return the corner radius of the magnifier window, in pixels
418 * @see Magnifier.Builder#setCornerRadius(float)
419 */
420 @Px
421 public float getCornerRadius() {
422 return mWindowCornerRadius;
423 }
424
425 /**
426 * Returns the horizontal offset, in pixels, to be applied to the source center position
427 * to obtain the magnifier center position when {@link #show(float, float)} is called.
428 * The value is ignored when {@link #show(float, float, float, float)} is used instead.
Mihai Popa0450a162018-04-27 13:09:12 +0100429 *
Mihai Popac2e0bee2018-07-19 12:18:30 +0100430 * @return the default horizontal offset between the source center and the magnifier
431 * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
432 * @see Magnifier#show(float, float)
433 */
434 @Px
435 public int getDefaultHorizontalSourceToMagnifierOffset() {
436 return mDefaultHorizontalSourceToMagnifierOffset;
437 }
438
439 /**
440 * Returns the vertical offset, in pixels, to be applied to the source center position
441 * to obtain the magnifier center position when {@link #show(float, float)} is called.
442 * The value is ignored when {@link #show(float, float, float, float)} is used instead.
443 *
444 * @return the default vertical offset between the source center and the magnifier
445 * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
446 * @see Magnifier#show(float, float)
447 */
448 @Px
449 public int getDefaultVerticalSourceToMagnifierOffset() {
450 return mDefaultVerticalSourceToMagnifierOffset;
451 }
452
453 /**
Mihai Popabecda342018-12-13 17:12:37 +0000454 * Returns the overlay to be drawn on the top of the magnifier, or
Mihai Popa1ddabb22018-08-06 11:55:54 +0100455 * {@code null} if no overlay should be drawn.
456 * @return the overlay
457 * @see Magnifier.Builder#setOverlay(Drawable)
458 */
459 @Nullable
460 public Drawable getOverlay() {
461 return mOverlay;
462 }
463
464 /**
Mihai Popa1903cab2018-08-01 14:33:12 +0100465 * Returns whether the magnifier position will be adjusted such that the magnifier will be
Mihai Popabecda342018-12-13 17:12:37 +0000466 * fully within the bounds of the main application window, by also avoiding any overlap
467 * with system insets (such as the one corresponding to the status bar) i.e. whether the
468 * area where the magnifier can be positioned will be clipped to the main application window
469 * and the system insets.
Mihai Popa1903cab2018-08-01 14:33:12 +0100470 * @return whether the magnifier position will be adjusted
Mihai Popabecda342018-12-13 17:12:37 +0000471 * @see Magnifier.Builder#setClippingEnabled(boolean)
Mihai Popa1903cab2018-08-01 14:33:12 +0100472 */
Mihai Popabecda342018-12-13 17:12:37 +0000473 public boolean isClippingEnabled() {
474 return mClippingEnabled;
Mihai Popa1903cab2018-08-01 14:33:12 +0100475 }
476
477 /**
Mihai Popaef456972019-02-18 15:17:09 +0000478 * Returns the top left coordinates of the magnifier, relative to the main application
479 * window. They will be determined by the coordinates of the last {@link #show(float, float)}
480 * or {@link #show(float, float, float, float)} call, adjusted to take into account any
481 * potential clamping behavior. The method can be used immediately after a #show
482 * call to find out where the magnifier will be positioned. However, the position of the
483 * magnifier will not be updated visually in the same frame, due to the async nature of
484 * the content copying and of the magnifier rendering.
Mihai Popac2e0bee2018-07-19 12:18:30 +0100485 * The method will return {@code null} if #show has not yet been called, or if the last
486 * operation performed was a #dismiss.
487 *
488 * @return the top left coordinates of the magnifier
Mihai Popa63ee7f12018-04-05 12:01:53 +0100489 */
490 @Nullable
Mihai Popac2e0bee2018-07-19 12:18:30 +0100491 public Point getPosition() {
Mihai Popa63ee7f12018-04-05 12:01:53 +0100492 if (mWindow == null) {
493 return null;
494 }
Mihai Popaef456972019-02-18 15:17:09 +0000495 final Point position = getCurrentClampedWindowCoordinates();
496 position.offset(-mParentSurface.mInsets.left, -mParentSurface.mInsets.top);
497 return new Point(position);
Mihai Popac2e0bee2018-07-19 12:18:30 +0100498 }
499
500 /**
501 * Returns the top left coordinates of the magnifier source (i.e. the view region going to
Mihai Popaef456972019-02-18 15:17:09 +0000502 * be magnified and copied to the magnifier), relative to the window or surface the content
503 * is copied from. The content will be copied:
Mihai Popac2e0bee2018-07-19 12:18:30 +0100504 * - if the magnified view is a {@link SurfaceView}, from the surface backing it
Mihai Popaef456972019-02-18 15:17:09 +0000505 * - otherwise, from the surface backing the main application window, and the coordinates
506 * returned will be relative to the main application window
Mihai Popac2e0bee2018-07-19 12:18:30 +0100507 * The method will return {@code null} if #show has not yet been called, or if the last
508 * operation performed was a #dismiss.
509 *
510 * @return the top left coordinates of the magnifier source
511 */
512 @Nullable
513 public Point getSourcePosition() {
514 if (mWindow == null) {
515 return null;
516 }
Mihai Popaef456972019-02-18 15:17:09 +0000517 final Point position = new Point(mPixelCopyRequestRect.left, mPixelCopyRequestRect.top);
518 position.offset(-mContentCopySurface.mInsets.left, -mContentCopySurface.mInsets.top);
519 return new Point(position);
Mihai Popa63ee7f12018-04-05 12:01:53 +0100520 }
521
Mihai Popaf2980682018-04-30 19:08:57 +0100522 /**
523 * Retrieves the surfaces used by the magnifier:
524 * - a parent surface for the magnifier surface. This will usually be the main app window.
525 * - a surface where the magnified content will be copied from. This will be the main app
526 * window unless the magnified view is a SurfaceView, in which case its backing surface
527 * will be used.
528 */
529 private void obtainSurfaces() {
530 // Get the main window surface.
531 SurfaceInfo validMainWindowSurface = SurfaceInfo.NULL;
Mihai Popa819e90d2018-04-16 14:27:05 +0100532 if (mView.getViewRootImpl() != null) {
Mihai Popaf2980682018-04-30 19:08:57 +0100533 final ViewRootImpl viewRootImpl = mView.getViewRootImpl();
534 final Surface mainWindowSurface = viewRootImpl.mSurface;
Mihai Popa819e90d2018-04-16 14:27:05 +0100535 if (mainWindowSurface != null && mainWindowSurface.isValid()) {
Mihai Popaf2980682018-04-30 19:08:57 +0100536 final Rect surfaceInsets = viewRootImpl.mWindowAttributes.surfaceInsets;
537 final int surfaceWidth =
538 viewRootImpl.getWidth() + surfaceInsets.left + surfaceInsets.right;
539 final int surfaceHeight =
540 viewRootImpl.getHeight() + surfaceInsets.top + surfaceInsets.bottom;
541 validMainWindowSurface =
Robert Carr5fea55b2018-12-10 13:05:52 -0800542 new SurfaceInfo(viewRootImpl.getSurfaceControl(), mainWindowSurface,
Mihai Popaef456972019-02-18 15:17:09 +0000543 surfaceWidth, surfaceHeight, surfaceInsets, true);
Mihai Popa819e90d2018-04-16 14:27:05 +0100544 }
Mihai Popa17ea3052018-03-06 14:24:07 +0000545 }
Mihai Popaf2980682018-04-30 19:08:57 +0100546 // Get the surface backing the magnified view, if it is a SurfaceView.
547 SurfaceInfo validSurfaceViewSurface = SurfaceInfo.NULL;
Mihai Popa819e90d2018-04-16 14:27:05 +0100548 if (mView instanceof SurfaceView) {
Robert Carr5fea55b2018-12-10 13:05:52 -0800549 final SurfaceControl sc = ((SurfaceView) mView).getSurfaceControl();
Mihai Popaf2980682018-04-30 19:08:57 +0100550 final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder();
551 final Surface surfaceViewSurface = surfaceHolder.getSurface();
Robert Carr5fea55b2018-12-10 13:05:52 -0800552
553 if (sc != null && sc.isValid()) {
Mihai Popaf2980682018-04-30 19:08:57 +0100554 final Rect surfaceFrame = surfaceHolder.getSurfaceFrame();
Robert Carr5fea55b2018-12-10 13:05:52 -0800555 validSurfaceViewSurface = new SurfaceInfo(sc, surfaceViewSurface,
Mihai Popaef456972019-02-18 15:17:09 +0000556 surfaceFrame.right, surfaceFrame.bottom, new Rect(), false);
Mihai Popa819e90d2018-04-16 14:27:05 +0100557 }
558 }
Mihai Popaf2980682018-04-30 19:08:57 +0100559
560 // Choose the parent surface for the magnifier and the source surface for the content.
561 mParentSurface = validMainWindowSurface != SurfaceInfo.NULL
562 ? validMainWindowSurface : validSurfaceViewSurface;
563 mContentCopySurface = mView instanceof SurfaceView
564 ? validSurfaceViewSurface : validMainWindowSurface;
Mihai Popa17ea3052018-03-06 14:24:07 +0000565 }
566
Mihai Popaf2980682018-04-30 19:08:57 +0100567 /**
568 * Computes the coordinates of the center of the content going to be displayed in the
569 * magnifier. These are relative to the surface the content is copied from.
570 */
571 private void obtainContentCoordinates(final float xPosInView, final float yPosInView) {
Mihai Popa3e1aed12018-08-03 18:25:52 +0100572 final int prevViewXInSurface = mViewCoordinatesInSurface[0];
573 final int prevViewYInSurface = mViewCoordinatesInSurface[1];
Mihai Popa819e90d2018-04-16 14:27:05 +0100574 mView.getLocationInSurface(mViewCoordinatesInSurface);
Mihai Popa3e1aed12018-08-03 18:25:52 +0100575 if (mViewCoordinatesInSurface[0] != prevViewXInSurface
576 || mViewCoordinatesInSurface[1] != prevViewYInSurface) {
577 mDirtyState = true;
578 }
579
Mihai Popa7433a072018-07-18 12:18:34 +0100580 final int zoomCenterX;
581 final int zoomCenterY;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000582 if (mView instanceof SurfaceView) {
583 // No offset required if the backing Surface matches the size of the SurfaceView.
Mihai Popa7433a072018-07-18 12:18:34 +0100584 zoomCenterX = Math.round(xPosInView);
585 zoomCenterY = Math.round(yPosInView);
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000586 } else {
Mihai Popa7433a072018-07-18 12:18:34 +0100587 zoomCenterX = Math.round(xPosInView + mViewCoordinatesInSurface[0]);
588 zoomCenterY = Math.round(yPosInView + mViewCoordinatesInSurface[1]);
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000589 }
590
Mihai Popa520e44752019-01-29 21:26:26 +0000591 final Rect[] bounds = new Rect[2]; // [MAX_IN_SURFACE, MAX_VISIBLE]
Mihai Popa3e1aed12018-08-03 18:25:52 +0100592 // Obtain the surface bounds rectangle.
593 final Rect surfaceBounds = new Rect(0, 0,
594 mContentCopySurface.mWidth, mContentCopySurface.mHeight);
595 bounds[0] = surfaceBounds;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100596 // Obtain the visible view region rectangle.
Mihai Popaf2980682018-04-30 19:08:57 +0100597 final Rect viewVisibleRegion = new Rect();
598 mView.getGlobalVisibleRect(viewVisibleRegion);
599 if (mView.getViewRootImpl() != null) {
600 // Clamping coordinates relative to the surface, not to the window.
601 final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets;
602 viewVisibleRegion.offset(surfaceInsets.left, surfaceInsets.top);
603 }
604 if (mView instanceof SurfaceView) {
605 // If we copy content from a SurfaceView, clamp coordinates relative to it.
606 viewVisibleRegion.offset(-mViewCoordinatesInSurface[0], -mViewCoordinatesInSurface[1]);
607 }
Mihai Popa520e44752019-01-29 21:26:26 +0000608 bounds[1] = viewVisibleRegion;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100609
610 // Aggregate the above to obtain the bounds where the content copy will be restricted.
611 int resolvedLeft = Integer.MIN_VALUE;
612 for (int i = mLeftContentBound; i >= 0; --i) {
613 resolvedLeft = Math.max(resolvedLeft, bounds[i].left);
614 }
615 int resolvedTop = Integer.MIN_VALUE;
616 for (int i = mTopContentBound; i >= 0; --i) {
617 resolvedTop = Math.max(resolvedTop, bounds[i].top);
618 }
619 int resolvedRight = Integer.MAX_VALUE;
620 for (int i = mRightContentBound; i >= 0; --i) {
621 resolvedRight = Math.min(resolvedRight, bounds[i].right);
622 }
623 int resolvedBottom = Integer.MAX_VALUE;
624 for (int i = mBottomContentBound; i >= 0; --i) {
625 resolvedBottom = Math.min(resolvedBottom, bounds[i].bottom);
626 }
627 // Adjust <left-right> and <top-bottom> pairs of bounds to make sense.
628 resolvedLeft = Math.min(resolvedLeft, mContentCopySurface.mWidth - mSourceWidth);
629 resolvedTop = Math.min(resolvedTop, mContentCopySurface.mHeight - mSourceHeight);
630 if (resolvedLeft < 0 || resolvedTop < 0) {
631 Log.e(TAG, "Magnifier's content is copied from a surface smaller than"
Mihai Popa30b1fe32019-05-07 22:29:52 +0100632 + "the content requested size. The magnifier will be dismissed.");
Mihai Popa3e1aed12018-08-03 18:25:52 +0100633 }
634 resolvedRight = Math.max(resolvedRight, resolvedLeft + mSourceWidth);
635 resolvedBottom = Math.max(resolvedBottom, resolvedTop + mSourceHeight);
636
637 // Finally compute the coordinates of the source center.
638 mClampedCenterZoomCoords.x = Math.max(resolvedLeft + mSourceWidth / 2, Math.min(
639 zoomCenterX, resolvedRight - mSourceWidth / 2));
640 mClampedCenterZoomCoords.y = Math.max(resolvedTop + mSourceHeight / 2, Math.min(
641 zoomCenterY, resolvedBottom - mSourceHeight / 2));
Mihai Popaf2980682018-04-30 19:08:57 +0100642 }
643
Mihai Popa7433a072018-07-18 12:18:34 +0100644 /**
645 * Computes the coordinates of the top left corner of the magnifier window.
646 * These are relative to the surface the magnifier window is attached to.
647 */
648 private void obtainWindowCoordinates(final float xWindowPos, final float yWindowPos) {
649 final int windowCenterX;
650 final int windowCenterY;
651 if (mView instanceof SurfaceView) {
652 // No offset required if the backing Surface matches the size of the SurfaceView.
653 windowCenterX = Math.round(xWindowPos);
654 windowCenterY = Math.round(yWindowPos);
655 } else {
656 windowCenterX = Math.round(xWindowPos + mViewCoordinatesInSurface[0]);
657 windowCenterY = Math.round(yWindowPos + mViewCoordinatesInSurface[1]);
658 }
659
660 mWindowCoords.x = windowCenterX - mWindowWidth / 2;
661 mWindowCoords.y = windowCenterY - mWindowHeight / 2;
Mihai Popaf2980682018-04-30 19:08:57 +0100662 if (mParentSurface != mContentCopySurface) {
663 mWindowCoords.x += mViewCoordinatesInSurface[0];
664 mWindowCoords.y += mViewCoordinatesInSurface[1];
Mihai Popa819e90d2018-04-16 14:27:05 +0100665 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100666 }
667
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000668 private void performPixelCopy(final int startXInSurface, final int startYInSurface,
669 final boolean updateWindowPosition) {
Mihai Popaf2980682018-04-30 19:08:57 +0100670 if (mContentCopySurface.mSurface == null || !mContentCopySurface.mSurface.isValid()) {
Mihai Popa30b1fe32019-05-07 22:29:52 +0100671 onPixelCopyFailed();
Mihai Popa3589c2c2018-01-25 19:26:30 +0000672 return;
673 }
Mihai Popa3e1aed12018-08-03 18:25:52 +0100674
Mihai Popa953b1342018-03-21 18:05:13 +0000675 // Clamp window coordinates inside the parent surface, to avoid displaying
676 // the magnifier out of screen or overlapping with system insets.
Mihai Popa7433a072018-07-18 12:18:34 +0100677 final Point windowCoords = getCurrentClampedWindowCoordinates();
Mihai Popa3589c2c2018-01-25 19:26:30 +0000678
679 // Perform the pixel copy.
Mihai Popa3e1aed12018-08-03 18:25:52 +0100680 mPixelCopyRequestRect.set(startXInSurface,
681 startYInSurface,
682 startXInSurface + mSourceWidth,
683 startYInSurface + mSourceHeight);
Mihai Popa39a71332018-02-22 19:30:24 +0000684 final InternalPopupWindow currentWindowInstance = mWindow;
Mihai Popa8b789102018-02-15 12:06:59 +0000685 final Bitmap bitmap =
Mihai Popa7433a072018-07-18 12:18:34 +0100686 Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ARGB_8888);
Mihai Popaf2980682018-04-30 19:08:57 +0100687 PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap,
Mihai Popa3589c2c2018-01-25 19:26:30 +0000688 result -> {
Mihai Popa30b1fe32019-05-07 22:29:52 +0100689 if (result != PixelCopy.SUCCESS) {
690 onPixelCopyFailed();
691 return;
692 }
Mihai Popa39a71332018-02-22 19:30:24 +0000693 synchronized (mLock) {
694 if (mWindow != currentWindowInstance) {
695 // The magnifier was dismissed (and maybe shown again) in the meantime.
696 return;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000697 }
Mihai Popa39a71332018-02-22 19:30:24 +0000698 if (updateWindowPosition) {
699 // TODO: pull the position update outside #performPixelCopy
Mihai Popa7433a072018-07-18 12:18:34 +0100700 mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
Mihai Popa39a71332018-02-22 19:30:24 +0000701 }
702 mWindow.updateContent(bitmap);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000703 }
Mihai Popa3589c2c2018-01-25 19:26:30 +0000704 },
Mihai Popa8b789102018-02-15 12:06:59 +0000705 sPixelCopyHandlerThread.getThreadHandler());
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000706 mPrevStartCoordsInSurface.x = startXInSurface;
707 mPrevStartCoordsInSurface.y = startYInSurface;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100708 mDirtyState = false;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000709 }
710
Mihai Popa30b1fe32019-05-07 22:29:52 +0100711 private void onPixelCopyFailed() {
712 Log.e(TAG, "Magnifier failed to copy content from the view Surface. It will be dismissed.");
713 // Post to make sure #dismiss is done on the main thread.
714 Handler.getMain().postAtFrontOfQueue(() -> {
715 dismiss();
716 if (mCallback != null) {
717 mCallback.onOperationComplete();
718 }
719 });
720 }
721
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000722 /**
Mihai Popa7433a072018-07-18 12:18:34 +0100723 * Clamp window coordinates inside the surface the magnifier is attached to, to avoid
724 * displaying the magnifier out of screen or overlapping with system insets.
725 * @return the current window coordinates, after they are clamped inside the parent surface
726 */
727 private Point getCurrentClampedWindowCoordinates() {
Mihai Popabecda342018-12-13 17:12:37 +0000728 if (!mClippingEnabled) {
Mihai Popa1903cab2018-08-01 14:33:12 +0100729 // No position adjustment should be done, so return the raw coordinates.
730 return new Point(mWindowCoords);
731 }
732
Mihai Popa7433a072018-07-18 12:18:34 +0100733 final Rect windowBounds;
734 if (mParentSurface.mIsMainWindowSurface) {
Adrian Roos60f59292018-08-24 16:29:06 +0200735 final Insets systemInsets = mView.getRootWindowInsets().getSystemWindowInsets();
Mihai Popaef456972019-02-18 15:17:09 +0000736 windowBounds = new Rect(
737 systemInsets.left + mParentSurface.mInsets.left,
738 systemInsets.top + mParentSurface.mInsets.top,
739 mParentSurface.mWidth - systemInsets.right - mParentSurface.mInsets.right,
740 mParentSurface.mHeight - systemInsets.bottom
741 - mParentSurface.mInsets.bottom
742 );
Mihai Popa7433a072018-07-18 12:18:34 +0100743 } else {
744 windowBounds = new Rect(0, 0, mParentSurface.mWidth, mParentSurface.mHeight);
745 }
746 final int windowCoordsX = Math.max(windowBounds.left,
747 Math.min(windowBounds.right - mWindowWidth, mWindowCoords.x));
748 final int windowCoordsY = Math.max(windowBounds.top,
749 Math.min(windowBounds.bottom - mWindowHeight, mWindowCoords.y));
750 return new Point(windowCoordsX, windowCoordsY);
751 }
752
753 /**
Mihai Popaf2980682018-04-30 19:08:57 +0100754 * Contains a surface and metadata corresponding to it.
755 */
756 private static class SurfaceInfo {
Mihai Popaef456972019-02-18 15:17:09 +0000757 public static final SurfaceInfo NULL = new SurfaceInfo(null, null, 0, 0, null, false);
Mihai Popaf2980682018-04-30 19:08:57 +0100758
759 private Surface mSurface;
Robert Carr5fea55b2018-12-10 13:05:52 -0800760 private SurfaceControl mSurfaceControl;
Mihai Popaf2980682018-04-30 19:08:57 +0100761 private int mWidth;
762 private int mHeight;
Mihai Popaef456972019-02-18 15:17:09 +0000763 private Rect mInsets;
Mihai Popaf2980682018-04-30 19:08:57 +0100764 private boolean mIsMainWindowSurface;
765
Robert Carr5fea55b2018-12-10 13:05:52 -0800766 SurfaceInfo(final SurfaceControl surfaceControl, final Surface surface,
Mihai Popaef456972019-02-18 15:17:09 +0000767 final int width, final int height, final Rect insets,
Mihai Popaf2980682018-04-30 19:08:57 +0100768 final boolean isMainWindowSurface) {
Robert Carr5fea55b2018-12-10 13:05:52 -0800769 mSurfaceControl = surfaceControl;
Mihai Popaf2980682018-04-30 19:08:57 +0100770 mSurface = surface;
771 mWidth = width;
772 mHeight = height;
Mihai Popaef456972019-02-18 15:17:09 +0000773 mInsets = insets;
Mihai Popaf2980682018-04-30 19:08:57 +0100774 mIsMainWindowSurface = isMainWindowSurface;
775 }
776 }
777
778 /**
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000779 * Magnifier's own implementation of PopupWindow-similar floating window.
780 * This exists to ensure frame-synchronization between window position updates and window
781 * content updates. By using a PopupWindow, these events would happen in different frames,
782 * producing a shakiness effect for the magnifier content.
783 */
784 private static class InternalPopupWindow {
Mihai Popa819e90d2018-04-16 14:27:05 +0100785 // The z of the magnifier surface, defining its z order in the list of
786 // siblings having the same parent surface (usually the main app surface).
787 private static final int SURFACE_Z = 5;
Mihai Popad870b882018-02-27 14:25:52 +0000788
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000789 // Display associated to the view the magnifier is attached to.
790 private final Display mDisplay;
791 // The size of the content of the magnifier.
792 private final int mContentWidth;
793 private final int mContentHeight;
794 // The size of the allocated surface.
795 private final int mSurfaceWidth;
796 private final int mSurfaceHeight;
797 // The insets of the content inside the allocated surface.
798 private final int mOffsetX;
799 private final int mOffsetY;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100800 // The overlay to be drawn on the top of the content.
801 private final Drawable mOverlay;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000802 // The surface we allocate for the magnifier content + shadow.
803 private final SurfaceSession mSurfaceSession;
804 private final SurfaceControl mSurfaceControl;
805 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 Popa5d983d22018-03-29 15:56:17 +0100838 // The lock used to synchronize the UI and render threads when a #destroy
839 // is performed on the UI thread and a frame callback on the render thread.
840 // When both mLock and mDestroyLock need to be held at the same time,
841 // mDestroyLock should be acquired before mLock in order to avoid deadlocks.
Bin Chenc8beb5c2019-05-10 16:05:05 +0800842 private final Object mDestroyLock;
Mihai Popa5d983d22018-03-29 15:56:17 +0100843
Mihai Popa1ddabb22018-08-06 11:55:54 +0100844 // The current content of the magnifier. It is mBitmap + mOverlay, only used for testing.
845 private Bitmap mCurrentContent;
846
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000847 InternalPopupWindow(final Context context, final Display display,
Robert Carr5fea55b2018-12-10 13:05:52 -0800848 final SurfaceControl parentSurfaceControl, final int width, final int height,
Mihai Popa1ddabb22018-08-06 11:55:54 +0100849 final float elevation, final float cornerRadius, final Drawable overlay,
Bin Chenc8beb5c2019-05-10 16:05:05 +0800850 final Handler handler, final Object lock, final Object destroyLock,
851 final Callback callback) {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000852 mDisplay = display;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100853 mOverlay = overlay;
Mihai Popa39a71332018-02-22 19:30:24 +0000854 mLock = lock;
Bin Chenc8beb5c2019-05-10 16:05:05 +0800855 mDestroyLock = destroyLock;
Mihai Popa2ba5d8e2018-02-20 18:50:20 +0000856 mCallback = callback;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000857
858 mContentWidth = width;
859 mContentHeight = height;
Mihai Popabd391f92019-01-08 20:07:01 +0000860 mOffsetX = (int) (1.05f * elevation);
861 mOffsetY = (int) (1.05f * elevation);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000862 // Setup the surface we will use for drawing the content and shadow.
863 mSurfaceWidth = mContentWidth + 2 * mOffsetX;
864 mSurfaceHeight = mContentHeight + 2 * mOffsetY;
Robert Carr5fea55b2018-12-10 13:05:52 -0800865 mSurfaceSession = new SurfaceSession();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000866 mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
867 .setFormat(PixelFormat.TRANSLUCENT)
Vishnu Naire86bd982018-11-28 13:23:17 -0800868 .setBufferSize(mSurfaceWidth, mSurfaceHeight)
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000869 .setName("magnifier surface")
870 .setFlags(SurfaceControl.HIDDEN)
Robert Carr5fea55b2018-12-10 13:05:52 -0800871 .setParent(parentSurfaceControl)
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000872 .build();
873 mSurface = new Surface();
874 mSurface.copyFrom(mSurfaceControl);
875
Mihai Popa1ddabb22018-08-06 11:55:54 +0100876 // Setup the RenderNode tree. The root has two children, one containing the bitmap
877 // and one containing the overlay. We use a separate render node for the overlay
878 // to avoid drawing this as the same rate we do for content.
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000879 mRenderer = new ThreadedRenderer.SimpleRenderer(
880 context,
881 "magnifier renderer",
882 mSurface
883 );
884 mBitmapRenderNode = createRenderNodeForBitmap(
885 "magnifier content",
Mihai Popafb4b6b82018-03-01 16:08:14 +0000886 elevation,
887 cornerRadius
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000888 );
Mihai Popa1ddabb22018-08-06 11:55:54 +0100889 mOverlayRenderNode = createRenderNodeForOverlay(
890 "magnifier overlay",
891 cornerRadius
892 );
893 setupOverlay();
Mihai Popa8b789102018-02-15 12:06:59 +0000894
John Recke57475e2019-02-20 17:39:52 -0800895 final RecordingCanvas canvas = mRenderer.getRootNode().beginRecording(width, height);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000896 try {
897 canvas.insertReorderBarrier();
898 canvas.drawRenderNode(mBitmapRenderNode);
899 canvas.insertInorderBarrier();
Mihai Popa1ddabb22018-08-06 11:55:54 +0100900 canvas.drawRenderNode(mOverlayRenderNode);
901 canvas.insertInorderBarrier();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000902 } finally {
John Recke57475e2019-02-20 17:39:52 -0800903 mRenderer.getRootNode().endRecording();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000904 }
Mihai Popa1ddabb22018-08-06 11:55:54 +0100905 if (mCallback != null) {
906 mCurrentContent =
907 Bitmap.createBitmap(mContentWidth, mContentHeight, Bitmap.Config.ARGB_8888);
908 updateCurrentContentForTesting();
909 }
Mihai Popa8b789102018-02-15 12:06:59 +0000910
911 // Initialize the update job and the handler where this will be post'd.
912 mHandler = handler;
913 mMagnifierUpdater = this::doDraw;
914 mFrameDrawScheduled = false;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000915 }
916
Mihai Popafb4b6b82018-03-01 16:08:14 +0000917 private RenderNode createRenderNodeForBitmap(final String name,
918 final float elevation, final float cornerRadius) {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000919 final RenderNode bitmapRenderNode = RenderNode.create(name, null);
920
921 // Define the position of the bitmap in the parent render node. The surface regions
922 // outside the bitmap are used to draw elevation.
923 bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
924 mOffsetX + mContentWidth, mOffsetY + mContentHeight);
925 bitmapRenderNode.setElevation(elevation);
926
927 final Outline outline = new Outline();
Mihai Popafb4b6b82018-03-01 16:08:14 +0000928 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000929 outline.setAlpha(1.0f);
930 bitmapRenderNode.setOutline(outline);
931 bitmapRenderNode.setClipToOutline(true);
932
933 // Create a dummy draw, which will be replaced later with real drawing.
John Recke57475e2019-02-20 17:39:52 -0800934 final RecordingCanvas canvas = bitmapRenderNode.beginRecording(
935 mContentWidth, mContentHeight);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000936 try {
937 canvas.drawColor(0xFF00FF00);
938 } finally {
John Recke57475e2019-02-20 17:39:52 -0800939 bitmapRenderNode.endRecording();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000940 }
941
942 return bitmapRenderNode;
943 }
944
Mihai Popa1ddabb22018-08-06 11:55:54 +0100945 private RenderNode createRenderNodeForOverlay(final String name, final float cornerRadius) {
946 final RenderNode overlayRenderNode = RenderNode.create(name, null);
947
948 // Define the position of the overlay in the parent render node.
949 // This coincides with the position of the content.
950 overlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
951 mOffsetX + mContentWidth, mOffsetY + mContentHeight);
952
953 final Outline outline = new Outline();
954 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
955 outline.setAlpha(1.0f);
956 overlayRenderNode.setOutline(outline);
957 overlayRenderNode.setClipToOutline(true);
958
959 return overlayRenderNode;
960 }
961
962 private void setupOverlay() {
963 drawOverlay();
964
965 mOverlay.setCallback(new Drawable.Callback() {
966 @Override
967 public void invalidateDrawable(Drawable who) {
968 // When the overlay drawable is invalidated, redraw it to the render node.
969 drawOverlay();
970 if (mCallback != null) {
971 updateCurrentContentForTesting();
972 }
973 }
974
975 @Override
976 public void scheduleDrawable(Drawable who, Runnable what, long when) {
977 Handler.getMain().postAtTime(what, who, when);
978 }
979
980 @Override
981 public void unscheduleDrawable(Drawable who, Runnable what) {
982 Handler.getMain().removeCallbacks(what, who);
983 }
984 });
985 }
986
987 private void drawOverlay() {
988 // Draw the drawable to the render node. This happens once during
989 // initialization and whenever the overlay drawable is invalidated.
990 final RecordingCanvas canvas =
John Recke57475e2019-02-20 17:39:52 -0800991 mOverlayRenderNode.beginRecording(mContentWidth, mContentHeight);
Mihai Popa1ddabb22018-08-06 11:55:54 +0100992 try {
993 mOverlay.setBounds(0, 0, mContentWidth, mContentHeight);
994 mOverlay.draw(canvas);
995 } finally {
996 mOverlayRenderNode.endRecording();
997 }
998 }
999
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001000 /**
1001 * Sets the position of the magnifier content relative to the parent surface.
1002 * The position update will happen in the same frame with the next draw.
Mihai Popa8b789102018-02-15 12:06:59 +00001003 * The method has to be called in a context that holds {@link #mLock}.
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001004 *
1005 * @param contentX the x coordinate of the content
1006 * @param contentY the y coordinate of the content
1007 */
1008 public void setContentPositionForNextDraw(final int contentX, final int contentY) {
1009 mWindowPositionX = contentX - mOffsetX;
1010 mWindowPositionY = contentY - mOffsetY;
1011 mPendingWindowPositionUpdate = true;
Mihai Popa8b789102018-02-15 12:06:59 +00001012 requestUpdate();
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001013 }
1014
1015 /**
1016 * Sets the content that should be displayed in the magnifier.
1017 * The update happens immediately, and possibly triggers a pending window movement set
1018 * by {@link #setContentPositionForNextDraw(int, int)}.
Mihai Popa8b789102018-02-15 12:06:59 +00001019 * The method has to be called in a context that holds {@link #mLock}.
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001020 *
1021 * @param bitmap the content bitmap
1022 */
Mihai Popa8b789102018-02-15 12:06:59 +00001023 public void updateContent(final @NonNull Bitmap bitmap) {
1024 if (mBitmap != null) {
1025 mBitmap.recycle();
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001026 }
Mihai Popa8b789102018-02-15 12:06:59 +00001027 mBitmap = bitmap;
1028 requestUpdate();
1029 }
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001030
Mihai Popa8b789102018-02-15 12:06:59 +00001031 private void requestUpdate() {
1032 if (mFrameDrawScheduled) {
1033 return;
1034 }
1035 final Message request = Message.obtain(mHandler, mMagnifierUpdater);
1036 request.setAsynchronous(true);
1037 request.sendToTarget();
1038 mFrameDrawScheduled = true;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001039 }
1040
1041 /**
1042 * Destroys this instance.
1043 */
1044 public void destroy() {
Mihai Popa5d983d22018-03-29 15:56:17 +01001045 synchronized (mDestroyLock) {
1046 mSurface.destroy();
1047 }
Mihai Popa8b789102018-02-15 12:06:59 +00001048 synchronized (mLock) {
Mihai Popa95688002018-02-23 16:10:11 +00001049 mRenderer.destroy();
Robert Carr5ea304d2019-02-04 16:04:55 -08001050 mSurfaceControl.remove();
Mihai Popa95688002018-02-23 16:10:11 +00001051 mSurfaceSession.kill();
Mihai Popa8b789102018-02-15 12:06:59 +00001052 mHandler.removeCallbacks(mMagnifierUpdater);
1053 if (mBitmap != null) {
1054 mBitmap.recycle();
1055 }
1056 }
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001057 }
1058
1059 private void doDraw() {
1060 final ThreadedRenderer.FrameDrawingCallback callback;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001061
Mihai Popa8b789102018-02-15 12:06:59 +00001062 // Draw the current bitmap to the surface, and prepare the callback which updates the
1063 // surface position. These have to be in the same synchronized block, in order to
1064 // guarantee the consistency between the bitmap content and the surface position.
1065 synchronized (mLock) {
1066 if (!mSurface.isValid()) {
1067 // Probably #destroy() was called for the current instance, so we skip the draw.
1068 return;
1069 }
1070
John Reck32f140aa62018-10-04 15:08:24 -07001071 final RecordingCanvas canvas =
John Recke57475e2019-02-20 17:39:52 -08001072 mBitmapRenderNode.beginRecording(mContentWidth, mContentHeight);
Mihai Popa8b789102018-02-15 12:06:59 +00001073 try {
1074 final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
1075 final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
1076 final Paint paint = new Paint();
1077 paint.setFilterBitmap(true);
1078 canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
1079 } finally {
John Recke57475e2019-02-20 17:39:52 -08001080 mBitmapRenderNode.endRecording();
Mihai Popa8b789102018-02-15 12:06:59 +00001081 }
1082
1083 if (mPendingWindowPositionUpdate || mFirstDraw) {
1084 // If the window has to be shown or moved, defer this until the next draw.
1085 final boolean firstDraw = mFirstDraw;
1086 mFirstDraw = false;
1087 final boolean updateWindowPosition = mPendingWindowPositionUpdate;
1088 mPendingWindowPositionUpdate = false;
1089 final int pendingX = mWindowPositionX;
1090 final int pendingY = mWindowPositionY;
1091
1092 callback = frame -> {
Mihai Popa5d983d22018-03-29 15:56:17 +01001093 synchronized (mDestroyLock) {
Mihai Popa95688002018-02-23 16:10:11 +00001094 if (!mSurface.isValid()) {
1095 return;
1096 }
Mihai Popa5d983d22018-03-29 15:56:17 +01001097 synchronized (mLock) {
Mihai Popa5d983d22018-03-29 15:56:17 +01001098 // Show or move the window at the content draw frame.
1099 SurfaceControl.openTransaction();
1100 mSurfaceControl.deferTransactionUntil(mSurface, frame);
1101 if (updateWindowPosition) {
1102 mSurfaceControl.setPosition(pendingX, pendingY);
1103 }
1104 if (firstDraw) {
Mihai Popa819e90d2018-04-16 14:27:05 +01001105 mSurfaceControl.setLayer(SURFACE_Z);
Mihai Popa5d983d22018-03-29 15:56:17 +01001106 mSurfaceControl.show();
1107 }
1108 SurfaceControl.closeTransaction();
Mihai Popa95688002018-02-23 16:10:11 +00001109 }
Mihai Popa8b789102018-02-15 12:06:59 +00001110 }
Mihai Popa8b789102018-02-15 12:06:59 +00001111 };
Mihai Popa6bf0b2f62019-02-26 18:01:37 +00001112 mRenderer.setLightCenter(mDisplay, pendingX, pendingY);
Mihai Popa8b789102018-02-15 12:06:59 +00001113 } else {
1114 callback = null;
1115 }
1116
Mihai Popa63ee7f12018-04-05 12:01:53 +01001117 mLastDrawContentPositionX = mWindowPositionX + mOffsetX;
1118 mLastDrawContentPositionY = mWindowPositionY + mOffsetY;
Mihai Popa8b789102018-02-15 12:06:59 +00001119 mFrameDrawScheduled = false;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001120 }
1121
1122 mRenderer.draw(callback);
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001123 if (mCallback != null) {
Mihai Popa1ddabb22018-08-06 11:55:54 +01001124 // The current content bitmap is only used in testing, so, for performance,
1125 // we only want to update it when running tests. For this, we check that
1126 // mCallback is not null, as it can only be set from a @TestApi.
1127 updateCurrentContentForTesting();
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001128 mCallback.onOperationComplete();
1129 }
1130 }
Mihai Popa1ddabb22018-08-06 11:55:54 +01001131
1132 /**
1133 * Updates mCurrentContent, which reproduces what is currently supposed to be
1134 * drawn in the magnifier. mCurrentContent is only used for testing, so this method
1135 * should only be called otherwise.
1136 */
1137 private void updateCurrentContentForTesting() {
1138 final Canvas canvas = new Canvas(mCurrentContent);
1139 final Rect bounds = new Rect(0, 0, mContentWidth, mContentHeight);
1140 if (mBitmap != null && !mBitmap.isRecycled()) {
1141 final Rect originalBounds = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
1142 canvas.drawBitmap(mBitmap, originalBounds, bounds, null);
1143 }
1144 mOverlay.setBounds(bounds);
1145 mOverlay.draw(canvas);
1146 }
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001147 }
1148
Mihai Popa469aba82018-07-18 14:52:26 +01001149 /**
1150 * Builder class for {@link Magnifier} objects.
1151 */
Mihai Popa53fd6962019-03-05 20:37:35 +00001152 public static final class Builder {
Mihai Popa469aba82018-07-18 14:52:26 +01001153 private @NonNull View mView;
1154 private @Px @IntRange(from = 0) int mWidth;
1155 private @Px @IntRange(from = 0) int mHeight;
1156 private float mZoom;
1157 private @FloatRange(from = 0f) float mElevation;
1158 private @FloatRange(from = 0f) float mCornerRadius;
Mihai Popa1ddabb22018-08-06 11:55:54 +01001159 private @Nullable Drawable mOverlay;
Mihai Popa469aba82018-07-18 14:52:26 +01001160 private int mHorizontalDefaultSourceToMagnifierOffset;
1161 private int mVerticalDefaultSourceToMagnifierOffset;
Mihai Popabecda342018-12-13 17:12:37 +00001162 private boolean mClippingEnabled;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001163 private @SourceBound int mLeftContentBound;
1164 private @SourceBound int mTopContentBound;
1165 private @SourceBound int mRightContentBound;
1166 private @SourceBound int mBottomContentBound;
Mihai Popa469aba82018-07-18 14:52:26 +01001167
1168 /**
1169 * Construct a new builder for {@link Magnifier} objects.
1170 * @param view the view this magnifier is attached to
1171 */
1172 public Builder(@NonNull View view) {
1173 mView = Preconditions.checkNotNull(view);
1174 applyDefaults();
1175 }
1176
1177 private void applyDefaults() {
Mihai Popac6950292018-11-15 21:32:42 +00001178 final Resources resources = mView.getContext().getResources();
1179 mWidth = resources.getDimensionPixelSize(R.dimen.default_magnifier_width);
1180 mHeight = resources.getDimensionPixelSize(R.dimen.default_magnifier_height);
1181 mElevation = resources.getDimension(R.dimen.default_magnifier_elevation);
1182 mCornerRadius = resources.getDimension(R.dimen.default_magnifier_corner_radius);
1183 mZoom = resources.getFloat(R.dimen.default_magnifier_zoom);
Mihai Popa469aba82018-07-18 14:52:26 +01001184 mHorizontalDefaultSourceToMagnifierOffset =
Mihai Popac6950292018-11-15 21:32:42 +00001185 resources.getDimensionPixelSize(R.dimen.default_magnifier_horizontal_offset);
Mihai Popa469aba82018-07-18 14:52:26 +01001186 mVerticalDefaultSourceToMagnifierOffset =
Mihai Popac6950292018-11-15 21:32:42 +00001187 resources.getDimensionPixelSize(R.dimen.default_magnifier_vertical_offset);
1188 mOverlay = new ColorDrawable(resources.getColor(
1189 R.color.default_magnifier_color_overlay, null));
Mihai Popabecda342018-12-13 17:12:37 +00001190 mClippingEnabled = true;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001191 mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
Mihai Popac6950292018-11-15 21:32:42 +00001192 mTopContentBound = SOURCE_BOUND_MAX_VISIBLE;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001193 mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
Mihai Popac6950292018-11-15 21:32:42 +00001194 mBottomContentBound = SOURCE_BOUND_MAX_VISIBLE;
Mihai Popa469aba82018-07-18 14:52:26 +01001195 }
1196
1197 /**
1198 * Sets the size of the magnifier window, in pixels. Defaults to (100dp, 48dp).
1199 * Note that the size of the content being magnified and copied to the magnifier
1200 * will be computed as (window width / zoom, window height / zoom).
1201 * @param width the window width to be set
1202 * @param height the window height to be set
1203 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001204 @NonNull
Mihai Popa469aba82018-07-18 14:52:26 +01001205 public Builder setSize(@Px @IntRange(from = 0) int width,
1206 @Px @IntRange(from = 0) int height) {
1207 Preconditions.checkArgumentPositive(width, "Width should be positive");
1208 Preconditions.checkArgumentPositive(height, "Height should be positive");
1209 mWidth = width;
1210 mHeight = height;
1211 return this;
1212 }
1213
1214 /**
1215 * Sets the zoom to be applied to the chosen content before being copied to the magnifier.
1216 * A content of size (content_width, content_height) will be magnified to
1217 * (content_width * zoom, content_height * zoom), which will coincide with the size
1218 * of the magnifier. A zoom of 1 will translate to no magnification (the content will
1219 * be just copied to the magnifier with no scaling). The zoom defaults to 1.25.
Mihai Popa27cf08f2019-01-10 19:59:29 +00001220 * Note that the zoom can also be changed after the instance is built, using the
1221 * {@link Magnifier#setZoom(float)} method.
Mihai Popa469aba82018-07-18 14:52:26 +01001222 * @param zoom the zoom to be set
1223 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001224 @NonNull
Mihai Popa27cf08f2019-01-10 19:59:29 +00001225 public Builder setInitialZoom(@FloatRange(from = 0f) float zoom) {
Mihai Popa469aba82018-07-18 14:52:26 +01001226 Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
1227 mZoom = zoom;
1228 return this;
1229 }
1230
1231 /**
1232 * Sets the elevation of the magnifier window, in pixels. Defaults to 4dp.
1233 * @param elevation the elevation to be set
1234 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001235 @NonNull
Mihai Popa469aba82018-07-18 14:52:26 +01001236 public Builder setElevation(@Px @FloatRange(from = 0) float elevation) {
1237 Preconditions.checkArgumentNonNegative(elevation, "Elevation should be non-negative");
1238 mElevation = elevation;
1239 return this;
1240 }
1241
1242 /**
Mihai Popac6950292018-11-15 21:32:42 +00001243 * Sets the corner radius of the magnifier window, in pixels. Defaults to 2dp.
Mihai Popa469aba82018-07-18 14:52:26 +01001244 * @param cornerRadius the corner radius to be set
1245 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001246 @NonNull
Mihai Popa469aba82018-07-18 14:52:26 +01001247 public Builder setCornerRadius(@Px @FloatRange(from = 0) float cornerRadius) {
1248 Preconditions.checkArgumentNonNegative(cornerRadius,
1249 "Corner radius should be non-negative");
1250 mCornerRadius = cornerRadius;
1251 return this;
1252 }
1253
1254 /**
Mihai Popabecda342018-12-13 17:12:37 +00001255 * Sets an overlay that will be drawn on the top of the magnifier.
1256 * In general, the overlay should not be opaque, in order to let the magnified
1257 * content be partially visible in the magnifier. The default overlay is {@code null}
1258 * (no overlay). As an example, TextView applies a white {@link ColorDrawable}
1259 * overlay with 5% alpha, aiming to make the magnifier distinguishable when shown in dark
Mihai Popac6950292018-11-15 21:32:42 +00001260 * application regions. To disable the overlay, the parameter should be set
1261 * to {@code null}. If not null, the overlay will be automatically redrawn
Mihai Popa1ddabb22018-08-06 11:55:54 +01001262 * when the drawable is invalidated. To achieve this, the magnifier will set a new
1263 * {@link android.graphics.drawable.Drawable.Callback} for the overlay drawable,
1264 * so keep in mind that any existing one set by the application will be lost.
1265 * @param overlay the overlay to be drawn on top
1266 */
1267 @NonNull
1268 public Builder setOverlay(@Nullable Drawable overlay) {
1269 mOverlay = overlay;
1270 return this;
1271 }
1272
1273 /**
1274 * Sets an offset that should be added to the content source center to obtain
Mihai Popa469aba82018-07-18 14:52:26 +01001275 * the position of the magnifier window, when the {@link #show(float, float)}
1276 * method is called. The offset is ignored when {@link #show(float, float, float, float)}
Mihai Popac6950292018-11-15 21:32:42 +00001277 * is used. The offset can be negative. It defaults to (0dp, 0dp).
Mihai Popa469aba82018-07-18 14:52:26 +01001278 * @param horizontalOffset the horizontal component of the offset
1279 * @param verticalOffset the vertical component of the offset
1280 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001281 @NonNull
Mihai Popa469aba82018-07-18 14:52:26 +01001282 public Builder setDefaultSourceToMagnifierOffset(@Px int horizontalOffset,
1283 @Px int verticalOffset) {
1284 mHorizontalDefaultSourceToMagnifierOffset = horizontalOffset;
1285 mVerticalDefaultSourceToMagnifierOffset = verticalOffset;
1286 return this;
1287 }
1288
1289 /**
Mihai Popa1903cab2018-08-01 14:33:12 +01001290 * Defines the behavior of the magnifier when it is requested to position outside the
1291 * surface of the main application window. The default value is {@code true}, which means
1292 * that the position will be adjusted such that the magnifier will be fully within the
Mihai Popabecda342018-12-13 17:12:37 +00001293 * bounds of the main application window, while also avoiding any overlap with system insets
1294 * (such as the one corresponding to the status bar). If this flag is set to {@code false},
1295 * the area where the magnifier can be positioned will no longer be clipped, so the
1296 * magnifier will be able to extend outside the main application window boundaries (and also
1297 * overlap the system insets). This can be useful if you require a custom behavior, but it
1298 * should be handled with care, when passing coordinates to {@link #show(float, float)};
1299 * note that:
Mihai Popa1903cab2018-08-01 14:33:12 +01001300 * <ul>
1301 * <li>in a multiwindow context, if the magnifier crosses the boundary between the two
1302 * windows, it will not be able to show over the window of the other application</li>
1303 * <li>if the magnifier overlaps the status bar, there is no guarantee about which one
1304 * will be displayed on top. This should be handled with care.</li>
1305 * </ul>
Mihai Popabecda342018-12-13 17:12:37 +00001306 * @param clip whether the magnifier position will be adjusted
Mihai Popa1903cab2018-08-01 14:33:12 +01001307 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001308 @NonNull
Mihai Popabecda342018-12-13 17:12:37 +00001309 public Builder setClippingEnabled(boolean clip) {
1310 mClippingEnabled = clip;
Mihai Popa1903cab2018-08-01 14:33:12 +01001311 return this;
1312 }
1313
1314 /**
Mihai Popa3e1aed12018-08-03 18:25:52 +01001315 * Defines the bounds of the rectangle where the magnifier will be able to copy its content
1316 * from. The content will always be copied from the {@link Surface} of the main application
1317 * window unless the magnified view is a {@link SurfaceView}, in which case its backing
1318 * surface will be used. Each bound can have a different behavior, with the options being:
1319 * <ul>
1320 * <li>{@link #SOURCE_BOUND_MAX_VISIBLE}, which extends the bound as much as possible
1321 * while remaining in the visible region of the magnified view, as given by
1322 * {@link android.view.View#getGlobalVisibleRect(Rect)}. For example, this will take into
1323 * account the case when the view is contained in a scrollable container, and the
1324 * magnifier will refuse to copy content outside of the visible view region</li>
Mihai Popa3e1aed12018-08-03 18:25:52 +01001325 * <li>{@link #SOURCE_BOUND_MAX_IN_SURFACE}, which extends the bound as much
1326 * as possible while remaining inside the surface the content is copied from.</li>
1327 * </ul>
1328 * Note that if either of the first three options is used, the bound will be compared to
1329 * the bound of the surface (i.e. as if {@link #SOURCE_BOUND_MAX_IN_SURFACE} was used),
1330 * and the more restrictive one will be chosen. In other words, no attempt to copy content
1331 * from outside the surface will be permitted. If two opposite bounds are not well-behaved
1332 * (i.e. left + sourceWidth > right or top + sourceHeight > bottom), the left and top
1333 * bounds will have priority and the others will be extended accordingly. If the pairs
1334 * obtained this way still remain out of bounds, the smallest possible offset will be added
1335 * to the pairs to bring them inside the surface bounds. If this is impossible
1336 * (i.e. the surface is too small for the size of the content we try to copy on either
1337 * dimension), an error will be logged and the magnifier content will look distorted.
1338 * The default values assumed by the builder for the source bounds are
1339 * left: {@link #SOURCE_BOUND_MAX_VISIBLE}, top: {@link #SOURCE_BOUND_MAX_IN_SURFACE},
1340 * right: {@link #SOURCE_BOUND_MAX_VISIBLE}, bottom: {@link #SOURCE_BOUND_MAX_IN_SURFACE}.
1341 * @param left the left bound for content copy
1342 * @param top the top bound for content copy
1343 * @param right the right bound for content copy
1344 * @param bottom the bottom bound for content copy
1345 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001346 @NonNull
Mihai Popa3e1aed12018-08-03 18:25:52 +01001347 public Builder setSourceBounds(@SourceBound int left, @SourceBound int top,
1348 @SourceBound int right, @SourceBound int bottom) {
1349 mLeftContentBound = left;
1350 mTopContentBound = top;
1351 mRightContentBound = right;
1352 mBottomContentBound = bottom;
1353 return this;
1354 }
1355
1356 /**
Mihai Popa469aba82018-07-18 14:52:26 +01001357 * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}.
1358 */
1359 public @NonNull Magnifier build() {
1360 return new Magnifier(this);
1361 }
1362 }
1363
Mihai Popa3e1aed12018-08-03 18:25:52 +01001364 /**
1365 * A source bound that will extend as much as possible, while remaining within the surface
1366 * the content is copied from.
1367 */
Mihai Popa3e1aed12018-08-03 18:25:52 +01001368 public static final int SOURCE_BOUND_MAX_IN_SURFACE = 0;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001369
1370 /**
1371 * A source bound that will extend as much as possible, while remaining within the
1372 * visible region of the magnified view, as determined by
1373 * {@link View#getGlobalVisibleRect(Rect)}.
1374 */
Mihai Popa520e44752019-01-29 21:26:26 +00001375 public static final int SOURCE_BOUND_MAX_VISIBLE = 1;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001376
1377
1378 /**
1379 * Used to describe the {@link Surface} rectangle where the magnifier's content is allowed
1380 * to be copied from. For more details, see method
1381 * {@link Magnifier.Builder#setSourceBounds(int, int, int, int)}
1382 *
1383 * @hide
1384 */
Mihai Popa520e44752019-01-29 21:26:26 +00001385 @IntDef({SOURCE_BOUND_MAX_IN_SURFACE, SOURCE_BOUND_MAX_VISIBLE})
Mihai Popa3e1aed12018-08-03 18:25:52 +01001386 @Retention(RetentionPolicy.SOURCE)
1387 public @interface SourceBound {}
1388
Mihai Popa1ddabb22018-08-06 11:55:54 +01001389 // The rest of the file consists of test APIs and methods relevant for tests.
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001390
1391 /**
1392 * See {@link #setOnOperationCompleteCallback(Callback)}.
1393 */
1394 @TestApi
1395 private Callback mCallback;
1396
1397 /**
1398 * Sets a callback which will be invoked at the end of the next
1399 * {@link #show(float, float)} or {@link #update()} operation.
1400 *
1401 * @hide
1402 */
1403 @TestApi
1404 public void setOnOperationCompleteCallback(final Callback callback) {
1405 mCallback = callback;
1406 if (mWindow != null) {
1407 mWindow.mCallback = callback;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001408 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01001409 }
Mihai Popa137b5842018-01-30 15:03:22 +00001410
1411 /**
Mihai Popa1ddabb22018-08-06 11:55:54 +01001412 * @return the drawing being currently displayed in the magnifier, as bitmap
Mihai Popa137b5842018-01-30 15:03:22 +00001413 *
1414 * @hide
1415 */
1416 @TestApi
Mihai Popa8b789102018-02-15 12:06:59 +00001417 public @Nullable Bitmap getContent() {
1418 if (mWindow == null) {
1419 return null;
1420 }
1421 synchronized (mWindow.mLock) {
Mihai Popa1ddabb22018-08-06 11:55:54 +01001422 return mWindow.mCurrentContent;
Mihai Popa8b789102018-02-15 12:06:59 +00001423 }
Mihai Popa137b5842018-01-30 15:03:22 +00001424 }
1425
1426 /**
Mihai Popa1ddabb22018-08-06 11:55:54 +01001427 * Returns a bitmap containing the content that was magnified and drew to the
1428 * magnifier, at its original size, without the overlay applied.
1429 * @return the content that is magnified, as bitmap
Mihai Popabeeaf552018-07-19 15:50:43 +01001430 *
1431 * @hide
1432 */
1433 @TestApi
1434 public @Nullable Bitmap getOriginalContent() {
1435 if (mWindow == null) {
1436 return null;
1437 }
1438 synchronized (mWindow.mLock) {
1439 return Bitmap.createBitmap(mWindow.mBitmap);
1440 }
1441 }
1442
1443 /**
Mihai Popa137b5842018-01-30 15:03:22 +00001444 * @return the size of the magnifier window in dp
1445 *
1446 * @hide
1447 */
1448 @TestApi
1449 public static PointF getMagnifierDefaultSize() {
1450 final Resources resources = Resources.getSystem();
1451 final float density = resources.getDisplayMetrics().density;
1452 final PointF size = new PointF();
Mihai Popac6950292018-11-15 21:32:42 +00001453 size.x = resources.getDimension(R.dimen.default_magnifier_width) / density;
1454 size.y = resources.getDimension(R.dimen.default_magnifier_height) / density;
Mihai Popa137b5842018-01-30 15:03:22 +00001455 return size;
1456 }
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001457
1458 /**
1459 * @hide
1460 */
1461 @TestApi
1462 public interface Callback {
1463 /**
1464 * Callback called after the drawing for a magnifier update has happened.
1465 */
1466 void onOperationComplete();
1467 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01001468}