Automatic sources dropoff on 2020-06-10 18:32:38.095721
The change is generated with prebuilt drop tool.
Change-Id: I24cbf6ba6db262a1ae1445db1427a08fee35b3b4
diff --git a/android/widget/Magnifier.java b/android/widget/Magnifier.java
new file mode 100644
index 0000000..8ea824d
--- /dev/null
+++ b/android/widget/Magnifier.java
@@ -0,0 +1,1720 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Px;
+import android.annotation.TestApi;
+import android.annotation.UiThread;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.Display;
+import android.view.PixelCopy;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceHolder;
+import android.view.SurfaceSession;
+import android.view.SurfaceView;
+import android.view.ThreadedRenderer;
+import android.view.View;
+import android.view.ViewRootImpl;
+
+import com.android.internal.R;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Android magnifier widget. Can be used by any view which is attached to a window.
+ */
+@UiThread
+public final class Magnifier {
+ private static final String TAG = "Magnifier";
+ // Use this to specify that a previous configuration value does not exist.
+ private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
+ // The callbacks of the pixel copy requests will be invoked on
+ // the Handler of this Thread when the copy is finished.
+ private static final HandlerThread sPixelCopyHandlerThread =
+ new HandlerThread("magnifier pixel copy result handler");
+ // The width of the ramp region in DP on the left & right sides of the fish-eye effect.
+ private static final float FISHEYE_RAMP_WIDTH = 12f;
+
+ // The view to which this magnifier is attached.
+ private final View mView;
+ // The coordinates of the view in the surface.
+ private final int[] mViewCoordinatesInSurface;
+ // The window containing the magnifier.
+ private InternalPopupWindow mWindow;
+ // The width of the window containing the magnifier.
+ private final int mWindowWidth;
+ // The height of the window containing the magnifier.
+ private int mWindowHeight;
+ // The zoom applied to the view region copied to the magnifier view.
+ private float mZoom;
+ // The width of the content that will be copied to the magnifier.
+ private int mSourceWidth;
+ // The height of the content that will be copied to the magnifier.
+ private int mSourceHeight;
+ // Whether the zoom of the magnifier or the view position have changed since last content copy.
+ private boolean mDirtyState;
+ // The elevation of the window containing the magnifier.
+ private final float mWindowElevation;
+ // The corner radius of the window containing the magnifier.
+ private final float mWindowCornerRadius;
+ // The overlay to be drawn on the top of the magnifier content.
+ private final Drawable mOverlay;
+ // The horizontal offset between the source and window coords when #show(float, float) is used.
+ private final int mDefaultHorizontalSourceToMagnifierOffset;
+ // The vertical offset between the source and window coords when #show(float, float) is used.
+ private final int mDefaultVerticalSourceToMagnifierOffset;
+ // Whether the area where the magnifier can be positioned will be clipped to the main window
+ // and within system insets.
+ private final boolean mClippingEnabled;
+ // The behavior of the left bound of the rectangle where the content can be copied from.
+ private @SourceBound int mLeftContentBound;
+ // The behavior of the top bound of the rectangle where the content can be copied from.
+ private @SourceBound int mTopContentBound;
+ // The behavior of the right bound of the rectangle where the content can be copied from.
+ private @SourceBound int mRightContentBound;
+ // The behavior of the bottom bound of the rectangle where the content can be copied from.
+ private @SourceBound int mBottomContentBound;
+ // The parent surface for the magnifier surface.
+ private SurfaceInfo mParentSurface;
+ // The surface where the content will be copied from.
+ private SurfaceInfo mContentCopySurface;
+ // The center coordinates of the window containing the magnifier.
+ private final Point mWindowCoords = new Point();
+ // The center coordinates of the content to be magnified,
+ // clamped inside the visible region of the magnified view.
+ private final Point mClampedCenterZoomCoords = new Point();
+ // Variables holding previous states, used for detecting redundant calls and invalidation.
+ private final Point mPrevStartCoordsInSurface = new Point(
+ NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
+ private final PointF mPrevShowSourceCoords = new PointF(
+ NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
+ private final PointF mPrevShowWindowCoords = new PointF(
+ NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE);
+ // Rectangle defining the view surface area we pixel copy content from.
+ private final Rect mPixelCopyRequestRect = new Rect();
+ // Lock to synchronize between the UI thread and the thread that handles pixel copy results.
+ // Only sync mWindow writes from UI thread with mWindow reads from sPixelCopyHandlerThread.
+ private final Object mLock = new Object();
+ // The lock used to synchronize the UI and render threads when a #dismiss is performed.
+ private final Object mDestroyLock = new Object();
+
+ // Members for new styled magnifier (Eloquent style).
+
+ // Whether the magnifier is in new style.
+ private boolean mIsFishEyeStyle;
+ // The width of the cut region on the left edge of the pixel copy source rect.
+ private int mLeftCutWidth = 0;
+ // The width of the cut region on the right edge of the pixel copy source rect.
+ private int mRightCutWidth = 0;
+ // The horizontal bounds of the content source in pixels, relative to the view.
+ private int mLeftBound = Integer.MIN_VALUE;
+ private int mRightBound = Integer.MAX_VALUE;
+ // The width of the ramp region in pixels on the left & right sides of the fish-eye effect.
+ private final int mRamp;
+
+ /**
+ * Initializes a magnifier.
+ *
+ * @param view the view for which this magnifier is attached
+ *
+ * @deprecated Please use {@link Builder} instead
+ */
+ @Deprecated
+ public Magnifier(@NonNull View view) {
+ this(createBuilderWithOldMagnifierDefaults(view));
+ }
+
+ static Builder createBuilderWithOldMagnifierDefaults(final View view) {
+ final Builder params = new Builder(view);
+ final Context context = view.getContext();
+ final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Magnifier,
+ R.attr.magnifierStyle, 0);
+ params.mWidth = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierWidth, 0);
+ params.mHeight = a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHeight, 0);
+ params.mElevation = a.getDimension(R.styleable.Magnifier_magnifierElevation, 0);
+ params.mCornerRadius = getDeviceDefaultDialogCornerRadius(context);
+ params.mZoom = a.getFloat(R.styleable.Magnifier_magnifierZoom, 0);
+ params.mHorizontalDefaultSourceToMagnifierOffset =
+ a.getDimensionPixelSize(R.styleable.Magnifier_magnifierHorizontalOffset, 0);
+ params.mVerticalDefaultSourceToMagnifierOffset =
+ a.getDimensionPixelSize(R.styleable.Magnifier_magnifierVerticalOffset, 0);
+ params.mOverlay = new ColorDrawable(a.getColor(
+ R.styleable.Magnifier_magnifierColorOverlay, Color.TRANSPARENT));
+ a.recycle();
+ params.mClippingEnabled = true;
+ params.mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
+ params.mTopContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
+ params.mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
+ params.mBottomContentBound = SOURCE_BOUND_MAX_IN_SURFACE;
+ return params;
+ }
+
+ /**
+ * Returns the device default theme dialog corner radius attribute.
+ * We retrieve this from the device default theme to avoid
+ * using the values set in the custom application themes.
+ */
+ private static float getDeviceDefaultDialogCornerRadius(final Context context) {
+ final Context deviceDefaultContext =
+ new ContextThemeWrapper(context, R.style.Theme_DeviceDefault);
+ final TypedArray ta = deviceDefaultContext.obtainStyledAttributes(
+ new int[]{android.R.attr.dialogCornerRadius});
+ final float dialogCornerRadius = ta.getDimension(0, 0);
+ ta.recycle();
+ return dialogCornerRadius;
+ }
+
+ private Magnifier(@NonNull Builder params) {
+ // Copy params from builder.
+ mView = params.mView;
+ mWindowWidth = params.mWidth;
+ mWindowHeight = params.mHeight;
+ mZoom = params.mZoom;
+ mIsFishEyeStyle = params.mIsFishEyeStyle;
+ if (params.mSourceWidth > 0 && params.mSourceHeight > 0) {
+ mSourceWidth = params.mSourceWidth;
+ mSourceHeight = params.mSourceHeight;
+ } else {
+ mSourceWidth = Math.round(mWindowWidth / mZoom);
+ mSourceHeight = Math.round(mWindowHeight / mZoom);
+ }
+ mWindowElevation = params.mElevation;
+ mWindowCornerRadius = params.mCornerRadius;
+ mOverlay = params.mOverlay;
+ mDefaultHorizontalSourceToMagnifierOffset =
+ params.mHorizontalDefaultSourceToMagnifierOffset;
+ mDefaultVerticalSourceToMagnifierOffset =
+ params.mVerticalDefaultSourceToMagnifierOffset;
+ mClippingEnabled = params.mClippingEnabled;
+ mLeftContentBound = params.mLeftContentBound;
+ mTopContentBound = params.mTopContentBound;
+ mRightContentBound = params.mRightContentBound;
+ mBottomContentBound = params.mBottomContentBound;
+ // The view's surface coordinates will not be updated until the magnifier is first shown.
+ mViewCoordinatesInSurface = new int[2];
+ mRamp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, FISHEYE_RAMP_WIDTH,
+ mView.getContext().getResources().getDisplayMetrics());
+ }
+
+ static {
+ sPixelCopyHandlerThread.start();
+ }
+
+ /**
+ * Sets the horizontal bounds of the source when showing the magnifier.
+ * This is used for new style magnifier. e.g. limit the source bounds by the text line bounds.
+ *
+ * @param left the left of the bounds, relative to the view.
+ * @param right the right of the bounds, relative to the view.
+ */
+ void setSourceHorizontalBounds(int left, int right) {
+ mLeftBound = left;
+ mRightBound = right;
+ }
+
+ /**
+ * Shows the magnifier on the screen. The method takes the coordinates of the center
+ * of the content source going to be magnified and copied to the magnifier. The coordinates
+ * are relative to the top left corner of the magnified view. The magnifier will be
+ * positioned such that its center will be at the default offset from the center of the source.
+ * The default offset can be specified using the method
+ * {@link Builder#setDefaultSourceToMagnifierOffset(int, int)}. If the offset should
+ * be different across calls to this method, you should consider to use method
+ * {@link #show(float, float, float, float)} instead.
+ *
+ * @param sourceCenterX horizontal coordinate of the source center, relative to the view
+ * @param sourceCenterY vertical coordinate of the source center, relative to the view
+ *
+ * @see Builder#setDefaultSourceToMagnifierOffset(int, int)
+ * @see Builder#getDefaultHorizontalSourceToMagnifierOffset()
+ * @see Builder#getDefaultVerticalSourceToMagnifierOffset()
+ * @see #show(float, float, float, float)
+ */
+ public void show(@FloatRange(from = 0) float sourceCenterX,
+ @FloatRange(from = 0) float sourceCenterY) {
+ show(sourceCenterX, sourceCenterY,
+ sourceCenterX + mDefaultHorizontalSourceToMagnifierOffset,
+ sourceCenterY + mDefaultVerticalSourceToMagnifierOffset);
+ }
+
+ /**
+ * Shows the magnifier on the screen at a position that is independent from its content
+ * position. The first two arguments represent the coordinates of the center of the
+ * content source going to be magnified and copied to the magnifier. The last two arguments
+ * represent the coordinates of the center of the magnifier itself. All four coordinates
+ * are relative to the top left corner of the magnified view. If you consider using this
+ * method such that the offset between the source center and the magnifier center coordinates
+ * remains constant, you should consider using method {@link #show(float, float)} instead.
+ *
+ * @param sourceCenterX horizontal coordinate of the source center relative to the view
+ * @param sourceCenterY vertical coordinate of the source center, relative to the view
+ * @param magnifierCenterX horizontal coordinate of the magnifier center, relative to the view
+ * @param magnifierCenterY vertical coordinate of the magnifier center, relative to the view
+ */
+ public void show(@FloatRange(from = 0) float sourceCenterX,
+ @FloatRange(from = 0) float sourceCenterY,
+ float magnifierCenterX, float magnifierCenterY) {
+
+ obtainSurfaces();
+ obtainContentCoordinates(sourceCenterX, sourceCenterY);
+
+ int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2;
+ final int startY = mClampedCenterZoomCoords.y - mSourceHeight / 2;
+
+ if (mIsFishEyeStyle) {
+ // The magnifier center is the same as source center in new style.
+ magnifierCenterX = mClampedCenterZoomCoords.x - mViewCoordinatesInSurface[0];
+ magnifierCenterY = mClampedCenterZoomCoords.y - mViewCoordinatesInSurface[1];
+
+ // mLeftBound & mRightBound (typically the text line left/right) is for magnified
+ // content. However the PixelCopy requires the pre-magnified bounds.
+ // The below logic calculates the leftBound & rightBound for the pre-magnified bounds.
+ final float rampPre =
+ (mSourceWidth - (mSourceWidth - 2 * mRamp) / mZoom) / 2;
+
+ // Calculates the pre-zoomed left edge.
+ // The leftEdge moves from the left of view towards to sourceCenterX, considering the
+ // fisheye-like zooming.
+ final float x0 = sourceCenterX - mSourceWidth / 2;
+ final float rampX0 = x0 + mRamp;
+ float leftEdge = 0;
+ if (leftEdge > rampX0) {
+ // leftEdge is in the zoom range, the distance from leftEdge to sourceCenterX
+ // should reduce per mZoom.
+ leftEdge = sourceCenterX - (sourceCenterX - leftEdge) / mZoom;
+ } else if (leftEdge > x0) {
+ // leftEdge is in the ramp range, the distance from leftEdge to rampX0 should
+ // increase per ramp zoom (ramp / rampPre).
+ leftEdge = x0 + rampPre - (rampX0 - leftEdge) * rampPre / mRamp;
+ }
+ int leftBound = Math.min(Math.max((int) leftEdge, mLeftBound), mRightBound);
+
+ // Calculates the pre-zoomed right edge.
+ // The rightEdge moves from the right of view towards to sourceCenterX, considering the
+ // fisheye-like zooming.
+ final float x1 = sourceCenterX + mSourceWidth / 2;
+ final float rampX1 = x1 - mRamp;
+ float rightEdge = mView.getWidth();
+ if (rightEdge < rampX1) {
+ // rightEdge is in the zoom range, the distance from rightEdge to sourceCenterX
+ // should reduce per mZoom.
+ rightEdge = sourceCenterX + (rightEdge - sourceCenterX) / mZoom;
+ } else if (rightEdge < x1) {
+ // rightEdge is in the ramp range, the distance from rightEdge to rampX1 should
+ // increase per ramp zoom (ramp / rampPre).
+ rightEdge = x1 - rampPre + (rightEdge - rampX1) * rampPre / mRamp;
+ }
+ int rightBound = Math.max(leftBound, Math.min((int) rightEdge, mRightBound));
+
+ // Gets the startX for new style, which should be bounded by the horizontal bounds.
+ // Also calculates the left/right cut width for pixel copy.
+ leftBound = Math.max(leftBound + mViewCoordinatesInSurface[0], 0);
+ rightBound = Math.min(
+ rightBound + mViewCoordinatesInSurface[0], mContentCopySurface.mWidth);
+ mLeftCutWidth = Math.max(0, leftBound - startX);
+ mRightCutWidth = Math.max(0, startX + mSourceWidth - rightBound);
+ startX = Math.max(startX, leftBound);
+ }
+ obtainWindowCoordinates(magnifierCenterX, magnifierCenterY);
+
+ if (sourceCenterX != mPrevShowSourceCoords.x || sourceCenterY != mPrevShowSourceCoords.y
+ || mDirtyState) {
+ if (mWindow == null) {
+ synchronized (mLock) {
+ mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(),
+ mParentSurface.mSurfaceControl, mWindowWidth, mWindowHeight, mZoom,
+ mRamp, mWindowElevation, mWindowCornerRadius,
+ mOverlay != null ? mOverlay : new ColorDrawable(Color.TRANSPARENT),
+ Handler.getMain() /* draw the magnifier on the UI thread */, mLock,
+ mCallback, mIsFishEyeStyle);
+ }
+ }
+ performPixelCopy(startX, startY, true /* update window position */);
+ } else if (magnifierCenterX != mPrevShowWindowCoords.x
+ || magnifierCenterY != mPrevShowWindowCoords.y) {
+ final Point windowCoords = getCurrentClampedWindowCoordinates();
+ final InternalPopupWindow currentWindowInstance = mWindow;
+ sPixelCopyHandlerThread.getThreadHandler().post(() -> {
+ synchronized (mLock) {
+ if (mWindow != currentWindowInstance) {
+ // The magnifier was dismissed (and maybe shown again) in the meantime.
+ return;
+ }
+ mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y);
+ }
+ });
+ }
+ mPrevShowSourceCoords.x = sourceCenterX;
+ mPrevShowSourceCoords.y = sourceCenterY;
+ mPrevShowWindowCoords.x = magnifierCenterX;
+ mPrevShowWindowCoords.y = magnifierCenterY;
+ }
+
+ /**
+ * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op.
+ */
+ public void dismiss() {
+ if (mWindow != null) {
+ synchronized (mLock) {
+ mWindow.destroy();
+ mWindow = null;
+ }
+ mPrevShowSourceCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
+ mPrevShowSourceCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
+ mPrevShowWindowCoords.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
+ mPrevShowWindowCoords.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
+ mPrevStartCoordsInSurface.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
+ mPrevStartCoordsInSurface.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE;
+ }
+ }
+
+ /**
+ * Asks the magnifier to update its content. It uses the previous coordinates passed to
+ * {@link #show(float, float)} or {@link #show(float, float, float, float)}. The
+ * method only has effect if the magnifier is currently showing.
+ */
+ public void update() {
+ if (mWindow != null) {
+ obtainSurfaces();
+ if (!mDirtyState) {
+ // Update the content shown in the magnifier.
+ performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y,
+ false /* update window position */);
+ } else {
+ // If for example the zoom has changed, we cannot use the same top left
+ // coordinates as before, so just #show again to have them recomputed.
+ show(mPrevShowSourceCoords.x, mPrevShowSourceCoords.y,
+ mPrevShowWindowCoords.x, mPrevShowWindowCoords.y);
+ }
+ }
+ }
+
+ /**
+ * @return the width of the magnifier window, in pixels
+ * @see Magnifier.Builder#setSize(int, int)
+ */
+ @Px
+ public int getWidth() {
+ return mWindowWidth;
+ }
+
+ /**
+ * @return the height of the magnifier window, in pixels
+ * @see Magnifier.Builder#setSize(int, int)
+ */
+ @Px
+ public int getHeight() {
+ return mWindowHeight;
+ }
+
+ /**
+ * @return the initial width of the content magnified and copied to the magnifier, in pixels
+ * @see Magnifier.Builder#setSize(int, int)
+ * @see Magnifier.Builder#setInitialZoom(float)
+ */
+ @Px
+ public int getSourceWidth() {
+ return mSourceWidth;
+ }
+
+ /**
+ * @return the initial height of the content magnified and copied to the magnifier, in pixels
+ * @see Magnifier.Builder#setSize(int, int)
+ * @see Magnifier.Builder#setInitialZoom(float)
+ */
+ @Px
+ public int getSourceHeight() {
+ return mSourceHeight;
+ }
+
+ /**
+ * Sets the zoom to be applied to the chosen content before being copied to the magnifier popup.
+ * The change will become effective at the next #show or #update call.
+ * @param zoom the zoom to be set
+ */
+ public void setZoom(@FloatRange(from = 0f) float zoom) {
+ Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
+ mZoom = zoom;
+ mSourceWidth = mIsFishEyeStyle ? mWindowWidth : Math.round(mWindowWidth / mZoom);
+ mSourceHeight = Math.round(mWindowHeight / mZoom);
+ mDirtyState = true;
+ }
+
+ /**
+ * Updates the factors of source which may impact the magnifier's size.
+ * This can be called while the magnifier is showing and moving.
+ * @param sourceHeight the new source height.
+ * @param zoom the new zoom factor.
+ */
+ void updateSourceFactors(final int sourceHeight, final float zoom) {
+ mZoom = zoom;
+ mSourceHeight = sourceHeight;
+ mWindowHeight = (int) (sourceHeight * zoom);
+ if (mWindow != null) {
+ mWindow.updateContentFactors(mWindowHeight, zoom);
+ }
+ }
+
+ /**
+ * Returns the zoom to be applied to the magnified view region copied to the magnifier.
+ * If the zoom is x and the magnifier window size is (width, height), the original size
+ * of the content being magnified will be (width / x, height / x).
+ * @return the zoom applied to the content
+ * @see Magnifier.Builder#setInitialZoom(float)
+ */
+ public float getZoom() {
+ return mZoom;
+ }
+
+ /**
+ * @return the elevation set for the magnifier window, in pixels
+ * @see Magnifier.Builder#setElevation(float)
+ */
+ @Px
+ public float getElevation() {
+ return mWindowElevation;
+ }
+
+ /**
+ * @return the corner radius of the magnifier window, in pixels
+ * @see Magnifier.Builder#setCornerRadius(float)
+ */
+ @Px
+ public float getCornerRadius() {
+ return mWindowCornerRadius;
+ }
+
+ /**
+ * Returns the horizontal offset, in pixels, to be applied to the source center position
+ * to obtain the magnifier center position when {@link #show(float, float)} is called.
+ * The value is ignored when {@link #show(float, float, float, float)} is used instead.
+ *
+ * @return the default horizontal offset between the source center and the magnifier
+ * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
+ * @see Magnifier#show(float, float)
+ */
+ @Px
+ public int getDefaultHorizontalSourceToMagnifierOffset() {
+ return mDefaultHorizontalSourceToMagnifierOffset;
+ }
+
+ /**
+ * Returns the vertical offset, in pixels, to be applied to the source center position
+ * to obtain the magnifier center position when {@link #show(float, float)} is called.
+ * The value is ignored when {@link #show(float, float, float, float)} is used instead.
+ *
+ * @return the default vertical offset between the source center and the magnifier
+ * @see Magnifier.Builder#setDefaultSourceToMagnifierOffset(int, int)
+ * @see Magnifier#show(float, float)
+ */
+ @Px
+ public int getDefaultVerticalSourceToMagnifierOffset() {
+ return mDefaultVerticalSourceToMagnifierOffset;
+ }
+
+ /**
+ * Returns the overlay to be drawn on the top of the magnifier, or
+ * {@code null} if no overlay should be drawn.
+ * @return the overlay
+ * @see Magnifier.Builder#setOverlay(Drawable)
+ */
+ @Nullable
+ public Drawable getOverlay() {
+ return mOverlay;
+ }
+
+ /**
+ * Returns whether the magnifier position will be adjusted such that the magnifier will be
+ * fully within the bounds of the main application window, by also avoiding any overlap
+ * with system insets (such as the one corresponding to the status bar) i.e. whether the
+ * area where the magnifier can be positioned will be clipped to the main application window
+ * and the system insets.
+ * @return whether the magnifier position will be adjusted
+ * @see Magnifier.Builder#setClippingEnabled(boolean)
+ */
+ public boolean isClippingEnabled() {
+ return mClippingEnabled;
+ }
+
+ /**
+ * Returns the top left coordinates of the magnifier, relative to the main application
+ * window. They will be determined by the coordinates of the last {@link #show(float, float)}
+ * or {@link #show(float, float, float, float)} call, adjusted to take into account any
+ * potential clamping behavior. The method can be used immediately after a #show
+ * call to find out where the magnifier will be positioned. However, the position of the
+ * magnifier will not be updated visually in the same frame, due to the async nature of
+ * the content copying and of the magnifier rendering.
+ * The method will return {@code null} if #show has not yet been called, or if the last
+ * operation performed was a #dismiss.
+ *
+ * @return the top left coordinates of the magnifier
+ */
+ @Nullable
+ public Point getPosition() {
+ if (mWindow == null) {
+ return null;
+ }
+ final Point position = getCurrentClampedWindowCoordinates();
+ position.offset(-mParentSurface.mInsets.left, -mParentSurface.mInsets.top);
+ return new Point(position);
+ }
+
+ /**
+ * Returns the top left coordinates of the magnifier source (i.e. the view region going to
+ * be magnified and copied to the magnifier), relative to the window or surface the content
+ * is copied from. The content will be copied:
+ * - if the magnified view is a {@link SurfaceView}, from the surface backing it
+ * - otherwise, from the surface backing the main application window, and the coordinates
+ * returned will be relative to the main application window
+ * The method will return {@code null} if #show has not yet been called, or if the last
+ * operation performed was a #dismiss.
+ *
+ * @return the top left coordinates of the magnifier source
+ */
+ @Nullable
+ public Point getSourcePosition() {
+ if (mWindow == null) {
+ return null;
+ }
+ final Point position = new Point(mPixelCopyRequestRect.left, mPixelCopyRequestRect.top);
+ position.offset(-mContentCopySurface.mInsets.left, -mContentCopySurface.mInsets.top);
+ return new Point(position);
+ }
+
+ /**
+ * Retrieves the surfaces used by the magnifier:
+ * - a parent surface for the magnifier surface. This will usually be the main app window.
+ * - a surface where the magnified content will be copied from. This will be the main app
+ * window unless the magnified view is a SurfaceView, in which case its backing surface
+ * will be used.
+ */
+ private void obtainSurfaces() {
+ // Get the main window surface.
+ SurfaceInfo validMainWindowSurface = SurfaceInfo.NULL;
+ if (mView.getViewRootImpl() != null) {
+ final ViewRootImpl viewRootImpl = mView.getViewRootImpl();
+ final Surface mainWindowSurface = viewRootImpl.mSurface;
+ if (mainWindowSurface != null && mainWindowSurface.isValid()) {
+ final Rect surfaceInsets = viewRootImpl.mWindowAttributes.surfaceInsets;
+ final int surfaceWidth =
+ viewRootImpl.getWidth() + surfaceInsets.left + surfaceInsets.right;
+ final int surfaceHeight =
+ viewRootImpl.getHeight() + surfaceInsets.top + surfaceInsets.bottom;
+ validMainWindowSurface =
+ new SurfaceInfo(viewRootImpl.getSurfaceControl(), mainWindowSurface,
+ surfaceWidth, surfaceHeight, surfaceInsets, true);
+ }
+ }
+ // Get the surface backing the magnified view, if it is a SurfaceView.
+ SurfaceInfo validSurfaceViewSurface = SurfaceInfo.NULL;
+ if (mView instanceof SurfaceView) {
+ final SurfaceControl sc = ((SurfaceView) mView).getSurfaceControl();
+ final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder();
+ final Surface surfaceViewSurface = surfaceHolder.getSurface();
+
+ if (sc != null && sc.isValid()) {
+ final Rect surfaceFrame = surfaceHolder.getSurfaceFrame();
+ validSurfaceViewSurface = new SurfaceInfo(sc, surfaceViewSurface,
+ surfaceFrame.right, surfaceFrame.bottom, new Rect(), false);
+ }
+ }
+
+ // Choose the parent surface for the magnifier and the source surface for the content.
+ mParentSurface = validMainWindowSurface != SurfaceInfo.NULL
+ ? validMainWindowSurface : validSurfaceViewSurface;
+ mContentCopySurface = mView instanceof SurfaceView
+ ? validSurfaceViewSurface : validMainWindowSurface;
+ }
+
+ /**
+ * Computes the coordinates of the center of the content going to be displayed in the
+ * magnifier. These are relative to the surface the content is copied from.
+ */
+ private void obtainContentCoordinates(final float xPosInView, final float yPosInView) {
+ final int prevViewXInSurface = mViewCoordinatesInSurface[0];
+ final int prevViewYInSurface = mViewCoordinatesInSurface[1];
+ mView.getLocationInSurface(mViewCoordinatesInSurface);
+ if (mViewCoordinatesInSurface[0] != prevViewXInSurface
+ || mViewCoordinatesInSurface[1] != prevViewYInSurface) {
+ mDirtyState = true;
+ }
+
+ final int zoomCenterX;
+ final int zoomCenterY;
+ if (mView instanceof SurfaceView) {
+ // No offset required if the backing Surface matches the size of the SurfaceView.
+ zoomCenterX = Math.round(xPosInView);
+ zoomCenterY = Math.round(yPosInView);
+ } else {
+ zoomCenterX = Math.round(xPosInView + mViewCoordinatesInSurface[0]);
+ zoomCenterY = Math.round(yPosInView + mViewCoordinatesInSurface[1]);
+ }
+
+ final Rect[] bounds = new Rect[2]; // [MAX_IN_SURFACE, MAX_VISIBLE]
+ // Obtain the surface bounds rectangle.
+ final Rect surfaceBounds = new Rect(0, 0,
+ mContentCopySurface.mWidth, mContentCopySurface.mHeight);
+ bounds[0] = surfaceBounds;
+ // Obtain the visible view region rectangle.
+ final Rect viewVisibleRegion = new Rect();
+ mView.getGlobalVisibleRect(viewVisibleRegion);
+ if (mView.getViewRootImpl() != null) {
+ // Clamping coordinates relative to the surface, not to the window.
+ final Rect surfaceInsets = mView.getViewRootImpl().mWindowAttributes.surfaceInsets;
+ viewVisibleRegion.offset(surfaceInsets.left, surfaceInsets.top);
+ }
+ if (mView instanceof SurfaceView) {
+ // If we copy content from a SurfaceView, clamp coordinates relative to it.
+ viewVisibleRegion.offset(-mViewCoordinatesInSurface[0], -mViewCoordinatesInSurface[1]);
+ }
+ bounds[1] = viewVisibleRegion;
+
+ // Aggregate the above to obtain the bounds where the content copy will be restricted.
+ int resolvedLeft = Integer.MIN_VALUE;
+ for (int i = mLeftContentBound; i >= 0; --i) {
+ resolvedLeft = Math.max(resolvedLeft, bounds[i].left);
+ }
+ int resolvedTop = Integer.MIN_VALUE;
+ for (int i = mTopContentBound; i >= 0; --i) {
+ resolvedTop = Math.max(resolvedTop, bounds[i].top);
+ }
+ int resolvedRight = Integer.MAX_VALUE;
+ for (int i = mRightContentBound; i >= 0; --i) {
+ resolvedRight = Math.min(resolvedRight, bounds[i].right);
+ }
+ int resolvedBottom = Integer.MAX_VALUE;
+ for (int i = mBottomContentBound; i >= 0; --i) {
+ resolvedBottom = Math.min(resolvedBottom, bounds[i].bottom);
+ }
+ // Adjust <left-right> and <top-bottom> pairs of bounds to make sense.
+ resolvedLeft = Math.min(resolvedLeft, mContentCopySurface.mWidth - mSourceWidth);
+ resolvedTop = Math.min(resolvedTop, mContentCopySurface.mHeight - mSourceHeight);
+ if (resolvedLeft < 0 || resolvedTop < 0) {
+ Log.e(TAG, "Magnifier's content is copied from a surface smaller than"
+ + "the content requested size. The magnifier will be dismissed.");
+ }
+ resolvedRight = Math.max(resolvedRight, resolvedLeft + mSourceWidth);
+ resolvedBottom = Math.max(resolvedBottom, resolvedTop + mSourceHeight);
+
+ // Finally compute the coordinates of the source center.
+ mClampedCenterZoomCoords.x = mIsFishEyeStyle
+ ? Math.max(resolvedLeft, Math.min(zoomCenterX, resolvedRight))
+ : Math.max(resolvedLeft + mSourceWidth / 2, Math.min(
+ zoomCenterX, resolvedRight - mSourceWidth / 2));
+ mClampedCenterZoomCoords.y = Math.max(resolvedTop + mSourceHeight / 2, Math.min(
+ zoomCenterY, resolvedBottom - mSourceHeight / 2));
+ }
+
+ /**
+ * Computes the coordinates of the top left corner of the magnifier window.
+ * These are relative to the surface the magnifier window is attached to.
+ */
+ private void obtainWindowCoordinates(final float xWindowPos, final float yWindowPos) {
+ final int windowCenterX;
+ final int windowCenterY;
+ if (mView instanceof SurfaceView) {
+ // No offset required if the backing Surface matches the size of the SurfaceView.
+ windowCenterX = Math.round(xWindowPos);
+ windowCenterY = Math.round(yWindowPos);
+ } else {
+ windowCenterX = Math.round(xWindowPos + mViewCoordinatesInSurface[0]);
+ windowCenterY = Math.round(yWindowPos + mViewCoordinatesInSurface[1]);
+ }
+
+ mWindowCoords.x = windowCenterX - mWindowWidth / 2;
+ mWindowCoords.y = windowCenterY - mWindowHeight / 2;
+ if (mParentSurface != mContentCopySurface) {
+ mWindowCoords.x += mViewCoordinatesInSurface[0];
+ mWindowCoords.y += mViewCoordinatesInSurface[1];
+ }
+ }
+
+ private void performPixelCopy(final int startXInSurface, final int startYInSurface,
+ final boolean updateWindowPosition) {
+ if (mContentCopySurface.mSurface == null || !mContentCopySurface.mSurface.isValid()) {
+ onPixelCopyFailed();
+ return;
+ }
+
+ // Clamp window coordinates inside the parent surface, to avoid displaying
+ // the magnifier out of screen or overlapping with system insets.
+ final Point windowCoords = getCurrentClampedWindowCoordinates();
+
+ // Perform the pixel copy.
+ mPixelCopyRequestRect.set(startXInSurface,
+ startYInSurface,
+ startXInSurface + mSourceWidth - mLeftCutWidth - mRightCutWidth,
+ startYInSurface + mSourceHeight);
+ mPrevStartCoordsInSurface.x = startXInSurface;
+ mPrevStartCoordsInSurface.y = startYInSurface;
+ mDirtyState = false;
+
+ final InternalPopupWindow currentWindowInstance = mWindow;
+ if (mPixelCopyRequestRect.width() == 0) {
+ // If the copy rect is empty, updates an empty bitmap to the window.
+ mWindow.updateContent(
+ Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ALPHA_8));
+ return;
+ }
+ final Bitmap bitmap =
+ Bitmap.createBitmap(mSourceWidth - mLeftCutWidth - mRightCutWidth,
+ mSourceHeight, Bitmap.Config.ARGB_8888);
+ PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap,
+ result -> {
+ if (result != PixelCopy.SUCCESS) {
+ onPixelCopyFailed();
+ return;
+ }
+ synchronized (mLock) {
+ if (mWindow != currentWindowInstance) {
+ // The magnifier was dismissed (and maybe shown again) in the meantime.
+ return;
+ }
+ if (updateWindowPosition) {
+ // TODO: pull the position update outside #performPixelCopy
+ mWindow.setContentPositionForNextDraw(windowCoords.x,
+ windowCoords.y);
+ }
+ if (bitmap.getWidth() < mSourceWidth) {
+ // When bitmap width has been cut, re-fills it with full width bitmap.
+ // This only happens in new styled magnifier.
+ final Bitmap newBitmap = Bitmap.createBitmap(
+ mSourceWidth, bitmap.getHeight(), bitmap.getConfig());
+ final Canvas can = new Canvas(newBitmap);
+ final Rect dstRect = new Rect(mLeftCutWidth, 0,
+ mSourceWidth - mRightCutWidth, bitmap.getHeight());
+ can.drawBitmap(bitmap, null, dstRect, null);
+ mWindow.updateContent(newBitmap);
+ } else {
+ mWindow.updateContent(bitmap);
+ }
+ }
+ },
+ sPixelCopyHandlerThread.getThreadHandler());
+ }
+
+ private void onPixelCopyFailed() {
+ Log.e(TAG, "Magnifier failed to copy content from the view Surface. It will be dismissed.");
+ // Post to make sure #dismiss is done on the main thread.
+ Handler.getMain().postAtFrontOfQueue(() -> {
+ dismiss();
+ if (mCallback != null) {
+ mCallback.onOperationComplete();
+ }
+ });
+ }
+
+ /**
+ * Clamp window coordinates inside the surface the magnifier is attached to, to avoid
+ * displaying the magnifier out of screen or overlapping with system insets.
+ * @return the current window coordinates, after they are clamped inside the parent surface
+ */
+ private Point getCurrentClampedWindowCoordinates() {
+ if (!mClippingEnabled) {
+ // No position adjustment should be done, so return the raw coordinates.
+ return new Point(mWindowCoords);
+ }
+
+ final Rect windowBounds;
+ if (mParentSurface.mIsMainWindowSurface) {
+ final Insets systemInsets = mView.getRootWindowInsets().getSystemWindowInsets();
+ windowBounds = new Rect(
+ systemInsets.left + mParentSurface.mInsets.left,
+ systemInsets.top + mParentSurface.mInsets.top,
+ mParentSurface.mWidth - systemInsets.right - mParentSurface.mInsets.right,
+ mParentSurface.mHeight - systemInsets.bottom
+ - mParentSurface.mInsets.bottom
+ );
+ } else {
+ windowBounds = new Rect(0, 0, mParentSurface.mWidth, mParentSurface.mHeight);
+ }
+ final int windowCoordsX = Math.max(windowBounds.left,
+ Math.min(windowBounds.right - mWindowWidth, mWindowCoords.x));
+ final int windowCoordsY = Math.max(windowBounds.top,
+ Math.min(windowBounds.bottom - mWindowHeight, mWindowCoords.y));
+ return new Point(windowCoordsX, windowCoordsY);
+ }
+
+ /**
+ * Contains a surface and metadata corresponding to it.
+ */
+ private static class SurfaceInfo {
+ public static final SurfaceInfo NULL = new SurfaceInfo(null, null, 0, 0, null, false);
+
+ private Surface mSurface;
+ private SurfaceControl mSurfaceControl;
+ private int mWidth;
+ private int mHeight;
+ private Rect mInsets;
+ private boolean mIsMainWindowSurface;
+
+ SurfaceInfo(final SurfaceControl surfaceControl, final Surface surface,
+ final int width, final int height, final Rect insets,
+ final boolean isMainWindowSurface) {
+ mSurfaceControl = surfaceControl;
+ mSurface = surface;
+ mWidth = width;
+ mHeight = height;
+ mInsets = insets;
+ mIsMainWindowSurface = isMainWindowSurface;
+ }
+ }
+
+ /**
+ * Magnifier's own implementation of PopupWindow-similar floating window.
+ * This exists to ensure frame-synchronization between window position updates and window
+ * content updates. By using a PopupWindow, these events would happen in different frames,
+ * producing a shakiness effect for the magnifier content.
+ */
+ private static class InternalPopupWindow {
+ // The z of the magnifier surface, defining its z order in the list of
+ // siblings having the same parent surface (usually the main app surface).
+ private static final int SURFACE_Z = 5;
+
+ // Display associated to the view the magnifier is attached to.
+ private final Display mDisplay;
+ // The size of the content of the magnifier.
+ private final int mContentWidth;
+ private int mContentHeight;
+ // The insets of the content inside the allocated surface.
+ private final int mOffsetX;
+ private final int mOffsetY;
+ // The overlay to be drawn on the top of the content.
+ private final Drawable mOverlay;
+ // The surface we allocate for the magnifier content + shadow.
+ private final SurfaceSession mSurfaceSession;
+ private final SurfaceControl mSurfaceControl;
+ private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
+ private final Surface mSurface;
+ // The renderer used for the allocated surface.
+ private final ThreadedRenderer.SimpleRenderer mRenderer;
+ // The RenderNode used to draw the magnifier content in the surface.
+ private final RenderNode mBitmapRenderNode;
+ // The RenderNode used to draw the overlay over the magnifier content.
+ private final RenderNode mOverlayRenderNode;
+ // The job that will be post'd to apply the pending magnifier updates to the surface.
+ private final Runnable mMagnifierUpdater;
+ // The handler where the magnifier updater jobs will be post'd.
+ private final Handler mHandler;
+ // The callback to be run after the next draw.
+ private Callback mCallback;
+
+ // Members below describe the state of the magnifier. Reads/writes to them
+ // have to be synchronized between the UI thread and the thread that handles
+ // the pixel copy results. This is the purpose of mLock.
+ private final Object mLock;
+ // Whether a magnifier frame draw is currently pending in the UI thread queue.
+ private boolean mFrameDrawScheduled;
+ // The content bitmap, as returned by pixel copy.
+ private Bitmap mBitmap;
+ // Whether the next draw will be the first one for the current instance.
+ private boolean mFirstDraw = true;
+ // The window position in the parent surface. Might be applied during the next draw,
+ // when mPendingWindowPositionUpdate is true.
+ private int mWindowPositionX;
+ private int mWindowPositionY;
+ private boolean mPendingWindowPositionUpdate;
+
+ // The current content of the magnifier. It is mBitmap + mOverlay, only used for testing.
+ private Bitmap mCurrentContent;
+
+ private float mZoom;
+ // The width of the ramp region in pixels on the left & right sides of the fish-eye effect.
+ private final int mRamp;
+ // Whether is in the new magnifier style.
+ private boolean mIsFishEyeStyle;
+ // The mesh matrix for the fish-eye effect.
+ private float[] mMeshLeft;
+ private float[] mMeshRight;
+ private int mMeshWidth;
+ private int mMeshHeight;
+
+ InternalPopupWindow(final Context context, final Display display,
+ final SurfaceControl parentSurfaceControl, final int width, final int height,
+ final float zoom, final int ramp, final float elevation, final float cornerRadius,
+ final Drawable overlay, final Handler handler, final Object lock,
+ final Callback callback, final boolean isFishEyeStyle) {
+ mDisplay = display;
+ mOverlay = overlay;
+ mLock = lock;
+ mCallback = callback;
+
+ mContentWidth = width;
+ mContentHeight = height;
+ mZoom = zoom;
+ mRamp = ramp;
+ mOffsetX = (int) (1.05f * elevation);
+ mOffsetY = (int) (1.05f * elevation);
+ // Setup the surface we will use for drawing the content and shadow.
+ final int surfaceWidth = mContentWidth + 2 * mOffsetX;
+ final int surfaceHeight = mContentHeight + 2 * mOffsetY;
+ mSurfaceSession = new SurfaceSession();
+ mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
+ .setFormat(PixelFormat.TRANSLUCENT)
+ .setBufferSize(surfaceWidth, surfaceHeight)
+ .setName("magnifier surface")
+ .setFlags(SurfaceControl.HIDDEN)
+ .setParent(parentSurfaceControl)
+ .build();
+ mSurface = new Surface();
+ mSurface.copyFrom(mSurfaceControl);
+
+ // Setup the RenderNode tree. The root has two children, one containing the bitmap
+ // and one containing the overlay. We use a separate render node for the overlay
+ // to avoid drawing this as the same rate we do for content.
+ mRenderer = new ThreadedRenderer.SimpleRenderer(
+ context,
+ "magnifier renderer",
+ mSurface
+ );
+ mBitmapRenderNode = createRenderNodeForBitmap(
+ "magnifier content",
+ elevation,
+ cornerRadius
+ );
+ mOverlayRenderNode = createRenderNodeForOverlay(
+ "magnifier overlay",
+ cornerRadius
+ );
+ setupOverlay();
+
+ final RecordingCanvas canvas = mRenderer.getRootNode().beginRecording(width, height);
+ try {
+ canvas.enableZ();
+ canvas.drawRenderNode(mBitmapRenderNode);
+ canvas.disableZ();
+ canvas.drawRenderNode(mOverlayRenderNode);
+ canvas.disableZ();
+ } finally {
+ mRenderer.getRootNode().endRecording();
+ }
+ if (mCallback != null) {
+ mCurrentContent =
+ Bitmap.createBitmap(mContentWidth, mContentHeight, Bitmap.Config.ARGB_8888);
+ updateCurrentContentForTesting();
+ }
+
+ // Initialize the update job and the handler where this will be post'd.
+ mHandler = handler;
+ mMagnifierUpdater = this::doDraw;
+ mFrameDrawScheduled = false;
+ mIsFishEyeStyle = isFishEyeStyle;
+
+ if (mIsFishEyeStyle) {
+ createMeshMatrixForFishEyeEffect();
+ }
+ }
+
+ /**
+ * Updates the factors of content which may resize the window.
+ * @param contentHeight the new height of content.
+ * @param zoom the new zoom factor.
+ */
+ private void updateContentFactors(final int contentHeight, final float zoom) {
+ if (mContentHeight == contentHeight && mZoom == zoom) {
+ return;
+ }
+ if (mContentHeight < contentHeight) {
+ // Grows the surface height as necessary.
+ new SurfaceControl.Transaction().setBufferSize(
+ mSurfaceControl, mContentWidth, contentHeight).apply();
+ mSurface.copyFrom(mSurfaceControl);
+ mRenderer.setSurface(mSurface);
+
+ final Outline outline = new Outline();
+ outline.setRoundRect(0, 0, mContentWidth, contentHeight, 0);
+ outline.setAlpha(1.0f);
+
+ mBitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
+ mOffsetX + mContentWidth, mOffsetY + contentHeight);
+ mBitmapRenderNode.setOutline(outline);
+
+ mOverlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
+ mOffsetX + mContentWidth, mOffsetY + contentHeight);
+ mOverlayRenderNode.setOutline(outline);
+
+ final RecordingCanvas canvas =
+ mRenderer.getRootNode().beginRecording(mContentWidth, contentHeight);
+ try {
+ canvas.enableZ();
+ canvas.drawRenderNode(mBitmapRenderNode);
+ canvas.disableZ();
+ canvas.drawRenderNode(mOverlayRenderNode);
+ canvas.disableZ();
+ } finally {
+ mRenderer.getRootNode().endRecording();
+ }
+ }
+ mContentHeight = contentHeight;
+ mZoom = zoom;
+ fillMeshMatrix();
+ }
+
+ private void createMeshMatrixForFishEyeEffect() {
+ mMeshWidth = 1;
+ mMeshHeight = 6;
+ mMeshLeft = new float[2 * (mMeshWidth + 1) * (mMeshHeight + 1)];
+ mMeshRight = new float[2 * (mMeshWidth + 1) * (mMeshHeight + 1)];
+ fillMeshMatrix();
+ }
+
+ private void fillMeshMatrix() {
+ mMeshWidth = 1;
+ mMeshHeight = 6;
+ final float w = mContentWidth;
+ final float h = mContentHeight;
+ final float h0 = h / mZoom;
+ final float dh = h - h0;
+ for (int i = 0; i < 2 * (mMeshWidth + 1) * (mMeshHeight + 1); i += 2) {
+ // Calculates X value.
+ final int colIndex = i % (2 * (mMeshWidth + 1)) / 2;
+ mMeshLeft[i] = (float) colIndex * mRamp / mMeshWidth;
+ mMeshRight[i] = w - mRamp + colIndex * mRamp / mMeshWidth;
+
+ // Calculates Y value.
+ final int rowIndex = i / 2 / (mMeshWidth + 1);
+ final float hl = h0 + dh * colIndex / mMeshWidth;
+ final float yl = (h - hl) / 2;
+ mMeshLeft[i + 1] = yl + hl * rowIndex / mMeshHeight;
+ final float hr = h - dh * colIndex / mMeshWidth;
+ final float yr = (h - hr) / 2;
+ mMeshRight[i + 1] = yr + hr * rowIndex / mMeshHeight;
+ }
+ }
+
+ private RenderNode createRenderNodeForBitmap(final String name,
+ final float elevation, final float cornerRadius) {
+ final RenderNode bitmapRenderNode = RenderNode.create(name, null);
+
+ // Define the position of the bitmap in the parent render node. The surface regions
+ // outside the bitmap are used to draw elevation.
+ bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
+ mOffsetX + mContentWidth, mOffsetY + mContentHeight);
+ bitmapRenderNode.setElevation(elevation);
+
+ final Outline outline = new Outline();
+ outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
+ outline.setAlpha(1.0f);
+ bitmapRenderNode.setOutline(outline);
+ bitmapRenderNode.setClipToOutline(true);
+
+ // Create a dummy draw, which will be replaced later with real drawing.
+ final RecordingCanvas canvas = bitmapRenderNode.beginRecording(
+ mContentWidth, mContentHeight);
+ try {
+ canvas.drawColor(0xFF00FF00);
+ } finally {
+ bitmapRenderNode.endRecording();
+ }
+
+ return bitmapRenderNode;
+ }
+
+ private RenderNode createRenderNodeForOverlay(final String name, final float cornerRadius) {
+ final RenderNode overlayRenderNode = RenderNode.create(name, null);
+
+ // Define the position of the overlay in the parent render node.
+ // This coincides with the position of the content.
+ overlayRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY,
+ mOffsetX + mContentWidth, mOffsetY + mContentHeight);
+
+ final Outline outline = new Outline();
+ outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius);
+ outline.setAlpha(1.0f);
+ overlayRenderNode.setOutline(outline);
+ overlayRenderNode.setClipToOutline(true);
+
+ return overlayRenderNode;
+ }
+
+ private void setupOverlay() {
+ drawOverlay();
+
+ mOverlay.setCallback(new Drawable.Callback() {
+ @Override
+ public void invalidateDrawable(Drawable who) {
+ // When the overlay drawable is invalidated, redraw it to the render node.
+ drawOverlay();
+ if (mCallback != null) {
+ updateCurrentContentForTesting();
+ }
+ }
+
+ @Override
+ public void scheduleDrawable(Drawable who, Runnable what, long when) {
+ Handler.getMain().postAtTime(what, who, when);
+ }
+
+ @Override
+ public void unscheduleDrawable(Drawable who, Runnable what) {
+ Handler.getMain().removeCallbacks(what, who);
+ }
+ });
+ }
+
+ private void drawOverlay() {
+ // Draw the drawable to the render node. This happens once during
+ // initialization and whenever the overlay drawable is invalidated.
+ final RecordingCanvas canvas =
+ mOverlayRenderNode.beginRecording(mContentWidth, mContentHeight);
+ try {
+ mOverlay.setBounds(0, 0, mContentWidth, mContentHeight);
+ mOverlay.draw(canvas);
+ } finally {
+ mOverlayRenderNode.endRecording();
+ }
+ }
+
+ /**
+ * Sets the position of the magnifier content relative to the parent surface.
+ * The position update will happen in the same frame with the next draw.
+ * The method has to be called in a context that holds {@link #mLock}.
+ *
+ * @param contentX the x coordinate of the content
+ * @param contentY the y coordinate of the content
+ */
+ public void setContentPositionForNextDraw(final int contentX, final int contentY) {
+ mWindowPositionX = contentX - mOffsetX;
+ mWindowPositionY = contentY - mOffsetY;
+ mPendingWindowPositionUpdate = true;
+ requestUpdate();
+ }
+
+ /**
+ * Sets the content that should be displayed in the magnifier.
+ * The update happens immediately, and possibly triggers a pending window movement set
+ * by {@link #setContentPositionForNextDraw(int, int)}.
+ * The method has to be called in a context that holds {@link #mLock}.
+ *
+ * @param bitmap the content bitmap
+ */
+ public void updateContent(final @NonNull Bitmap bitmap) {
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ }
+ mBitmap = bitmap;
+ requestUpdate();
+ }
+
+ private void requestUpdate() {
+ if (mFrameDrawScheduled) {
+ return;
+ }
+ final Message request = Message.obtain(mHandler, mMagnifierUpdater);
+ request.setAsynchronous(true);
+ request.sendToTarget();
+ mFrameDrawScheduled = true;
+ }
+
+ /**
+ * Destroys this instance. The method has to be called in a context holding {@link #mLock}.
+ */
+ public void destroy() {
+ // Destroy the renderer. This will not proceed until pending frame callbacks complete.
+ mRenderer.destroy();
+ mSurface.destroy();
+ new SurfaceControl.Transaction().remove(mSurfaceControl).apply();
+ mSurfaceSession.kill();
+ mHandler.removeCallbacks(mMagnifierUpdater);
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ }
+ mOverlay.setCallback(null);
+ }
+
+ private void doDraw() {
+ final ThreadedRenderer.FrameDrawingCallback callback;
+
+ // Draw the current bitmap to the surface, and prepare the callback which updates the
+ // surface position. These have to be in the same synchronized block, in order to
+ // guarantee the consistency between the bitmap content and the surface position.
+ synchronized (mLock) {
+ if (!mSurface.isValid()) {
+ // Probably #destroy() was called for the current instance, so we skip the draw.
+ return;
+ }
+
+ final RecordingCanvas canvas =
+ mBitmapRenderNode.beginRecording(mContentWidth, mContentHeight);
+ try {
+ final int w = mBitmap.getWidth();
+ final int h = mBitmap.getHeight();
+ final Paint paint = new Paint();
+ paint.setFilterBitmap(true);
+ if (mIsFishEyeStyle) {
+ final int margin =
+ (int)((mContentWidth - (mContentWidth - 2 * mRamp) / mZoom) / 2);
+
+ // Draws the middle part.
+ final Rect srcRect = new Rect(margin, 0, w - margin, h);
+ final Rect dstRect = new Rect(
+ mRamp, 0, mContentWidth - mRamp, mContentHeight);
+ canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
+
+ // Draws the left/right parts with mesh matrixes.
+ canvas.drawBitmapMesh(
+ Bitmap.createBitmap(mBitmap, 0, 0, margin, h),
+ mMeshWidth, mMeshHeight, mMeshLeft, 0, null, 0, paint);
+ canvas.drawBitmapMesh(
+ Bitmap.createBitmap(mBitmap, w - margin, 0, margin, h),
+ mMeshWidth, mMeshHeight, mMeshRight, 0, null, 0, paint);
+ } else {
+ final Rect srcRect = new Rect(0, 0, w, h);
+ final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
+ canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
+ }
+ } finally {
+ mBitmapRenderNode.endRecording();
+ }
+ if (mPendingWindowPositionUpdate || mFirstDraw) {
+ // If the window has to be shown or moved, defer this until the next draw.
+ final boolean firstDraw = mFirstDraw;
+ mFirstDraw = false;
+ final boolean updateWindowPosition = mPendingWindowPositionUpdate;
+ mPendingWindowPositionUpdate = false;
+ final int pendingX = mWindowPositionX;
+ final int pendingY = mWindowPositionY;
+
+ callback = frame -> {
+ if (!mSurface.isValid()) {
+ return;
+ }
+ // Show or move the window at the content draw frame.
+ mTransaction.deferTransactionUntil(mSurfaceControl, mSurfaceControl,
+ frame);
+ if (updateWindowPosition) {
+ mTransaction.setPosition(mSurfaceControl, pendingX, pendingY);
+ }
+ if (firstDraw) {
+ mTransaction.setLayer(mSurfaceControl, SURFACE_Z)
+ .show(mSurfaceControl);
+
+ }
+ mTransaction.apply();
+ };
+ if (!mIsFishEyeStyle) {
+ // The new style magnifier doesn't need the light/shadow.
+ mRenderer.setLightCenter(mDisplay, pendingX, pendingY);
+ }
+ } else {
+ callback = null;
+ }
+
+ mFrameDrawScheduled = false;
+ }
+
+ mRenderer.draw(callback);
+ if (mCallback != null) {
+ // The current content bitmap is only used in testing, so, for performance,
+ // we only want to update it when running tests. For this, we check that
+ // mCallback is not null, as it can only be set from a @TestApi.
+ updateCurrentContentForTesting();
+ mCallback.onOperationComplete();
+ }
+ }
+
+ /**
+ * Updates mCurrentContent, which reproduces what is currently supposed to be
+ * drawn in the magnifier. mCurrentContent is only used for testing, so this method
+ * should only be called otherwise.
+ */
+ private void updateCurrentContentForTesting() {
+ final Canvas canvas = new Canvas(mCurrentContent);
+ final Rect bounds = new Rect(0, 0, mContentWidth, mContentHeight);
+ if (mBitmap != null && !mBitmap.isRecycled()) {
+ final Rect originalBounds = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
+ canvas.drawBitmap(mBitmap, originalBounds, bounds, null);
+ }
+ mOverlay.setBounds(bounds);
+ mOverlay.draw(canvas);
+ }
+ }
+
+ /**
+ * Builder class for {@link Magnifier} objects.
+ */
+ public static final class Builder {
+ private @NonNull View mView;
+ private @Px @IntRange(from = 0) int mWidth;
+ private @Px @IntRange(from = 0) int mHeight;
+ private float mZoom;
+ private @FloatRange(from = 0f) float mElevation;
+ private @FloatRange(from = 0f) float mCornerRadius;
+ private @Nullable Drawable mOverlay;
+ private int mHorizontalDefaultSourceToMagnifierOffset;
+ private int mVerticalDefaultSourceToMagnifierOffset;
+ private boolean mClippingEnabled;
+ private @SourceBound int mLeftContentBound;
+ private @SourceBound int mTopContentBound;
+ private @SourceBound int mRightContentBound;
+ private @SourceBound int mBottomContentBound;
+ private boolean mIsFishEyeStyle;
+ private int mSourceWidth;
+ private int mSourceHeight;
+
+ /**
+ * Construct a new builder for {@link Magnifier} objects.
+ * @param view the view this magnifier is attached to
+ */
+ public Builder(@NonNull View view) {
+ mView = Objects.requireNonNull(view);
+ applyDefaults();
+ }
+
+ private void applyDefaults() {
+ final Resources resources = mView.getContext().getResources();
+ mWidth = resources.getDimensionPixelSize(R.dimen.default_magnifier_width);
+ mHeight = resources.getDimensionPixelSize(R.dimen.default_magnifier_height);
+ mElevation = resources.getDimension(R.dimen.default_magnifier_elevation);
+ mCornerRadius = resources.getDimension(R.dimen.default_magnifier_corner_radius);
+ mZoom = resources.getFloat(R.dimen.default_magnifier_zoom);
+ mHorizontalDefaultSourceToMagnifierOffset =
+ resources.getDimensionPixelSize(R.dimen.default_magnifier_horizontal_offset);
+ mVerticalDefaultSourceToMagnifierOffset =
+ resources.getDimensionPixelSize(R.dimen.default_magnifier_vertical_offset);
+ mOverlay = new ColorDrawable(resources.getColor(
+ R.color.default_magnifier_color_overlay, null));
+ mClippingEnabled = true;
+ mLeftContentBound = SOURCE_BOUND_MAX_VISIBLE;
+ mTopContentBound = SOURCE_BOUND_MAX_VISIBLE;
+ mRightContentBound = SOURCE_BOUND_MAX_VISIBLE;
+ mBottomContentBound = SOURCE_BOUND_MAX_VISIBLE;
+ mIsFishEyeStyle = false;
+ }
+
+ /**
+ * Sets the size of the magnifier window, in pixels. Defaults to (100dp, 48dp).
+ * Note that the size of the content being magnified and copied to the magnifier
+ * will be computed as (window width / zoom, window height / zoom).
+ * @param width the window width to be set
+ * @param height the window height to be set
+ */
+ @NonNull
+ public Builder setSize(@Px @IntRange(from = 0) int width,
+ @Px @IntRange(from = 0) int height) {
+ Preconditions.checkArgumentPositive(width, "Width should be positive");
+ Preconditions.checkArgumentPositive(height, "Height should be positive");
+ mWidth = width;
+ mHeight = height;
+ return this;
+ }
+
+ /**
+ * Sets the zoom to be applied to the chosen content before being copied to the magnifier.
+ * A content of size (content_width, content_height) will be magnified to
+ * (content_width * zoom, content_height * zoom), which will coincide with the size
+ * of the magnifier. A zoom of 1 will translate to no magnification (the content will
+ * be just copied to the magnifier with no scaling). The zoom defaults to 1.25.
+ * Note that the zoom can also be changed after the instance is built, using the
+ * {@link Magnifier#setZoom(float)} method.
+ * @param zoom the zoom to be set
+ */
+ @NonNull
+ public Builder setInitialZoom(@FloatRange(from = 0f) float zoom) {
+ Preconditions.checkArgumentPositive(zoom, "Zoom should be positive");
+ mZoom = zoom;
+ return this;
+ }
+
+ /**
+ * Sets the elevation of the magnifier window, in pixels. Defaults to 4dp.
+ * @param elevation the elevation to be set
+ */
+ @NonNull
+ public Builder setElevation(@Px @FloatRange(from = 0) float elevation) {
+ Preconditions.checkArgumentNonNegative(elevation, "Elevation should be non-negative");
+ mElevation = elevation;
+ return this;
+ }
+
+ /**
+ * Sets the corner radius of the magnifier window, in pixels. Defaults to 2dp.
+ * @param cornerRadius the corner radius to be set
+ */
+ @NonNull
+ public Builder setCornerRadius(@Px @FloatRange(from = 0) float cornerRadius) {
+ Preconditions.checkArgumentNonNegative(cornerRadius,
+ "Corner radius should be non-negative");
+ mCornerRadius = cornerRadius;
+ return this;
+ }
+
+ /**
+ * Sets an overlay that will be drawn on the top of the magnifier.
+ * In general, the overlay should not be opaque, in order to let the magnified
+ * content be partially visible in the magnifier. The default overlay is {@code null}
+ * (no overlay). As an example, TextView applies a white {@link ColorDrawable}
+ * overlay with 5% alpha, aiming to make the magnifier distinguishable when shown in dark
+ * application regions. To disable the overlay, the parameter should be set
+ * to {@code null}. If not null, the overlay will be automatically redrawn
+ * when the drawable is invalidated. To achieve this, the magnifier will set a new
+ * {@link android.graphics.drawable.Drawable.Callback} for the overlay drawable,
+ * so keep in mind that any existing one set by the application will be lost.
+ * @param overlay the overlay to be drawn on top
+ */
+ @NonNull
+ public Builder setOverlay(@Nullable Drawable overlay) {
+ mOverlay = overlay;
+ return this;
+ }
+
+ /**
+ * Sets an offset that should be added to the content source center to obtain
+ * the position of the magnifier window, when the {@link #show(float, float)}
+ * method is called. The offset is ignored when {@link #show(float, float, float, float)}
+ * is used. The offset can be negative. It defaults to (0dp, 0dp).
+ * @param horizontalOffset the horizontal component of the offset
+ * @param verticalOffset the vertical component of the offset
+ */
+ @NonNull
+ public Builder setDefaultSourceToMagnifierOffset(@Px int horizontalOffset,
+ @Px int verticalOffset) {
+ mHorizontalDefaultSourceToMagnifierOffset = horizontalOffset;
+ mVerticalDefaultSourceToMagnifierOffset = verticalOffset;
+ return this;
+ }
+
+ /**
+ * Defines the behavior of the magnifier when it is requested to position outside the
+ * surface of the main application window. The default value is {@code true}, which means
+ * that the position will be adjusted such that the magnifier will be fully within the
+ * bounds of the main application window, while also avoiding any overlap with system insets
+ * (such as the one corresponding to the status bar). If this flag is set to {@code false},
+ * the area where the magnifier can be positioned will no longer be clipped, so the
+ * magnifier will be able to extend outside the main application window boundaries (and also
+ * overlap the system insets). This can be useful if you require a custom behavior, but it
+ * should be handled with care, when passing coordinates to {@link #show(float, float)};
+ * note that:
+ * <ul>
+ * <li>in a multiwindow context, if the magnifier crosses the boundary between the two
+ * windows, it will not be able to show over the window of the other application</li>
+ * <li>if the magnifier overlaps the status bar, there is no guarantee about which one
+ * will be displayed on top. This should be handled with care.</li>
+ * </ul>
+ * @param clip whether the magnifier position will be adjusted
+ */
+ @NonNull
+ public Builder setClippingEnabled(boolean clip) {
+ mClippingEnabled = clip;
+ return this;
+ }
+
+ /**
+ * Defines the bounds of the rectangle where the magnifier will be able to copy its content
+ * from. The content will always be copied from the {@link Surface} of the main application
+ * window unless the magnified view is a {@link SurfaceView}, in which case its backing
+ * surface will be used. Each bound can have a different behavior, with the options being:
+ * <ul>
+ * <li>{@link #SOURCE_BOUND_MAX_VISIBLE}, which extends the bound as much as possible
+ * while remaining in the visible region of the magnified view, as given by
+ * {@link android.view.View#getGlobalVisibleRect(Rect)}. For example, this will take into
+ * account the case when the view is contained in a scrollable container, and the
+ * magnifier will refuse to copy content outside of the visible view region</li>
+ * <li>{@link #SOURCE_BOUND_MAX_IN_SURFACE}, which extends the bound as much
+ * as possible while remaining inside the surface the content is copied from.</li>
+ * </ul>
+ * Note that if either of the first three options is used, the bound will be compared to
+ * the bound of the surface (i.e. as if {@link #SOURCE_BOUND_MAX_IN_SURFACE} was used),
+ * and the more restrictive one will be chosen. In other words, no attempt to copy content
+ * from outside the surface will be permitted. If two opposite bounds are not well-behaved
+ * (i.e. left + sourceWidth > right or top + sourceHeight > bottom), the left and top
+ * bounds will have priority and the others will be extended accordingly. If the pairs
+ * obtained this way still remain out of bounds, the smallest possible offset will be added
+ * to the pairs to bring them inside the surface bounds. If this is impossible
+ * (i.e. the surface is too small for the size of the content we try to copy on either
+ * dimension), an error will be logged and the magnifier content will look distorted.
+ * The default values assumed by the builder for the source bounds are
+ * left: {@link #SOURCE_BOUND_MAX_VISIBLE}, top: {@link #SOURCE_BOUND_MAX_IN_SURFACE},
+ * right: {@link #SOURCE_BOUND_MAX_VISIBLE}, bottom: {@link #SOURCE_BOUND_MAX_IN_SURFACE}.
+ * @param left the left bound for content copy
+ * @param top the top bound for content copy
+ * @param right the right bound for content copy
+ * @param bottom the bottom bound for content copy
+ */
+ @NonNull
+ public Builder setSourceBounds(@SourceBound int left, @SourceBound int top,
+ @SourceBound int right, @SourceBound int bottom) {
+ mLeftContentBound = left;
+ mTopContentBound = top;
+ mRightContentBound = right;
+ mBottomContentBound = bottom;
+ return this;
+ }
+
+ /**
+ * Sets the source width/height.
+ */
+ @NonNull
+ Builder setSourceSize(int width, int height) {
+ mSourceWidth = width;
+ mSourceHeight = height;
+ return this;
+ }
+
+ /**
+ * Sets the magnifier as the new fish-eye style.
+ */
+ @NonNull
+ Builder setFishEyeStyle() {
+ mIsFishEyeStyle = true;
+ return this;
+ }
+
+ /**
+ * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}.
+ */
+ public @NonNull Magnifier build() {
+ return new Magnifier(this);
+ }
+ }
+
+ /**
+ * A source bound that will extend as much as possible, while remaining within the surface
+ * the content is copied from.
+ */
+ public static final int SOURCE_BOUND_MAX_IN_SURFACE = 0;
+
+ /**
+ * A source bound that will extend as much as possible, while remaining within the
+ * visible region of the magnified view, as determined by
+ * {@link View#getGlobalVisibleRect(Rect)}.
+ */
+ public static final int SOURCE_BOUND_MAX_VISIBLE = 1;
+
+
+ /**
+ * Used to describe the {@link Surface} rectangle where the magnifier's content is allowed
+ * to be copied from. For more details, see method
+ * {@link Magnifier.Builder#setSourceBounds(int, int, int, int)}
+ *
+ * @hide
+ */
+ @IntDef({SOURCE_BOUND_MAX_IN_SURFACE, SOURCE_BOUND_MAX_VISIBLE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SourceBound {}
+
+ // The rest of the file consists of test APIs and methods relevant for tests.
+
+ /**
+ * See {@link #setOnOperationCompleteCallback(Callback)}.
+ */
+ @TestApi
+ private Callback mCallback;
+
+ /**
+ * Sets a callback which will be invoked at the end of the next
+ * {@link #show(float, float)} or {@link #update()} operation.
+ *
+ * @hide
+ */
+ @TestApi
+ public void setOnOperationCompleteCallback(final Callback callback) {
+ mCallback = callback;
+ if (mWindow != null) {
+ mWindow.mCallback = callback;
+ }
+ }
+
+ /**
+ * @return the drawing being currently displayed in the magnifier, as bitmap
+ *
+ * @hide
+ */
+ @TestApi
+ public @Nullable Bitmap getContent() {
+ if (mWindow == null) {
+ return null;
+ }
+ synchronized (mWindow.mLock) {
+ return mWindow.mCurrentContent;
+ }
+ }
+
+ /**
+ * Returns a bitmap containing the content that was magnified and drew to the
+ * magnifier, at its original size, without the overlay applied.
+ * @return the content that is magnified, as bitmap
+ *
+ * @hide
+ */
+ @TestApi
+ public @Nullable Bitmap getOriginalContent() {
+ if (mWindow == null) {
+ return null;
+ }
+ synchronized (mWindow.mLock) {
+ return Bitmap.createBitmap(mWindow.mBitmap);
+ }
+ }
+
+ /**
+ * @return the size of the magnifier window in dp
+ *
+ * @hide
+ */
+ @TestApi
+ public static PointF getMagnifierDefaultSize() {
+ final Resources resources = Resources.getSystem();
+ final float density = resources.getDisplayMetrics().density;
+ final PointF size = new PointF();
+ size.x = resources.getDimension(R.dimen.default_magnifier_width) / density;
+ size.y = resources.getDimension(R.dimen.default_magnifier_height) / density;
+ return size;
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public interface Callback {
+ /**
+ * Callback called after the drawing for a magnifier update has happened.
+ */
+ void onOperationComplete();
+ }
+}