[Magnifier - 3] Reduce number of calls to PixelCopy
* Magnifier#show() takes snapshots of the content and displays
them in the Magnifier bitmap
* calling show(...) consecutive times with the same arguments is
a no-op if already showing (to cater with the miriad of motion
events produced by touch which end up calling show(...))
* introduced Magnifier#invalidate(...) which, if currently
showing, forces updating the content using the last configuration
* clamped the start horizontal value of the Rect which delimits the
content to show in order to avoid distorting the rendering of the
magnifier content
* fixed invalidating the magnifier (invalidate() does not
automatically call invalidate(RectF) !)
Bug: 63531115
Bug: 67296158
Test: bit FrameworksCoreTests:android.widget.TextViewActivityTest
Test: bit CtsWidgetTestCases:android.widget.cts.TextViewTest
Test: manual test that shows the magnifier working
Change-Id: I8e53dfb6582d541922fe05b60311658fb07ca880
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 91f6799..d4be7e5 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -476,6 +476,17 @@
stopTextActionModeWithPreservingSelection();
}
+ void invalidateMagnifier() {
+ final DisplayMetrics dm = mTextView.getResources().getDisplayMetrics();
+ invalidateMagnifier(0, 0, dm.widthPixels, dm.heightPixels);
+ }
+
+ void invalidateMagnifier(final float l, final float t, final float r, final float b) {
+ if (mMagnifier != null) {
+ mTextView.post(() -> mMagnifier.invalidate(new RectF(l, t, r, b)));
+ }
+ }
+
private void discardTextDisplayLists() {
if (mTextRenderNodes != null) {
for (int i = 0; i < mTextRenderNodes.length; i++) {
@@ -4545,10 +4556,8 @@
+ mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f;
final int[] coordinatesOnScreen = new int[2];
mTextView.getLocationOnScreen(coordinatesOnScreen);
- final float centerXOnScreen = xPosInView + mTextView.getTotalPaddingLeft()
- - mTextView.getScrollX() + coordinatesOnScreen[0];
- final float centerYOnScreen = yPosInView + mTextView.getTotalPaddingTop()
- - mTextView.getScrollY() + coordinatesOnScreen[1];
+ final float centerXOnScreen = mTextView.convertViewToScreenCoord(xPosInView, true);
+ final float centerYOnScreen = mTextView.convertViewToScreenCoord(yPosInView, false);
suspendBlink();
mMagnifier.show(centerXOnScreen, centerYOnScreen, MAGNIFIER_ZOOM);
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index d9bc51f..ce80552 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -9219,6 +9219,36 @@
}
}
+ @Override
+ public void invalidate() {
+ super.invalidate();
+
+ if (mEditor != null) {
+ mEditor.invalidateMagnifier();
+ }
+ }
+
+ @Override
+ public void invalidate(int l, int t, int r, int b) {
+ super.invalidate(l, t, r, b);
+
+ if (mEditor != null) {
+ mEditor.invalidateMagnifier(
+ convertViewToScreenCoord(l, true /* isHorizontal */),
+ convertViewToScreenCoord(t, false /* isHorizontal */),
+ convertViewToScreenCoord(r, true /* isHorizontal */),
+ convertViewToScreenCoord(b, false /* isHorizontal */));
+ }
+ }
+
+ float convertViewToScreenCoord(float viewCoord, boolean isHorizontal) {
+ final int[] coordinatesOnScreen = new int[2];
+ getLocationOnScreen(coordinatesOnScreen);
+ return isHorizontal
+ ? viewCoord + getTotalPaddingLeft() - getScrollX() + coordinatesOnScreen[0]
+ : viewCoord + getTotalPaddingTop() - getScrollY() + coordinatesOnScreen[1];
+ }
+
/**
* @return whether or not the cursor is visible (assuming this TextView is editable)
*
diff --git a/core/java/com/android/internal/widget/Magnifier.java b/core/java/com/android/internal/widget/Magnifier.java
index 284f2b2..9bc0778 100644
--- a/core/java/com/android/internal/widget/Magnifier.java
+++ b/core/java/com/android/internal/widget/Magnifier.java
@@ -22,7 +22,9 @@
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
+import android.graphics.PointF;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.os.Handler;
import android.util.Log;
import android.view.Gravity;
@@ -41,8 +43,8 @@
*/
public final class Magnifier {
private static final String LOG_TAG = "magnifier";
- private static final int MINIMUM_MAGNIFIER_SCALE = 1;
- private static final int MAXIMUM_MAGNIFIER_SCALE = 4;
+ // Use this to specify that a previous configuration value does not exist.
+ private static final int INEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
// The view for which this magnifier is attached.
private final View mView;
// The window containing the magnifier.
@@ -61,6 +63,15 @@
// the copy is finished.
private final Handler mPixelCopyHandler = Handler.getMain();
+ private RectF mTmpRectF;
+
+ // Variables holding previous states, used for detecting redundant calls and invalidation.
+ private Point mPrevStartCoordsOnScreen = new Point(
+ INEXISTENT_PREVIOUS_CONFIG_VALUE, INEXISTENT_PREVIOUS_CONFIG_VALUE);
+ private PointF mPrevCenterCoordsOnScreen = new PointF(
+ INEXISTENT_PREVIOUS_CONFIG_VALUE, INEXISTENT_PREVIOUS_CONFIG_VALUE);
+ private float mPrevScale = INEXISTENT_PREVIOUS_CONFIG_VALUE;
+
/**
* Initializes a magnifier.
*
@@ -90,19 +101,22 @@
/**
* Shows the magnifier on the screen.
*
- * @param centerXOnScreen horizontal coordinate of the center point of the magnifier source
- * @param centerYOnScreen vertical coordinate of the center point of the magnifier source
- * @param scale the scale at which the magnifier zooms on the source content
+ * @param centerXOnScreen horizontal coordinate of the center point of the magnifier source. The
+ * lower end is clamped to 0
+ * @param centerYOnScreen vertical coordinate of the center point of the magnifier source. The
+ * lower end is clamped to 0
+ * @param scale the scale at which the magnifier zooms on the source content. The
+ * lower end is clamped to 1 and the higher end to 4
*/
public void show(@FloatRange(from=0) float centerXOnScreen,
@FloatRange(from=0) float centerYOnScreen,
- @FloatRange(from=MINIMUM_MAGNIFIER_SCALE, to=MAXIMUM_MAGNIFIER_SCALE) float scale) {
- if (scale > MAXIMUM_MAGNIFIER_SCALE) {
- scale = MAXIMUM_MAGNIFIER_SCALE;
+ @FloatRange(from=1, to=4) float scale) {
+ if (scale > 4) {
+ scale = 4;
}
- if (scale < MINIMUM_MAGNIFIER_SCALE) {
- scale = MINIMUM_MAGNIFIER_SCALE;
+ if (scale < 1) {
+ scale = 1;
}
if (centerXOnScreen < 0) {
@@ -113,9 +127,19 @@
centerYOnScreen = 0;
}
- maybeResizeBitmap(scale);
+ showInternal(centerXOnScreen, centerYOnScreen, scale, false);
+ }
+
+ private void showInternal(@FloatRange(from=0) float centerXOnScreen,
+ @FloatRange(from=0) float centerYOnScreen,
+ @FloatRange(from=1, to=4) float scale,
+ boolean forceShow) {
+ if (mPrevScale != scale) {
+ resizeBitmap(scale);
+ mPrevScale = scale;
+ }
configureCoordinates(centerXOnScreen, centerYOnScreen);
- performPixelCopy();
+ maybePerformPixelCopy(scale, forceShow);
if (mWindow.isShowing()) {
mWindow.update(mWindowCoords.x, mWindowCoords.y, mWindow.getWidth(),
@@ -124,6 +148,9 @@
mWindow.showAtLocation(mView.getRootView(), Gravity.NO_GRAVITY,
mWindowCoords.x, mWindowCoords.y);
}
+
+ mPrevCenterCoordsOnScreen.x = centerXOnScreen;
+ mPrevCenterCoordsOnScreen.y = centerYOnScreen;
}
/**
@@ -131,6 +158,38 @@
*/
public void dismiss() {
mWindow.dismiss();
+
+ mPrevStartCoordsOnScreen.x = INEXISTENT_PREVIOUS_CONFIG_VALUE;
+ mPrevStartCoordsOnScreen.y = INEXISTENT_PREVIOUS_CONFIG_VALUE;
+ mPrevCenterCoordsOnScreen.x = INEXISTENT_PREVIOUS_CONFIG_VALUE;
+ mPrevCenterCoordsOnScreen.y = INEXISTENT_PREVIOUS_CONFIG_VALUE;
+ mPrevScale = INEXISTENT_PREVIOUS_CONFIG_VALUE;
+ }
+
+ /**
+ * Forces the magnifier to update content by taking and showing a new snapshot using the
+ * previous coordinates. It does this only if the magnifier is showing and the dirty rectangle
+ * intersects the rectangle which holds the content to be magnified.
+ *
+ * @param dirtyRectOnScreen the rectangle representing the screen bounds of the dirty region
+ */
+ public void invalidate(RectF dirtyRectOnScreen) {
+ if (mWindow.isShowing() && mPrevCenterCoordsOnScreen.x != INEXISTENT_PREVIOUS_CONFIG_VALUE
+ && mPrevCenterCoordsOnScreen.y != INEXISTENT_PREVIOUS_CONFIG_VALUE
+ && mPrevScale != INEXISTENT_PREVIOUS_CONFIG_VALUE) {
+ // Update the current showing RectF.
+ mTmpRectF = new RectF(mPrevStartCoordsOnScreen.x,
+ mPrevStartCoordsOnScreen.y,
+ mPrevStartCoordsOnScreen.x + mBitmap.getWidth(),
+ mPrevStartCoordsOnScreen.y + mBitmap.getHeight());
+
+ // Update only if we are currently showing content that has been declared as invalid.
+ if (RectF.intersects(dirtyRectOnScreen, mTmpRectF)) {
+ // Update the contents shown in the magnifier.
+ showInternal(mPrevCenterCoordsOnScreen.x, mPrevCenterCoordsOnScreen.y, mPrevScale,
+ true /* forceShow */);
+ }
+ }
}
/**
@@ -147,13 +206,11 @@
return mWindowWidth;
}
- private void maybeResizeBitmap(float scale) {
+ private void resizeBitmap(float scale) {
final int bitmapWidth = (int) (mWindowWidth / scale);
final int bitmapHeight = (int) (mWindowHeight / scale);
- if (mBitmap.getWidth() != bitmapWidth || mBitmap.getHeight() != bitmapHeight) {
- mBitmap.reconfigure(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
- getImageView().setImageBitmap(mBitmap);
- }
+ mBitmap.reconfigure(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
+ getImageView().setImageBitmap(mBitmap);
}
private void configureCoordinates(float posXOnScreen, float posYOnScreen) {
@@ -166,16 +223,25 @@
mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 - verticalMagnifierOffset;
}
- private void performPixelCopy() {
- int startX = mCenterZoomCoords.x - mBitmap.getWidth() / 2;
+ private void maybePerformPixelCopy(final float scale, final boolean forceShow) {
+ final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;
+ int rawStartX = mCenterZoomCoords.x - mBitmap.getWidth() / 2;
+
// Clamp startX value to avoid distorting the rendering of the magnifier content.
- if (startX < 0) {
- startX = 0;
- } else if (startX + mBitmap.getWidth() > mView.getWidth()) {
- startX = mView.getWidth() - mBitmap.getWidth();
+ if (rawStartX < 0) {
+ rawStartX = 0;
+ } else if (rawStartX + mBitmap.getWidth() > mView.getWidth()) {
+ rawStartX = mView.getWidth() - mBitmap.getWidth();
}
- final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;
+ if (!forceShow && rawStartX == mPrevStartCoordsOnScreen.x
+ && startY == mPrevStartCoordsOnScreen.y
+ && scale == mPrevScale) {
+ // Skip, we are already showing the desired content.
+ return;
+ }
+
+ final int startX = rawStartX;
final ViewRootImpl viewRootImpl = mView.getViewRootImpl();
if (viewRootImpl != null && viewRootImpl.mSurface != null
@@ -185,7 +251,11 @@
new Rect(startX, startY, startX + mBitmap.getWidth(),
startY + mBitmap.getHeight()),
mBitmap,
- result -> getImageView().invalidate(),
+ result -> {
+ getImageView().invalidate();
+ mPrevStartCoordsOnScreen.x = startX;
+ mPrevStartCoordsOnScreen.y = startY;
+ },
mPixelCopyHandler);
} else {
Log.d(LOG_TAG, "Could not perform PixelCopy request");