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