diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index d477ffdf8b2b2..d0c2d869b542b 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -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,15 +436,21 @@ public class Editor { } final ViewTreeObserver observer = mTextView.getViewTreeObserver(); - // No need to create the controller. - // The get method will add the listener on controller creation. - if (mInsertionPointCursorController != null) { - observer.addOnTouchModeChangeListener(mInsertionPointCursorController); - } - if (mSelectionModifierCursorController != null) { - mSelectionModifierCursorController.resetTouchOffsets(); - observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); + if (observer.isAlive()) { + // No need to create the controller. + // The get method will add the listener on controller creation. + if (mInsertionPointCursorController != null) { + observer.addOnTouchModeChangeListener(mInsertionPointCursorController); + } + if (mSelectionModifierCursorController != null) { + 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(); } diff --git a/core/java/com/android/internal/widget/Magnifier.java b/core/java/com/android/internal/widget/Magnifier.java index f6741c3b5169f..f79a7f519b74c 100644 --- a/core/java/com/android/internal/widget/Magnifier.java +++ b/core/java/com/android/internal/widget/Magnifier.java @@ -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 (mWindow.isShowing()) { - mWindow.update(mWindowCoords.x, mWindowCoords.y, mWindow.getWidth(), - mWindow.getHeight()); - } else { - mWindow.showAtLocation(mView.getRootView(), Gravity.NO_GRAVITY, - mWindowCoords.x, mWindowCoords.y); + 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, 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; - mCenterZoomCoords.x = (int) posXOnScreen; - mCenterZoomCoords.y = (int) posYOnScreen; + 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 = Math.round(posX); + mCenterZoomCoords.y = Math.round(posY); final int verticalMagnifierOffset = mView.getContext().getResources().getDimensionPixelSize( R.dimen.magnifier_offset); @@ -184,34 +200,36 @@ 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); - } else { - Log.d(LOG_TAG, "Could not perform PixelCopy request"); } } + @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 { + surface = null; + } + + return (surface != null && surface.isValid()) ? surface : null; + } + private ImageView getImageView() { return mWindow.getContentView().findViewById(R.id.magnifier_image); }