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

Commit 7c8196f1 authored by Petar Šegina's avatar Petar Šegina
Browse files

Invert the animation over RTL selections

When smart select is performed over right-to-left text (either
standalone or mixed with left-to-right text), the animation should flow
in from the opposite side than what it does over left-to-right text.

Test: manual - verify smart select still works
Test: manual - manually set the layout flags to RTL and verify the
animation behaves as expected
Test: bit FrameworksCoreTests:android.widget.SelectionActionModeHelperTest

Change-Id: I2e7766e7dfe74e0861e91fc50b061da74432aee0
parent 0debe0b7
Loading
Loading
Loading
Loading
+42 −19
Original line number Diff line number Diff line
@@ -43,9 +43,11 @@ import com.android.internal.util.Preconditions;

import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;

@@ -232,7 +234,7 @@ public final class SelectionActionModeHelper {
            return;
        }

        final List<RectF> selectionRectangles =
        final List<SmartSelectSprite.RectangleWithTextSelectionLayout> selectionRectangles =
                convertSelectionToRectangles(layout, result.mStart, result.mEnd);

        final PointF touchPoint = new PointF(
@@ -240,7 +242,8 @@ public final class SelectionActionModeHelper {
                mEditor.getLastUpPositionY());

        final PointF animationStartPoint =
                movePointInsideNearestRectangle(touchPoint, selectionRectangles);
                movePointInsideNearestRectangle(touchPoint, selectionRectangles,
                        SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle);

        mSmartSelectSprite.startAnimation(
                animationStartPoint,
@@ -248,38 +251,57 @@ public final class SelectionActionModeHelper {
                onAnimationEndCallback);
    }

    private List<RectF> convertSelectionToRectangles(final Layout layout, final int start,
            final int end) {
        final List<RectF> result = new ArrayList<>();
        layout.getSelection(start, end, (left, top, right, bottom, textSelectionLayout) ->
                mergeRectangleIntoList(result, new RectF(left, top, right, bottom)));
    private List<SmartSelectSprite.RectangleWithTextSelectionLayout> convertSelectionToRectangles(
            final Layout layout, final int start, final int end) {
        final List<SmartSelectSprite.RectangleWithTextSelectionLayout> result = new ArrayList<>();

        final Layout.SelectionRectangleConsumer consumer =
                (left, top, right, bottom, textSelectionLayout) -> mergeRectangleIntoList(
                        result,
                        new RectF(left, top, right, bottom),
                        SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle,
                        r -> new SmartSelectSprite.RectangleWithTextSelectionLayout(r,
                                textSelectionLayout)
                );

        layout.getSelection(start, end, consumer);

        result.sort(Comparator.comparing(
                SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle,
                SmartSelectSprite.RECTANGLE_COMPARATOR));

        result.sort(SmartSelectSprite.RECTANGLE_COMPARATOR);
        return result;
    }

    /**
     * Merges a {@link RectF} into an existing list of rectangles. While merging, this method
     * makes sure that:
     * Merges a {@link RectF} into an existing list of any objects which contain a rectangle.
     * 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 list      the list of rectangles (or other rectangle containers) to merge the new
     *                  rectangle into
     * @param candidate the {@link RectF} to merge into the list
     * @param extractor a function that can extract a {@link RectF} from an element of the given
     *                  list
     * @param packer    a function that can wrap the resulting {@link RectF} into an element that
     *                  the list contains
     * @hide
     */
    @VisibleForTesting
    public static void mergeRectangleIntoList(List<RectF> list, RectF candidate) {
    public static <T> void mergeRectangleIntoList(final List<T> list,
            final RectF candidate, final Function<T, RectF> extractor,
            final Function<RectF, T> packer) {
        if (candidate.isEmpty()) {
            return;
        }

        final int elementCount = list.size();
        for (int index = 0; index < elementCount; ++index) {
            final RectF existingRectangle = list.get(index);
            final RectF existingRectangle = extractor.apply(list.get(index));
            if (existingRectangle.contains(candidate)) {
                return;
            }
@@ -302,26 +324,27 @@ public final class SelectionActionModeHelper {
        }

        for (int index = elementCount - 1; index >= 0; --index) {
            if (list.get(index).isEmpty()) {
            final RectF rectangle = extractor.apply(list.get(index));
            if (rectangle.isEmpty()) {
                list.remove(index);
            }
        }

        list.add(candidate);
        list.add(packer.apply(candidate));
    }


    /** @hide */
    @VisibleForTesting
    public static PointF movePointInsideNearestRectangle(final PointF point,
            final List<RectF> rectangles) {
    public static <T> PointF movePointInsideNearestRectangle(final PointF point,
            final List<T> list, final Function<T, RectF> extractor) {
        float bestX = -1;
        float bestY = -1;
        double bestDistance = Double.MAX_VALUE;

        final int elementCount = rectangles.size();
        final int elementCount = list.size();
        for (int index = 0; index < elementCount; ++index) {
            final RectF rectangle = rectangles.get(index);
            final RectF rectangle = extractor.apply(list.get(index));
            final float candidateY = rectangle.centerY();
            final float candidateX;

+102 −40
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.Shape;
import android.text.Layout;
import android.util.TypedValue;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
@@ -42,9 +43,9 @@ import android.view.animation.Interpolator;
import com.android.internal.util.Preconditions;

import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;

/**
@@ -76,6 +77,26 @@ final class SmartSelectSprite {
    private Drawable mExistingDrawable = null;
    private RectangleList mExistingRectangleList = null;

    static final class RectangleWithTextSelectionLayout {
        private final RectF mRectangle;
        @Layout.TextSelectionLayout
        private final int mTextSelectionLayout;

        RectangleWithTextSelectionLayout(RectF rectangle, int textSelectionLayout) {
            mRectangle = Preconditions.checkNotNull(rectangle);
            mTextSelectionLayout = textSelectionLayout;
        }

        public RectF getRectangle() {
            return mRectangle;
        }

        @Layout.TextSelectionLayout
        public int getTextSelectionLayout() {
            return mTextSelectionLayout;
        }
    }

    /**
     * A rounded rectangle with a configurable corner radius and the ability to expand outside of
     * its bounding rectangle and clip against it.
@@ -84,12 +105,23 @@ final class SmartSelectSprite {

        private static final String PROPERTY_ROUND_RATIO = "roundRatio";

        /**
         * The direction in which the rectangle will perform its expansion. A rectangle can expand
         * from its left edge, its right edge or from the center (or, more precisely, the user's
         * touch point). For example, in left-to-right text, a selection spanning two lines with the
         * user's action being on the first line will have the top rectangle and expansion direction
         * of CENTER, while the bottom one will have an expansion direction of RIGHT.
         */
        @Retention(SOURCE)
        @IntDef({ExpansionDirection.LEFT, ExpansionDirection.CENTER, ExpansionDirection.RIGHT})
        private @interface ExpansionDirection {
        int LEFT = 0;
        int CENTER = 1;
        int RIGHT = 2;
            int LEFT = -1;
            int CENTER = 0;
            int RIGHT = 1;
        }

        private static @ExpansionDirection int invert(@ExpansionDirection int expansionDirection) {
            return expansionDirection * -1;
        }

        @Retention(SOURCE)
@@ -119,15 +151,28 @@ final class SmartSelectSprite {
        /** How far offset the right edge of the rectangle is from the bounding box. */
        private float mRightBoundary = 0;

        /** Whether the horizontal bounds are inverted (for RTL scenarios). */
        private final boolean mInverted;

        private final float mBoundingWidth;

        private RoundedRectangleShape(
                final RectF boundingRectangle,
                final @ExpansionDirection int expansionDirection,
                final @RectangleBorderType int rectangleBorderType,
                final boolean inverted,
                final float strokeWidth) {
            mBoundingRectangle = new RectF(boundingRectangle);
            mExpansionDirection = expansionDirection;
            mBoundingWidth = boundingRectangle.width();
            mRectangleBorderType = rectangleBorderType;
            mStrokeWidth = strokeWidth;
            mInverted = inverted && expansionDirection != ExpansionDirection.CENTER;

            if (inverted) {
                mExpansionDirection = invert(expansionDirection);
            } else {
                mExpansionDirection = expansionDirection;
            }

            if (boundingRectangle.height() > boundingRectangle.width()) {
                setRoundRatio(0.0f);
@@ -190,20 +235,28 @@ final class SmartSelectSprite {
            canvas.restore();
        }

        public void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) {
        void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) {
            mRoundRatio = roundRatio;
        }

        public float getRoundRatio() {
        float getRoundRatio() {
            return mRoundRatio;
        }

        private void setLeftBoundary(final float leftBoundary) {
            mLeftBoundary = leftBoundary;
        private void setStartBoundary(final float startBoundary) {
            if (mInverted) {
                mRightBoundary = mBoundingWidth - startBoundary;
            } else {
                mLeftBoundary = startBoundary;
            }
        }

        private void setRightBoundary(final float rightBoundary) {
            mRightBoundary = rightBoundary;
        private void setEndBoundary(final float endBoundary) {
            if (mInverted) {
                mLeftBoundary = mBoundingWidth - endBoundary;
            } else {
                mRightBoundary = endBoundary;
            }
        }

        private float getCornerRadius() {
@@ -247,8 +300,8 @@ final class SmartSelectSprite {
        private @DisplayType int mDisplayType = DisplayType.RECTANGLES;

        private RectangleList(final List<RoundedRectangleShape> rectangles) {
            mRectangles = new LinkedList<>(rectangles);
            mReversedRectangles = new LinkedList<>(rectangles);
            mRectangles = new ArrayList<>(rectangles);
            mReversedRectangles = new ArrayList<>(rectangles);
            Collections.reverse(mReversedRectangles);
            mOutlinePolygonPath = generateOutlinePolygonPath(rectangles);
        }
@@ -258,11 +311,11 @@ final class SmartSelectSprite {
            for (RoundedRectangleShape rectangle : mReversedRectangles) {
                final float rectangleLeftBoundary = boundarySoFar - rectangle.getBoundingWidth();
                if (leftBoundary < rectangleLeftBoundary) {
                    rectangle.setLeftBoundary(0);
                    rectangle.setStartBoundary(0);
                } else if (leftBoundary > boundarySoFar) {
                    rectangle.setLeftBoundary(rectangle.getBoundingWidth());
                    rectangle.setStartBoundary(rectangle.getBoundingWidth());
                } else {
                    rectangle.setLeftBoundary(
                    rectangle.setStartBoundary(
                            rectangle.getBoundingWidth() - boundarySoFar + leftBoundary);
                }

@@ -275,11 +328,11 @@ final class SmartSelectSprite {
            for (RoundedRectangleShape rectangle : mRectangles) {
                final float rectangleRightBoundary = rectangle.getBoundingWidth() + boundarySoFar;
                if (rectangleRightBoundary < rightBoundary) {
                    rectangle.setRightBoundary(rectangle.getBoundingWidth());
                    rectangle.setEndBoundary(rectangle.getBoundingWidth());
                } else if (boundarySoFar > rightBoundary) {
                    rectangle.setRightBoundary(0);
                    rectangle.setEndBoundary(0);
                } else {
                    rectangle.setRightBoundary(rightBoundary - boundarySoFar);
                    rectangle.setEndBoundary(rightBoundary - boundarySoFar);
                }

                boundarySoFar = rectangleRightBoundary;
@@ -331,8 +384,8 @@ final class SmartSelectSprite {
    }

    /**
     * @param context     The {@link Context} in which the animation will run
     * @param invalidator A {@link Runnable} which will be called every time the animation updates,
     * @param context     the {@link Context} in which the animation will run
     * @param invalidator a {@link Runnable} which will be called every time the animation updates,
     *                    indicating that the view drawing the animation should invalidate itself
     */
    SmartSelectSprite(final Context context, final Runnable invalidator) {
@@ -356,30 +409,36 @@ final class SmartSelectSprite {
     *                              "selection" and finally join them into a single polygon. In
     *                              order to get the correct visual behavior, these rectangles
     *                              should be sorted according to {@link #RECTANGLE_COMPARATOR}.
     * @param onAnimationEnd        The callback which will be invoked once the whole animation
     *                              completes.
     * @param onAnimationEnd        the callback which will be invoked once the whole animation
     *                              completes
     * @throws IllegalArgumentException if the given start point is not in any of the
     *                                  destinationRectangles.
     *                                  destinationRectangles
     * @see #cancelAnimation()
     */
    // TODO nullability checks on parameters
    public void startAnimation(
            final PointF start,
            final List<RectF> destinationRectangles,
            final List<RectangleWithTextSelectionLayout> destinationRectangles,
            final Runnable onAnimationEnd) {
        cancelAnimation();

        final ValueAnimator.AnimatorUpdateListener updateListener =
                valueAnimator -> mInvalidator.run();

        final List<RoundedRectangleShape> shapes = new LinkedList<>();
        final List<Animator> cornerAnimators = new LinkedList<>();
        final int rectangleCount = destinationRectangles.size();

        final List<RoundedRectangleShape> shapes = new ArrayList<>(rectangleCount);
        final List<Animator> cornerAnimators = new ArrayList<>(rectangleCount);

        RectF centerRectangle = null;
        RectangleWithTextSelectionLayout centerRectangle = null;

        int startingOffset = 0;
        for (RectF rectangle : destinationRectangles) {
        for (int index = 0; index < rectangleCount; ++index) {
            final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout =
                    destinationRectangles.get(index);
            final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle();
            if (contains(rectangle, start)) {
                centerRectangle = rectangle;
                centerRectangle = rectangleWithTextSelectionLayout;
                break;
            }
            startingOffset += rectangle.width();
@@ -389,9 +448,9 @@ final class SmartSelectSprite {
            throw new IllegalArgumentException("Center point is not inside any of the rectangles!");
        }

        startingOffset += start.x - centerRectangle.left;
        startingOffset += start.x - centerRectangle.getRectangle().left;

        final float centerRectangleHalfHeight = centerRectangle.height() / 2;
        final float centerRectangleHalfHeight = centerRectangle.getRectangle().height() / 2;
        final float startingOffsetLeft = startingOffset - centerRectangleHalfHeight;
        final float startingOffsetRight = startingOffset + centerRectangleHalfHeight;

@@ -399,19 +458,21 @@ final class SmartSelectSprite {
                generateDirections(centerRectangle, destinationRectangles);

        final @RoundedRectangleShape.RectangleBorderType int[] rectangleBorderTypes =
                generateBorderTypes(destinationRectangles);

        int index = 0;
                generateBorderTypes(rectangleCount);

        for (RectF rectangle : destinationRectangles) {
        for (int index = 0; index < rectangleCount; ++index) {
            final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout =
                    destinationRectangles.get(index);
            final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle();
            final RoundedRectangleShape shape = new RoundedRectangleShape(
                    rectangle,
                    expansionDirections[index],
                    rectangleBorderTypes[index],
                    rectangleWithTextSelectionLayout.getTextSelectionLayout()
                            == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT,
                    mStrokeWidth);
            cornerAnimators.add(createCornerAnimator(shape, updateListener));
            shapes.add(shape);
            index++;
        }

        final RectangleList rectangleList = new RectangleList(shapes);
@@ -511,7 +572,8 @@ final class SmartSelectSprite {
    }

    private static @RoundedRectangleShape.ExpansionDirection int[] generateDirections(
            final RectF centerRectangle, final List<RectF> rectangles) {
            final RectangleWithTextSelectionLayout centerRectangle,
            final List<RectangleWithTextSelectionLayout> rectangles) {
        final @RoundedRectangleShape.ExpansionDirection int[] result = new int[rectangles.size()];

        final int centerRectangleIndex = rectangles.indexOf(centerRectangle);
@@ -538,8 +600,8 @@ final class SmartSelectSprite {
    }

    private static @RoundedRectangleShape.RectangleBorderType int[] generateBorderTypes(
            final List<RectF> rectangles) {
        final @RoundedRectangleShape.RectangleBorderType int[] result = new int[rectangles.size()];
            final int numberOfRectangles) {
        final @RoundedRectangleShape.RectangleBorderType int[] result = new int[numberOfRectangles];

        for (int i = 1; i < result.length - 1; ++i) {
            result[i] = RoundedRectangleShape.RectangleBorderType.OVERSHOOT;
+5 −2
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package android.widget;

import static org.junit.Assert.assertEquals;

import static java.util.function.Function.identity;

import android.graphics.PointF;
import android.graphics.RectF;

@@ -105,7 +107,7 @@ public final class SelectionActionModeHelperTest {
        final PointF point = new PointF(pointX, pointY);
        final PointF adjustedPoint =
                SelectionActionModeHelper.movePointInsideNearestRectangle(point,
                        mRectFList);
                        mRectFList, identity());

        assertEquals(expectedPointX, adjustedPoint.x, 0.0f);
        assertEquals(expectedPointY, adjustedPoint.y, 0.0f);
@@ -254,7 +256,8 @@ public final class SelectionActionModeHelperTest {
        final List<RectF> result = new ArrayList<>();
        final int size = inputRectangles.length;
        for (int index = 0; index < size; ++index) {
            SelectionActionModeHelper.mergeRectangleIntoList(result, inputRectangles[index]);
            SelectionActionModeHelper.mergeRectangleIntoList(result, inputRectangles[index],
                    identity(), identity());
        }

        assertEquals(expectedOutput, result);