/gist:a5d64c0402696f525a76 Secret
Last active
July 10, 2016 16:50
CounterView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (C) 2014 Twitter Inc. | |
* | |
* 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.content.Context; | |
import android.graphics.Canvas; | |
import android.graphics.Color; | |
import android.graphics.Paint; | |
import android.graphics.Rect; | |
import android.graphics.Typeface; | |
import android.util.AttributeSet; | |
import android.util.Log; | |
import android.view.View; | |
import java.util.ArrayList; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* Usage: http://edisonwang.com/blog/vine-loop-counter-view | |
* @author ewang (@wew) | |
*/ | |
public class CounterView extends View implements Runnable { | |
private static final String TAG = "CounterView"; | |
private static final int FRAME_DELAY = 20; // 1000 / 20 = 50 fps | |
private static final int DIGIT_SPACING_X = 3; // px between text | |
private static final int DIGIT_SPACING_Y = 3; // px between text | |
private static final int ANIMATION_DURATION_MAX = (int) TimeUnit.NANOSECONDS.convert(150, TimeUnit.MILLISECONDS); | |
private static final int ANIMATION_DURATION_MIN = (int) TimeUnit.NANOSECONDS.convert(20, TimeUnit.MILLISECONDS); | |
private static final String K_SEPARATOR = ","; | |
private static final int K_INDEX = 10; | |
private static final boolean NO_ANIMATE_TO_NEXT_NUMBER = false; | |
private final Paint mPaint; | |
private long mKnownCount; | |
private double mVelocityPerMS; | |
private int mLastDigitCount; | |
private long mKnownCountRefreshTime; | |
private long mDrawingCount; | |
private Rect[] mBounds; | |
private int mMaxTextWidth; | |
private int mMaxTextHeight; | |
private final ArrayList<DigitAnimation> mDigitAnimations = new ArrayList<>(); | |
private String mLastPrint; | |
private long mExtraCount; | |
private AnimationMode mAnimationMode = new AnimationMode(false, false, true); | |
private long mMinAnimationSeparation; | |
private long mLastAnimationTime; | |
private volatile boolean mCanAnimate; | |
private final int[] mLOCK = new int[0]; | |
private long mMaxAnimationSeparation; | |
private boolean mIsPaused; | |
public void setAnimationMode(AnimationMode animationMode) { | |
mAnimationMode = animationMode; | |
} | |
public void setMinAnimationSeparation(long minAnimationSeparation) { | |
mMinAnimationSeparation = minAnimationSeparation; | |
} | |
public void setMaxAnimationSeparation(long maxAnimationSeparation) { | |
mMaxAnimationSeparation = maxAnimationSeparation; | |
} | |
public void incrementExtraCount(int n) { | |
adjustExtraCount(mExtraCount + n); | |
} | |
public static class AnimationMode { | |
public final boolean continuousAnimation; | |
public final boolean pedometerAnimation; | |
public final boolean alphaAnimation; | |
public AnimationMode(boolean continuousAnimation, boolean pedometerAnimation, boolean alphaAnimation) { | |
this.continuousAnimation = continuousAnimation; | |
this.pedometerAnimation = pedometerAnimation; | |
this.alphaAnimation = alphaAnimation; | |
} | |
} | |
//Holds the state for individual digits. | |
public class DigitAnimation { | |
public final int mIndexFromRight; | |
private final double mDivider; | |
private final double mNextDivider; | |
public long mAnimationDuration; | |
public long mAnimationStartTime; | |
public long mAnimatingCount; | |
public boolean mIsAnimating; | |
public int mDrawingDigit; | |
public int mCurrentAnimatingToDigit; | |
public DigitAnimation(int indexFromRight, double velocityPerMS, long drawingCount) { | |
mIndexFromRight = indexFromRight; | |
mDivider = Math.pow(10, mIndexFromRight); | |
mNextDivider = Math.pow(10, mIndexFromRight + 1); | |
if (mIndexFromRight > 0) { | |
mDrawingDigit = (int) (((drawingCount / mDivider) % 10)); | |
} else { | |
mDrawingDigit = (int) (drawingCount % 10); | |
} | |
mAnimatingCount = drawingCount; | |
if (velocityPerMS > 0) { | |
if (mAnimationMode.continuousAnimation) { | |
if (mIndexFromRight > 0) { | |
mAnimationDuration = (long) (1 / (velocityPerMS / (10 * mIndexFromRight))); | |
} else { | |
mAnimationDuration = (long) (1 / velocityPerMS); | |
} | |
} else { | |
mAnimationDuration = Integer.MAX_VALUE; | |
} | |
} else { | |
mAnimationDuration = Integer.MAX_VALUE; | |
} | |
mAnimationDuration = TimeUnit.NANOSECONDS.convert(mAnimationDuration, TimeUnit.MILLISECONDS); | |
mAnimationDuration = Math.min(mAnimationDuration, ANIMATION_DURATION_MAX); | |
mAnimationDuration = Math.max(mAnimationDuration, ANIMATION_DURATION_MIN); | |
} | |
public boolean invalidate(long currentTime, long currentCount, boolean shouldAnimate) { | |
int currentRealDigit = mDrawingDigit; | |
if (mIsAnimating) { | |
if ((currentTime - mAnimationStartTime) > mAnimationDuration) { | |
if (mIndexFromRight > 0) { | |
currentRealDigit = (int) ((mAnimatingCount / mDivider) % 10); | |
} else { | |
currentRealDigit = (int) (mAnimatingCount % 10); | |
} | |
if (mAnimationMode.pedometerAnimation) { | |
mDrawingDigit = (mDrawingDigit + 1) % 10; | |
} else { | |
mDrawingDigit = mCurrentAnimatingToDigit; | |
} | |
mIsAnimating = false; | |
} | |
} | |
boolean shouldAnimateNext = false; | |
if (shouldAnimate || currentRealDigit != mDrawingDigit) { | |
if (!mIsAnimating) { | |
mIsAnimating = true; | |
mAnimationStartTime = System.nanoTime(); | |
int currentRest = (int) ((mAnimatingCount / mNextDivider)); | |
int nextRest = (int) ((currentCount / mNextDivider) ); | |
shouldAnimateNext = currentRest != nextRest; | |
mAnimatingCount = currentCount; | |
if (mAnimationMode.pedometerAnimation) { | |
mCurrentAnimatingToDigit = (mDrawingDigit + 1) % 10; | |
} else { | |
if (mIndexFromRight > 0) { | |
mCurrentAnimatingToDigit = (int) ((mAnimatingCount / mDivider) % 10); | |
} else { | |
mCurrentAnimatingToDigit = (int) (mAnimatingCount % 10); | |
} | |
} | |
} | |
} | |
return shouldAnimateNext; | |
} | |
} | |
public CounterView(Context context) { | |
super(context); | |
mPaint = init(); | |
updateTextSizes(); | |
} | |
private Paint init() { | |
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG); | |
mBounds = new Rect[] { | |
new Rect(), | |
new Rect(), | |
new Rect(), | |
new Rect(), | |
new Rect(), | |
new Rect(), | |
new Rect(), | |
new Rect(), | |
new Rect(), | |
new Rect(), | |
new Rect(), | |
}; | |
paint.setTextSize(32); | |
paint.setColor(Color.BLACK); | |
paint.setStyle(Paint.Style.FILL); | |
paint.setTextAlign(Paint.Align.RIGHT); | |
return paint; | |
} | |
private void updateTextSizes() { | |
mMaxTextWidth = 0; | |
mMaxTextHeight = 0; | |
for (int i = 0; i < mBounds.length; i++) { | |
if (i != K_INDEX) { | |
mPaint.getTextBounds(String.valueOf(i), 0, 1, mBounds[i]); | |
} else { | |
mPaint.getTextBounds(K_SEPARATOR, 0, 1, mBounds[i]); | |
} | |
if (mMaxTextWidth < mBounds[i].width()) { | |
mMaxTextWidth = mBounds[i].width(); | |
} | |
if (mMaxTextHeight < mBounds[i].height()) { | |
mMaxTextHeight = mBounds[i].height(); | |
} | |
} | |
} | |
public CounterView(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
mPaint = init(); | |
updateTextSizes(); | |
} | |
public CounterView(Context context, AttributeSet attrs, int defStyle) { | |
super(context, attrs, defStyle); | |
mPaint = init(); | |
updateTextSizes(); | |
} | |
public void setTypeFace(Typeface typeFace) { | |
mPaint.setTypeface(typeFace); | |
requestLayout(); | |
} | |
public void setTextSize(float textSize) { | |
mPaint.setTextSize(textSize); | |
requestLayout(); | |
} | |
public void reset() { | |
mLastDigitCount = 0; | |
mKnownCount = 0; | |
mKnownCountRefreshTime = -1; | |
mVelocityPerMS = 0; | |
mDigitAnimations.clear(); | |
} | |
public long setKnownCount(long knownCount, double velocityPerMS, long knownCountRefreshTime) { | |
if (knownCountRefreshTime > 0) { | |
long time = System.currentTimeMillis(); | |
mKnownCount = (long) (knownCount + velocityPerMS * (time - knownCountRefreshTime)); | |
mKnownCountRefreshTime = time; | |
} else { | |
mKnownCount = knownCount; | |
} | |
mVelocityPerMS = velocityPerMS; | |
synchronized (mLOCK) { | |
mLastDigitCount = 0; | |
mDigitAnimations.clear(); | |
invalidateDigitSize(true); | |
} | |
return mKnownCount; | |
} | |
public void pause() { | |
mIsPaused = true; | |
removeCallbacks(this); | |
} | |
public void resume() { | |
if (mIsPaused) { | |
mIsPaused = false; | |
postInvalidate(); | |
} | |
} | |
public long getCount() { | |
return (long) (mKnownCount + (mKnownCountRefreshTime > 0 ? mVelocityPerMS * (System.currentTimeMillis() - mKnownCountRefreshTime) : 0)); | |
} | |
@Override | |
public void onDraw(Canvas canvas) { | |
int x = getMeasuredWidth(); | |
final int measuredHeight = getMeasuredHeight(); | |
synchronized (mLOCK) { | |
int length = mDigitAnimations.size(); | |
String num = ""; | |
for (int i = 0; i < length; i++) { | |
if (i >= 3 && i % 3 == 0) { | |
mPaint.setAlpha(255); | |
canvas.drawText(K_SEPARATOR, x, (measuredHeight >> 1) + mBounds[K_INDEX].height(), mPaint); | |
x -= mBounds[K_INDEX].width(); | |
} | |
DigitAnimation digitAnimation = mDigitAnimations.get(i); | |
int digit = digitAnimation.mDrawingDigit; | |
String digitText = String.valueOf(digit); | |
final int y = getIntrinsicHeightForDigit(measuredHeight, digit); | |
if (digitAnimation.mIsAnimating) { | |
long diff = (System.nanoTime() - digitAnimation.mAnimationStartTime); | |
double progress = diff / (double) digitAnimation.mAnimationDuration; | |
if (diff >= digitAnimation.mAnimationDuration || (digitAnimation.mCurrentAnimatingToDigit == digit && !mAnimationMode.pedometerAnimation)) { | |
num += digitAnimation.mCurrentAnimatingToDigit; | |
mPaint.setAlpha(255); | |
canvas.drawText(String.valueOf(digitAnimation.mCurrentAnimatingToDigit), x, y, mPaint); | |
} else { | |
int topY = (int) (y - (progress * (mBounds[digit].height() + DIGIT_SPACING_Y))); | |
if (digit != 0 || i != length - 1 || length == 1) { | |
if (mAnimationMode.alphaAnimation) { | |
mPaint.setAlpha((int) ((1 - progress) * 255)); | |
} | |
canvas.drawText(digitText, x, topY, mPaint); | |
} | |
num += digitText; | |
int botY = topY + mBounds[digit].height() + DIGIT_SPACING_Y; | |
if (mAnimationMode.alphaAnimation) { | |
mPaint.setAlpha((int) (progress * 255)); | |
} | |
canvas.drawText(String.valueOf(digitAnimation.mCurrentAnimatingToDigit), x, botY, mPaint); | |
} | |
} else { | |
num += digitText; | |
mPaint.setAlpha(255); | |
canvas.drawText(digitText, x, y, mPaint); | |
} | |
x -= mMaxTextWidth + DIGIT_SPACING_X; | |
} | |
if (!num.equals(mLastPrint)) { | |
mLastPrint = num; | |
} | |
} | |
removeCallbacks(this); | |
if (!mIsPaused) { | |
postDelayed(this, FRAME_DELAY); | |
} | |
} | |
@Override | |
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
int widthMode = MeasureSpec.getMode(widthMeasureSpec); | |
int heightMode = MeasureSpec.getMode(heightMeasureSpec); | |
int widthSize = MeasureSpec.getSize(widthMeasureSpec); | |
int heightSize = MeasureSpec.getSize(heightMeasureSpec); | |
int width, height; | |
updateTextSizes(); | |
if (widthMode == MeasureSpec.EXACTLY) { | |
width = widthSize; | |
} else { | |
int digits = Math.max(mLastDigitCount, String.valueOf(mDrawingCount).length()); | |
width = (digits * (mMaxTextWidth + DIGIT_SPACING_X)) + | |
(digits / 3 * mBounds[K_INDEX].width()) + | |
getPaddingLeft() + getPaddingRight(); | |
if (heightMode == MeasureSpec.AT_MOST) { | |
width = Math.min(width, widthSize); | |
} | |
} | |
if (heightMode == MeasureSpec.EXACTLY) { | |
height = heightSize; | |
} else { | |
height = mMaxTextHeight + getPaddingTop() + getPaddingBottom() + DIGIT_SPACING_Y; | |
if (heightMode == MeasureSpec.AT_MOST) { | |
height = Math.min(height, heightSize); | |
} | |
} | |
Log.d(TAG, "onMeasure " + width + " * " + height); | |
setMeasuredDimension(width, height); | |
} | |
private int getIntrinsicHeightForDigit(int measuredHeight, int digit) { | |
return (measuredHeight + mBounds[digit].height()) >> 1; | |
} | |
public void invalidateCounterUI() { | |
mCanAnimate = true; | |
} | |
/** | |
* Adjust extraCount, this will trigger the animation. | |
* @param extraCount | |
*/ | |
public void adjustExtraCount(long extraCount) { | |
mExtraCount = extraCount; | |
} | |
private void invalidateDigitSize(boolean forceDigitSizeRecal) { | |
synchronized (mLOCK) { | |
if (mDigitAnimations.size() == 0) { | |
mDigitAnimations.add(new DigitAnimation(0, mVelocityPerMS, mDrawingCount)); | |
} | |
long currentCount = getCount() + mExtraCount; | |
long currentTime = System.nanoTime(); | |
int digitCount = String.valueOf(mDrawingCount).length(); | |
long diffTimeMs = System.currentTimeMillis() - mLastAnimationTime; | |
boolean shouldAnimateNext = currentCount > mDigitAnimations.get(0).mAnimatingCount; | |
if (forceDigitSizeRecal || ( //If we got a new digit | |
diffTimeMs >= mMinAnimationSeparation && // Or (if min separation time has passed | |
(mCanAnimate || diffTimeMs >= mMaxAnimationSeparation))) { // AND we should) | |
mLastAnimationTime = System.currentTimeMillis(); | |
int i = 0; | |
long newDrawingCount = 0; | |
while (i < digitCount || shouldAnimateNext) { | |
if (mDigitAnimations.size() <= i) { | |
mDigitAnimations.add(new DigitAnimation(i, mVelocityPerMS, mDrawingCount)); | |
} | |
DigitAnimation digitAnim = mDigitAnimations.get(i); | |
shouldAnimateNext = digitAnim.invalidate(currentTime, currentCount, shouldAnimateNext); | |
if (NO_ANIMATE_TO_NEXT_NUMBER && forceDigitSizeRecal) { | |
digitAnim.mAnimationStartTime = 1; | |
} | |
newDrawingCount += i == 0 ? digitAnim.mDrawingDigit : digitAnim.mDrawingDigit * Math.pow(10, i); | |
i++; | |
} | |
setDrawingCount(newDrawingCount, forceDigitSizeRecal); | |
if (mLastDigitCount != mDigitAnimations.size()) { | |
mLastDigitCount = mDigitAnimations.size(); | |
requestLayout(); | |
} | |
if (!mAnimationMode.continuousAnimation) { | |
mCanAnimate = false; | |
} | |
} | |
} | |
} | |
private void setDrawingCount(long newDrawingCount, boolean invalidation) { | |
mDrawingCount = newDrawingCount; | |
} | |
@Override | |
public void run() { | |
invalidateDigitSize(false); | |
if ((!mIsPaused) && isShown()) { | |
postInvalidate(); | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment