blob: 16ddd0fc8247115e2787688ab80f275118eeddaf [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 Popa469aba82018-07-18 14:52:26 +010020import android.annotation.IntRange;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010021import android.annotation.NonNull;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000022import android.annotation.Nullable;
Mihai Popa469aba82018-07-18 14:52:26 +010023import android.annotation.Px;
Mihai Popa137b5842018-01-30 15:03:22 +000024import android.annotation.TestApi;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010025import android.annotation.UiThread;
26import android.content.Context;
Mihai Popa137b5842018-01-30 15:03:22 +000027import android.content.res.Resources;
Mihai Popafb4b6b82018-03-01 16:08:14 +000028import android.content.res.TypedArray;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010029import android.graphics.Bitmap;
Mihai Popad870b882018-02-27 14:25:52 +000030import android.graphics.Color;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000031import android.graphics.Outline;
32import android.graphics.Paint;
33import android.graphics.PixelFormat;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010034import android.graphics.Point;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +000035import android.graphics.PointF;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010036import android.graphics.Rect;
37import android.os.Handler;
Mihai Popa8b789102018-02-15 12:06:59 +000038import android.os.HandlerThread;
39import android.os.Message;
Mihai Popafb4b6b82018-03-01 16:08:14 +000040import android.view.ContextThemeWrapper;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000041import android.view.Display;
42import android.view.DisplayListCanvas;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010043import android.view.PixelCopy;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000044import android.view.RenderNode;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +000045import android.view.Surface;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000046import android.view.SurfaceControl;
Mihai Popa3589c2c2018-01-25 19:26:30 +000047import android.view.SurfaceHolder;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000048import android.view.SurfaceSession;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +000049import android.view.SurfaceView;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000050import android.view.ThreadedRenderer;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010051import android.view.View;
Mihai Popa3589c2c2018-01-25 19:26:30 +000052import android.view.ViewRootImpl;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010053
Mihai Popafb4b6b82018-03-01 16:08:14 +000054import com.android.internal.R;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010055import com.android.internal.util.Preconditions;
56
57/**
Andrei Stingaceanud6644dc2017-11-21 14:53:38 +000058 * Android magnifier widget. Can be used by any view which is attached to a window.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010059 */
Andrei Stingaceanud6644dc2017-11-21 14:53:38 +000060@UiThread
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010061public final class Magnifier {
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +000062 // Use this to specify that a previous configuration value does not exist.
63 private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
Mihai Popa8b789102018-02-15 12:06:59 +000064 // The callbacks of the pixel copy requests will be invoked on
65 // the Handler of this Thread when the copy is finished.
66 private static final HandlerThread sPixelCopyHandlerThread =
67 new HandlerThread("magnifier pixel copy result handler");
68
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +000069 // The view to which this magnifier is attached.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010070 private final View mView;
Mihai Popa1d1ed0c2018-01-12 12:38:12 +000071 // The coordinates of the view in the surface.
72 private final int[] mViewCoordinatesInSurface;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010073 // The window containing the magnifier.
Mihai Popa4bcd4d402018-02-07 17:13:51 +000074 private InternalPopupWindow mWindow;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +010075 // The width of the window containing the magnifier.
76 private final int mWindowWidth;
77 // The height of the window containing the magnifier.
78 private final int mWindowHeight;
Mihai Popa469aba82018-07-18 14:52:26 +010079 // The zoom applied to the view region copied to the magnifier view.
Mihai Popabeeaf552018-07-19 15:50:43 +010080 private float mZoom;
Mihai Popa7433a072018-07-18 12:18:34 +010081 // The width of the content that will be copied to the magnifier.
Mihai Popabeeaf552018-07-19 15:50:43 +010082 private int mSourceWidth;
Mihai Popa7433a072018-07-18 12:18:34 +010083 // The height of the content that will be copied to the magnifier.
Mihai Popabeeaf552018-07-19 15:50:43 +010084 private int mSourceHeight;
85 // Whether the zoom of the magnifier has changed since last content copy.
86 private boolean mDirtyZoom;
Mihai Popa4bcd4d402018-02-07 17:13:51 +000087 // The elevation of the window containing the magnifier.
88 private final float mWindowElevation;
Mihai Popafb4b6b82018-03-01 16:08:14 +000089 // The corner radius of the window containing the magnifier.
90 private final float mWindowCornerRadius;
Mihai Popa469aba82018-07-18 14:52:26 +010091 // The horizontal offset between the source and window coords when #show(float, float) is used.
92 private final int mDefaultHorizontalSourceToMagnifierOffset;
93 // The vertical offset between the source and window coords when #show(float, float) is used.
94 private final int mDefaultVerticalSourceToMagnifierOffset;
Mihai Popaf2980682018-04-30 19:08:57 +010095 // The parent surface for the magnifier surface.
96 private SurfaceInfo mParentSurface;
97 // The surface where the content will be copied from.
98 private SurfaceInfo mContentCopySurface;
99 // The center coordinates of the window containing the magnifier.
100 private final Point mWindowCoords = new Point();
101 // The center coordinates of the content to be magnified,
Mihai Popaf2980682018-04-30 19:08:57 +0100102 // clamped inside the visible region of the magnified view.
103 private final Point mClampedCenterZoomCoords = new Point();
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000104 // Variables holding previous states, used for detecting redundant calls and invalidation.
105 private final Point mPrevStartCoordsInSurface = new Point(
106 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
Mihai Popa7433a072018-07-18 12:18:34 +0100107 private final PointF mPrevShowSourceCoords = new PointF(
108 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
109 private final PointF mPrevShowWindowCoords = new PointF(
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000110 NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000111 // Rectangle defining the view surface area we pixel copy content from.
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000112 private final Rect mPixelCopyRequestRect = new Rect();
Mihai Popa39a71332018-02-22 19:30:24 +0000113 // Lock to synchronize between the UI thread and the thread that handles pixel copy results.
114 // Only sync mWindow writes from UI thread with mWindow reads from sPixelCopyHandlerThread.
115 private final Object mLock = new Object();
Andrei Stingaceanu15af5612017-10-13 12:53:23 +0100116
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100117 /**
118 * Initializes a magnifier.
119 *
120 * @param view the view for which this magnifier is attached
Mihai Popa469aba82018-07-18 14:52:26 +0100121 *
Mihai Popab6ca9092018-09-24 21:14:50 +0100122 * @deprecated Please use {@link Builder} instead
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100123 */
Mihai Popab6ca9092018-09-24 21:14:50 +0100124 @Deprecated
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100125 public Magnifier(@NonNull View view) {
Mihai Popa469aba82018-07-18 14:52:26 +0100126 this(new Builder(view));
127 }
128
129 private Magnifier(@NonNull Builder params) {
130 // Copy params from builder.
131 mView = params.mView;
132 mWindowWidth = params.mWidth;
133 mWindowHeight = params.mHeight;
134 mZoom = params.mZoom;
Mihai Popa7433a072018-07-18 12:18:34 +0100135 mSourceWidth = Math.round(mWindowWidth / mZoom);
136 mSourceHeight = Math.round(mWindowHeight / mZoom);
Mihai Popa469aba82018-07-18 14:52:26 +0100137 mWindowElevation = params.mElevation;
138 mWindowCornerRadius = params.mCornerRadius;
139 mDefaultHorizontalSourceToMagnifierOffset =
140 params.mHorizontalDefaultSourceToMagnifierOffset;
141 mDefaultVerticalSourceToMagnifierOffset =
142 params.mVerticalDefaultSourceToMagnifierOffset;
Mihai Popa1d1ed0c2018-01-12 12:38:12 +0000143 // The view's surface coordinates will not be updated until the magnifier is first shown.
144 mViewCoordinatesInSurface = new int[2];
Mihai Popa8b789102018-02-15 12:06:59 +0000145 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100146
Mihai Popa8b789102018-02-15 12:06:59 +0000147 static {
148 sPixelCopyHandlerThread.start();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100149 }
150
151 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100152 * Shows the magnifier on the screen. The method takes the coordinates of the center
153 * of the content source going to be magnified and copied to the magnifier. The coordinates
154 * are relative to the top left corner of the magnified view. The magnifier will be
155 * positioned such that its center will be at the default offset from the center of the source.
156 * The default offset can be specified using the method
157 * {@link Builder#setDefaultSourceToMagnifierOffset(int, int)}. If the offset should
158 * be different across calls to this method, you should consider to use method
159 * {@link #show(float, float, float, float)} instead.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100160 *
Mihai Popac2e0bee2018-07-19 12:18:30 +0100161 * @param sourceCenterX horizontal coordinate of the source center, relative to the view
162 * @param sourceCenterY vertical coordinate of the source center, relative to the view
163 *
164 * @see Builder#setDefaultSourceToMagnifierOffset(int, int)
165 * @see Builder#getDefaultHorizontalSourceToMagnifierOffset()
166 * @see Builder#getDefaultVerticalSourceToMagnifierOffset()
167 * @see #show(float, float, float, float)
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100168 */
Mihai Popa7433a072018-07-18 12:18:34 +0100169 public void show(@FloatRange(from = 0) float sourceCenterX,
170 @FloatRange(from = 0) float sourceCenterY) {
Mihai Popa469aba82018-07-18 14:52:26 +0100171 show(sourceCenterX, sourceCenterY,
172 sourceCenterX + mDefaultHorizontalSourceToMagnifierOffset,
173 sourceCenterY + mDefaultVerticalSourceToMagnifierOffset);
Mihai Popa7433a072018-07-18 12:18:34 +0100174 }
175
176 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100177 * Shows the magnifier on the screen at a position that is independent from its content
178 * position. The first two arguments represent the coordinates of the center of the
179 * content source going to be magnified and copied to the magnifier. The last two arguments
180 * represent the coordinates of the center of the magnifier itself. All four coordinates
181 * are relative to the top left corner of the magnified view. If you consider using this
182 * method such that the offset between the source center and the magnifier center coordinates
183 * remains constant, you should consider using method {@link #show(float, float)} instead.
Mihai Popa7433a072018-07-18 12:18:34 +0100184 *
Mihai Popac2e0bee2018-07-19 12:18:30 +0100185 * @param sourceCenterX horizontal coordinate of the source center relative to the view
186 * @param sourceCenterY vertical coordinate of the source center, relative to the view
187 * @param magnifierCenterX horizontal coordinate of the magnifier center, relative to the view
188 * @param magnifierCenterY vertical coordinate of the magnifier center, relative to the view
Mihai Popa7433a072018-07-18 12:18:34 +0100189 */
190 public void show(@FloatRange(from = 0) float sourceCenterX,
191 @FloatRange(from = 0) float sourceCenterY,
192 float magnifierCenterX, float magnifierCenterY) {
193 sourceCenterX = Math.max(0, Math.min(sourceCenterX, mView.getWidth()));
194 sourceCenterY = Math.max(0, Math.min(sourceCenterY, mView.getHeight()));
Andrei Stingaceanu451f9472017-10-13 16:41:28 +0100195
Mihai Popaf2980682018-04-30 19:08:57 +0100196 obtainSurfaces();
Mihai Popa7433a072018-07-18 12:18:34 +0100197 obtainContentCoordinates(sourceCenterX, sourceCenterY);
198 obtainWindowCoordinates(magnifierCenterX, magnifierCenterY);
Andrei Stingaceanuca189fe2017-10-19 17:02:22 +0100199
Mihai Popa7433a072018-07-18 12:18:34 +0100200 final int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2;
201 final int startY = mClampedCenterZoomCoords.y - mSourceHeight / 2;
Mihai Popabeeaf552018-07-19 15:50:43 +0100202 if (sourceCenterX != mPrevShowSourceCoords.x || sourceCenterY != mPrevShowSourceCoords.y
203 || mDirtyZoom) {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000204 if (mWindow == null) {
Mihai Popa39a71332018-02-22 19:30:24 +0000205 synchronized (mLock) {
206 mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(),
Mihai Popaf2980682018-04-30 19:08:57 +0100207 mParentSurface.mSurface,
Mihai Popafb4b6b82018-03-01 16:08:14 +0000208 mWindowWidth, mWindowHeight, mWindowElevation, mWindowCornerRadius,
Mihai Popa39a71332018-02-22 19:30:24 +0000209 Handler.getMain() /* draw the magnifier on the UI thread */, mLock,
210 mCallback);
211 }
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000212 }
213 performPixelCopy(startX, startY, true /* update window position */);
Mihai Popa7433a072018-07-18 12:18:34 +0100214 } else if (magnifierCenterX != mPrevShowWindowCoords.x
215 || magnifierCenterY != mPrevShowWindowCoords.y) {
216 final Point windowCoords = getCurrentClampedWindowCoordinates();
217 final InternalPopupWindow currentWindowInstance = mWindow;
218 sPixelCopyHandlerThread.getThreadHandler().post(() -> {
Mihai Popa7433a072018-07-18 12:18:34 +0100219 synchronized (mLock) {
Mihai Popaddcd54812018-09-03 17:25:54 +0100220 if (mWindow != currentWindowInstance) {
221 // The magnifier was dismissed (and maybe shown again) in the meantime.
222 return;
223 }
Mihai Popa7433a072018-07-18 12:18:34 +0100224 mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
225 }
226 });
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100227 }
Mihai Popa7433a072018-07-18 12:18:34 +0100228 mPrevShowSourceCoords.x = sourceCenterX;
229 mPrevShowSourceCoords.y = sourceCenterY;
230 mPrevShowWindowCoords.x = magnifierCenterX;
231 mPrevShowWindowCoords.y = magnifierCenterY;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100232 }
233
234 /**
Andrei Stingaceanud6644dc2017-11-21 14:53:38 +0000235 * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op.
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100236 */
237 public void dismiss() {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000238 if (mWindow != null) {
Mihai Popa39a71332018-02-22 19:30:24 +0000239 synchronized (mLock) {
240 mWindow.destroy();
241 mWindow = null;
242 }
Mihai Popa7433a072018-07-18 12:18:34 +0100243 mPrevShowSourceCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
244 mPrevShowSourceCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
245 mPrevShowWindowCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
246 mPrevShowWindowCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
Mihai Popa953b1342018-03-21 18:05:13 +0000247 mPrevStartCoordsInSurface.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
248 mPrevStartCoordsInSurface.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000249 }
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000250 }
Andrei Stingaceanu15af5612017-10-13 12:53:23 +0100251
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000252 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100253 * Asks the magnifier to update its content. It uses the previous coordinates passed to
254 * {@link #show(float, float)} or {@link #show(float, float, float, float)}. The
255 * method only has effect if the magnifier is currently showing.
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000256 */
257 public void update() {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000258 if (mWindow != null) {
Mihai Popaf2980682018-04-30 19:08:57 +0100259 obtainSurfaces();
Mihai Popabeeaf552018-07-19 15:50:43 +0100260 if (!mDirtyZoom) {
261 // Update the content shown in the magnifier.
262 performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y,
263 false /* update window position */);
264 } else {
265 // If the zoom has changed, we cannot use the same top left coordinates
266 // as before, so just #show again to have them recomputed.
267 show(mPrevShowSourceCoords.x, mPrevShowSourceCoords.y,
268 mPrevShowWindowCoords.x, mPrevShowWindowCoords.y);
269 }
Andrei Stingaceanu15af5612017-10-13 12:53:23 +0100270 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100271 }
272
Mihai Popa17ea3052018-03-06 14:24:07 +0000273 /**
Mihai Popa469aba82018-07-18 14:52:26 +0100274 * @return the width of the magnifier window, in pixels
Mihai Popac2e0bee2018-07-19 12:18:30 +0100275 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa17ea3052018-03-06 14:24:07 +0000276 */
Mihai Popac2e0bee2018-07-19 12:18:30 +0100277 @Px
Mihai Popa17ea3052018-03-06 14:24:07 +0000278 public int getWidth() {
279 return mWindowWidth;
280 }
281
282 /**
Mihai Popa469aba82018-07-18 14:52:26 +0100283 * @return the height of the magnifier window, in pixels
Mihai Popac2e0bee2018-07-19 12:18:30 +0100284 * @see Magnifier.Builder#setSize(int, int)
Mihai Popa17ea3052018-03-06 14:24:07 +0000285 */
Mihai Popac2e0bee2018-07-19 12:18:30 +0100286 @Px
Mihai Popa17ea3052018-03-06 14:24:07 +0000287 public int getHeight() {
288 return mWindowHeight;
289 }
290
291 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100292 * @return the initial width of the content magnified and copied to the magnifier, in pixels
293 * @see Magnifier.Builder#setSize(int, int)
294 * @see Magnifier.Builder#setZoom(float)
295 */
296 @Px
297 public int getSourceWidth() {
298 return mSourceWidth;
299 }
300
301 /**
302 * @return the initial height of the content magnified and copied to the magnifier, in pixels
303 * @see Magnifier.Builder#setSize(int, int)
304 * @see Magnifier.Builder#setZoom(float)
305 */
306 @Px
307 public int getSourceHeight() {
308 return mSourceHeight;
309 }
310
311 /**
Mihai Popabeeaf552018-07-19 15:50:43 +0100312 * Sets the zoom to be applied to the chosen content before being copied to the magnifier popup.
313 * @param zoom the zoom to be set
314 */
315 public void setZoom(@FloatRange(from = 0f) float zoom) {
316 Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
317 mZoom = zoom;
318 mSourceWidth = Math.round(mWindowWidth / mZoom);
319 mSourceHeight = Math.round(mWindowHeight / mZoom);
320 mDirtyZoom = true;
321 }
322
323 /**
Mihai Popa469aba82018-07-18 14:52:26 +0100324 * Returns the zoom to be applied to the magnified view region copied to the magnifier.
Mihai Popa17ea3052018-03-06 14:24:07 +0000325 * If the zoom is x and the magnifier window size is (width, height), the original size
Mihai Popa469aba82018-07-18 14:52:26 +0100326 * of the content being magnified will be (width / x, height / x).
327 * @return the zoom applied to the content
Mihai Popac2e0bee2018-07-19 12:18:30 +0100328 * @see Magnifier.Builder#setZoom(float)
Mihai Popa17ea3052018-03-06 14:24:07 +0000329 */
330 public float getZoom() {
331 return mZoom;
332 }
333
Mihai Popa63ee7f12018-04-05 12:01:53 +0100334 /**
Mihai Popac2e0bee2018-07-19 12:18:30 +0100335 * @return the elevation set for the magnifier window, in pixels
336 * @see Magnifier.Builder#setElevation(float)
337 */
338 @Px
339 public float getElevation() {
340 return mWindowElevation;
341 }
342
343 /**
344 * @return the corner radius of the magnifier window, in pixels
345 * @see Magnifier.Builder#setCornerRadius(float)
346 */
347 @Px
348 public float getCornerRadius() {
349 return mWindowCornerRadius;
350 }
351
352 /**
353 * Returns the horizontal offset, in pixels, to be applied to the source center position
354 * to obtain the magnifier center position when {@link #show(float, float)} is called.
355 * The value is ignored when {@link #show(float, float, float, float)} is used instead.
Mihai Popa0450a162018-04-27 13:09:12 +0100356 *
Mihai Popac2e0bee2018-07-19 12:18:30 +0100357 * @return the default horizontal offset between the source center and the magnifier
358 * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
359 * @see Magnifier#show(float, float)
360 */
361 @Px
362 public int getDefaultHorizontalSourceToMagnifierOffset() {
363 return mDefaultHorizontalSourceToMagnifierOffset;
364 }
365
366 /**
367 * Returns the vertical offset, in pixels, to be applied to the source center position
368 * to obtain the magnifier center position when {@link #show(float, float)} is called.
369 * The value is ignored when {@link #show(float, float, float, float)} is used instead.
370 *
371 * @return the default vertical offset between the source center and the magnifier
372 * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
373 * @see Magnifier#show(float, float)
374 */
375 @Px
376 public int getDefaultVerticalSourceToMagnifierOffset() {
377 return mDefaultVerticalSourceToMagnifierOffset;
378 }
379
380 /**
381 * Returns the top left coordinates of the magnifier, relative to the surface of the
382 * main application window. They will be determined by the coordinates of the last
383 * {@link #show(float, float)} or {@link #show(float, float, float, float)} call, adjusted
384 * to take into account any potential clamping behavior. The method can be used immediately
385 * after a #show call to find out where the magnifier will be positioned. However, the
386 * position of the magnifier will not be updated in the same frame due to the async
387 * copying of the content copying and of the magnifier rendering.
388 * The method will return {@code null} if #show has not yet been called, or if the last
389 * operation performed was a #dismiss.
390 *
391 * @return the top left coordinates of the magnifier
Mihai Popa63ee7f12018-04-05 12:01:53 +0100392 */
393 @Nullable
Mihai Popac2e0bee2018-07-19 12:18:30 +0100394 public Point getPosition() {
Mihai Popa63ee7f12018-04-05 12:01:53 +0100395 if (mWindow == null) {
396 return null;
397 }
Mihai Popac2e0bee2018-07-19 12:18:30 +0100398 return new Point(getCurrentClampedWindowCoordinates());
399 }
400
401 /**
402 * Returns the top left coordinates of the magnifier source (i.e. the view region going to
403 * be magnified and copied to the magnifier), relative to the surface the content is copied
404 * from. The content will be copied:
405 * - if the magnified view is a {@link SurfaceView}, from the surface backing it
406 * - otherwise, from the surface of the main application window
407 * The method will return {@code null} if #show has not yet been called, or if the last
408 * operation performed was a #dismiss.
409 *
410 * @return the top left coordinates of the magnifier source
411 */
412 @Nullable
413 public Point getSourcePosition() {
414 if (mWindow == null) {
415 return null;
416 }
417 return new Point(mPixelCopyRequestRect.left, mPixelCopyRequestRect.top);
Mihai Popa63ee7f12018-04-05 12:01:53 +0100418 }
419
Mihai Popaf2980682018-04-30 19:08:57 +0100420 /**
421 * Retrieves the surfaces used by the magnifier:
422 * - a parent surface for the magnifier surface. This will usually be the main app window.
423 * - a surface where the magnified content will be copied from. This will be the main app
424 * window unless the magnified view is a SurfaceView, in which case its backing surface
425 * will be used.
426 */
427 private void obtainSurfaces() {
428 // Get the main window surface.
429 SurfaceInfo validMainWindowSurface = SurfaceInfo.NULL;
Mihai Popa819e90d2018-04-16 14:27:05 +0100430 if (mView.getViewRootImpl() != null) {
Mihai Popaf2980682018-04-30 19:08:57 +0100431 final ViewRootImpl viewRootImpl = mView.getViewRootImpl();
432 final Surface mainWindowSurface = viewRootImpl.mSurface;
Mihai Popa819e90d2018-04-16 14:27:05 +0100433 if (mainWindowSurface != null && mainWindowSurface.isValid()) {
Mihai Popaf2980682018-04-30 19:08:57 +0100434 final Rect surfaceInsets = viewRootImpl.mWindowAttributes.surfaceInsets;
435 final int surfaceWidth =
436 viewRootImpl.getWidth() + surfaceInsets.left + surfaceInsets.right;
437 final int surfaceHeight =
438 viewRootImpl.getHeight() + surfaceInsets.top + surfaceInsets.bottom;
439 validMainWindowSurface =
440 new SurfaceInfo(mainWindowSurface, surfaceWidth, surfaceHeight, true);
Mihai Popa819e90d2018-04-16 14:27:05 +0100441 }
Mihai Popa17ea3052018-03-06 14:24:07 +0000442 }
Mihai Popaf2980682018-04-30 19:08:57 +0100443 // Get the surface backing the magnified view, if it is a SurfaceView.
444 SurfaceInfo validSurfaceViewSurface = SurfaceInfo.NULL;
Mihai Popa819e90d2018-04-16 14:27:05 +0100445 if (mView instanceof SurfaceView) {
Mihai Popaf2980682018-04-30 19:08:57 +0100446 final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder();
447 final Surface surfaceViewSurface = surfaceHolder.getSurface();
Mihai Popa819e90d2018-04-16 14:27:05 +0100448 if (surfaceViewSurface != null && surfaceViewSurface.isValid()) {
Mihai Popaf2980682018-04-30 19:08:57 +0100449 final Rect surfaceFrame = surfaceHolder.getSurfaceFrame();
450 validSurfaceViewSurface = new SurfaceInfo(surfaceViewSurface,
451 surfaceFrame.right, surfaceFrame.bottom, false);
Mihai Popa819e90d2018-04-16 14:27:05 +0100452 }
453 }
Mihai Popaf2980682018-04-30 19:08:57 +0100454
455 // Choose the parent surface for the magnifier and the source surface for the content.
456 mParentSurface = validMainWindowSurface != SurfaceInfo.NULL
457 ? validMainWindowSurface : validSurfaceViewSurface;
458 mContentCopySurface = mView instanceof SurfaceView
459 ? validSurfaceViewSurface : validMainWindowSurface;
Mihai Popa17ea3052018-03-06 14:24:07 +0000460 }
461
Mihai Popaf2980682018-04-30 19:08:57 +0100462 /**
463 * Computes the coordinates of the center of the content going to be displayed in the
464 * magnifier. These are relative to the surface the content is copied from.
465 */
466 private void obtainContentCoordinates(final float xPosInView, final float yPosInView) {
Mihai Popa819e90d2018-04-16 14:27:05 +0100467 mView.getLocationInSurface(mViewCoordinatesInSurface);
Mihai Popa7433a072018-07-18 12:18:34 +0100468 final int zoomCenterX;
469 final int zoomCenterY;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000470 if (mView instanceof SurfaceView) {
471 // No offset required if the backing Surface matches the size of the SurfaceView.
Mihai Popa7433a072018-07-18 12:18:34 +0100472 zoomCenterX = Math.round(xPosInView);
473 zoomCenterY = Math.round(yPosInView);
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000474 } else {
Mihai Popa7433a072018-07-18 12:18:34 +0100475 zoomCenterX = Math.round(xPosInView + mViewCoordinatesInSurface[0]);
476 zoomCenterY = Math.round(yPosInView + mViewCoordinatesInSurface[1]);
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000477 }
478
Mihai Popaf2980682018-04-30 19:08:57 +0100479 // Clamp the x location to avoid magnifying content which does not belong
480 // to the magnified view. This will not take into account overlapping views.
481 final Rect viewVisibleRegion = new Rect();
482 mView.getGlobalVisibleRect(viewVisibleRegion);
483 if (mView.getViewRootImpl() != null) {
484 // Clamping coordinates relative to the surface, not to the window.
485 final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets;
486 viewVisibleRegion.offset(surfaceInsets.left, surfaceInsets.top);
487 }
488 if (mView instanceof SurfaceView) {
489 // If we copy content from a SurfaceView, clamp coordinates relative to it.
490 viewVisibleRegion.offset(-mViewCoordinatesInSurface[0], -mViewCoordinatesInSurface[1]);
491 }
Mihai Popa7433a072018-07-18 12:18:34 +0100492 mClampedCenterZoomCoords.x = Math.max(viewVisibleRegion.left + mSourceWidth / 2, Math.min(
493 zoomCenterX, viewVisibleRegion.right - mSourceWidth / 2));
494 mClampedCenterZoomCoords.y = zoomCenterY;
Mihai Popaf2980682018-04-30 19:08:57 +0100495 }
496
Mihai Popa7433a072018-07-18 12:18:34 +0100497 /**
498 * Computes the coordinates of the top left corner of the magnifier window.
499 * These are relative to the surface the magnifier window is attached to.
500 */
501 private void obtainWindowCoordinates(final float xWindowPos, final float yWindowPos) {
502 final int windowCenterX;
503 final int windowCenterY;
504 if (mView instanceof SurfaceView) {
505 // No offset required if the backing Surface matches the size of the SurfaceView.
506 windowCenterX = Math.round(xWindowPos);
507 windowCenterY = Math.round(yWindowPos);
508 } else {
509 windowCenterX = Math.round(xWindowPos + mViewCoordinatesInSurface[0]);
510 windowCenterY = Math.round(yWindowPos + mViewCoordinatesInSurface[1]);
511 }
512
513 mWindowCoords.x = windowCenterX - mWindowWidth / 2;
514 mWindowCoords.y = windowCenterY - mWindowHeight / 2;
Mihai Popaf2980682018-04-30 19:08:57 +0100515 if (mParentSurface != mContentCopySurface) {
516 mWindowCoords.x += mViewCoordinatesInSurface[0];
517 mWindowCoords.y += mViewCoordinatesInSurface[1];
Mihai Popa819e90d2018-04-16 14:27:05 +0100518 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100519 }
520
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000521 private void performPixelCopy(final int startXInSurface, final int startYInSurface,
522 final boolean updateWindowPosition) {
Mihai Popaf2980682018-04-30 19:08:57 +0100523 if (mContentCopySurface.mSurface == null || !mContentCopySurface.mSurface.isValid()) {
Mihai Popa3589c2c2018-01-25 19:26:30 +0000524 return;
525 }
Mihai Popa3589c2c2018-01-25 19:26:30 +0000526 // Clamp copy coordinates inside the surface to avoid displaying distorted content.
527 final int clampedStartXInSurface = Math.max(0,
Mihai Popa7433a072018-07-18 12:18:34 +0100528 Math.min(startXInSurface, mContentCopySurface.mWidth - mSourceWidth));
Mihai Popa3589c2c2018-01-25 19:26:30 +0000529 final int clampedStartYInSurface = Math.max(0,
Mihai Popa7433a072018-07-18 12:18:34 +0100530 Math.min(startYInSurface, mContentCopySurface.mHeight - mSourceHeight));
Mihai Popa953b1342018-03-21 18:05:13 +0000531 // Clamp window coordinates inside the parent surface, to avoid displaying
532 // the magnifier out of screen or overlapping with system insets.
Mihai Popa7433a072018-07-18 12:18:34 +0100533 final Point windowCoords = getCurrentClampedWindowCoordinates();
Mihai Popa3589c2c2018-01-25 19:26:30 +0000534
535 // Perform the pixel copy.
536 mPixelCopyRequestRect.set(clampedStartXInSurface,
537 clampedStartYInSurface,
Mihai Popa7433a072018-07-18 12:18:34 +0100538 clampedStartXInSurface + mSourceWidth,
539 clampedStartYInSurface + mSourceHeight);
Mihai Popa39a71332018-02-22 19:30:24 +0000540 final InternalPopupWindow currentWindowInstance = mWindow;
Mihai Popa8b789102018-02-15 12:06:59 +0000541 final Bitmap bitmap =
Mihai Popa7433a072018-07-18 12:18:34 +0100542 Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ARGB_8888);
Mihai Popaf2980682018-04-30 19:08:57 +0100543 PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap,
Mihai Popa3589c2c2018-01-25 19:26:30 +0000544 result -> {
Mihai Popa39a71332018-02-22 19:30:24 +0000545 synchronized (mLock) {
546 if (mWindow != currentWindowInstance) {
547 // The magnifier was dismissed (and maybe shown again) in the meantime.
548 return;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000549 }
Mihai Popa39a71332018-02-22 19:30:24 +0000550 if (updateWindowPosition) {
551 // TODO: pull the position update outside #performPixelCopy
Mihai Popa7433a072018-07-18 12:18:34 +0100552 mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
Mihai Popa39a71332018-02-22 19:30:24 +0000553 }
554 mWindow.updateContent(bitmap);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000555 }
Mihai Popa3589c2c2018-01-25 19:26:30 +0000556 },
Mihai Popa8b789102018-02-15 12:06:59 +0000557 sPixelCopyHandlerThread.getThreadHandler());
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000558 mPrevStartCoordsInSurface.x = startXInSurface;
559 mPrevStartCoordsInSurface.y = startYInSurface;
Mihai Popabeeaf552018-07-19 15:50:43 +0100560 mDirtyZoom = false;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000561 }
562
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000563 /**
Mihai Popa7433a072018-07-18 12:18:34 +0100564 * Clamp window coordinates inside the surface the magnifier is attached to, to avoid
565 * displaying the magnifier out of screen or overlapping with system insets.
566 * @return the current window coordinates, after they are clamped inside the parent surface
567 */
568 private Point getCurrentClampedWindowCoordinates() {
569 final Rect windowBounds;
570 if (mParentSurface.mIsMainWindowSurface) {
571 final Rect systemInsets = mView.getRootWindowInsets().getSystemWindowInsets();
572 windowBounds = new Rect(systemInsets.left, systemInsets.top,
573 mParentSurface.mWidth - systemInsets.right,
574 mParentSurface.mHeight - systemInsets.bottom);
575 } else {
576 windowBounds = new Rect(0, 0, mParentSurface.mWidth, mParentSurface.mHeight);
577 }
578 final int windowCoordsX = Math.max(windowBounds.left,
579 Math.min(windowBounds.right - mWindowWidth, mWindowCoords.x));
580 final int windowCoordsY = Math.max(windowBounds.top,
581 Math.min(windowBounds.bottom - mWindowHeight, mWindowCoords.y));
582 return new Point(windowCoordsX, windowCoordsY);
583 }
584
585 /**
Mihai Popaf2980682018-04-30 19:08:57 +0100586 * Contains a surface and metadata corresponding to it.
587 */
588 private static class SurfaceInfo {
589 public static final SurfaceInfo NULL = new SurfaceInfo(null, 0, 0, false);
590
591 private Surface mSurface;
592 private int mWidth;
593 private int mHeight;
594 private boolean mIsMainWindowSurface;
595
596 SurfaceInfo(final Surface surface, final int width, final int height,
597 final boolean isMainWindowSurface) {
598 mSurface = surface;
599 mWidth = width;
600 mHeight = height;
601 mIsMainWindowSurface = isMainWindowSurface;
602 }
603 }
604
605 /**
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000606 * Magnifier's own implementation of PopupWindow-similar floating window.
607 * This exists to ensure frame-synchronization between window position updates and window
608 * content updates. By using a PopupWindow, these events would happen in different frames,
609 * producing a shakiness effect for the magnifier content.
610 */
611 private static class InternalPopupWindow {
Mihai Popad870b882018-02-27 14:25:52 +0000612 // The alpha set on the magnifier's content, which defines how
613 // prominent the white background is.
614 private static final int CONTENT_BITMAP_ALPHA = 242;
Mihai Popa819e90d2018-04-16 14:27:05 +0100615 // The z of the magnifier surface, defining its z order in the list of
616 // siblings having the same parent surface (usually the main app surface).
617 private static final int SURFACE_Z = 5;
Mihai Popad870b882018-02-27 14:25:52 +0000618
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000619 // Display associated to the view the magnifier is attached to.
620 private final Display mDisplay;
621 // The size of the content of the magnifier.
622 private final int mContentWidth;
623 private final int mContentHeight;
624 // The size of the allocated surface.
625 private final int mSurfaceWidth;
626 private final int mSurfaceHeight;
627 // The insets of the content inside the allocated surface.
628 private final int mOffsetX;
629 private final int mOffsetY;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000630 // The surface we allocate for the magnifier content + shadow.
631 private final SurfaceSession mSurfaceSession;
632 private final SurfaceControl mSurfaceControl;
633 private final Surface mSurface;
634 // The renderer used for the allocated surface.
635 private final ThreadedRenderer.SimpleRenderer mRenderer;
636 // The RenderNode used to draw the magnifier content in the surface.
637 private final RenderNode mBitmapRenderNode;
Mihai Popa8b789102018-02-15 12:06:59 +0000638 // The job that will be post'd to apply the pending magnifier updates to the surface.
639 private final Runnable mMagnifierUpdater;
640 // The handler where the magnifier updater jobs will be post'd.
641 private final Handler mHandler;
Mihai Popa63ee7f12018-04-05 12:01:53 +0100642 // The callback to be run after the next draw.
Mihai Popa2ba5d8e2018-02-20 18:50:20 +0000643 private Callback mCallback;
Mihai Popa63ee7f12018-04-05 12:01:53 +0100644 // The position of the magnifier content when the last draw was requested.
645 private int mLastDrawContentPositionX;
646 private int mLastDrawContentPositionY;
Mihai Popa8b789102018-02-15 12:06:59 +0000647
648 // Members below describe the state of the magnifier. Reads/writes to them
649 // have to be synchronized between the UI thread and the thread that handles
650 // the pixel copy results. This is the purpose of mLock.
Mihai Popa39a71332018-02-22 19:30:24 +0000651 private final Object mLock;
Mihai Popa8b789102018-02-15 12:06:59 +0000652 // Whether a magnifier frame draw is currently pending in the UI thread queue.
653 private boolean mFrameDrawScheduled;
654 // The content bitmap.
655 private Bitmap mBitmap;
656 // Whether the next draw will be the first one for the current instance.
657 private boolean mFirstDraw = true;
658 // The window position in the parent surface. Might be applied during the next draw,
659 // when mPendingWindowPositionUpdate is true.
660 private int mWindowPositionX;
661 private int mWindowPositionY;
662 private boolean mPendingWindowPositionUpdate;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000663
Mihai Popa5d983d22018-03-29 15:56:17 +0100664 // The lock used to synchronize the UI and render threads when a #destroy
665 // is performed on the UI thread and a frame callback on the render thread.
666 // When both mLock and mDestroyLock need to be held at the same time,
667 // mDestroyLock should be acquired before mLock in order to avoid deadlocks.
668 private final Object mDestroyLock = new Object();
669
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000670 InternalPopupWindow(final Context context, final Display display,
671 final Surface parentSurface,
Mihai Popafb4b6b82018-03-01 16:08:14 +0000672 final int width, final int height, final float elevation, final float cornerRadius,
Mihai Popa39a71332018-02-22 19:30:24 +0000673 final Handler handler, final Object lock, final Callback callback) {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000674 mDisplay = display;
Mihai Popa39a71332018-02-22 19:30:24 +0000675 mLock = lock;
Mihai Popa2ba5d8e2018-02-20 18:50:20 +0000676 mCallback = callback;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000677
678 mContentWidth = width;
679 mContentHeight = height;
680 mOffsetX = (int) (0.1f * width);
681 mOffsetY = (int) (0.1f * height);
682 // Setup the surface we will use for drawing the content and shadow.
683 mSurfaceWidth = mContentWidth + 2 * mOffsetX;
684 mSurfaceHeight = mContentHeight + 2 * mOffsetY;
685 mSurfaceSession = new SurfaceSession(parentSurface);
686 mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
687 .setFormat(PixelFormat.TRANSLUCENT)
688 .setSize(mSurfaceWidth, mSurfaceHeight)
689 .setName("magnifier surface")
690 .setFlags(SurfaceControl.HIDDEN)
691 .build();
692 mSurface = new Surface();
693 mSurface.copyFrom(mSurfaceControl);
694
695 // Setup the RenderNode tree. The root has only one child, which contains the bitmap.
696 mRenderer = new ThreadedRenderer.SimpleRenderer(
697 context,
698 "magnifier renderer",
699 mSurface
700 );
701 mBitmapRenderNode = createRenderNodeForBitmap(
702 "magnifier content",
Mihai Popafb4b6b82018-03-01 16:08:14 +0000703 elevation,
704 cornerRadius
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000705 );
Mihai Popa8b789102018-02-15 12:06:59 +0000706
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000707 final DisplayListCanvas canvas = mRenderer.getRootNode().start(width, height);
708 try {
709 canvas.insertReorderBarrier();
710 canvas.drawRenderNode(mBitmapRenderNode);
711 canvas.insertInorderBarrier();
712 } finally {
713 mRenderer.getRootNode().end(canvas);
714 }
Mihai Popa8b789102018-02-15 12:06:59 +0000715
716 // Initialize the update job and the handler where this will be post'd.
717 mHandler = handler;
718 mMagnifierUpdater = this::doDraw;
719 mFrameDrawScheduled = false;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000720 }
721
Mihai Popafb4b6b82018-03-01 16:08:14 +0000722 private RenderNode createRenderNodeForBitmap(final String name,
723 final float elevation, final float cornerRadius) {
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000724 final RenderNode bitmapRenderNode = RenderNode.create(name, null);
725
726 // Define the position of the bitmap in the parent render node. The surface regions
727 // outside the bitmap are used to draw elevation.
728 bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
729 mOffsetX + mContentWidth, mOffsetY + mContentHeight);
730 bitmapRenderNode.setElevation(elevation);
731
732 final Outline outline = new Outline();
Mihai Popafb4b6b82018-03-01 16:08:14 +0000733 outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000734 outline.setAlpha(1.0f);
735 bitmapRenderNode.setOutline(outline);
736 bitmapRenderNode.setClipToOutline(true);
737
738 // Create a dummy draw, which will be replaced later with real drawing.
739 final DisplayListCanvas canvas = bitmapRenderNode.start(mContentWidth, mContentHeight);
740 try {
741 canvas.drawColor(0xFF00FF00);
742 } finally {
743 bitmapRenderNode.end(canvas);
744 }
745
746 return bitmapRenderNode;
747 }
748
749 /**
750 * Sets the position of the magnifier content relative to the parent surface.
751 * The position update will happen in the same frame with the next draw.
Mihai Popa8b789102018-02-15 12:06:59 +0000752 * The method has to be called in a context that holds {@link #mLock}.
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000753 *
754 * @param contentX the x coordinate of the content
755 * @param contentY the y coordinate of the content
756 */
757 public void setContentPositionForNextDraw(final int contentX, final int contentY) {
758 mWindowPositionX = contentX - mOffsetX;
759 mWindowPositionY = contentY - mOffsetY;
760 mPendingWindowPositionUpdate = true;
Mihai Popa8b789102018-02-15 12:06:59 +0000761 requestUpdate();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000762 }
763
764 /**
765 * Sets the content that should be displayed in the magnifier.
766 * The update happens immediately, and possibly triggers a pending window movement set
767 * by {@link #setContentPositionForNextDraw(int, int)}.
Mihai Popa8b789102018-02-15 12:06:59 +0000768 * The method has to be called in a context that holds {@link #mLock}.
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000769 *
770 * @param bitmap the content bitmap
771 */
Mihai Popa8b789102018-02-15 12:06:59 +0000772 public void updateContent(final @NonNull Bitmap bitmap) {
773 if (mBitmap != null) {
774 mBitmap.recycle();
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000775 }
Mihai Popa8b789102018-02-15 12:06:59 +0000776 mBitmap = bitmap;
777 requestUpdate();
778 }
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000779
Mihai Popa8b789102018-02-15 12:06:59 +0000780 private void requestUpdate() {
781 if (mFrameDrawScheduled) {
782 return;
783 }
784 final Message request = Message.obtain(mHandler, mMagnifierUpdater);
785 request.setAsynchronous(true);
786 request.sendToTarget();
787 mFrameDrawScheduled = true;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000788 }
789
790 /**
791 * Destroys this instance.
792 */
793 public void destroy() {
Mihai Popa5d983d22018-03-29 15:56:17 +0100794 synchronized (mDestroyLock) {
795 mSurface.destroy();
796 }
Mihai Popa8b789102018-02-15 12:06:59 +0000797 synchronized (mLock) {
Mihai Popa95688002018-02-23 16:10:11 +0000798 mRenderer.destroy();
Mihai Popa95688002018-02-23 16:10:11 +0000799 mSurfaceControl.destroy();
800 mSurfaceSession.kill();
Mihai Popa8b789102018-02-15 12:06:59 +0000801 mHandler.removeCallbacks(mMagnifierUpdater);
802 if (mBitmap != null) {
803 mBitmap.recycle();
804 }
805 }
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000806 }
807
808 private void doDraw() {
809 final ThreadedRenderer.FrameDrawingCallback callback;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000810
Mihai Popa8b789102018-02-15 12:06:59 +0000811 // Draw the current bitmap to the surface, and prepare the callback which updates the
812 // surface position. These have to be in the same synchronized block, in order to
813 // guarantee the consistency between the bitmap content and the surface position.
814 synchronized (mLock) {
815 if (!mSurface.isValid()) {
816 // Probably #destroy() was called for the current instance, so we skip the draw.
817 return;
818 }
819
820 final DisplayListCanvas canvas =
821 mBitmapRenderNode.start(mContentWidth, mContentHeight);
822 try {
Mihai Popad870b882018-02-27 14:25:52 +0000823 canvas.drawColor(Color.WHITE);
824
Mihai Popa8b789102018-02-15 12:06:59 +0000825 final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
826 final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
827 final Paint paint = new Paint();
828 paint.setFilterBitmap(true);
Mihai Popad870b882018-02-27 14:25:52 +0000829 paint.setAlpha(CONTENT_BITMAP_ALPHA);
Mihai Popa8b789102018-02-15 12:06:59 +0000830 canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
831 } finally {
832 mBitmapRenderNode.end(canvas);
833 }
834
835 if (mPendingWindowPositionUpdate || mFirstDraw) {
836 // If the window has to be shown or moved, defer this until the next draw.
837 final boolean firstDraw = mFirstDraw;
838 mFirstDraw = false;
839 final boolean updateWindowPosition = mPendingWindowPositionUpdate;
840 mPendingWindowPositionUpdate = false;
841 final int pendingX = mWindowPositionX;
842 final int pendingY = mWindowPositionY;
843
844 callback = frame -> {
Mihai Popa5d983d22018-03-29 15:56:17 +0100845 synchronized (mDestroyLock) {
Mihai Popa95688002018-02-23 16:10:11 +0000846 if (!mSurface.isValid()) {
847 return;
848 }
Mihai Popa5d983d22018-03-29 15:56:17 +0100849 synchronized (mLock) {
850 mRenderer.setLightCenter(mDisplay, pendingX, pendingY);
851 // Show or move the window at the content draw frame.
852 SurfaceControl.openTransaction();
853 mSurfaceControl.deferTransactionUntil(mSurface, frame);
854 if (updateWindowPosition) {
855 mSurfaceControl.setPosition(pendingX, pendingY);
856 }
857 if (firstDraw) {
Mihai Popa819e90d2018-04-16 14:27:05 +0100858 mSurfaceControl.setLayer(SURFACE_Z);
Mihai Popa5d983d22018-03-29 15:56:17 +0100859 mSurfaceControl.show();
860 }
861 SurfaceControl.closeTransaction();
Mihai Popa95688002018-02-23 16:10:11 +0000862 }
Mihai Popa8b789102018-02-15 12:06:59 +0000863 }
Mihai Popa8b789102018-02-15 12:06:59 +0000864 };
865 } else {
866 callback = null;
867 }
868
Mihai Popa63ee7f12018-04-05 12:01:53 +0100869 mLastDrawContentPositionX = mWindowPositionX + mOffsetX;
870 mLastDrawContentPositionY = mWindowPositionY + mOffsetY;
Mihai Popa8b789102018-02-15 12:06:59 +0000871 mFrameDrawScheduled = false;
Mihai Popa4bcd4d402018-02-07 17:13:51 +0000872 }
873
874 mRenderer.draw(callback);
Mihai Popa2ba5d8e2018-02-20 18:50:20 +0000875 if (mCallback != null) {
876 mCallback.onOperationComplete();
877 }
878 }
879 }
880
Mihai Popa469aba82018-07-18 14:52:26 +0100881 /**
882 * Builder class for {@link Magnifier} objects.
883 */
884 public static class Builder {
885 private @NonNull View mView;
886 private @Px @IntRange(from = 0) int mWidth;
887 private @Px @IntRange(from = 0) int mHeight;
888 private float mZoom;
889 private @FloatRange(from = 0f) float mElevation;
890 private @FloatRange(from = 0f) float mCornerRadius;
891 private int mHorizontalDefaultSourceToMagnifierOffset;
892 private int mVerticalDefaultSourceToMagnifierOffset;
893
894 /**
895 * Construct a new builder for {@link Magnifier} objects.
896 * @param view the view this magnifier is attached to
897 */
898 public Builder(@NonNull View view) {
899 mView = Preconditions.checkNotNull(view);
900 applyDefaults();
901 }
902
903 private void applyDefaults() {
904 final Context context = mView.getContext();
905 final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Magnifier,
906 R.attr.magnifierStyle, 0);
907 mWidth = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierWidth, 0);
908 mHeight = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHeight, 0);
909 mElevation = a.getDimension(R.styleable.Magnifier_magnifierElevation, 0);
910 mCornerRadius = getDeviceDefaultDialogCornerRadius();
911 mZoom = a.getFloat(R.styleable.Magnifier_magnifierZoom, 0);
912 mHorizontalDefaultSourceToMagnifierOffset =
913 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHorizontalOffset, 0);
914 mVerticalDefaultSourceToMagnifierOffset =
915 a.getDimensionPixelSize(R.styleable.Magnifier_magnifierVerticalOffset, 0);
916 a.recycle();
917 }
918
919 /**
920 * Returns the device default theme dialog corner radius attribute.
921 * We retrieve this from the device default theme to avoid
922 * using the values set in the custom application themes.
923 */
924 private float getDeviceDefaultDialogCornerRadius() {
925 final Context deviceDefaultContext =
926 new ContextThemeWrapper(mView.getContext(), R.style.Theme_DeviceDefault);
927 final TypedArray ta = deviceDefaultContext.obtainStyledAttributes(
928 new int[]{android.R.attr.dialogCornerRadius});
929 final float dialogCornerRadius = ta.getDimension(0, 0);
930 ta.recycle();
931 return dialogCornerRadius;
932 }
933
934 /**
935 * Sets the size of the magnifier window, in pixels. Defaults to (100dp, 48dp).
936 * Note that the size of the content being magnified and copied to the magnifier
937 * will be computed as (window width / zoom, window height / zoom).
938 * @param width the window width to be set
939 * @param height the window height to be set
940 */
941 public Builder setSize(@Px @IntRange(from = 0) int width,
942 @Px @IntRange(from = 0) int height) {
943 Preconditions.checkArgumentPositive(width, "Width should be positive");
944 Preconditions.checkArgumentPositive(height, "Height should be positive");
945 mWidth = width;
946 mHeight = height;
947 return this;
948 }
949
950 /**
951 * Sets the zoom to be applied to the chosen content before being copied to the magnifier.
952 * A content of size (content_width, content_height) will be magnified to
953 * (content_width * zoom, content_height * zoom), which will coincide with the size
954 * of the magnifier. A zoom of 1 will translate to no magnification (the content will
955 * be just copied to the magnifier with no scaling). The zoom defaults to 1.25.
956 * @param zoom the zoom to be set
957 */
958 public Builder setZoom(@FloatRange(from = 0f) float zoom) {
959 Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
960 mZoom = zoom;
961 return this;
962 }
963
964 /**
965 * Sets the elevation of the magnifier window, in pixels. Defaults to 4dp.
966 * @param elevation the elevation to be set
967 */
968 public Builder setElevation(@Px @FloatRange(from = 0) float elevation) {
969 Preconditions.checkArgumentNonNegative(elevation, "Elevation should be non-negative");
970 mElevation = elevation;
971 return this;
972 }
973
974 /**
975 * Sets the corner radius of the magnifier window, in pixels.
976 * Defaults to the corner radius defined in the device default theme.
977 * @param cornerRadius the corner radius to be set
978 */
979 public Builder setCornerRadius(@Px @FloatRange(from = 0) float cornerRadius) {
980 Preconditions.checkArgumentNonNegative(cornerRadius,
981 "Corner radius should be non-negative");
982 mCornerRadius = cornerRadius;
983 return this;
984 }
985
986 /**
987 * Sets an offset, in pixels, that should be added to the content source center to obtain
988 * the position of the magnifier window, when the {@link #show(float, float)}
989 * method is called. The offset is ignored when {@link #show(float, float, float, float)}
990 * is used. The offset can be negative, and it defaults to (0dp, -42dp).
991 * @param horizontalOffset the horizontal component of the offset
992 * @param verticalOffset the vertical component of the offset
993 */
994 public Builder setDefaultSourceToMagnifierOffset(@Px int horizontalOffset,
995 @Px int verticalOffset) {
996 mHorizontalDefaultSourceToMagnifierOffset = horizontalOffset;
997 mVerticalDefaultSourceToMagnifierOffset = verticalOffset;
998 return this;
999 }
1000
1001 /**
1002 * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}.
1003 */
1004 public @NonNull Magnifier build() {
1005 return new Magnifier(this);
1006 }
1007 }
1008
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001009 // The rest of the file consists of test APIs.
1010
1011 /**
1012 * See {@link #setOnOperationCompleteCallback(Callback)}.
1013 */
1014 @TestApi
1015 private Callback mCallback;
1016
1017 /**
1018 * Sets a callback which will be invoked at the end of the next
1019 * {@link #show(float, float)} or {@link #update()} operation.
1020 *
1021 * @hide
1022 */
1023 @TestApi
1024 public void setOnOperationCompleteCallback(final Callback callback) {
1025 mCallback = callback;
1026 if (mWindow != null) {
1027 mWindow.mCallback = callback;
Mihai Popa4bcd4d402018-02-07 17:13:51 +00001028 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01001029 }
Mihai Popa137b5842018-01-30 15:03:22 +00001030
1031 /**
1032 * @return the content being currently displayed in the magnifier, as bitmap
1033 *
1034 * @hide
1035 */
1036 @TestApi
Mihai Popa8b789102018-02-15 12:06:59 +00001037 public @Nullable Bitmap getContent() {
1038 if (mWindow == null) {
1039 return null;
1040 }
1041 synchronized (mWindow.mLock) {
Mihai Popa17ea3052018-03-06 14:24:07 +00001042 return Bitmap.createScaledBitmap(mWindow.mBitmap, mWindowWidth, mWindowHeight, true);
Mihai Popa8b789102018-02-15 12:06:59 +00001043 }
Mihai Popa137b5842018-01-30 15:03:22 +00001044 }
1045
1046 /**
Mihai Popabeeaf552018-07-19 15:50:43 +01001047 * @return the content to be magnified, as bitmap
1048 *
1049 * @hide
1050 */
1051 @TestApi
1052 public @Nullable Bitmap getOriginalContent() {
1053 if (mWindow == null) {
1054 return null;
1055 }
1056 synchronized (mWindow.mLock) {
1057 return Bitmap.createBitmap(mWindow.mBitmap);
1058 }
1059 }
1060
1061 /**
Mihai Popa137b5842018-01-30 15:03:22 +00001062 * @return the size of the magnifier window in dp
1063 *
1064 * @hide
1065 */
1066 @TestApi
1067 public static PointF getMagnifierDefaultSize() {
1068 final Resources resources = Resources.getSystem();
1069 final float density = resources.getDisplayMetrics().density;
1070 final PointF size = new PointF();
Mihai Popafb4b6b82018-03-01 16:08:14 +00001071 size.x = resources.getDimension(R.dimen.magnifier_width) / density;
1072 size.y = resources.getDimension(R.dimen.magnifier_height) / density;
Mihai Popa137b5842018-01-30 15:03:22 +00001073 return size;
1074 }
Mihai Popa2ba5d8e2018-02-20 18:50:20 +00001075
1076 /**
1077 * @hide
1078 */
1079 @TestApi
1080 public interface Callback {
1081 /**
1082 * Callback called after the drawing for a magnifier update has happened.
1083 */
1084 void onOperationComplete();
1085 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01001086}