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

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

Merge "Updating smart text selection animation"

parents 15c13979 80620c52
Loading
Loading
Loading
Loading
+7 −4
Original line number Diff line number Diff line
@@ -1745,16 +1745,19 @@ public class Editor {
            highlight = null;
        }

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

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

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

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

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

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

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

    private void cancelAsyncTask() {
        if (mTextClassificationAsyncTask != null) {
            mTextClassificationAsyncTask.cancel(true);
+25 −143
Original line number Diff line number Diff line
@@ -26,7 +26,6 @@ import android.annotation.ColorInt;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
@@ -36,7 +35,6 @@ 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;

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

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

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

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

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

    static final Comparator<RectF> RECTANGLE_COMPARATOR = Comparator
            .<RectF>comparingDouble(e -> e.bottom)
@@ -124,26 +116,11 @@ final class SmartSelectSprite {
            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 float mRoundRatio = 1.0f;
        private final @ExpansionDirection int mExpansionDirection;
        private final @RectangleBorderType int mRectangleBorderType;

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

        /** 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(
                final RectF boundingRectangle,
                final @ExpansionDirection int expansionDirection,
                final @RectangleBorderType int rectangleBorderType,
                final boolean inverted,
                final float strokeWidth) {
                final boolean inverted) {
            mBoundingRectangle = new RectF(boundingRectangle);
            mBoundingWidth = boundingRectangle.width();
            mRectangleBorderType = rectangleBorderType;
            mStrokeWidth = strokeWidth;
            mInverted = inverted && expansionDirection != ExpansionDirection.CENTER;

            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
         * done in two passes. In this context, the wall is the bounding rectangle and in the first
         * 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.
         * In order to achieve the "rounded rectangle hits the wall" effect, we draw an expanding
         * rounded rectangle that is clipped by the bounding box of the selected text.
         */
        @Override
        public void draw(Canvas canvas, Paint paint) {
@@ -201,31 +168,8 @@ final class SmartSelectSprite {
            final float adjustedCornerRadius = getAdjustedCornerRadius();

            mDrawRect.set(mBoundingRectangle);
            mDrawRect.left = mBoundingRectangle.left + mLeftBoundary;
            mDrawRect.right = mBoundingRectangle.left + mRightBoundary;

            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();
            mDrawRect.left = mBoundingRectangle.left + mLeftBoundary - cornerRadius / 2;
            mDrawRect.right = mBoundingRectangle.left + mRightBoundary + cornerRadius / 2;

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

        private float getBoundingWidth() {
            if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) {
            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 highlightColor the highlight color of the underlying {@link TextView}
     * @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) {
    SmartSelectSprite(final Context context, @ColorInt int highlightColor,
            final Runnable invalidator) {
        mExpandInterpolator = AnimationUtils.loadInterpolator(
                context,
                android.R.interpolator.fast_out_slow_in);
        mCornerInterpolator = AnimationUtils.loadInterpolator(
                context,
                android.R.interpolator.fast_out_linear_in);
        mStrokeWidth = dpToPixel(context, STROKE_WIDTH_DP);
        mStrokeColor = getStrokeColor(context);
        mFillColor = highlightColor;
        mInvalidator = Preconditions.checkNotNull(invalidator);
    }

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

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

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

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

        for (int index = 0; index < rectangleCount; ++index) {
            final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout =
                    destinationRectangles.get(index);
@@ -469,10 +404,8 @@ final class SmartSelectSprite {
            final RoundedRectangleShape shape = new RoundedRectangleShape(
                    rectangle,
                    expansionDirections[index],
                    rectangleBorderTypes[index],
                    rectangleWithTextSelectionLayout.getTextSelectionLayout()
                            == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT,
                    mStrokeWidth);
                            == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT);
            cornerAnimators.add(createCornerAnimator(shape, updateListener));
            shapes.add(shape);
        }
@@ -480,44 +413,23 @@ final class SmartSelectSprite {
        final RectangleList rectangleList = new RectangleList(shapes);
        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();
        paint.setColor(mStrokeColor);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(mStrokeWidth);
        paint.setColor(mFillColor);
        paint.setStyle(Paint.Style.FILL);

        mExistingRectangleList = rectangleList;
        mExistingDrawable = shapeDrawable;

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

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

    private Animator createAnimator(
            final RectangleList rectangleList,
            final float startingOffsetLeft,
@@ -625,36 +537,6 @@ final class SmartSelectSprite {
        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
     * the right boundary of the rectangle.