Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ba1b8566 authored by Petar Šegina's avatar Petar Šegina
Browse files

Filter out empty and unnecessary rectangles

Layout::getSelection will sometimes give us rectangles which are
redundant - specifically, those with dimension zero and those that may
already be contained in some other rectangles. In order to keep the
smart select animation simple and in line with what the user would
visually expect, we remove these rectangles.

Test: manual - verify smart select animation over multiple lines
Test: bit FrameworksCoreTests:android.widget.SelectionActionModeHelperTest
Change-Id: Ia4406d9505366245609b44e3ec3743f84820ba0a
parent 0a227782
Loading
Loading
Loading
Loading
+58 −7
Original line number Diff line number Diff line
@@ -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;

+151 −0
Original line number Diff line number Diff line
@@ -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);
    }


}