| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.widget; |
| |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.animation.ValueAnimator; |
| import android.annotation.ColorInt; |
| import android.annotation.FloatRange; |
| import android.annotation.IntDef; |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.graphics.PointF; |
| import android.graphics.RectF; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.ShapeDrawable; |
| import android.graphics.drawable.shapes.Shape; |
| import android.util.TypedValue; |
| import android.view.View; |
| import android.view.ViewOverlay; |
| import android.view.animation.AnimationUtils; |
| import android.view.animation.Interpolator; |
| |
| import java.lang.annotation.Retention; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.Stack; |
| |
| /** |
| * A utility class for creating and animating the Smart Select animation. |
| */ |
| // TODO Do not rely on ViewOverlays for drawing the Smart Select sprite |
| final class SmartSelectSprite { |
| |
| private static final int EXPAND_DURATION = 300; |
| private static final int CORNER_DURATION = 150; |
| private static final float STROKE_WIDTH_DP = 1.5F; |
| private static final int POINTS_PER_LINE = 4; |
| |
| // GBLUE700 |
| @ColorInt |
| private static final int DEFAULT_STROKE_COLOR = 0xFF3367D6; |
| |
| private final Interpolator mExpandInterpolator; |
| private final Interpolator mCornerInterpolator; |
| private final float mStrokeWidth; |
| |
| private final View mView; |
| private Animator mActiveAnimator = null; |
| @ColorInt |
| private final int mStrokeColor; |
| private Set<Drawable> mExistingAnimationDrawables = new HashSet<>(); |
| |
| /** |
| * Represents a set of points connected by lines. |
| */ |
| private static final class PolygonShape extends Shape { |
| |
| private final float[] mLineCoordinates; |
| |
| private PolygonShape(final List<PointF> points) { |
| mLineCoordinates = new float[points.size() * POINTS_PER_LINE]; |
| |
| int index = 0; |
| PointF currentPoint = points.get(0); |
| for (final PointF nextPoint : points) { |
| mLineCoordinates[index] = currentPoint.x; |
| mLineCoordinates[index + 1] = currentPoint.y; |
| mLineCoordinates[index + 2] = nextPoint.x; |
| mLineCoordinates[index + 3] = nextPoint.y; |
| |
| index += POINTS_PER_LINE; |
| currentPoint = nextPoint; |
| } |
| } |
| |
| @Override |
| public void draw(Canvas canvas, Paint paint) { |
| canvas.drawLines(mLineCoordinates, paint); |
| } |
| } |
| |
| /** |
| * A rounded rectangle with a configurable corner radius and the ability to expand outside of |
| * its bounding rectangle and clip against it. |
| */ |
| private static final class RoundedRectangleShape extends Shape { |
| |
| private static final String PROPERTY_ROUND_PERCENTAGE = "roundPercentage"; |
| |
| @Retention(SOURCE) |
| @IntDef({ExpansionDirection.LEFT, ExpansionDirection.CENTER, ExpansionDirection.RIGHT}) |
| private @interface ExpansionDirection { |
| int LEFT = 0; |
| int CENTER = 1; |
| int RIGHT = 2; |
| } |
| |
| @Retention(SOURCE) |
| @IntDef({RectangleBorderType.FIT, RectangleBorderType.OVERSHOOT}) |
| private @interface RectangleBorderType { |
| /** A rectangle which, fully expanded, fits inside of its bounding rectangle. */ |
| int FIT = 0; |
| /** |
| * A rectangle which, when fully expanded, clips outside of its bounding rectangle so that |
| * its edges no longer appear rounded. |
| */ |
| int OVERSHOOT = 1; |
| } |
| |
| private final float mStrokeWidth; |
| private final RectF mBoundingRectangle; |
| private float mRoundPercentage = 1.0f; |
| private final @ExpansionDirection int mExpansionDirection; |
| private final @RectangleBorderType int mRectangleBorderType; |
| |
| private final RectF mDrawRect = new RectF(); |
| private final RectF mClipRect = new RectF(); |
| private final Path mClipPath = new Path(); |
| |
| /** How far offset the left edge of the rectangle is from the bounding box. */ |
| private float mLeftBoundary = 0; |
| /** How far offset the right edge of the rectangle is from the bounding box. */ |
| private float mRightBoundary = 0; |
| |
| private RoundedRectangleShape( |
| final RectF boundingRectangle, |
| final @ExpansionDirection int expansionDirection, |
| final @RectangleBorderType int rectangleBorderType, |
| final float strokeWidth) { |
| mBoundingRectangle = new RectF(boundingRectangle); |
| mExpansionDirection = expansionDirection; |
| mRectangleBorderType = rectangleBorderType; |
| mStrokeWidth = strokeWidth; |
| } |
| |
| /* |
| * In order to achieve the "rounded rectangle hits the wall" effect, the drawing needs to be |
| * done in two passes. In this context, the wall is the bounding rectangle and in the first |
| * pass we need to draw the rounded rectangle (expanded and with a corner radius as per |
| * object properties) clipped by the bounding box. If the rounded rectangle expands outside |
| * of the bounding box, one more pass needs to be done, as there will now be a hole in the |
| * rounded rectangle where it "flattened" against the bounding box. In order to fill just |
| * this hole, we need to draw the bounding box, but clip it with the rounded rectangle and |
| * this will connect the missing pieces. |
| */ |
| @Override |
| public void draw(Canvas canvas, Paint paint) { |
| final float cornerRadius = getCornerRadius(); |
| final float adjustedCornerRadius = getAdjustedCornerRadius(); |
| |
| mDrawRect.set(mBoundingRectangle); |
| mDrawRect.left = mBoundingRectangle.left + mLeftBoundary; |
| mDrawRect.right = mBoundingRectangle.left + mRightBoundary; |
| |
| if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) { |
| mDrawRect.left -= cornerRadius / 2; |
| mDrawRect.right -= cornerRadius / 2; |
| } else { |
| switch (mExpansionDirection) { |
| case ExpansionDirection.CENTER: |
| break; |
| case ExpansionDirection.LEFT: |
| mDrawRect.right += cornerRadius; |
| break; |
| case ExpansionDirection.RIGHT: |
| mDrawRect.left -= cornerRadius; |
| break; |
| } |
| } |
| |
| canvas.save(); |
| mClipRect.set(mBoundingRectangle); |
| mClipRect.top -= mStrokeWidth; |
| mClipRect.bottom += mStrokeWidth; |
| mClipRect.left -= mStrokeWidth; |
| mClipRect.right += mStrokeWidth; |
| canvas.clipRect(mClipRect); |
| canvas.drawRoundRect(mDrawRect, adjustedCornerRadius, adjustedCornerRadius, paint); |
| canvas.restore(); |
| |
| canvas.save(); |
| mClipPath.reset(); |
| mClipPath.addRoundRect( |
| mDrawRect, |
| adjustedCornerRadius, |
| adjustedCornerRadius, |
| Path.Direction.CW); |
| canvas.clipPath(mClipPath); |
| canvas.drawRect(mBoundingRectangle, paint); |
| canvas.restore(); |
| } |
| |
| public void setRoundPercentage( |
| @FloatRange(from = 0.0, to = 1.0) final float newPercentage) { |
| mRoundPercentage = newPercentage; |
| } |
| |
| private void setLeftBoundary(final float leftBoundary) { |
| mLeftBoundary = leftBoundary; |
| } |
| |
| private void setRightBoundary(final float rightBoundary) { |
| mRightBoundary = rightBoundary; |
| } |
| |
| private float getCornerRadius() { |
| return Math.min(mBoundingRectangle.width(), mBoundingRectangle.height()); |
| } |
| |
| private float getAdjustedCornerRadius() { |
| return (getCornerRadius() * mRoundPercentage); |
| } |
| |
| private float getBoundingWidth() { |
| if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) { |
| return (int) (mBoundingRectangle.width() + getCornerRadius()); |
| } else { |
| return mBoundingRectangle.width(); |
| } |
| } |
| |
| } |
| |
| /** |
| * A collection of {@link RoundedRectangleShape}s that abstracts them to a single shape whose |
| * collective left and right boundary can be manipulated. |
| */ |
| private static final class RectangleList extends Shape { |
| |
| private static final String PROPERTY_RIGHT_BOUNDARY = "rightBoundary"; |
| private static final String PROPERTY_LEFT_BOUNDARY = "leftBoundary"; |
| |
| private final List<RoundedRectangleShape> mRectangles; |
| private final List<RoundedRectangleShape> mReversedRectangles; |
| |
| private RectangleList(List<RoundedRectangleShape> rectangles) { |
| mRectangles = new LinkedList<>(rectangles); |
| mRectangles.sort((o1, o2) -> { |
| if (o1.mBoundingRectangle.top == o2.mBoundingRectangle.top) { |
| return Float.compare(o1.mBoundingRectangle.left, o2.mBoundingRectangle.left); |
| } else { |
| return Float.compare(o1.mBoundingRectangle.top, o2.mBoundingRectangle.top); |
| } |
| }); |
| mReversedRectangles = new LinkedList<>(rectangles); |
| Collections.reverse(mReversedRectangles); |
| } |
| |
| private void setLeftBoundary(final float leftBoundary) { |
| float boundarySoFar = getTotalWidth(); |
| for (RoundedRectangleShape rectangle : mReversedRectangles) { |
| final float rectangleLeftBoundary = boundarySoFar - rectangle.getBoundingWidth(); |
| if (leftBoundary < rectangleLeftBoundary) { |
| rectangle.setLeftBoundary(0); |
| } else if (leftBoundary > boundarySoFar) { |
| rectangle.setLeftBoundary(rectangle.getBoundingWidth()); |
| } else { |
| rectangle.setLeftBoundary( |
| rectangle.getBoundingWidth() - boundarySoFar + leftBoundary); |
| } |
| |
| boundarySoFar = rectangleLeftBoundary; |
| } |
| } |
| |
| private void setRightBoundary(final float rightBoundary) { |
| float boundarySoFar = 0; |
| for (RoundedRectangleShape rectangle : mRectangles) { |
| final float rectangleRightBoundary = rectangle.getBoundingWidth() + boundarySoFar; |
| if (rectangleRightBoundary < rightBoundary) { |
| rectangle.setRightBoundary(rectangle.getBoundingWidth()); |
| } else if (boundarySoFar > rightBoundary) { |
| rectangle.setRightBoundary(0); |
| } else { |
| rectangle.setRightBoundary(rightBoundary - boundarySoFar); |
| } |
| |
| boundarySoFar = rectangleRightBoundary; |
| } |
| } |
| |
| private int getTotalWidth() { |
| int sum = 0; |
| for (RoundedRectangleShape rectangle : mRectangles) { |
| sum += rectangle.getBoundingWidth(); |
| } |
| return sum; |
| } |
| |
| @Override |
| public void draw(Canvas canvas, Paint paint) { |
| for (RoundedRectangleShape rectangle : mRectangles) { |
| rectangle.draw(canvas, paint); |
| } |
| } |
| |
| } |
| |
| SmartSelectSprite(final View view) { |
| final Context context = view.getContext(); |
| mExpandInterpolator = AnimationUtils.loadInterpolator( |
| context, |
| android.R.interpolator.fast_out_slow_in); |
| mCornerInterpolator = AnimationUtils.loadInterpolator( |
| context, |
| android.R.interpolator.fast_out_linear_in); |
| mStrokeWidth = dpToPixel(context, STROKE_WIDTH_DP); |
| mStrokeColor = getStrokeColor(context); |
| mView = view; |
| } |
| |
| private static boolean intersectsOrTouches(RectF a, RectF b) { |
| return a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom; |
| } |
| |
| private List<Drawable> mergeRectanglesToPolygonShape( |
| final List<RectF> rectangles, |
| final int color) { |
| final List<Drawable> drawables = new LinkedList<>(); |
| final Set<List<PointF>> mergedPaths = calculateMergedPolygonPoints(rectangles); |
| |
| for (List<PointF> path : mergedPaths) { |
| // Add the starting point to the end of the polygon so that it ends up closed. |
| path.add(path.get(0)); |
| |
| final PolygonShape shape = new PolygonShape(path); |
| final ShapeDrawable drawable = new ShapeDrawable(shape); |
| |
| drawable.getPaint().setColor(color); |
| drawable.getPaint().setStyle(Paint.Style.STROKE); |
| drawable.getPaint().setStrokeWidth(mStrokeWidth); |
| |
| drawables.add(drawable); |
| } |
| |
| return drawables; |
| } |
| |
| private static Set<List<PointF>> calculateMergedPolygonPoints( |
| List<RectF> rectangles) { |
| final Set<List<RectF>> partitions = new HashSet<>(); |
| final LinkedList<RectF> listOfRects = new LinkedList<>(rectangles); |
| |
| while (!listOfRects.isEmpty()) { |
| final RectF candidate = listOfRects.removeFirst(); |
| final List<RectF> partition = new LinkedList<>(); |
| partition.add(candidate); |
| |
| final LinkedList<RectF> otherCandidates = new LinkedList<>(); |
| otherCandidates.addAll(listOfRects); |
| |
| while (!otherCandidates.isEmpty()) { |
| final RectF otherCandidate = otherCandidates.removeFirst(); |
| for (RectF partitionElement : partition) { |
| if (intersectsOrTouches(partitionElement, otherCandidate)) { |
| partition.add(otherCandidate); |
| listOfRects.remove(otherCandidate); |
| break; |
| } |
| } |
| } |
| |
| partition.sort(Comparator.comparing(o -> o.top)); |
| partitions.add(partition); |
| } |
| |
| final Set<List<PointF>> result = new HashSet<>(); |
| for (List<RectF> partition : partitions) { |
| final List<PointF> points = new LinkedList<>(); |
| |
| final Stack<RectF> rects = new Stack<>(); |
| for (RectF rect : partition) { |
| points.add(new PointF(rect.right, rect.top)); |
| points.add(new PointF(rect.right, rect.bottom)); |
| rects.add(rect); |
| } |
| while (!rects.isEmpty()) { |
| final RectF rect = rects.pop(); |
| points.add(new PointF(rect.left, rect.bottom)); |
| points.add(new PointF(rect.left, rect.top)); |
| } |
| |
| result.add(points); |
| } |
| |
| return result; |
| |
| } |
| |
| /** |
| * Performs the Smart Select animation on the view bound to this SmartSelectSprite. |
| * |
| * @param start The point from which the animation will start. Must be inside |
| * destinationRectangles. |
| * @param destinationRectangles The rectangles which the animation will fill out by its |
| * "selection" and finally join them into a single polygon. |
| * @param onAnimationEnd The callback which will be invoked once the whole animation |
| * completes. |
| * @throws IllegalArgumentException if the given start point is not in any of the |
| * destinationRectangles. |
| * @see #cancelAnimation() |
| */ |
| public void startAnimation( |
| final PointF start, |
| final List<RectF> destinationRectangles, |
| final Runnable onAnimationEnd) throws IllegalArgumentException { |
| cancelAnimation(); |
| |
| final ValueAnimator.AnimatorUpdateListener updateListener = |
| valueAnimator -> mView.invalidate(); |
| |
| final List<RoundedRectangleShape> shapes = new LinkedList<>(); |
| final List<Animator> cornerAnimators = new LinkedList<>(); |
| |
| final RectF centerRectangle = destinationRectangles |
| .stream() |
| .filter((r) -> contains(r, start)) |
| .findFirst() |
| .orElseThrow(() -> new IllegalArgumentException( |
| "Center point is not inside any of the rectangles!")); |
| |
| int startingOffset = 0; |
| for (RectF rectangle : destinationRectangles) { |
| if (rectangle.equals(centerRectangle)) { |
| break; |
| } |
| startingOffset += rectangle.width(); |
| } |
| |
| startingOffset += start.x - centerRectangle.left; |
| |
| final float centerRectangleHalfHeight = centerRectangle.height() / 2; |
| final float startingOffsetLeft = startingOffset - centerRectangleHalfHeight; |
| final float startingOffsetRight = startingOffset + centerRectangleHalfHeight; |
| |
| final @RoundedRectangleShape.ExpansionDirection int[] expansionDirections = |
| generateDirections(centerRectangle, destinationRectangles); |
| |
| final @RoundedRectangleShape.RectangleBorderType int[] rectangleBorderTypes = |
| generateBorderTypes(destinationRectangles); |
| |
| int index = 0; |
| |
| for (RectF rectangle : destinationRectangles) { |
| final RoundedRectangleShape shape = new RoundedRectangleShape( |
| rectangle, |
| expansionDirections[index], |
| rectangleBorderTypes[index], |
| mStrokeWidth); |
| cornerAnimators.add(createCornerAnimator(shape, updateListener)); |
| shapes.add(shape); |
| index++; |
| } |
| |
| final RectangleList rectangleList = new RectangleList(shapes); |
| final ShapeDrawable shapeDrawable = new ShapeDrawable(rectangleList); |
| |
| final Paint paint = shapeDrawable.getPaint(); |
| paint.setColor(mStrokeColor); |
| paint.setStyle(Paint.Style.STROKE); |
| paint.setStrokeWidth(mStrokeWidth); |
| |
| addToOverlay(shapeDrawable); |
| |
| mActiveAnimator = createAnimator(mStrokeColor, destinationRectangles, rectangleList, |
| startingOffsetLeft, startingOffsetRight, cornerAnimators, updateListener, |
| onAnimationEnd); |
| mActiveAnimator.start(); |
| } |
| |
| private Animator createAnimator( |
| final @ColorInt int color, |
| final List<RectF> destinationRectangles, |
| final RectangleList rectangleList, |
| final float startingOffsetLeft, |
| final float startingOffsetRight, |
| final List<Animator> cornerAnimators, |
| final ValueAnimator.AnimatorUpdateListener updateListener, |
| final Runnable onAnimationEnd) { |
| final ObjectAnimator rightBoundaryAnimator = ObjectAnimator.ofFloat( |
| rectangleList, |
| RectangleList.PROPERTY_RIGHT_BOUNDARY, |
| startingOffsetRight, |
| rectangleList.getTotalWidth()); |
| |
| final ObjectAnimator leftBoundaryAnimator = ObjectAnimator.ofFloat( |
| rectangleList, |
| RectangleList.PROPERTY_LEFT_BOUNDARY, |
| startingOffsetLeft, |
| 0); |
| |
| rightBoundaryAnimator.setDuration(EXPAND_DURATION); |
| leftBoundaryAnimator.setDuration(EXPAND_DURATION); |
| |
| rightBoundaryAnimator.addUpdateListener(updateListener); |
| leftBoundaryAnimator.addUpdateListener(updateListener); |
| |
| rightBoundaryAnimator.setInterpolator(mExpandInterpolator); |
| leftBoundaryAnimator.setInterpolator(mExpandInterpolator); |
| |
| final AnimatorSet cornerAnimator = new AnimatorSet(); |
| cornerAnimator.playTogether(cornerAnimators); |
| |
| final AnimatorSet boundaryAnimator = new AnimatorSet(); |
| boundaryAnimator.playTogether(leftBoundaryAnimator, rightBoundaryAnimator); |
| |
| final AnimatorSet animatorSet = new AnimatorSet(); |
| animatorSet.playSequentially(boundaryAnimator, cornerAnimator); |
| |
| setUpAnimatorListener(animatorSet, destinationRectangles, color, onAnimationEnd); |
| |
| return animatorSet; |
| } |
| |
| private void setUpAnimatorListener(final Animator animator, |
| final List<RectF> destinationRectangles, |
| final @ColorInt int color, |
| final Runnable onAnimationEnd) { |
| animator.addListener(new Animator.AnimatorListener() { |
| @Override |
| public void onAnimationStart(Animator animator) { |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animator) { |
| removeExistingDrawables(); |
| |
| final List<Drawable> polygonShapes = mergeRectanglesToPolygonShape( |
| destinationRectangles, |
| color); |
| |
| for (Drawable drawable : polygonShapes) { |
| addToOverlay(drawable); |
| } |
| |
| onAnimationEnd.run(); |
| } |
| |
| @Override |
| public void onAnimationCancel(Animator animator) { |
| } |
| |
| @Override |
| public void onAnimationRepeat(Animator animator) { |
| } |
| }); |
| } |
| |
| private ObjectAnimator createCornerAnimator( |
| final RoundedRectangleShape shape, |
| final ValueAnimator.AnimatorUpdateListener listener) { |
| final ObjectAnimator animator = ObjectAnimator.ofFloat( |
| shape, |
| RoundedRectangleShape.PROPERTY_ROUND_PERCENTAGE, |
| 1.0F, 0.0F); |
| animator.setDuration(CORNER_DURATION); |
| animator.addUpdateListener(listener); |
| animator.setInterpolator(mCornerInterpolator); |
| return animator; |
| } |
| |
| private static @RoundedRectangleShape.ExpansionDirection int[] generateDirections( |
| final RectF centerRectangle, |
| final List<RectF> rectangles) throws IllegalArgumentException { |
| final @RoundedRectangleShape.ExpansionDirection int[] result = new int[rectangles.size()]; |
| |
| final int centerRectangleIndex = rectangles.indexOf(centerRectangle); |
| |
| for (int i = 0; i < centerRectangleIndex - 1; ++i) { |
| result[i] = RoundedRectangleShape.ExpansionDirection.LEFT; |
| } |
| result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.CENTER; |
| for (int i = centerRectangleIndex + 1; i < result.length; ++i) { |
| result[i] = RoundedRectangleShape.ExpansionDirection.RIGHT; |
| } |
| |
| return result; |
| } |
| |
| private static @RoundedRectangleShape.RectangleBorderType int[] generateBorderTypes( |
| final List<RectF> rectangles) { |
| final @RoundedRectangleShape.RectangleBorderType int[] result = new int[rectangles.size()]; |
| |
| for (int i = 1; i < result.length - 1; ++i) { |
| result[i] = RoundedRectangleShape.RectangleBorderType.OVERSHOOT; |
| } |
| |
| result[0] = RoundedRectangleShape.RectangleBorderType.FIT; |
| result[result.length - 1] = RoundedRectangleShape.RectangleBorderType.FIT; |
| return result; |
| } |
| |
| private static float dpToPixel(final Context context, final float dp) { |
| return TypedValue.applyDimension( |
| TypedValue.COMPLEX_UNIT_DIP, |
| dp, |
| context.getResources().getDisplayMetrics()); |
| } |
| |
| @ColorInt |
| private static int getStrokeColor(final Context context) { |
| final TypedValue typedValue = new TypedValue(); |
| final TypedArray array = context.obtainStyledAttributes(typedValue.data, new int[]{ |
| android.R.attr.colorControlActivated}); |
| final int result = array.getColor(0, DEFAULT_STROKE_COLOR); |
| array.recycle(); |
| return result; |
| } |
| |
| /** |
| * A variant of {@link RectF#contains(float, float)} that also allows the point to reside on |
| * the right boundary of the rectangle. |
| * |
| * @param rectangle the rectangle inside which the point should be to be considered "contained" |
| * @param point the point which will be tested |
| * @return whether the point is inside the rectangle (or on it's right boundary) |
| */ |
| private static boolean contains(final RectF rectangle, final PointF point) { |
| final float x = point.x; |
| final float y = point.y; |
| return x >= rectangle.left && x <= rectangle.right && y >= rectangle.top |
| && y <= rectangle.bottom; |
| } |
| |
| private void addToOverlay(final Drawable drawable) { |
| mView.getOverlay().add(drawable); |
| mExistingAnimationDrawables.add(drawable); |
| } |
| |
| private void removeExistingDrawables() { |
| final ViewOverlay overlay = mView.getOverlay(); |
| for (Drawable drawable : mExistingAnimationDrawables) { |
| overlay.remove(drawable); |
| } |
| mExistingAnimationDrawables.clear(); |
| } |
| |
| /** |
| * Cancels any active Smart Select animation that might be in progress. |
| */ |
| public void cancelAnimation() { |
| if (mActiveAnimator != null) { |
| mActiveAnimator.cancel(); |
| mActiveAnimator = null; |
| removeExistingDrawables(); |
| } |
| } |
| |
| } |