├── README.md └── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── screen_apture └── digitalview.gif └── src ├── androidTest └── java │ └── com │ └── salmonzhg │ └── digitview │ └── ApplicationTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── salmonzhg │ │ └── digitview │ │ ├── MainActivity.java │ │ ├── utils │ │ └── DisplayUtils.java │ │ └── views │ │ ├── DigitalGroupView.java │ │ └── DigitalView.java └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── attr.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── test └── java └── com └── salmonzhg └── digitview └── ExampleUnitTest.java /README.md: -------------------------------------------------------------------------------- 1 | # DigitView 2 | 一个滚动数字的控件 3 | 4 | ![](https://github.com/billy96322/DigitView/blob/master/app/screen_apture/digitalview.gif "demo") 5 | 6 | ###特性 7 | * 基于属性动画实现,如需兼容2.3需修改使用[NineOldAndroids](https://github.com/JakeWharton/NineOldAndroids) 8 | * 可以动态修改颜色,字体大小,间距等 9 | * 可以在activity中动态创建 10 | 11 | ###已知缺陷 12 | * 位数一旦设置则固定下来,不能根据设置的数字位数来动态变化。修改位数将重置数字 13 | * 初始的数字只能是若干个0,暂不支持设置初始值 14 | * 动态设置字体大小可能导致字体位置在Y轴上产生偏移 15 | 16 | ###使用 17 | 在布局文件中添加 18 | ``` 19 | 29 | ``` 30 | 以下参数分变为数字的颜色,位数,间距,大小 31 | ``` 32 | app:digiGroupColor="@color/colorAccent" 33 | app:digiGroupFigureCounts="5" 34 | app:digiGroupInterval="1dp" 35 | app:digiGroupTextSize="16sp" 36 | ``` 37 | 在Activity中添加 38 | ```Java 39 | digitalGroupView.setDigits(num); 40 | ``` 41 | 即可触发动画。如果转入的参数为负数,将取绝对值。如果需要在Activity中动态添加,添加如下代码。 42 | ```Java 43 | DigitalGroupView view = new DigitalGroupView(this); 44 | view.setTextSize(14); 45 | view.setFigureCount(3); 46 | view.setInterval(5); 47 | view.setColor(Color.BLACK); 48 | viewGroup.addView(view); 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | applicationId "com.salmonzhg.digitview" 9 | minSdkVersion 14 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:23.3.0' 26 | } 27 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\Salmon\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/screen_apture/digitalview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salmonzhg/DigitView/d91d9fdff5a42d592ab7c9bd7ce1d61fd0824ddb/app/screen_apture/digitalview.gif -------------------------------------------------------------------------------- /app/src/androidTest/java/com/salmonzhg/digitview/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.salmonzhg.digitview; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/salmonzhg/digitview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.salmonzhg.digitview; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.support.v7.widget.AppCompatSeekBar; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.Button; 9 | import android.widget.EditText; 10 | import android.widget.RelativeLayout; 11 | import android.widget.SeekBar; 12 | 13 | import com.salmonzhg.digitview.views.DigitalGroupView; 14 | 15 | public class MainActivity extends AppCompatActivity { 16 | 17 | Button buttonPlay, buttonAddView; 18 | AppCompatSeekBar seekInterval, seekFigureCount, seekSize; 19 | DigitalGroupView digitalGroupView; 20 | EditText editDigit; 21 | private boolean hasViewAdded; 22 | 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_main); 27 | 28 | buttonPlay = (Button) findViewById(R.id.button_play); 29 | buttonAddView = (Button) findViewById(R.id.button_add_view); 30 | seekInterval = (AppCompatSeekBar) findViewById(R.id.seek_interval); 31 | seekFigureCount = (AppCompatSeekBar) findViewById(R.id.seek_figure_count); 32 | seekSize = (AppCompatSeekBar) findViewById(R.id.seek_size); 33 | digitalGroupView = (DigitalGroupView) findViewById(R.id.digital); 34 | editDigit = (EditText) findViewById(R.id.edit_digital); 35 | 36 | buttonPlay.setOnClickListener(new View.OnClickListener() { 37 | @Override 38 | public void onClick(View v) { 39 | int num = 0; 40 | try { 41 | num = Integer.parseInt(editDigit.getText().toString()); 42 | } catch (NumberFormatException e) { 43 | e.printStackTrace(); 44 | } 45 | digitalGroupView.setDigits(num); 46 | } 47 | }); 48 | 49 | 50 | SeekBar.OnSeekBarChangeListener seekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { 51 | @Override 52 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 53 | if (progress == 0) { 54 | return; 55 | } 56 | switch (seekBar.getId()) { 57 | case R.id.seek_interval: 58 | digitalGroupView.setInterval(progress); 59 | break; 60 | case R.id.seek_figure_count: 61 | digitalGroupView.setFigureCount(progress); 62 | break; 63 | case R.id.seek_size: 64 | digitalGroupView.setTextSize(progress); 65 | break; 66 | } 67 | } 68 | 69 | @Override 70 | public void onStartTrackingTouch(SeekBar seekBar) { 71 | 72 | } 73 | 74 | @Override 75 | public void onStopTrackingTouch(SeekBar seekBar) { 76 | 77 | } 78 | }; 79 | 80 | seekFigureCount.setOnSeekBarChangeListener(seekBarChangeListener); 81 | seekSize.setOnSeekBarChangeListener(seekBarChangeListener); 82 | seekInterval.setOnSeekBarChangeListener(seekBarChangeListener); 83 | 84 | hasViewAdded = false; 85 | buttonAddView.setOnClickListener(new View.OnClickListener() { 86 | @Override 87 | public void onClick(View v) { 88 | if (hasViewAdded) 89 | return; 90 | addView(); 91 | hasViewAdded = true; 92 | } 93 | }); 94 | } 95 | 96 | private void addView() { 97 | DigitalGroupView view = new DigitalGroupView(this); 98 | view.setTextSize(14); 99 | view.setFigureCount(3); 100 | 101 | RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( 102 | ViewGroup.LayoutParams.WRAP_CONTENT, 103 | ViewGroup.LayoutParams.WRAP_CONTENT); 104 | params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE); 105 | params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE); 106 | view.setLayoutParams(params); 107 | 108 | ViewGroup vg = (ViewGroup) buttonPlay.getParent(); 109 | vg.addView(view); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/com/salmonzhg/digitview/utils/DisplayUtils.java: -------------------------------------------------------------------------------- 1 | package com.salmonzhg.digitview.utils; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * Created by Salmon on 2016/4/22 0022. 7 | */ 8 | public class DisplayUtils { 9 | 10 | public static int dip2px(Context context, float dip) { 11 | final float density = context.getResources().getDisplayMetrics().density; 12 | return (int) (dip * density + 0.5); 13 | } 14 | 15 | public static int px2dip(Context context, float px) { 16 | final float density = context.getResources().getDisplayMetrics().density; 17 | return (int) (px / density + 0.5f); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/salmonzhg/digitview/views/DigitalGroupView.java: -------------------------------------------------------------------------------- 1 | package com.salmonzhg.digitview.views; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Color; 6 | import android.util.AttributeSet; 7 | import android.view.ViewGroup; 8 | import android.widget.LinearLayout; 9 | 10 | import com.salmonzhg.digitview.R; 11 | import com.salmonzhg.digitview.utils.DisplayUtils; 12 | 13 | 14 | /** 15 | * Created by Salmon on 2016/6/23 0023. 16 | */ 17 | public class DigitalGroupView extends LinearLayout { 18 | private static final int DEFAULT_COLOR = Color.BLACK; 19 | private static final int DEFAULT_TEXT_SIZE = 16; 20 | private static final int DEFAULT_FIGURE_COUNT = 1; 21 | private static final int DEFAULT_INTERVAL = 2; 22 | private int mFigureCount = DEFAULT_FIGURE_COUNT; 23 | private int mColor = DEFAULT_COLOR; 24 | private int mTextSize = DEFAULT_TEXT_SIZE; 25 | private int mTextInterval = DEFAULT_INTERVAL; 26 | private int mDigits = 0; 27 | private int[] mParsedDigits; 28 | 29 | public DigitalGroupView(Context context) { 30 | super(context); 31 | 32 | init(context, null); 33 | } 34 | 35 | public DigitalGroupView(Context context, AttributeSet attrs) { 36 | super(context, attrs); 37 | 38 | init(context, attrs); 39 | } 40 | 41 | private void init(Context context, AttributeSet attrs) { 42 | setOrientation(HORIZONTAL); 43 | 44 | if (attrs != null) { 45 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DigitalGroupView); 46 | 47 | mFigureCount = a.getInteger(R.styleable.DigitalGroupView_digiGroupFigureCounts, DEFAULT_FIGURE_COUNT); 48 | mColor = a.getColor(R.styleable.DigitalGroupView_digiGroupColor, Color.BLACK); 49 | mTextSize = a.getDimensionPixelSize(R.styleable.DigitalGroupView_digiGroupTextSize, DEFAULT_TEXT_SIZE); 50 | mTextInterval = a.getDimensionPixelOffset(R.styleable.DigitalGroupView_digiGroupInterval, DEFAULT_INTERVAL); 51 | } 52 | 53 | resetChildren(); 54 | } 55 | 56 | public void setFigureCount(int count) { 57 | if (count < 1) 58 | return; 59 | mFigureCount = count; 60 | resetChildren(); 61 | 62 | requestLayout(); 63 | invalidate(); 64 | } 65 | 66 | public void setInterval(int interval) { 67 | if (interval < 0) { 68 | return; 69 | } 70 | mTextInterval = dp2px(interval); 71 | 72 | int count = getChildCount(); 73 | for (int i = 0; i < count; i++) { 74 | DigitalView v = (DigitalView) getChildAt(i); 75 | LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 76 | ViewGroup.LayoutParams.WRAP_CONTENT); 77 | if (i > 0) 78 | params.leftMargin = dp2px(mTextInterval); 79 | v.setLayoutParams(params); 80 | } 81 | 82 | requestLayout(); 83 | } 84 | 85 | public void setColor(int color) { 86 | mColor = color; 87 | for (DigitalView v : getChildren()) { 88 | v.setTextColor(mColor); 89 | } 90 | invalidate(); 91 | } 92 | 93 | public void setTextSize(int size) { 94 | mTextSize = size; 95 | for (DigitalView v : getChildren()) { 96 | v.setTextSize(mTextSize); 97 | } 98 | invalidate(); 99 | } 100 | 101 | private int dp2px(int pxValue) { 102 | return DisplayUtils.dip2px(getContext(), pxValue); 103 | } 104 | 105 | public void setDigits(int digits) { 106 | digits = Math.abs(digits); 107 | mDigits = digits; 108 | 109 | parseDigits(); 110 | play(); 111 | } 112 | 113 | private void play() { 114 | int count = getChildCount(); 115 | for (int i = 0; i < count; i++) { 116 | DigitalView v = (DigitalView) getChildAt(i); 117 | v.startAnim(mParsedDigits[i]); 118 | } 119 | } 120 | 121 | /** 122 | * 截取数字,规则如下: 123 | * | 1 | 5 | 6 | 7 | <-- 输入 124 | * ↓ ↓ ↓ 125 | * | 0 | 0 | 0 | <-- view中的长度 126 | * ↑ ↑ 127 | * | 5 | 9 | <-- 输入 128 | */ 129 | private void parseDigits() { 130 | String s = String.valueOf(mDigits); 131 | int[] newParsed = new int[mFigureCount]; 132 | int shorterOne = s.length() < newParsed.length ? s.length() : newParsed.length; 133 | 134 | int index = newParsed.length - 1; 135 | for (int i = s.length() - 1; i >= s.length() - shorterOne; i--) { 136 | newParsed[index] = Integer.parseInt(s.substring(i, i + 1)); 137 | index--; 138 | } 139 | mParsedDigits = newParsed; 140 | } 141 | 142 | private void resetChildren() { 143 | mParsedDigits = new int[mFigureCount]; 144 | 145 | removeAllViews(); 146 | for (int i = 0; i < mFigureCount; i++) { 147 | DigitalView v = new DigitalView(getContext()); 148 | LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 149 | ViewGroup.LayoutParams.WRAP_CONTENT); 150 | if (i > 0) 151 | params.leftMargin = dp2px(mTextInterval); 152 | v.setLayoutParams(params); 153 | v.setTextColor(mColor); 154 | v.setTextSize(mTextSize); 155 | addView(v); 156 | } 157 | } 158 | 159 | private DigitalView[] getChildren() { 160 | DigitalView[] result = new DigitalView[getChildCount()]; 161 | for (int i = 0; i < result.length; i++) { 162 | result[i] = (DigitalView) getChildAt(i); 163 | } 164 | return result; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /app/src/main/java/com/salmonzhg/digitview/views/DigitalView.java: -------------------------------------------------------------------------------- 1 | package com.salmonzhg.digitview.views; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ValueAnimator; 6 | import android.content.Context; 7 | import android.content.res.TypedArray; 8 | import android.graphics.Canvas; 9 | import android.graphics.Color; 10 | import android.graphics.Paint; 11 | import android.util.AttributeSet; 12 | import android.view.View; 13 | 14 | import com.salmonzhg.digitview.R; 15 | import com.salmonzhg.digitview.utils.DisplayUtils; 16 | 17 | 18 | /** 19 | * Created by Salmon on 2016/5/25 0025. 20 | */ 21 | public class DigitalView extends View { 22 | private static final int DEFAULT_COLOR = Color.BLACK; 23 | private static final int DEFAULT_TEXT_SIZE = 16; 24 | private int mColor = DEFAULT_COLOR; 25 | private int mTextSize = DEFAULT_TEXT_SIZE; 26 | private int mOffset = 0; 27 | private int mWid = 0; 28 | private int mHei = 0; 29 | private Paint mPaint; 30 | private int mRequiredWid = 0; 31 | private int mRequiredHei = 0; 32 | private float mBaseLine; 33 | private int mCurrentNum = 0; 34 | private boolean isPlaying = false; 35 | 36 | public DigitalView(Context context) { 37 | super(context); 38 | 39 | init(context, null); 40 | } 41 | 42 | public DigitalView(Context context, AttributeSet attrs) { 43 | super(context, attrs); 44 | 45 | init(context, attrs); 46 | } 47 | 48 | private void init(Context context, AttributeSet attrs) { 49 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 50 | 51 | if (attrs != null) { 52 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DigitalView); 53 | if (ta != null) { 54 | mColor = ta.getColor(R.styleable.DigitalView_digiColor, Color.BLACK); 55 | mTextSize = ta.getDimensionPixelSize(R.styleable.DigitalView_digiTextSize, DEFAULT_TEXT_SIZE); 56 | 57 | ta.recycle(); 58 | } 59 | } 60 | 61 | setTextSize(mTextSize); 62 | setTextColor(mColor); 63 | } 64 | 65 | @Override 66 | protected void onDraw(Canvas canvas) { 67 | for (int i = 0; i < 10; i++) { 68 | canvas.drawText(String.valueOf(i), (mWid - mRequiredWid) / 2, 69 | mHei * i + (mHei - mBaseLine) - mOffset, mPaint); 70 | } 71 | } 72 | 73 | private void calRequireSize() { 74 | Paint.FontMetrics fm = mPaint.getFontMetrics(); 75 | 76 | mRequiredWid = (int) mPaint.measureText("0"); 77 | mRequiredHei = (int) (fm.bottom - fm.top); 78 | 79 | // canvas.drawText()其中一个参数为baseLine,如果设置为试图的高度,则会截掉部分 80 | // 如小写字母p中的下吧吗部分,就属于baseLine以下的部分 81 | // 故需要计算baseLine的高度,公式参考如下 82 | // http://www.sjsjw.com/kf_mobile/article/9_31376_30207.asp 83 | mBaseLine = (fm.bottom - fm.top) / 2 - fm.descent; 84 | } 85 | 86 | @Override 87 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 88 | super.onSizeChanged(w, h, oldw, oldh); 89 | mWid = w; 90 | mHei = h; 91 | } 92 | 93 | @Override 94 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 95 | setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false)); 96 | } 97 | 98 | private int measure(int measureSpec, boolean isWidSpec) { 99 | int mode = MeasureSpec.getMode(measureSpec); 100 | int size = MeasureSpec.getSize(measureSpec); 101 | 102 | int requireSize = isWidSpec ? mRequiredWid : mRequiredHei; 103 | 104 | if (!(mode == MeasureSpec.EXACTLY && size > requireSize)) 105 | size = requireSize; 106 | 107 | return size; 108 | } 109 | 110 | public void startAnim(int num) { 111 | if (isPlaying) 112 | return; 113 | if (num < 0) { 114 | num = 0; 115 | } else if (num > 9) { 116 | num = 9; 117 | } 118 | mCurrentNum = num; 119 | ValueAnimator anim = ValueAnimator.ofInt(mOffset, num * mHei); 120 | anim.setDuration(1000); 121 | anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 122 | @Override 123 | public void onAnimationUpdate(ValueAnimator animation) { 124 | mOffset = (int) animation.getAnimatedValue(); 125 | invalidate(); 126 | } 127 | }); 128 | anim.addListener(new AnimatorListenerAdapter() { 129 | @Override 130 | public void onAnimationEnd(Animator animation) { 131 | isPlaying = false; 132 | } 133 | }); 134 | anim.start(); 135 | isPlaying = true; 136 | } 137 | 138 | public void setTextColor(int color) { 139 | mColor = color; 140 | mPaint.setColor(mColor); 141 | 142 | invalidate(); 143 | } 144 | 145 | public void setTextSize(int size) { 146 | if (size < 0) { 147 | return; 148 | } 149 | mTextSize = size; 150 | int pxSize = DisplayUtils.dip2px(getContext(), mTextSize); 151 | mPaint.setTextSize(pxSize); 152 | 153 | calRequireSize(); 154 | requestLayout(); 155 | mOffset = mCurrentNum * mHei; 156 | invalidate(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 24 | 25 | 33 | 34 |