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

Commit 80620c52 authored by Jan Althaus's avatar Jan Althaus
Browse files

Updating smart text selection animation

Now animates the highlight itself as opposed to an outline.

Bug: 70540865
Test: Manually tested it with single and multi-line - ltr and rtl
Change-Id: I8afee259c9952fcff0b713bca62c82a1022f2b0d
parent 928835eb
Loading
Loading
Loading
Loading
+7 −4
Original line number Original line Diff line number Diff line
@@ -1745,16 +1745,19 @@ public class Editor {
            highlight = null;
            highlight = null;
        }
        }


        if (mSelectionActionModeHelper != null) {
            mSelectionActionModeHelper.onDraw(canvas);
            if (mSelectionActionModeHelper.isDrawingHighlight()) {
                highlight = null;
            }
        }

        if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
        if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
            drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
            drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
                    cursorOffsetVertical);
                    cursorOffsetVertical);
        } else {
        } else {
            layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
            layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
        }
        }

        if (mSelectionActionModeHelper != null) {
            mSelectionActionModeHelper.onDraw(canvas);
        }
    }
    }


    private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
    private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
+6 −2
Original line number Original line Diff line number Diff line
@@ -93,7 +93,7 @@ public final class SelectionActionModeHelper {


        if (SMART_SELECT_ANIMATION_ENABLED) {
        if (SMART_SELECT_ANIMATION_ENABLED) {
            mSmartSelectSprite = new SmartSelectSprite(mTextView.getContext(),
            mSmartSelectSprite = new SmartSelectSprite(mTextView.getContext(),
                    mTextView::invalidate);
                    editor.getTextView().mHighlightColor, mTextView::invalidate);
        } else {
        } else {
            mSmartSelectSprite = null;
            mSmartSelectSprite = null;
        }
        }
@@ -200,11 +200,15 @@ public final class SelectionActionModeHelper {
    }
    }


    public void onDraw(final Canvas canvas) {
    public void onDraw(final Canvas canvas) {
        if (mSmartSelectSprite != null) {
        if (isDrawingHighlight() && mSmartSelectSprite != null) {
            mSmartSelectSprite.draw(canvas);
            mSmartSelectSprite.draw(canvas);
        }
        }
    }
    }


    public boolean isDrawingHighlight() {
        return mSmartSelectSprite != null && mSmartSelectSprite.isAnimationActive();
    }

    private void cancelAsyncTask() {
    private void cancelAsyncTask() {
        if (mTextClassificationAsyncTask != null) {
        if (mTextClassificationAsyncTask != null) {
            mTextClassificationAsyncTask.cancel(true);
            mTextClassificationAsyncTask.cancel(true);
+25 −143
Original line number Original line Diff line number Diff line
@@ -26,7 +26,6 @@ import android.annotation.ColorInt;
import android.annotation.FloatRange;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.IntDef;
import android.content.Context;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Path;
@@ -36,7 +35,6 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.Shape;
import android.graphics.drawable.shapes.Shape;
import android.text.Layout;
import android.text.Layout;
import android.util.TypedValue;
import android.view.animation.AnimationUtils;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.view.animation.Interpolator;


@@ -54,21 +52,15 @@ import java.util.List;
final class SmartSelectSprite {
final class SmartSelectSprite {


    private static final int EXPAND_DURATION = 300;
    private static final int EXPAND_DURATION = 300;
    private static final int CORNER_DURATION = 150;
    private static final int CORNER_DURATION = 50;
    private static final float STROKE_WIDTH_DP = 1.5F;

    // GBLUE700
    @ColorInt
    private static final int DEFAULT_STROKE_COLOR = 0xFF3367D6;


    private final Interpolator mExpandInterpolator;
    private final Interpolator mExpandInterpolator;
    private final Interpolator mCornerInterpolator;
    private final Interpolator mCornerInterpolator;
    private final float mStrokeWidth;


    private Animator mActiveAnimator = null;
    private Animator mActiveAnimator = null;
    private final Runnable mInvalidator;
    private final Runnable mInvalidator;
    @ColorInt
    @ColorInt
    private final int mStrokeColor;
    private final int mFillColor;


    static final Comparator<RectF> RECTANGLE_COMPARATOR = Comparator
    static final Comparator<RectF> RECTANGLE_COMPARATOR = Comparator
            .<RectF>comparingDouble(e -> e.bottom)
            .<RectF>comparingDouble(e -> e.bottom)
@@ -124,26 +116,11 @@ final class SmartSelectSprite {
            return expansionDirection * -1;
            return expansionDirection * -1;
        }
        }


        @Retention(SOURCE)
        @IntDef({RectangleBorderType.FIT, RectangleBorderType.OVERSHOOT})
        private @interface RectangleBorderType {
        /** A rectangle which, fully expanded, fits inside of its bounding rectangle. */
        int FIT = 0;
        /**
         * A rectangle which, when fully expanded, clips outside of its bounding rectangle so that
         * its edges no longer appear rounded.
         */
        int OVERSHOOT = 1;
        }

        private final float mStrokeWidth;
        private final RectF mBoundingRectangle;
        private final RectF mBoundingRectangle;
        private float mRoundRatio = 1.0f;
        private float mRoundRatio = 1.0f;
        private final @ExpansionDirection int mExpansionDirection;
        private final @ExpansionDirection int mExpansionDirection;
        private final @RectangleBorderType int mRectangleBorderType;


        private final RectF mDrawRect = new RectF();
        private final RectF mDrawRect = new RectF();
        private final RectF mClipRect = new RectF();
        private final Path mClipPath = new Path();
        private final Path mClipPath = new Path();


        /** How offset the left edge of the rectangle is from the left side of the bounding box. */
        /** How offset the left edge of the rectangle is from the left side of the bounding box. */
@@ -159,13 +136,9 @@ final class SmartSelectSprite {
        private RoundedRectangleShape(
        private RoundedRectangleShape(
                final RectF boundingRectangle,
                final RectF boundingRectangle,
                final @ExpansionDirection int expansionDirection,
                final @ExpansionDirection int expansionDirection,
                final @RectangleBorderType int rectangleBorderType,
                final boolean inverted) {
                final boolean inverted,
                final float strokeWidth) {
            mBoundingRectangle = new RectF(boundingRectangle);
            mBoundingRectangle = new RectF(boundingRectangle);
            mBoundingWidth = boundingRectangle.width();
            mBoundingWidth = boundingRectangle.width();
            mRectangleBorderType = rectangleBorderType;
            mStrokeWidth = strokeWidth;
            mInverted = inverted && expansionDirection != ExpansionDirection.CENTER;
            mInverted = inverted && expansionDirection != ExpansionDirection.CENTER;


            if (inverted) {
            if (inverted) {
@@ -182,14 +155,8 @@ final class SmartSelectSprite {
        }
        }


        /*
        /*
         * In order to achieve the "rounded rectangle hits the wall" effect, the drawing needs to be
         * In order to achieve the "rounded rectangle hits the wall" effect, we draw an expanding
         * done in two passes. In this context, the wall is the bounding rectangle and in the first
         * rounded rectangle that is clipped by the bounding box of the selected text.
         * pass we need to draw the rounded rectangle (expanded and with a corner radius as per
         * object properties) clipped by the bounding box. If the rounded rectangle expands outside
         * of the bounding box, one more pass needs to be done, as there will now be a hole in the
         * rounded rectangle where it "flattened" against the bounding box. In order to fill just
         * this hole, we need to draw the bounding box, but clip it with the rounded rectangle and
         * this will connect the missing pieces.
         */
         */
        @Override
        @Override
        public void draw(Canvas canvas, Paint paint) {
        public void draw(Canvas canvas, Paint paint) {
@@ -201,31 +168,8 @@ final class SmartSelectSprite {
            final float adjustedCornerRadius = getAdjustedCornerRadius();
            final float adjustedCornerRadius = getAdjustedCornerRadius();


            mDrawRect.set(mBoundingRectangle);
            mDrawRect.set(mBoundingRectangle);
            mDrawRect.left = mBoundingRectangle.left + mLeftBoundary;
            mDrawRect.left = mBoundingRectangle.left + mLeftBoundary - cornerRadius / 2;
            mDrawRect.right = mBoundingRectangle.left + mRightBoundary;
            mDrawRect.right = mBoundingRectangle.left + mRightBoundary + cornerRadius / 2;

            if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) {
                mDrawRect.left -= cornerRadius / 2;
                mDrawRect.right += cornerRadius / 2;
            } else {
                switch (mExpansionDirection) {
                    case ExpansionDirection.CENTER:
                        break;
                    case ExpansionDirection.LEFT:
                        mDrawRect.right += cornerRadius;
                        break;
                    case ExpansionDirection.RIGHT:
                        mDrawRect.left -= cornerRadius;
                        break;
                }
            }

            canvas.save();
            mClipRect.set(mBoundingRectangle);
            mClipRect.inset(-mStrokeWidth / 2, -mStrokeWidth / 2);
            canvas.clipRect(mClipRect);
            canvas.drawRoundRect(mDrawRect, adjustedCornerRadius, adjustedCornerRadius, paint);
            canvas.restore();


            canvas.save();
            canvas.save();
            mClipPath.reset();
            mClipPath.reset();
@@ -272,11 +216,7 @@ final class SmartSelectSprite {
        }
        }


        private float getBoundingWidth() {
        private float getBoundingWidth() {
            if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) {
            return (int) (mBoundingRectangle.width() + getCornerRadius());
            return (int) (mBoundingRectangle.width() + getCornerRadius());
            } else {
                return mBoundingRectangle.width();
            }
        }
        }


    }
    }
@@ -389,18 +329,19 @@ final class SmartSelectSprite {


    /**
    /**
     * @param context the {@link Context} in which the animation will run
     * @param context the {@link Context} in which the animation will run
     * @param highlightColor the highlight color of the underlying {@link TextView}
     * @param invalidator a {@link Runnable} which will be called every time the animation updates,
     * @param invalidator a {@link Runnable} which will be called every time the animation updates,
     *                    indicating that the view drawing the animation should invalidate itself
     *                    indicating that the view drawing the animation should invalidate itself
     */
     */
    SmartSelectSprite(final Context context, final Runnable invalidator) {
    SmartSelectSprite(final Context context, @ColorInt int highlightColor,
            final Runnable invalidator) {
        mExpandInterpolator = AnimationUtils.loadInterpolator(
        mExpandInterpolator = AnimationUtils.loadInterpolator(
                context,
                context,
                android.R.interpolator.fast_out_slow_in);
                android.R.interpolator.fast_out_slow_in);
        mCornerInterpolator = AnimationUtils.loadInterpolator(
        mCornerInterpolator = AnimationUtils.loadInterpolator(
                context,
                context,
                android.R.interpolator.fast_out_linear_in);
                android.R.interpolator.fast_out_linear_in);
        mStrokeWidth = dpToPixel(context, STROKE_WIDTH_DP);
        mFillColor = highlightColor;
        mStrokeColor = getStrokeColor(context);
        mInvalidator = Preconditions.checkNotNull(invalidator);
        mInvalidator = Preconditions.checkNotNull(invalidator);
    }
    }


@@ -437,17 +378,14 @@ final class SmartSelectSprite {
        RectangleWithTextSelectionLayout centerRectangle = null;
        RectangleWithTextSelectionLayout centerRectangle = null;


        int startingOffset = 0;
        int startingOffset = 0;
        int startingRectangleIndex = 0;
        for (RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout :
        for (int index = 0; index < rectangleCount; ++index) {
                destinationRectangles) {
            final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout =
                    destinationRectangles.get(index);
            final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle();
            final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle();
            if (contains(rectangle, start)) {
            if (contains(rectangle, start)) {
                centerRectangle = rectangleWithTextSelectionLayout;
                centerRectangle = rectangleWithTextSelectionLayout;
                break;
                break;
            }
            }
            startingOffset += rectangle.width();
            startingOffset += rectangle.width();
            ++startingRectangleIndex;
        }
        }


        if (centerRectangle == null) {
        if (centerRectangle == null) {
@@ -459,9 +397,6 @@ final class SmartSelectSprite {
        final @RoundedRectangleShape.ExpansionDirection int[] expansionDirections =
        final @RoundedRectangleShape.ExpansionDirection int[] expansionDirections =
                generateDirections(centerRectangle, destinationRectangles);
                generateDirections(centerRectangle, destinationRectangles);


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

        for (int index = 0; index < rectangleCount; ++index) {
        for (int index = 0; index < rectangleCount; ++index) {
            final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout =
            final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout =
                    destinationRectangles.get(index);
                    destinationRectangles.get(index);
@@ -469,10 +404,8 @@ final class SmartSelectSprite {
            final RoundedRectangleShape shape = new RoundedRectangleShape(
            final RoundedRectangleShape shape = new RoundedRectangleShape(
                    rectangle,
                    rectangle,
                    expansionDirections[index],
                    expansionDirections[index],
                    rectangleBorderTypes[index],
                    rectangleWithTextSelectionLayout.getTextSelectionLayout()
                    rectangleWithTextSelectionLayout.getTextSelectionLayout()
                            == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT,
                            == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT);
                    mStrokeWidth);
            cornerAnimators.add(createCornerAnimator(shape, updateListener));
            cornerAnimators.add(createCornerAnimator(shape, updateListener));
            shapes.add(shape);
            shapes.add(shape);
        }
        }
@@ -480,44 +413,23 @@ final class SmartSelectSprite {
        final RectangleList rectangleList = new RectangleList(shapes);
        final RectangleList rectangleList = new RectangleList(shapes);
        final ShapeDrawable shapeDrawable = new ShapeDrawable(rectangleList);
        final ShapeDrawable shapeDrawable = new ShapeDrawable(rectangleList);


        final float startingOffsetLeft;
        final float startingOffsetRight;

        final RoundedRectangleShape startingRectangleShape = shapes.get(startingRectangleIndex);
        final float cornerRadius = startingRectangleShape.getCornerRadius();
        if (startingRectangleShape.mRectangleBorderType
                == RoundedRectangleShape.RectangleBorderType.FIT) {
            switch (startingRectangleShape.mExpansionDirection) {
                case RoundedRectangleShape.ExpansionDirection.LEFT:
                    startingOffsetLeft = startingOffsetRight = startingOffset - cornerRadius / 2;
                    break;
                case RoundedRectangleShape.ExpansionDirection.RIGHT:
                    startingOffsetLeft = startingOffsetRight = startingOffset + cornerRadius / 2;
                    break;
                case RoundedRectangleShape.ExpansionDirection.CENTER:  // fall through
                default:
                    startingOffsetLeft = startingOffset - cornerRadius / 2;
                    startingOffsetRight = startingOffset + cornerRadius / 2;
                    break;
            }
        } else {
            startingOffsetLeft = startingOffsetRight = startingOffset;
        }

        final Paint paint = shapeDrawable.getPaint();
        final Paint paint = shapeDrawable.getPaint();
        paint.setColor(mStrokeColor);
        paint.setColor(mFillColor);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStyle(Paint.Style.FILL);
        paint.setStrokeWidth(mStrokeWidth);


        mExistingRectangleList = rectangleList;
        mExistingRectangleList = rectangleList;
        mExistingDrawable = shapeDrawable;
        mExistingDrawable = shapeDrawable;


        mActiveAnimator = createAnimator(rectangleList, startingOffsetLeft, startingOffsetRight,
        mActiveAnimator = createAnimator(rectangleList, startingOffset, startingOffset,
                cornerAnimators, updateListener,
                cornerAnimators, updateListener, onAnimationEnd);
                onAnimationEnd);
        mActiveAnimator.start();
        mActiveAnimator.start();
    }
    }


    /** Returns whether the sprite is currently animating. */
    public boolean isAnimationActive() {
        return mActiveAnimator != null && mActiveAnimator.isRunning();
    }

    private Animator createAnimator(
    private Animator createAnimator(
            final RectangleList rectangleList,
            final RectangleList rectangleList,
            final float startingOffsetLeft,
            final float startingOffsetLeft,
@@ -625,36 +537,6 @@ final class SmartSelectSprite {
        return result;
        return result;
    }
    }


    private static @RoundedRectangleShape.RectangleBorderType int[] generateBorderTypes(
            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;
        }

        result[0] = RoundedRectangleShape.RectangleBorderType.FIT;
        result[result.length - 1] = RoundedRectangleShape.RectangleBorderType.FIT;
        return result;
    }

    private static float dpToPixel(final Context context, final float dp) {
        return TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP,
                dp,
                context.getResources().getDisplayMetrics());
    }

    @ColorInt
    private static int getStrokeColor(final Context context) {
        final TypedValue typedValue = new TypedValue();
        final TypedArray array = context.obtainStyledAttributes(typedValue.data, new int[]{
                android.R.attr.colorControlActivated});
        final int result = array.getColor(0, DEFAULT_STROKE_COLOR);
        array.recycle();
        return result;
    }

    /**
    /**
     * A variant of {@link RectF#contains(float, float)} that also allows the point to reside on
     * A variant of {@link RectF#contains(float, float)} that also allows the point to reside on
     * the right boundary of the rectangle.
     * the right boundary of the rectangle.