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

Commit 41589fa8 authored by Andrei Stingaceanu's avatar Andrei Stingaceanu
Browse files

[Magnifier - 8] SurfaceView support and invalidate revival

It turns out that the auto-invalidate at a defined time,
practically polling, is not a safe way to update content
and also has more chances of producing poor quality so
temporary bring back update() and keep it hidden as the
plan is to have direct update listeners from the graphics
stack in the near future. This solution works well for
TextView, WebView and Chrome.

Added support for SurfaceView (used by Chrome).

Editor adds an onDrawListener to the TextView's tree observer
which posts to Magnifier update. This makes sure the
absolutely everytime anything changes in the view hierarchy
update() will be posted (after the actual drawing).

Bug: 63531115
Test: bit FrameworksCoreTests:android.widget.TextViewActivityTest
Test: bit CtsWidgetTestCases:android.widget.cts.TextViewTest
Test: manual test that shows the magnifier working
Change-Id: If1b858d793c7cc338d23a850051022768a3f1e40
parent 622597fb
Loading
Loading
Loading
Loading
+42 −8
Original line number Diff line number Diff line
@@ -195,6 +195,27 @@ public class Editor {
    private final boolean mHapticTextHandleEnabled;

    private final Magnifier mMagnifier;
    private final Runnable mUpdateMagnifierRunnable = new Runnable() {
        @Override
        public void run() {
            mMagnifier.update();
        }
    };
    // Update the magnifier contents whenever anything in the view hierarchy is updated.
    // Note: this only captures UI thread-visible changes, so it's a known issue that an animating
    // VectorDrawable or Ripple animation will not trigger capture, since they're owned by
    // RenderThread.
    private final ViewTreeObserver.OnDrawListener mMagnifierOnDrawListener =
            new ViewTreeObserver.OnDrawListener() {
        @Override
        public void onDraw() {
            if (mMagnifier != null) {
                // Posting the method will ensure that updating the magnifier contents will
                // happen right after the rendering of the current frame.
                mTextView.post(mUpdateMagnifierRunnable);
            }
        }
    };

    // Used to highlight a word when it is corrected by the IME
    private CorrectionHighlighter mCorrectionHighlighter;
@@ -415,6 +436,7 @@ public class Editor {
        }

        final ViewTreeObserver observer = mTextView.getViewTreeObserver();
        if (observer.isAlive()) {
            // No need to create the controller.
            // The get method will add the listener on controller creation.
            if (mInsertionPointCursorController != null) {
@@ -424,6 +446,11 @@ public class Editor {
                mSelectionModifierCursorController.resetTouchOffsets();
                observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
            }
            if (FLAG_USE_MAGNIFIER) {
                observer.addOnDrawListener(mMagnifierOnDrawListener);
            }
        }

        updateSpellCheckSpans(0, mTextView.getText().length(),
                true /* create the spell checker if needed */);

@@ -472,6 +499,13 @@ public class Editor {
            mSpellChecker = null;
        }

        if (FLAG_USE_MAGNIFIER) {
            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
            if (observer.isAlive()) {
                observer.removeOnDrawListener(mMagnifierOnDrawListener);
            }
        }

        hideCursorAndSpanControllers();
        stopTextActionModeWithPreservingSelection();
    }
+84 −66
Original line number Diff line number Diff line
@@ -18,34 +18,33 @@ package com.android.internal.widget;

import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Handler;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.PixelCopy;
import android.view.Surface;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewRootImpl;
import android.widget.ImageView;
import android.widget.PopupWindow;

import com.android.internal.R;
import com.android.internal.util.Preconditions;

import java.util.Timer;
import java.util.TimerTask;

/**
 * Android magnifier widget. Can be used by any view which is attached to window.
 */
public final class Magnifier {
    private static final String LOG_TAG = "magnifier";
    private static final int MAGNIFIER_REFRESH_RATE_MS = 33; // ~30fps
    // The view for which this magnifier is attached.
    // Use this to specify that a previous configuration value does not exist.
    private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
    // The view to which this magnifier is attached.
    private final View mView;
    // The window containing the magnifier.
    private final PopupWindow mWindow;
@@ -64,8 +63,12 @@ public final class Magnifier {
    private final Handler mPixelCopyHandler = Handler.getMain();
    // Current magnification scale.
    private final float mZoomScale;
    // Timer used to schedule the copy task.
    private Timer mTimer;
    // 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);
    private final Rect mPixelCopyRequestRect = new Rect();

    /**
     * Initializes a magnifier.
@@ -91,8 +94,8 @@ public final class Magnifier {
        mWindow.setTouchable(false);
        mWindow.setBackgroundDrawable(null);

        final int bitmapWidth = (int) (mWindowWidth / mZoomScale);
        final int bitmapHeight = (int) (mWindowHeight / mZoomScale);
        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);
    }
@@ -106,32 +109,29 @@ public final class Magnifier {
     *        relative to the view. The lower end is clamped to 0
     */
    public void show(@FloatRange(from=0) float xPosInView, @FloatRange(from=0) float yPosInView) {
        if (xPosInView < 0) {
            xPosInView = 0;
        }

        if (yPosInView < 0) {
            yPosInView = 0;
        }
        xPosInView = Math.max(0, xPosInView);
        yPosInView = Math.max(0, yPosInView);

        configureCoordinates(xPosInView, yPosInView);

        if (mTimer == null) {
            mTimer = new Timer();
            mTimer.schedule(new TimerTask() {
                @Override
                public void run() {
                    performPixelCopy();
                }
            }, 0 /* delay */, MAGNIFIER_REFRESH_RATE_MS);
        }
        // Clamp startX value to avoid distorting the rendering of the magnifier content.
        final int startX = Math.max(0, Math.min(
                mCenterZoomCoords.x - mBitmap.getWidth() / 2,
                mView.getWidth() - mBitmap.getWidth()));
        final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;

        if (startX != mPrevStartCoordsInSurface.x || startY != mPrevStartCoordsInSurface.y) {
            performPixelCopy(startX, startY);

            mPrevPosInView.x = xPosInView;
            mPrevPosInView.y = yPosInView;

            if (mWindow.isShowing()) {
                mWindow.update(mWindowCoords.x, mWindowCoords.y, mWindow.getWidth(),
                        mWindow.getHeight());
            } else {
            mWindow.showAtLocation(mView.getRootView(), Gravity.NO_GRAVITY,
                    mWindowCoords.x, mWindowCoords.y);
                mWindow.showAtLocation(mView, Gravity.NO_GRAVITY, mWindowCoords.x, mWindowCoords.y);
            }
        }
    }

@@ -140,11 +140,18 @@ public final class Magnifier {
     */
    public void dismiss() {
        mWindow.dismiss();
    }

        if (mTimer != null) {
            mTimer.cancel();
            mTimer.purge();
            mTimer = null;
    /**
     * Forces the magnifier to update its content. It uses the previous coordinates passed to
     * {@link #show(float, float)}. This only happens if the magnifier is currently showing.
     *
     * @hide
     */
    public void update() {
        if (mWindow.isShowing()) {
            // Update the contents shown in the magnifier.
            performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y);
        }
    }

@@ -170,13 +177,22 @@ public final class Magnifier {
    }

    private void configureCoordinates(float xPosInView, float yPosInView) {
        final int[] coordinatesOnScreen = new int[2];
        mView.getLocationOnScreen(coordinatesOnScreen);
        final float posXOnScreen = xPosInView + coordinatesOnScreen[0];
        final float posYOnScreen = yPosInView + coordinatesOnScreen[1];
        final float posX;
        final float posY;

        if (mView instanceof SurfaceView) {
            // No offset required if the backing Surface matches the size of the SurfaceView.
            posX = xPosInView;
            posY = yPosInView;
        } else {
            final int[] coordinatesInSurface = new int[2];
            mView.getLocationInSurface(coordinatesInSurface);
            posX = xPosInView + coordinatesInSurface[0];
            posY = yPosInView + coordinatesInSurface[1];
        }

        mCenterZoomCoords.x = (int) posXOnScreen;
        mCenterZoomCoords.y = (int) posYOnScreen;
        mCenterZoomCoords.x = Math.round(posX);
        mCenterZoomCoords.y = Math.round(posY);

        final int verticalMagnifierOffset = mView.getContext().getResources().getDimensionPixelSize(
                R.dimen.magnifier_offset);
@@ -184,32 +200,34 @@ public final class Magnifier {
        mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 - verticalMagnifierOffset;
    }

    private void performPixelCopy() {
        final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;
        int rawStartX = mCenterZoomCoords.x - mBitmap.getWidth() / 2;
    private void performPixelCopy(final int startXInSurface, final int startYInSurface) {
        final Surface surface = getValidViewSurface();
        if (surface != null) {
            mPixelCopyRequestRect.set(startXInSurface, startYInSurface,
                    startXInSurface + mBitmap.getWidth(), startYInSurface + mBitmap.getHeight());

        // Clamp startX value to avoid distorting the rendering of the magnifier content.
        if (rawStartX < 0) {
            rawStartX = 0;
        } else if (rawStartX + mBitmap.getWidth() > mView.getWidth()) {
            rawStartX = mView.getWidth() - mBitmap.getWidth();
        }

        final int startX = rawStartX;
        final ViewRootImpl viewRootImpl = mView.getViewRootImpl();

        if (viewRootImpl != null && viewRootImpl.mSurface != null
                && viewRootImpl.mSurface.isValid()) {
            PixelCopy.request(
                    viewRootImpl.mSurface,
                    new Rect(startX, startY, startX + mBitmap.getWidth(),
                            startY + mBitmap.getHeight()),
                    mBitmap,
                    result -> getImageView().invalidate(),
            PixelCopy.request(surface, mPixelCopyRequestRect, mBitmap,
                    result -> {
                        getImageView().invalidate();
                        mPrevStartCoordsInSurface.x = startXInSurface;
                        mPrevStartCoordsInSurface.y = startYInSurface;
                    },
                    mPixelCopyHandler);
        }
    }

    @Nullable
    private Surface getValidViewSurface() {
        final Surface surface;
        if (mView instanceof SurfaceView) {
            surface = ((SurfaceView) mView).getHolder().getSurface();
        } else if (mView.getViewRootImpl() != null) {
            surface = mView.getViewRootImpl().mSurface;
        } else {
            Log.d(LOG_TAG, "Could not perform PixelCopy request");
            surface = null;
        }

        return (surface != null && surface.isValid()) ? surface : null;
    }

    private ImageView getImageView() {