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

Commit 7d208271 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Invert the animation over RTL selections"

parents e5f03a66 7c8196f1
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;

@@ -226,7 +228,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(
@@ -234,7 +236,8 @@ public final class SelectionActionModeHelper {
                mEditor.getLastUpPositionY());

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

        mSmartSelectSprite.startAnimation(
                animationStartPoint,
@@ -242,39 +245,58 @@ 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;
    }

    // TODO: Move public pure functions out of this class and make it package-private.
    /**
     * 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;
            }
@@ -297,26 +319,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);