Loading core/java/android/widget/SelectionActionModeHelper.java +58 −7 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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, Loading @@ -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; Loading core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java +151 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } } Loading
core/java/android/widget/SelectionActionModeHelper.java +58 −7 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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, Loading @@ -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; Loading
core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java +151 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } }