├── .gitignore ├── README.md ├── build.gradle ├── design └── output.gif ├── gradlew ├── gradlew.bat ├── library ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── deange │ └── numberview │ ├── NumberView.java │ ├── NumberViewGroup.java │ ├── PaintProvider.java │ └── digits │ ├── Digit.java │ ├── Digits.java │ ├── Eight.java │ ├── Empty.java │ ├── Five.java │ ├── Four.java │ ├── Nine.java │ ├── One.java │ ├── Seven.java │ ├── Six.java │ ├── Three.java │ ├── Two.java │ └── Zero.java ├── sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── deange │ │ └── numberview │ │ └── sample │ │ ├── MainActivity.java │ │ ├── NumberActivity.java │ │ └── NumberGroupActivity.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── drawable-xxxhdpi │ └── ic_launcher.png │ ├── layout │ ├── activity_number.xml │ └── activity_number_group.xml │ ├── values-v11 │ └── styles.xml │ ├── values-v21 │ └── styles.xml │ └── values │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | .classpath 20 | .project 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Intellij project files 26 | *.iml 27 | *.ipr 28 | *.iws 29 | .idea/ 30 | .gradle/ 31 | gradle/ 32 | build/ 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NumberView 2 | 3 | A simple number tweener. 4 | 5 | ![gif](design/output.gif) 6 | 7 | --- 8 | ### What is it? 9 | NumberView is mean to be a take on [Timely][]'s beautiful and tasteful number tweening animations. Despite looking like it draws actual numbers in it, the numbers drawn are represented as Bèzier curve paths, meticulously calculated with control points and anchors. 10 | 11 | --- 12 | ### Usage 13 | Using [NumberView][] to show a single digit in your project is simple. You can drop it into your XML files as a regular custom view: 14 | 15 | Layout file: 16 | ```xml 17 | 20 | ``` 21 | 22 | And in your Java file: 23 | ```java 24 | final NumberView view = findViewById(...); 25 | view.advance(1); 26 | postDelayed(() -> view.advance(2), 1000); 27 | postDelayed(() -> view.advance(), 2000); // Displays "3" 28 | ``` 29 | 30 | However, typically you'll want to show more than one digit at a time. For that you can use [NumberViewGroup][], which automatically takes care of adding new digits as you need them. 31 | 32 | Layout file: 33 | ```xml 34 | 37 | 38 | ``` 39 | 40 | And in your Java file: 41 | ```java 42 | final NumberViewGroup view = findViewById(...); 43 | view.advance(1); 44 | postDelayed(() -> view.advance(20), 1000); 45 | postDelayed(() -> view.advance(), 2000); // Displays "21" 46 | ``` 47 | 48 | You can always view the sample application code for more usage demos. 49 | 50 | --- 51 | ### Dependencies 52 | No dependencies. Works all the way back to API level 14. 53 | 54 | --- 55 | ### Download 56 | 57 | ```groovy 58 | repositories { 59 | maven { 60 | url "https://jitpack.io" 61 | } 62 | } 63 | 64 | dependencies { 65 | compile 'com.github.cdeange:NumberView:1.1.0' 66 | } 67 | ``` 68 | 69 | --- 70 | ### Developed By 71 | - Christian De Angelis - 72 | 73 | --- 74 | ### License 75 | 76 | ``` 77 | Copyright 2017 Christian De Angelis 78 | 79 | Licensed under the Apache License, Version 2.0 (the "License"); 80 | you may not use this file except in compliance with the License. 81 | You may obtain a copy of the License at 82 | 83 | http://www.apache.org/licenses/LICENSE-2.0 84 | 85 | Unless required by applicable law or agreed to in writing, software 86 | distributed under the License is distributed on an "AS IS" BASIS, 87 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 88 | See the License for the specific language governing permissions and 89 | limitations under the License. 90 | ``` 91 | 92 | 93 | [Timely]: https://play.google.com/store/apps/details?id=ch.bitspin.timely 94 | [NumberView]: https://github.com/cdeange/NumberView/blob/master/library/src/main/java/com/deange/numberview/NumberView.java 95 | [NumberViewGroup]: https://github.com/cdeange/NumberView/blob/master/library/src/main/java/com/deange/numberview/NumberViewGroup.java 96 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | mavenCentral() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:2.3.0-alpha1' 8 | } 9 | } -------------------------------------------------------------------------------- /design/output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/NumberView/68f9cd9cd87a302501c30c70768e9344788c7cc6/design/output.gif -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 24 5 | buildToolsVersion '25.0.0' 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 24 10 | versionCode 1 11 | versionName '1.1.0' 12 | } 13 | 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/NumberView.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ObjectAnimator; 6 | import android.animation.ValueAnimator; 7 | import android.annotation.TargetApi; 8 | import android.content.Context; 9 | import android.graphics.Canvas; 10 | import android.graphics.Color; 11 | import android.graphics.Paint; 12 | import android.graphics.Path; 13 | import android.os.Build; 14 | import android.os.Parcel; 15 | import android.os.Parcelable; 16 | import android.util.AttributeSet; 17 | import android.util.Property; 18 | import android.util.TypedValue; 19 | import android.view.View; 20 | import android.view.ViewGroup; 21 | import android.view.animation.AccelerateDecelerateInterpolator; 22 | import android.view.animation.Interpolator; 23 | import android.view.animation.LinearInterpolator; 24 | 25 | import com.deange.numberview.digits.Digit; 26 | import com.deange.numberview.digits.Digits; 27 | 28 | public class NumberView extends View { 29 | 30 | private static final boolean DEBUG = true; 31 | 32 | public static final long DEFAULT_ANIMATION_DURATION = 500L; 33 | public static final float DEFAULT_WIDTH = 140f; 34 | public static final float DEFAULT_HEIGHT = 200f; 35 | public static final float ASPECT_RATIO = DEFAULT_WIDTH / DEFAULT_HEIGHT; 36 | 37 | // "8" is used since it constitutes the widest number drawn 38 | private static final String MEASURING_TEXT = "8"; 39 | 40 | private static final Property FACTOR = 41 | new Property(Float.class, "factor") { 42 | @Override 43 | public Float get(final NumberView object) { 44 | return object.mFactor; 45 | } 46 | 47 | @Override 48 | public void set(final NumberView object, final Float value) { 49 | object.mFactor = value; 50 | object.invalidate(); 51 | } 52 | }; 53 | 54 | private final NumberViewPaint mPaint = new NumberViewPaint(); 55 | private final Path mPath = new Path(); 56 | 57 | private Digit mNext = Digits.empty(); 58 | private Digit mCurrent = Digits.empty(); 59 | private boolean mFirstLayout = true; 60 | 61 | private float mWidth; 62 | private float mHeight; 63 | private float mScale; 64 | private float mFactor; 65 | private ValueAnimator mAnimator; 66 | 67 | public NumberView(final Context context) { 68 | super(context); 69 | init(); 70 | } 71 | 72 | public NumberView(final Context context, final AttributeSet attrs) { 73 | super(context, attrs); 74 | init(); 75 | } 76 | 77 | public NumberView(final Context context, final AttributeSet attrs, final int defStyleAttr) { 78 | super(context, attrs, defStyleAttr); 79 | init(); 80 | } 81 | 82 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 83 | public NumberView(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) { 84 | super(context, attrs, defStyleAttr, defStyleRes); 85 | init(); 86 | } 87 | 88 | private void init() { 89 | setWillNotDraw(false); 90 | 91 | // A new paint with the style as stroke 92 | mPaint.setAntiAlias(true); 93 | mPaint.setColor(Color.BLACK); 94 | mPaint.setStrokeWidth(2f); 95 | mPaint.setStyle(Paint.Style.STROKE); 96 | 97 | // Set up size values 98 | mScale = 1; 99 | mWidth = DEFAULT_WIDTH; 100 | mHeight = DEFAULT_HEIGHT; 101 | 102 | measureTextSize(mWidth); 103 | 104 | mAnimator = ObjectAnimator.ofFloat(this, FACTOR, 0f, 1f); 105 | mAnimator.setDuration(DEFAULT_ANIMATION_DURATION); 106 | mAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); 107 | mAnimator.addListener(new AnimatorListenerAdapter() { 108 | @Override 109 | public void onAnimationEnd(final Animator animation) { 110 | // End of the current number animation 111 | // Begin setting values for the next number in the sequence 112 | mCurrent = mNext; 113 | } 114 | }); 115 | mAnimator.start(); 116 | } 117 | 118 | private void measureTextSize(final float targetMaxWidth) { 119 | // Calculate the right scale for the text size 120 | int sp = 0; 121 | float px = 0; 122 | float validPx; 123 | do { 124 | sp++; 125 | validPx = px; 126 | px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics()); 127 | mPaint.setTextSizeInternal(px); 128 | } while (mPaint.measureText(MEASURING_TEXT) < targetMaxWidth); 129 | 130 | setTextSize(validPx); 131 | } 132 | 133 | public void setAnimationDuration(final long duration) { 134 | mAnimator.setDuration(duration); 135 | } 136 | 137 | public void setInterpolator(final Interpolator interpolator) { 138 | mAnimator.setInterpolator((interpolator == null) ? new LinearInterpolator() : interpolator); 139 | } 140 | 141 | public void setPaint(final Paint paint) { 142 | mPaint.set(paint); 143 | } 144 | 145 | public Paint getPaint() { 146 | return mPaint; 147 | } 148 | 149 | public void setTextSize(final int sizeUnit, final float textSize) { 150 | final float pixelSize = TypedValue.applyDimension(sizeUnit, textSize, getResources().getDisplayMetrics()); 151 | setTextSize(pixelSize); 152 | } 153 | 154 | public void setTextSize(final float textSize) { 155 | mPaint.setTextSize(textSize); 156 | } 157 | 158 | public float getTextSize() { 159 | return getPaint().getTextSize(); 160 | } 161 | 162 | public Digit getDigit() { 163 | return mNext; 164 | } 165 | 166 | public void hide() { 167 | show(Digits.empty()); 168 | } 169 | 170 | public void hideNow() { 171 | showNow(Digits.empty()); 172 | } 173 | 174 | public void show(final Digit digit) { 175 | if (digit == null) { 176 | throw new IllegalArgumentException("digit cannot be null"); 177 | } 178 | mNext = digit; 179 | mAnimator.start(); 180 | } 181 | 182 | public void showNow(final Digit digit) { 183 | if (digit == null) { 184 | throw new IllegalArgumentException("digit cannot be null"); 185 | } 186 | mNext = digit; 187 | mCurrent = digit; 188 | mAnimator.start(); 189 | } 190 | 191 | private void setScale(float scale) { 192 | if (scale == 0) { 193 | throw new IllegalArgumentException("Scale cannot be 0"); 194 | } 195 | 196 | scale = Math.abs(scale); 197 | 198 | if (mScale == scale) return; 199 | 200 | final float inverseFactor = (scale / mScale); 201 | mWidth *= inverseFactor; 202 | mHeight *= inverseFactor; 203 | 204 | mScale = scale; 205 | 206 | requestLayout(); 207 | invalidate(); 208 | } 209 | 210 | private boolean isAnimating() { 211 | return mAnimator.isRunning(); 212 | } 213 | 214 | private float lerp(float v0, float v1, float t) { 215 | return (1 - t) * v0 + t * v1; 216 | } 217 | 218 | private boolean fequals(final float f0, final float f1) { 219 | final float ulp0 = Math.ulp(f0); 220 | final float ulp1 = Math.ulp(f1); 221 | return Math.abs(f0 - f1) <= Math.max(ulp0, ulp1); 222 | } 223 | 224 | @Override 225 | protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 226 | final int minWidth = getSuggestedMinimumWidth(); 227 | final int minHeight = getSuggestedMinimumHeight(); 228 | int width, height; 229 | 230 | if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) { 231 | if (!isAnimating()) { 232 | mWidth = mScale * mCurrent.getWidth(); 233 | } 234 | width = (int) Math.max(minWidth, mWidth); 235 | } else { 236 | width = MeasureSpec.getSize(widthMeasureSpec); 237 | } 238 | 239 | if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) { 240 | height = (int) Math.max(minHeight, mHeight); 241 | } else { 242 | height = MeasureSpec.getSize(heightMeasureSpec); 243 | } 244 | 245 | if (height * ASPECT_RATIO < width) { 246 | height = (int) (width / ASPECT_RATIO); 247 | } 248 | 249 | setMeasuredDimension(width, height); 250 | } 251 | 252 | @Override 253 | protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) { 254 | super.onLayout(changed, l, t, r, b); 255 | 256 | // Handles the case of an absolute dimension specified in the layout params 257 | if (mFirstLayout) { 258 | mFirstLayout = false; 259 | final ViewGroup.LayoutParams params = getLayoutParams(); 260 | if (params != null && params.width > 0) { 261 | measureTextSize(params.width); 262 | } 263 | } 264 | } 265 | 266 | @Override 267 | public void onDraw(final Canvas canvas) { 268 | super.onDraw(canvas); 269 | 270 | final Digit thisNumber = mCurrent; 271 | final Digit nextNumber = mNext; 272 | 273 | final float[][] current = thisNumber.getPoints(); 274 | final float[][] next = nextNumber.getPoints(); 275 | final float[][] curr1 = thisNumber.getControlPoints1(); 276 | final float[][] next1 = nextNumber.getControlPoints1(); 277 | final float[][] curr2 = thisNumber.getControlPoints2(); 278 | final float[][] next2 = nextNumber.getControlPoints2(); 279 | 280 | // A factor of the difference between current and next frame based on interpolation 281 | // If we ourselves did not specifically request drawing, then draw our previous state 282 | final float factor = mFactor; 283 | 284 | final float thisWidth = mScale * thisNumber.getWidth(); 285 | final float nextWidth = mScale * nextNumber.getWidth(); 286 | final float interpolatedWidth = lerp(thisWidth, nextWidth, factor); 287 | if (!fequals(thisWidth, nextWidth) || !fequals(mWidth, interpolatedWidth)) { 288 | mWidth = Math.max(interpolatedWidth, 1f); 289 | requestLayout(); 290 | } 291 | 292 | final float translateX = ((float) getMeasuredWidth() - mWidth) / 2f; 293 | final float translateY = ((float) getMeasuredHeight() - mHeight) / 2f; 294 | 295 | // Reset the path 296 | mPath.reset(); 297 | 298 | // Draw the first point 299 | mPath.moveTo( 300 | mScale * (lerp(current[0][0], next[0][0], factor) + translateX), 301 | mScale * (lerp(current[0][1], next[0][1], factor) + translateY)); 302 | 303 | // Connect the rest of the points as a bezier curve 304 | for (int i = 0; i < 4; i++) { 305 | mPath.cubicTo( 306 | mScale * (lerp(curr1[i][0], next1[i][0], factor) + translateX), 307 | mScale * (lerp(curr1[i][1], next1[i][1], factor) + translateY), 308 | mScale * (lerp(curr2[i][0], next2[i][0], factor) + translateX), 309 | mScale * (lerp(curr2[i][1], next2[i][1], factor) + translateY), 310 | mScale * (lerp(current[i + 1][0], next[i + 1][0], factor) + translateX), 311 | mScale * (lerp(current[i + 1][1], next[i + 1][1], factor) + translateY)); 312 | } 313 | 314 | // Draw the path 315 | canvas.drawPath(mPath, mPaint); 316 | 317 | if (DEBUG) { 318 | canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint); 319 | } 320 | } 321 | 322 | @Override 323 | public Parcelable onSaveInstanceState() { 324 | final SavedState ss = new SavedState(super.onSaveInstanceState()); 325 | 326 | // If we are animating while saving state, skip to the end by saving mCurrent as mNext 327 | ss.next = mNext; 328 | ss.current = isAnimating() ? mNext : mCurrent; 329 | 330 | return ss; 331 | } 332 | 333 | @Override 334 | public void onRestoreInstanceState(Parcelable state) { 335 | if (!(state instanceof SavedState)) { 336 | super.onRestoreInstanceState(state); 337 | return; 338 | } 339 | 340 | final SavedState ss = (SavedState) state; 341 | super.onRestoreInstanceState(ss.getSuperState()); 342 | 343 | mNext = ss.next; 344 | mCurrent = ss.current; 345 | } 346 | 347 | private static class SavedState extends BaseSavedState { 348 | public Digit next; 349 | public Digit current; 350 | 351 | private SavedState(Parcelable superState) { 352 | super(superState); 353 | } 354 | 355 | private SavedState(Parcel in) { 356 | super(in); 357 | next = (Digit) in.readSerializable(); 358 | current = (Digit) in.readSerializable(); 359 | } 360 | 361 | @Override 362 | public void writeToParcel(Parcel out, int flags) { 363 | super.writeToParcel(out, flags); 364 | out.writeSerializable(next); 365 | out.writeSerializable(current); 366 | } 367 | 368 | public static final Parcelable.Creator CREATOR = 369 | new Parcelable.Creator() { 370 | public SavedState createFromParcel(Parcel in) { 371 | return new SavedState(in); 372 | } 373 | 374 | public SavedState[] newArray(int size) { 375 | return new SavedState[size]; 376 | } 377 | }; 378 | } 379 | 380 | private class NumberViewPaint extends Paint { 381 | @Override 382 | public void setTextSize(final float textSize) { 383 | super.setTextSize(textSize); 384 | setScale(measureText(MEASURING_TEXT) / DEFAULT_WIDTH); 385 | } 386 | 387 | @Override 388 | public void set(final Paint src) { 389 | super.set(src); 390 | setScale(measureText(MEASURING_TEXT) / DEFAULT_WIDTH); 391 | } 392 | 393 | protected void setTextSizeInternal(final float textSize) { 394 | super.setTextSize(textSize); 395 | } 396 | } 397 | 398 | } 399 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/NumberViewGroup.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.util.AttributeSet; 7 | import android.view.Gravity; 8 | import android.widget.LinearLayout; 9 | 10 | import com.deange.numberview.digits.Digit; 11 | import com.deange.numberview.digits.Digits; 12 | 13 | public class NumberViewGroup extends LinearLayout { 14 | 15 | private boolean mPerformNow; 16 | private int mMinShown; 17 | private int mNumber; 18 | private boolean mHide; 19 | 20 | private PaintProvider mPaintProvider; 21 | 22 | public NumberViewGroup(final Context context) { 23 | super(context); 24 | init(); 25 | } 26 | 27 | public NumberViewGroup(final Context context, final AttributeSet attrs) { 28 | super(context, attrs); 29 | init(); 30 | } 31 | 32 | public NumberViewGroup(final Context context, final AttributeSet attrs, final int defStyleAttr) { 33 | super(context, attrs, defStyleAttr); 34 | init(); 35 | } 36 | 37 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 38 | public NumberViewGroup( 39 | final Context context, 40 | final AttributeSet attrs, 41 | final int defStyleAttr, 42 | final int defStyleRes) { 43 | super(context, attrs, defStyleAttr, defStyleRes); 44 | init(); 45 | } 46 | 47 | private void init() { 48 | setOrientation(HORIZONTAL); 49 | setGravity(Gravity.CENTER); 50 | } 51 | 52 | protected NumberView addNewChild() { 53 | final NumberView child = new NumberView(getContext()); 54 | if (mPaintProvider != null) { 55 | mPaintProvider.mutate(child.getPaint(), getChildCount()); 56 | } 57 | 58 | child.hideNow(); 59 | addView(child, 0); 60 | 61 | return child; 62 | } 63 | 64 | private Digit resolveDigit(int number, int digit) { 65 | return (mHide) ? Digits.empty() : Digits.forInt((int) ((number / Math.pow(10, digit)) % 10)); 66 | } 67 | 68 | private int getIntLength(final int number) { 69 | // Yeah it's ugly as sin but it works, and it works fast. 70 | final int n = (number == Integer.MIN_VALUE) ? Integer.MAX_VALUE : Math.abs(number); 71 | return n < 100000 72 | ? n < 100 ? n < 10 ? 1 : 2 : n < 1000 ? 3 : n < 10000 ? 4 : 5 73 | : n < 10000000 ? n < 1000000 ? 6 : 7 : n < 100000000 ? 8 : n < 1000000000 ? 9 : 10; 74 | } 75 | 76 | private int getRequiredChildCount() { 77 | return Math.max(mMinShown, mHide ? 0 : getIntLength(mNumber)); 78 | } 79 | 80 | private void bindViews() { 81 | 82 | final int size = getRequiredChildCount(); 83 | 84 | for (int i = 0; i < size; i++) { 85 | 86 | while (i >= getChildCount()) { 87 | addNewChild(); 88 | } 89 | 90 | final NumberView child = getDigitAt(i); 91 | final Digit d = resolveDigit(mNumber, i); 92 | 93 | if (mPerformNow) { 94 | child.showNow(d); 95 | } else { 96 | child.show(d); 97 | } 98 | } 99 | 100 | for (int i = size; i < getChildCount(); i++) { 101 | // Unused children :'( 102 | final NumberView child = getDigitAt(i); 103 | if (mPerformNow) { 104 | child.hideNow(); 105 | } else { 106 | child.hide(); 107 | } 108 | } 109 | 110 | requestLayout(); 111 | invalidate(); 112 | } 113 | 114 | public NumberView getDigitAt(final int index) { 115 | // Reverse the indexing order of the children 116 | return (NumberView) getChildAt(getChildCount() - index - 1); 117 | } 118 | 119 | public NumberView[] getDigits() { 120 | // Returns views in order from LSB to MSB 121 | final NumberView[] views = new NumberView[getChildCount()]; 122 | for (int i = 0; i < getChildCount(); i++) { 123 | views[i] = getDigitAt(i); 124 | } 125 | return views; 126 | } 127 | 128 | public void show(final int number) { 129 | mHide = false; 130 | mNumber = number; 131 | mPerformNow = false; 132 | bindViews(); 133 | } 134 | 135 | public void showNow(final int number) { 136 | mHide = false; 137 | mNumber = number; 138 | mPerformNow = true; 139 | bindViews(); 140 | } 141 | 142 | public void hide() { 143 | mHide = true; 144 | mPerformNow = false; 145 | bindViews(); 146 | } 147 | 148 | public void hideNow() { 149 | mHide = true; 150 | mPerformNow = true; 151 | bindViews(); 152 | } 153 | 154 | public void setMinimumNumbersShown(final int minimum) { 155 | mMinShown = minimum; 156 | 157 | while (getChildCount() < mMinShown) { 158 | addNewChild(); 159 | } 160 | } 161 | 162 | public void setPaintProvider(final PaintProvider paintProvider) { 163 | mPaintProvider = paintProvider; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/PaintProvider.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview; 2 | 3 | import android.graphics.Paint; 4 | 5 | public interface PaintProvider { 6 | void mutate(final Paint paint, final int digit); 7 | } 8 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/Digit.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import java.io.Serializable; 4 | 5 | public interface Digit extends Serializable { 6 | 7 | float[][] getPoints(); 8 | 9 | float[][] getControlPoints1(); 10 | 11 | float[][] getControlPoints2(); 12 | 13 | float getWidth(); 14 | 15 | char getChar(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/Digits.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import android.util.Log; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public final class Digits { 9 | 10 | private static final String TAG = "Digits"; 11 | 12 | private static final Map DIGITS = new HashMap<>(); 13 | 14 | static { 15 | register(new Zero()); 16 | register(new One()); 17 | register(new Two()); 18 | register(new Three()); 19 | register(new Four()); 20 | register(new Five()); 21 | register(new Six()); 22 | register(new Seven()); 23 | register(new Eight()); 24 | register(new Nine()); 25 | register(new Empty()); 26 | } 27 | 28 | private Digits() { 29 | throw new AssertionError(); 30 | } 31 | 32 | public static void register(final Digit digit) { 33 | final char character = digit.getChar(); 34 | final Digit oldValue = DIGITS.put(character, digit); 35 | if (oldValue != null) { 36 | Log.w(TAG, "Replacing existing digit " + oldValue + " for character '" + character + "'"); 37 | } 38 | } 39 | 40 | public static Digit empty() { 41 | return forChar('\0'); 42 | } 43 | 44 | public static Digit forChar(final char character) { 45 | return DIGITS.get(character); 46 | } 47 | 48 | public static Digit forInt(final int digit) { 49 | if (digit < 0 || digit > 9) { 50 | throw new IllegalArgumentException("Digit must be between 0 and 9"); 51 | } 52 | return forChar((char) ('0' + digit)); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/Eight.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import static com.deange.numberview.NumberView.DEFAULT_WIDTH; 4 | 5 | /* package */ class Eight implements Digit { 6 | 7 | private final float[][] POINTS = { { 71, 96 }, { 71, 19 }, { 71, 96 }, { 71, 179 }, { 71, 96 } }; 8 | private final float[][] CONTROLS1 = { { 14, 95 }, { 124, 19 }, { 14, 96 }, { 124, 179 } }; 9 | private final float[][] CONTROLS2 = { { 14, 19 }, { 124, 96 }, { 6, 179 }, { 124, 96 } }; 10 | 11 | @Override 12 | public float[][] getPoints() { 13 | return POINTS; 14 | } 15 | 16 | @Override 17 | public float[][] getControlPoints1() { 18 | return CONTROLS1; 19 | } 20 | 21 | @Override 22 | public float[][] getControlPoints2() { 23 | return CONTROLS2; 24 | } 25 | 26 | @Override 27 | public float getWidth() { 28 | return DEFAULT_WIDTH; 29 | } 30 | 31 | @Override 32 | public char getChar() { 33 | return '8'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/Empty.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import com.deange.numberview.NumberView; 4 | 5 | /* package */ class Empty implements Digit { 6 | 7 | private static final float[] F = { NumberView.DEFAULT_WIDTH / 8f, NumberView.DEFAULT_HEIGHT / 2f }; 8 | private static final float[][] POINTS = new float[][]{ F.clone(), F.clone(), F.clone(), F.clone(), F.clone() }; 9 | private static final float[][] CONTROLS1 = new float[][]{ F.clone(), F.clone(), F.clone(), F.clone() }; 10 | private static final float[][] CONTROLS2 = new float[][]{ F.clone(), F.clone(), F.clone(), F.clone() }; 11 | 12 | @Override 13 | public float[][] getPoints() { 14 | return POINTS; 15 | } 16 | 17 | @Override 18 | public float[][] getControlPoints1() { 19 | return CONTROLS1; 20 | } 21 | 22 | @Override 23 | public float[][] getControlPoints2() { 24 | return CONTROLS2; 25 | } 26 | 27 | @Override 28 | public float getWidth() { 29 | return 1f; 30 | } 31 | 32 | @Override 33 | public char getChar() { 34 | return '\0'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/Five.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import static com.deange.numberview.NumberView.DEFAULT_WIDTH; 4 | 5 | /* package */ class Five implements Digit { 6 | 7 | private final float[][] POINTS = { { 116, 20 }, { 61, 20 }, { 42, 78 }, { 115, 129 }, { 15, 154 } }; 8 | private final float[][] CONTROLS1 = { { 61, 20 }, { 42, 78 }, { 67, 66 }, { 110, 183 } }; 9 | private final float[][] CONTROLS2 = { { 61, 20 }, { 42, 78 }, { 115, 85 }, { 38, 198 } }; 10 | 11 | @Override 12 | public float[][] getPoints() { 13 | return POINTS; 14 | } 15 | 16 | @Override 17 | public float[][] getControlPoints1() { 18 | return CONTROLS1; 19 | } 20 | 21 | @Override 22 | public float[][] getControlPoints2() { 23 | return CONTROLS2; 24 | } 25 | 26 | @Override 27 | public float getWidth() { 28 | return DEFAULT_WIDTH; 29 | } 30 | 31 | @Override 32 | public char getChar() { 33 | return '5'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/Four.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import static com.deange.numberview.NumberView.DEFAULT_WIDTH; 4 | 5 | /* package */ class Four implements Digit { 6 | 7 | private final float[][] POINTS = { { 125, 146 }, { 13, 146 }, { 99, 25 }, { 99, 146 }, { 99, 179 } }; 8 | private final float[][] CONTROLS1 = { { 125, 146 }, { 13, 146 }, { 99, 25 }, { 99, 146 } }; 9 | private final float[][] CONTROLS2 = { { 13, 146 }, { 99, 25 }, { 99, 146 }, { 99, 179 } }; 10 | 11 | @Override 12 | public float[][] getPoints() { 13 | return POINTS; 14 | } 15 | 16 | @Override 17 | public float[][] getControlPoints1() { 18 | return CONTROLS1; 19 | } 20 | 21 | @Override 22 | public float[][] getControlPoints2() { 23 | return CONTROLS2; 24 | } 25 | 26 | @Override 27 | public float getWidth() { 28 | return DEFAULT_WIDTH; 29 | } 30 | 31 | @Override 32 | public char getChar() { 33 | return '4'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/Nine.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import static com.deange.numberview.NumberView.DEFAULT_WIDTH; 4 | 5 | /* package */ class Nine implements Digit { 6 | 7 | private final float[][] POINTS = { { 117, 100 }, { 17, 74 }, { 124, 74 }, { 60, 180 }, { 60, 180 } }; 8 | private final float[][] CONTROLS1 = { { 94, 136 }, { 12, 8 }, { 122, 108 }, { 60, 180 } }; 9 | private final float[][] CONTROLS2 = { { 24, 134 }, { 118, -8 }, { 99, 121 }, { 60, 180 } }; 10 | 11 | @Override 12 | public float[][] getPoints() { 13 | return POINTS; 14 | } 15 | 16 | @Override 17 | public float[][] getControlPoints1() { 18 | return CONTROLS1; 19 | } 20 | 21 | @Override 22 | public float[][] getControlPoints2() { 23 | return CONTROLS2; 24 | } 25 | 26 | @Override 27 | public float getWidth() { 28 | return DEFAULT_WIDTH; 29 | } 30 | 31 | @Override 32 | public char getChar() { 33 | return '9'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/One.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import static com.deange.numberview.NumberView.DEFAULT_WIDTH; 4 | 5 | /* package */ class One implements Digit { 6 | 7 | private final float[][] POINTS = { { 15, 20.5f }, { 42.5f, 20.5f }, { 42.5f, 181 }, { 42.5f, 181 }, { 42.5f, 181 } }; 8 | private final float[][] CONTROLS1 = { { 15, 20.5f }, { 42.5f, 20.5f }, { 42.5f, 181 }, { 42.5f, 181 } }; 9 | private final float[][] CONTROLS2 = { { 15, 20.5f }, { 42.5f, 20.5f }, { 42.5f, 181 }, { 42.5f, 181 } }; 10 | 11 | @Override 12 | public float[][] getPoints() { 13 | return POINTS; 14 | } 15 | 16 | @Override 17 | public float[][] getControlPoints1() { 18 | return CONTROLS1; 19 | } 20 | 21 | @Override 22 | public float[][] getControlPoints2() { 23 | return CONTROLS2; 24 | } 25 | 26 | @Override 27 | public float getWidth() { 28 | return DEFAULT_WIDTH / 2f; 29 | } 30 | 31 | @Override 32 | public char getChar() { 33 | return '1'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/Seven.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import static com.deange.numberview.NumberView.DEFAULT_WIDTH; 4 | 5 | /* package */ class Seven implements Digit { 6 | 7 | private final float[][] POINTS = { { 17, 21 }, { 128, 21 }, { 90.67f, 73.34f }, { 53.34f, 126.67f }, { 16, 181 } }; 8 | private final float[][] CONTROLS1 = { { 17, 21 }, { 128, 21 }, { 90.67f, 73.34f }, { 53.34f, 126.67f } }; 9 | private final float[][] CONTROLS2 = { { 128, 21 }, { 90.67f, 73.34f }, { 53.34f, 126.67f }, { 16, 181 } }; 10 | 11 | @Override 12 | public float[][] getPoints() { 13 | return POINTS; 14 | } 15 | 16 | @Override 17 | public float[][] getControlPoints1() { 18 | return CONTROLS1; 19 | } 20 | 21 | @Override 22 | public float[][] getControlPoints2() { 23 | return CONTROLS2; 24 | } 25 | 26 | @Override 27 | public float getWidth() { 28 | return DEFAULT_WIDTH; 29 | } 30 | 31 | @Override 32 | public char getChar() { 33 | return '7'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/Six.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import static com.deange.numberview.NumberView.DEFAULT_WIDTH; 4 | 5 | /* package */ class Six implements Digit { 6 | 7 | private final float[][] POINTS = { { 80, 20 }, { 80, 20 }, { 16, 126 }, { 123, 126 }, { 23, 100 } }; 8 | private final float[][] CONTROLS1 = { { 80, 20 }, { 41, 79 }, { 22, 208 }, { 116, 66 } }; 9 | private final float[][] CONTROLS2 = { { 80, 20 }, { 18, 92 }, { 128, 192 }, { 46, 64 } }; 10 | 11 | @Override 12 | public float[][] getPoints() { 13 | return POINTS; 14 | } 15 | 16 | @Override 17 | public float[][] getControlPoints1() { 18 | return CONTROLS1; 19 | } 20 | 21 | @Override 22 | public float[][] getControlPoints2() { 23 | return CONTROLS2; 24 | } 25 | 26 | @Override 27 | public float getWidth() { 28 | return DEFAULT_WIDTH; 29 | } 30 | 31 | @Override 32 | public char getChar() { 33 | return '6'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/Three.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import static com.deange.numberview.NumberView.DEFAULT_WIDTH; 4 | 5 | /* package */ class Three implements Digit { 6 | 7 | private final float[][] POINTS = { { 33.25f, 54 }, { 69.5f, 18 }, { 69.5f, 96 }, { 70, 180 }, { 26.5f, 143 } }; 8 | private final float[][] CONTROLS1 = { { 33, 27 }, { 126, 18 }, { 128, 96 }, { 24, 180 } }; 9 | private final float[][] CONTROLS2 = { { 56, 18 }, { 116, 96 }, { 120, 180 }, { 26, 150 } }; 10 | 11 | @Override 12 | public float[][] getPoints() { 13 | return POINTS; 14 | } 15 | 16 | @Override 17 | public float[][] getControlPoints1() { 18 | return CONTROLS1; 19 | } 20 | 21 | @Override 22 | public float[][] getControlPoints2() { 23 | return CONTROLS2; 24 | } 25 | 26 | @Override 27 | public float getWidth() { 28 | return DEFAULT_WIDTH; 29 | } 30 | 31 | @Override 32 | public char getChar() { 33 | return '3'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/Two.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import static com.deange.numberview.NumberView.DEFAULT_WIDTH; 4 | 5 | /* package */ class Two implements Digit { 6 | 7 | private final float[][] POINTS = { { 26, 60 }, { 114.5f, 61 }, { 78, 122 }, { 27, 177 }, { 117, 177 } }; 8 | private final float[][] CONTROLS1 = { { 29, 2 }, { 114.5f, 78 }, { 64, 138 }, { 27, 177 } }; 9 | private final float[][] CONTROLS2 = { { 113, 4 }, { 100, 98 }, { 44, 155 }, { 117, 177 } }; 10 | 11 | @Override 12 | public float[][] getPoints() { 13 | return POINTS; 14 | } 15 | 16 | @Override 17 | public float[][] getControlPoints1() { 18 | return CONTROLS1; 19 | } 20 | 21 | @Override 22 | public float[][] getControlPoints2() { 23 | return CONTROLS2; 24 | } 25 | 26 | @Override 27 | public float getWidth() { 28 | return DEFAULT_WIDTH; 29 | } 30 | 31 | @Override 32 | public char getChar() { 33 | return '2'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /library/src/main/java/com/deange/numberview/digits/Zero.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.digits; 2 | 3 | import static com.deange.numberview.NumberView.DEFAULT_WIDTH; 4 | 5 | /* package */ class Zero implements Digit { 6 | 7 | private final float[][] POINTS = { { 14.5f, 100 }, { 70, 18 }, { 126, 100 }, { 70, 180 }, { 14.5f, 100 } }; 8 | private final float[][] CONTROLS1 = { { 14.5f, 60 }, { 103, 18 }, { 126, 140 }, { 37, 180 } }; 9 | private final float[][] CONTROLS2 = { { 37, 18 }, { 126, 60 }, { 103, 180 }, { 14.5f, 140 } }; 10 | 11 | @Override 12 | public float[][] getPoints() { 13 | return POINTS; 14 | } 15 | 16 | @Override 17 | public float[][] getControlPoints1() { 18 | return CONTROLS1; 19 | } 20 | 21 | @Override 22 | public float[][] getControlPoints2() { 23 | return CONTROLS2; 24 | } 25 | 26 | @Override 27 | public float getWidth() { 28 | return DEFAULT_WIDTH; 29 | } 30 | 31 | @Override 32 | public char getChar() { 33 | return '0'; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | repositories { 4 | mavenCentral() 5 | } 6 | 7 | android { 8 | compileSdkVersion 24 9 | buildToolsVersion '25.0.0' 10 | 11 | defaultConfig { 12 | applicationId 'com.deange.numberview.sample' 13 | minSdkVersion 14 14 | targetSdkVersion 24 15 | versionCode 1 16 | versionName '1.1.0' 17 | } 18 | } 19 | 20 | dependencies { 21 | compile project(':library') 22 | } 23 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sample/src/main/java/com/deange/numberview/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.sample; 2 | 3 | import android.app.ListActivity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.widget.ArrayAdapter; 8 | import android.widget.ListView; 9 | 10 | public class MainActivity extends ListActivity { 11 | 12 | private static final String[] TYPES = { 13 | "NumberView", 14 | "NumberViewGroup", 15 | }; 16 | 17 | private static final Class[] ACTIVITIES = new Class[]{ 18 | NumberActivity.class, 19 | NumberGroupActivity.class, 20 | }; 21 | 22 | @Override 23 | protected void onCreate(final Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | setListAdapter(new TypesAdapter()); 26 | } 27 | 28 | @Override 29 | protected void onListItemClick(final ListView l, final View v, final int position, final long id) { 30 | startActivity(new Intent(this, ACTIVITIES[position])); 31 | } 32 | 33 | private class TypesAdapter extends ArrayAdapter { 34 | public TypesAdapter() { 35 | super(MainActivity.this, android.R.layout.simple_list_item_1, TYPES); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sample/src/main/java/com/deange/numberview/sample/NumberActivity.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.sample; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.os.Handler; 6 | import android.os.Looper; 7 | import android.view.View; 8 | import android.widget.Button; 9 | 10 | import com.deange.numberview.NumberView; 11 | import com.deange.numberview.digits.Digits; 12 | 13 | import java.util.Timer; 14 | import java.util.TimerTask; 15 | 16 | public class NumberActivity extends Activity implements View.OnClickListener { 17 | 18 | private static final String KEY_TIME = "time"; 19 | 20 | private Timer mTimer = new Timer(); 21 | private Handler mMainHandler = new Handler(Looper.getMainLooper()); 22 | 23 | private NumberView mMinuteTensView; 24 | private NumberView mMinuteOnesView; 25 | private NumberView mSecondTensView; 26 | private NumberView mSecondOnesView; 27 | 28 | private Button mResetButton; 29 | private Button mStartStopButton; 30 | 31 | private int mTime = 0; 32 | 33 | private boolean mStarted = false; 34 | 35 | @Override 36 | protected void onCreate(final Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | setContentView(R.layout.activity_number); 39 | 40 | mResetButton = (Button) findViewById(R.id.button_reset); 41 | mStartStopButton = (Button) findViewById(R.id.button_start_stop); 42 | 43 | mResetButton.setOnClickListener(this); 44 | mStartStopButton.setOnClickListener(this); 45 | 46 | mSecondTensView = (NumberView) findViewById(R.id.number_second_tens_position); 47 | mSecondOnesView = (NumberView) findViewById(R.id.number_second_ones_position); 48 | mMinuteTensView = (NumberView) findViewById(R.id.number_minute_tens_position); 49 | mMinuteOnesView = (NumberView) findViewById(R.id.number_minute_ones_position); 50 | 51 | mMinuteTensView.getPaint().setStrokeWidth(5f); 52 | mMinuteOnesView.getPaint().setStrokeWidth(5f); 53 | 54 | mTime = savedInstanceState == null ? 0 : savedInstanceState.getInt(KEY_TIME); 55 | } 56 | 57 | @Override 58 | protected void onResume() { 59 | handleStartStop(); 60 | super.onResume(); 61 | } 62 | 63 | @Override 64 | protected void onPause() { 65 | handleStartStop(); 66 | super.onPause(); 67 | } 68 | 69 | @Override 70 | protected void onSaveInstanceState(final Bundle outState) { 71 | outState.putInt(KEY_TIME, mTime); 72 | super.onSaveInstanceState(outState); 73 | } 74 | 75 | private void updateUi() { 76 | mSecondOnesView.show(Digits.forInt(mTime % 10)); 77 | mSecondTensView.show(Digits.forInt((mTime / 10) % 6)); 78 | mMinuteOnesView.show(Digits.forInt((mTime / 60) % 10)); 79 | mMinuteTensView.show(Digits.forInt((mTime / 600) % 6)); 80 | 81 | mTime++; 82 | } 83 | 84 | private void startTimer() { 85 | mTimer.scheduleAtFixedRate(new UpdateTask(), 0, 1000); 86 | } 87 | 88 | private void handleStartStop() { 89 | if (mStarted) { 90 | mTimer.cancel(); 91 | mStartStopButton.setText(R.string.button_start); 92 | 93 | } else { 94 | mTimer = new Timer(); 95 | startTimer(); 96 | mStartStopButton.setText(R.string.button_stop); 97 | } 98 | 99 | mStarted = !mStarted; 100 | } 101 | 102 | private void handleReset() { 103 | mTime = 0; 104 | mSecondOnesView.hideNow(); 105 | mSecondTensView.hideNow(); 106 | mMinuteOnesView.hideNow(); 107 | mMinuteTensView.hideNow(); 108 | } 109 | 110 | @Override 111 | public void onClick(View v) { 112 | switch (v.getId()) { 113 | 114 | case R.id.button_reset: 115 | handleReset(); 116 | break; 117 | 118 | case R.id.button_start_stop: 119 | handleStartStop(); 120 | break; 121 | 122 | } 123 | } 124 | 125 | private class UpdateTask extends TimerTask { 126 | @Override 127 | public void run() { 128 | mMainHandler.post(new Runnable() { 129 | @Override 130 | public void run() { 131 | updateUi(); 132 | } 133 | }); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /sample/src/main/java/com/deange/numberview/sample/NumberGroupActivity.java: -------------------------------------------------------------------------------- 1 | package com.deange.numberview.sample; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.os.Handler; 6 | import android.os.Looper; 7 | import android.view.View; 8 | import android.widget.Button; 9 | 10 | import com.deange.numberview.NumberViewGroup; 11 | import com.deange.numberview.digits.Digits; 12 | 13 | import java.util.Timer; 14 | import java.util.TimerTask; 15 | 16 | public class NumberGroupActivity extends Activity implements View.OnClickListener { 17 | 18 | private static final String KEY_TIME = "time"; 19 | 20 | private Timer mTimer = new Timer(); 21 | private Handler mMainHandler = new Handler(Looper.getMainLooper()); 22 | 23 | private NumberViewGroup mNumberViewGroup; 24 | 25 | private Button mResetButton; 26 | private Button mStartStopButton; 27 | 28 | private boolean mStarted = false; 29 | private int mTime; 30 | 31 | @Override 32 | protected void onCreate(final Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_number_group); 35 | 36 | mResetButton = (Button) findViewById(R.id.button_reset); 37 | mStartStopButton = (Button) findViewById(R.id.button_start_stop); 38 | 39 | mResetButton.setOnClickListener(this); 40 | mStartStopButton.setOnClickListener(this); 41 | 42 | mNumberViewGroup = (NumberViewGroup) findViewById(R.id.number_group); 43 | mNumberViewGroup.hideNow(); 44 | 45 | mTime = savedInstanceState == null ? 0 : savedInstanceState.getInt(KEY_TIME); 46 | } 47 | 48 | @Override 49 | protected void onResume() { 50 | handleStartStop(); 51 | super.onResume(); 52 | } 53 | 54 | @Override 55 | protected void onPause() { 56 | handleStartStop(); 57 | super.onPause(); 58 | } 59 | 60 | @Override 61 | protected void onSaveInstanceState(final Bundle outState) { 62 | outState.putInt(KEY_TIME, mTime); 63 | super.onSaveInstanceState(outState); 64 | } 65 | 66 | private void updateUi() { 67 | mNumberViewGroup.show(mTime); 68 | mTime++; 69 | } 70 | 71 | private void startTimer() { 72 | mTimer.scheduleAtFixedRate(new UpdateTask(), 0, 1000); 73 | } 74 | 75 | private void handleStartStop() { 76 | if (mStarted) { 77 | mTimer.cancel(); 78 | mStartStopButton.setText(R.string.button_start); 79 | 80 | } else { 81 | mTimer = new Timer(); 82 | startTimer(); 83 | mStartStopButton.setText(R.string.button_stop); 84 | } 85 | 86 | mStarted = !mStarted; 87 | } 88 | 89 | private void handleReset() { 90 | mNumberViewGroup.hideNow(); 91 | mTime = 0; 92 | } 93 | 94 | @Override 95 | public void onClick(View v) { 96 | switch (v.getId()) { 97 | 98 | case R.id.button_reset: 99 | handleReset(); 100 | break; 101 | 102 | case R.id.button_start_stop: 103 | handleStartStop(); 104 | break; 105 | 106 | } 107 | } 108 | 109 | private class UpdateTask extends TimerTask { 110 | @Override 111 | public void run() { 112 | mMainHandler.post(new Runnable() { 113 | @Override 114 | public void run() { 115 | updateUi(); 116 | } 117 | }); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/NumberView/68f9cd9cd87a302501c30c70768e9344788c7cc6/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/NumberView/68f9cd9cd87a302501c30c70768e9344788c7cc6/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/NumberView/68f9cd9cd87a302501c30c70768e9344788c7cc6/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/NumberView/68f9cd9cd87a302501c30c70768e9344788c7cc6/sample/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/NumberView/68f9cd9cd87a302501c30c70768e9344788c7cc6/sample/src/main/res/drawable-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_number.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 18 | 19 | 23 | 24 | 28 | 29 | 33 | 34 | 35 | 36 | 43 | 44 |