[Magnifier - 5] Auto invalidate and misc
* due to having to make the API public, drop the
invalidate_by_client strategy in favor of self invalidation
by polling
* in Magnifier#show(...) stop sending screen coordinates in favor
of view coordinates
* UI/UX:
** width from 200dp to 164dp
** fixed the shadow depth (elevation)
** fixed the rounded corners (2dp)
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: I6451767b0a618c0c4ea396ba3ee3144523dadc57
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index d4be7e5..4cb49a3 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -476,17 +476,6 @@
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++) {
@@ -4550,17 +4539,15 @@
final Layout layout = mTextView.getLayout();
final int lineNumber = layout.getLineForOffset(offset);
// Horizontally snap to character offset.
- final float xPosInView = getHorizontal(mTextView.getLayout(), offset);
+ final float xPosInView = getHorizontal(mTextView.getLayout(), offset)
+ + mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
// Vertically snap to middle of current line.
final float yPosInView = (mTextView.getLayout().getLineTop(lineNumber)
- + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f;
- final int[] coordinatesOnScreen = new int[2];
- mTextView.getLocationOnScreen(coordinatesOnScreen);
- final float centerXOnScreen = mTextView.convertViewToScreenCoord(xPosInView, true);
- final float centerYOnScreen = mTextView.convertViewToScreenCoord(yPosInView, false);
+ + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f
+ + mTextView.getTotalPaddingTop() - mTextView.getScrollY();
suspendBlink();
- mMagnifier.show(centerXOnScreen, centerYOnScreen, MAGNIFIER_ZOOM);
+ mMagnifier.show(xPosInView, yPosInView, MAGNIFIER_ZOOM);
}
protected final void dismissMagnifier() {
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index ce80552..d9bc51f 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -9219,36 +9219,6 @@
}
}
- @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 9bc0778..6d54d7b 100644
--- a/core/java/com/android/internal/widget/Magnifier.java
+++ b/core/java/com/android/internal/widget/Magnifier.java
@@ -22,9 +22,7 @@
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;
@@ -38,13 +36,15 @@
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";
- // Use this to specify that a previous configuration value does not exist.
- private static final int INEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
+ private static final int MAGNIFIER_REFRESH_RATE_MS = 33; // ~30fps
// The view for which this magnifier is attached.
private final View mView;
// The window containing the magnifier.
@@ -62,15 +62,10 @@
// The callback of the pixel copy request will be invoked on this Handler when
// 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;
+ // Current magnification scale.
+ private float mScale;
+ // Timer used to schedule the copy task.
+ private Timer mTimer;
/**
* Initializes a magnifier.
@@ -82,6 +77,7 @@
mView = Preconditions.checkNotNull(view);
final Context context = mView.getContext();
final View content = LayoutInflater.from(context).inflate(R.layout.magnifier, null);
+ content.findViewById(R.id.magnifier_inner).setClipToOutline(true);
mWindowWidth = context.getResources().getDimensionPixelSize(R.dimen.magnifier_width);
mWindowHeight = context.getResources().getDimensionPixelSize(R.dimen.magnifier_height);
final float elevation = context.getResources().getDimension(R.dimen.magnifier_elevation);
@@ -101,15 +97,15 @@
/**
* Shows the magnifier on the screen.
*
- * @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 xPosInView horizontal coordinate of the center point of the magnifier source relative
+ * to the view. The lower end is clamped to 0
+ * @param yPosInView vertical coordinate of the center point of the magnifier source
+ * relative to the view. 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,
+ public void show(@FloatRange(from=0) float xPosInView,
+ @FloatRange(from=0) float yPosInView,
@FloatRange(from=1, to=4) float scale) {
if (scale > 4) {
scale = 4;
@@ -119,27 +115,29 @@
scale = 1;
}
- if (centerXOnScreen < 0) {
- centerXOnScreen = 0;
+ if (xPosInView < 0) {
+ xPosInView = 0;
}
- if (centerYOnScreen < 0) {
- centerYOnScreen = 0;
+ if (yPosInView < 0) {
+ yPosInView = 0;
}
- 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) {
+ if (mScale != scale) {
resizeBitmap(scale);
- mPrevScale = scale;
}
- configureCoordinates(centerXOnScreen, centerYOnScreen);
- maybePerformPixelCopy(scale, forceShow);
+ mScale = scale;
+ configureCoordinates(xPosInView, yPosInView);
+
+ if (mTimer == null) {
+ mTimer = new Timer();
+ mTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ performPixelCopy();
+ }
+ }, 0 /* delay */, MAGNIFIER_REFRESH_RATE_MS);
+ }
if (mWindow.isShowing()) {
mWindow.update(mWindowCoords.x, mWindowCoords.y, mWindow.getWidth(),
@@ -148,9 +146,6 @@
mWindow.showAtLocation(mView.getRootView(), Gravity.NO_GRAVITY,
mWindowCoords.x, mWindowCoords.y);
}
-
- mPrevCenterCoordsOnScreen.x = centerXOnScreen;
- mPrevCenterCoordsOnScreen.y = centerYOnScreen;
}
/**
@@ -159,36 +154,10 @@
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 */);
- }
+ if (mTimer != null) {
+ mTimer.cancel();
+ mTimer.purge();
+ mTimer = null;
}
}
@@ -213,7 +182,12 @@
getImageView().setImageBitmap(mBitmap);
}
- private void configureCoordinates(float posXOnScreen, float posYOnScreen) {
+ 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];
+
mCenterZoomCoords.x = (int) posXOnScreen;
mCenterZoomCoords.y = (int) posYOnScreen;
@@ -223,7 +197,7 @@
mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 - verticalMagnifierOffset;
}
- private void maybePerformPixelCopy(final float scale, final boolean forceShow) {
+ private void performPixelCopy() {
final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;
int rawStartX = mCenterZoomCoords.x - mBitmap.getWidth() / 2;
@@ -234,13 +208,6 @@
rawStartX = mView.getWidth() - mBitmap.getWidth();
}
- 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();
@@ -251,11 +218,7 @@
new Rect(startX, startY, startX + mBitmap.getWidth(),
startY + mBitmap.getHeight()),
mBitmap,
- result -> {
- getImageView().invalidate();
- mPrevStartCoordsOnScreen.x = startX;
- mPrevStartCoordsOnScreen.y = startY;
- },
+ result -> getImageView().invalidate(),
mPixelCopyHandler);
} else {
Log.d(LOG_TAG, "Could not perform PixelCopy request");
diff --git a/core/res/res/layout/magnifier.xml b/core/res/res/layout/magnifier.xml
index 181e5e5..d6cd8b4 100644
--- a/core/res/res/layout/magnifier.xml
+++ b/core/res/res/layout/magnifier.xml
@@ -18,10 +18,17 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="?android:attr/floatingToolbarPopupBackgroundDrawable">
- <ImageView
- android:id="@+id/magnifier_image"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
+ android:layout_height="wrap_content">
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/magnifier_inner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/floatingToolbarPopupBackgroundDrawable"
+ android:elevation="@android:dimen/magnifier_elevation">
+ <ImageView
+ android:id="@+id/magnifier_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+ </LinearLayout>
</LinearLayout>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 14069e7..08e2233 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -521,7 +521,7 @@
<dimen name="content_rect_bottom_clip_allowance">20dp</dimen>
<!-- Magnifier dimensions -->
- <dimen name="magnifier_width">200dp</dimen>
+ <dimen name="magnifier_width">164dp</dimen>
<dimen name="magnifier_height">48dp</dimen>
<dimen name="magnifier_elevation">2dp</dimen>
<dimen name="magnifier_offset">42dp</dimen>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index a42cf42c..a12187e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2480,6 +2480,7 @@
<!-- Magnifier -->
<java-symbol type="id" name="magnifier_image" />
+ <java-symbol type="id" name="magnifier_inner" />
<java-symbol type="layout" name="magnifier" />
<java-symbol type="dimen" name="magnifier_width" />
<java-symbol type="dimen" name="magnifier_height" />