From 80620c52b96936bbd605cdffc71bed5bb01e2067 Mon Sep 17 00:00:00 2001 From: Jan Althaus Date: Fri, 2 Feb 2018 17:39:22 +0100 Subject: [PATCH] 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 --- core/java/android/widget/Editor.java | 11 +- .../widget/SelectionActionModeHelper.java | 8 +- .../android/widget/SmartSelectSprite.java | 168 +++--------------- 3 files changed, 38 insertions(+), 149 deletions(-) diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 7bb0db1cf9109..5ab579df5ed6e 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -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, diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java index 2e354c1eee1f8..e7a4c0246e1f7 100644 --- a/core/java/android/widget/SelectionActionModeHelper.java +++ b/core/java/android/widget/SelectionActionModeHelper.java @@ -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); diff --git a/core/java/android/widget/SmartSelectSprite.java b/core/java/android/widget/SmartSelectSprite.java index a391c6ee8ec35..9a84f69d120a9 100644 --- a/core/java/android/widget/SmartSelectSprite.java +++ b/core/java/android/widget/SmartSelectSprite.java @@ -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 RECTANGLE_COMPARATOR = Comparator .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(); - } + return (int) (mBoundingRectangle.width() + getCornerRadius()); } } @@ -388,19 +328,20 @@ 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, * 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.