Merge changes from topic "magnifier_sync_movement"
* changes:
[Magnifier-24] Add completion callback TestApi
[Magnifier-21] Rate-limit drawings to renderer
[Magnifier-20] Raw Surface instead of PopupWindow
diff --git a/api/test-current.txt b/api/test-current.txt
index 049211d..121ca6e 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1097,6 +1097,11 @@
method public android.graphics.Bitmap getContent();
method public static android.graphics.PointF getMagnifierDefaultSize();
method public android.graphics.Rect getWindowPositionOnScreen();
+ method public void setOnOperationCompleteCallback(android.widget.Magnifier.Callback);
+ }
+
+ public static abstract interface Magnifier.Callback {
+ method public abstract void onOperationComplete();
}
public class NumberPicker extends android.widget.LinearLayout {
diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java
index 59472d9..6da51d1 100644
--- a/core/java/android/view/ThreadedRenderer.java
+++ b/core/java/android/view/ThreadedRenderer.java
@@ -34,6 +34,7 @@
import android.util.Log;
import android.view.Surface.OutOfResourcesException;
import android.view.View.AttachInfo;
+import android.view.animation.AnimationUtils;
import com.android.internal.R;
import com.android.internal.util.VirtualRefBasePtr;
@@ -943,6 +944,109 @@
}
}
+ /**
+ * Basic synchronous renderer. Currently only used to render the Magnifier, so use with care.
+ * TODO: deduplicate against ThreadedRenderer.
+ *
+ * @hide
+ */
+ public static class SimpleRenderer {
+ private final RenderNode mRootNode;
+ private long mNativeProxy;
+ private final float mLightY, mLightZ;
+ private Surface mSurface;
+ private final FrameInfo mFrameInfo = new FrameInfo();
+
+ public SimpleRenderer(final Context context, final String name, final Surface surface) {
+ final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Lighting, 0, 0);
+ mLightY = a.getDimension(R.styleable.Lighting_lightY, 0);
+ mLightZ = a.getDimension(R.styleable.Lighting_lightZ, 0);
+ final float lightRadius = a.getDimension(R.styleable.Lighting_lightRadius, 0);
+ final int ambientShadowAlpha =
+ (int) (255 * a.getFloat(R.styleable.Lighting_ambientShadowAlpha, 0) + 0.5f);
+ final int spotShadowAlpha =
+ (int) (255 * a.getFloat(R.styleable.Lighting_spotShadowAlpha, 0) + 0.5f);
+ a.recycle();
+
+ final long rootNodePtr = nCreateRootRenderNode();
+ mRootNode = RenderNode.adopt(rootNodePtr);
+ mRootNode.setClipToBounds(false);
+ mNativeProxy = nCreateProxy(true /* translucent */, rootNodePtr);
+ nSetName(mNativeProxy, name);
+
+ ProcessInitializer.sInstance.init(context, mNativeProxy);
+ nLoadSystemProperties(mNativeProxy);
+
+ nSetup(mNativeProxy, lightRadius, ambientShadowAlpha, spotShadowAlpha);
+
+ mSurface = surface;
+ nUpdateSurface(mNativeProxy, surface);
+ }
+
+ /**
+ * Set the light center.
+ */
+ public void setLightCenter(final Display display,
+ final int windowLeft, final int windowTop) {
+ // Adjust light position for window offsets.
+ final Point displaySize = new Point();
+ display.getRealSize(displaySize);
+ final float lightX = displaySize.x / 2f - windowLeft;
+ final float lightY = mLightY - windowTop;
+
+ nSetLightCenter(mNativeProxy, lightX, lightY, mLightZ);
+ }
+
+ public RenderNode getRootNode() {
+ return mRootNode;
+ }
+
+ /**
+ * Draw the surface.
+ */
+ public void draw(final FrameDrawingCallback callback) {
+ final long vsync = AnimationUtils.currentAnimationTimeMillis() * 1000000L;
+ mFrameInfo.setVsync(vsync, vsync);
+ mFrameInfo.addFlags(1 << 2 /* VSYNC */);
+ // TODO: remove this fence
+ nFence(mNativeProxy);
+ if (callback != null) {
+ callback.onFrameDraw(mSurface.getNextFrameNumber());
+ }
+ nSyncAndDrawFrame(mNativeProxy, mFrameInfo.mFrameInfo, mFrameInfo.mFrameInfo.length);
+ }
+
+ /**
+ * Destroy the renderer.
+ */
+ public void destroy() {
+ mSurface = null;
+ nDestroy(mNativeProxy, mRootNode.mNativeRenderNode);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ nDeleteProxy(mNativeProxy);
+ mNativeProxy = 0;
+ } finally {
+ super.finalize();
+ }
+ }
+ }
+
+ /**
+ * Interface used to receive callbacks when a frame is being drawn.
+ */
+ public interface FrameDrawingCallback {
+ /**
+ * Invoked during a frame drawing.
+ *
+ * @param frame The id of the frame being drawn.
+ */
+ void onFrameDraw(long frame);
+ }
+
private static class ProcessInitializer {
static ProcessInitializer sInstance = new ProcessInitializer();
diff --git a/core/java/android/widget/Magnifier.java b/core/java/android/widget/Magnifier.java
index 8836561..6427965 100644
--- a/core/java/android/widget/Magnifier.java
+++ b/core/java/android/widget/Magnifier.java
@@ -18,21 +18,32 @@
import android.annotation.FloatRange;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.TestApi;
import android.annotation.UiThread;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Handler;
-import android.view.Gravity;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.view.Display;
+import android.view.DisplayListCanvas;
import android.view.LayoutInflater;
import android.view.PixelCopy;
+import android.view.RenderNode;
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.ViewParent;
import android.view.ViewRootImpl;
@@ -46,32 +57,37 @@
public final class 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 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 final PopupWindow mWindow;
+ private InternalPopupWindow mWindow;
// The center coordinates of the window containing the magnifier.
private final Point mWindowCoords = new Point();
// The width of the window containing the magnifier.
private final int mWindowWidth;
// The height of the window containing the magnifier.
private final int mWindowHeight;
- // The bitmap used to display the contents of the magnifier.
- private final Bitmap mBitmap;
+ // The width of the bitmaps where the magnifier content is copied.
+ private final int mBitmapWidth;
+ // The height of the bitmaps where the magnifier content is copied.
+ private final int mBitmapHeight;
+ // The elevation of the window containing the magnifier.
+ private final float mWindowElevation;
// The center coordinates of the content that is to be magnified.
private final Point mCenterZoomCoords = new Point();
- // The callback of the pixel copy request will be invoked on this Handler when
- // the copy is finished.
- private final Handler mPixelCopyHandler = Handler.getMain();
- // Current magnification scale.
- private final float mZoomScale;
// 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 mPrevPosInView = 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();
/**
@@ -82,8 +98,6 @@
public Magnifier(@NonNull View view) {
mView = Preconditions.checkNotNull(view);
final Context context = mView.getContext();
- final float elevation = context.getResources().getDimension(
- com.android.internal.R.dimen.magnifier_elevation);
final View content = LayoutInflater.from(context).inflate(
com.android.internal.R.layout.magnifier, null);
content.findViewById(com.android.internal.R.id.magnifier_inner).setClipToOutline(true);
@@ -91,23 +105,18 @@
com.android.internal.R.dimen.magnifier_width);
mWindowHeight = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.magnifier_height);
- mZoomScale = context.getResources().getFloat(
+ mWindowElevation = context.getResources().getDimension(
+ com.android.internal.R.dimen.magnifier_elevation);
+ final float zoomScale = context.getResources().getFloat(
com.android.internal.R.dimen.magnifier_zoom_scale);
+ mBitmapWidth = Math.round(mWindowWidth / zoomScale);
+ mBitmapHeight = Math.round(mWindowHeight / zoomScale);
// The view's surface coordinates will not be updated until the magnifier is first shown.
mViewCoordinatesInSurface = new int[2];
+ }
- mWindow = new PopupWindow(context);
- mWindow.setContentView(content);
- mWindow.setWidth(mWindowWidth);
- mWindow.setHeight(mWindowHeight);
- mWindow.setElevation(elevation);
- mWindow.setTouchable(false);
- mWindow.setBackgroundDrawable(null);
-
- final int bitmapWidth = Math.round(mWindowWidth / mZoomScale);
- final int bitmapHeight = Math.round(mWindowHeight / mZoomScale);
- mBitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
- getImageView().setImageBitmap(mBitmap);
+ static {
+ sPixelCopyHandlerThread.start();
}
/**
@@ -155,30 +164,45 @@
}
final int startX = Math.max(zeroScrollXInSurface, Math.min(
- mCenterZoomCoords.x - mBitmap.getWidth() / 2,
- zeroScrollXInSurface + actualWidth - mBitmap.getWidth()));
- final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;
+ mCenterZoomCoords.x - mBitmapWidth / 2,
+ zeroScrollXInSurface + actualWidth - mBitmapWidth));
+ final int startY = mCenterZoomCoords.y - mBitmapHeight / 2;
if (xPosInView != mPrevPosInView.x || yPosInView != mPrevPosInView.y) {
- performPixelCopy(startX, startY);
-
+ if (mWindow == null) {
+ mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(),
+ getValidViewSurface(), mWindowWidth, mWindowHeight, mWindowElevation,
+ Handler.getMain() /* draw the magnifier on the UI thread */, mCallback);
+ }
+ performPixelCopy(startX, startY, true /* update window position */);
mPrevPosInView.x = xPosInView;
mPrevPosInView.y = yPosInView;
-
- if (mWindow.isShowing()) {
- mWindow.update(mWindowCoords.x, mWindowCoords.y, mWindow.getWidth(),
- mWindow.getHeight());
- } else {
- mWindow.showAtLocation(mView, Gravity.NO_GRAVITY, mWindowCoords.x, mWindowCoords.y);
- }
}
}
+ @Nullable
+ private Surface getValidViewSurface() {
+ // TODO: deduplicate this against the first part of #performPixelCopy
+ final Surface surface;
+ if (mView instanceof SurfaceView) {
+ surface = ((SurfaceView) mView).getHolder().getSurface();
+ } else if (mView.getViewRootImpl() != null) {
+ surface = mView.getViewRootImpl().mSurface;
+ } else {
+ surface = null;
+ }
+
+ return (surface != null && surface.isValid()) ? surface : null;
+ }
+
/**
* Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op.
*/
public void dismiss() {
- mWindow.dismiss();
+ if (mWindow != null) {
+ mWindow.destroy();
+ mWindow = null;
+ }
}
/**
@@ -186,43 +210,40 @@
* {@link #show(float, float)}. This only happens if the magnifier is currently showing.
*/
public void update() {
- if (mWindow.isShowing()) {
- // Update the contents shown in the magnifier.
- performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y);
+ if (mWindow != null) {
+ // Update the content shown in the magnifier.
+ performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y,
+ false /* update window position */);
}
}
private void configureCoordinates(final float xPosInView, final float yPosInView) {
// Compute 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.
- final float contentPosX;
- final float contentPosY;
+ final float posX;
+ final float posY;
if (mView instanceof SurfaceView) {
// No offset required if the backing Surface matches the size of the SurfaceView.
- contentPosX = xPosInView;
- contentPosY = yPosInView;
+ posX = xPosInView;
+ posY = yPosInView;
} else {
mView.getLocationInSurface(mViewCoordinatesInSurface);
- contentPosX = xPosInView + mViewCoordinatesInSurface[0];
- contentPosY = yPosInView + mViewCoordinatesInSurface[1];
+ posX = xPosInView + mViewCoordinatesInSurface[0];
+ posY = yPosInView + mViewCoordinatesInSurface[1];
}
- mCenterZoomCoords.x = Math.round(contentPosX);
- mCenterZoomCoords.y = Math.round(contentPosY);
+ mCenterZoomCoords.x = Math.round(posX);
+ mCenterZoomCoords.y = Math.round(posY);
- // Compute the position of the magnifier window. These have to be relative to the window
- // of the view the magnifier is attached to, as the magnifier popup is a panel window
- // attached to that window.
- final int[] viewCoordinatesInWindow = new int[2];
- mView.getLocationInWindow(viewCoordinatesInWindow);
+ // Compute the position of the magnifier window. Again, this has to be relative to the
+ // surface of the magnified view, as this surface is the parent of the magnifier surface.
final int verticalOffset = mView.getContext().getResources().getDimensionPixelSize(
com.android.internal.R.dimen.magnifier_offset);
- final float magnifierPosX = xPosInView + viewCoordinatesInWindow[0];
- final float magnifierPosY = yPosInView + viewCoordinatesInWindow[1] - verticalOffset;
- mWindowCoords.x = Math.round(magnifierPosX - mWindowWidth / 2);
- mWindowCoords.y = Math.round(magnifierPosY - mWindowHeight / 2);
+ mWindowCoords.x = mCenterZoomCoords.x - mWindowWidth / 2;
+ mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 - verticalOffset;
}
- private void performPixelCopy(final int startXInSurface, final int startYInSurface) {
+ private void performPixelCopy(final int startXInSurface, final int startYInSurface,
+ final boolean updateWindowPosition) {
// Get the view surface where the content will be copied from.
final Surface surface;
final int surfaceWidth;
@@ -256,20 +277,293 @@
// Perform the pixel copy.
mPixelCopyRequestRect.set(clampedStartXInSurface,
clampedStartYInSurface,
- clampedStartXInSurface + mBitmap.getWidth(),
- clampedStartYInSurface + mBitmap.getHeight());
- PixelCopy.request(surface, mPixelCopyRequestRect, mBitmap,
+ clampedStartXInSurface + mBitmapWidth,
+ clampedStartYInSurface + mBitmapHeight);
+ final int windowCoordsX = mWindowCoords.x;
+ final int windowCoordsY = mWindowCoords.y;
+
+ final Bitmap bitmap =
+ Bitmap.createBitmap(mBitmapWidth, mBitmapHeight, Bitmap.Config.ARGB_8888);
+ PixelCopy.request(surface, mPixelCopyRequestRect, bitmap,
result -> {
- getImageView().invalidate();
- mPrevStartCoordsInSurface.x = startXInSurface;
- mPrevStartCoordsInSurface.y = startYInSurface;
+ synchronized (mWindow.mLock) {
+ if (mWindow != null) {
+ if (updateWindowPosition) {
+ // TODO: pull the position update outside #performPixelCopy
+ mWindow.setContentPositionForNextDraw(windowCoordsX, windowCoordsY);
+ }
+ mWindow.updateContent(bitmap);
+ }
+ }
},
- mPixelCopyHandler);
+ sPixelCopyHandlerThread.getThreadHandler());
+ mPrevStartCoordsInSurface.x = startXInSurface;
+ mPrevStartCoordsInSurface.y = startYInSurface;
}
- private ImageView getImageView() {
- return mWindow.getContentView().findViewById(
- com.android.internal.R.id.magnifier_image);
+ /**
+ * 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 {
+ // 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 final int mContentHeight;
+ // The size of the allocated surface.
+ private final int mSurfaceWidth;
+ private final int mSurfaceHeight;
+ // The insets of the content inside the allocated surface.
+ private final int mOffsetX;
+ private final int mOffsetY;
+ // The surface we allocate for the magnifier content + shadow.
+ private final SurfaceSession mSurfaceSession;
+ private final SurfaceControl mSurfaceControl;
+ 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 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. Only used for testing.
+ 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 = new Object();
+ // Whether a magnifier frame draw is currently pending in the UI thread queue.
+ private boolean mFrameDrawScheduled;
+ // The content bitmap.
+ 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;
+
+ InternalPopupWindow(final Context context, final Display display,
+ final Surface parentSurface,
+ final int width, final int height, final float elevation,
+ final Handler handler, final Callback callback) {
+ mDisplay = display;
+ mCallback = callback;
+
+ mContentWidth = width;
+ mContentHeight = height;
+ mOffsetX = (int) (0.1f * width);
+ mOffsetY = (int) (0.1f * height);
+ // Setup the surface we will use for drawing the content and shadow.
+ mSurfaceWidth = mContentWidth + 2 * mOffsetX;
+ mSurfaceHeight = mContentHeight + 2 * mOffsetY;
+ mSurfaceSession = new SurfaceSession(parentSurface);
+ mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
+ .setFormat(PixelFormat.TRANSLUCENT)
+ .setSize(mSurfaceWidth, mSurfaceHeight)
+ .setName("magnifier surface")
+ .setFlags(SurfaceControl.HIDDEN)
+ .build();
+ mSurface = new Surface();
+ mSurface.copyFrom(mSurfaceControl);
+
+ // Setup the RenderNode tree. The root has only one child, which contains the bitmap.
+ mRenderer = new ThreadedRenderer.SimpleRenderer(
+ context,
+ "magnifier renderer",
+ mSurface
+ );
+ mBitmapRenderNode = createRenderNodeForBitmap(
+ "magnifier content",
+ elevation
+ );
+
+ final DisplayListCanvas canvas = mRenderer.getRootNode().start(width, height);
+ try {
+ canvas.insertReorderBarrier();
+ canvas.drawRenderNode(mBitmapRenderNode);
+ canvas.insertInorderBarrier();
+ } finally {
+ mRenderer.getRootNode().end(canvas);
+ }
+
+ // Initialize the update job and the handler where this will be post'd.
+ mHandler = handler;
+ mMagnifierUpdater = this::doDraw;
+ mFrameDrawScheduled = false;
+ }
+
+ private RenderNode createRenderNodeForBitmap(final String name, final float elevation) {
+ 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, 3);
+ outline.setAlpha(1.0f);
+ bitmapRenderNode.setOutline(outline);
+ bitmapRenderNode.setClipToOutline(true);
+
+ // Create a dummy draw, which will be replaced later with real drawing.
+ final DisplayListCanvas canvas = bitmapRenderNode.start(mContentWidth, mContentHeight);
+ try {
+ canvas.drawColor(0xFF00FF00);
+ } finally {
+ bitmapRenderNode.end(canvas);
+ }
+
+ return bitmapRenderNode;
+ }
+
+ /**
+ * 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.
+ */
+ public void destroy() {
+ mRenderer.destroy();
+ mSurface.destroy();
+ mSurfaceControl.destroy();
+ mSurfaceSession.kill();
+ mBitmapRenderNode.destroy();
+ synchronized (mLock) {
+ mHandler.removeCallbacks(mMagnifierUpdater);
+ if (mBitmap != null) {
+ mBitmap.recycle();
+ }
+ }
+ }
+
+ 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 DisplayListCanvas canvas =
+ mBitmapRenderNode.start(mContentWidth, mContentHeight);
+ try {
+ final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
+ final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
+ final Paint paint = new Paint();
+ paint.setFilterBitmap(true);
+ canvas.drawBitmap(mBitmap, srcRect, dstRect, paint);
+ } finally {
+ mBitmapRenderNode.end(canvas);
+ }
+
+ 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 -> {
+ mRenderer.setLightCenter(mDisplay, pendingX, pendingY);
+ // Show or move the window at the content draw frame.
+ SurfaceControl.openTransaction();
+ mSurfaceControl.deferTransactionUntil(mSurface, frame);
+ if (updateWindowPosition) {
+ mSurfaceControl.setPosition(pendingX, pendingY);
+ }
+ if (firstDraw) {
+ mSurfaceControl.show();
+ }
+ SurfaceControl.closeTransaction();
+ };
+ } else {
+ callback = null;
+ }
+
+ mFrameDrawScheduled = false;
+ }
+
+ mRenderer.draw(callback);
+ if (mCallback != null) {
+ mCallback.onOperationComplete();
+ }
+ }
+ }
+
+ // The rest of the file consists of test APIs.
+
+ /**
+ * 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;
+ }
}
/**
@@ -278,8 +572,13 @@
* @hide
*/
@TestApi
- public Bitmap getContent() {
- return mBitmap;
+ public @Nullable Bitmap getContent() {
+ if (mWindow == null) {
+ return null;
+ }
+ synchronized (mWindow.mLock) {
+ return mWindow.mBitmap;
+ }
}
/**
@@ -296,7 +595,7 @@
final int left = mWindowCoords.x + viewLocationOnScreen[0] - viewLocationInSurface[0];
final int top = mWindowCoords.y + viewLocationOnScreen[1] - viewLocationInSurface[1];
- return new Rect(left, top, left + mWindow.getWidth(), top + mWindow.getHeight());
+ return new Rect(left, top, left + mWindowWidth, top + mWindowHeight);
}
/**
@@ -313,4 +612,15 @@
size.y = resources.getDimension(com.android.internal.R.dimen.magnifier_height) / density;
return size;
}
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public interface Callback {
+ /**
+ * Callback called after the drawing for a magnifier update has happened.
+ */
+ void onOperationComplete();
+ }
}