/* * Copyright (C) 2015 Thomas Robert Altstidl * * 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 com.tr4android.support.extension.drawable; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.support.annotation.ColorInt; import android.view.animation.Interpolator; import com.tr4android.support.extension.animation.AnimationUtils; import com.tr4android.support.extension.animation.ValueAnimatorCompat; public class MediaControlDrawable extends Drawable { private static final String TAG = "Media Control"; public enum State { PLAY, PAUSE, STOP } private State mCurrentState = State.PLAY; private State mTargetState = State.PLAY; // Paint private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private float mPadding = 0f; private RectF mInternalBounds = new RectF(); // Values that determine the current drawing state private float mRotation; private Path mPrimaryPath = new Path(); private Path mSecondaryPath = new Path(); // Cached metrics private float mCenter; private float mSize; private float mPlayTipOffset; private float mPlayBaseOffset; // Animator private ValueAnimatorCompat mAnimator; public MediaControlDrawable(Context context, @ColorInt int color, float padding, State state, Interpolator interpolator, int duration) { mPadding = padding; mCurrentState = state; mTargetState = state; // The paint used to draw the icons mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(color); // The animator used to animate the icons mAnimator = AnimationUtils.createAnimator(); mAnimator.setFloatValues(0f, 90f); mAnimator.setDuration(duration); mAnimator.setInterpolator(interpolator); mAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimatorCompat animator) { setTransitionState(animator.getAnimatedFloatValue(), animator.getAnimatedFraction()); } }); mAnimator.setListener(new ValueAnimatorCompat.AnimatorListener() { @Override public void onAnimationStart(ValueAnimatorCompat animator) {} @Override public void onAnimationEnd(ValueAnimatorCompat animator) { mCurrentState = mTargetState; // make sure the icon has reached its final appearance setTransitionState(0f, 0f); } @Override public void onAnimationCancel(ValueAnimatorCompat animator) {} @Override public void onAnimationRepeat(ValueAnimatorCompat animator) {} }); } @Override public void draw(Canvas canvas) { Rect bounds = getBounds(); int saveCount = canvas.save(); canvas.rotate(mRotation, bounds.centerX(), bounds.centerY()); // Draw the previously calculated paths canvas.drawPath(mPrimaryPath, mPaint); canvas.drawPath(mSecondaryPath, mPaint); canvas.restoreToCount(saveCount); } @Override public void setBounds(Rect bounds) { calculateTrimArea(bounds); super.setBounds(bounds); } @Override public void setBounds(int left, int top, int right, int bottom) { calculateTrimArea(new Rect(left, top, right, bottom)); super.setBounds(left, top, right, bottom); } @Override public void setAlpha(int i) { mPaint.setAlpha(i); } @Override public void setColorFilter(ColorFilter colorFilter) { mPaint.setColorFilter(colorFilter); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } private void setTransitionState(float rotation, float fraction) { if (mCurrentState == mTargetState) rotation = fraction = 0f; // Calculate current drawable metrics mRotation = rotation; mPrimaryPath.reset(); mSecondaryPath.reset(); if (mCurrentState == State.PLAY && (mTargetState == State.STOP || mTargetState == State.PLAY)) { // Transition between play and stop icon float offset = (1f - fraction) * mPlayTipOffset; float offsetBase = (1f - fraction) * mPlayBaseOffset; mPrimaryPath.moveTo(mInternalBounds.right + offset, interpolate(mCenter, mInternalBounds.bottom, fraction)); mPrimaryPath.lineTo(mInternalBounds.left + offset, mInternalBounds.bottom + offsetBase); mPrimaryPath.lineTo(mInternalBounds.left + offset, interpolate(mCenter, mInternalBounds.top, fraction)); mPrimaryPath.lineTo(interpolate(mInternalBounds.left, mInternalBounds.right, fraction) + offset, mInternalBounds.top - offsetBase); } else if (mCurrentState == State.STOP && mTargetState == State.PAUSE) { // Transition between stop and pause icon float primaryBottom = mCenter - fraction * 3f / 20f * mSize; float secondaryTop = mCenter + fraction * 3f / 20f * mSize; mPrimaryPath.moveTo(mInternalBounds.right, mInternalBounds.top); mPrimaryPath.lineTo(mInternalBounds.right, primaryBottom); mPrimaryPath.lineTo(mInternalBounds.left, primaryBottom); mPrimaryPath.lineTo(mInternalBounds.left, mInternalBounds.top); mSecondaryPath.moveTo(mInternalBounds.right, mInternalBounds.bottom); mSecondaryPath.lineTo(mInternalBounds.right, secondaryTop); mSecondaryPath.lineTo(mInternalBounds.left, secondaryTop); mSecondaryPath.lineTo(mInternalBounds.left, mInternalBounds.bottom); } else if (mCurrentState == State.PAUSE && mTargetState == State.PLAY) { // Transition between pause and play icon float offset = fraction * mPlayTipOffset; float offsetBase = fraction * mPlayBaseOffset; if (fraction < 0.5f) { // two paths float primaryRight = mCenter - (-2f * fraction + 1f) * 3f / 20f * mSize; float primaryBottomLeft = mInternalBounds.left + fraction * (mSize / 2f); float secondaryLeft = mCenter + (-2f * fraction + 1f) * 3f / 20f * mSize; float secondaryBottomRight = mInternalBounds.right - fraction * (mSize / 2f); mPrimaryPath.moveTo(mInternalBounds.left - offsetBase, mInternalBounds.bottom - offset); mPrimaryPath.lineTo(primaryRight, mInternalBounds.bottom - offset); mPrimaryPath.lineTo(primaryRight, mInternalBounds.top - offset); mPrimaryPath.lineTo(primaryBottomLeft, mInternalBounds.top - offset); mSecondaryPath.moveTo(mInternalBounds.right + offsetBase, mInternalBounds.bottom - offset); mSecondaryPath.lineTo(secondaryLeft, mInternalBounds.bottom - offset); mSecondaryPath.lineTo(secondaryLeft, mInternalBounds.top - offset); mSecondaryPath.lineTo(secondaryBottomRight, mInternalBounds.top - offset); } else { // one path float primaryBottomLeft = mInternalBounds.left + fraction * (mSize / 2f); float secondaryBottomRight = mInternalBounds.right - fraction * (mSize / 2f); mPrimaryPath.moveTo(mInternalBounds.left - offsetBase, mInternalBounds.bottom - offset); mPrimaryPath.lineTo(primaryBottomLeft, mInternalBounds.top - offset); mPrimaryPath.lineTo(secondaryBottomRight, mInternalBounds.top - offset); mPrimaryPath.lineTo(mInternalBounds.right + offsetBase, mInternalBounds.bottom - offset); } } else if (mCurrentState == State.PLAY && mTargetState == State.PAUSE) { // Transition between play and pause icon float offset = (1f - fraction) * mPlayTipOffset; float offsetBase = (1f - fraction) * mPlayBaseOffset; if (fraction > 0.5f) { // two paths float primaryBottom = mCenter - (2f * fraction - 1f) * 3f / 20f * mSize; float primaryLeftTop = mInternalBounds.left + (1f - fraction) * (mSize / 2f); float secondaryTop = mCenter + (2f * fraction - 1f) * 3f / 20f * mSize; float secondaryLeftBottom = mInternalBounds.right - (1f - fraction) * (mSize / 2f); mPrimaryPath.moveTo(mInternalBounds.left + offset, mInternalBounds.top - offsetBase); mPrimaryPath.lineTo(mInternalBounds.left + offset, primaryBottom); mPrimaryPath.lineTo(mInternalBounds.right + offset, primaryBottom); mPrimaryPath.lineTo(mInternalBounds.right + offset, primaryLeftTop); mSecondaryPath.moveTo(mInternalBounds.left + offset, mInternalBounds.bottom + offsetBase); mSecondaryPath.lineTo(mInternalBounds.left + offset, secondaryTop); mSecondaryPath.lineTo(mInternalBounds.right + offset, secondaryTop); mSecondaryPath.lineTo(mInternalBounds.right + offset, secondaryLeftBottom); } else { // one path float primaryLeftTop = mInternalBounds.left + (1f - fraction) * (mSize / 2f); float secondaryLeftBottom = mInternalBounds.right - (1f - fraction) * (mSize / 2f); mPrimaryPath.moveTo(mInternalBounds.left + offset, mInternalBounds.top - offsetBase); mPrimaryPath.lineTo(mInternalBounds.right + offset, primaryLeftTop); mPrimaryPath.lineTo(mInternalBounds.right + offset, secondaryLeftBottom); mPrimaryPath.lineTo(mInternalBounds.left + offset, mInternalBounds.bottom + offsetBase); } } else if (mCurrentState == State.PAUSE && (mTargetState == State.STOP || mTargetState == State.PAUSE)) { // Transition between pause and stop icon float primaryRight = mCenter - (1f - fraction) * 3f / 20f * mSize; float secondaryLeft = mCenter + (1f - fraction) * 3f / 20f * mSize; mPrimaryPath.moveTo(mInternalBounds.left, mInternalBounds.top); mPrimaryPath.lineTo(primaryRight, mInternalBounds.top); mPrimaryPath.lineTo(primaryRight, mInternalBounds.bottom); mPrimaryPath.lineTo(mInternalBounds.left, mInternalBounds.bottom); mSecondaryPath.moveTo(mInternalBounds.right, mInternalBounds.top); mSecondaryPath.lineTo(secondaryLeft, mInternalBounds.top); mSecondaryPath.lineTo(secondaryLeft, mInternalBounds.bottom); mSecondaryPath.lineTo(mInternalBounds.right, mInternalBounds.bottom); } else if (mCurrentState == State.STOP && (mTargetState == State.PLAY || mTargetState == State.STOP)) { // Transition between stop and play icon float offset = fraction * mPlayTipOffset; float offsetBase = fraction * mPlayBaseOffset; mPrimaryPath.moveTo(interpolate(mInternalBounds.left, mCenter, fraction), mInternalBounds.top - offset); mPrimaryPath.lineTo(mInternalBounds.left - offsetBase, mInternalBounds.bottom - offset); mPrimaryPath.lineTo(interpolate(mInternalBounds.right, mCenter, fraction), mInternalBounds.bottom - offset); mPrimaryPath.lineTo(mInternalBounds.right + offsetBase, interpolate(mInternalBounds.top, mInternalBounds.bottom, fraction) - offset); } invalidateSelf(); } /** * This calculates the trim area for the icon as specified in the guidelines * * @param bounds * @see <a href="https://www.google.com/design/spec/style/icons.html#icons-system-icons"> * Google design guidelines - Icon - Style - System Icons</a> */ private void calculateTrimArea(Rect bounds) { float size = Math.min(bounds.height(), bounds.width()); float yOffset = (bounds.height() - size) / 2f; float xOffset = (bounds.width() - size) / 2f; float padding = mPadding + (bounds.height() - 2f * mPadding) * 1f / 6f; mInternalBounds.set(bounds.left + padding + xOffset, bounds.top + padding + yOffset, bounds.right - padding - xOffset, bounds.bottom - padding - yOffset); mCenter = mInternalBounds.centerX(); mSize = mInternalBounds.width(); mPlayTipOffset = 1f / 6f * mSize; mPlayBaseOffset = 0.07735f * mSize; setTransitionState(0f, 0f); } /** * Helper for interpolating between two values (ultimately points) * This is only used for the more complex play/stop animations * * @param start The value of the coordinate at the start of the animation * @param end The value of the coordinate at the end of the animation * @param fraction The current fraction of the animation * @return The interpolated value */ private float interpolate(float start, float end, float fraction) { return (1f - fraction) * start + fraction * end; } public void setMediaControlState(State state) { if (mAnimator.isRunning()) { mAnimator.end(); } mTargetState = state; mAnimator.start(); } public State getMediaControlState() { return mCurrentState; } public static class Builder { private Context mContext; private int mColor; private float mPadding; private State mInitialState; private Interpolator mAnimationInterpolator; private int mAnimationDuration; public Builder(Context context) { mContext = context; // Default values mColor = Color.WHITE; mPadding = 0f; mInitialState = State.PLAY; mAnimationInterpolator = AnimationUtils.ACCELERATE_DECELERATE_INTERPOLATOR; mAnimationDuration = 500; } public Builder setColor(@ColorInt int color) { mColor = color; return this; } public Builder setPadding(float padding) { mPadding = padding; return this; } public Builder setInitialState(State initialState) { mInitialState = initialState; return this; } public Builder setAnimationInterpolator(Interpolator interpolator) { mAnimationInterpolator = interpolator; return this; } public Builder setAnimationDuration(int duration) { mAnimationDuration = duration; return this; } public MediaControlDrawable build() { return new MediaControlDrawable(mContext, mColor, mPadding, mInitialState, mAnimationInterpolator, mAnimationDuration); } } }