Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4bcd4d40 authored by Mihai Popa's avatar Mihai Popa
Browse files

[Magnifier-20] Raw Surface instead of PopupWindow

Since the movement of PopupWindows is handled by the global window
manager, using one does not provide synchronization guarantees between
the window movement and its view tree rendering. In the case of the
magnifier this was producing a shaky-text effect, when the window was
moved in one frame, but the content was only updated in the following
one (although the movement and content update were issued during the
same #show operation on the UI thread).

This CL abandons the usage of a PopupWindow for the magnifier, in favour
of a raw Surface whose movement can be controlled using the deferred
transactions API. This way, we can defer the surface movement until the
frame when the updated content is rendered. The allocated magnifier
surface is slightly bigger than the magnifier content to have space for
drawing the elevation. We obtain a native elevation by using a
RenderNode to draw the content of the magnifier.

Bug: 72041926
Test: atest CtsWidgetTestCases:android.widget.cts.MagnifierTest
Change-Id: Icb8ecf48a2f528e06790db95d54575f2ae119747
parent 944836c6
Loading
Loading
Loading
Loading
+102 −0
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import android.os.Trace;
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,107 @@ public final class ThreadedRenderer {
        }
    }

    /**
     * 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 */);
            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();

+248 −51
Original line number Diff line number Diff line
@@ -18,21 +18,30 @@ package android.widget;

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.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;
@@ -51,7 +60,7 @@ public final class Magnifier {
    // 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.
@@ -60,6 +69,8 @@ public final class Magnifier {
    private final int mWindowHeight;
    // The bitmap used to display the contents of the magnifier.
    private final Bitmap mBitmap;
    // 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
@@ -72,6 +83,7 @@ public final class Magnifier {
            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 +94,6 @@ public final class Magnifier {
    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 +101,16 @@ public final class Magnifier {
                com.android.internal.R.dimen.magnifier_width);
        mWindowHeight = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.magnifier_height);
        mWindowElevation = context.getResources().getDimension(
                com.android.internal.R.dimen.magnifier_elevation);
        mZoomScale = context.getResources().getFloat(
                com.android.internal.R.dimen.magnifier_zoom_scale);
        // 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);
    }

    /**
@@ -160,25 +163,39 @@ public final class Magnifier {
        final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 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);
            }
            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());
    @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 {
                mWindow.showAtLocation(mView, Gravity.NO_GRAVITY, mWindowCoords.x, mWindowCoords.y);
            }
            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 +203,40 @@ public final class Magnifier {
     * {@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;
@@ -258,18 +272,201 @@ public final class Magnifier {
                clampedStartYInSurface,
                clampedStartXInSurface + mBitmap.getWidth(),
                clampedStartYInSurface + mBitmap.getHeight());
        final int windowCoordsX = mWindowCoords.x;
        final int windowCoordsY = mWindowCoords.y;
        PixelCopy.request(surface, mPixelCopyRequestRect, mBitmap,
                result -> {
                    getImageView().invalidate();
                    mPrevStartCoordsInSurface.x = startXInSurface;
                    mPrevStartCoordsInSurface.y = startYInSurface;
                    if (mWindow != null) {
                        if (updateWindowPosition) {
                            // TODO: pull the magnifier position update outside #performPixelCopy
                            mWindow.setContentPositionForNextDraw(windowCoordsX, windowCoordsY);
                        }
                        mWindow.updateContent(mBitmap);
                    }
                },
                mPixelCopyHandler);
        mPrevStartCoordsInSurface.x = startXInSurface;
        mPrevStartCoordsInSurface.y = startYInSurface;
    }

    /**
     * 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;
        // 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 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;

        InternalPopupWindow(final Context context, final Display display,
                final Surface parentSurface,
                final int width, final int height, final float elevation) {
            mDisplay = display;

            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);
            }
        }

        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);
            }

    private ImageView getImageView() {
        return mWindow.getContentView().findViewById(
                com.android.internal.R.id.magnifier_image);
            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.
         *
         * @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;
        }

        /**
         * 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)}.
         *
         * @param bitmap the content bitmap
         */
        public void updateContent(final Bitmap bitmap) {
            final DisplayListCanvas canvas = mBitmapRenderNode.start(mContentWidth, mContentHeight);
            try {
                final Rect srcRect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
                final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight);
                final Paint paint = new Paint();
                paint.setFilterBitmap(true);
                canvas.drawBitmap(bitmap, srcRect, dstRect, paint);
            } finally {
                mBitmapRenderNode.end(canvas);
            }

            doDraw();
        }

        /**
         * Destroys this instance.
         */
        public void destroy() {
            mRenderer.destroy();
            mSurface.destroy();
            mSurfaceControl.destroy();
            mSurfaceSession.kill();
            mBitmapRenderNode.destroy();
        }

        private void doDraw() {
            final ThreadedRenderer.FrameDrawingCallback callback;
            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;
            }

            mRenderer.draw(callback);
        }
    }

    /**
@@ -296,7 +493,7 @@ public final class Magnifier {

        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);
    }

    /**