Java tutorial
/* * Copyright (C) 2016 sin3hz * * 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 io.github.sin3hz.wifispinnerview; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; 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.Animatable; import android.graphics.drawable.Drawable; import android.support.v4.view.animation.PathInterpolatorCompat; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; public class WifiSpinnerDrawable extends Drawable implements Animatable { private static final Interpolator ANGLE_ANIMATOR_INTERPOLATOR = new LinearInterpolator(); private static final Interpolator SWEEP_ANIMATOR_INTERPOLATOR = new SweepInterpolator(); private static final int SWEEP_ANIMATOR_DELAY = 200; private static final int SWEEP_ANIMATOR_DURATION = 850; private static final int ANGLE_ANIMATOR_DURATION = 5000; public static final int MAX_SWEEP_ANGLE = 180; public static final int SPINNER_ANGLE = 90; public static final int DEFAULT_SPINNER_COLOR = Color.parseColor("#4CAF50"); public static final int DEFAULT_SPINNER_COUNT = 3; private Rect mBounds; private Paint mSpinnerPaint; private int mSpinnerCount; private Spinner[] mSpinners; private float mGlobalAngle; private int mSweepAnimatorDuration; private ValueAnimator mAngleAnimator; private AnimatorSet mSweepAnimator; private boolean mIsRunning; public WifiSpinnerDrawable(int color, int spinnerCount) { if (spinnerCount <= 0) { throw new IllegalArgumentException("spinner count must be positive"); } mSpinnerPaint = new Paint(); mSpinnerPaint.setAntiAlias(true); mSpinnerPaint.setStyle(Paint.Style.STROKE); super.setVisible(false, false); setColor(color); setupSpinnerCount(spinnerCount); setupAnimators(); } public WifiSpinnerDrawable() { this(DEFAULT_SPINNER_COLOR, DEFAULT_SPINNER_COUNT); } static class SweepInterpolator implements Interpolator { TimeInterpolator mPathInterpolator = PathInterpolatorCompat.create(0.4f, 0.0f, 0.2f, 1f); @Override public float getInterpolation(float input) { if (input < 0.5) return 0; return mPathInterpolator.getInterpolation((input - 0.5f) / 0.5f); } } private void setupAnimators() { AnimatorSet set = new AnimatorSet(); for (int i = 0; i < mSpinnerCount; i++) { final int index = i; final ValueAnimator sweepAnimator = ValueAnimator.ofFloat(0, MAX_SWEEP_ANGLE); sweepAnimator.setInterpolator(SWEEP_ANIMATOR_INTERPOLATOR); sweepAnimator.setDuration(mSweepAnimatorDuration); sweepAnimator.setRepeatMode(ValueAnimator.RESTART); sweepAnimator.setRepeatCount(ValueAnimator.INFINITE); sweepAnimator.setStartDelay(index * SWEEP_ANIMATOR_DELAY); sweepAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mSpinners[index].sweepAngle = (float) animation.getAnimatedValue(); mSpinners[index].updatePath(); invalidateSelf(); } }); sweepAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationRepeat(Animator animation) { mSpinners[index].sweepAngleOffset = (mSpinners[index].sweepAngleOffset + MAX_SWEEP_ANGLE) % 360; mSpinners[index].updatePath(); } }); set.playTogether(sweepAnimator); } mSweepAnimator = set; mAngleAnimator = ValueAnimator.ofFloat(0, 360); mAngleAnimator.setInterpolator(ANGLE_ANIMATOR_INTERPOLATOR); mAngleAnimator.setRepeatCount(ValueAnimator.INFINITE); mAngleAnimator.setRepeatMode(ValueAnimator.RESTART); mAngleAnimator.setDuration(ANGLE_ANIMATOR_DURATION); mAngleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mGlobalAngle = (float) animation.getAnimatedValue(); updatePath(); invalidateSelf(); } }); } @Override public boolean setVisible(boolean visible, boolean restart) { if (restart) { reset(true); } if (isVisible() != visible) { if (visible) { if (mIsRunning) { startRunning(); } } else { stopRunning(); } } return super.setVisible(visible, restart); } private void reset(boolean resetGlobalAngle) { if (resetGlobalAngle) { mGlobalAngle = 0; } for (Spinner spinner : mSpinners) { spinner.sweepAngle = 0; spinner.sweepAngleOffset = 0; } } @Override public void start() { if (!isRunning()) { mIsRunning = true; if (isVisible()) { startRunning(); } } } @Override public void stop() { if (isRunning()) { mIsRunning = false; stopRunning(); } } @Override public boolean isRunning() { return mIsRunning; } private boolean isAnimatorStarted() { return mSweepAnimator.isStarted() || mAngleAnimator.isStarted(); } private void startRunning() { if (isAnimatorStarted()) { return; } mSweepAnimator.start(); mAngleAnimator.start(); } private void stopRunning() { if (!isAnimatorStarted()) { return; } mSweepAnimator.end(); mAngleAnimator.end(); reset(true); } @Override public void draw(Canvas canvas) { canvas.save(); canvas.rotate(mGlobalAngle, mBounds.centerX(), mBounds.centerY()); for (int i = 0; i < mSpinnerCount; i++) { canvas.drawPath(mSpinners[i].path, mSpinners[i].paint); } canvas.restore(); } @Override public void setAlpha(int alpha) { if (alpha != mSpinnerPaint.getAlpha()) { mSpinnerPaint.setAlpha(alpha); invalidateSelf(); } } @Override public void setColorFilter(ColorFilter colorFilter) { mSpinnerPaint.setColorFilter(colorFilter); invalidateSelf(); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); if (mBounds == null) { mBounds = new Rect(); } int size = Math.min(bounds.height(), bounds.width()); mBounds.left = bounds.centerX() - size / 2; mBounds.right = mBounds.left + size; mBounds.top = bounds.centerY() - size / 2; mBounds.bottom = mBounds.top + size; configureBounds(); } private void configureBounds() { if (mBounds == null) return; float spinnerStrokeWidth = mBounds.width() / 2f / (mSpinnerCount * 2 - 1); mSpinnerPaint.setStrokeWidth(spinnerStrokeWidth); float halfSpinnerWidth = spinnerStrokeWidth / 2f; for (int i = mSpinnerCount - 1; i >= 0; i--) { float outsizeSpinnerWidth = spinnerStrokeWidth * 2 * (mSpinnerCount - 1 - i); mSpinners[i].bounds.set(mBounds.left + halfSpinnerWidth + outsizeSpinnerWidth, mBounds.top + halfSpinnerWidth + outsizeSpinnerWidth, mBounds.right - halfSpinnerWidth - outsizeSpinnerWidth, mBounds.bottom - halfSpinnerWidth - outsizeSpinnerWidth); } updatePath(); } private void updatePath() { for (Spinner spinner : mSpinners) { spinner.updatePath(); } } public void setColor(int color) { if (color != mSpinnerPaint.getColor()) { mSpinnerPaint.setColor(color); invalidateSelf(); } } public int getColor() { return mSpinnerPaint.getColor(); } private int getSpinnerCount() { return mSpinnerCount; } private void setSpinnerCount(int spinnerCount) { if (isRunning()) { throw new IllegalStateException("spinner count must set before running"); } if (mSpinnerCount != spinnerCount) { setupSpinnerCount(spinnerCount); setupAnimators(); } } private void setupSpinnerCount(int spinnerCount) { mSpinnerCount = spinnerCount; mSweepAnimatorDuration = SWEEP_ANIMATOR_DURATION + SWEEP_ANIMATOR_DELAY * (mSpinnerCount - 1); mSpinners = new Spinner[spinnerCount]; for (int i = mSpinners.length - 1; i >= 0; i--) { mSpinners[i] = new Spinner(); mSpinners[i].paint = mSpinnerPaint; } configureBounds(); } static final class Spinner { RectF bounds; float sweepAngle; float sweepAngleOffset; Path path; Paint paint; public Spinner() { bounds = new RectF(); path = new Path(); } public void updatePath() { path.rewind(); path.addArc(bounds, sweepAngle + sweepAngleOffset, SPINNER_ANGLE); } } }