blob: b7cdad2d5e51837d4579882bf11044b6a2803678 [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();
Andrei Stingaceanu15af5612017-10-13 12:53:23 +0100139
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100140 /**
141 * Initializes a magnifier.
142 *
143 * @param view the view for which this magnifier is attached
Mihai Popa469aba82018-07-18 14:52:26 +0100144 *
Mihai Popab6ca9092018-09-24 21:14:50 +0100145 * @deprecated Please use {@link Builder} instead
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100146 */
Mihai Popab6ca9092018-09-24 21:14:50 +0100147 @Deprecated
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100148 public Magnifier(@NonNull View view) {
Mihai Popac6950292018-11-15 21:32:42 +0000149 this(createBuilderWithOldMagnifierDefaults(view));
150 }
151
152 static Builder createBuilderWithOldMagnifierDefaults(final View view) {
153 final Builder params = new Builder(view);
154 final Context context = view.getContext();
155 final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Magnifier,
156 R.attr.magnifierStyle, 0);
157 params.mWidth = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierWidth, 0);
158 params.mHeight = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHeight, 0);
159 params.mElevation = a.getDimension(R.styleable.Magnifier_magnifierElevation, 0);
160 params.mCornerRadius = getDeviceDefaultDialogCornerRadius(context);
161 params.mZoom = a.getFloat(R.styleable.Magnifier_magnifierZoom, 0);
162 params.mHorizontalDefaultSourceToMagnifierOffset =
163 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHorizontalOffset, 0);
164 params.mVerticalDefaultSourceToMagnifierOffset =
165 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierVerticalOffset, 0);
166 params.mOverlay = new ColorDrawable(a.getColor(
167 R.styleable.Magnifier_magnifierColorOverlay, Color.TRANSPARENT));
168 a.recycle();
Mihai Popabecda342018-12-13 17:12:37 +0000169 params.mClippingEnabled = true;
Mihai Popac6950292018-11-15 21:32:42 +0000170 params.mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
171 params.mTopContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
172 params.mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
173 params.mBottomContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
174 return params;
175 }
176
177 /**
178 * Returns the device default theme dialog corner radius attribute.
179 * We retrieve this from the device default theme to avoid
180 * using the values set in the custom application themes.
181 */
182 private static float getDeviceDefaultDialogCornerRadius(final Context context) {
183 final Context deviceDefaultContext =
184 new ContextThemeWrapper(context, R.style.Theme_DeviceDefault);
185 final TypedArray ta = deviceDefaultContext.obtainStyledAttributes(
186 new int[]{android.R.attr.dialogCornerRadius});
187 final float dialogCornerRadius = ta.getDimension(0, 0);
188 ta.recycle();
189 return dialogCornerRadius;
Mihai Popa469aba82018-07-18 14:52:26 +0100190 }
191
192 private Magnifier(@NonNull Builder params) {
193 // Copy params from builder.
194 mView = params.mView;
195 mWindowWidth = params.mWidth;
196 mWindowHeight = params.mHeight;
197 mZoom = params.mZoom;
Mihai Popa7433a072018-07-18 12:18:34 +0100198 mSourceWidth = Math.round(mWindowWidth / mZoom);
199 mSourceHeight = Math.round(mWindowHeight / mZoom);
Mihai Popa469aba82018-07-18 14:52:26 +0100200 mWindowElevation = params.mElevation;
201 mWindowCornerRadius = params.mCornerRadius;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100202 mOverlay = params.mOverlay;
Mihai Popa469aba82018-07-18 14:52:26 +0100203 mDefaultHorizontalSourceToMagnifierOffset =
204 params.mHorizontalDefaultSourceToMagnifierOffset;
205 mDefaultVerticalSourceToMagnifierOffset =
206 params.mVerticalDefaultSourceToMagnifierOffset;
Mihai Popabecda342018-12-13 17:12:37 +0000207 mClippingEnabled = params.mClippingEnabled;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100208 mLeftContentBound = params.mLeftContentBound;
209 mTopContentBound = params.mTopContentBound;
210 mRightContentBound = params.mRightContentBound;
211 mBottomContentBound = params.mBottomContentBound;
Mihai Popa1d1ed0c2018-01-12 12:38:12 +0000212 // The view's surface coordinates will not be updated until the magnifier is first shown.
213 mViewCoordinatesInSurface = new int[2];
Mihai Popa8b789102018-02-15 12:06:59 +0000214 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100215
Mihai Popa8b789102018-02-15 12:06:59 +0000216 static {
217 sPixelCopyHandlerThread.start();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100218 }
219
220 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100221 * Shows the magnifier on the screen. The method takes the coordinates of the center
222 * of the content source going to be magnified and copied to the magnifier. The coordinates
223 * are relative to the top left corner of the magnified view. The magnifier will be
224 * positioned such that its center will be at the default offset from the center of the source.
225 * The default offset can be specified using the method
226 * {@link Builder#setDefaultSourceToMagnifierOffset(int, int)}. If the offset should
227 * be different across calls to this method, you should consider to use method
228 * {@link #show(float, float, float, float)} instead.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100229 *
Mihai Popac2e0bee2018-07-19 12:18:30 +0100230 * @param sourceCenterX horizontal coordinate of the source center, relative to the view
231 * @param sourceCenterY vertical coordinate of the source center, relative to the view
232 *
233 * @see Builder#setDefaultSourceToMagnifierOffset(int, int)
234 * @see Builder#getDefaultHorizontalSourceToMagnifierOffset()
235 * @see Builder#getDefaultVerticalSourceToMagnifierOffset()
236 * @see #show(float, float, float, float)
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100237 */
Mihai Popa7433a072018-07-18 12:18:34 +0100238 public void show(@FloatRange(from = 0) float sourceCenterX,
239 @FloatRange(from = 0) float sourceCenterY) {
Mihai Popa469aba82018-07-18 14:52:26 +0100240 show(sourceCenterX, sourceCenterY,
241 sourceCenterX + mDefaultHorizontalSourceToMagnifierOffset,
242 sourceCenterY + mDefaultVerticalSourceToMagnifierOffset);
Mihai Popa7433a072018-07-18 12:18:34 +0100243 }
244
245 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100246 * Shows the magnifier on the screen at a position that is independent from its content
247 * position. The first two arguments represent the coordinates of the center of the
248 * content source going to be magnified and copied to the magnifier. The last two arguments
249 * represent the coordinates of the center of the magnifier itself. All four coordinates
250 * are relative to the top left corner of the magnified view. If you consider using this
251 * method such that the offset between the source center and the magnifier center coordinates
252 * remains constant, you should consider using method {@link #show(float, float)} instead.
Mihai Popa7433a072018-07-18 12:18:34 +0100253 *
Mihai Popac2e0bee2018-07-19 12:18:30 +0100254 * @param sourceCenterX horizontal coordinate of the source center relative to the view
255 * @param sourceCenterY vertical coordinate of the source center, relative to the view
256 * @param magnifierCenterX horizontal coordinate of the magnifier center, relative to the view
257 * @param magnifierCenterY vertical coordinate of the magnifier center, relative to the view
Mihai Popa7433a072018-07-18 12:18:34 +0100258 */
259 public void show(@FloatRange(from = 0) float sourceCenterX,
260 @FloatRange(from = 0) float sourceCenterY,
261 float magnifierCenterX, float magnifierCenterY) {
Andrei Stingaceanu451f9472017-10-13 16:41:28 +0100262
Mihai Popaf2980682018-04-30 19:08:57 +0100263 obtainSurfaces();
Mihai Popa7433a072018-07-18 12:18:34 +0100264 obtainContentCoordinates(sourceCenterX, sourceCenterY);
265 obtainWindowCoordinates(magnifierCenterX, magnifierCenterY);
Andrei Stingaceanuca189fe2017-10-19 17:02:22 +0100266
Mihai Popa7433a072018-07-18 12:18:34 +0100267 final int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2;
268 final int startY = mClampedCenterZoomCoords.y - mSourceHeight / 2;
Mihai Popabeeaf552018-07-19 15:50:43 +0100269 if (sourceCenterX != mPrevShowSourceCoords.x || sourceCenterY != mPrevShowSourceCoords.y
Mihai Popa3e1aed12018-08-03 18:25:52 +0100270 || mDirtyState) {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000271 if (mWindow == null) {
Mihai Popa39a71332018-02-22 19:30:24 +0000272 synchronized (mLock) {
273 mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(),
Robert Carr5fea55b2018-12-10 13:05:52 -0800274 mParentSurface.mSurfaceControl, mWindowWidth, mWindowHeight,
Mihai Popa1ddabb22018-08-06 11:55:54 +0100275 mWindowElevation, mWindowCornerRadius,
276 mOverlay != null ? mOverlay : new ColorDrawable(Color.TRANSPARENT),
Mihai Popa39a71332018-02-22 19:30:24 +0000277 Handler.getMain() /* draw the magnifier on the UI thread */, mLock,
278 mCallback);
279 }
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000280 }
281 performPixelCopy(startX, startY, true /* update window position */);
Mihai Popa7433a072018-07-18 12:18:34 +0100282 } else if (magnifierCenterX != mPrevShowWindowCoords.x
283 || magnifierCenterY != mPrevShowWindowCoords.y) {
284 final Point windowCoords = getCurrentClampedWindowCoordinates();
285 final InternalPopupWindow currentWindowInstance = mWindow;
286 sPixelCopyHandlerThread.getThreadHandler().post(() -> {
Mihai Popa7433a072018-07-18 12:18:34 +0100287 synchronized (mLock) {
Mihai Popaddcd54812018-09-03 17:25:54 +0100288 if (mWindow != currentWindowInstance) {
289 // The magnifier was dismissed (and maybe shown again) in the meantime.
290 return;
291 }
Mihai Popa7433a072018-07-18 12:18:34 +0100292 mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
293 }
294 });
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100295 }
Mihai Popa7433a072018-07-18 12:18:34 +0100296 mPrevShowSourceCoords.x = sourceCenterX;
297 mPrevShowSourceCoords.y = sourceCenterY;
298 mPrevShowWindowCoords.x = magnifierCenterX;
299 mPrevShowWindowCoords.y = magnifierCenterY;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100300 }
301
302 /**
Andrei Stingaceanud6644dc2017-11-21 14:53:38 +0000303 * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100304 */
305 public void dismiss() {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000306 if (mWindow != null) {
Mihai Popa39a71332018-02-22 19:30:24 +0000307 synchronized (mLock) {
308 mWindow.destroy();
309 mWindow = null;
310 }
Mihai Popa7433a072018-07-18 12:18:34 +0100311 mPrevShowSourceCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
312 mPrevShowSourceCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
313 mPrevShowWindowCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
314 mPrevShowWindowCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
Mihai Popa953b1342018-03-21 18:05:13 +0000315 mPrevStartCoordsInSurface.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
316 mPrevStartCoordsInSurface.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000317 }
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000318 }
Andrei Stingaceanu15af5612017-10-13 12:53:23 +0100319
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000320 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100321 * Asks the magnifier to update its content. It uses the previous coordinates passed to
322 * {@link #show(float, float)} or {@link #show(float, float, float, float)}. The
323 * method only has effect if the magnifier is currently showing.
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000324 */
325 public void update() {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000326 if (mWindow != null) {
Mihai Popaf2980682018-04-30 19:08:57 +0100327 obtainSurfaces();
Mihai Popa3e1aed12018-08-03 18:25:52 +0100328 if (!mDirtyState) {
Mihai Popabeeaf552018-07-19 15:50:43 +0100329 // Update the content shown in the magnifier.
330 performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y,
331 false /* update window position */);
332 } else {
Mihai Popa3e1aed12018-08-03 18:25:52 +0100333 // If for example the zoom has changed, we cannot use the same top left
334 // coordinates as before, so just #show again to have them recomputed.
Mihai Popabeeaf552018-07-19 15:50:43 +0100335 show(mPrevShowSourceCoords.x, mPrevShowSourceCoords.y,
336 mPrevShowWindowCoords.x, mPrevShowWindowCoords.y);
337 }
Andrei Stingaceanu15af5612017-10-13 12:53:23 +0100338 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100339 }
340
Mihai Popa17ea3052018-03-06 14:24:07 +0000341 /**
Mihai Popa469aba82018-07-18 14:52:26 +0100342 * @return the width of the magnifier window, in pixels
Mihai Popac2e0bee2018-07-19 12:18:30 +0100343 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa17ea3052018-03-06 14:24:07 +0000344 */
Mihai Popac2e0bee2018-07-19 12:18:30 +0100345 @Px
Mihai Popa17ea3052018-03-06 14:24:07 +0000346 public int getWidth() {
347 return mWindowWidth;
348 }
349
350 /**
Mihai Popa469aba82018-07-18 14:52:26 +0100351 * @return the height of the magnifier window, in pixels
Mihai Popac2e0bee2018-07-19 12:18:30 +0100352 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa17ea3052018-03-06 14:24:07 +0000353 */
Mihai Popac2e0bee2018-07-19 12:18:30 +0100354 @Px
Mihai Popa17ea3052018-03-06 14:24:07 +0000355 public int getHeight() {
356 return mWindowHeight;
357 }
358
359 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100360 * @return the initial width of the content magnified and copied to the magnifier, in pixels
361 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa27cf08f2019-01-10 19:59:29 +0000362 * @see Magnifier.Builder#setInitialZoom(float)
Mihai Popac2e0bee2018-07-19 12:18:30 +0100363 */
364 @Px
365 public int getSourceWidth() {
366 return mSourceWidth;
367 }
368
369 /**
370 * @return the initial height of the content magnified and copied to the magnifier, in pixels
371 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa27cf08f2019-01-10 19:59:29 +0000372 * @see Magnifier.Builder#setInitialZoom(float)
Mihai Popac2e0bee2018-07-19 12:18:30 +0100373 */
374 @Px
375 public int getSourceHeight() {
376 return mSourceHeight;
377 }
378
379 /**
Mihai Popabeeaf552018-07-19 15:50:43 +0100380 * Sets the zoom to be applied to the chosen content before being copied to the magnifier popup.
Mihai Popa3e1aed12018-08-03 18:25:52 +0100381 * The change will become effective at the next #show or #update call.
Mihai Popabeeaf552018-07-19 15:50:43 +0100382 * @param zoom the zoom to be set
383 */
384 public void setZoom(@FloatRange(from = 0f) float zoom) {
385 Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
386 mZoom = zoom;
387 mSourceWidth = Math.round(mWindowWidth / mZoom);
388 mSourceHeight = Math.round(mWindowHeight / mZoom);
Mihai Popa3e1aed12018-08-03 18:25:52 +0100389 mDirtyState = true;
Mihai Popabeeaf552018-07-19 15:50:43 +0100390 }
391
392 /**
Mihai Popa469aba82018-07-18 14:52:26 +0100393 * Returns the zoom to be applied to the magnified view region copied to the magnifier.
Mihai Popa17ea3052018-03-06 14:24:07 +0000394 * If the zoom is x and the magnifier window size is (width, height), the original size
Mihai Popa469aba82018-07-18 14:52:26 +0100395 * of the content being magnified will be (width / x, height / x).
396 * @return the zoom applied to the content
Mihai Popa27cf08f2019-01-10 19:59:29 +0000397 * @see Magnifier.Builder#setInitialZoom(float)
Mihai Popa17ea3052018-03-06 14:24:07 +0000398 */
399 public float getZoom() {
400 return mZoom;
401 }
402
Mihai Popa63ee7f12018-04-05 12:01:53 +0100403 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100404 * @return the elevation set for the magnifier window, in pixels
405 * @see Magnifier.Builder#setElevation(float)
406 */
407 @Px
408 public float getElevation() {
409 return mWindowElevation;
410 }
411
412 /**
413 * @return the corner radius of the magnifier window, in pixels
414 * @see Magnifier.Builder#setCornerRadius(float)
415 */
416 @Px
417 public float getCornerRadius() {
418 return mWindowCornerRadius;
419 }
420
421 /**
422 * Returns the horizontal offset, in pixels, to be applied to the source center position
423 * to obtain the magnifier center position when {@link #show(float, float)} is called.
424 * The value is ignored when {@link #show(float, float, float, float)} is used instead.
Mihai Popa0450a162018-04-27 13:09:12 +0100425 *
Mihai Popac2e0bee2018-07-19 12:18:30 +0100426 * @return the default horizontal offset between the source center and the magnifier
427 * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
428 * @see Magnifier#show(float, float)
429 */
430 @Px
431 public int getDefaultHorizontalSourceToMagnifierOffset() {
432 return mDefaultHorizontalSourceToMagnifierOffset;
433 }
434
435 /**
436 * Returns the vertical offset, in pixels, to be applied to the source center position
437 * to obtain the magnifier center position when {@link #show(float, float)} is called.
438 * The value is ignored when {@link #show(float, float, float, float)} is used instead.
439 *
440 * @return the default vertical offset between the source center and the magnifier
441 * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
442 * @see Magnifier#show(float, float)
443 */
444 @Px
445 public int getDefaultVerticalSourceToMagnifierOffset() {
446 return mDefaultVerticalSourceToMagnifierOffset;
447 }
448
449 /**
Mihai Popabecda342018-12-13 17:12:37 +0000450 * Returns the overlay to be drawn on the top of the magnifier, or
Mihai Popa1ddabb22018-08-06 11:55:54 +0100451 * {@code null} if no overlay should be drawn.
452 * @return the overlay
453 * @see Magnifier.Builder#setOverlay(Drawable)
454 */
455 @Nullable
456 public Drawable getOverlay() {
457 return mOverlay;
458 }
459
460 /**
Mihai Popa1903cab2018-08-01 14:33:12 +0100461 * Returns whether the magnifier position will be adjusted such that the magnifier will be
Mihai Popabecda342018-12-13 17:12:37 +0000462 * fully within the bounds of the main application window, by also avoiding any overlap
463 * with system insets (such as the one corresponding to the status bar) i.e. whether the
464 * area where the magnifier can be positioned will be clipped to the main application window
465 * and the system insets.
Mihai Popa1903cab2018-08-01 14:33:12 +0100466 * @return whether the magnifier position will be adjusted
Mihai Popabecda342018-12-13 17:12:37 +0000467 * @see Magnifier.Builder#setClippingEnabled(boolean)
Mihai Popa1903cab2018-08-01 14:33:12 +0100468 */
Mihai Popabecda342018-12-13 17:12:37 +0000469 public boolean isClippingEnabled() {
470 return mClippingEnabled;
Mihai Popa1903cab2018-08-01 14:33:12 +0100471 }
472
473 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100474 * Returns the top left coordinates of the magnifier, relative to the surface of the
475 * main application window. They will be determined by the coordinates of the last
476 * {@link #show(float, float)} or {@link #show(float, float, float, float)} call, adjusted
477 * to take into account any potential clamping behavior. The method can be used immediately
478 * after a #show call to find out where the magnifier will be positioned. However, the
479 * position of the magnifier will not be updated in the same frame due to the async
480 * copying of the content copying and of the magnifier rendering.
481 * The method will return {@code null} if #show has not yet been called, or if the last
482 * operation performed was a #dismiss.
483 *
484 * @return the top left coordinates of the magnifier
Mihai Popa63ee7f12018-04-05 12:01:53 +0100485 */
486 @Nullable
Mihai Popac2e0bee2018-07-19 12:18:30 +0100487 public Point getPosition() {
Mihai Popa63ee7f12018-04-05 12:01:53 +0100488 if (mWindow == null) {
489 return null;
490 }
Mihai Popac2e0bee2018-07-19 12:18:30 +0100491 return new Point(getCurrentClampedWindowCoordinates());
492 }
493
494 /**
495 * Returns the top left coordinates of the magnifier source (i.e. the view region going to
496 * be magnified and copied to the magnifier), relative to the surface the content is copied
497 * from. The content will be copied:
498 * - if the magnified view is a {@link SurfaceView}, from the surface backing it
499 * - otherwise, from the surface of the main application window
500 * The method will return {@code null} if #show has not yet been called, or if the last
501 * operation performed was a #dismiss.
502 *
503 * @return the top left coordinates of the magnifier source
504 */
505 @Nullable
506 public Point getSourcePosition() {
507 if (mWindow == null) {
508 return null;
509 }
510 return new Point(mPixelCopyRequestRect.left, mPixelCopyRequestRect.top);
Mihai Popa63ee7f12018-04-05 12:01:53 +0100511 }
512
Mihai Popaf2980682018-04-30 19:08:57 +0100513 /**
514 * Retrieves the surfaces used by the magnifier:
515 * - a parent surface for the magnifier surface. This will usually be the main app window.
516 * - a surface where the magnified content will be copied from. This will be the main app
517 * window unless the magnified view is a SurfaceView, in which case its backing surface
518 * will be used.
519 */
520 private void obtainSurfaces() {
521 // Get the main window surface.
522 SurfaceInfo validMainWindowSurface = SurfaceInfo.NULL;
Mihai Popa819e90d2018-04-16 14:27:05 +0100523 if (mView.getViewRootImpl() != null) {
Mihai Popaf2980682018-04-30 19:08:57 +0100524 final ViewRootImpl viewRootImpl = mView.getViewRootImpl();
525 final Surface mainWindowSurface = viewRootImpl.mSurface;
Mihai Popa819e90d2018-04-16 14:27:05 +0100526 if (mainWindowSurface != null && mainWindowSurface.isValid()) {
Mihai Popaf2980682018-04-30 19:08:57 +0100527 final Rect surfaceInsets = viewRootImpl.mWindowAttributes.surfaceInsets;
528 final int surfaceWidth =
529 viewRootImpl.getWidth() + surfaceInsets.left + surfaceInsets.right;
530 final int surfaceHeight =
531 viewRootImpl.getHeight() + surfaceInsets.top + surfaceInsets.bottom;
532 validMainWindowSurface =
Robert Carr5fea55b2018-12-10 13:05:52 -0800533 new SurfaceInfo(viewRootImpl.getSurfaceControl(), mainWindowSurface,
534 surfaceWidth, surfaceHeight, true);
Mihai Popa819e90d2018-04-16 14:27:05 +0100535 }
Mihai Popa17ea3052018-03-06 14:24:07 +0000536 }
Mihai Popaf2980682018-04-30 19:08:57 +0100537 // Get the surface backing the magnified view, if it is a SurfaceView.
538 SurfaceInfo validSurfaceViewSurface = SurfaceInfo.NULL;
Mihai Popa819e90d2018-04-16 14:27:05 +0100539 if (mView instanceof SurfaceView) {
Robert Carr5fea55b2018-12-10 13:05:52 -0800540 final SurfaceControl sc = ((SurfaceView) mView).getSurfaceControl();
Mihai Popaf2980682018-04-30 19:08:57 +0100541 final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder();
542 final Surface surfaceViewSurface = surfaceHolder.getSurface();
Robert Carr5fea55b2018-12-10 13:05:52 -0800543
544 if (sc != null && sc.isValid()) {
Mihai Popaf2980682018-04-30 19:08:57 +0100545 final Rect surfaceFrame = surfaceHolder.getSurfaceFrame();
Robert Carr5fea55b2018-12-10 13:05:52 -0800546 validSurfaceViewSurface = new SurfaceInfo(sc, surfaceViewSurface,
Mihai Popaf2980682018-04-30 19:08:57 +0100547 surfaceFrame.right, surfaceFrame.bottom, false);
Mihai Popa819e90d2018-04-16 14:27:05 +0100548 }
549 }
Mihai Popaf2980682018-04-30 19:08:57 +0100550
551 // Choose the parent surface for the magnifier and the source surface for the content.
552 mParentSurface = validMainWindowSurface != SurfaceInfo.NULL
553 ? validMainWindowSurface : validSurfaceViewSurface;
554 mContentCopySurface = mView instanceof SurfaceView
555 ? validSurfaceViewSurface : validMainWindowSurface;
Mihai Popa17ea3052018-03-06 14:24:07 +0000556 }
557
Mihai Popaf2980682018-04-30 19:08:57 +0100558 /**
559 * Computes the coordinates of the center of the content going to be displayed in the
560 * magnifier. These are relative to the surface the content is copied from.
561 */
562 private void obtainContentCoordinates(final float xPosInView, final float yPosInView) {
Mihai Popa3e1aed12018-08-03 18:25:52 +0100563 final int prevViewXInSurface = mViewCoordinatesInSurface[0];
564 final int prevViewYInSurface = mViewCoordinatesInSurface[1];
Mihai Popa819e90d2018-04-16 14:27:05 +0100565 mView.getLocationInSurface(mViewCoordinatesInSurface);
Mihai Popa3e1aed12018-08-03 18:25:52 +0100566 if (mViewCoordinatesInSurface[0] != prevViewXInSurface
567 || mViewCoordinatesInSurface[1] != prevViewYInSurface) {
568 mDirtyState = true;
569 }
570
Mihai Popa7433a072018-07-18 12:18:34 +0100571 final int zoomCenterX;
572 final int zoomCenterY;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000573 if (mView instanceof SurfaceView) {
574 // No offset required if the backing Surface matches the size of the SurfaceView.
Mihai Popa7433a072018-07-18 12:18:34 +0100575 zoomCenterX = Math.round(xPosInView);
576 zoomCenterY = Math.round(yPosInView);
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000577 } else {
Mihai Popa7433a072018-07-18 12:18:34 +0100578 zoomCenterX = Math.round(xPosInView + mViewCoordinatesInSurface[0]);
579 zoomCenterY = Math.round(yPosInView + mViewCoordinatesInSurface[1]);
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000580 }
581
Mihai Popa520e44752019-01-29 21:26:26 +0000582 final Rect[] bounds = new Rect[2]; // [MAX_IN_SURFACE, MAX_VISIBLE]
Mihai Popa3e1aed12018-08-03 18:25:52 +0100583 // Obtain the surface bounds rectangle.
584 final Rect surfaceBounds = new Rect(0, 0,
585 mContentCopySurface.mWidth, mContentCopySurface.mHeight);
586 bounds[0] = surfaceBounds;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100587 // Obtain the visible view region rectangle.
Mihai Popaf2980682018-04-30 19:08:57 +0100588 final Rect viewVisibleRegion = new Rect();
589 mView.getGlobalVisibleRect(viewVisibleRegion);
590 if (mView.getViewRootImpl() != null) {
591 // Clamping coordinates relative to the surface, not to the window.
592 final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets;
593 viewVisibleRegion.offset(surfaceInsets.left, surfaceInsets.top);
594 }
595 if (mView instanceof SurfaceView) {
596 // If we copy content from a SurfaceView, clamp coordinates relative to it.
597 viewVisibleRegion.offset(-mViewCoordinatesInSurface[0], -mViewCoordinatesInSurface[1]);
598 }
Mihai Popa520e44752019-01-29 21:26:26 +0000599 bounds[1] = viewVisibleRegion;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100600
601 // Aggregate the above to obtain the bounds where the content copy will be restricted.
602 int resolvedLeft = Integer.MIN_VALUE;
603 for (int i = mLeftContentBound; i >= 0; --i) {
604 resolvedLeft = Math.max(resolvedLeft, bounds[i].left);
605 }
606 int resolvedTop = Integer.MIN_VALUE;
607 for (int i = mTopContentBound; i >= 0; --i) {
608 resolvedTop = Math.max(resolvedTop, bounds[i].top);
609 }
610 int resolvedRight = Integer.MAX_VALUE;
611 for (int i = mRightContentBound; i >= 0; --i) {
612 resolvedRight = Math.min(resolvedRight, bounds[i].right);
613 }
614 int resolvedBottom = Integer.MAX_VALUE;
615 for (int i = mBottomContentBound; i >= 0; --i) {
616 resolvedBottom = Math.min(resolvedBottom, bounds[i].bottom);
617 }
618 // Adjust <left-right> and <top-bottom> pairs of bounds to make sense.
619 resolvedLeft = Math.min(resolvedLeft, mContentCopySurface.mWidth - mSourceWidth);
620 resolvedTop = Math.min(resolvedTop, mContentCopySurface.mHeight - mSourceHeight);
621 if (resolvedLeft < 0 || resolvedTop < 0) {
622 Log.e(TAG, "Magnifier's content is copied from a surface smaller than"
623 + "the content requested size. This will probably lead to distorted content.");
624 }
625 resolvedRight = Math.max(resolvedRight, resolvedLeft + mSourceWidth);
626 resolvedBottom = Math.max(resolvedBottom, resolvedTop + mSourceHeight);
627
628 // Finally compute the coordinates of the source center.
629 mClampedCenterZoomCoords.x = Math.max(resolvedLeft + mSourceWidth / 2, Math.min(
630 zoomCenterX, resolvedRight - mSourceWidth / 2));
631 mClampedCenterZoomCoords.y = Math.max(resolvedTop + mSourceHeight / 2, Math.min(
632 zoomCenterY, resolvedBottom - mSourceHeight / 2));
Mihai Popaf2980682018-04-30 19:08:57 +0100633 }
634
Mihai Popa7433a072018-07-18 12:18:34 +0100635 /**
636 * Computes the coordinates of the top left corner of the magnifier window.
637 * These are relative to the surface the magnifier window is attached to.
638 */
639 private void obtainWindowCoordinates(final float xWindowPos, final float yWindowPos) {
640 final int windowCenterX;
641 final int windowCenterY;
642 if (mView instanceof SurfaceView) {
643 // No offset required if the backing Surface matches the size of the SurfaceView.
644 windowCenterX = Math.round(xWindowPos);
645 windowCenterY = Math.round(yWindowPos);
646 } else {
647 windowCenterX = Math.round(xWindowPos + mViewCoordinatesInSurface[0]);
648 windowCenterY = Math.round(yWindowPos + mViewCoordinatesInSurface[1]);
649 }
650
651 mWindowCoords.x = windowCenterX - mWindowWidth / 2;
652 mWindowCoords.y = windowCenterY - mWindowHeight / 2;
Mihai Popaf2980682018-04-30 19:08:57 +0100653 if (mParentSurface != mContentCopySurface) {
654 mWindowCoords.x += mViewCoordinatesInSurface[0];
655 mWindowCoords.y += mViewCoordinatesInSurface[1];
Mihai Popa819e90d2018-04-16 14:27:05 +0100656 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100657 }
658
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000659 private void performPixelCopy(final int startXInSurface, final int startYInSurface,
660 final boolean updateWindowPosition) {
Mihai Popaf2980682018-04-30 19:08:57 +0100661 if (mContentCopySurface.mSurface == null || !mContentCopySurface.mSurface.isValid()) {
Mihai Popa3589c2c2018-01-25 19:26:30 +0000662 return;
663 }
Mihai Popa3e1aed12018-08-03 18:25:52 +0100664
Mihai Popa953b1342018-03-21 18:05:13 +0000665 // Clamp window coordinates inside the parent surface, to avoid displaying
666 // the magnifier out of screen or overlapping with system insets.
Mihai Popa7433a072018-07-18 12:18:34 +0100667 final Point windowCoords = getCurrentClampedWindowCoordinates();
Mihai Popa3589c2c2018-01-25 19:26:30 +0000668
669 // Perform the pixel copy.
Mihai Popa3e1aed12018-08-03 18:25:52 +0100670 mPixelCopyRequestRect.set(startXInSurface,
671 startYInSurface,
672 startXInSurface + mSourceWidth,
673 startYInSurface + mSourceHeight);
Mihai Popa39a71332018-02-22 19:30:24 +0000674 final InternalPopupWindow currentWindowInstance = mWindow;
Mihai Popa8b789102018-02-15 12:06:59 +0000675 final Bitmap bitmap =
Mihai Popa7433a072018-07-18 12:18:34 +0100676 Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ARGB_8888);
Mihai Popaf2980682018-04-30 19:08:57 +0100677 PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap,
Mihai Popa3589c2c2018-01-25 19:26:30 +0000678 result -> {
Mihai Popa39a71332018-02-22 19:30:24 +0000679 synchronized (mLock) {
680 if (mWindow != currentWindowInstance) {
681 // The magnifier was dismissed (and maybe shown again) in the meantime.
682 return;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000683 }
Mihai Popa39a71332018-02-22 19:30:24 +0000684 if (updateWindowPosition) {
685 // TODO: pull the position update outside #performPixelCopy
Mihai Popa7433a072018-07-18 12:18:34 +0100686 mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
Mihai Popa39a71332018-02-22 19:30:24 +0000687 }
688 mWindow.updateContent(bitmap);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000689 }
Mihai Popa3589c2c2018-01-25 19:26:30 +0000690 },
Mihai Popa8b789102018-02-15 12:06:59 +0000691 sPixelCopyHandlerThread.getThreadHandler());
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000692 mPrevStartCoordsInSurface.x = startXInSurface;
693 mPrevStartCoordsInSurface.y = startYInSurface;
Mihai Popa3e1aed12018-08-03 18:25:52 +0100694 mDirtyState = false;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000695 }
696
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000697 /**
Mihai Popa7433a072018-07-18 12:18:34 +0100698 * Clamp window coordinates inside the surface the magnifier is attached to, to avoid
699 * displaying the magnifier out of screen or overlapping with system insets.
700 * @return the current window coordinates, after they are clamped inside the parent surface
701 */
702 private Point getCurrentClampedWindowCoordinates() {
Mihai Popabecda342018-12-13 17:12:37 +0000703 if (!mClippingEnabled) {
Mihai Popa1903cab2018-08-01 14:33:12 +0100704 // No position adjustment should be done, so return the raw coordinates.
705 return new Point(mWindowCoords);
706 }
707
Mihai Popa7433a072018-07-18 12:18:34 +0100708 final Rect windowBounds;
709 if (mParentSurface.mIsMainWindowSurface) {
Adrian Roos60f59292018-08-24 16:29:06 +0200710 final Insets systemInsets = mView.getRootWindowInsets().getSystemWindowInsets();
Mihai Popa7433a072018-07-18 12:18:34 +0100711 windowBounds = new Rect(systemInsets.left, systemInsets.top,
712 mParentSurface.mWidth - systemInsets.right,
713 mParentSurface.mHeight - systemInsets.bottom);
714 } else {
715 windowBounds = new Rect(0, 0, mParentSurface.mWidth, mParentSurface.mHeight);
716 }
717 final int windowCoordsX = Math.max(windowBounds.left,
718 Math.min(windowBounds.right - mWindowWidth, mWindowCoords.x));
719 final int windowCoordsY = Math.max(windowBounds.top,
720 Math.min(windowBounds.bottom - mWindowHeight, mWindowCoords.y));
721 return new Point(windowCoordsX, windowCoordsY);
722 }
723
724 /**
Mihai Popaf2980682018-04-30 19:08:57 +0100725 * Contains a surface and metadata corresponding to it.
726 */
727 private static class SurfaceInfo {
Robert Carr5fea55b2018-12-10 13:05:52 -0800728 public static final SurfaceInfo NULL = new SurfaceInfo(null, null, 0, 0, false);
Mihai Popaf2980682018-04-30 19:08:57 +0100729
730 private Surface mSurface;
Robert Carr5fea55b2018-12-10 13:05:52 -0800731 private SurfaceControl mSurfaceControl;
Mihai Popaf2980682018-04-30 19:08:57 +0100732 private int mWidth;
733 private int mHeight;
734 private boolean mIsMainWindowSurface;
735
Robert Carr5fea55b2018-12-10 13:05:52 -0800736 SurfaceInfo(final SurfaceControl surfaceControl, final Surface surface,
737 final int width, final int height,
Mihai Popaf2980682018-04-30 19:08:57 +0100738 final boolean isMainWindowSurface) {
Robert Carr5fea55b2018-12-10 13:05:52 -0800739 mSurfaceControl = surfaceControl;
Mihai Popaf2980682018-04-30 19:08:57 +0100740 mSurface = surface;
741 mWidth = width;
742 mHeight = height;
743 mIsMainWindowSurface = isMainWindowSurface;
744 }
745 }
746
747 /**
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000748 * Magnifier's own implementation of PopupWindow-similar floating window.
749 * This exists to ensure frame-synchronization between window position updates and window
750 * content updates. By using a PopupWindow, these events would happen in different frames,
751 * producing a shakiness effect for the magnifier content.
752 */
753 private static class InternalPopupWindow {
Mihai Popa819e90d2018-04-16 14:27:05 +0100754 // The z of the magnifier surface, defining its z order in the list of
755 // siblings having the same parent surface (usually the main app surface).
756 private static final int SURFACE_Z = 5;
Mihai Popad870b882018-02-27 14:25:52 +0000757
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000758 // Display associated to the view the magnifier is attached to.
759 private final Display mDisplay;
760 // The size of the content of the magnifier.
761 private final int mContentWidth;
762 private final int mContentHeight;
763 // The size of the allocated surface.
764 private final int mSurfaceWidth;
765 private final int mSurfaceHeight;
766 // The insets of the content inside the allocated surface.
767 private final int mOffsetX;
768 private final int mOffsetY;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100769 // The overlay to be drawn on the top of the content.
770 private final Drawable mOverlay;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000771 // The surface we allocate for the magnifier content + shadow.
772 private final SurfaceSession mSurfaceSession;
773 private final SurfaceControl mSurfaceControl;
774 private final Surface mSurface;
775 // The renderer used for the allocated surface.
776 private final ThreadedRenderer.SimpleRenderer mRenderer;
777 // The RenderNode used to draw the magnifier content in the surface.
778 private final RenderNode mBitmapRenderNode;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100779 // The RenderNode used to draw the overlay over the magnifier content.
780 private final RenderNode mOverlayRenderNode;
Mihai Popa8b789102018-02-15 12:06:59 +0000781 // The job that will be post'd to apply the pending magnifier updates to the surface.
782 private final Runnable mMagnifierUpdater;
783 // The handler where the magnifier updater jobs will be post'd.
784 private final Handler mHandler;
Mihai Popa63ee7f12018-04-05 12:01:53 +0100785 // The callback to be run after the next draw.
Mihai Popa2ba5d8e2018-02-20 18:50:20 +0000786 private Callback mCallback;
Mihai Popa63ee7f12018-04-05 12:01:53 +0100787 // The position of the magnifier content when the last draw was requested.
788 private int mLastDrawContentPositionX;
789 private int mLastDrawContentPositionY;
Mihai Popa8b789102018-02-15 12:06:59 +0000790
791 // Members below describe the state of the magnifier. Reads/writes to them
792 // have to be synchronized between the UI thread and the thread that handles
793 // the pixel copy results. This is the purpose of mLock.
Mihai Popa39a71332018-02-22 19:30:24 +0000794 private final Object mLock;
Mihai Popa8b789102018-02-15 12:06:59 +0000795 // Whether a magnifier frame draw is currently pending in the UI thread queue.
796 private boolean mFrameDrawScheduled;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100797 // The content bitmap, as returned by pixel copy.
Mihai Popa8b789102018-02-15 12:06:59 +0000798 private Bitmap mBitmap;
799 // Whether the next draw will be the first one for the current instance.
800 private boolean mFirstDraw = true;
801 // The window position in the parent surface. Might be applied during the next draw,
802 // when mPendingWindowPositionUpdate is true.
803 private int mWindowPositionX;
804 private int mWindowPositionY;
805 private boolean mPendingWindowPositionUpdate;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000806
Mihai Popa5d983d22018-03-29 15:56:17 +0100807 // The lock used to synchronize the UI and render threads when a #destroy
808 // is performed on the UI thread and a frame callback on the render thread.
809 // When both mLock and mDestroyLock need to be held at the same time,
810 // mDestroyLock should be acquired before mLock in order to avoid deadlocks.
811 private final Object mDestroyLock = new Object();
812
Mihai Popa1ddabb22018-08-06 11:55:54 +0100813 // The current content of the magnifier. It is mBitmap + mOverlay, only used for testing.
814 private Bitmap mCurrentContent;
815
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000816 InternalPopupWindow(final Context context, final Display display,
Robert Carr5fea55b2018-12-10 13:05:52 -0800817 final SurfaceControl parentSurfaceControl, final int width, final int height,
Mihai Popa1ddabb22018-08-06 11:55:54 +0100818 final float elevation, final float cornerRadius, final Drawable overlay,
Mihai Popa39a71332018-02-22 19:30:24 +0000819 final Handler handler, final Object lock, final Callback callback) {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000820 mDisplay = display;
Mihai Popa1ddabb22018-08-06 11:55:54 +0100821 mOverlay = overlay;
Mihai Popa39a71332018-02-22 19:30:24 +0000822 mLock = lock;
Mihai Popa2ba5d8e2018-02-20 18:50:20 +0000823 mCallback = callback;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000824
825 mContentWidth = width;
826 mContentHeight = height;
Mihai Popabd391f92019-01-08 20:07:01 +0000827 mOffsetX = (int) (1.05f * elevation);
828 mOffsetY = (int) (1.05f * elevation);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000829 // Setup the surface we will use for drawing the content and shadow.
830 mSurfaceWidth = mContentWidth + 2 * mOffsetX;
831 mSurfaceHeight = mContentHeight + 2 * mOffsetY;
Robert Carr5fea55b2018-12-10 13:05:52 -0800832 mSurfaceSession = new SurfaceSession();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000833 mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
834 .setFormat(PixelFormat.TRANSLUCENT)
Vishnu Naire86bd982018-11-28 13:23:17 -0800835 .setBufferSize(mSurfaceWidth, mSurfaceHeight)
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000836 .setName("magnifier surface")
837 .setFlags(SurfaceControl.HIDDEN)
Robert Carr5fea55b2018-12-10 13:05:52 -0800838 .setParent(parentSurfaceControl)
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000839 .build();
840 mSurface = new Surface();
841 mSurface.copyFrom(mSurfaceControl);
842
Mihai Popa1ddabb22018-08-06 11:55:54 +0100843 // Setup the RenderNode tree. The root has two children, one containing the bitmap
844 // and one containing the overlay. We use a separate render node for the overlay
845 // to avoid drawing this as the same rate we do for content.
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000846 mRenderer = new ThreadedRenderer.SimpleRenderer(
847 context,
848 "magnifier renderer",
849 mSurface
850 );
851 mBitmapRenderNode = createRenderNodeForBitmap(
852 "magnifier content",
Mihai Popafb4b6b82018-03-01 16:08:14 +0000853 elevation,
854 cornerRadius
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000855 );
Mihai Popa1ddabb22018-08-06 11:55:54 +0100856 mOverlayRenderNode = createRenderNodeForOverlay(
857 "magnifier overlay",
858 cornerRadius
859 );
860 setupOverlay();
Mihai Popa8b789102018-02-15 12:06:59 +0000861
John Recke57475e2019-02-20 17:39:52 -0800862 final RecordingCanvas canvas = mRenderer.getRootNode().beginRecording(width, height);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000863 try {
864 canvas.insertReorderBarrier();
865 canvas.drawRenderNode(mBitmapRenderNode);
866 canvas.insertInorderBarrier();
Mihai Popa1ddabb22018-08-06 11:55:54 +0100867 canvas.drawRenderNode(mOverlayRenderNode);
868 canvas.insertInorderBarrier();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000869 } finally {
John Recke57475e2019-02-20 17:39:52 -0800870 mRenderer.getRootNode().endRecording();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000871 }
Mihai Popa1ddabb22018-08-06 11:55:54 +0100872 if (mCallback != null) {
873 mCurrentContent =
874 Bitmap.createBitmap(mContentWidth, mContentHeight, Bitmap.Config.ARGB_8888);
875 updateCurrentContentForTesting();
876 }
Mihai Popa8b789102018-02-15 12:06:59 +0000877
878 // Initialize the update job and the handler where this will be post'd.
879 mHandler = handler;
880 mMagnifierUpdater = this::doDraw;
881 mFrameDrawScheduled = false;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000882 }
883
Mihai Popafb4b6b82018-03-01 16:08:14 +0000884 private RenderNode createRenderNodeForBitmap(final String name,
885 final float elevation, final float cornerRadius) {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000886 final RenderNode bitmapRenderNode = RenderNode.create(name, null);
887
888 // Define the position of the bitmap in the parent render node. The surface regions
889 // outside the bitmap are used to draw elevation.
890 bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
891 mOffsetX + mContentWidth, mOffsetY + mContentHeight);
892 bitmapRenderNode.setElevation(elevation);
893
894 final Outline outline = new Outline();
Mihai Popafb4b6b82018-03-01 16:08:14 +0000895 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000896 outline.setAlpha(1.0f);
897 bitmapRenderNode.setOutline(outline);
898 bitmapRenderNode.setClipToOutline(true);
899
900 // Create a dummy draw, which will be replaced later with real drawing.
John Recke57475e2019-02-20 17:39:52 -0800901 final RecordingCanvas canvas = bitmapRenderNode.beginRecording(
902 mContentWidth, mContentHeight);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000903 try {
904 canvas.drawColor(0xFF00FF00);
905 } finally {
John Recke57475e2019-02-20 17:39:52 -0800906 bitmapRenderNode.endRecording();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000907 }
908
909 return bitmapRenderNode;
910 }
911
Mihai Popa1ddabb22018-08-06 11:55:54 +0100912 private RenderNode createRenderNodeForOverlay(final String name, final float cornerRadius) {
913 final RenderNode overlayRenderNode = RenderNode.create(name, null);
914
915 // Define the position of the overlay in the parent render node.
916 // This coincides with the position of the content.
917 overlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
918 mOffsetX + mContentWidth, mOffsetY + mContentHeight);
919
920 final Outline outline = new Outline();
921 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
922 outline.setAlpha(1.0f);
923 overlayRenderNode.setOutline(outline);
924 overlayRenderNode.setClipToOutline(true);
925
926 return overlayRenderNode;
927 }
928
929 private void setupOverlay() {
930 drawOverlay();
931
932 mOverlay.setCallback(new Drawable.Callback() {
933 @Override
934 public void invalidateDrawable(Drawable who) {
935 // When the overlay drawable is invalidated, redraw it to the render node.
936 drawOverlay();
937 if (mCallback != null) {
938 updateCurrentContentForTesting();
939 }
940 }
941
942 @Override
943 public void scheduleDrawable(Drawable who, Runnable what, long when) {
944 Handler.getMain().postAtTime(what, who, when);
945 }
946
947 @Override
948 public void unscheduleDrawable(Drawable who, Runnable what) {
949 Handler.getMain().removeCallbacks(what, who);
950 }
951 });
952 }
953
954 private void drawOverlay() {
955 // Draw the drawable to the render node. This happens once during
956 // initialization and whenever the overlay drawable is invalidated.
957 final RecordingCanvas canvas =
John Recke57475e2019-02-20 17:39:52 -0800958 mOverlayRenderNode.beginRecording(mContentWidth, mContentHeight);
Mihai Popa1ddabb22018-08-06 11:55:54 +0100959 try {
960 mOverlay.setBounds(0, 0, mContentWidth, mContentHeight);
961 mOverlay.draw(canvas);
962 } finally {
963 mOverlayRenderNode.endRecording();
964 }
965 }
966
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000967 /**
968 * Sets the position of the magnifier content relative to the parent surface.
969 * The position update will happen in the same frame with the next draw.
Mihai Popa8b789102018-02-15 12:06:59 +0000970 * The method has to be called in a context that holds {@link #mLock}.
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000971 *
972 * @param contentX the x coordinate of the content
973 * @param contentY the y coordinate of the content
974 */
975 public void setContentPositionForNextDraw(final int contentX, final int contentY) {
976 mWindowPositionX = contentX - mOffsetX;
977 mWindowPositionY = contentY - mOffsetY;
978 mPendingWindowPositionUpdate = true;
Mihai Popa8b789102018-02-15 12:06:59 +0000979 requestUpdate();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000980 }
981
982 /**
983 * Sets the content that should be displayed in the magnifier.
984 * The update happens immediately, and possibly triggers a pending window movement set
985 * by {@link #setContentPositionForNextDraw(int, int)}.
Mihai Popa8b789102018-02-15 12:06:59 +0000986 * The method has to be called in a context that holds {@link #mLock}.
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000987 *
988 * @param bitmap the content bitmap
989 */
Mihai Popa8b789102018-02-15 12:06:59 +0000990 public void updateContent(final @NonNull Bitmap bitmap) {
991 if (mBitmap != null) {
992 mBitmap.recycle();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000993 }
Mihai Popa8b789102018-02-15 12:06:59 +0000994 mBitmap = bitmap;
995 requestUpdate();
996 }
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000997
Mihai Popa8b789102018-02-15 12:06:59 +0000998 private void requestUpdate() {
999 if (mFrameDrawScheduled) {
1000 return;
1001 }
1002 final Message request = Message.obtain(mHandler, mMagnifierUpdater);
1003 request.setAsynchronous(true);
1004 request.sendToTarget();
1005 mFrameDrawScheduled = true;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001006 }
1007
1008 /**
1009 * Destroys this instance.
1010 */
1011 public void destroy() {
Mihai Popa5d983d22018-03-29 15:56:17 +01001012 synchronized (mDestroyLock) {
1013 mSurface.destroy();
1014 }
Mihai Popa8b789102018-02-15 12:06:59 +00001015 synchronized (mLock) {
Mihai Popa95688002018-02-23 16:10:11 +00001016 mRenderer.destroy();
Robert Carr5ea304d2019-02-04 16:04:55 -08001017 mSurfaceControl.remove();
Mihai Popa95688002018-02-23 16:10:11 +00001018 mSurfaceSession.kill();
Mihai Popa8b789102018-02-15 12:06:59 +00001019 mHandler.removeCallbacks(mMagnifierUpdater);
1020 if (mBitmap != null) {
1021 mBitmap.recycle();
1022 }
1023 }
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001024 }
1025
1026 private void doDraw() {
1027 final ThreadedRenderer.FrameDrawingCallback callback;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001028
Mihai Popa8b789102018-02-15 12:06:59 +00001029 // Draw the current bitmap to the surface, and prepare the callback which updates the
1030 // surface position. These have to be in the same synchronized block, in order to
1031 // guarantee the consistency between the bitmap content and the surface position.
1032 synchronized (mLock) {
1033 if (!mSurface.isValid()) {
1034 // Probably #destroy() was called for the current instance, so we skip the draw.
1035 return;
1036 }
1037
John Reck32f140aa62018-10-04 15:08:24 -07001038 final RecordingCanvas canvas =
John Recke57475e2019-02-20 17:39:52 -08001039 mBitmapRenderNode.beginRecording(mContentWidth, mContentHeight);
Mihai Popa8b789102018-02-15 12:06:59 +00001040 try {
1041 final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
1042 final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
1043 final Paint paint = new Paint();
1044 paint.setFilterBitmap(true);
1045 canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
1046 } finally {
John Recke57475e2019-02-20 17:39:52 -08001047 mBitmapRenderNode.endRecording();
Mihai Popa8b789102018-02-15 12:06:59 +00001048 }
1049
1050 if (mPendingWindowPositionUpdate || mFirstDraw) {
1051 // If the window has to be shown or moved, defer this until the next draw.
1052 final boolean firstDraw = mFirstDraw;
1053 mFirstDraw = false;
1054 final boolean updateWindowPosition = mPendingWindowPositionUpdate;
1055 mPendingWindowPositionUpdate = false;
1056 final int pendingX = mWindowPositionX;
1057 final int pendingY = mWindowPositionY;
1058
1059 callback = frame -> {
Mihai Popa5d983d22018-03-29 15:56:17 +01001060 synchronized (mDestroyLock) {
Mihai Popa95688002018-02-23 16:10:11 +00001061 if (!mSurface.isValid()) {
1062 return;
1063 }
Mihai Popa5d983d22018-03-29 15:56:17 +01001064 synchronized (mLock) {
Mihai Popa5d983d22018-03-29 15:56:17 +01001065 // Show or move the window at the content draw frame.
1066 SurfaceControl.openTransaction();
1067 mSurfaceControl.deferTransactionUntil(mSurface, frame);
1068 if (updateWindowPosition) {
1069 mSurfaceControl.setPosition(pendingX, pendingY);
1070 }
1071 if (firstDraw) {
Mihai Popa819e90d2018-04-16 14:27:05 +01001072 mSurfaceControl.setLayer(SURFACE_Z);
Mihai Popa5d983d22018-03-29 15:56:17 +01001073 mSurfaceControl.show();
1074 }
1075 SurfaceControl.closeTransaction();
Mihai Popa95688002018-02-23 16:10:11 +00001076 }
Mihai Popa8b789102018-02-15 12:06:59 +00001077 }
Mihai Popa8b789102018-02-15 12:06:59 +00001078 };
Mihai Popa6bf0b2f62019-02-26 18:01:37 +00001079 mRenderer.setLightCenter(mDisplay, pendingX, pendingY);
Mihai Popa8b789102018-02-15 12:06:59 +00001080 } else {
1081 callback = null;
1082 }
1083
Mihai Popa63ee7f12018-04-05 12:01:53 +01001084 mLastDrawContentPositionX = mWindowPositionX + mOffsetX;
1085 mLastDrawContentPositionY = mWindowPositionY + mOffsetY;
Mihai Popa8b789102018-02-15 12:06:59 +00001086 mFrameDrawScheduled = false;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001087 }
1088
1089 mRenderer.draw(callback);
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001090 if (mCallback != null) {
Mihai Popa1ddabb22018-08-06 11:55:54 +01001091 // The current content bitmap is only used in testing, so, for performance,
1092 // we only want to update it when running tests. For this, we check that
1093 // mCallback is not null, as it can only be set from a @TestApi.
1094 updateCurrentContentForTesting();
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001095 mCallback.onOperationComplete();
1096 }
1097 }
Mihai Popa1ddabb22018-08-06 11:55:54 +01001098
1099 /**
1100 * Updates mCurrentContent, which reproduces what is currently supposed to be
1101 * drawn in the magnifier. mCurrentContent is only used for testing, so this method
1102 * should only be called otherwise.
1103 */
1104 private void updateCurrentContentForTesting() {
1105 final Canvas canvas = new Canvas(mCurrentContent);
1106 final Rect bounds = new Rect(0, 0, mContentWidth, mContentHeight);
1107 if (mBitmap != null && !mBitmap.isRecycled()) {
1108 final Rect originalBounds = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
1109 canvas.drawBitmap(mBitmap, originalBounds, bounds, null);
1110 }
1111 mOverlay.setBounds(bounds);
1112 mOverlay.draw(canvas);
1113 }
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001114 }
1115
Mihai Popa469aba82018-07-18 14:52:26 +01001116 /**
1117 * Builder class for {@link Magnifier} objects.
1118 */
1119 public static class Builder {
1120 private @NonNull View mView;
1121 private @Px @IntRange(from = 0) int mWidth;
1122 private @Px @IntRange(from = 0) int mHeight;
1123 private float mZoom;
1124 private @FloatRange(from = 0f) float mElevation;
1125 private @FloatRange(from = 0f) float mCornerRadius;
Mihai Popa1ddabb22018-08-06 11:55:54 +01001126 private @Nullable Drawable mOverlay;
Mihai Popa469aba82018-07-18 14:52:26 +01001127 private int mHorizontalDefaultSourceToMagnifierOffset;
1128 private int mVerticalDefaultSourceToMagnifierOffset;
Mihai Popabecda342018-12-13 17:12:37 +00001129 private boolean mClippingEnabled;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001130 private @SourceBound int mLeftContentBound;
1131 private @SourceBound int mTopContentBound;
1132 private @SourceBound int mRightContentBound;
1133 private @SourceBound int mBottomContentBound;
Mihai Popa469aba82018-07-18 14:52:26 +01001134
1135 /**
1136 * Construct a new builder for {@link Magnifier} objects.
1137 * @param view the view this magnifier is attached to
1138 */
1139 public Builder(@NonNull View view) {
1140 mView = Preconditions.checkNotNull(view);
1141 applyDefaults();
1142 }
1143
1144 private void applyDefaults() {
Mihai Popac6950292018-11-15 21:32:42 +00001145 final Resources resources = mView.getContext().getResources();
1146 mWidth = resources.getDimensionPixelSize(R.dimen.default_magnifier_width);
1147 mHeight = resources.getDimensionPixelSize(R.dimen.default_magnifier_height);
1148 mElevation = resources.getDimension(R.dimen.default_magnifier_elevation);
1149 mCornerRadius = resources.getDimension(R.dimen.default_magnifier_corner_radius);
1150 mZoom = resources.getFloat(R.dimen.default_magnifier_zoom);
Mihai Popa469aba82018-07-18 14:52:26 +01001151 mHorizontalDefaultSourceToMagnifierOffset =
Mihai Popac6950292018-11-15 21:32:42 +00001152 resources.getDimensionPixelSize(R.dimen.default_magnifier_horizontal_offset);
Mihai Popa469aba82018-07-18 14:52:26 +01001153 mVerticalDefaultSourceToMagnifierOffset =
Mihai Popac6950292018-11-15 21:32:42 +00001154 resources.getDimensionPixelSize(R.dimen.default_magnifier_vertical_offset);
1155 mOverlay = new ColorDrawable(resources.getColor(
1156 R.color.default_magnifier_color_overlay, null));
Mihai Popabecda342018-12-13 17:12:37 +00001157 mClippingEnabled = true;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001158 mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
Mihai Popac6950292018-11-15 21:32:42 +00001159 mTopContentBound = SOURCE_BOUND_MAX_VISIBLE;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001160 mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
Mihai Popac6950292018-11-15 21:32:42 +00001161 mBottomContentBound = SOURCE_BOUND_MAX_VISIBLE;
Mihai Popa469aba82018-07-18 14:52:26 +01001162 }
1163
1164 /**
1165 * Sets the size of the magnifier window, in pixels. Defaults to (100dp, 48dp).
1166 * Note that the size of the content being magnified and copied to the magnifier
1167 * will be computed as (window width / zoom, window height / zoom).
1168 * @param width the window width to be set
1169 * @param height the window height to be set
1170 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001171 @NonNull
Mihai Popa469aba82018-07-18 14:52:26 +01001172 public Builder setSize(@Px @IntRange(from = 0) int width,
1173 @Px @IntRange(from = 0) int height) {
1174 Preconditions.checkArgumentPositive(width, "Width should be positive");
1175 Preconditions.checkArgumentPositive(height, "Height should be positive");
1176 mWidth = width;
1177 mHeight = height;
1178 return this;
1179 }
1180
1181 /**
1182 * Sets the zoom to be applied to the chosen content before being copied to the magnifier.
1183 * A content of size (content_width, content_height) will be magnified to
1184 * (content_width * zoom, content_height * zoom), which will coincide with the size
1185 * of the magnifier. A zoom of 1 will translate to no magnification (the content will
1186 * be just copied to the magnifier with no scaling). The zoom defaults to 1.25.
Mihai Popa27cf08f2019-01-10 19:59:29 +00001187 * Note that the zoom can also be changed after the instance is built, using the
1188 * {@link Magnifier#setZoom(float)} method.
Mihai Popa469aba82018-07-18 14:52:26 +01001189 * @param zoom the zoom to be set
1190 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001191 @NonNull
Mihai Popa27cf08f2019-01-10 19:59:29 +00001192 public Builder setInitialZoom(@FloatRange(from = 0f) float zoom) {
Mihai Popa469aba82018-07-18 14:52:26 +01001193 Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
1194 mZoom = zoom;
1195 return this;
1196 }
1197
1198 /**
1199 * Sets the elevation of the magnifier window, in pixels. Defaults to 4dp.
1200 * @param elevation the elevation to be set
1201 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001202 @NonNull
Mihai Popa469aba82018-07-18 14:52:26 +01001203 public Builder setElevation(@Px @FloatRange(from = 0) float elevation) {
1204 Preconditions.checkArgumentNonNegative(elevation, "Elevation should be non-negative");
1205 mElevation = elevation;
1206 return this;
1207 }
1208
1209 /**
Mihai Popac6950292018-11-15 21:32:42 +00001210 * Sets the corner radius of the magnifier window, in pixels. Defaults to 2dp.
Mihai Popa469aba82018-07-18 14:52:26 +01001211 * @param cornerRadius the corner radius to be set
1212 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001213 @NonNull
Mihai Popa469aba82018-07-18 14:52:26 +01001214 public Builder setCornerRadius(@Px @FloatRange(from = 0) float cornerRadius) {
1215 Preconditions.checkArgumentNonNegative(cornerRadius,
1216 "Corner radius should be non-negative");
1217 mCornerRadius = cornerRadius;
1218 return this;
1219 }
1220
1221 /**
Mihai Popabecda342018-12-13 17:12:37 +00001222 * Sets an overlay that will be drawn on the top of the magnifier.
1223 * In general, the overlay should not be opaque, in order to let the magnified
1224 * content be partially visible in the magnifier. The default overlay is {@code null}
1225 * (no overlay). As an example, TextView applies a white {@link ColorDrawable}
1226 * overlay with 5% alpha, aiming to make the magnifier distinguishable when shown in dark
Mihai Popac6950292018-11-15 21:32:42 +00001227 * application regions. To disable the overlay, the parameter should be set
1228 * to {@code null}. If not null, the overlay will be automatically redrawn
Mihai Popa1ddabb22018-08-06 11:55:54 +01001229 * when the drawable is invalidated. To achieve this, the magnifier will set a new
1230 * {@link android.graphics.drawable.Drawable.Callback} for the overlay drawable,
1231 * so keep in mind that any existing one set by the application will be lost.
1232 * @param overlay the overlay to be drawn on top
1233 */
1234 @NonNull
1235 public Builder setOverlay(@Nullable Drawable overlay) {
1236 mOverlay = overlay;
1237 return this;
1238 }
1239
1240 /**
1241 * Sets an offset that should be added to the content source center to obtain
Mihai Popa469aba82018-07-18 14:52:26 +01001242 * the position of the magnifier window, when the {@link #show(float, float)}
1243 * method is called. The offset is ignored when {@link #show(float, float, float, float)}
Mihai Popac6950292018-11-15 21:32:42 +00001244 * is used. The offset can be negative. It defaults to (0dp, 0dp).
Mihai Popa469aba82018-07-18 14:52:26 +01001245 * @param horizontalOffset the horizontal component of the offset
1246 * @param verticalOffset the vertical component of the offset
1247 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001248 @NonNull
Mihai Popa469aba82018-07-18 14:52:26 +01001249 public Builder setDefaultSourceToMagnifierOffset(@Px int horizontalOffset,
1250 @Px int verticalOffset) {
1251 mHorizontalDefaultSourceToMagnifierOffset = horizontalOffset;
1252 mVerticalDefaultSourceToMagnifierOffset = verticalOffset;
1253 return this;
1254 }
1255
1256 /**
Mihai Popa1903cab2018-08-01 14:33:12 +01001257 * Defines the behavior of the magnifier when it is requested to position outside the
1258 * surface of the main application window. The default value is {@code true}, which means
1259 * that the position will be adjusted such that the magnifier will be fully within the
Mihai Popabecda342018-12-13 17:12:37 +00001260 * bounds of the main application window, while also avoiding any overlap with system insets
1261 * (such as the one corresponding to the status bar). If this flag is set to {@code false},
1262 * the area where the magnifier can be positioned will no longer be clipped, so the
1263 * magnifier will be able to extend outside the main application window boundaries (and also
1264 * overlap the system insets). This can be useful if you require a custom behavior, but it
1265 * should be handled with care, when passing coordinates to {@link #show(float, float)};
1266 * note that:
Mihai Popa1903cab2018-08-01 14:33:12 +01001267 * <ul>
1268 * <li>in a multiwindow context, if the magnifier crosses the boundary between the two
1269 * windows, it will not be able to show over the window of the other application</li>
1270 * <li>if the magnifier overlaps the status bar, there is no guarantee about which one
1271 * will be displayed on top. This should be handled with care.</li>
1272 * </ul>
Mihai Popabecda342018-12-13 17:12:37 +00001273 * @param clip whether the magnifier position will be adjusted
Mihai Popa1903cab2018-08-01 14:33:12 +01001274 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001275 @NonNull
Mihai Popabecda342018-12-13 17:12:37 +00001276 public Builder setClippingEnabled(boolean clip) {
1277 mClippingEnabled = clip;
Mihai Popa1903cab2018-08-01 14:33:12 +01001278 return this;
1279 }
1280
1281 /**
Mihai Popa3e1aed12018-08-03 18:25:52 +01001282 * Defines the bounds of the rectangle where the magnifier will be able to copy its content
1283 * from. The content will always be copied from the {@link Surface} of the main application
1284 * window unless the magnified view is a {@link SurfaceView}, in which case its backing
1285 * surface will be used. Each bound can have a different behavior, with the options being:
1286 * <ul>
1287 * <li>{@link #SOURCE_BOUND_MAX_VISIBLE}, which extends the bound as much as possible
1288 * while remaining in the visible region of the magnified view, as given by
1289 * {@link android.view.View#getGlobalVisibleRect(Rect)}. For example, this will take into
1290 * account the case when the view is contained in a scrollable container, and the
1291 * magnifier will refuse to copy content outside of the visible view region</li>
Mihai Popa3e1aed12018-08-03 18:25:52 +01001292 * <li>{@link #SOURCE_BOUND_MAX_IN_SURFACE}, which extends the bound as much
1293 * as possible while remaining inside the surface the content is copied from.</li>
1294 * </ul>
1295 * Note that if either of the first three options is used, the bound will be compared to
1296 * the bound of the surface (i.e. as if {@link #SOURCE_BOUND_MAX_IN_SURFACE} was used),
1297 * and the more restrictive one will be chosen. In other words, no attempt to copy content
1298 * from outside the surface will be permitted. If two opposite bounds are not well-behaved
1299 * (i.e. left + sourceWidth > right or top + sourceHeight > bottom), the left and top
1300 * bounds will have priority and the others will be extended accordingly. If the pairs
1301 * obtained this way still remain out of bounds, the smallest possible offset will be added
1302 * to the pairs to bring them inside the surface bounds. If this is impossible
1303 * (i.e. the surface is too small for the size of the content we try to copy on either
1304 * dimension), an error will be logged and the magnifier content will look distorted.
1305 * The default values assumed by the builder for the source bounds are
1306 * left: {@link #SOURCE_BOUND_MAX_VISIBLE}, top: {@link #SOURCE_BOUND_MAX_IN_SURFACE},
1307 * right: {@link #SOURCE_BOUND_MAX_VISIBLE}, bottom: {@link #SOURCE_BOUND_MAX_IN_SURFACE}.
1308 * @param left the left bound for content copy
1309 * @param top the top bound for content copy
1310 * @param right the right bound for content copy
1311 * @param bottom the bottom bound for content copy
1312 */
Mihai Popa1ddabb22018-08-06 11:55:54 +01001313 @NonNull
Mihai Popa3e1aed12018-08-03 18:25:52 +01001314 public Builder setSourceBounds(@SourceBound int left, @SourceBound int top,
1315 @SourceBound int right, @SourceBound int bottom) {
1316 mLeftContentBound = left;
1317 mTopContentBound = top;
1318 mRightContentBound = right;
1319 mBottomContentBound = bottom;
1320 return this;
1321 }
1322
1323 /**
Mihai Popa469aba82018-07-18 14:52:26 +01001324 * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}.
1325 */
1326 public @NonNull Magnifier build() {
1327 return new Magnifier(this);
1328 }
1329 }
1330
Mihai Popa3e1aed12018-08-03 18:25:52 +01001331 /**
1332 * A source bound that will extend as much as possible, while remaining within the surface
1333 * the content is copied from.
1334 */
Mihai Popa3e1aed12018-08-03 18:25:52 +01001335 public static final int SOURCE_BOUND_MAX_IN_SURFACE = 0;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001336
1337 /**
1338 * A source bound that will extend as much as possible, while remaining within the
1339 * visible region of the magnified view, as determined by
1340 * {@link View#getGlobalVisibleRect(Rect)}.
1341 */
Mihai Popa520e44752019-01-29 21:26:26 +00001342 public static final int SOURCE_BOUND_MAX_VISIBLE = 1;
Mihai Popa3e1aed12018-08-03 18:25:52 +01001343
1344
1345 /**
1346 * Used to describe the {@link Surface} rectangle where the magnifier's content is allowed
1347 * to be copied from. For more details, see method
1348 * {@link Magnifier.Builder#setSourceBounds(int, int, int, int)}
1349 *
1350 * @hide
1351 */
Mihai Popa520e44752019-01-29 21:26:26 +00001352 @IntDef({SOURCE_BOUND_MAX_IN_SURFACE, SOURCE_BOUND_MAX_VISIBLE})
Mihai Popa3e1aed12018-08-03 18:25:52 +01001353 @Retention(RetentionPolicy.SOURCE)
1354 public @interface SourceBound {}
1355
Mihai Popa1ddabb22018-08-06 11:55:54 +01001356 // The rest of the file consists of test APIs and methods relevant for tests.
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001357
1358 /**
1359 * See {@link #setOnOperationCompleteCallback(Callback)}.
1360 */
1361 @TestApi
1362 private Callback mCallback;
1363
1364 /**
1365 * Sets a callback which will be invoked at the end of the next
1366 * {@link #show(float, float)} or {@link #update()} operation.
1367 *
1368 * @hide
1369 */
1370 @TestApi
1371 public void setOnOperationCompleteCallback(final Callback callback) {
1372 mCallback = callback;
1373 if (mWindow != null) {
1374 mWindow.mCallback = callback;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001375 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01001376 }
Mihai Popa137b5842018-01-30 15:03:22 +00001377
1378 /**
Mihai Popa1ddabb22018-08-06 11:55:54 +01001379 * @return the drawing being currently displayed in the magnifier, as bitmap
Mihai Popa137b5842018-01-30 15:03:22 +00001380 *
1381 * @hide
1382 */
1383 @TestApi
Mihai Popa8b789102018-02-15 12:06:59 +00001384 public @Nullable Bitmap getContent() {
1385 if (mWindow == null) {
1386 return null;
1387 }
1388 synchronized (mWindow.mLock) {
Mihai Popa1ddabb22018-08-06 11:55:54 +01001389 return mWindow.mCurrentContent;
Mihai Popa8b789102018-02-15 12:06:59 +00001390 }
Mihai Popa137b5842018-01-30 15:03:22 +00001391 }
1392
1393 /**
Mihai Popa1ddabb22018-08-06 11:55:54 +01001394 * Returns a bitmap containing the content that was magnified and drew to the
1395 * magnifier, at its original size, without the overlay applied.
1396 * @return the content that is magnified, as bitmap
Mihai Popabeeaf552018-07-19 15:50:43 +01001397 *
1398 * @hide
1399 */
1400 @TestApi
1401 public @Nullable Bitmap getOriginalContent() {
1402 if (mWindow == null) {
1403 return null;
1404 }
1405 synchronized (mWindow.mLock) {
1406 return Bitmap.createBitmap(mWindow.mBitmap);
1407 }
1408 }
1409
1410 /**
Mihai Popa137b5842018-01-30 15:03:22 +00001411 * @return the size of the magnifier window in dp
1412 *
1413 * @hide
1414 */
1415 @TestApi
1416 public static PointF getMagnifierDefaultSize() {
1417 final Resources resources = Resources.getSystem();
1418 final float density = resources.getDisplayMetrics().density;
1419 final PointF size = new PointF();
Mihai Popac6950292018-11-15 21:32:42 +00001420 size.x = resources.getDimension(R.dimen.default_magnifier_width) / density;
1421 size.y = resources.getDimension(R.dimen.default_magnifier_height) / density;
Mihai Popa137b5842018-01-30 15:03:22 +00001422 return size;
1423 }
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001424
1425 /**
1426 * @hide
1427 */
1428 @TestApi
1429 public interface Callback {
1430 /**
1431 * Callback called after the drawing for a magnifier update has happened.
1432 */
1433 void onOperationComplete();
1434 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01001435}