blob: 9a84f69d120a9dd49eb8c7c3ad30ff136f8b0137 [file] [log] [blame]
Petar Šegina701ba332017-08-01 17:57:26 +01001/*
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
17package android.widget;
18
19import static java.lang.annotation.RetentionPolicy.SOURCE;
20
21import android.animation.Animator;
22import android.animation.AnimatorSet;
23import android.animation.ObjectAnimator;
24import android.animation.ValueAnimator;
25import android.annotation.ColorInt;
26import android.annotation.FloatRange;
27import android.annotation.IntDef;
28import android.content.Context;
29import android.graphics.Canvas;
30import android.graphics.Paint;
31import android.graphics.Path;
Petar Šegina91df3f92017-08-15 16:20:43 +010032import android.graphics.PointF;
Petar Šegina701ba332017-08-01 17:57:26 +010033import android.graphics.RectF;
34import android.graphics.drawable.Drawable;
35import android.graphics.drawable.ShapeDrawable;
36import android.graphics.drawable.shapes.Shape;
Petar Šegina7c8196f2017-09-11 18:03:14 +010037import android.text.Layout;
Petar Šegina701ba332017-08-01 17:57:26 +010038import android.view.animation.AnimationUtils;
39import android.view.animation.Interpolator;
40
Petar Šegina5ab7bb22017-09-05 20:48:42 +010041import com.android.internal.util.Preconditions;
42
Petar Šegina701ba332017-08-01 17:57:26 +010043import java.lang.annotation.Retention;
Petar Šegina7c8196f2017-09-11 18:03:14 +010044import java.util.ArrayList;
Petar Šegina701ba332017-08-01 17:57:26 +010045import java.util.Collections;
46import java.util.Comparator;
Petar Šegina701ba332017-08-01 17:57:26 +010047import java.util.List;
Petar Šegina701ba332017-08-01 17:57:26 +010048
49/**
50 * A utility class for creating and animating the Smart Select animation.
51 */
Petar Šegina701ba332017-08-01 17:57:26 +010052final class SmartSelectSprite {
53
54 private static final int EXPAND_DURATION = 300;
Jan Althaus80620c52018-02-02 17:39:22 +010055 private static final int CORNER_DURATION = 50;
Petar Šegina5a239f02017-08-15 12:38:51 +010056
Petar Šegina701ba332017-08-01 17:57:26 +010057 private final Interpolator mExpandInterpolator;
58 private final Interpolator mCornerInterpolator;
Petar Šegina701ba332017-08-01 17:57:26 +010059
Petar Šegina701ba332017-08-01 17:57:26 +010060 private Animator mActiveAnimator = null;
Petar Šegina5ab7bb22017-09-05 20:48:42 +010061 private final Runnable mInvalidator;
Petar Šegina5a239f02017-08-15 12:38:51 +010062 @ColorInt
Jan Althaus80620c52018-02-02 17:39:22 +010063 private final int mFillColor;
Petar Šegina701ba332017-08-01 17:57:26 +010064
Petar Šegina72729252017-08-31 15:25:06 +010065 static final Comparator<RectF> RECTANGLE_COMPARATOR = Comparator
66 .<RectF>comparingDouble(e -> e.bottom)
67 .thenComparingDouble(e -> e.left);
68
Petar Šeginaaee97ac2017-08-31 11:28:20 +010069 private Drawable mExistingDrawable = null;
70 private RectangleList mExistingRectangleList = null;
Petar Šegina701ba332017-08-01 17:57:26 +010071
Petar Šegina7c8196f2017-09-11 18:03:14 +010072 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 Šegina701ba332017-08-01 17:57:26 +010092 /**
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 Šegina0d37b1a2017-08-31 12:25:41 +010098 private static final String PROPERTY_ROUND_RATIO = "roundRatio";
Petar Šegina701ba332017-08-01 17:57:26 +010099
Petar Šegina7c8196f2017-09-11 18:03:14 +0100100 /**
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 Šegina701ba332017-08-01 17:57:26 +0100107 @Retention(SOURCE)
108 @IntDef({ExpansionDirection.LEFT, ExpansionDirection.CENTER, ExpansionDirection.RIGHT})
109 private @interface ExpansionDirection {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100110 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 Šegina701ba332017-08-01 17:57:26 +0100117 }
118
Petar Šegina701ba332017-08-01 17:57:26 +0100119 private final RectF mBoundingRectangle;
Petar Šegina0d37b1a2017-08-31 12:25:41 +0100120 private float mRoundRatio = 1.0f;
Petar Šegina701ba332017-08-01 17:57:26 +0100121 private final @ExpansionDirection int mExpansionDirection;
Petar Šegina701ba332017-08-01 17:57:26 +0100122
123 private final RectF mDrawRect = new RectF();
Petar Šegina701ba332017-08-01 17:57:26 +0100124 private final Path mClipPath = new Path();
125
Petar Šeginac1950a02017-09-27 20:06:39 +0100126 /** How offset the left edge of the rectangle is from the left side of the bounding box. */
Petar Šegina701ba332017-08-01 17:57:26 +0100127 private float mLeftBoundary = 0;
Petar Šeginac1950a02017-09-27 20:06:39 +0100128 /** How offset the right edge of the rectangle is from the left side of the bounding box. */
Petar Šegina701ba332017-08-01 17:57:26 +0100129 private float mRightBoundary = 0;
130
Petar Šegina7c8196f2017-09-11 18:03:14 +0100131 /** Whether the horizontal bounds are inverted (for RTL scenarios). */
132 private final boolean mInverted;
133
134 private final float mBoundingWidth;
135
Petar Šegina701ba332017-08-01 17:57:26 +0100136 private RoundedRectangleShape(
137 final RectF boundingRectangle,
138 final @ExpansionDirection int expansionDirection,
Jan Althaus80620c52018-02-02 17:39:22 +0100139 final boolean inverted) {
Petar Šegina701ba332017-08-01 17:57:26 +0100140 mBoundingRectangle = new RectF(boundingRectangle);
Petar Šegina7c8196f2017-09-11 18:03:14 +0100141 mBoundingWidth = boundingRectangle.width();
Petar Šegina7c8196f2017-09-11 18:03:14 +0100142 mInverted = inverted && expansionDirection != ExpansionDirection.CENTER;
143
144 if (inverted) {
145 mExpansionDirection = invert(expansionDirection);
146 } else {
147 mExpansionDirection = expansionDirection;
148 }
Petar Šegina29e59d82017-08-23 20:04:08 +0100149
150 if (boundingRectangle.height() > boundingRectangle.width()) {
Petar Šegina0d37b1a2017-08-31 12:25:41 +0100151 setRoundRatio(0.0f);
Petar Šegina29e59d82017-08-23 20:04:08 +0100152 } else {
Petar Šegina0d37b1a2017-08-31 12:25:41 +0100153 setRoundRatio(1.0f);
Petar Šegina29e59d82017-08-23 20:04:08 +0100154 }
Petar Šegina701ba332017-08-01 17:57:26 +0100155 }
156
157 /*
Jan Althaus80620c52018-02-02 17:39:22 +0100158 * 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 Šegina701ba332017-08-01 17:57:26 +0100160 */
161 @Override
162 public void draw(Canvas canvas, Paint paint) {
Petar Šeginac1950a02017-09-27 20:06:39 +0100163 if (mLeftBoundary == mRightBoundary) {
164 return;
165 }
166
Petar Šegina701ba332017-08-01 17:57:26 +0100167 final float cornerRadius = getCornerRadius();
168 final float adjustedCornerRadius = getAdjustedCornerRadius();
169
170 mDrawRect.set(mBoundingRectangle);
Jan Althaus80620c52018-02-02 17:39:22 +0100171 mDrawRect.left = mBoundingRectangle.left + mLeftBoundary - cornerRadius / 2;
172 mDrawRect.right = mBoundingRectangle.left + mRightBoundary + cornerRadius / 2;
Petar Šegina701ba332017-08-01 17:57:26 +0100173
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 Šegina7c8196f2017-09-11 18:03:14 +0100186 void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) {
Petar Šegina0d37b1a2017-08-31 12:25:41 +0100187 mRoundRatio = roundRatio;
Petar Šegina701ba332017-08-01 17:57:26 +0100188 }
189
Petar Šegina7c8196f2017-09-11 18:03:14 +0100190 float getRoundRatio() {
Petar Šegina0d37b1a2017-08-31 12:25:41 +0100191 return mRoundRatio;
Petar Šegina29e59d82017-08-23 20:04:08 +0100192 }
193
Petar Šegina7c8196f2017-09-11 18:03:14 +0100194 private void setStartBoundary(final float startBoundary) {
195 if (mInverted) {
196 mRightBoundary = mBoundingWidth - startBoundary;
197 } else {
198 mLeftBoundary = startBoundary;
199 }
Petar Šegina701ba332017-08-01 17:57:26 +0100200 }
201
Petar Šegina7c8196f2017-09-11 18:03:14 +0100202 private void setEndBoundary(final float endBoundary) {
203 if (mInverted) {
204 mLeftBoundary = mBoundingWidth - endBoundary;
205 } else {
206 mRightBoundary = endBoundary;
207 }
Petar Šegina701ba332017-08-01 17:57:26 +0100208 }
209
210 private float getCornerRadius() {
211 return Math.min(mBoundingRectangle.width(), mBoundingRectangle.height());
212 }
213
214 private float getAdjustedCornerRadius() {
Petar Šegina0d37b1a2017-08-31 12:25:41 +0100215 return (getCornerRadius() * mRoundRatio);
Petar Šegina701ba332017-08-01 17:57:26 +0100216 }
217
218 private float getBoundingWidth() {
Jan Althaus80620c52018-02-02 17:39:22 +0100219 return (int) (mBoundingRectangle.width() + getCornerRadius());
Petar Šegina701ba332017-08-01 17:57:26 +0100220 }
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 Šeginaaee97ac2017-08-31 11:28:20 +0100230 @Retention(SOURCE)
231 @IntDef({DisplayType.RECTANGLES, DisplayType.POLYGON})
232 private @interface DisplayType {
233 int RECTANGLES = 0;
234 int POLYGON = 1;
235 }
236
Petar Šegina701ba332017-08-01 17:57:26 +0100237 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 Šeginaaee97ac2017-08-31 11:28:20 +0100243 private final Path mOutlinePolygonPath;
244 private @DisplayType int mDisplayType = DisplayType.RECTANGLES;
245
246 private RectangleList(final List<RoundedRectangleShape> rectangles) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100247 mRectangles = new ArrayList<>(rectangles);
248 mReversedRectangles = new ArrayList<>(rectangles);
Petar Šegina701ba332017-08-01 17:57:26 +0100249 Collections.reverse(mReversedRectangles);
Petar Šeginaaee97ac2017-08-31 11:28:20 +0100250 mOutlinePolygonPath = generateOutlinePolygonPath(rectangles);
Petar Šegina701ba332017-08-01 17:57:26 +0100251 }
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 Šegina7c8196f2017-09-11 18:03:14 +0100258 rectangle.setStartBoundary(0);
Petar Šegina701ba332017-08-01 17:57:26 +0100259 } else if (leftBoundary > boundarySoFar) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100260 rectangle.setStartBoundary(rectangle.getBoundingWidth());
Petar Šegina701ba332017-08-01 17:57:26 +0100261 } else {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100262 rectangle.setStartBoundary(
Petar Šegina701ba332017-08-01 17:57:26 +0100263 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 Šegina7c8196f2017-09-11 18:03:14 +0100275 rectangle.setEndBoundary(rectangle.getBoundingWidth());
Petar Šegina701ba332017-08-01 17:57:26 +0100276 } else if (boundarySoFar > rightBoundary) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100277 rectangle.setEndBoundary(0);
Petar Šegina701ba332017-08-01 17:57:26 +0100278 } else {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100279 rectangle.setEndBoundary(rightBoundary - boundarySoFar);
Petar Šegina701ba332017-08-01 17:57:26 +0100280 }
281
282 boundarySoFar = rectangleRightBoundary;
283 }
284 }
285
Petar Šeginaaee97ac2017-08-31 11:28:20 +0100286 void setDisplayType(@DisplayType int displayType) {
287 mDisplayType = displayType;
288 }
289
Petar Šegina701ba332017-08-01 17:57:26 +0100290 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 Šeginaaee97ac2017-08-31 11:28:20 +0100300 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 Šegina701ba332017-08-01 17:57:26 +0100308 for (RoundedRectangleShape rectangle : mRectangles) {
309 rectangle.draw(canvas, paint);
310 }
311 }
312
Petar Šeginaaee97ac2017-08-31 11:28:20 +0100313 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 Šegina701ba332017-08-01 17:57:26 +0100328 }
329
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100330 /**
Jan Althaus80620c52018-02-02 17:39:22 +0100331 * @param context the {@link Context} in which the animation will run
332 * @param highlightColor the highlight color of the underlying {@link TextView}
Petar Šegina7c8196f2017-09-11 18:03:14 +0100333 * @param invalidator a {@link Runnable} which will be called every time the animation updates,
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100334 * indicating that the view drawing the animation should invalidate itself
335 */
Jan Althaus80620c52018-02-02 17:39:22 +0100336 SmartSelectSprite(final Context context, @ColorInt int highlightColor,
337 final Runnable invalidator) {
Petar Šegina701ba332017-08-01 17:57:26 +0100338 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 Althaus80620c52018-02-02 17:39:22 +0100344 mFillColor = highlightColor;
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100345 mInvalidator = Preconditions.checkNotNull(invalidator);
Petar Šegina701ba332017-08-01 17:57:26 +0100346 }
347
Petar Šegina701ba332017-08-01 17:57:26 +0100348 /**
349 * Performs the Smart Select animation on the view bound to this SmartSelectSprite.
350 *
Petar Šegina701ba332017-08-01 17:57:26 +0100351 * @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 Šegina72729252017-08-31 15:25:06 +0100354 * "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 Šegina7c8196f2017-09-11 18:03:14 +0100357 * @param onAnimationEnd the callback which will be invoked once the whole animation
358 * completes
Petar Šegina701ba332017-08-01 17:57:26 +0100359 * @throws IllegalArgumentException if the given start point is not in any of the
Petar Šegina7c8196f2017-09-11 18:03:14 +0100360 * destinationRectangles
Petar Šegina701ba332017-08-01 17:57:26 +0100361 * @see #cancelAnimation()
362 */
Petar Šegina7c8196f2017-09-11 18:03:14 +0100363 // TODO nullability checks on parameters
Petar Šegina701ba332017-08-01 17:57:26 +0100364 public void startAnimation(
Petar Šegina91df3f92017-08-15 16:20:43 +0100365 final PointF start,
Petar Šegina7c8196f2017-09-11 18:03:14 +0100366 final List<RectangleWithTextSelectionLayout> destinationRectangles,
Petar Šegina18f3c382017-09-27 20:27:24 +0100367 final Runnable onAnimationEnd) {
Petar Šegina701ba332017-08-01 17:57:26 +0100368 cancelAnimation();
369
370 final ValueAnimator.AnimatorUpdateListener updateListener =
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100371 valueAnimator -> mInvalidator.run();
Petar Šegina701ba332017-08-01 17:57:26 +0100372
Petar Šegina7c8196f2017-09-11 18:03:14 +0100373 final int rectangleCount = destinationRectangles.size();
Petar Šegina701ba332017-08-01 17:57:26 +0100374
Petar Šegina7c8196f2017-09-11 18:03:14 +0100375 final List<RoundedRectangleShape> shapes = new ArrayList<>(rectangleCount);
376 final List<Animator> cornerAnimators = new ArrayList<>(rectangleCount);
377
378 RectangleWithTextSelectionLayout centerRectangle = null;
Petar Šegina701ba332017-08-01 17:57:26 +0100379
380 int startingOffset = 0;
Jan Althaus80620c52018-02-02 17:39:22 +0100381 for (RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout :
382 destinationRectangles) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100383 final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle();
Petar Šeginacb2fdb82017-09-27 20:18:03 +0100384 if (contains(rectangle, start)) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100385 centerRectangle = rectangleWithTextSelectionLayout;
Petar Šegina701ba332017-08-01 17:57:26 +0100386 break;
387 }
388 startingOffset += rectangle.width();
389 }
390
Petar Šeginacb2fdb82017-09-27 20:18:03 +0100391 if (centerRectangle == null) {
392 throw new IllegalArgumentException("Center point is not inside any of the rectangles!");
393 }
394
Petar Šegina7c8196f2017-09-11 18:03:14 +0100395 startingOffset += start.x - centerRectangle.getRectangle().left;
Petar Šegina701ba332017-08-01 17:57:26 +0100396
Petar Šegina701ba332017-08-01 17:57:26 +0100397 final @RoundedRectangleShape.ExpansionDirection int[] expansionDirections =
398 generateDirections(centerRectangle, destinationRectangles);
399
Petar Šegina7c8196f2017-09-11 18:03:14 +0100400 for (int index = 0; index < rectangleCount; ++index) {
401 final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout =
402 destinationRectangles.get(index);
403 final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle();
Petar Šegina701ba332017-08-01 17:57:26 +0100404 final RoundedRectangleShape shape = new RoundedRectangleShape(
405 rectangle,
406 expansionDirections[index],
Petar Šegina7c8196f2017-09-11 18:03:14 +0100407 rectangleWithTextSelectionLayout.getTextSelectionLayout()
Jan Althaus80620c52018-02-02 17:39:22 +0100408 == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT);
Petar Šegina701ba332017-08-01 17:57:26 +0100409 cornerAnimators.add(createCornerAnimator(shape, updateListener));
410 shapes.add(shape);
Petar Šegina701ba332017-08-01 17:57:26 +0100411 }
412
413 final RectangleList rectangleList = new RectangleList(shapes);
414 final ShapeDrawable shapeDrawable = new ShapeDrawable(rectangleList);
415
416 final Paint paint = shapeDrawable.getPaint();
Jan Althaus80620c52018-02-02 17:39:22 +0100417 paint.setColor(mFillColor);
418 paint.setStyle(Paint.Style.FILL);
Petar Šegina701ba332017-08-01 17:57:26 +0100419
Petar Šeginaaee97ac2017-08-31 11:28:20 +0100420 mExistingRectangleList = rectangleList;
421 mExistingDrawable = shapeDrawable;
Petar Šegina701ba332017-08-01 17:57:26 +0100422
Jan Althaus80620c52018-02-02 17:39:22 +0100423 mActiveAnimator = createAnimator(rectangleList, startingOffset, startingOffset,
424 cornerAnimators, updateListener, onAnimationEnd);
Petar Šegina701ba332017-08-01 17:57:26 +0100425 mActiveAnimator.start();
426 }
427
Jan Althaus80620c52018-02-02 17:39:22 +0100428 /** Returns whether the sprite is currently animating. */
429 public boolean isAnimationActive() {
430 return mActiveAnimator != null && mActiveAnimator.isRunning();
431 }
432
Petar Šegina701ba332017-08-01 17:57:26 +0100433 private Animator createAnimator(
Petar Šegina701ba332017-08-01 17:57:26 +0100434 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 Šeginaaee97ac2017-08-31 11:28:20 +0100470 setUpAnimatorListener(animatorSet, onAnimationEnd);
Petar Šegina701ba332017-08-01 17:57:26 +0100471
472 return animatorSet;
473 }
474
Petar Šeginaaee97ac2017-08-31 11:28:20 +0100475 private void setUpAnimatorListener(final Animator animator, final Runnable onAnimationEnd) {
Petar Šegina701ba332017-08-01 17:57:26 +0100476 animator.addListener(new Animator.AnimatorListener() {
477 @Override
478 public void onAnimationStart(Animator animator) {
479 }
480
481 @Override
482 public void onAnimationEnd(Animator animator) {
Petar Šeginaaee97ac2017-08-31 11:28:20 +0100483 mExistingRectangleList.setDisplayType(RectangleList.DisplayType.POLYGON);
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100484 mInvalidator.run();
Petar Šegina701ba332017-08-01 17:57:26 +0100485
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 Šegina0d37b1a2017-08-31 12:25:41 +0100504 RoundedRectangleShape.PROPERTY_ROUND_RATIO,
505 shape.getRoundRatio(), 0.0F);
Petar Šegina701ba332017-08-01 17:57:26 +0100506 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 Šegina7c8196f2017-09-11 18:03:14 +0100513 final RectangleWithTextSelectionLayout centerRectangle,
514 final List<RectangleWithTextSelectionLayout> rectangles) {
Petar Šegina701ba332017-08-01 17:57:26 +0100515 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 Šegina72729252017-08-31 15:25:06 +0100522
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 Šegina701ba332017-08-01 17:57:26 +0100533 for (int i = centerRectangleIndex + 1; i < result.length; ++i) {
534 result[i] = RoundedRectangleShape.ExpansionDirection.RIGHT;
535 }
536
537 return result;
538 }
539
Petar Šegina91df3f92017-08-15 16:20:43 +0100540 /**
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 Šegina701ba332017-08-01 17:57:26 +0100555 private void removeExistingDrawables() {
Petar Šeginaaee97ac2017-08-31 11:28:20 +0100556 mExistingDrawable = null;
557 mExistingRectangleList = null;
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100558 mInvalidator.run();
Petar Šegina701ba332017-08-01 17:57:26 +0100559 }
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 Šegina5ab7bb22017-09-05 20:48:42 +0100572 public void draw(Canvas canvas) {
573 if (mExistingDrawable != null) {
574 mExistingDrawable.draw(canvas);
575 }
576 }
577
Petar Šegina701ba332017-08-01 17:57:26 +0100578}