pl.rmakowiecki.simplemusicplayer.ui.widget.MusicCoverView.java Source code

Java tutorial

Introduction

Here is the source code for pl.rmakowiecki.simplemusicplayer.ui.widget.MusicCoverView.java

Source

package pl.rmakowiecki.simplemusicplayer.ui.widget;

/*
 * Copyright (c) 2016 Andr Mion
 *
 * 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.
 */

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.drawable.Animatable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.ColorInt;
import android.support.annotation.IntDef;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.v4.os.ParcelableCompat;
import android.support.v4.os.ParcelableCompatCreatorCallbacks;
import android.support.v4.view.AbsSavedState;
import android.transition.ChangeImageTransform;
import android.transition.ChangeTransform;
import android.transition.Transition;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import pl.rmakowiecki.simplemusicplayer.R;

public class MusicCoverView extends android.support.v7.widget.AppCompatImageView implements Animatable {

    public static final int SHAPE_RECTANGLE = 0;
    public static final int SHAPE_CIRCLE = 1;

    static final int ALPHA_TRANSPARENT = 0;
    static final int ALPHA_OPAQUE = 255;

    private static final float TRACK_SIZE = 10;
    private static final float TRACK_WIDTH = 1;
    private static final int TRACK_COLOR = Color.parseColor("#56FFFFFF");

    private static final float FULL_ANGLE = 360;
    private static final float HALF_ANGLE = FULL_ANGLE / 2;
    private static final int DURATION = 2500;
    private static final float DURATION_PER_DEGREES = DURATION / FULL_ANGLE;

    private final ValueAnimator mStartRotateAnimator;
    private final ValueAnimator mEndRotateAnimator;
    private final Transition mCircleToRectTransition;
    private final Transition mRectToCircleTransition;

    private final float mTrackSize;
    private final Paint mTrackPaint;
    private int mTrackAlpha;

    private final Path mClipPath = new Path();
    private final Path mRectPath = new Path();
    private final Path mTrackPath = new Path();

    private boolean mIsMorphing;
    private float mRadius = 0;

    private Callbacks mCallbacks;
    private int mShape;

    @IntDef({ SHAPE_CIRCLE, SHAPE_RECTANGLE })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Shape {
    }

    public interface Callbacks {
        void onMorphEnd(MusicCoverView coverView);

        void onRotateEnd(MusicCoverView coverView);
    }

    public MusicCoverView(Context context) {
        this(context, null, 0);
    }

    public MusicCoverView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MusicCoverView(Context context, AttributeSet attrs, final int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // Canvas.clipPath works wrong when running with hardware acceleration on Android N
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
            setLayerType(View.LAYER_TYPE_HARDWARE, null);
        }

        final float density = getResources().getDisplayMetrics().density;
        mTrackSize = TRACK_SIZE * density;
        mTrackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTrackPaint.setStyle(Paint.Style.STROKE);
        mTrackPaint.setStrokeWidth(TRACK_WIDTH * density);

