diff options
| -rw-r--r-- | core/java/android/widget/SelectionActionModeHelper.java | 65 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java | 151 |
2 files changed, 209 insertions, 7 deletions
diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java index 041b515f7e47..81a5eadf8173 100644 --- a/core/java/android/widget/SelectionActionModeHelper.java +++ b/core/java/android/widget/SelectionActionModeHelper.java @@ -56,7 +56,7 @@ import java.util.function.Supplier; */ @UiThread @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) -final class SelectionActionModeHelper { +public final class SelectionActionModeHelper { /** * Maximum time (in milliseconds) to wait for a result before timing out. @@ -262,17 +262,66 @@ final class SelectionActionModeHelper { private List<RectF> convertSelectionToRectangles(final Layout layout, final int start, final int end) { final List<RectF> result = new ArrayList<>(); - // TODO filter out invalid rectangles - // getSelection might give us overlapping and zero-dimension rectangles which will interfere - // with the Smart Select animation layout.getSelection(start, end, (left, top, right, bottom, textSelectionLayout) -> - result.add(new RectF(left, top, right, bottom))); + mergeRectangleIntoList(result, new RectF(left, top, right, bottom))); result.sort(SmartSelectSprite.RECTANGLE_COMPARATOR); - return result; } + /** + * Merges a {@link RectF} into an existing list of rectangles. While merging, this method + * makes sure that: + * + * <ol> + * <li>No rectangle is redundant (contained within a bigger rectangle)</li> + * <li>Rectangles of the same height and vertical position that intersect get merged</li> + * </ol> + * + * @param list the list of rectangles to merge the new rectangle in + * @param candidate the {@link RectF} to merge into the list + * @hide + */ + @VisibleForTesting + public static void mergeRectangleIntoList(List<RectF> list, RectF candidate) { + if (candidate.isEmpty()) { + return; + } + + final int elementCount = list.size(); + for (int index = 0; index < elementCount; ++index) { + final RectF existingRectangle = list.get(index); + if (existingRectangle.contains(candidate)) { + return; + } + if (candidate.contains(existingRectangle)) { + existingRectangle.setEmpty(); + continue; + } + + final boolean rectanglesContinueEachOther = candidate.left == existingRectangle.right + || candidate.right == existingRectangle.left; + final boolean canMerge = candidate.top == existingRectangle.top + && candidate.bottom == existingRectangle.bottom + && (RectF.intersects(candidate, existingRectangle) + || rectanglesContinueEachOther); + + if (canMerge) { + candidate.union(existingRectangle); + existingRectangle.setEmpty(); + } + } + + for (int index = elementCount - 1; index >= 0; --index) { + if (list.get(index).isEmpty()) { + list.remove(index); + } + } + + list.add(candidate); + } + + /** @hide */ @VisibleForTesting public static PointF movePointInsideNearestRectangle(final PointF point, @@ -281,7 +330,9 @@ final class SelectionActionModeHelper { float bestY = -1; double bestDistance = Double.MAX_VALUE; - for (final RectF rectangle : rectangles) { + final int elementCount = rectangles.size(); + for (int index = 0; index < elementCount; ++index) { + final RectF rectangle = rectangles.get(index); final float candidateY = rectangle.centerY(); final float candidateX; diff --git a/core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java b/core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java index d94a017c27fd..b881053a5872 100644 --- a/core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java +++ b/core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java @@ -25,6 +25,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -110,4 +111,154 @@ public final class SelectionActionModeHelperTest { assertEquals(expectedPointY, adjustedPoint.y, 0.0f); } + @Test + public void testMergeRectangleIntoList_addThreeDisjointRectangles() { + testExpandRectangleList( + new RectF[] { + new RectF(0, 0, 1, 1), + new RectF(10, 10, 11, 11), + new RectF(20, 20, 21, 21) + }, + new RectF[] { + new RectF(0, 0, 1, 1), + new RectF(10, 10, 11, 11), + new RectF(20, 20, 21, 21) + } + ); + } + + @Test + public void testMergeRectangleIntoList_addAnEmptyRectangle() { + testExpandRectangleList( + new RectF[] { + new RectF(0, 0, 0, 0) + }, + new RectF[] { + } + ); + } + + @Test + public void testMergeRectangleIntoList_addAContainedRectangle() { + testExpandRectangleList( + new RectF[] { + new RectF(0, 0, 10, 10), + new RectF(9, 0, 10, 10) + }, + new RectF[] { + new RectF(0, 0, 10, 10) + } + ); + } + + @Test + public void testMergeRectangleIntoList_addARectangleThatContainsExistingRectangles() { + testExpandRectangleList( + new RectF[] { + new RectF(0, 0, 1, 1), + new RectF(1, 0, 2, 1), + new RectF(0, 0, 2, 1) + }, + new RectF[] { + new RectF(0, 0, 2, 1) + } + ); + } + + @Test + public void testMergeRectangleIntoList_addRectangleThatIntersectsAndHasTheSameHeightOnRight() { + testExpandRectangleList( + new RectF[] { + new RectF(0, 0, 1, 1), + new RectF(0.5f, 0, 1.5f, 1) + }, + new RectF[] { + new RectF(0, 0, 1.5f, 1) + } + ); + } + + @Test + public void testMergeRectangleIntoList_addRectangleThatIntersectsAndHasTheSameHeightOnLeft() { + testExpandRectangleList( + new RectF[] { + new RectF(0.5f, 0, 1.5f, 1), + new RectF(0, 0, 1, 1) + }, + new RectF[] { + new RectF(0, 0, 1.5f, 1) + } + ); + } + + @Test + public void testMergeRectangleIntoList_addRectangleThatExpandsToTheRight() { + testExpandRectangleList( + new RectF[] { + new RectF(0, 0, 1, 1), + new RectF(1, 0, 1.5f, 1) + }, + new RectF[] { + new RectF(0, 0, 1.5f, 1) + } + ); + } + + @Test + public void testMergeRectangleIntoList_addRectangleThatExpandsToTheLeft() { + testExpandRectangleList( + new RectF[] { + new RectF(1, 0, 1.5f, 1), + new RectF(0, 0, 1, 1) + }, + new RectF[] { + new RectF(0, 0, 1.5f, 1) + } + ); + } + + + @Test + public void testMergeRectangleIntoList_addRectangleMadeObsoleteByMultipleExistingRectangles() { + testExpandRectangleList( + new RectF[] { + new RectF(0, 0, 1, 1), + new RectF(0.5f, 0, 1.5f, 1), + new RectF(0.25f, 0, 1.25f, 1) + }, + new RectF[] { + new RectF(0, 0, 1.5f, 1) + } + ); + } + + @Test + public void testMergeRectangleIntoList_threeRectanglesThatTouchEachOther() { + testExpandRectangleList( + new RectF[] { + new RectF(0, 0, 1, 1), + new RectF(1, 0, 2, 1), + new RectF(2, 0, 3, 1) + }, + new RectF[] { + new RectF(0, 0, 3, 1) + } + ); + } + + + private void testExpandRectangleList(final RectF[] inputRectangles, + final RectF[] outputRectangles) { + final List<RectF> expectedOutput = Arrays.asList(outputRectangles); + + final List<RectF> result = new ArrayList<>(); + final int size = inputRectangles.length; + for (int index = 0; index < size; ++index) { + SelectionActionModeHelper.mergeRectangleIntoList(result, inputRectangles[index]); + } + + assertEquals(expectedOutput, result); + } + + } |