summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/widget/Editor.java14
-rw-r--r--core/java/android/widget/SelectionActionModeHelper.java50
-rw-r--r--core/java/android/widget/SmartSelectSprite.java55
-rw-r--r--core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java113
4 files changed, 205 insertions, 27 deletions
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index d02d6ff9a9b0..7fa80666d053 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -260,6 +260,7 @@ public class Editor {
private PositionListener mPositionListener;
private float mLastDownPositionX, mLastDownPositionY;
+ private float mLastUpPositionX, mLastUpPositionY;
private float mContextMenuAnchorX, mContextMenuAnchorY;
Callback mCustomSelectionActionModeCallback;
Callback mCustomInsertionActionModeCallback;
@@ -1130,6 +1131,14 @@ public class Editor {
return handled;
}
+ float getLastUpPositionX() {
+ return mLastUpPositionX;
+ }
+
+ float getLastUpPositionY() {
+ return mLastUpPositionY;
+ }
+
private long getLastTouchOffsets() {
SelectionModifierCursorController selectionController = getSelectionController();
final int minOffset = selectionController.getMinTouchOffset();
@@ -1371,6 +1380,11 @@ public class Editor {
mShowSuggestionRunnable = null;
}
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ mLastUpPositionX = event.getX();
+ mLastUpPositionY = event.getY();
+ }
+
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mLastDownPositionX = event.getX();
mLastDownPositionY = event.getY();
diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java
index 5e70ef0b973f..2561ffe572ab 100644
--- a/core/java/android/widget/SelectionActionModeHelper.java
+++ b/core/java/android/widget/SelectionActionModeHelper.java
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.annotation.WorkerThread;
+import android.graphics.PointF;
import android.graphics.RectF;
import android.os.AsyncTask;
import android.os.LocaleList;
@@ -27,13 +28,13 @@ import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.TextUtils;
-import android.util.Pair;
import android.view.ActionMode;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextSelection;
import android.widget.Editor.SelectionModifierCursorController;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
@@ -45,8 +46,10 @@ import java.util.function.Supplier;
/**
* Helper class for starting selection action mode
* (synchronously without the TextClassifier, asynchronously with the TextClassifier).
+ * @hide
*/
@UiThread
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
final class SelectionActionModeHelper {
/**
@@ -224,15 +227,15 @@ final class SelectionActionModeHelper {
rectangle.bottom += textView.getPaddingTop();
}
- final RectF firstRectangle = selectionRectangles.get(0);
+ final PointF touchPoint = new PointF(
+ mEditor.getLastUpPositionX(),
+ mEditor.getLastUpPositionY());
- // TODO use the original touch point instead of the hardcoded point generated here
- final Pair<Float, Float> halfPoint = new Pair<>(
- firstRectangle.centerX(),
- firstRectangle.centerY());
+ final PointF animationStartPoint =
+ movePointInsideNearestRectangle(touchPoint, selectionRectangles);
mSmartSelectSprite.startAnimation(
- halfPoint,
+ animationStartPoint,
selectionRectangles,
onAnimationEndCallback);
}
@@ -248,6 +251,39 @@ final class SelectionActionModeHelper {
return result;
}
+ /** @hide */
+ @VisibleForTesting
+ public static PointF movePointInsideNearestRectangle(final PointF point,
+ final List<RectF> rectangles) {
+ float bestX = -1;
+ float bestY = -1;
+ double bestDistance = Double.MAX_VALUE;
+
+ for (final RectF rectangle : rectangles) {
+ final float candidateY = rectangle.centerY();
+ final float candidateX;
+
+ if (point.x > rectangle.right) {
+ candidateX = rectangle.right;
+ } else if (point.x < rectangle.left) {
+ candidateX = rectangle.left;
+ } else {
+ candidateX = point.x;
+ }
+
+ final double candidateDistance = Math.pow(point.x - candidateX, 2)
+ + Math.pow(point.y - candidateY, 2);
+
+ if (candidateDistance < bestDistance) {
+ bestX = candidateX;
+ bestY = candidateY;
+ bestDistance = candidateDistance;
+ }
+ }
+
+ return new PointF(bestX, bestY);
+ }
+
private void invalidateActionMode(@Nullable SelectionResult result) {
cancelSmartSelectAnimation();
mTextClassification = result != null ? result.mClassification : null;
diff --git a/core/java/android/widget/SmartSelectSprite.java b/core/java/android/widget/SmartSelectSprite.java
index e641a9bc5176..94109d741fd8 100644
--- a/core/java/android/widget/SmartSelectSprite.java
+++ b/core/java/android/widget/SmartSelectSprite.java
@@ -30,11 +30,11 @@ import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
+import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.Shape;
-import android.util.Pair;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewOverlay;
@@ -82,16 +82,16 @@ final class SmartSelectSprite {
private final float[] mLineCoordinates;
- private PolygonShape(final List<Pair<Float, Float>> points) {
+ private PolygonShape(final List<PointF> points) {
mLineCoordinates = new float[points.size() * POINTS_PER_LINE];
int index = 0;
- Pair<Float, Float> currentPoint = points.get(0);
- for (final Pair<Float, Float> nextPoint : points) {
- mLineCoordinates[index] = currentPoint.first;
- mLineCoordinates[index + 1] = currentPoint.second;
- mLineCoordinates[index + 2] = nextPoint.first;
- mLineCoordinates[index + 3] = nextPoint.second;
+ PointF currentPoint = points.get(0);
+ for (final PointF nextPoint : points) {
+ mLineCoordinates[index] = currentPoint.x;
+ mLineCoordinates[index + 1] = currentPoint.y;
+ mLineCoordinates[index + 2] = nextPoint.x;
+ mLineCoordinates[index + 3] = nextPoint.y;
index += POINTS_PER_LINE;
currentPoint = nextPoint;
@@ -342,9 +342,9 @@ final class SmartSelectSprite {
final List<RectF> rectangles,
final int color) {
final List<Drawable> drawables = new LinkedList<>();
- final Set<List<Pair<Float, Float>>> mergedPaths = calculateMergedPolygonPoints(rectangles);
+ final Set<List<PointF>> mergedPaths = calculateMergedPolygonPoints(rectangles);
- for (List<Pair<Float, Float>> path : mergedPaths) {
+ for (List<PointF> path : mergedPaths) {
// Add the starting point to the end of the polygon so that it ends up closed.
path.add(path.get(0));
@@ -361,7 +361,7 @@ final class SmartSelectSprite {
return drawables;
}
- private static Set<List<Pair<Float, Float>>> calculateMergedPolygonPoints(
+ private static Set<List<PointF>> calculateMergedPolygonPoints(
List<RectF> rectangles) {
final Set<List<RectF>> partitions = new HashSet<>();
final LinkedList<RectF> listOfRects = new LinkedList<>(rectangles);
@@ -389,20 +389,20 @@ final class SmartSelectSprite {
partitions.add(partition);
}
- final Set<List<Pair<Float, Float>>> result = new HashSet<>();
+ final Set<List<PointF>> result = new HashSet<>();
for (List<RectF> partition : partitions) {
- final List<Pair<Float, Float>> points = new LinkedList<>();
+ final List<PointF> points = new LinkedList<>();
final Stack<RectF> rects = new Stack<>();
for (RectF rect : partition) {
- points.add(new Pair<>(rect.right, rect.top));
- points.add(new Pair<>(rect.right, rect.bottom));
+ points.add(new PointF(rect.right, rect.top));
+ points.add(new PointF(rect.right, rect.bottom));
rects.add(rect);
}
while (!rects.isEmpty()) {
final RectF rect = rects.pop();
- points.add(new Pair<>(rect.left, rect.bottom));
- points.add(new Pair<>(rect.left, rect.top));
+ points.add(new PointF(rect.left, rect.bottom));
+ points.add(new PointF(rect.left, rect.top));
}
result.add(points);
@@ -426,7 +426,7 @@ final class SmartSelectSprite {
* @see #cancelAnimation()
*/
public void startAnimation(
- final Pair<Float, Float> start,
+ final PointF start,
final List<RectF> destinationRectangles,
final Runnable onAnimationEnd) throws IllegalArgumentException {
cancelAnimation();
@@ -439,7 +439,7 @@ final class SmartSelectSprite {
final RectF centerRectangle = destinationRectangles
.stream()
- .filter((r) -> r.contains(start.first, start.second))
+ .filter((r) -> contains(r, start))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(
"Center point is not inside any of the rectangles!"));
@@ -452,7 +452,7 @@ final class SmartSelectSprite {
startingOffset += rectangle.width();
}
- startingOffset += start.first - centerRectangle.left;
+ startingOffset += start.x - centerRectangle.left;
final float centerRectangleHalfHeight = centerRectangle.height() / 2;
final float startingOffsetLeft = startingOffset - centerRectangleHalfHeight;
@@ -632,6 +632,21 @@ final class SmartSelectSprite {
return result;
}
+ /**
+ * A variant of {@link RectF#contains(float, float)} that also allows the point to reside on
+ * the right boundary of the rectangle.
+ *
+ * @param rectangle the rectangle inside which the point should be to be considered "contained"
+ * @param point the point which will be tested
+ * @return whether the point is inside the rectangle (or on it's right boundary)
+ */
+ private static boolean contains(final RectF rectangle, final PointF point) {
+ final float x = point.x;
+ final float y = point.y;
+ return x >= rectangle.left && x <= rectangle.right && y >= rectangle.top
+ && y <= rectangle.bottom;
+ }
+
private void addToOverlay(final Drawable drawable) {
mView.getOverlay().add(drawable);
mExistingAnimationDrawables.add(drawable);
diff --git a/core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java b/core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java
new file mode 100644
index 000000000000..d94a017c27fd
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.PointF;
+import android.graphics.RectF;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public final class SelectionActionModeHelperTest {
+
+ /*
+ * The test rectangle set is composed of three 1x1 rectangles as illustrated below.
+ *
+ * (0, 0) ____________ (100001, 0)
+ * |█ █|
+ * |_█________|
+ * (0, 2) (100001, 2)
+ */
+ private final List<RectF> mRectFList = Arrays.asList(
+ new RectF(0, 0, 1, 1),
+ new RectF(100000, 0, 100001, 1),
+ new RectF(1, 1, 2, 2));
+
+ @Test
+ public void testMovePointInsideNearestRectangle_pointIsInsideRectangle() {
+ testMovePointInsideNearestRectangle(
+ 0.1f /* pointX */,
+ 0.1f /* pointY */,
+ 0.1f /* expectedPointX */,
+ 0.5f /* expectedPointY */);
+ }
+
+ @Test
+ public void testMovePointInsideNearestRectangle_pointIsAboveRectangle() {
+ testMovePointInsideNearestRectangle(
+ 0.1f /* pointX */,
+ -1.0f /* pointY */,
+ 0.1f /* expectedPointX */,
+ 0.5f /* expectedPointY */);
+ }
+
+ @Test
+ public void testMovePointInsideNearestRectangle_pointIsLeftOfRectangle() {
+ testMovePointInsideNearestRectangle(
+ -1.0f /* pointX */,
+ 0.4f /* pointY */,
+ 0.0f /* expectedPointX */,
+ 0.5f /* expectedPointY */);
+ }
+
+ @Test
+ public void testMovePointInsideNearestRectangle_pointIsRightOfRectangle() {
+ testMovePointInsideNearestRectangle(
+ 1.1f /* pointX */,
+ 0.0f /* pointY */,
+ 1.0f /* expectedPointX */,
+ 0.5f /* expectedPointY */);
+ }
+
+ @Test
+ public void testMovePointInsideNearestRectangle_pointIsBelowRectangle() {
+ testMovePointInsideNearestRectangle(
+ 0.1f /* pointX */,
+ 1.1f /* pointY */,
+ 0.1f /* expectedPointX */,
+ 0.5f /* expectedPointY */);
+ }
+
+ @Test
+ public void testMovePointInsideNearestRectangle_pointIsToRightOfTheRightmostRectangle() {
+ testMovePointInsideNearestRectangle(
+ 200000.0f /* pointX */,
+ 0.1f /* pointY */,
+ 100001.0f /* expectedPointX */,
+ 0.5f /* expectedPointY */);
+ }
+
+ private void testMovePointInsideNearestRectangle(final float pointX, final float pointY,
+ final float expectedPointX,
+ final float expectedPointY) {
+ final PointF point = new PointF(pointX, pointY);
+ final PointF adjustedPoint =
+ SelectionActionModeHelper.movePointInsideNearestRectangle(point,
+ mRectFList);
+
+ assertEquals(expectedPointX, adjustedPoint.x, 0.0f);
+ assertEquals(expectedPointY, adjustedPoint.y, 0.0f);
+ }
+
+}