Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2017 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package android.widget; |
| 18 | |
| 19 | import static java.lang.annotation.RetentionPolicy.SOURCE; |
| 20 | |
| 21 | import android.animation.Animator; |
| 22 | import android.animation.AnimatorSet; |
| 23 | import android.animation.ObjectAnimator; |
| 24 | import android.animation.ValueAnimator; |
| 25 | import android.annotation.ColorInt; |
| 26 | import android.annotation.FloatRange; |
| 27 | import android.annotation.IntDef; |
| 28 | import android.content.Context; |
| 29 | import android.graphics.Canvas; |
| 30 | import android.graphics.Paint; |
| 31 | import android.graphics.Path; |
Petar Šegina | 91df3f9 | 2017-08-15 16:20:43 +0100 | [diff] [blame] | 32 | import android.graphics.PointF; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 33 | import android.graphics.RectF; |
| 34 | import android.graphics.drawable.Drawable; |
| 35 | import android.graphics.drawable.ShapeDrawable; |
| 36 | import android.graphics.drawable.shapes.Shape; |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 37 | import android.text.Layout; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 38 | import android.view.animation.AnimationUtils; |
| 39 | import android.view.animation.Interpolator; |
| 40 | |
Petar Šegina | 5ab7bb2 | 2017-09-05 20:48:42 +0100 | [diff] [blame] | 41 | import com.android.internal.util.Preconditions; |
| 42 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 43 | import java.lang.annotation.Retention; |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 44 | import java.util.ArrayList; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 45 | import java.util.Collections; |
| 46 | import java.util.Comparator; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 47 | import java.util.List; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 48 | |
| 49 | /** |
| 50 | * A utility class for creating and animating the Smart Select animation. |
| 51 | */ |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 52 | final class SmartSelectSprite { |
| 53 | |
| 54 | private static final int EXPAND_DURATION = 300; |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 55 | private static final int CORNER_DURATION = 50; |
Petar Šegina | 5a239f0 | 2017-08-15 12:38:51 +0100 | [diff] [blame] | 56 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 57 | private final Interpolator mExpandInterpolator; |
| 58 | private final Interpolator mCornerInterpolator; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 59 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 60 | private Animator mActiveAnimator = null; |
Petar Šegina | 5ab7bb2 | 2017-09-05 20:48:42 +0100 | [diff] [blame] | 61 | private final Runnable mInvalidator; |
Petar Šegina | 5a239f0 | 2017-08-15 12:38:51 +0100 | [diff] [blame] | 62 | @ColorInt |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 63 | private final int mFillColor; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 64 | |
Petar Šegina | 7272925 | 2017-08-31 15:25:06 +0100 | [diff] [blame] | 65 | static final Comparator<RectF> RECTANGLE_COMPARATOR = Comparator |
| 66 | .<RectF>comparingDouble(e -> e.bottom) |
| 67 | .thenComparingDouble(e -> e.left); |
| 68 | |
Petar Šegina | aee97ac | 2017-08-31 11:28:20 +0100 | [diff] [blame] | 69 | private Drawable mExistingDrawable = null; |
| 70 | private RectangleList mExistingRectangleList = null; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 71 | |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 72 | static final class RectangleWithTextSelectionLayout { |
| 73 | private final RectF mRectangle; |
| 74 | @Layout.TextSelectionLayout |
| 75 | private final int mTextSelectionLayout; |
| 76 | |
| 77 | RectangleWithTextSelectionLayout(RectF rectangle, int textSelectionLayout) { |
| 78 | mRectangle = Preconditions.checkNotNull(rectangle); |
| 79 | mTextSelectionLayout = textSelectionLayout; |
| 80 | } |
| 81 | |
| 82 | public RectF getRectangle() { |
| 83 | return mRectangle; |
| 84 | } |
| 85 | |
| 86 | @Layout.TextSelectionLayout |
| 87 | public int getTextSelectionLayout() { |
| 88 | return mTextSelectionLayout; |
| 89 | } |
| 90 | } |
| 91 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 92 | /** |
| 93 | * A rounded rectangle with a configurable corner radius and the ability to expand outside of |
| 94 | * its bounding rectangle and clip against it. |
| 95 | */ |
| 96 | private static final class RoundedRectangleShape extends Shape { |
| 97 | |
Petar Šegina | 0d37b1a | 2017-08-31 12:25:41 +0100 | [diff] [blame] | 98 | private static final String PROPERTY_ROUND_RATIO = "roundRatio"; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 99 | |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 100 | /** |
| 101 | * The direction in which the rectangle will perform its expansion. A rectangle can expand |
| 102 | * from its left edge, its right edge or from the center (or, more precisely, the user's |
| 103 | * touch point). For example, in left-to-right text, a selection spanning two lines with the |
| 104 | * user's action being on the first line will have the top rectangle and expansion direction |
| 105 | * of CENTER, while the bottom one will have an expansion direction of RIGHT. |
| 106 | */ |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 107 | @Retention(SOURCE) |
| 108 | @IntDef({ExpansionDirection.LEFT, ExpansionDirection.CENTER, ExpansionDirection.RIGHT}) |
| 109 | private @interface ExpansionDirection { |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 110 | int LEFT = -1; |
| 111 | int CENTER = 0; |
| 112 | int RIGHT = 1; |
| 113 | } |
| 114 | |
| 115 | private static @ExpansionDirection int invert(@ExpansionDirection int expansionDirection) { |
| 116 | return expansionDirection * -1; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 117 | } |
| 118 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 119 | private final RectF mBoundingRectangle; |
Petar Šegina | 0d37b1a | 2017-08-31 12:25:41 +0100 | [diff] [blame] | 120 | private float mRoundRatio = 1.0f; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 121 | private final @ExpansionDirection int mExpansionDirection; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 122 | |
| 123 | private final RectF mDrawRect = new RectF(); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 124 | private final Path mClipPath = new Path(); |
| 125 | |
Petar Šegina | c1950a0 | 2017-09-27 20:06:39 +0100 | [diff] [blame] | 126 | /** How offset the left edge of the rectangle is from the left side of the bounding box. */ |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 127 | private float mLeftBoundary = 0; |
Petar Šegina | c1950a0 | 2017-09-27 20:06:39 +0100 | [diff] [blame] | 128 | /** How offset the right edge of the rectangle is from the left side of the bounding box. */ |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 129 | private float mRightBoundary = 0; |
| 130 | |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 131 | /** Whether the horizontal bounds are inverted (for RTL scenarios). */ |
| 132 | private final boolean mInverted; |
| 133 | |
| 134 | private final float mBoundingWidth; |
| 135 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 136 | private RoundedRectangleShape( |
| 137 | final RectF boundingRectangle, |
| 138 | final @ExpansionDirection int expansionDirection, |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 139 | final boolean inverted) { |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 140 | mBoundingRectangle = new RectF(boundingRectangle); |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 141 | mBoundingWidth = boundingRectangle.width(); |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 142 | mInverted = inverted && expansionDirection != ExpansionDirection.CENTER; |
| 143 | |
| 144 | if (inverted) { |
| 145 | mExpansionDirection = invert(expansionDirection); |
| 146 | } else { |
| 147 | mExpansionDirection = expansionDirection; |
| 148 | } |
Petar Šegina | 29e59d8 | 2017-08-23 20:04:08 +0100 | [diff] [blame] | 149 | |
| 150 | if (boundingRectangle.height() > boundingRectangle.width()) { |
Petar Šegina | 0d37b1a | 2017-08-31 12:25:41 +0100 | [diff] [blame] | 151 | setRoundRatio(0.0f); |
Petar Šegina | 29e59d8 | 2017-08-23 20:04:08 +0100 | [diff] [blame] | 152 | } else { |
Petar Šegina | 0d37b1a | 2017-08-31 12:25:41 +0100 | [diff] [blame] | 153 | setRoundRatio(1.0f); |
Petar Šegina | 29e59d8 | 2017-08-23 20:04:08 +0100 | [diff] [blame] | 154 | } |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 155 | } |
| 156 | |
| 157 | /* |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 158 | * In order to achieve the "rounded rectangle hits the wall" effect, we draw an expanding |
| 159 | * rounded rectangle that is clipped by the bounding box of the selected text. |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 160 | */ |
| 161 | @Override |
| 162 | public void draw(Canvas canvas, Paint paint) { |
Petar Šegina | c1950a0 | 2017-09-27 20:06:39 +0100 | [diff] [blame] | 163 | if (mLeftBoundary == mRightBoundary) { |
| 164 | return; |
| 165 | } |
| 166 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 167 | final float cornerRadius = getCornerRadius(); |
| 168 | final float adjustedCornerRadius = getAdjustedCornerRadius(); |
| 169 | |
| 170 | mDrawRect.set(mBoundingRectangle); |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 171 | mDrawRect.left = mBoundingRectangle.left + mLeftBoundary - cornerRadius / 2; |
| 172 | mDrawRect.right = mBoundingRectangle.left + mRightBoundary + cornerRadius / 2; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 173 | |
| 174 | canvas.save(); |
| 175 | mClipPath.reset(); |
| 176 | mClipPath.addRoundRect( |
| 177 | mDrawRect, |
| 178 | adjustedCornerRadius, |
| 179 | adjustedCornerRadius, |
| 180 | Path.Direction.CW); |
| 181 | canvas.clipPath(mClipPath); |
| 182 | canvas.drawRect(mBoundingRectangle, paint); |
| 183 | canvas.restore(); |
| 184 | } |
| 185 | |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 186 | void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) { |
Petar Šegina | 0d37b1a | 2017-08-31 12:25:41 +0100 | [diff] [blame] | 187 | mRoundRatio = roundRatio; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 188 | } |
| 189 | |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 190 | float getRoundRatio() { |
Petar Šegina | 0d37b1a | 2017-08-31 12:25:41 +0100 | [diff] [blame] | 191 | return mRoundRatio; |
Petar Šegina | 29e59d8 | 2017-08-23 20:04:08 +0100 | [diff] [blame] | 192 | } |
| 193 | |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 194 | private void setStartBoundary(final float startBoundary) { |
| 195 | if (mInverted) { |
| 196 | mRightBoundary = mBoundingWidth - startBoundary; |
| 197 | } else { |
| 198 | mLeftBoundary = startBoundary; |
| 199 | } |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 200 | } |
| 201 | |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 202 | private void setEndBoundary(final float endBoundary) { |
| 203 | if (mInverted) { |
| 204 | mLeftBoundary = mBoundingWidth - endBoundary; |
| 205 | } else { |
| 206 | mRightBoundary = endBoundary; |
| 207 | } |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 208 | } |
| 209 | |
| 210 | private float getCornerRadius() { |
| 211 | return Math.min(mBoundingRectangle.width(), mBoundingRectangle.height()); |
| 212 | } |
| 213 | |
| 214 | private float getAdjustedCornerRadius() { |
Petar Šegina | 0d37b1a | 2017-08-31 12:25:41 +0100 | [diff] [blame] | 215 | return (getCornerRadius() * mRoundRatio); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 216 | } |
| 217 | |
| 218 | private float getBoundingWidth() { |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 219 | return (int) (mBoundingRectangle.width() + getCornerRadius()); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 220 | } |
| 221 | |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * A collection of {@link RoundedRectangleShape}s that abstracts them to a single shape whose |
| 226 | * collective left and right boundary can be manipulated. |
| 227 | */ |
| 228 | private static final class RectangleList extends Shape { |
| 229 | |
Petar Šegina | aee97ac | 2017-08-31 11:28:20 +0100 | [diff] [blame] | 230 | @Retention(SOURCE) |
| 231 | @IntDef({DisplayType.RECTANGLES, DisplayType.POLYGON}) |
| 232 | private @interface DisplayType { |
| 233 | int RECTANGLES = 0; |
| 234 | int POLYGON = 1; |
| 235 | } |
| 236 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 237 | private static final String PROPERTY_RIGHT_BOUNDARY = "rightBoundary"; |
| 238 | private static final String PROPERTY_LEFT_BOUNDARY = "leftBoundary"; |
| 239 | |
| 240 | private final List<RoundedRectangleShape> mRectangles; |
| 241 | private final List<RoundedRectangleShape> mReversedRectangles; |
| 242 | |
Petar Šegina | aee97ac | 2017-08-31 11:28:20 +0100 | [diff] [blame] | 243 | private final Path mOutlinePolygonPath; |
| 244 | private @DisplayType int mDisplayType = DisplayType.RECTANGLES; |
| 245 | |
| 246 | private RectangleList(final List<RoundedRectangleShape> rectangles) { |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 247 | mRectangles = new ArrayList<>(rectangles); |
| 248 | mReversedRectangles = new ArrayList<>(rectangles); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 249 | Collections.reverse(mReversedRectangles); |
Petar Šegina | aee97ac | 2017-08-31 11:28:20 +0100 | [diff] [blame] | 250 | mOutlinePolygonPath = generateOutlinePolygonPath(rectangles); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 251 | } |
| 252 | |
| 253 | private void setLeftBoundary(final float leftBoundary) { |
| 254 | float boundarySoFar = getTotalWidth(); |
| 255 | for (RoundedRectangleShape rectangle : mReversedRectangles) { |
| 256 | final float rectangleLeftBoundary = boundarySoFar - rectangle.getBoundingWidth(); |
| 257 | if (leftBoundary < rectangleLeftBoundary) { |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 258 | rectangle.setStartBoundary(0); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 259 | } else if (leftBoundary > boundarySoFar) { |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 260 | rectangle.setStartBoundary(rectangle.getBoundingWidth()); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 261 | } else { |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 262 | rectangle.setStartBoundary( |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 263 | rectangle.getBoundingWidth() - boundarySoFar + leftBoundary); |
| 264 | } |
| 265 | |
| 266 | boundarySoFar = rectangleLeftBoundary; |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | private void setRightBoundary(final float rightBoundary) { |
| 271 | float boundarySoFar = 0; |
| 272 | for (RoundedRectangleShape rectangle : mRectangles) { |
| 273 | final float rectangleRightBoundary = rectangle.getBoundingWidth() + boundarySoFar; |
| 274 | if (rectangleRightBoundary < rightBoundary) { |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 275 | rectangle.setEndBoundary(rectangle.getBoundingWidth()); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 276 | } else if (boundarySoFar > rightBoundary) { |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 277 | rectangle.setEndBoundary(0); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 278 | } else { |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 279 | rectangle.setEndBoundary(rightBoundary - boundarySoFar); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 280 | } |
| 281 | |
| 282 | boundarySoFar = rectangleRightBoundary; |
| 283 | } |
| 284 | } |
| 285 | |
Petar Šegina | aee97ac | 2017-08-31 11:28:20 +0100 | [diff] [blame] | 286 | void setDisplayType(@DisplayType int displayType) { |
| 287 | mDisplayType = displayType; |
| 288 | } |
| 289 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 290 | private int getTotalWidth() { |
| 291 | int sum = 0; |
| 292 | for (RoundedRectangleShape rectangle : mRectangles) { |
| 293 | sum += rectangle.getBoundingWidth(); |
| 294 | } |
| 295 | return sum; |
| 296 | } |
| 297 | |
| 298 | @Override |
| 299 | public void draw(Canvas canvas, Paint paint) { |
Petar Šegina | aee97ac | 2017-08-31 11:28:20 +0100 | [diff] [blame] | 300 | if (mDisplayType == DisplayType.POLYGON) { |
| 301 | drawPolygon(canvas, paint); |
| 302 | } else { |
| 303 | drawRectangles(canvas, paint); |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | private void drawRectangles(final Canvas canvas, final Paint paint) { |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 308 | for (RoundedRectangleShape rectangle : mRectangles) { |
| 309 | rectangle.draw(canvas, paint); |
| 310 | } |
| 311 | } |
| 312 | |
Petar Šegina | aee97ac | 2017-08-31 11:28:20 +0100 | [diff] [blame] | 313 | private void drawPolygon(final Canvas canvas, final Paint paint) { |
| 314 | canvas.drawPath(mOutlinePolygonPath, paint); |
| 315 | } |
| 316 | |
| 317 | private static Path generateOutlinePolygonPath( |
| 318 | final List<RoundedRectangleShape> rectangles) { |
| 319 | final Path path = new Path(); |
| 320 | for (final RoundedRectangleShape shape : rectangles) { |
| 321 | final Path rectanglePath = new Path(); |
| 322 | rectanglePath.addRect(shape.mBoundingRectangle, Path.Direction.CW); |
| 323 | path.op(rectanglePath, Path.Op.UNION); |
| 324 | } |
| 325 | return path; |
| 326 | } |
| 327 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 328 | } |
| 329 | |
Petar Šegina | 5ab7bb2 | 2017-09-05 20:48:42 +0100 | [diff] [blame] | 330 | /** |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 331 | * @param context the {@link Context} in which the animation will run |
| 332 | * @param highlightColor the highlight color of the underlying {@link TextView} |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 333 | * @param invalidator a {@link Runnable} which will be called every time the animation updates, |
Petar Šegina | 5ab7bb2 | 2017-09-05 20:48:42 +0100 | [diff] [blame] | 334 | * indicating that the view drawing the animation should invalidate itself |
| 335 | */ |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 336 | SmartSelectSprite(final Context context, @ColorInt int highlightColor, |
| 337 | final Runnable invalidator) { |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 338 | mExpandInterpolator = AnimationUtils.loadInterpolator( |
| 339 | context, |
| 340 | android.R.interpolator.fast_out_slow_in); |
| 341 | mCornerInterpolator = AnimationUtils.loadInterpolator( |
| 342 | context, |
| 343 | android.R.interpolator.fast_out_linear_in); |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 344 | mFillColor = highlightColor; |
Petar Šegina | 5ab7bb2 | 2017-09-05 20:48:42 +0100 | [diff] [blame] | 345 | mInvalidator = Preconditions.checkNotNull(invalidator); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 346 | } |
| 347 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 348 | /** |
| 349 | * Performs the Smart Select animation on the view bound to this SmartSelectSprite. |
| 350 | * |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 351 | * @param start The point from which the animation will start. Must be inside |
| 352 | * destinationRectangles. |
| 353 | * @param destinationRectangles The rectangles which the animation will fill out by its |
Petar Šegina | 7272925 | 2017-08-31 15:25:06 +0100 | [diff] [blame] | 354 | * "selection" and finally join them into a single polygon. In |
| 355 | * order to get the correct visual behavior, these rectangles |
| 356 | * should be sorted according to {@link #RECTANGLE_COMPARATOR}. |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 357 | * @param onAnimationEnd the callback which will be invoked once the whole animation |
| 358 | * completes |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 359 | * @throws IllegalArgumentException if the given start point is not in any of the |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 360 | * destinationRectangles |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 361 | * @see #cancelAnimation() |
| 362 | */ |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 363 | // TODO nullability checks on parameters |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 364 | public void startAnimation( |
Petar Šegina | 91df3f9 | 2017-08-15 16:20:43 +0100 | [diff] [blame] | 365 | final PointF start, |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 366 | final List<RectangleWithTextSelectionLayout> destinationRectangles, |
Petar Šegina | 18f3c38 | 2017-09-27 20:27:24 +0100 | [diff] [blame] | 367 | final Runnable onAnimationEnd) { |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 368 | cancelAnimation(); |
| 369 | |
| 370 | final ValueAnimator.AnimatorUpdateListener updateListener = |
Petar Šegina | 5ab7bb2 | 2017-09-05 20:48:42 +0100 | [diff] [blame] | 371 | valueAnimator -> mInvalidator.run(); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 372 | |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 373 | final int rectangleCount = destinationRectangles.size(); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 374 | |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 375 | final List<RoundedRectangleShape> shapes = new ArrayList<>(rectangleCount); |
| 376 | final List<Animator> cornerAnimators = new ArrayList<>(rectangleCount); |
| 377 | |
| 378 | RectangleWithTextSelectionLayout centerRectangle = null; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 379 | |
| 380 | int startingOffset = 0; |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 381 | for (RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout : |
| 382 | destinationRectangles) { |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 383 | final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle(); |
Petar Šegina | cb2fdb8 | 2017-09-27 20:18:03 +0100 | [diff] [blame] | 384 | if (contains(rectangle, start)) { |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 385 | centerRectangle = rectangleWithTextSelectionLayout; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 386 | break; |
| 387 | } |
| 388 | startingOffset += rectangle.width(); |
| 389 | } |
| 390 | |
Petar Šegina | cb2fdb8 | 2017-09-27 20:18:03 +0100 | [diff] [blame] | 391 | if (centerRectangle == null) { |
| 392 | throw new IllegalArgumentException("Center point is not inside any of the rectangles!"); |
| 393 | } |
| 394 | |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 395 | startingOffset += start.x - centerRectangle.getRectangle().left; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 396 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 397 | final @RoundedRectangleShape.ExpansionDirection int[] expansionDirections = |
| 398 | generateDirections(centerRectangle, destinationRectangles); |
| 399 | |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 400 | for (int index = 0; index < rectangleCount; ++index) { |
| 401 | final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout = |
| 402 | destinationRectangles.get(index); |
| 403 | final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle(); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 404 | final RoundedRectangleShape shape = new RoundedRectangleShape( |
| 405 | rectangle, |
| 406 | expansionDirections[index], |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 407 | rectangleWithTextSelectionLayout.getTextSelectionLayout() |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 408 | == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 409 | cornerAnimators.add(createCornerAnimator(shape, updateListener)); |
| 410 | shapes.add(shape); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 411 | } |
| 412 | |
| 413 | final RectangleList rectangleList = new RectangleList(shapes); |
| 414 | final ShapeDrawable shapeDrawable = new ShapeDrawable(rectangleList); |
| 415 | |
| 416 | final Paint paint = shapeDrawable.getPaint(); |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 417 | paint.setColor(mFillColor); |
| 418 | paint.setStyle(Paint.Style.FILL); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 419 | |
Petar Šegina | aee97ac | 2017-08-31 11:28:20 +0100 | [diff] [blame] | 420 | mExistingRectangleList = rectangleList; |
| 421 | mExistingDrawable = shapeDrawable; |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 422 | |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 423 | mActiveAnimator = createAnimator(rectangleList, startingOffset, startingOffset, |
| 424 | cornerAnimators, updateListener, onAnimationEnd); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 425 | mActiveAnimator.start(); |
| 426 | } |
| 427 | |
Jan Althaus | 80620c5 | 2018-02-02 17:39:22 +0100 | [diff] [blame] | 428 | /** Returns whether the sprite is currently animating. */ |
| 429 | public boolean isAnimationActive() { |
| 430 | return mActiveAnimator != null && mActiveAnimator.isRunning(); |
| 431 | } |
| 432 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 433 | private Animator createAnimator( |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 434 | final RectangleList rectangleList, |
| 435 | final float startingOffsetLeft, |
| 436 | final float startingOffsetRight, |
| 437 | final List<Animator> cornerAnimators, |
| 438 | final ValueAnimator.AnimatorUpdateListener updateListener, |
| 439 | final Runnable onAnimationEnd) { |
| 440 | final ObjectAnimator rightBoundaryAnimator = ObjectAnimator.ofFloat( |
| 441 | rectangleList, |
| 442 | RectangleList.PROPERTY_RIGHT_BOUNDARY, |
| 443 | startingOffsetRight, |
| 444 | rectangleList.getTotalWidth()); |
| 445 | |
| 446 | final ObjectAnimator leftBoundaryAnimator = ObjectAnimator.ofFloat( |
| 447 | rectangleList, |
| 448 | RectangleList.PROPERTY_LEFT_BOUNDARY, |
| 449 | startingOffsetLeft, |
| 450 | 0); |
| 451 | |
| 452 | rightBoundaryAnimator.setDuration(EXPAND_DURATION); |
| 453 | leftBoundaryAnimator.setDuration(EXPAND_DURATION); |
| 454 | |
| 455 | rightBoundaryAnimator.addUpdateListener(updateListener); |
| 456 | leftBoundaryAnimator.addUpdateListener(updateListener); |
| 457 | |
| 458 | rightBoundaryAnimator.setInterpolator(mExpandInterpolator); |
| 459 | leftBoundaryAnimator.setInterpolator(mExpandInterpolator); |
| 460 | |
| 461 | final AnimatorSet cornerAnimator = new AnimatorSet(); |
| 462 | cornerAnimator.playTogether(cornerAnimators); |
| 463 | |
| 464 | final AnimatorSet boundaryAnimator = new AnimatorSet(); |
| 465 | boundaryAnimator.playTogether(leftBoundaryAnimator, rightBoundaryAnimator); |
| 466 | |
| 467 | final AnimatorSet animatorSet = new AnimatorSet(); |
| 468 | animatorSet.playSequentially(boundaryAnimator, cornerAnimator); |
| 469 | |
Petar Šegina | aee97ac | 2017-08-31 11:28:20 +0100 | [diff] [blame] | 470 | setUpAnimatorListener(animatorSet, onAnimationEnd); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 471 | |
| 472 | return animatorSet; |
| 473 | } |
| 474 | |
Petar Šegina | aee97ac | 2017-08-31 11:28:20 +0100 | [diff] [blame] | 475 | private void setUpAnimatorListener(final Animator animator, final Runnable onAnimationEnd) { |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 476 | animator.addListener(new Animator.AnimatorListener() { |
| 477 | @Override |
| 478 | public void onAnimationStart(Animator animator) { |
| 479 | } |
| 480 | |
| 481 | @Override |
| 482 | public void onAnimationEnd(Animator animator) { |
Petar Šegina | aee97ac | 2017-08-31 11:28:20 +0100 | [diff] [blame] | 483 | mExistingRectangleList.setDisplayType(RectangleList.DisplayType.POLYGON); |
Petar Šegina | 5ab7bb2 | 2017-09-05 20:48:42 +0100 | [diff] [blame] | 484 | mInvalidator.run(); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 485 | |
| 486 | onAnimationEnd.run(); |
| 487 | } |
| 488 | |
| 489 | @Override |
| 490 | public void onAnimationCancel(Animator animator) { |
| 491 | } |
| 492 | |
| 493 | @Override |
| 494 | public void onAnimationRepeat(Animator animator) { |
| 495 | } |
| 496 | }); |
| 497 | } |
| 498 | |
| 499 | private ObjectAnimator createCornerAnimator( |
| 500 | final RoundedRectangleShape shape, |
| 501 | final ValueAnimator.AnimatorUpdateListener listener) { |
| 502 | final ObjectAnimator animator = ObjectAnimator.ofFloat( |
| 503 | shape, |
Petar Šegina | 0d37b1a | 2017-08-31 12:25:41 +0100 | [diff] [blame] | 504 | RoundedRectangleShape.PROPERTY_ROUND_RATIO, |
| 505 | shape.getRoundRatio(), 0.0F); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 506 | animator.setDuration(CORNER_DURATION); |
| 507 | animator.addUpdateListener(listener); |
| 508 | animator.setInterpolator(mCornerInterpolator); |
| 509 | return animator; |
| 510 | } |
| 511 | |
| 512 | private static @RoundedRectangleShape.ExpansionDirection int[] generateDirections( |
Petar Šegina | 7c8196f | 2017-09-11 18:03:14 +0100 | [diff] [blame] | 513 | final RectangleWithTextSelectionLayout centerRectangle, |
| 514 | final List<RectangleWithTextSelectionLayout> rectangles) { |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 515 | final @RoundedRectangleShape.ExpansionDirection int[] result = new int[rectangles.size()]; |
| 516 | |
| 517 | final int centerRectangleIndex = rectangles.indexOf(centerRectangle); |
| 518 | |
| 519 | for (int i = 0; i < centerRectangleIndex - 1; ++i) { |
| 520 | result[i] = RoundedRectangleShape.ExpansionDirection.LEFT; |
| 521 | } |
Petar Šegina | 7272925 | 2017-08-31 15:25:06 +0100 | [diff] [blame] | 522 | |
| 523 | if (rectangles.size() == 1) { |
| 524 | result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.CENTER; |
| 525 | } else if (centerRectangleIndex == 0) { |
| 526 | result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.LEFT; |
| 527 | } else if (centerRectangleIndex == rectangles.size() - 1) { |
| 528 | result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.RIGHT; |
| 529 | } else { |
| 530 | result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.CENTER; |
| 531 | } |
| 532 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 533 | for (int i = centerRectangleIndex + 1; i < result.length; ++i) { |
| 534 | result[i] = RoundedRectangleShape.ExpansionDirection.RIGHT; |
| 535 | } |
| 536 | |
| 537 | return result; |
| 538 | } |
| 539 | |
Petar Šegina | 91df3f9 | 2017-08-15 16:20:43 +0100 | [diff] [blame] | 540 | /** |
| 541 | * A variant of {@link RectF#contains(float, float)} that also allows the point to reside on |
| 542 | * the right boundary of the rectangle. |
| 543 | * |
| 544 | * @param rectangle the rectangle inside which the point should be to be considered "contained" |
| 545 | * @param point the point which will be tested |
| 546 | * @return whether the point is inside the rectangle (or on it's right boundary) |
| 547 | */ |
| 548 | private static boolean contains(final RectF rectangle, final PointF point) { |
| 549 | final float x = point.x; |
| 550 | final float y = point.y; |
| 551 | return x >= rectangle.left && x <= rectangle.right && y >= rectangle.top |
| 552 | && y <= rectangle.bottom; |
| 553 | } |
| 554 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 555 | private void removeExistingDrawables() { |
Petar Šegina | aee97ac | 2017-08-31 11:28:20 +0100 | [diff] [blame] | 556 | mExistingDrawable = null; |
| 557 | mExistingRectangleList = null; |
Petar Šegina | 5ab7bb2 | 2017-09-05 20:48:42 +0100 | [diff] [blame] | 558 | mInvalidator.run(); |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 559 | } |
| 560 | |
| 561 | /** |
| 562 | * Cancels any active Smart Select animation that might be in progress. |
| 563 | */ |
| 564 | public void cancelAnimation() { |
| 565 | if (mActiveAnimator != null) { |
| 566 | mActiveAnimator.cancel(); |
| 567 | mActiveAnimator = null; |
| 568 | removeExistingDrawables(); |
| 569 | } |
| 570 | } |
| 571 | |
Petar Šegina | 5ab7bb2 | 2017-09-05 20:48:42 +0100 | [diff] [blame] | 572 | public void draw(Canvas canvas) { |
| 573 | if (mExistingDrawable != null) { |
| 574 | mExistingDrawable.draw(canvas); |
| 575 | } |
| 576 | } |
| 577 | |
Petar Šegina | 701ba33 | 2017-08-01 17:57:26 +0100 | [diff] [blame] | 578 | } |