Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
CounterView
/*
* 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