/* * Copyright (c) Gustavo Claramunt (AnderWeb) 2014. * * 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.marshalchen.common.uimodule.discreteseekbar.internal.drawable; import android.content.res.ColorStateList; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Animatable; import android.os.SystemClock; import android.support.annotation.NonNull; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; /** * Implementation of {@link com.marshalchen.common.uimodule.discreteseekbar.internal.drawable.StateDrawable} to draw a morphing marker symbol. * <p> * It's basically an implementatin of an {@link android.graphics.drawable.Animatable} Drawable with the following details: * </p> * <ul> * <li>Animates from a circle shape to a "marker" shape just using a RoundRect</li> * <li>Animates color change from the normal state color to the pressed state color</li> * <li>Uses a {@link android.graphics.Path} to also serve as Outline for API>=21</li> * </ul> * * @hide */ public class MarkerDrawable extends StateDrawable implements Animatable { private static final long FRAME_DURATION = 1000 / 60; private static final int ANIMATION_DURATION = 250; private float mCurrentScale = 0f; private Interpolator mInterpolator; private long mStartTime; private boolean mReverse = false; private boolean mRunning = false; private int mDuration = ANIMATION_DURATION; //size of the actual thumb drawable to use as circle state size private float mClosedStateSize; //value to store que current scale when starting an animation and interpolate from it private float mAnimationInitialValue; //extra offset directed from the View to account //for its internal padding between circle state and marker state private int mExternalOffset; //colors for interpolation private int mStartColor; private int mEndColor; Path mPath = new Path(); RectF mRect = new RectF(); Matrix mMatrix = new Matrix(); private MarkerAnimationListener mMarkerListener; public MarkerDrawable(@NonNull ColorStateList tintList, int closedSize) { super(tintList); mInterpolator = new AccelerateDecelerateInterpolator(); mClosedStateSize = closedSize; mStartColor = tintList.getColorForState(new int[]{android.R.attr.state_pressed}, tintList.getDefaultColor()); mEndColor = tintList.getDefaultColor(); } public void setExternalOffset(int offset) { mExternalOffset = offset; } @Override void doDraw(Canvas canvas, Paint paint) { if (!mPath.isEmpty()) { paint.setStyle(Paint.Style.FILL); int color = blendColors(mStartColor, mEndColor, mCurrentScale); paint.setColor(color); canvas.drawPath(mPath, paint); } } public Path getPath() { return mPath; } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); computePath(bounds); } private void computePath(Rect bounds) { final float currentScale = mCurrentScale; final Path path = mPath; final RectF rect = mRect; final Matrix matrix = mMatrix; path.reset(); int totalSize = Math.min(bounds.width(), bounds.height()); float initial = mClosedStateSize; float destination = totalSize; float currentSize = initial + (destination - initial) * currentScale; float halfSize = currentSize / 2f; float inverseScale = 1f - currentScale; float cornerSize = halfSize * inverseScale; float[] corners = new float[]{halfSize, halfSize, halfSize, halfSize, halfSize, halfSize, cornerSize, cornerSize}; rect.set(bounds.left, bounds.top, bounds.left + currentSize, bounds.top + currentSize); path.addRoundRect(rect, corners, Path.Direction.CCW); matrix.reset(); matrix.postRotate(-45, bounds.left + halfSize, bounds.top + halfSize); matrix.postTranslate((bounds.width() - currentSize) / 2, 0); float hDiff = (bounds.bottom - currentSize - mExternalOffset) * inverseScale; matrix.postTranslate(0, hDiff); path.transform(matrix); } private void updateAnimation(float factor) { float initial = mAnimationInitialValue; float destination = mReverse ? 0f : 1f; mCurrentScale = initial + (destination - initial) * factor; computePath(getBounds()); invalidateSelf(); } public void animateToPressed() { unscheduleSelf(mUpdater); mReverse = false; if (mCurrentScale < 1) { mRunning = true; mAnimationInitialValue = mCurrentScale; float durationFactor = 1f - mCurrentScale; mDuration = (int) (ANIMATION_DURATION * durationFactor); mStartTime = SystemClock.uptimeMillis(); scheduleSelf(mUpdater, mStartTime + FRAME_DURATION); } else { notifyFinishedToListener(); } } public void animateToNormal() { mReverse = true; unscheduleSelf(mUpdater); if (mCurrentScale > 0) { mRunning = true; mAnimationInitialValue = mCurrentScale; float durationFactor = 1f - mCurrentScale; mDuration = ANIMATION_DURATION - (int) (ANIMATION_DURATION * durationFactor); mStartTime = SystemClock.uptimeMillis(); scheduleSelf(mUpdater, mStartTime + FRAME_DURATION); } else { notifyFinishedToListener(); } } private final Runnable mUpdater = new Runnable() { @Override public void run() { long currentTime = SystemClock.uptimeMillis(); long diff = currentTime - mStartTime; if (diff < mDuration) { float interpolation = mInterpolator.getInterpolation((float) diff / (float) mDuration); scheduleSelf(mUpdater, currentTime + FRAME_DURATION); updateAnimation(interpolation); } else { unscheduleSelf(mUpdater); mRunning = false; updateAnimation(1f); notifyFinishedToListener(); } } }; public void setMarkerListener(MarkerAnimationListener listener) { mMarkerListener = listener; } private void notifyFinishedToListener() { if (mMarkerListener != null) { if (mReverse) { mMarkerListener.onClosingComplete(); } else { mMarkerListener.onOpeningComplete(); } } } @Override public void start() { //No-Op. We control our own animation } @Override public void stop() { unscheduleSelf(mUpdater); } @Override public boolean isRunning() { return mRunning; } private static int blendColors(int color1, int color2, float factor) { final float inverseFactor = 1f - factor; float a = (Color.alpha(color1) * factor) + (Color.alpha(color2) * inverseFactor); float r = (Color.red(color1) * factor) + (Color.red(color2) * inverseFactor); float g = (Color.green(color1) * factor) + (Color.green(color2) * inverseFactor); float b = (Color.blue(color1) * factor) + (Color.blue(color2) * inverseFactor); return Color.argb((int) a, (int) r, (int) g, (int) b); } /** * A listener interface to porpagate animation events * This is the "poor's man" AnimatorListener for this Drawable */ public interface MarkerAnimationListener { public void onClosingComplete(); public void onOpeningComplete(); } }