blob: 94109d741fd8d875a6724d62587efb18d47e29e5 [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;
Petar Šegina5a239f02017-08-15 12:38:51 +010029import android.content.res.TypedArray;
Petar Šegina701ba332017-08-01 17:57:26 +010030import android.graphics.Canvas;
31import android.graphics.Paint;
32import android.graphics.Path;
Petar Šegina91df3f92017-08-15 16:20:43 +010033import android.graphics.PointF;
Petar Šegina701ba332017-08-01 17:57:26 +010034import android.graphics.RectF;
35import android.graphics.drawable.Drawable;
36import android.graphics.drawable.ShapeDrawable;
37import android.graphics.drawable.shapes.Shape;
Petar Šegina701ba332017-08-01 17:57:26 +010038import android.util.TypedValue;
39import android.view.View;
40import android.view.ViewOverlay;
41import android.view.animation.AnimationUtils;
42import android.view.animation.Interpolator;
43
44import java.lang.annotation.Retention;
45import java.util.Collections;
46import java.util.Comparator;
47import java.util.HashSet;
48import java.util.LinkedList;
49import java.util.List;
50import java.util.Set;
51import java.util.Stack;
52
53/**
54 * A utility class for creating and animating the Smart Select animation.
55 */
56// TODO Do not rely on ViewOverlays for drawing the Smart Select sprite
57final class SmartSelectSprite {
58
59 private static final int EXPAND_DURATION = 300;
60 private static final int CORNER_DURATION = 150;
61 private static final float STROKE_WIDTH_DP = 1.5F;
62 private static final int POINTS_PER_LINE = 4;
Petar Šegina5a239f02017-08-15 12:38:51 +010063
64 // GBLUE700
65 @ColorInt
66 private static final int DEFAULT_STROKE_COLOR = 0xFF3367D6;
67
Petar Šegina701ba332017-08-01 17:57:26 +010068 private final Interpolator mExpandInterpolator;
69 private final Interpolator mCornerInterpolator;
70 private final float mStrokeWidth;
71
72 private final View mView;
73 private Animator mActiveAnimator = null;
Petar Šegina5a239f02017-08-15 12:38:51 +010074 @ColorInt
75 private final int mStrokeColor;
Petar Šegina701ba332017-08-01 17:57:26 +010076 private Set<Drawable> mExistingAnimationDrawables = new HashSet<>();
77
78 /**
79 * Represents a set of points connected by lines.
80 */
81 private static final class PolygonShape extends Shape {
82
83 private final float[] mLineCoordinates;
84
Petar Šegina91df3f92017-08-15 16:20:43 +010085 private PolygonShape(final List<PointF> points) {
Petar Šegina701ba332017-08-01 17:57:26 +010086 mLineCoordinates = new float[points.size() * POINTS_PER_LINE];
87
88 int index = 0;
Petar Šegina91df3f92017-08-15 16:20:43 +010089 PointF currentPoint = points.get(0);
90 for (final PointF nextPoint : points) {
91 mLineCoordinates[index] = currentPoint.x;
92 mLineCoordinates[index + 1] = currentPoint.y;
93 mLineCoordinates[index + 2] = nextPoint.x;
94 mLineCoordinates[index + 3] = nextPoint.y;
Petar Šegina701ba332017-08-01 17:57:26 +010095
96 index += POINTS_PER_LINE;
97 currentPoint = nextPoint;
98 }
99 }
100
101 @Override
102 public void draw(Canvas canvas, Paint paint) {
103 canvas.drawLines(mLineCoordinates, paint);
104 }
105 }
106
107 /**
108 * A rounded rectangle with a configurable corner radius and the ability to expand outside of
109 * its bounding rectangle and clip against it.
110 */
111 private static final class RoundedRectangleShape extends Shape {
112
113 private static final String PROPERTY_ROUND_PERCENTAGE = "roundPercentage";
114
115 @Retention(SOURCE)
116 @IntDef({ExpansionDirection.LEFT, ExpansionDirection.CENTER, ExpansionDirection.RIGHT})
117 private @interface ExpansionDirection {
118 int LEFT = 0;
119 int CENTER = 1;
120 int RIGHT = 2;
121 }
122
123 @Retention(SOURCE)
124 @IntDef({RectangleBorderType.FIT, RectangleBorderType.OVERSHOOT})
125 private @interface RectangleBorderType {
126 /** A rectangle which, fully expanded, fits inside of its bounding rectangle. */
127 int FIT = 0;
128 /**
129 * A rectangle which, when fully expanded, clips outside of its bounding rectangle so that
130 * its edges no longer appear rounded.
131 */
132 int OVERSHOOT = 1;
133 }
134
135 private final float mStrokeWidth;
136 private final RectF mBoundingRectangle;
137 private float mRoundPercentage = 1.0f;
138 private final @ExpansionDirection int mExpansionDirection;
139 private final @RectangleBorderType int mRectangleBorderType;
140
141 private final RectF mDrawRect = new RectF();
142 private final RectF mClipRect = new RectF();
143 private final Path mClipPath = new Path();
144
145 /** How far offset the left edge of the rectangle is from the bounding box. */
146 private float mLeftBoundary = 0;
147 /** How far offset the right edge of the rectangle is from the bounding box. */
148 private float mRightBoundary = 0;
149
150 private RoundedRectangleShape(
151 final RectF boundingRectangle,
152 final @ExpansionDirection int expansionDirection,
153 final @RectangleBorderType int rectangleBorderType,
154 final float strokeWidth) {
155 mBoundingRectangle = new RectF(boundingRectangle);
156 mExpansionDirection = expansionDirection;
157 mRectangleBorderType = rectangleBorderType;
158 mStrokeWidth = strokeWidth;
159 }
160
161 /*
162 * In order to achieve the "rounded rectangle hits the wall" effect, the drawing needs to be
163 * done in two passes. In this context, the wall is the bounding rectangle and in the first
164 * pass we need to draw the rounded rectangle (expanded and with a corner radius as per
165 * object properties) clipped by the bounding box. If the rounded rectangle expands outside
166 * of the bounding box, one more pass needs to be done, as there will now be a hole in the
167 * rounded rectangle where it "flattened" against the bounding box. In order to fill just
168 * this hole, we need to draw the bounding box, but clip it with the rounded rectangle and
169 * this will connect the missing pieces.
170 */
171 @Override
172 public void draw(Canvas canvas, Paint paint) {
173 final float cornerRadius = getCornerRadius();
174 final float adjustedCornerRadius = getAdjustedCornerRadius();
175
176 mDrawRect.set(mBoundingRectangle);
177 mDrawRect.left = mBoundingRectangle.left + mLeftBoundary;
178 mDrawRect.right = mBoundingRectangle.left + mRightBoundary;
179
180 if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) {
181 mDrawRect.left -= cornerRadius / 2;
182 mDrawRect.right -= cornerRadius / 2;
183 } else {
184 switch (mExpansionDirection) {
185 case ExpansionDirection.CENTER:
186 break;
187 case ExpansionDirection.LEFT:
188 mDrawRect.right += cornerRadius;
189 break;
190 case ExpansionDirection.RIGHT:
191 mDrawRect.left -= cornerRadius;
192 break;
193 }
194 }
195
196 canvas.save();
197 mClipRect.set(mBoundingRectangle);
198 mClipRect.top -= mStrokeWidth;
199 mClipRect.bottom += mStrokeWidth;
200 mClipRect.left -= mStrokeWidth;
201 mClipRect.right += mStrokeWidth;
202 canvas.clipRect(mClipRect);
203 canvas.drawRoundRect(mDrawRect, adjustedCornerRadius, adjustedCornerRadius, paint);
204 canvas.restore();
205
206 canvas.save();
207 mClipPath.reset();
208 mClipPath.addRoundRect(
209 mDrawRect,
210 adjustedCornerRadius,
211 adjustedCornerRadius,
212 Path.Direction.CW);
213 canvas.clipPath(mClipPath);
214 canvas.drawRect(mBoundingRectangle, paint);
215 canvas.restore();
216 }
217
218 public void setRoundPercentage(
219 @FloatRange(from = 0.0, to = 1.0) final float newPercentage) {
220 mRoundPercentage = newPercentage;
221 }
222
223 private void setLeftBoundary(final float leftBoundary) {
224 mLeftBoundary = leftBoundary;
225 }
226
227 private void setRightBoundary(final float rightBoundary) {
228 mRightBoundary = rightBoundary;
229 }
230
231 private float getCornerRadius() {
232 return Math.min(mBoundingRectangle.width(), mBoundingRectangle.height());
233 }
234
235 private float getAdjustedCornerRadius() {
236 return (getCornerRadius() * mRoundPercentage);
237 }
238
239 private float getBoundingWidth() {
240 if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) {
241 return (int) (mBoundingRectangle.width() + getCornerRadius());
242 } else {
243 return mBoundingRectangle.width();
244 }
245 }
246
247 }
248
249 /**
250 * A collection of {@link RoundedRectangleShape}s that abstracts them to a single shape whose
251 * collective left and right boundary can be manipulated.
252 */
253 private static final class RectangleList extends Shape {
254
255 private static final String PROPERTY_RIGHT_BOUNDARY = "rightBoundary";
256 private static final String PROPERTY_LEFT_BOUNDARY = "leftBoundary";
257
258 private final List<RoundedRectangleShape> mRectangles;
259 private final List<RoundedRectangleShape> mReversedRectangles;
260
261 private RectangleList(List<RoundedRectangleShape> rectangles) {
262 mRectangles = new LinkedList<>(rectangles);
263 mRectangles.sort((o1, o2) -> {
264 if (o1.mBoundingRectangle.top == o2.mBoundingRectangle.top) {
265 return Float.compare(o1.mBoundingRectangle.left, o2.mBoundingRectangle.left);
266 } else {
267 return Float.compare(o1.mBoundingRectangle.top, o2.mBoundingRectangle.top);
268 }
269 });
270 mReversedRectangles = new LinkedList<>(rectangles);
271 Collections.reverse(mReversedRectangles);
272 }
273
274 private void setLeftBoundary(final float leftBoundary) {
275 float boundarySoFar = getTotalWidth();
276 for (RoundedRectangleShape rectangle : mReversedRectangles) {
277 final float rectangleLeftBoundary = boundarySoFar - rectangle.getBoundingWidth();
278 if (leftBoundary < rectangleLeftBoundary) {
279 rectangle.setLeftBoundary(0);
280 } else if (leftBoundary > boundarySoFar) {
281 rectangle.setLeftBoundary(rectangle.getBoundingWidth());
282 } else {
283 rectangle.setLeftBoundary(
284 rectangle.getBoundingWidth() - boundarySoFar + leftBoundary);
285 }
286
287 boundarySoFar = rectangleLeftBoundary;
288 }
289 }
290
291 private void setRightBoundary(final float rightBoundary) {
292 float boundarySoFar = 0;
293 for (RoundedRectangleShape rectangle : mRectangles) {
294 final float rectangleRightBoundary = rectangle.getBoundingWidth() + boundarySoFar;
295 if (rectangleRightBoundary < rightBoundary) {
296 rectangle.setRightBoundary(rectangle.getBoundingWidth());
297 } else if (boundarySoFar > rightBoundary) {
298 rectangle.setRightBoundary(0);
299 } else {
300 rectangle.setRightBoundary(rightBoundary - boundarySoFar);
301 }
302
303 boundarySoFar = rectangleRightBoundary;
304 }
305 }
306
307 private int getTotalWidth() {
308 int sum = 0;
309 for (RoundedRectangleShape rectangle : mRectangles) {
310 sum += rectangle.getBoundingWidth();
311 }
312 return sum;
313 }
314
315 @Override
316 public void draw(Canvas canvas, Paint paint) {
317 for (RoundedRectangleShape rectangle : mRectangles) {
318 rectangle.draw(canvas, paint);
319 }
320 }
321
322 }
323
324 SmartSelectSprite(final View view) {
325 final Context context = view.getContext();
326 mExpandInterpolator = AnimationUtils.loadInterpolator(
327 context,
328 android.R.interpolator.fast_out_slow_in);
329 mCornerInterpolator = AnimationUtils.loadInterpolator(
330 context,
331 android.R.interpolator.fast_out_linear_in);
332 mStrokeWidth = dpToPixel(context, STROKE_WIDTH_DP);
Petar Šegina5a239f02017-08-15 12:38:51 +0100333 mStrokeColor = getStrokeColor(context);
Petar Šegina701ba332017-08-01 17:57:26 +0100334 mView = view;
335 }
336
337 private static boolean intersectsOrTouches(RectF a, RectF b) {
338 return a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom;
339 }
340
341 private List<Drawable> mergeRectanglesToPolygonShape(
342 final List<RectF> rectangles,
343 final int color) {
344 final List<Drawable> drawables = new LinkedList<>();
Petar Šegina91df3f92017-08-15 16:20:43 +0100345 final Set<List<PointF>> mergedPaths = calculateMergedPolygonPoints(rectangles);
Petar Šegina701ba332017-08-01 17:57:26 +0100346
Petar Šegina91df3f92017-08-15 16:20:43 +0100347 for (List<PointF> path : mergedPaths) {
Petar Šegina701ba332017-08-01 17:57:26 +0100348 // Add the starting point to the end of the polygon so that it ends up closed.
349 path.add(path.get(0));
350
351 final PolygonShape shape = new PolygonShape(path);
352 final ShapeDrawable drawable = new ShapeDrawable(shape);
353
354 drawable.getPaint().setColor(color);
355 drawable.getPaint().setStyle(Paint.Style.STROKE);
356 drawable.getPaint().setStrokeWidth(mStrokeWidth);
357
358 drawables.add(drawable);
359 }
360
361 return drawables;
362 }
363
Petar Šegina91df3f92017-08-15 16:20:43 +0100364 private static Set<List<PointF>> calculateMergedPolygonPoints(
Petar Šegina701ba332017-08-01 17:57:26 +0100365 List<RectF> rectangles) {
366 final Set<List<RectF>> partitions = new HashSet<>();
367 final LinkedList<RectF> listOfRects = new LinkedList<>(rectangles);
368
369 while (!listOfRects.isEmpty()) {
370 final RectF candidate = listOfRects.removeFirst();
371 final List<RectF> partition = new LinkedList<>();
372 partition.add(candidate);
373
374 final LinkedList<RectF> otherCandidates = new LinkedList<>();
375 otherCandidates.addAll(listOfRects);
376
377 while (!otherCandidates.isEmpty()) {
378 final RectF otherCandidate = otherCandidates.removeFirst();
379 for (RectF partitionElement : partition) {
380 if (intersectsOrTouches(partitionElement, otherCandidate)) {
381 partition.add(otherCandidate);
382 listOfRects.remove(otherCandidate);
383 break;
384 }
385 }
386 }
387
388 partition.sort(Comparator.comparing(o -> o.top));
389 partitions.add(partition);
390 }
391
Petar Šegina91df3f92017-08-15 16:20:43 +0100392 final Set<List<PointF>> result = new HashSet<>();
Petar Šegina701ba332017-08-01 17:57:26 +0100393 for (List<RectF> partition : partitions) {
Petar Šegina91df3f92017-08-15 16:20:43 +0100394 final List<PointF> points = new LinkedList<>();
Petar Šegina701ba332017-08-01 17:57:26 +0100395
396 final Stack<RectF> rects = new Stack<>();
397 for (RectF rect : partition) {
Petar Šegina91df3f92017-08-15 16:20:43 +0100398 points.add(new PointF(rect.right, rect.top));
399 points.add(new PointF(rect.right, rect.bottom));
Petar Šegina701ba332017-08-01 17:57:26 +0100400 rects.add(rect);
401 }
402 while (!rects.isEmpty()) {
403 final RectF rect = rects.pop();
Petar Šegina91df3f92017-08-15 16:20:43 +0100404 points.add(new PointF(rect.left, rect.bottom));
405 points.add(new PointF(rect.left, rect.top));
Petar Šegina701ba332017-08-01 17:57:26 +0100406 }
407
408 result.add(points);
409 }
410
411 return result;
412
413 }
414
415 /**
416 * Performs the Smart Select animation on the view bound to this SmartSelectSprite.
417 *
Petar Šegina701ba332017-08-01 17:57:26 +0100418 * @param start The point from which the animation will start. Must be inside
419 * destinationRectangles.
420 * @param destinationRectangles The rectangles which the animation will fill out by its
421 * "selection" and finally join them into a single polygon.
422 * @param onAnimationEnd The callback which will be invoked once the whole animation
423 * completes.
424 * @throws IllegalArgumentException if the given start point is not in any of the
425 * destinationRectangles.
426 * @see #cancelAnimation()
427 */
428 public void startAnimation(
Petar Šegina91df3f92017-08-15 16:20:43 +0100429 final PointF start,
Petar Šegina701ba332017-08-01 17:57:26 +0100430 final List<RectF> destinationRectangles,
431 final Runnable onAnimationEnd) throws IllegalArgumentException {
432 cancelAnimation();
433
434 final ValueAnimator.AnimatorUpdateListener updateListener =
435 valueAnimator -> mView.invalidate();
436
437 final List<RoundedRectangleShape> shapes = new LinkedList<>();
438 final List<Animator> cornerAnimators = new LinkedList<>();
439
440 final RectF centerRectangle = destinationRectangles
441 .stream()
Petar Šegina91df3f92017-08-15 16:20:43 +0100442 .filter((r) -> contains(r, start))
Petar Šegina701ba332017-08-01 17:57:26 +0100443 .findFirst()
444 .orElseThrow(() -> new IllegalArgumentException(
445 "Center point is not inside any of the rectangles!"));
446
447 int startingOffset = 0;
448 for (RectF rectangle : destinationRectangles) {
449 if (rectangle.equals(centerRectangle)) {
450 break;
451 }
452 startingOffset += rectangle.width();
453 }
454
Petar Šegina91df3f92017-08-15 16:20:43 +0100455 startingOffset += start.x - centerRectangle.left;
Petar Šegina701ba332017-08-01 17:57:26 +0100456
457 final float centerRectangleHalfHeight = centerRectangle.height() / 2;
458 final float startingOffsetLeft = startingOffset - centerRectangleHalfHeight;
459 final float startingOffsetRight = startingOffset + centerRectangleHalfHeight;
460
461 final @RoundedRectangleShape.ExpansionDirection int[] expansionDirections =
462 generateDirections(centerRectangle, destinationRectangles);
463
464 final @RoundedRectangleShape.RectangleBorderType int[] rectangleBorderTypes =
465 generateBorderTypes(destinationRectangles);
466
467 int index = 0;
468
469 for (RectF rectangle : destinationRectangles) {
470 final RoundedRectangleShape shape = new RoundedRectangleShape(
471 rectangle,
472 expansionDirections[index],
473 rectangleBorderTypes[index],
474 mStrokeWidth);
475 cornerAnimators.add(createCornerAnimator(shape, updateListener));
476 shapes.add(shape);
477 index++;
478 }
479
480 final RectangleList rectangleList = new RectangleList(shapes);
481 final ShapeDrawable shapeDrawable = new ShapeDrawable(rectangleList);
482
483 final Paint paint = shapeDrawable.getPaint();
Petar Šegina5a239f02017-08-15 12:38:51 +0100484 paint.setColor(mStrokeColor);
Petar Šegina701ba332017-08-01 17:57:26 +0100485 paint.setStyle(Paint.Style.STROKE);
486 paint.setStrokeWidth(mStrokeWidth);
487
488 addToOverlay(shapeDrawable);
489
Petar Šegina5a239f02017-08-15 12:38:51 +0100490 mActiveAnimator = createAnimator(mStrokeColor, destinationRectangles, rectangleList,
Petar Šegina701ba332017-08-01 17:57:26 +0100491 startingOffsetLeft, startingOffsetRight, cornerAnimators, updateListener,
492 onAnimationEnd);
493 mActiveAnimator.start();
494 }
495
496 private Animator createAnimator(
497 final @ColorInt int color,
498 final List<RectF> destinationRectangles,
499 final RectangleList rectangleList,
500 final float startingOffsetLeft,
501 final float startingOffsetRight,
502 final List<Animator> cornerAnimators,
503 final ValueAnimator.AnimatorUpdateListener updateListener,
504 final Runnable onAnimationEnd) {
505 final ObjectAnimator rightBoundaryAnimator = ObjectAnimator.ofFloat(
506 rectangleList,
507 RectangleList.PROPERTY_RIGHT_BOUNDARY,
508 startingOffsetRight,
509 rectangleList.getTotalWidth());
510
511 final ObjectAnimator leftBoundaryAnimator = ObjectAnimator.ofFloat(
512 rectangleList,
513 RectangleList.PROPERTY_LEFT_BOUNDARY,
514 startingOffsetLeft,
515 0);
516
517 rightBoundaryAnimator.setDuration(EXPAND_DURATION);
518 leftBoundaryAnimator.setDuration(EXPAND_DURATION);
519
520 rightBoundaryAnimator.addUpdateListener(updateListener);
521 leftBoundaryAnimator.addUpdateListener(updateListener);
522
523 rightBoundaryAnimator.setInterpolator(mExpandInterpolator);
524 leftBoundaryAnimator.setInterpolator(mExpandInterpolator);
525
526 final AnimatorSet cornerAnimator = new AnimatorSet();
527 cornerAnimator.playTogether(cornerAnimators);
528
529 final AnimatorSet boundaryAnimator = new AnimatorSet();
530 boundaryAnimator.playTogether(leftBoundaryAnimator, rightBoundaryAnimator);
531
532 final AnimatorSet animatorSet = new AnimatorSet();
533 animatorSet.playSequentially(boundaryAnimator, cornerAnimator);
534
535 setUpAnimatorListener(animatorSet, destinationRectangles, color, onAnimationEnd);
536
537 return animatorSet;
538 }
539
540 private void setUpAnimatorListener(final Animator animator,
541 final List<RectF> destinationRectangles,
542 final @ColorInt int color,
543 final Runnable onAnimationEnd) {
544 animator.addListener(new Animator.AnimatorListener() {
545 @Override
546 public void onAnimationStart(Animator animator) {
547 }
548
549 @Override
550 public void onAnimationEnd(Animator animator) {
551 removeExistingDrawables();
552
553 final List<Drawable> polygonShapes = mergeRectanglesToPolygonShape(
554 destinationRectangles,
555 color);
556
557 for (Drawable drawable : polygonShapes) {
558 addToOverlay(drawable);
559 }
560
561 onAnimationEnd.run();
562 }
563
564 @Override
565 public void onAnimationCancel(Animator animator) {
566 }
567
568 @Override
569 public void onAnimationRepeat(Animator animator) {
570 }
571 });
572 }
573
574 private ObjectAnimator createCornerAnimator(
575 final RoundedRectangleShape shape,
576 final ValueAnimator.AnimatorUpdateListener listener) {
577 final ObjectAnimator animator = ObjectAnimator.ofFloat(
578 shape,
579 RoundedRectangleShape.PROPERTY_ROUND_PERCENTAGE,
580 1.0F, 0.0F);
581 animator.setDuration(CORNER_DURATION);
582 animator.addUpdateListener(listener);
583 animator.setInterpolator(mCornerInterpolator);
584 return animator;
585 }
586
587 private static @RoundedRectangleShape.ExpansionDirection int[] generateDirections(
588 final RectF centerRectangle,
589 final List<RectF> rectangles) throws IllegalArgumentException {
590 final @RoundedRectangleShape.ExpansionDirection int[] result = new int[rectangles.size()];
591
592 final int centerRectangleIndex = rectangles.indexOf(centerRectangle);
593
594 for (int i = 0; i < centerRectangleIndex - 1; ++i) {
595 result[i] = RoundedRectangleShape.ExpansionDirection.LEFT;
596 }
597 result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.CENTER;
598 for (int i = centerRectangleIndex + 1; i < result.length; ++i) {
599 result[i] = RoundedRectangleShape.ExpansionDirection.RIGHT;
600 }
601
602 return result;
603 }
604
605 private static @RoundedRectangleShape.RectangleBorderType int[] generateBorderTypes(
606 final List<RectF> rectangles) {
607 final @RoundedRectangleShape.RectangleBorderType int[] result = new int[rectangles.size()];
608
609 for (int i = 1; i < result.length - 1; ++i) {
610 result[i] = RoundedRectangleShape.RectangleBorderType.OVERSHOOT;
611 }
612
613 result[0] = RoundedRectangleShape.RectangleBorderType.FIT;
614 result[result.length - 1] = RoundedRectangleShape.RectangleBorderType.FIT;
615 return result;
616 }
617
618 private static float dpToPixel(final Context context, final float dp) {
619 return TypedValue.applyDimension(
620 TypedValue.COMPLEX_UNIT_DIP,
621 dp,
622 context.getResources().getDisplayMetrics());
623 }
624
Petar Šegina5a239f02017-08-15 12:38:51 +0100625 @ColorInt
626 private static int getStrokeColor(final Context context) {
627 final TypedValue typedValue = new TypedValue();
628 final TypedArray array = context.obtainStyledAttributes(typedValue.data, new int[]{
629 android.R.attr.colorControlActivated});
630 final int result = array.getColor(0, DEFAULT_STROKE_COLOR);
631 array.recycle();
632 return result;
633 }
634
Petar Šegina91df3f92017-08-15 16:20:43 +0100635 /**
636 * A variant of {@link RectF#contains(float, float)} that also allows the point to reside on
637 * the right boundary of the rectangle.
638 *
639 * @param rectangle the rectangle inside which the point should be to be considered "contained"
640 * @param point the point which will be tested
641 * @return whether the point is inside the rectangle (or on it's right boundary)
642 */
643 private static boolean contains(final RectF rectangle, final PointF point) {
644 final float x = point.x;
645 final float y = point.y;
646 return x >= rectangle.left && x <= rectangle.right && y >= rectangle.top
647 && y <= rectangle.bottom;
648 }
649
Petar Šegina701ba332017-08-01 17:57:26 +0100650 private void addToOverlay(final Drawable drawable) {
651 mView.getOverlay().add(drawable);
652 mExistingAnimationDrawables.add(drawable);
653 }
654
655 private void removeExistingDrawables() {
656 final ViewOverlay overlay = mView.getOverlay();
657 for (Drawable drawable : mExistingAnimationDrawables) {
658 overlay.remove(drawable);
659 }
660 mExistingAnimationDrawables.clear();
661 }
662
663 /**
664 * Cancels any active Smart Select animation that might be in progress.
665 */
666 public void cancelAnimation() {
667 if (mActiveAnimator != null) {
668 mActiveAnimator.cancel();
669 mActiveAnimator = null;
670 removeExistingDrawables();
671 }
672 }
673
674}