/* * Copyright (C) 2014 Fastboot Mobile, LLC. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program; * if not, see <http://www.gnu.org/licenses>. */ package com.fastbootmobile.encore.app.ui; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.Log; import android.util.TypedValue; import android.view.animation.AccelerateDecelerateInterpolator; import com.dd.CircularAnimatedDrawable; /** * Animated drawable giving native Material animations between Play, Pause and Stop state */ public class PlayPauseDrawable extends Drawable { private static final String TAG = "PlayPauseDrawable"; public static final int SHAPE_PAUSE = 0; public static final int SHAPE_STOP = 1; public static final int SHAPE_PLAY = 2; private static final float TRANSITION_DURATION = 300; private static final float PAUSE_TRIM_RATIO = 0.12f; private int mCurrentShape; private int mRequestShape; private Path mPath; private Paint mPaint; private boolean mInitialDrawDone; private AccelerateDecelerateInterpolator mTransitionInterpolator; private long mTransitionAccumulator; private long mLastTransitionTick; private boolean mIsBuffering; private int mYOffset; private CircularAnimatedDrawable mBufferingDrawable; private int mIconWidth; private int mIconHeight; private int mWidth; private int mHeight; private int mPadding; private Rect mBufferingRect = new Rect(); private final int mTwoDP; /** * Default constructor */ public PlayPauseDrawable(Resources res, float scale) { this(res, scale, scale); } public PlayPauseDrawable(Resources res, float scaleOut, float scaleIcon) { mCurrentShape = mRequestShape = -1; mPath = new Path(); mPaint = new Paint(); mPaint.setColor(0xCCFFFFFF); mPaint.setStyle(Paint.Style.FILL); mInitialDrawDone = false; mTransitionInterpolator = new AccelerateDecelerateInterpolator(); mIconWidth = mIconHeight = (int) (dpToPx(res, 34) * scaleIcon); mWidth = mHeight = (int) (dpToPx(res, 48) * scaleOut); mPadding = mWidth - mIconWidth; mTwoDP = (int) dpToPx(res, 2); setBounds(0, 0, mWidth, mHeight); } /** * Sets the drawing color of the drawable * @param color The color */ public void setColor(int color) { mPaint.setColor(color); } /** * Sets the Y-axis offset of the drawable * @param offset The offset */ public void setYOffset(int offset) { mYOffset = offset; } /** * Sets the shape that the drawable should morph into * @param shape One of {@link #SHAPE_PLAY}, {@link #SHAPE_PAUSE}, {@link #SHAPE_STOP} */ public void setShape(int shape) { if (mRequestShape != shape) { mRequestShape = shape; mTransitionAccumulator = 0; mLastTransitionTick = System.currentTimeMillis(); invalidateSelf(); } } /** * Sets whether or not an indeterminate progress indicator should be displayed, indicating * that the playback is pending buffering. * @param buffering true to display the indicator, false otherwise */ public void setBuffering(boolean buffering) { mIsBuffering = buffering; invalidateSelf(); } /** * @return The currently visible shape */ public int getCurrentShape() { return mCurrentShape; } /** * @return The latest shape that has been requested */ public int getRequestedShape() { return mRequestShape; } private float getProgress() { float raw = mTransitionAccumulator / TRANSITION_DURATION; return Math.max(0.0f, Math.min(raw, 1.0f)); } private void shapeInitialPath() { mInitialDrawDone = true; mPath.reset(); switch (mRequestShape) { case SHAPE_PAUSE: transitionStopToPause(1.0f); break; case SHAPE_PLAY: transitionStopToPlay(1.0f); break; case SHAPE_STOP: transitionStopToPause(0.0f); break; } mCurrentShape = mRequestShape; } private void transitionPlayToStop(final float progress) { // Animation from play to stop: Play rotates 90°, point split at the tip (on the right) mPath.reset(); // Make the play triangle, with the "fourth" point at the tip moving towards making a // square (they split progressively) mPath.moveTo(mPadding + mTwoDP * (1.0f - progress), mPadding); mPath.lineTo(mWidth - mPadding, (mHeight) / 2 - (mHeight - mPadding * 2) / 2 * progress); mPath.lineTo(mWidth - mPadding, (mHeight) / 2 + (mHeight - mPadding * 2) / 2 * progress); mPath.lineTo(mPadding + mTwoDP * (1.0f - progress), mHeight - mPadding); // Rotate it Matrix matrix = new Matrix(); matrix.postRotate(90.0f * progress, mWidth / 2, mHeight / 2); mPath.transform(matrix); } private void transitionStopToPause(float progress) { mPath.reset(); final int halfWidth = mWidth / 2; // We glue two half-square together, which we then split and slightly trim (10%) mPath.addRect(mPadding, mPadding, mWidth / 2 - halfWidth * PAUSE_TRIM_RATIO * progress, mHeight - mPadding, Path.Direction.CW); mPath.addRect(mWidth / 2 + halfWidth * PAUSE_TRIM_RATIO * progress, mPadding, mWidth - mPadding, mHeight - mPadding, Path.Direction.CW); /* Another look (centering the pause bars instead of just splitting in the middle): Set PAUSE_TRIM_RATIO to 0.14f mPath.addRect(mHalfPadding + halfWidth * PAUSE_TRIM_RATIO * progress, mHalfPadding, halfWidth - halfWidth * PAUSE_TRIM_RATIO * progress, height - mHalfPadding, Path.Direction.CW); mPath.addRect(halfWidth + halfWidth * PAUSE_TRIM_RATIO * progress, mHalfPadding, width - mHalfPadding - halfWidth * PAUSE_TRIM_RATIO * progress, height - mHalfPadding, Path.Direction.CW); */ } private void transitionPlayToPause(float progress) { // Same as Play to Stop for half of the animation, then we split the rectangle in two to // make the pause shape if (progress < 0.75f) { transitionPlayToStop(progress * (1.0f / 0.75f)); } else { float localProgress = (progress - 0.75f) * (1.0f / 0.25f); transitionStopToPause(localProgress); } } private void transitionPauseToPlay(float progress) { transitionPlayToPause(1.0f - progress); } private void transitionPauseToStop(float progress) { transitionStopToPause(1.0f - progress); } private void transitionStopToPlay(float progress) { transitionPlayToStop(1.0f - progress); } @Override public void draw(Canvas canvas) { canvas.save(); canvas.translate((getBounds().width() - mWidth) / 2, (getBounds().height() - mHeight) / 2); canvas.translate(0, -mYOffset); if (!mInitialDrawDone) { shapeInitialPath(); } if (mCurrentShape != mRequestShape) { final float progress = mTransitionInterpolator.getInterpolation(getProgress()); // Play to Pause if (mCurrentShape == SHAPE_PLAY && mRequestShape == SHAPE_PAUSE) { transitionPlayToPause(progress); } // Play to Stop else if (mCurrentShape == SHAPE_PLAY && mRequestShape == SHAPE_STOP) { transitionPlayToStop(progress); } // Pause to Play else if (mCurrentShape == SHAPE_PAUSE && mRequestShape == SHAPE_PLAY) { transitionPauseToPlay(progress); } // Pause to Stop else if (mCurrentShape == SHAPE_PAUSE && mRequestShape == SHAPE_STOP) { transitionPauseToStop(progress); } // Stop to Pause else if (mCurrentShape == SHAPE_STOP && mRequestShape == SHAPE_PAUSE) { transitionStopToPause(progress); } // Stop to Play else if (mCurrentShape == SHAPE_STOP && mRequestShape == SHAPE_PLAY) { transitionStopToPlay(progress); } else { Log.e(TAG, "Unhandled transition from " + mCurrentShape + " to " + mRequestShape); } if (progress >= 1.0f) { mCurrentShape = mRequestShape; } else { mTransitionAccumulator += System.currentTimeMillis() - mLastTransitionTick; mLastTransitionTick = System.currentTimeMillis(); } } canvas.drawPath(mPath, mPaint); canvas.restore(); if (mIsBuffering) { if (mBufferingDrawable == null) { mBufferingDrawable = new CircularAnimatedDrawable(0xFFFFFFFF, 8.0f); mBufferingDrawable.setCallback(getCallback()); mBufferingDrawable.start(); } copyBounds(mBufferingRect); if (mBufferingRect.width() < mBufferingRect.height()) { final int previousHeight = mBufferingRect.height(); final int newHeight = mBufferingRect.width(); mBufferingRect.top = mBufferingRect.left - ((newHeight - previousHeight) / 2); mBufferingRect.bottom = mBufferingRect.right - ((newHeight - previousHeight) / 2); } else { final int previousWidth = mBufferingRect.width(); final int newWidth = mBufferingRect.height(); mBufferingRect.left = mBufferingRect.top - ((newWidth - previousWidth) / 2); mBufferingRect.right = mBufferingRect.bottom - ((newWidth - previousWidth) / 2); } mBufferingDrawable.setBounds(mBufferingRect); mBufferingDrawable.draw(canvas); } if (mCurrentShape != mRequestShape || mIsBuffering) { // Invalidate ourselves to redraw the next animation frame invalidateSelf(); } canvas.translate(0, mYOffset); } @Override public void setAlpha(int i) { } @Override public void setColorFilter(ColorFilter colorFilter) { } @Override public int getOpacity() { return 255; } static float dpToPx(Resources resources, float dp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.getDisplayMetrics()); } }