        mStartRotateAnimator = ObjectAnimator.ofFloat(this, View.ROTATION, 0, FULL_ANGLE);
        mStartRotateAnimator.setInterpolator(new LinearInterpolator());
        mStartRotateAnimator.setRepeatCount(Animation.INFINITE);
        mStartRotateAnimator.setDuration(DURATION);
        mStartRotateAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                float current = getRotation();
                float target = current > HALF_ANGLE ? FULL_ANGLE : 0; // Choose the shortest distance to 0 rotation
                float diff = target > 0 ? FULL_ANGLE - current : current;
                mEndRotateAnimator.setFloatValues(current, target);
                mEndRotateAnimator.setDuration((int) (DURATION_PER_DEGREES * diff));
                mEndRotateAnimator.start();
            }
        });

        mEndRotateAnimator = ObjectAnimator.ofFloat(MusicCoverView.this, View.ROTATION, 0);
        mEndRotateAnimator.setInterpolator(new LinearInterpolator());
        mEndRotateAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                setRotation(0);
                // isRunning method return true if it's called form here.
                // So we need call from post method to get the right returning.
                post(new Runnable() {
                    @Override
                    public void run() {
                        if (mCallbacks != null) {
                            mCallbacks.onRotateEnd(MusicCoverView.this);
                        }
                    }
                });
            }
        });

        mRectToCircleTransition = new MorphTransition(SHAPE_RECTANGLE);
        mRectToCircleTransition.addTarget(this);
        mRectToCircleTransition.addListener(new TransitionAdapter() {
            @Override
            public void onTransitionStart(Transition transition) {
                mIsMorphing = true;
            }

            @Override
            public void onTransitionEnd(Transition transition) {
                mIsMorphing = false;
                mShape = SHAPE_CIRCLE;
                if (mCallbacks != null) {
                    mCallbacks.onMorphEnd(MusicCoverView.this);
                }
            }
        });

        mCircleToRectTransition = new MorphTransition(SHAPE_CIRCLE);
        mCircleToRectTransition.addTarget(this);
        mCircleToRectTransition.addListener(new TransitionAdapter() {
            @Override
            public void onTransitionStart(Transition transition) {
                mIsMorphing = true;
            }

            @Override
            public void onTransitionEnd(Transition transition) {
                mIsMorphing = false;
                mShape = SHAPE_RECTANGLE;
                if (mCallbacks != null) {
                    mCallbacks.onMorphEnd(MusicCoverView.this);
                }
            }
        });

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MusicCoverView);
        @Shape
        int shape = a.getInt(R.styleable.MusicCoverView_cover_shape, SHAPE_RECTANGLE);
        @ColorInt
        int trackColor = a.getColor(R.styleable.MusicCoverView_trackColor, TRACK_COLOR);
        a.recycle();

        setShape(shape);
        setTrackColor(trackColor);
        setScaleType();
    }

    public void setCallbacks(Callbacks callbacks) {
        mCallbacks = callbacks;
    }

    /**
     * Return the current shape
     */
    public int getShape() {
        return mShape;
    }

    /**
     * Set which shape should be drawn by this {@link MusicCoverView}
     *
     * @param shape The shape as {@link #SHAPE_CIRCLE} or {@link #SHAPE_RECTANGLE}
     */
    public void setShape(@Shape int shape) {
        if (shape != mShape) {
            mShape = shape;
            setScaleType();
            if (!isInLayout() && !isLayoutRequested()) {
                calculateRadius();
                resetPaths();
            }
        }
    }

    /**
     * Set the color of the music tracks
     *
     * @param trackColor The color int
     */
    public void setTrackColor(@ColorInt int trackColor) {
        if (trackColor != getTrackColor()) {
            int alpha = mShape == SHAPE_CIRCLE ? ALPHA_OPAQUE : ALPHA_TRANSPARENT;
            mTrackPaint.setColor(trackColor);
            mTrackAlpha = Color.alpha(trackColor);
            mTrackPaint.setAlpha(alpha * mTrackAlpha / ALPHA_OPAQUE);
            invalidate();
        }
    }

    /**
     * Return the current color of the tracks
     */
    public int getTrackColor() {
        return mTrackPaint.getColor();
    }

    float getTransitionRadius() {
        return mRadius;
    }

    void setTransitionRadius(float radius) {
        if (radius != mRadius) {
            mRadius = radius;
            resetPaths();
            invalidate();
        }
    }

    int getTransitionAlpha() {
        return mTrackPaint.getAlpha() * ALPHA_OPAQUE / mTrackAlpha;
    }

    void setTransitionAlpha(@IntRange(from = ALPHA_TRANSPARENT, to = ALPHA_OPAQUE) int alpha) {
        if (alpha != getTransitionAlpha()) {
            mTrackPaint.setAlpha(alpha * mTrackAlpha / ALPHA_OPAQUE);
            invalidate();
        }
    }

    float getMinRadius() {
        final int w = getWidth();
        final int h = getHeight();
        return Math.min(w, h) / 2f;
    }

    float getMaxRadius() {
        final int w = getWidth();
        final int h = getHeight();
        return (float) Math.hypot(w / 2f, h / 2f);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        calculateRadius();
        resetPaths();
    }

    private void calculateRadius() {
        if (SHAPE_CIRCLE == mShape) {
            mRadius = getMinRadius();
        } else {
            mRadius = getMaxRadius();
        }
    }

    private void setScaleType() {
        if (SHAPE_CIRCLE == mShape) {
            setScaleType(ScaleType.CENTER_INSIDE);
        } else {
            setScaleType(ScaleType.CENTER_CROP);
        }
    }

    private void resetPaths() {

        final int w = getWidth();
        final int h = getHeight();
        final float centerX = w / 2f;
        final float centerY = h / 2f;

        mClipPath.reset();
        mClipPath.addCircle(centerX, centerY, mRadius, Path.Direction.CW);

        final int trackRadius = Math.min(w, h);
        final int trackCount = (int) (trackRadius / mTrackSize);

        mTrackPath.reset();
        for (int i = 3; i < trackCount; i++) {
            mTrackPath.addCircle(centerX, centerY, trackRadius * (i / (float) trackCount), Path.Direction.CW);
        }

        mRectPath.reset();
        mRectPath.addRect(0, 0, w, h, Path.Direction.CW);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.clipPath(mClipPath);
        super.onDraw(canvas);
        canvas.drawPath(mTrackPath, mTrackPaint);
    }

    @Override
    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
        // Don't need to consume the system window insets
        return insets;
    }

    /**
     * Starts the transition morph to rect or circle, depending the current shape.
     */
    public void morph() {
        if (SHAPE_CIRCLE == mShape) {
            morphToRect();
        } else {
            morphToCircle();
        }
    }

    private void morphToCircle() {
        if (mIsMorphing) {
            return;
        }
        TransitionManager.beginDelayedTransition((ViewGroup) getParent(), mRectToCircleTransition);
        setScaleType(ScaleType.CENTER_INSIDE);
    }

    private void morphToRect() {
        if (mIsMorphing) {
            return;
        }
        TransitionManager.beginDelayedTransition((ViewGroup) getParent(), mCircleToRectTransition);
        setScaleType(ScaleType.CENTER_CROP);
    }

    /**
     * Start the rotate animation
     */
    @Override
    public void start() {
        if (SHAPE_RECTANGLE == mShape) { // Only start rotate when shape is a circle
            return;
        }
        if (!isRunning()) {
            mStartRotateAnimator.start();
        }
    }

    /**
     * Stop the rotate animation
     */
    @Override
    public void stop() {
        if (mStartRotateAnimator.isRunning()) {
            mStartRotateAnimator.cancel();
        }
    }

    /**
     * Return if the rotate animation is running
     */
    @Override
    public boolean isRunning() {
        return mStartRotateAnimator.isRunning() || mEndRotateAnimator.isRunning() || mIsMorphing;
    }

    private static class MorphTransition extends TransitionSet {
        private MorphTransition(int shape) {
            setOrdering(ORDERING_TOGETHER);
            addTransition(new MusicCoverViewTransition(shape));
            addTransition(new ChangeImageTransform());
            addTransition(new ChangeTransform());
        }
    }

    private static class TransitionAdapter implements Transition.TransitionListener {

        @Override
        public void onTransitionStart(Transition transition) {
        }

        @Override
        public void onTransitionEnd(Transition transition) {
        }

        @Override
        public void onTransitionCancel(Transition transition) {
        }

        @Override
        public void onTransitionPause(Transition transition) {
        }

        @Override
        public void onTransitionResume(Transition transition) {
        }
    }

    /**
     * {@link SavedState} methods
     */

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);
        ss.shape = getShape();
        ss.trackColor = getTrackColor();
        ss.isRotating = mStartRotateAnimator.isRunning();
        return ss;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());
        setShape(ss.shape);
        setTrackColor(ss.trackColor);
        if (ss.isRotating) {
            start();
        }
    }

    public static class SavedState extends AbsSavedState {

        private int shape;
        private int trackColor;
        private boolean isRotating;

        private SavedState(Parcel in, ClassLoader loader) {
            super(in, loader);
            shape = in.readInt();
            trackColor = in.readInt();
            isRotating = (boolean) in.readValue(Boolean.class.getClassLoader());
        }

        private SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeInt(shape);
            dest.writeInt(trackColor);
            dest.writeValue(isRotating);
        }

        @Override
        public String toString() {
            return MusicCoverView.class.getSimpleName() + "." + SavedState.class.getSimpleName() + "{"
                    + Integer.toHexString(System.identityHashCode(this)) + " shape=" + shape + ", trackColor="
                    + trackColor + ", isRotating=" + isRotating + "}";
        }

        public static final Parcelable.Creator<SavedState> CREATOR = ParcelableCompat
                .newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
                    @Override
                    public SavedState createFromParcel(Parcel parcel, ClassLoader loader) {
                        return new SavedState(parcel, loader);
                    }

                    @Override
                    public SavedState[] newArray(int size) {
                        return new SavedState[size];
                    }
                });
    }
}