/*
* Copyright 2015 Hippo Seven
*
* 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.hippo.widget;
import android.animation.Animator;
import android.animation.ObjectAnimator;
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.RectF;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.PathInterpolatorCompat;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import com.hippo.nimingban.R;
import com.hippo.yorozuya.ViewUtils;
import java.util.ArrayList;
// Base on android.graphics.drawable.MaterialProgressDrawable in L preview
public class ProgressView extends View {
private static final Interpolator TRIM_START_INTERPOLATOR;
private static final Interpolator TRIM_END_INTERPOLATOR;
private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
static {
Path trimStartPath = new Path();
trimStartPath.moveTo(0.0f, 0.0f);
trimStartPath.lineTo(0.5f, 0.0f);
trimStartPath.cubicTo(0.7f, 0.0f, 0.6f, 1f, 1f, 1f);
TRIM_START_INTERPOLATOR = PathInterpolatorCompat.create(trimStartPath);
Path trimEndPath = new Path();
trimEndPath.moveTo(0.0f, 0.0f);
trimEndPath.cubicTo(0.2f, 0.0f, 0.1f, 1f, 0.5f, 1f);
trimEndPath.lineTo(1f, 1f);
TRIM_END_INTERPOLATOR = PathInterpolatorCompat.create(trimEndPath);
}
private final ArrayList<Animator> mAnimators = new ArrayList<>();
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final RectF mRectF = new RectF();
private boolean mIndeterminate;
private float mTrimStart = 0.0f;
private float mTrimEnd = 0.0f;
private float mTrimOffset = 0.0f;
private float mTrimRotation = 0.0f;
// It is a trick to avoid first sight stuck. Get it from ProgressBar
private boolean mShouldStartAnimationDrawable = false;
public ProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
int color = a.getColor(R.styleable.ProgressView_color, Color.BLACK);
mPaint.setColor(color);
mPaint.setStrokeCap(Paint.Cap.SQUARE);
mPaint.setStrokeJoin(Paint.Join.MITER);
mPaint.setStyle(Paint.Style.STROKE);
mIndeterminate = a.getBoolean(R.styleable.ProgressView_indeterminate, true);
setProgress(a.getFloat(R.styleable.ProgressView_progress, 0f));
a.recycle();
setupAnimators();
}
private void setupAnimators() {
ObjectAnimator trimStart = ObjectAnimator.ofFloat(this, "trimStart", 0.0f, 0.75f);
trimStart.setDuration(1333L);
trimStart.setInterpolator(TRIM_START_INTERPOLATOR);
trimStart.setRepeatCount(Animation.INFINITE);
ObjectAnimator trimEnd = ObjectAnimator.ofFloat(this, "trimEnd", 0.0f, 0.75f);
trimEnd.setDuration(1333L);
trimEnd.setInterpolator(TRIM_END_INTERPOLATOR);
trimEnd.setRepeatCount(Animation.INFINITE);
ObjectAnimator trimOffset = ObjectAnimator.ofFloat(this, "trimOffset", 0.0f, 0.25f);
trimOffset.setDuration(1333L);
trimOffset.setInterpolator(LINEAR_INTERPOLATOR);
trimOffset.setRepeatCount(Animation.INFINITE);
ObjectAnimator trimRotation = ObjectAnimator.ofFloat(this, "trimRotation", 0.0f, 720.0f);
trimRotation.setDuration(6665L);
trimRotation.setInterpolator(LINEAR_INTERPOLATOR);
trimRotation.setRepeatCount(Animation.INFINITE);
mAnimators.add(trimStart);
mAnimators.add(trimEnd);
mAnimators.add(trimOffset);
mAnimators.add(trimRotation);
}
private void startAnimation() {
mShouldStartAnimationDrawable = true;
postInvalidate();
}
private void startAnimationActually() {
ArrayList<Animator> animators = mAnimators;
int N = animators.size();
for (int i = 0; i < N; i++) {
Animator animator = animators.get(i);
if (!animator.isRunning()) {
animator.start();
}
}
}
private void stopAnimation() {
mShouldStartAnimationDrawable = false;
ArrayList<Animator> animators = mAnimators;
int N = animators.size();
for (int i = 0; i < N; i++) {
animators.get(i).cancel();
}
}
public boolean isRunning() {
ArrayList<Animator> animators = mAnimators;
int N = animators.size();
for (int i = 0; i < N; i++) {
Animator animator = animators.get(i);
if (animator.isRunning()) {
return true;
}
}
return false;
}
@SuppressWarnings("unused")
public float getTrimStart() {
return mTrimStart;
}
@SuppressWarnings("unused")
public void setTrimStart(float trimStart) {
mTrimStart = trimStart;
invalidate();
}
@SuppressWarnings("unused")
public float getTrimEnd() {
return mTrimEnd;
}
@SuppressWarnings("unused")
public void setTrimEnd(float trimEnd) {
mTrimEnd = trimEnd;
invalidate();
}
@SuppressWarnings("unused")
public float getTrimOffset() {
return mTrimOffset;
}
@SuppressWarnings("unused")
public void setTrimOffset(float trimOffset) {
mTrimOffset = trimOffset;
invalidate();
}
@SuppressWarnings("unused")
public float getTrimRotation() {
return mTrimRotation;
}
@SuppressWarnings("unused")
public void setTrimRotation(float trimRotation) {
mTrimRotation = trimRotation;
invalidate();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mIndeterminate && getVisibility() == VISIBLE) {
startAnimation();
}
}
@Override
protected void onDetachedFromWindow() {
if (mIndeterminate) {
stopAnimation();
}
// This should come after stopAnimation(), otherwise an invalidate message remains in the
// queue, which can prevent the entire view hierarchy from being GC'ed during a rotation
super.onDetachedFromWindow();
}
@Override
public void setVisibility(int v) {
if (getVisibility() != v) {
super.setVisibility(v);
if (mIndeterminate) {
if (v == GONE || v == INVISIBLE) {
stopAnimation();
} else if (ViewCompat.isAttachedToWindow(this)) {
startAnimation();
}
}
}
}
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (mIndeterminate && ViewCompat.isAttachedToWindow(this)) {
if (visibility == GONE || visibility == INVISIBLE) {
stopAnimation();
} else if (ViewCompat.isAttachedToWindow(this)) {
startAnimation();
}
}
}
public void setColor(int color) {
mPaint.setColor(color);
invalidate();
}
public void setIndeterminate(boolean indeterminate) {
if (mIndeterminate != indeterminate) {
mIndeterminate = indeterminate;
if (indeterminate) {
if (isShown() && ViewCompat.isAttachedToWindow(this)) {
startAnimation();
}
} else {
stopAnimation();
}
}
}
public boolean isIndeterminate() {
return mIndeterminate;
}
public void setProgress(float progress) {
if (!mIndeterminate) {
mTrimStart = 0f;
mTrimEnd = progress;
mTrimOffset = 0f;
mTrimRotation = 0f;
invalidate();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(ViewUtils.getSuitableSize(getSuggestedMinimumWidth(), widthMeasureSpec),
ViewUtils.getSuitableSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mPaint.setStrokeWidth(Math.min(w, h) / 12.0f);
mRectF.set(0, 0, w, h);
mRectF.inset(w / 48.0f * 5.0f, h / 48.0f * 5.0f);
}
@Override
protected void onDraw(Canvas canvas) {
int saved = canvas.save();
canvas.rotate(mTrimRotation, mRectF.centerX(), mRectF.centerY());
float startAngle = (mTrimStart + mTrimOffset) * 360.0f - 90;
float sweepAngle = (mTrimEnd - mTrimStart) * 360.0f;
canvas.drawArc(mRectF, startAngle, sweepAngle, false, mPaint);
canvas.restoreToCount(saved);
if (mShouldStartAnimationDrawable) {
mShouldStartAnimationDrawable = false;
startAnimationActually();
}
}
}