├── .github └── workflows │ └── android.yml ├── .gitignore ├── CircleProgressView ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── at │ │ └── grabner │ │ └── circleprogress │ │ ├── AnimationHandler.java │ │ ├── AnimationMsg.java │ │ ├── AnimationState.java │ │ ├── AnimationStateChangedListener.java │ │ ├── BarStartEndLine.java │ │ ├── CircleProgressView.java │ │ ├── ColorUtils.java │ │ ├── Direction.java │ │ ├── StrokeCap.java │ │ ├── TextMode.java │ │ └── UnitPosition.java │ └── res │ └── values │ └── attrs.xml ├── ExampleApp ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── fonts │ │ └── ANDROID_ROBOT.ttf │ ├── ic_launcher-web.png │ ├── java │ └── at │ │ └── grabner │ │ └── example │ │ └── circleprogressview │ │ └── MainActivity.java │ └── res │ ├── drawable │ └── mask.png │ ├── 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-v21 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── media ├── Capture.PNG ├── CircleParts.PNG ├── CircleProgressView.png ├── CircleProgressViewBlock.png ├── ColorGradient.jpg ├── big.png ├── demo.gif ├── demo.mp4 ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png └── screenshot4.png └── settings.gradle /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: set up JDK 11 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '11' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | #idea 29 | .idea/ 30 | *.iml 31 | -------------------------------------------------------------------------------- /CircleProgressView/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /CircleProgressView/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | apply plugin: 'maven-publish' 4 | 5 | group = 'com.github.jakob-grabner' 6 | android { 7 | compileSdkVersion 33 8 | defaultConfig { 9 | minSdkVersion 19 10 | targetSdkVersion 33 11 | versionCode 6 12 | versionName "1.5" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_1_6 23 | targetCompatibility JavaVersion.VERSION_1_6 24 | } 25 | productFlavors { 26 | } 27 | } 28 | 29 | dependencies { 30 | api 'androidx.annotation:annotation:1.1.0' 31 | } 32 | 33 | // build a jar with source files 34 | task sourcesJar(type: Jar) { 35 | from android.sourceSets.main.java.srcDirs 36 | classifier = 'sources' 37 | } 38 | 39 | task javadoc(type: Javadoc) { 40 | failOnError false 41 | source = android.sourceSets.main.java.sourceFiles 42 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 43 | classpath += configurations.compile 44 | } 45 | 46 | // build a jar with javadoc 47 | task javadocJar(type: Jar, dependsOn: javadoc) { 48 | classifier = 'javadoc' 49 | from javadoc.destinationDir 50 | } 51 | 52 | artifacts { 53 | archives sourcesJar 54 | archives javadocJar 55 | } -------------------------------------------------------------------------------- /CircleProgressView/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:/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 | -dontobfuscate 19 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable 20 | -assumenosideeffects class android.util.Log { 21 | public static *** d(...); 22 | public static *** v(...); 23 | } 24 | 25 | -keep public class at.grabner.circleprogress.* 26 | 27 | -keepparameternames 28 | -renamesourcefileattribute SourceFile 29 | -keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod 30 | 31 | -keep public class * { 32 | public protected *; 33 | } 34 | 35 | -keepclassmembernames class * { 36 | java.lang.Class class$(java.lang.String); 37 | java.lang.Class class$(java.lang.String, boolean); 38 | } 39 | 40 | -keepclasseswithmembernames,includedescriptorclasses class * { 41 | native ; 42 | } 43 | 44 | -keepclassmembers,allowoptimization enum * { 45 | public static **[] values(); 46 | public static ** valueOf(java.lang.String); 47 | } 48 | 49 | -keepclassmembers class * implements java.io.Serializable { 50 | static final long serialVersionUID; 51 | private static final java.io.ObjectStreamField[] serialPersistentFields; 52 | private void writeObject(java.io.ObjectOutputStream); 53 | private void readObject(java.io.ObjectInputStream); 54 | java.lang.Object writeReplace(); 55 | java.lang.Object readResolve(); 56 | } -------------------------------------------------------------------------------- /CircleProgressView/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /CircleProgressView/src/main/java/at/grabner/circleprogress/AnimationHandler.java: -------------------------------------------------------------------------------- 1 | package at.grabner.circleprogress; 2 | 3 | import android.animation.TimeInterpolator; 4 | import android.os.Handler; 5 | import android.os.Message; 6 | import android.os.SystemClock; 7 | import android.view.animation.AccelerateDecelerateInterpolator; 8 | import android.view.animation.DecelerateInterpolator; 9 | 10 | import java.lang.ref.WeakReference; 11 | 12 | public class AnimationHandler extends Handler { 13 | 14 | private final WeakReference mCircleViewWeakReference; 15 | // Spin bar length in degree at start of animation 16 | private float mSpinningBarLengthStart; 17 | private long mAnimationStartTime; 18 | private long mLengthChangeAnimationStartTime; 19 | private TimeInterpolator mLengthChangeInterpolator = new DecelerateInterpolator(); 20 | // The interpolator for value animations 21 | private TimeInterpolator mInterpolator = new AccelerateDecelerateInterpolator(); 22 | private double mLengthChangeAnimationDuration; 23 | private long mFrameStartTime = 0; 24 | 25 | AnimationHandler(CircleProgressView circleView) { 26 | super(circleView.getContext().getMainLooper()); 27 | mCircleViewWeakReference = new WeakReference(circleView); 28 | } 29 | 30 | 31 | /** 32 | * Sets interpolator for value animations. 33 | * 34 | * @param mInterpolator the m interpolator 35 | */ 36 | public void setValueInterpolator(TimeInterpolator mInterpolator) { 37 | this.mInterpolator = mInterpolator; 38 | } 39 | 40 | 41 | /** 42 | * Sets the interpolator for length changes of the bar. 43 | * 44 | * @param mLengthChangeInterpolator the m length change interpolator 45 | */ 46 | public void setLengthChangeInterpolator(TimeInterpolator mLengthChangeInterpolator) { 47 | this.mLengthChangeInterpolator = mLengthChangeInterpolator; 48 | } 49 | 50 | @Override 51 | public void handleMessage(Message msg) { 52 | CircleProgressView circleView = mCircleViewWeakReference.get(); 53 | if (circleView == null) { 54 | return; 55 | } 56 | AnimationMsg msgType = AnimationMsg.values()[msg.what]; 57 | if (msgType == AnimationMsg.TICK) { 58 | removeMessages(AnimationMsg.TICK.ordinal()); // necessary to remove concurrent ticks. 59 | } 60 | 61 | //if (msgType != AnimationMsg.TICK) 62 | // Log.d("JaGr", TAG + "LOG00099: State:" + circleView.mAnimationState + " Received: " + msgType); 63 | mFrameStartTime = SystemClock.uptimeMillis(); 64 | switch (circleView.mAnimationState) { 65 | 66 | 67 | case IDLE: 68 | switch (msgType) { 69 | 70 | case START_SPINNING: 71 | enterSpinning(circleView); 72 | 73 | break; 74 | case STOP_SPINNING: 75 | //IGNORE not spinning 76 | break; 77 | case SET_VALUE: 78 | setValue(msg, circleView); 79 | break; 80 | case SET_VALUE_ANIMATED: 81 | 82 | enterSetValueAnimated(msg, circleView); 83 | break; 84 | case TICK: 85 | removeMessages(AnimationMsg.TICK.ordinal()); // remove old ticks 86 | //IGNORE nothing to do 87 | break; 88 | } 89 | break; 90 | case SPINNING: 91 | switch (msgType) { 92 | 93 | case START_SPINNING: 94 | //IGNORE already spinning 95 | break; 96 | case STOP_SPINNING: 97 | enterEndSpinning(circleView); 98 | 99 | break; 100 | case SET_VALUE: 101 | setValue(msg, circleView); 102 | break; 103 | case SET_VALUE_ANIMATED: 104 | enterEndSpinningStartAnimating(circleView, msg); 105 | break; 106 | case TICK: 107 | // set length 108 | 109 | float length_delta = circleView.mSpinningBarLengthCurrent - circleView.mSpinningBarLengthOrig; 110 | float t = (float) ((System.currentTimeMillis() - mLengthChangeAnimationStartTime) 111 | / mLengthChangeAnimationDuration); 112 | t = t > 1.0f ? 1.0f : t; 113 | float interpolatedRatio = mLengthChangeInterpolator.getInterpolation(t); 114 | 115 | if (Math.abs(length_delta) < 1) { 116 | //spinner length is within bounds 117 | circleView.mSpinningBarLengthCurrent = circleView.mSpinningBarLengthOrig; 118 | } else if (circleView.mSpinningBarLengthCurrent < circleView.mSpinningBarLengthOrig) { 119 | //spinner to short, --> grow 120 | circleView.mSpinningBarLengthCurrent = mSpinningBarLengthStart + ((circleView.mSpinningBarLengthOrig - mSpinningBarLengthStart) * interpolatedRatio); 121 | } else { 122 | //spinner to long, --> shrink 123 | circleView.mSpinningBarLengthCurrent = (mSpinningBarLengthStart - ((mSpinningBarLengthStart - circleView.mSpinningBarLengthOrig) * interpolatedRatio)); 124 | } 125 | 126 | circleView.mCurrentSpinnerDegreeValue += circleView.mSpinSpeed; // spin speed value (in degree) 127 | 128 | if (circleView.mCurrentSpinnerDegreeValue > 360) { 129 | circleView.mCurrentSpinnerDegreeValue = 0; 130 | } 131 | sendEmptyMessageDelayed(AnimationMsg.TICK.ordinal(), circleView.mFrameDelayMillis - (SystemClock.uptimeMillis() - mFrameStartTime)); 132 | circleView.invalidate(); 133 | break; 134 | } 135 | 136 | break; 137 | case END_SPINNING: 138 | switch (msgType) { 139 | 140 | case START_SPINNING: 141 | circleView.mAnimationState = AnimationState.SPINNING; 142 | if (circleView.mAnimationStateChangedListener != null) { 143 | circleView.mAnimationStateChangedListener.onAnimationStateChanged(circleView.mAnimationState); 144 | } 145 | sendEmptyMessageDelayed(AnimationMsg.TICK.ordinal(), circleView.mFrameDelayMillis - (SystemClock.uptimeMillis() - mFrameStartTime)); 146 | 147 | break; 148 | case STOP_SPINNING: 149 | //IGNORE already stopping 150 | break; 151 | case SET_VALUE: 152 | setValue(msg, circleView); 153 | break; 154 | case SET_VALUE_ANIMATED: 155 | enterEndSpinningStartAnimating(circleView, msg); 156 | 157 | break; 158 | case TICK: 159 | 160 | float t = (float) ((System.currentTimeMillis() - mLengthChangeAnimationStartTime) 161 | / mLengthChangeAnimationDuration); 162 | t = t > 1.0f ? 1.0f : t; 163 | float interpolatedRatio = mLengthChangeInterpolator.getInterpolation(t); 164 | circleView.mSpinningBarLengthCurrent = (mSpinningBarLengthStart) * (1f - interpolatedRatio); 165 | 166 | circleView.mCurrentSpinnerDegreeValue += circleView.mSpinSpeed; // spin speed value (not in percent) 167 | if (circleView.mSpinningBarLengthCurrent < 0.01f) { 168 | //end here, spinning finished 169 | circleView.mAnimationState = AnimationState.IDLE; 170 | if (circleView.mAnimationStateChangedListener != null) { 171 | circleView.mAnimationStateChangedListener.onAnimationStateChanged(circleView.mAnimationState); 172 | } 173 | } 174 | sendEmptyMessageDelayed(AnimationMsg.TICK.ordinal(), circleView.mFrameDelayMillis - (SystemClock.uptimeMillis() - mFrameStartTime)); 175 | circleView.invalidate(); 176 | break; 177 | } 178 | 179 | break; 180 | case END_SPINNING_START_ANIMATING: 181 | switch (msgType) { 182 | 183 | case START_SPINNING: 184 | circleView.mDrawBarWhileSpinning = false; 185 | enterSpinning(circleView); 186 | 187 | break; 188 | case STOP_SPINNING: 189 | //IGNORE already stopping 190 | break; 191 | case SET_VALUE: 192 | circleView.mDrawBarWhileSpinning = false; 193 | setValue(msg, circleView); 194 | 195 | break; 196 | case SET_VALUE_ANIMATED: 197 | circleView.mValueFrom = 0; // start from zero after spinning 198 | circleView.mValueTo = ((float[]) msg.obj)[1]; 199 | sendEmptyMessageDelayed(AnimationMsg.TICK.ordinal(), circleView.mFrameDelayMillis - (SystemClock.uptimeMillis() - mFrameStartTime)); 200 | 201 | break; 202 | case TICK: 203 | //shrink spinner till it has its original length 204 | if (circleView.mSpinningBarLengthCurrent > circleView.mSpinningBarLengthOrig && !circleView.mDrawBarWhileSpinning) { 205 | //spinner to long, --> shrink 206 | float t = (float) ((System.currentTimeMillis() - mLengthChangeAnimationStartTime) 207 | / mLengthChangeAnimationDuration); 208 | t = t > 1.0f ? 1.0f : t; 209 | float interpolatedRatio = mLengthChangeInterpolator.getInterpolation(t); 210 | circleView.mSpinningBarLengthCurrent = (mSpinningBarLengthStart) * (1f - interpolatedRatio); 211 | } 212 | 213 | // move spinner for spin speed value (not in percent) 214 | circleView.mCurrentSpinnerDegreeValue += circleView.mSpinSpeed; 215 | 216 | //if the start of the spinner reaches zero, start animating the value 217 | if (circleView.mCurrentSpinnerDegreeValue > 360 && !circleView.mDrawBarWhileSpinning) { 218 | mAnimationStartTime = System.currentTimeMillis(); 219 | circleView.mDrawBarWhileSpinning = true; 220 | initReduceAnimation(circleView); 221 | if (circleView.mAnimationStateChangedListener != null) { 222 | circleView.mAnimationStateChangedListener.onAnimationStateChanged(AnimationState.START_ANIMATING_AFTER_SPINNING); 223 | } 224 | } 225 | 226 | //value is already animating, calc animation value and reduce spinner 227 | if (circleView.mDrawBarWhileSpinning) { 228 | circleView.mCurrentSpinnerDegreeValue = 360; 229 | circleView.mSpinningBarLengthCurrent -= circleView.mSpinSpeed; 230 | calcNextAnimationValue(circleView); 231 | 232 | float t = (float) ((System.currentTimeMillis() - mLengthChangeAnimationStartTime) 233 | / mLengthChangeAnimationDuration); 234 | t = t > 1.0f ? 1.0f : t; 235 | float interpolatedRatio = mLengthChangeInterpolator.getInterpolation(t); 236 | circleView.mSpinningBarLengthCurrent = (mSpinningBarLengthStart) * (1f - interpolatedRatio); 237 | } 238 | 239 | //spinner is no longer visible switch state to animating 240 | if (circleView.mSpinningBarLengthCurrent < 0.1) { 241 | //spinning finished, start animating the current value 242 | circleView.mAnimationState = AnimationState.ANIMATING; 243 | if (circleView.mAnimationStateChangedListener != null) { 244 | circleView.mAnimationStateChangedListener.onAnimationStateChanged(circleView.mAnimationState); 245 | } 246 | circleView.invalidate(); 247 | circleView.mDrawBarWhileSpinning = false; 248 | circleView.mSpinningBarLengthCurrent = circleView.mSpinningBarLengthOrig; 249 | 250 | } else { 251 | circleView.invalidate(); 252 | } 253 | sendEmptyMessageDelayed(AnimationMsg.TICK.ordinal(), circleView.mFrameDelayMillis - (SystemClock.uptimeMillis() - mFrameStartTime)); 254 | break; 255 | } 256 | 257 | break; 258 | case ANIMATING: 259 | switch (msgType) { 260 | 261 | case START_SPINNING: 262 | enterSpinning(circleView); 263 | break; 264 | case STOP_SPINNING: 265 | //Ignore, not spinning 266 | break; 267 | case SET_VALUE: 268 | setValue(msg, circleView); 269 | break; 270 | case SET_VALUE_ANIMATED: 271 | mAnimationStartTime = System.currentTimeMillis(); 272 | //restart animation from current value 273 | circleView.mValueFrom = circleView.mCurrentValue; 274 | circleView.mValueTo = ((float[]) msg.obj)[1]; 275 | 276 | break; 277 | case TICK: 278 | if (calcNextAnimationValue(circleView)) { 279 | //animation finished 280 | circleView.mAnimationState = AnimationState.IDLE; 281 | if (circleView.mAnimationStateChangedListener != null) { 282 | circleView.mAnimationStateChangedListener.onAnimationStateChanged(circleView.mAnimationState); 283 | } 284 | circleView.mCurrentValue = circleView.mValueTo; 285 | } 286 | sendEmptyMessageDelayed(AnimationMsg.TICK.ordinal(), circleView.mFrameDelayMillis - (SystemClock.uptimeMillis() - mFrameStartTime)); 287 | circleView.invalidate(); 288 | break; 289 | } 290 | 291 | break; 292 | 293 | } 294 | } 295 | 296 | private void enterSetValueAnimated(Message msg, CircleProgressView circleView) { 297 | circleView.mValueFrom = ((float[]) msg.obj)[0]; 298 | circleView.mValueTo = ((float[]) msg.obj)[1]; 299 | mAnimationStartTime = System.currentTimeMillis(); 300 | circleView.mAnimationState = AnimationState.ANIMATING; 301 | if (circleView.mAnimationStateChangedListener != null) { 302 | circleView.mAnimationStateChangedListener.onAnimationStateChanged(circleView.mAnimationState); 303 | } 304 | sendEmptyMessageDelayed(AnimationMsg.TICK.ordinal(), circleView.mFrameDelayMillis - (SystemClock.uptimeMillis() - mFrameStartTime)); 305 | } 306 | 307 | private void enterEndSpinningStartAnimating(CircleProgressView circleView, Message msg) { 308 | circleView.mAnimationState = AnimationState.END_SPINNING_START_ANIMATING; 309 | if (circleView.mAnimationStateChangedListener != null) { 310 | circleView.mAnimationStateChangedListener.onAnimationStateChanged(circleView.mAnimationState); 311 | } 312 | circleView.mValueFrom = 0; // start from zero after spinning 313 | circleView.mValueTo = ((float[]) msg.obj)[1]; 314 | 315 | mLengthChangeAnimationStartTime = System.currentTimeMillis(); 316 | mSpinningBarLengthStart = circleView.mSpinningBarLengthCurrent; 317 | 318 | sendEmptyMessageDelayed(AnimationMsg.TICK.ordinal(), circleView.mFrameDelayMillis - (SystemClock.uptimeMillis() - mFrameStartTime)); 319 | 320 | } 321 | 322 | private void enterEndSpinning(CircleProgressView circleView) { 323 | circleView.mAnimationState = AnimationState.END_SPINNING; 324 | initReduceAnimation(circleView); 325 | if (circleView.mAnimationStateChangedListener != null) { 326 | circleView.mAnimationStateChangedListener.onAnimationStateChanged(circleView.mAnimationState); 327 | } 328 | sendEmptyMessageDelayed(AnimationMsg.TICK.ordinal(), circleView.mFrameDelayMillis - (SystemClock.uptimeMillis() - mFrameStartTime)); 329 | } 330 | 331 | private void initReduceAnimation(CircleProgressView circleView) { 332 | float degreesTillFinish = circleView.mSpinningBarLengthCurrent; 333 | float stepsTillFinish = degreesTillFinish / circleView.mSpinSpeed; 334 | mLengthChangeAnimationDuration = (stepsTillFinish * circleView.mFrameDelayMillis) * 2f; 335 | 336 | mLengthChangeAnimationStartTime = System.currentTimeMillis(); 337 | mSpinningBarLengthStart = circleView.mSpinningBarLengthCurrent; 338 | } 339 | 340 | private void enterSpinning(CircleProgressView circleView) { 341 | circleView.mAnimationState = AnimationState.SPINNING; 342 | if (circleView.mAnimationStateChangedListener != null) { 343 | circleView.mAnimationStateChangedListener.onAnimationStateChanged(circleView.mAnimationState); 344 | } 345 | circleView.mSpinningBarLengthCurrent = (360f / circleView.mMaxValue * circleView.mCurrentValue); 346 | circleView.mCurrentSpinnerDegreeValue = (360f / circleView.mMaxValue * circleView.mCurrentValue); 347 | mLengthChangeAnimationStartTime = System.currentTimeMillis(); 348 | mSpinningBarLengthStart = circleView.mSpinningBarLengthCurrent; 349 | 350 | 351 | //calc animation time 352 | float stepsTillFinish = circleView.mSpinningBarLengthOrig / circleView.mSpinSpeed; 353 | mLengthChangeAnimationDuration = ((stepsTillFinish * circleView.mFrameDelayMillis) * 2f); 354 | 355 | 356 | sendEmptyMessageDelayed(AnimationMsg.TICK.ordinal(), circleView.mFrameDelayMillis - (SystemClock.uptimeMillis() - mFrameStartTime)); 357 | } 358 | 359 | 360 | /** 361 | * * 362 | * 363 | * @param circleView the circle view 364 | * @return false if animation still running, true if animation is finished. 365 | */ 366 | private boolean calcNextAnimationValue(CircleProgressView circleView) { 367 | float t = (float) ((System.currentTimeMillis() - mAnimationStartTime) 368 | / circleView.mAnimationDuration); 369 | t = t > 1.0f ? 1.0f : t; 370 | float interpolatedRatio = mInterpolator.getInterpolation(t); 371 | 372 | circleView.mCurrentValue = (circleView.mValueFrom + ((circleView.mValueTo - circleView.mValueFrom) * interpolatedRatio)); 373 | 374 | return t >= 1; 375 | } 376 | 377 | private void setValue(Message msg, CircleProgressView circleView) { 378 | circleView.mValueFrom = circleView.mValueTo; 379 | circleView.mCurrentValue = circleView.mValueTo = ((float[]) msg.obj)[0]; 380 | circleView.mAnimationState = AnimationState.IDLE; 381 | if (circleView.mAnimationStateChangedListener != null) { 382 | circleView.mAnimationStateChangedListener.onAnimationStateChanged(circleView.mAnimationState); 383 | } 384 | circleView.invalidate(); 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /CircleProgressView/src/main/java/at/grabner/circleprogress/AnimationMsg.java: -------------------------------------------------------------------------------- 1 | package at.grabner.circleprogress; 2 | 3 | enum AnimationMsg { 4 | 5 | START_SPINNING, 6 | STOP_SPINNING, 7 | SET_VALUE, 8 | SET_VALUE_ANIMATED, 9 | TICK 10 | 11 | } 12 | -------------------------------------------------------------------------------- /CircleProgressView/src/main/java/at/grabner/circleprogress/AnimationState.java: -------------------------------------------------------------------------------- 1 | package at.grabner.circleprogress; 2 | 3 | public enum AnimationState { 4 | IDLE, 5 | SPINNING, 6 | END_SPINNING, 7 | END_SPINNING_START_ANIMATING, 8 | START_ANIMATING_AFTER_SPINNING, ANIMATING 9 | } 10 | -------------------------------------------------------------------------------- /CircleProgressView/src/main/java/at/grabner/circleprogress/AnimationStateChangedListener.java: -------------------------------------------------------------------------------- 1 | package at.grabner.circleprogress; 2 | 3 | public interface AnimationStateChangedListener { 4 | 5 | /** 6 | * Call if animation state changes. 7 | * This code runs in the animation loop, so keep your code short! 8 | * 9 | * @param _animationState The new animation state 10 | */ 11 | void onAnimationStateChanged(AnimationState _animationState); 12 | } 13 | -------------------------------------------------------------------------------- /CircleProgressView/src/main/java/at/grabner/circleprogress/BarStartEndLine.java: -------------------------------------------------------------------------------- 1 | package at.grabner.circleprogress; 2 | 3 | /** 4 | * Created by jzeferino on 07.11.2016. 5 | */ 6 | public enum BarStartEndLine { 7 | /** 8 | * No lines 9 | */ 10 | NONE, /** 11 | * Show Start line in Bar 12 | */ 13 | START, 14 | /** 15 | * Show End line in Bar 16 | */ 17 | END, 18 | /** 19 | * Show both lines, start and end 20 | */ 21 | BOTH 22 | } 23 | -------------------------------------------------------------------------------- /CircleProgressView/src/main/java/at/grabner/circleprogress/CircleProgressView.java: -------------------------------------------------------------------------------- 1 | package at.grabner.circleprogress; 2 | 3 | import android.animation.TimeInterpolator; 4 | import android.annotation.TargetApi; 5 | import android.content.Context; 6 | import android.content.res.TypedArray; 7 | import android.graphics.Bitmap; 8 | import android.graphics.Canvas; 9 | import android.graphics.Color; 10 | import android.graphics.Matrix; 11 | import android.graphics.Paint; 12 | import android.graphics.Paint.Style; 13 | import android.graphics.PointF; 14 | import android.graphics.PorterDuff; 15 | import android.graphics.PorterDuffXfermode; 16 | import android.graphics.Rect; 17 | import android.graphics.RectF; 18 | import android.graphics.Shader; 19 | import android.graphics.SweepGradient; 20 | import android.graphics.Typeface; 21 | import android.os.Build; 22 | import android.os.Message; 23 | import androidx.annotation.ColorInt; 24 | import androidx.annotation.FloatRange; 25 | import androidx.annotation.IntRange; 26 | import androidx.annotation.NonNull; 27 | import android.util.AttributeSet; 28 | import android.util.Log; 29 | import android.view.MotionEvent; 30 | import android.view.View; 31 | 32 | import java.text.DecimalFormat; 33 | 34 | /** 35 | * An circle view, similar to Android's ProgressBar. 36 | * Can be used in 'value mode' or 'spinning mode'. 37 | *

38 | * In spinning mode it can be used like a intermediate progress bar. 39 | *

40 | * In value mode it can be used as a progress bar or to visualize any other value. 41 | * Setting a value is fully animated. There are also nice transitions from animating to value mode. 42 | *

43 | * Typical use case would be to load a new value. During the loading time set the CircleView to spinning. 44 | * As soon as you get your value, just set it with {@link #setValueAnimated(float, long)}. 45 | * 46 | * @author Jakob Grabner, based on the Progress wheel of Todd Davies 47 | * https://github.com/Todd-Davies/CircleView 48 | *

49 | * Licensed under the Creative Commons Attribution 3.0 license see: 50 | * http://creativecommons.org/licenses/by/3.0/ 51 | */ 52 | @SuppressWarnings("unused") 53 | public class CircleProgressView extends View { 54 | 55 | /** 56 | * The log tag. 57 | */ 58 | private final static String TAG = "CircleView"; 59 | private static final boolean DEBUG = false; 60 | //---------------------------------- 61 | //region members 62 | //Colors (with defaults) 63 | private final int mBarColorStandard = 0xff009688; //stylish blue 64 | protected int mLayoutHeight = 0; 65 | protected int mLayoutWidth = 0; 66 | //Rectangles 67 | protected RectF mCircleBounds = new RectF(); 68 | protected RectF mInnerCircleBound = new RectF(); 69 | protected PointF mCenter; 70 | /** 71 | * Maximum size of the text. 72 | */ 73 | protected RectF mOuterTextBounds = new RectF(); 74 | /** 75 | * Actual size of the text. 76 | */ 77 | protected RectF mActualTextBounds = new RectF(); 78 | protected RectF mUnitBounds = new RectF(); 79 | protected RectF mCircleOuterContour = new RectF(); 80 | protected RectF mCircleInnerContour = new RectF(); 81 | //value animation 82 | Direction mDirection = Direction.CW; 83 | float mCurrentValue = 0; 84 | float mValueTo = 0; 85 | float mValueFrom = 0; 86 | float mMaxValue = 100; 87 | float mMinValueAllowed = 0; 88 | float mMaxValueAllowed = -1; 89 | // spinner animation 90 | float mSpinningBarLengthCurrent = 0; 91 | float mSpinningBarLengthOrig = 42; 92 | float mCurrentSpinnerDegreeValue = 0; 93 | //Animation 94 | //The amount of degree to move the bar by on each draw 95 | float mSpinSpeed = 2.8f; 96 | //Enable spin 97 | boolean mSpin = false; 98 | /** 99 | * The animation duration in ms 100 | */ 101 | double mAnimationDuration = 900; 102 | //The number of milliseconds to wait in between each draw 103 | int mFrameDelayMillis = 10; 104 | // helper for AnimationState.END_SPINNING_START_ANIMATING 105 | boolean mDrawBarWhileSpinning; 106 | //The animation handler containing the animation state machine. 107 | AnimationHandler mAnimationHandler = new AnimationHandler(this); 108 | //The current state of the animation state machine. 109 | AnimationState mAnimationState = AnimationState.IDLE; 110 | AnimationStateChangedListener mAnimationStateChangedListener; 111 | private int mBarWidth = 40; 112 | private int mRimWidth = 40; 113 | private int mStartAngle = 270; 114 | private float mOuterContourSize = 1; 115 | private float mInnerContourSize = 1; 116 | 117 | // Bar start/end width and type 118 | private int mBarStartEndLineWidth = 0; 119 | private BarStartEndLine mBarStartEndLine = BarStartEndLine.NONE; 120 | private int mBarStartEndLineColor = 0xAA000000; 121 | private float mBarStartEndLineSweep = 10f; 122 | //Default text sizes 123 | private int mUnitTextSize = 10; 124 | private int mTextSize = 10; 125 | //Text scale 126 | private float mTextScale = 1; 127 | private float mUnitScale = 1; 128 | private int mOuterContourColor = 0xAA000000; 129 | private int mInnerContourColor = 0xAA000000; 130 | private int mSpinnerColor = mBarColorStandard; //stylish blue 131 | private int mBackgroundCircleColor = 0x00000000; //transparent 132 | private int mRimColor = 0xAA83d0c9; 133 | private int mTextColor = 0xFF000000; 134 | private int mUnitColor = 0xFF000000; 135 | private boolean mIsAutoColorEnabled = false; 136 | private int[] mBarColors = new int[]{ 137 | mBarColorStandard //stylish blue 138 | }; 139 | //Caps 140 | private Paint.Cap mBarStrokeCap = Paint.Cap.BUTT; 141 | private Paint.Cap mSpinnerStrokeCap = Paint.Cap.BUTT; 142 | //Paints 143 | private Paint mBarPaint = new Paint(); 144 | private Paint mShaderlessBarPaint; 145 | private Paint mBarSpinnerPaint = new Paint(); 146 | private Paint mBarStartEndLinePaint = new Paint(); 147 | private Paint mBackgroundCirclePaint = new Paint(); 148 | private Paint mRimPaint = new Paint(); 149 | private Paint mTextPaint = new Paint(); 150 | private Paint mUnitTextPaint = new Paint(); 151 | private Paint mOuterContourPaint = new Paint(); 152 | private Paint mInnerContourPaint = new Paint(); 153 | //Other 154 | // The text to show 155 | private String mText = ""; 156 | private int mTextLength; 157 | private String mUnit = ""; 158 | private UnitPosition mUnitPosition = UnitPosition.RIGHT_TOP; 159 | /** 160 | * Indicates if the given text, the current percentage, or the current value should be shown. 161 | */ 162 | private TextMode mTextMode = TextMode.PERCENT; 163 | private boolean mIsAutoTextSize; 164 | private boolean mShowUnit = false; 165 | //clipping 166 | private Bitmap mClippingBitmap; 167 | private Paint mMaskPaint; 168 | /** 169 | * Relative size of the unite string to the value string. 170 | */ 171 | private float mRelativeUniteSize = 1f; 172 | private boolean mSeekModeEnabled = false; 173 | private boolean mShowTextWhileSpinning = false; 174 | private boolean mShowBlock = false; 175 | private int mBlockCount = 18; 176 | private float mBlockScale = 0.9f; 177 | private float mBlockDegree = 360 / mBlockCount; 178 | private float mBlockScaleDegree = mBlockDegree * mBlockScale; 179 | private boolean mRoundToBlock = false; 180 | private boolean mRoundToWholeNumber = false; 181 | 182 | private int mTouchEventCount; 183 | private OnProgressChangedListener onProgressChangedListener; 184 | private float previousProgressChangedValue; 185 | 186 | 187 | private DecimalFormat decimalFormat = new DecimalFormat("0"); 188 | 189 | // Text typeface 190 | private Typeface textTypeface; 191 | private Typeface unitTextTypeface; 192 | //endregion members 193 | //---------------------------------- 194 | 195 | /** 196 | * The constructor for the CircleView 197 | * 198 | * @param context The context. 199 | * @param attrs The attributes. 200 | */ 201 | public CircleProgressView(Context context, AttributeSet attrs) { 202 | super(context, attrs); 203 | 204 | parseAttributes(context.obtainStyledAttributes(attrs, 205 | R.styleable.CircleProgressView)); 206 | 207 | if (!isInEditMode()) { 208 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 209 | setLayerType(View.LAYER_TYPE_HARDWARE, null); 210 | } 211 | } 212 | 213 | mMaskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 214 | mMaskPaint.setFilterBitmap(false); 215 | mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); 216 | setupPaints(); 217 | 218 | if (mSpin) { 219 | spin(); 220 | } 221 | } 222 | 223 | private static float calcTextSizeForRect(String _text, Paint _textPaint, RectF _rectBounds) { 224 | 225 | Matrix matrix = new Matrix(); 226 | Rect textBoundsTmp = new Rect(); 227 | //replace ones because for some fonts the 1 takes less space which causes issues 228 | String text = _text.replace('1', '0'); 229 | 230 | //get current mText bounds 231 | _textPaint.getTextBounds(text, 0, text.length(), textBoundsTmp); 232 | 233 | RectF textBoundsTmpF = new RectF(textBoundsTmp); 234 | 235 | matrix.setRectToRect(textBoundsTmpF, _rectBounds, Matrix.ScaleToFit.CENTER); 236 | float values[] = new float[9]; 237 | matrix.getValues(values); 238 | return _textPaint.getTextSize() * values[Matrix.MSCALE_X]; 239 | } 240 | 241 | /** 242 | * @param _angle The angle in degree to normalize 243 | * @return the angle between 0 (EAST) and 360 244 | */ 245 | private static float normalizeAngle(float _angle) { 246 | return (((_angle % 360) + 360) % 360); 247 | } 248 | 249 | /** 250 | * Calculates the angle from centerPt to targetPt in degrees. 251 | * The return should range from [0,360), rotating CLOCKWISE, 252 | * 0 and 360 degrees represents EAST, 253 | * 90 degrees represents SOUTH, etc... 254 | *

255 | * Assumes all points are in the same coordinate space. If they are not, 256 | * you will need to call SwingUtilities.convertPointToScreen or equivalent 257 | * on all arguments before passing them to this function. 258 | * 259 | * @param centerPt Point we are rotating around. 260 | * @param targetPt Point we want to calculate the angle to. 261 | * @return angle in degrees. This is the angle from centerPt to targetPt. 262 | */ 263 | public static double calcRotationAngleInDegrees(PointF centerPt, PointF targetPt) { 264 | // calculate the angle theta from the deltaY and deltaX values 265 | // (atan2 returns radians values from [-PI,PI]) 266 | // 0 currently points EAST. 267 | // NOTE: By preserving Y and X param order to atan2, we are expecting 268 | // a CLOCKWISE angle direction. 269 | double theta = Math.atan2(targetPt.y - centerPt.y, targetPt.x - centerPt.x); 270 | 271 | // rotate the theta angle clockwise by 90 degrees 272 | // (this makes 0 point NORTH) 273 | // NOTE: adding to an angle rotates it clockwise. 274 | // subtracting would rotate it counter-clockwise 275 | // theta += Math.PI/2.0; 276 | 277 | // convert from radians to degrees 278 | // this will give you an angle from [0->270],[-180,0] 279 | double angle = Math.toDegrees(theta); 280 | 281 | // convert to positive range [0-360) 282 | // since we want to prevent negative angles, adjust them now. 283 | // we can assume that atan2 will not return a negative value 284 | // greater than one partial rotation 285 | if (angle < 0) { 286 | angle += 360; 287 | } 288 | 289 | return angle; 290 | } 291 | 292 | //---------------------------------- 293 | //region getter/setter 294 | public BarStartEndLine getBarStartEndLine() { 295 | return mBarStartEndLine; 296 | } 297 | 298 | /** 299 | * Allows to add a line to the start/end of the bar 300 | * 301 | * @param _barWidth The width of the stroke on the start/end of the bar in pixel. 302 | * @param _barStartEndLine The type of line on the start/end of the bar. 303 | * @param _lineColor The line color 304 | * @param _sweepWidth The sweep amount in degrees for the start and end bars to cover. 305 | */ 306 | public void setBarStartEndLine(int _barWidth, BarStartEndLine _barStartEndLine, @ColorInt int _lineColor, float _sweepWidth) { 307 | mBarStartEndLineWidth = _barWidth; 308 | mBarStartEndLine = _barStartEndLine; 309 | mBarStartEndLineColor = _lineColor; 310 | mBarStartEndLineSweep = _sweepWidth; 311 | } 312 | 313 | public int[] getBarColors() { 314 | return mBarColors; 315 | } 316 | 317 | public Paint.Cap getBarStrokeCap() { 318 | return mBarStrokeCap; 319 | } 320 | 321 | /** 322 | * @param _barStrokeCap The stroke cap of the progress bar. 323 | */ 324 | public void setBarStrokeCap(Paint.Cap _barStrokeCap) { 325 | mBarStrokeCap = _barStrokeCap; 326 | mBarPaint.setStrokeCap(_barStrokeCap); 327 | if (mBarStrokeCap != Paint.Cap.BUTT) { 328 | mShaderlessBarPaint = new Paint(mBarPaint); 329 | mShaderlessBarPaint.setShader(null); 330 | mShaderlessBarPaint.setColor(mBarColors[0]); 331 | } 332 | } 333 | 334 | public int getBarWidth() { 335 | return mBarWidth; 336 | } 337 | 338 | /** 339 | * @param barWidth The width of the progress bar in pixel. 340 | */ 341 | public void setBarWidth(@IntRange(from = 0) int barWidth) { 342 | this.mBarWidth = barWidth; 343 | mBarPaint.setStrokeWidth(barWidth); 344 | mBarSpinnerPaint.setStrokeWidth(barWidth); 345 | } 346 | 347 | public int getBlockCount() { 348 | return mBlockCount; 349 | } 350 | 351 | public void setBlockCount(int blockCount) { 352 | if (blockCount > 1) { 353 | mShowBlock = true; 354 | mBlockCount = blockCount; 355 | mBlockDegree = 360.0f / blockCount; 356 | mBlockScaleDegree = mBlockDegree * mBlockScale; 357 | } else { 358 | mShowBlock = false; 359 | } 360 | } 361 | 362 | public void setRoundToBlock(boolean _roundToBlock) { 363 | mRoundToBlock = _roundToBlock; 364 | } 365 | 366 | public boolean getRoundToBlock() { 367 | return mRoundToBlock; 368 | } 369 | 370 | public void setRoundToWholeNumber(boolean roundToWholeNumber) { 371 | mRoundToWholeNumber = roundToWholeNumber; 372 | } 373 | 374 | public boolean getRoundToWholeNumber() { 375 | return mRoundToWholeNumber; 376 | } 377 | 378 | public float getBlockScale() { 379 | return mBlockScale; 380 | } 381 | 382 | public void setBlockScale(@FloatRange(from = 0.0, to = 1) float blockScale) { 383 | if (blockScale >= 0.0f && blockScale <= 1.0f) { 384 | mBlockScale = blockScale; 385 | mBlockScaleDegree = mBlockDegree * blockScale; 386 | } 387 | } 388 | 389 | public int getOuterContourColor() { 390 | return mOuterContourColor; 391 | } 392 | 393 | /** 394 | * @param _contourColor The color of the background contour of the circle. 395 | */ 396 | public void setOuterContourColor(@ColorInt int _contourColor) { 397 | mOuterContourColor = _contourColor; 398 | mOuterContourPaint.setColor(_contourColor); 399 | } 400 | 401 | public float getOuterContourSize() { 402 | return mOuterContourSize; 403 | } 404 | 405 | /** 406 | * @param _contourSize The size of the background contour of the circle. 407 | */ 408 | public void setOuterContourSize(@FloatRange(from = 0.0) float _contourSize) { 409 | mOuterContourSize = _contourSize; 410 | mOuterContourPaint.setStrokeWidth(_contourSize); 411 | } 412 | 413 | public int getInnerContourColor() { 414 | return mInnerContourColor; 415 | } 416 | 417 | /** 418 | * @param _contourColor The color of the background contour of the circle. 419 | */ 420 | public void setInnerContourColor(@ColorInt int _contourColor) { 421 | mInnerContourColor = _contourColor; 422 | mInnerContourPaint.setColor(_contourColor); 423 | } 424 | 425 | public float getInnerContourSize() { 426 | return mInnerContourSize; 427 | } 428 | 429 | /** 430 | * @param _contourSize The size of the background contour of the circle. 431 | */ 432 | public void setInnerContourSize(@FloatRange(from = 0.0) float _contourSize) { 433 | mInnerContourSize = _contourSize; 434 | mInnerContourPaint.setStrokeWidth(_contourSize); 435 | } 436 | 437 | /** 438 | * @return The number of ms to wait between each draw call. 439 | */ 440 | public int getDelayMillis() { 441 | return mFrameDelayMillis; 442 | } 443 | 444 | /** 445 | * @param delayMillis The number of ms to wait between each draw call. 446 | */ 447 | public void setDelayMillis(int delayMillis) { 448 | this.mFrameDelayMillis = delayMillis; 449 | } 450 | 451 | public int getFillColor() { 452 | return mBackgroundCirclePaint.getColor(); 453 | } 454 | 455 | public float getCurrentValue() { 456 | return mCurrentValue; 457 | } 458 | 459 | public float getMinValueAllowed() { 460 | return mMinValueAllowed; 461 | } 462 | 463 | public float getMaxValueAllowed() { 464 | return mMaxValueAllowed; 465 | } 466 | 467 | public float getMaxValue() { 468 | return mMaxValue; 469 | } 470 | 471 | /** 472 | * The max value of the progress bar. Used to calculate the percentage of the current value. 473 | * The bar fills according to the percentage. The default value is 100. 474 | * 475 | * @param _maxValue The max value. 476 | */ 477 | public void setMaxValue(@FloatRange(from = 0) float _maxValue) { 478 | mMaxValue = _maxValue; 479 | } 480 | 481 | /** 482 | * The min value allowed of the progress bar. Used to limit the min possible value of the current value. 483 | * 484 | * @param _minValueAllowed The min value allowed. 485 | */ 486 | public void setMinValueAllowed(@FloatRange(from = 0) float _minValueAllowed) { 487 | mMinValueAllowed = _minValueAllowed; 488 | } 489 | 490 | /** 491 | * The max value allowed of the progress bar. Used to limit the max possible value of the current value. 492 | * 493 | * @param _maxValueAllowed The max value allowed. 494 | */ 495 | public void setMaxValueAllowed(@FloatRange(from = 0) float _maxValueAllowed) { 496 | mMaxValueAllowed = _maxValueAllowed; 497 | } 498 | 499 | /** 500 | * @return The relative size (scale factor) of the unit text size to the text size 501 | */ 502 | public float getRelativeUniteSize() { 503 | return mRelativeUniteSize; 504 | } 505 | 506 | public int getRimColor() { 507 | return mRimColor; 508 | } 509 | 510 | /** 511 | * @param rimColor The color of the rim around the Circle. 512 | */ 513 | public void setRimColor(@ColorInt int rimColor) { 514 | mRimColor = rimColor; 515 | mRimPaint.setColor(rimColor); 516 | } 517 | 518 | public Shader getRimShader() { 519 | return mRimPaint.getShader(); 520 | } 521 | 522 | public void setRimShader(Shader shader) { 523 | this.mRimPaint.setShader(shader); 524 | } 525 | 526 | public int getRimWidth() { 527 | return mRimWidth; 528 | } 529 | 530 | /** 531 | * @param rimWidth The width in pixel of the rim around the circle 532 | */ 533 | public void setRimWidth(@IntRange(from = 0) int rimWidth) { 534 | mRimWidth = rimWidth; 535 | mRimPaint.setStrokeWidth(rimWidth); 536 | } 537 | 538 | public float getSpinSpeed() { 539 | return mSpinSpeed; 540 | } 541 | 542 | /** 543 | * The amount of degree to move the bar on every draw call. 544 | * 545 | * @param spinSpeed the speed of the spinner 546 | */ 547 | public void setSpinSpeed(float spinSpeed) { 548 | mSpinSpeed = spinSpeed; 549 | } 550 | 551 | public Paint.Cap getSpinnerStrokeCap() { 552 | return mSpinnerStrokeCap; 553 | } 554 | 555 | /** 556 | * @param _spinnerStrokeCap The stroke cap of the progress bar in spinning mode. 557 | */ 558 | public void setSpinnerStrokeCap(Paint.Cap _spinnerStrokeCap) { 559 | mSpinnerStrokeCap = _spinnerStrokeCap; 560 | mBarSpinnerPaint.setStrokeCap(_spinnerStrokeCap); 561 | } 562 | 563 | public int getStartAngle() { 564 | return mStartAngle; 565 | } 566 | 567 | public void setStartAngle(@IntRange(from = 0,to = 360) int _startAngle) { 568 | // get a angle between 0 and 360 569 | mStartAngle = (int) normalizeAngle(_startAngle); 570 | } 571 | 572 | public int calcTextColor() { 573 | return mTextColor; 574 | } 575 | 576 | /** 577 | * Sets the text color. 578 | * You also need to set {@link #setTextColorAuto(boolean)} to false to see your color. 579 | * 580 | * @param textColor the color 581 | */ 582 | public void setTextColor(@ColorInt int textColor) { 583 | mTextColor = textColor; 584 | mTextPaint.setColor(textColor); 585 | } 586 | 587 | /** 588 | * @return The scale value 589 | */ 590 | public float getTextScale() { 591 | return mTextScale; 592 | } 593 | 594 | /** 595 | * Scale factor for main text in the center of the circle view. 596 | * Only used if auto text size is enabled. 597 | * 598 | * @param _textScale The scale value. 599 | */ 600 | public void setTextScale(@FloatRange(from = 0.0) float _textScale) { 601 | mTextScale = _textScale; 602 | } 603 | 604 | public int getTextSize() { 605 | return mTextSize; 606 | } 607 | 608 | /** 609 | * Text size of the text string. Disables auto text size 610 | * If auto text size is on, use {@link #setTextScale(float)} to scale textSize. 611 | * 612 | * @param textSize The text size of the unit. 613 | */ 614 | public void setTextSize(@IntRange(from = 0) int textSize) { 615 | this.mTextPaint.setTextSize(textSize); 616 | mTextSize = textSize; 617 | mIsAutoTextSize = false; 618 | } 619 | 620 | public String getUnit() { 621 | return mUnit; 622 | } 623 | 624 | /** 625 | * @param _unit The unit to show next to the current value. 626 | * You also need to set {@link #setUnitVisible(boolean)} to true. 627 | */ 628 | public void setUnit(String _unit) { 629 | if (_unit == null) { 630 | mUnit = ""; 631 | } else { 632 | mUnit = _unit; 633 | } 634 | invalidate(); 635 | } 636 | 637 | /** 638 | * @return The scale value 639 | */ 640 | public float getUnitScale() { 641 | return mUnitScale; 642 | } 643 | 644 | /** 645 | * Scale factor for unit text next to the main text. 646 | * Only used if auto text size is enabled. 647 | * 648 | * @param _unitScale The scale value. 649 | */ 650 | public void setUnitScale(@FloatRange(from = 0.0) float _unitScale) { 651 | mUnitScale = _unitScale; 652 | } 653 | 654 | public int getUnitSize() { 655 | return mUnitTextSize; 656 | } 657 | 658 | /** 659 | * Text size of the unit string. Only used if text size is also set. (So automatic text size 660 | * calculation is off. see {@link #setTextSize(int)}). 661 | * If auto text size is on, use {@link #setUnitScale(float)} to scale unit size. 662 | * 663 | * @param unitSize The text size of the unit. 664 | */ 665 | public void setUnitSize(@IntRange(from = 0) int unitSize) { 666 | mUnitTextSize = unitSize; 667 | mUnitTextPaint.setTextSize(unitSize); 668 | } 669 | 670 | /** 671 | * @return true if auto text size is enabled, false otherwise. 672 | */ 673 | public boolean isAutoTextSize() { 674 | return mIsAutoTextSize; 675 | } 676 | 677 | /** 678 | * @param _autoTextSize true to enable auto text size calculation. 679 | */ 680 | public void setAutoTextSize(boolean _autoTextSize) { 681 | mIsAutoTextSize = _autoTextSize; 682 | } 683 | 684 | public boolean isSeekModeEnabled() { 685 | return mSeekModeEnabled; 686 | } 687 | 688 | public void setSeekModeEnabled(boolean _seekModeEnabled) { 689 | mSeekModeEnabled = _seekModeEnabled; 690 | } 691 | 692 | public boolean isShowBlock() { 693 | return mShowBlock; 694 | } 695 | 696 | public void setShowBlock(boolean showBlock) { 697 | mShowBlock = showBlock; 698 | } 699 | 700 | public boolean isShowTextWhileSpinning() { 701 | return mShowTextWhileSpinning; 702 | } 703 | 704 | /** 705 | * @param shouldDrawTextWhileSpinning True to show text in spinning mode, false to hide it. 706 | */ 707 | public void setShowTextWhileSpinning(boolean shouldDrawTextWhileSpinning) { 708 | mShowTextWhileSpinning = shouldDrawTextWhileSpinning; 709 | } 710 | 711 | public boolean isUnitVisible() { 712 | return mShowUnit; 713 | } 714 | 715 | /** 716 | * @param _showUnit True to show unit, false to hide it. 717 | */ 718 | public void setUnitVisible(boolean _showUnit) { 719 | if (_showUnit != mShowUnit) { 720 | mShowUnit = _showUnit; 721 | triggerReCalcTextSizesAndPositions(); // triggers recalculating text sizes 722 | } 723 | } 724 | 725 | /** 726 | * Sets the color of progress bar. 727 | * 728 | * @param barColors One or more colors. If more than one color is specified, a gradient of the colors is used. 729 | */ 730 | public void setBarColor(@ColorInt int... barColors) { 731 | this.mBarColors = barColors; 732 | setupBarPaint(); 733 | } 734 | 735 | /** 736 | * @param _clippingBitmap The bitmap used for clipping. Set to null to disable clipping. 737 | * Default: No clipping. 738 | */ 739 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 740 | public void setClippingBitmap(Bitmap _clippingBitmap) { 741 | 742 | if (getWidth() > 0 && getHeight() > 0) { 743 | mClippingBitmap = Bitmap.createScaledBitmap(_clippingBitmap, getWidth(), getHeight(), false); 744 | } else { 745 | mClippingBitmap = _clippingBitmap; 746 | } 747 | if (mClippingBitmap == null) { 748 | // enable HW acceleration 749 | setLayerType(View.LAYER_TYPE_HARDWARE, null); 750 | } else { 751 | // disable HW acceleration 752 | setLayerType(View.LAYER_TYPE_SOFTWARE, null); 753 | } 754 | } 755 | 756 | /** 757 | * Sets the background color of the entire Progress Circle. 758 | * Set the color to 0x00000000 (Color.TRANSPARENT) to hide it. 759 | * 760 | * @param circleColor the color. 761 | */ 762 | public void setFillCircleColor(@ColorInt int circleColor) { 763 | mBackgroundCircleColor = circleColor; 764 | mBackgroundCirclePaint.setColor(circleColor); 765 | } 766 | 767 | public void setOnAnimationStateChangedListener(AnimationStateChangedListener _animationStateChangedListener) { 768 | mAnimationStateChangedListener = _animationStateChangedListener; 769 | } 770 | 771 | public void setOnProgressChangedListener(OnProgressChangedListener listener) { 772 | onProgressChangedListener = listener; 773 | } 774 | 775 | /** 776 | * @param _color The color of progress the bar in spinning mode. 777 | */ 778 | public void setSpinBarColor(@ColorInt int _color) { 779 | mSpinnerColor = _color; 780 | mBarSpinnerPaint.setColor(mSpinnerColor); 781 | } 782 | 783 | /** 784 | * Length of spinning bar in degree. 785 | * 786 | * @param barLength length in degree 787 | */ 788 | public void setSpinningBarLength(@FloatRange(from = 0.0) float barLength) { 789 | this.mSpinningBarLengthCurrent = mSpinningBarLengthOrig = barLength; 790 | } 791 | 792 | /** 793 | * Set the text in the middle of the circle view. 794 | * You need also set the {@link TextMode} to TextMode.TEXT to see the text. 795 | * 796 | * @param text The text to show 797 | */ 798 | public void setText(String text) { 799 | mText = text != null ? text : ""; 800 | invalidate(); 801 | } 802 | 803 | /** 804 | * If auto text color is enabled, the text color and the unit color is always the same as the rim color. 805 | * This is useful if the rim has multiple colors (color gradient), than the text will always have 806 | * the color of the tip of the rim. 807 | * 808 | * @param isEnabled true to enable, false to disable 809 | */ 810 | public void setTextColorAuto(boolean isEnabled) { 811 | mIsAutoColorEnabled = isEnabled; 812 | } 813 | 814 | /** 815 | * Sets the auto text mode. 816 | * 817 | * @param _textValue The mode 818 | */ 819 | public void setTextMode(TextMode _textValue) { 820 | mTextMode = _textValue; 821 | } 822 | 823 | /** 824 | * @param typeface The typeface to use for the text 825 | */ 826 | public void setTextTypeface(Typeface typeface) { 827 | mTextPaint.setTypeface(typeface); 828 | } 829 | 830 | /** 831 | * Sets the unit text color. 832 | * Also sets {@link #setTextColorAuto(boolean)} to false 833 | * 834 | * @param unitColor The color. 835 | */ 836 | public void setUnitColor(@ColorInt int unitColor) { 837 | mUnitColor = unitColor; 838 | mUnitTextPaint.setColor(unitColor); 839 | mIsAutoColorEnabled = false; 840 | } 841 | 842 | public void setUnitPosition(UnitPosition _unitPosition) { 843 | mUnitPosition = _unitPosition; 844 | triggerReCalcTextSizesAndPositions(); // triggers recalculating text sizes 845 | } 846 | 847 | /** 848 | * @param typeface The typeface to use for the unit text 849 | */ 850 | public void setUnitTextTypeface(Typeface typeface) { 851 | mUnitTextPaint.setTypeface(typeface); 852 | } 853 | 854 | /** 855 | * @param _relativeUniteSize The relative scale factor of the unit text size to the text size. 856 | * Only useful for autotextsize=true; Effects both, the unit text size and the text size. 857 | */ 858 | public void setUnitToTextScale(@FloatRange(from = 0.0) float _relativeUniteSize) { 859 | mRelativeUniteSize = _relativeUniteSize; 860 | triggerReCalcTextSizesAndPositions(); 861 | } 862 | 863 | /** 864 | * Sets the direction of circular motion (clockwise or counter-clockwise). 865 | */ 866 | public void setDirection(Direction direction) { 867 | mDirection = direction; 868 | } 869 | 870 | /** 871 | * Set the value of the circle view without an animation. 872 | * Stops any currently active animations. 873 | * 874 | * @param _value The value. 875 | */ 876 | public void setValue(float _value) { 877 | // round to block 878 | if (mShowBlock && mRoundToBlock) { 879 | float value_per_block = mMaxValue / (float) mBlockCount; 880 | _value = Math.round(_value / value_per_block) * value_per_block; 881 | 882 | } else if (mRoundToWholeNumber) { // round to whole number 883 | _value = Math.round(_value); 884 | } 885 | 886 | // respect min and max values allowed 887 | _value = Math.max(mMinValueAllowed, _value); 888 | 889 | if (mMaxValueAllowed >= 0) 890 | _value = Math.min(mMaxValueAllowed, _value); 891 | 892 | Message msg = new Message(); 893 | msg.what = AnimationMsg.SET_VALUE.ordinal(); 894 | msg.obj = new float[]{_value, _value}; 895 | mAnimationHandler.sendMessage(msg); 896 | triggerOnProgressChanged(_value); 897 | } 898 | 899 | /** 900 | * Sets the value of the circle view with an animation. 901 | * The current value is used as the start value of the animation 902 | * 903 | * @param _valueTo value after animation 904 | */ 905 | public void setValueAnimated(float _valueTo) { 906 | setValueAnimated(_valueTo, 1200); 907 | } 908 | 909 | /** 910 | * Sets the value of the circle view with an animation. 911 | * The current value is used as the start value of the animation 912 | * 913 | * @param _valueTo value after animation 914 | * @param _animationDuration the duration of the animation in milliseconds. 915 | */ 916 | public void setValueAnimated(float _valueTo, long _animationDuration) { 917 | setValueAnimated(mCurrentValue, _valueTo, _animationDuration); 918 | } 919 | 920 | /** 921 | * Sets the value of the circle view with an animation. 922 | * 923 | * @param _valueFrom start value of the animation 924 | * @param _valueTo value after animation 925 | * @param _animationDuration the duration of the animation in milliseconds 926 | */ 927 | public void setValueAnimated(float _valueFrom, float _valueTo, long _animationDuration) { 928 | // round to block 929 | if (mShowBlock && mRoundToBlock) { 930 | float value_per_block = mMaxValue / (float) mBlockCount; 931 | _valueTo = Math.round(_valueTo / value_per_block) * value_per_block; 932 | 933 | } else if (mRoundToWholeNumber) { 934 | _valueTo = Math.round(_valueTo); 935 | } 936 | 937 | // respect min and max values allowed 938 | _valueTo = Math.max(mMinValueAllowed, _valueTo); 939 | 940 | if (mMaxValueAllowed >= 0) 941 | _valueTo = Math.min(mMaxValueAllowed, _valueTo); 942 | 943 | mAnimationDuration = _animationDuration; 944 | Message msg = new Message(); 945 | msg.what = AnimationMsg.SET_VALUE_ANIMATED.ordinal(); 946 | msg.obj = new float[]{_valueFrom, _valueTo}; 947 | mAnimationHandler.sendMessage(msg); 948 | triggerOnProgressChanged(_valueTo); 949 | } 950 | 951 | 952 | public DecimalFormat getDecimalFormat() { 953 | return decimalFormat; 954 | } 955 | 956 | public void setDecimalFormat(DecimalFormat decimalFormat) { 957 | if (decimalFormat == null) { 958 | throw new IllegalArgumentException("decimalFormat must not be null!"); 959 | } 960 | this.decimalFormat = decimalFormat; 961 | } 962 | 963 | /** 964 | * Sets interpolator for value animations. 965 | * 966 | * @param interpolator the interpolator 967 | */ 968 | public void setValueInterpolator(TimeInterpolator interpolator) { 969 | mAnimationHandler.setValueInterpolator(interpolator); 970 | } 971 | 972 | /** 973 | * Sets the interpolator for length changes of the bar. 974 | * 975 | * @param interpolator the interpolator 976 | */ 977 | public void setLengthChangeInterpolator(TimeInterpolator interpolator) { 978 | mAnimationHandler.setLengthChangeInterpolator(interpolator); 979 | } 980 | 981 | //endregion getter/setter 982 | //---------------------------------- 983 | 984 | 985 | /** 986 | * Parse the attributes passed to the view from the XML 987 | * 988 | * @param a the attributes to parse 989 | */ 990 | private void parseAttributes(TypedArray a) { 991 | setBarWidth((int) a.getDimension(R.styleable.CircleProgressView_cpv_barWidth, 992 | mBarWidth)); 993 | 994 | setRimWidth((int) a.getDimension(R.styleable.CircleProgressView_cpv_rimWidth, 995 | mRimWidth)); 996 | 997 | setSpinSpeed((int) a.getFloat(R.styleable.CircleProgressView_cpv_spinSpeed, 998 | mSpinSpeed)); 999 | 1000 | setSpin(a.getBoolean(R.styleable.CircleProgressView_cpv_spin, 1001 | mSpin)); 1002 | 1003 | setDirection(Direction.values()[a.getInt(R.styleable.CircleProgressView_cpv_direction, 0)]); 1004 | 1005 | float value = a.getFloat(R.styleable.CircleProgressView_cpv_value, mCurrentValue); 1006 | setValue(value); 1007 | mCurrentValue = value; 1008 | 1009 | if (a.hasValue(R.styleable.CircleProgressView_cpv_barColor) && a.hasValue(R.styleable.CircleProgressView_cpv_barColor1) && a.hasValue(R.styleable.CircleProgressView_cpv_barColor2) && a.hasValue(R.styleable.CircleProgressView_cpv_barColor3)) { 1010 | mBarColors = new int[]{a.getColor(R.styleable.CircleProgressView_cpv_barColor, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor1, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor2, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor3, mBarColorStandard)}; 1011 | 1012 | } else if (a.hasValue(R.styleable.CircleProgressView_cpv_barColor) && a.hasValue(R.styleable.CircleProgressView_cpv_barColor1) && a.hasValue(R.styleable.CircleProgressView_cpv_barColor2)) { 1013 | 1014 | mBarColors = new int[]{a.getColor(R.styleable.CircleProgressView_cpv_barColor, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor1, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor2, mBarColorStandard)}; 1015 | 1016 | } else if (a.hasValue(R.styleable.CircleProgressView_cpv_barColor) && a.hasValue(R.styleable.CircleProgressView_cpv_barColor1)) { 1017 | 1018 | mBarColors = new int[]{a.getColor(R.styleable.CircleProgressView_cpv_barColor, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor1, mBarColorStandard)}; 1019 | 1020 | } else { 1021 | mBarColors = new int[]{a.getColor(R.styleable.CircleProgressView_cpv_barColor, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor, mBarColorStandard)}; 1022 | } 1023 | 1024 | if (a.hasValue(R.styleable.CircleProgressView_cpv_barStrokeCap)) { 1025 | setBarStrokeCap(StrokeCap.values()[a.getInt(R.styleable.CircleProgressView_cpv_barStrokeCap, 0)].paintCap); 1026 | } 1027 | 1028 | if (a.hasValue(R.styleable.CircleProgressView_cpv_barStartEndLineWidth) && a.hasValue(R.styleable.CircleProgressView_cpv_barStartEndLine)) { 1029 | setBarStartEndLine((int) a.getDimension(R.styleable.CircleProgressView_cpv_barStartEndLineWidth, 0), 1030 | BarStartEndLine.values()[a.getInt(R.styleable.CircleProgressView_cpv_barStartEndLine, 3)], 1031 | a.getColor(R.styleable.CircleProgressView_cpv_barStartEndLineColor, mBarStartEndLineColor), 1032 | a.getFloat(R.styleable.CircleProgressView_cpv_barStartEndLineSweep, mBarStartEndLineSweep)); 1033 | } 1034 | 1035 | setSpinBarColor(a.getColor(R.styleable.CircleProgressView_cpv_spinColor, mSpinnerColor)); 1036 | setSpinningBarLength(a.getFloat(R.styleable.CircleProgressView_cpv_spinBarLength, 1037 | mSpinningBarLengthOrig)); 1038 | 1039 | if (a.hasValue(R.styleable.CircleProgressView_cpv_textSize)) { 1040 | setTextSize((int) a.getDimension(R.styleable.CircleProgressView_cpv_textSize, mTextSize)); 1041 | } 1042 | if (a.hasValue(R.styleable.CircleProgressView_cpv_unitSize)) { 1043 | setUnitSize((int) a.getDimension(R.styleable.CircleProgressView_cpv_unitSize, mUnitTextSize)); 1044 | } 1045 | if (a.hasValue(R.styleable.CircleProgressView_cpv_textColor)) { 1046 | setTextColor(a.getColor(R.styleable.CircleProgressView_cpv_textColor, mTextColor)); 1047 | } 1048 | if (a.hasValue(R.styleable.CircleProgressView_cpv_unitColor)) { 1049 | setUnitColor(a.getColor(R.styleable.CircleProgressView_cpv_unitColor, mUnitColor)); 1050 | } 1051 | if (a.hasValue(R.styleable.CircleProgressView_cpv_autoTextColor)) { 1052 | setTextColorAuto(a.getBoolean(R.styleable.CircleProgressView_cpv_autoTextColor, mIsAutoColorEnabled)); 1053 | } 1054 | if (a.hasValue(R.styleable.CircleProgressView_cpv_autoTextSize)) { 1055 | setAutoTextSize(a.getBoolean(R.styleable.CircleProgressView_cpv_autoTextSize, mIsAutoTextSize)); 1056 | } 1057 | if (a.hasValue(R.styleable.CircleProgressView_cpv_textMode)) { 1058 | setTextMode(TextMode.values()[a.getInt(R.styleable.CircleProgressView_cpv_textMode, 0)]); 1059 | } 1060 | if (a.hasValue(R.styleable.CircleProgressView_cpv_unitPosition)) { 1061 | setUnitPosition(UnitPosition.values()[a.getInt(R.styleable.CircleProgressView_cpv_unitPosition, 3)]); 1062 | } 1063 | //if the mText is empty, show current percentage value 1064 | if (a.hasValue(R.styleable.CircleProgressView_cpv_text)) { 1065 | setText(a.getString(R.styleable.CircleProgressView_cpv_text)); 1066 | } 1067 | 1068 | setUnitToTextScale(a.getFloat(R.styleable.CircleProgressView_cpv_unitToTextScale, 1f)); 1069 | 1070 | setRimColor(a.getColor(R.styleable.CircleProgressView_cpv_rimColor, 1071 | mRimColor)); 1072 | 1073 | setFillCircleColor(a.getColor(R.styleable.CircleProgressView_cpv_fillColor, 1074 | mBackgroundCircleColor)); 1075 | 1076 | setOuterContourColor(a.getColor(R.styleable.CircleProgressView_cpv_outerContourColor, mOuterContourColor)); 1077 | setOuterContourSize(a.getDimension(R.styleable.CircleProgressView_cpv_outerContourSize, mOuterContourSize)); 1078 | 1079 | setInnerContourColor(a.getColor(R.styleable.CircleProgressView_cpv_innerContourColor, mInnerContourColor)); 1080 | setInnerContourSize(a.getDimension(R.styleable.CircleProgressView_cpv_innerContourSize, mInnerContourSize)); 1081 | 1082 | setMaxValue(a.getFloat(R.styleable.CircleProgressView_cpv_maxValue, mMaxValue)); 1083 | 1084 | setMinValueAllowed(a.getFloat(R.styleable.CircleProgressView_cpv_minValueAllowed, mMinValueAllowed)); 1085 | setMaxValueAllowed(a.getFloat(R.styleable.CircleProgressView_cpv_maxValueAllowed, mMaxValueAllowed)); 1086 | 1087 | setRoundToBlock(a.getBoolean(R.styleable.CircleProgressView_cpv_roundToBlock, mRoundToBlock)); 1088 | setRoundToWholeNumber(a.getBoolean(R.styleable.CircleProgressView_cpv_roundToWholeNumber, mRoundToWholeNumber)); 1089 | 1090 | setUnit(a.getString(R.styleable.CircleProgressView_cpv_unit)); 1091 | setUnitVisible(a.getBoolean(R.styleable.CircleProgressView_cpv_showUnit, mShowUnit)); 1092 | 1093 | setTextScale(a.getFloat(R.styleable.CircleProgressView_cpv_textScale, mTextScale)); 1094 | setUnitScale(a.getFloat(R.styleable.CircleProgressView_cpv_unitScale, mUnitScale)); 1095 | 1096 | setSeekModeEnabled(a.getBoolean(R.styleable.CircleProgressView_cpv_seekMode, mSeekModeEnabled)); 1097 | 1098 | setStartAngle(a.getInt(R.styleable.CircleProgressView_cpv_startAngle, mStartAngle)); 1099 | 1100 | setShowTextWhileSpinning(a.getBoolean(R.styleable.CircleProgressView_cpv_showTextInSpinningMode, mShowTextWhileSpinning)); 1101 | 1102 | if (a.hasValue(R.styleable.CircleProgressView_cpv_blockCount)) { 1103 | setBlockCount(a.getInt(R.styleable.CircleProgressView_cpv_blockCount, 1)); 1104 | setBlockScale(a.getFloat(R.styleable.CircleProgressView_cpv_blockScale, 0.9f)); 1105 | } 1106 | 1107 | if (a.hasValue(R.styleable.CircleProgressView_cpv_textTypeface)) { 1108 | try { 1109 | textTypeface = Typeface.createFromAsset(getContext().getAssets(), a.getString(R.styleable.CircleProgressView_cpv_textTypeface)); 1110 | } catch (Exception exception) { 1111 | // error while trying to inflate typeface (is the path set correctly?) 1112 | } 1113 | } 1114 | if (a.hasValue(R.styleable.CircleProgressView_cpv_unitTypeface)) { 1115 | try { 1116 | unitTextTypeface = Typeface.createFromAsset(getContext().getAssets(), a.getString(R.styleable.CircleProgressView_cpv_unitTypeface)); 1117 | } catch (Exception exception) { 1118 | // error while trying to inflate typeface (is the path set correctly?) 1119 | } 1120 | } 1121 | 1122 | if (a.hasValue(R.styleable.CircleProgressView_cpv_decimalFormat)) { 1123 | try { 1124 | String pattern = a.getString(R.styleable.CircleProgressView_cpv_decimalFormat); 1125 | if (pattern != null) { 1126 | decimalFormat = new DecimalFormat(pattern); 1127 | } 1128 | 1129 | } catch (Exception exception) { 1130 | Log.w(TAG, exception.getMessage()); 1131 | } 1132 | } 1133 | 1134 | // Recycle 1135 | a.recycle(); 1136 | } 1137 | 1138 | /* 1139 | * When this is called, make the view square. 1140 | * From: http://www.jayway.com/2012/12/12/creating-custom-android-views-part-4-measuring-and-how-to-force-a-view-to-be-square/ 1141 | * 1142 | */ 1143 | @Override 1144 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1145 | // The first thing that happen is that we call the superclass 1146 | // implementation of onMeasure. The reason for that is that measuring 1147 | // can be quite a complex process and calling the super method is a 1148 | // convenient way to get most of this complexity handled. 1149 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1150 | 1151 | // We can’t use getWidth() or getHeight() here. During the measuring 1152 | // pass the view has not gotten its final size yet (this happens first 1153 | // at the start of the layout pass) so we have to use getMeasuredWidth() 1154 | // and getMeasuredHeight(). 1155 | int size; 1156 | int width = getMeasuredWidth(); 1157 | int height = getMeasuredHeight(); 1158 | int widthWithoutPadding = width - getPaddingLeft() - getPaddingRight(); 1159 | int heightWithoutPadding = height - getPaddingTop() - getPaddingBottom(); 1160 | 1161 | 1162 | // Finally we have some simple logic that calculates the size of the view 1163 | // and calls setMeasuredDimension() to set that size. 1164 | // Before we compare the width and height of the view, we remove the padding, 1165 | // and when we set the dimension we add it back again. Now the actual content 1166 | // of the view will be square, but, depending on the padding, the total dimensions 1167 | // of the view might not be. 1168 | if (widthWithoutPadding > heightWithoutPadding) { 1169 | size = heightWithoutPadding; 1170 | } else { 1171 | size = widthWithoutPadding; 1172 | } 1173 | 1174 | // If you override onMeasure() you have to call setMeasuredDimension(). 1175 | // This is how you report back the measured size. If you don’t call 1176 | // setMeasuredDimension() the parent will throw an exception and your 1177 | // application will crash. 1178 | // We are calling the onMeasure() method of the superclass so we don’t 1179 | // actually need to call setMeasuredDimension() since that takes care 1180 | // of that. However, the purpose with overriding onMeasure() was to 1181 | // change the default behaviour and to do that we need to call 1182 | // setMeasuredDimension() with our own values. 1183 | setMeasuredDimension(size + getPaddingLeft() + getPaddingRight(), size + getPaddingTop() + getPaddingBottom()); 1184 | } 1185 | 1186 | /** 1187 | * Use onSizeChanged instead of onAttachedToWindow to get the dimensions of the view, 1188 | * because this method is called after measuring the dimensions of MATCH_PARENT and WRAP_CONTENT. 1189 | * Use this dimensions to setup the bounds and paints. 1190 | */ 1191 | @Override 1192 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1193 | super.onSizeChanged(w, h, oldw, oldh); 1194 | 1195 | // Share the dimensions 1196 | mLayoutWidth = w; 1197 | mLayoutHeight = h; 1198 | 1199 | setupBounds(); 1200 | setupBarPaint(); 1201 | 1202 | if (mClippingBitmap != null) { 1203 | mClippingBitmap = Bitmap.createScaledBitmap(mClippingBitmap, getWidth(), getHeight(), false); 1204 | } 1205 | 1206 | invalidate(); 1207 | } 1208 | 1209 | //---------------------------------- 1210 | // region helper 1211 | private float calcTextSizeForCircle(String _text, Paint _textPaint, RectF _circleBounds) { 1212 | 1213 | //get mActualTextBounds bounds 1214 | RectF innerCircleBounds = getInnerCircleRect(_circleBounds); 1215 | return calcTextSizeForRect(_text, _textPaint, innerCircleBounds); 1216 | 1217 | } 1218 | 1219 | private RectF getInnerCircleRect(RectF _circleBounds) { 1220 | 1221 | double circleWidth = +_circleBounds.width() - (Math.max(mBarWidth, mRimWidth)) - mOuterContourSize - mInnerContourSize; 1222 | double width = ((circleWidth / 2d) * Math.sqrt(2d)); 1223 | float widthDelta = (_circleBounds.width() - (float) width) / 2f; 1224 | 1225 | float scaleX = 1; 1226 | float scaleY = 1; 1227 | if (isUnitVisible()) { 1228 | switch (mUnitPosition) { 1229 | case TOP: 1230 | case BOTTOM: 1231 | scaleX = 1.1f; // scaleX square to rectangle, so the longer text with unit fits better 1232 | scaleY = 0.88f; 1233 | break; 1234 | case LEFT_TOP: 1235 | case RIGHT_TOP: 1236 | case LEFT_BOTTOM: 1237 | case RIGHT_BOTTOM: 1238 | scaleX = 0.77f; // scaleX square to rectangle, so the longer text with unit fits better 1239 | scaleY = 1.33f; 1240 | break; 1241 | } 1242 | 1243 | } 1244 | return new RectF(_circleBounds.left + (widthDelta * scaleX), _circleBounds.top + (widthDelta * scaleY), _circleBounds.right - (widthDelta * scaleX), _circleBounds.bottom - (widthDelta * scaleY)); 1245 | 1246 | } 1247 | 1248 | private void triggerOnProgressChanged(float value) { 1249 | if (onProgressChangedListener != null && value != previousProgressChangedValue) { 1250 | onProgressChangedListener.onProgressChanged(value); 1251 | previousProgressChangedValue = value; 1252 | } 1253 | } 1254 | 1255 | private void triggerReCalcTextSizesAndPositions() { 1256 | mTextLength = -1; 1257 | mOuterTextBounds = getInnerCircleRect(mCircleBounds); 1258 | invalidate(); 1259 | } 1260 | 1261 | private int calcTextColor(double value) { 1262 | if (mBarColors.length > 1) { 1263 | double percent = 1f / getMaxValue() * value; 1264 | int low = (int) Math.floor((mBarColors.length - 1) * percent); 1265 | int high = low + 1; 1266 | if (low < 0) { 1267 | low = 0; 1268 | high = 1; 1269 | } else if (high >= mBarColors.length) { 1270 | low = mBarColors.length - 2; 1271 | high = mBarColors.length - 1; 1272 | } 1273 | return ColorUtils.getRGBGradient(mBarColors[low], mBarColors[high], (float) (1 - (((mBarColors.length - 1) * percent) % 1d))); 1274 | } else if (mBarColors.length == 1) { 1275 | return mBarColors[0]; 1276 | } else { 1277 | return Color.BLACK; 1278 | } 1279 | } 1280 | 1281 | private void setTextSizeAndTextBoundsWithAutoTextSize(float unitGapWidthHalf, float unitWidth, float unitGapHeightHalf, float unitHeight, String text) { 1282 | RectF textRect = mOuterTextBounds; 1283 | 1284 | if (mShowUnit) { 1285 | 1286 | //shrink text Rect so that there is space for the unit 1287 | switch (mUnitPosition) { 1288 | 1289 | case TOP: 1290 | textRect = new RectF(mOuterTextBounds.left, mOuterTextBounds.top + unitHeight + unitGapHeightHalf, mOuterTextBounds.right, mOuterTextBounds.bottom); 1291 | break; 1292 | case BOTTOM: 1293 | textRect = new RectF(mOuterTextBounds.left, mOuterTextBounds.top, mOuterTextBounds.right, mOuterTextBounds.bottom - unitHeight - unitGapHeightHalf); 1294 | break; 1295 | case LEFT_TOP: 1296 | case LEFT_BOTTOM: 1297 | textRect = new RectF(mOuterTextBounds.left + unitWidth + unitGapWidthHalf, mOuterTextBounds.top, mOuterTextBounds.right, mOuterTextBounds.bottom); 1298 | break; 1299 | case RIGHT_TOP: 1300 | case RIGHT_BOTTOM: 1301 | default: 1302 | textRect = new RectF(mOuterTextBounds.left, mOuterTextBounds.top, mOuterTextBounds.right - unitWidth - unitGapWidthHalf, mOuterTextBounds.bottom); 1303 | break; 1304 | } 1305 | 1306 | } 1307 | 1308 | mTextPaint.setTextSize(calcTextSizeForRect(text, mTextPaint, textRect) * mTextScale); 1309 | mActualTextBounds = calcTextBounds(text, mTextPaint, textRect); // center text in text rect 1310 | } 1311 | 1312 | private void setTextSizeAndTextBoundsWithFixedTextSize(String text) { 1313 | mTextPaint.setTextSize(mTextSize); 1314 | mActualTextBounds = calcTextBounds(text, mTextPaint, mCircleBounds); //center text in circle 1315 | } 1316 | 1317 | private void setUnitTextBoundsAndSizeWithAutoTextSize(float unitGapWidthHalf, float unitWidth, float unitGapHeightHalf, float unitHeight) { 1318 | //calc the rectangle containing the unit text 1319 | switch (mUnitPosition) { 1320 | 1321 | case TOP: { 1322 | mUnitBounds = new RectF(mOuterTextBounds.left, mOuterTextBounds.top, mOuterTextBounds.right, mOuterTextBounds.top + unitHeight - unitGapHeightHalf); 1323 | break; 1324 | } 1325 | case BOTTOM: 1326 | mUnitBounds = new RectF(mOuterTextBounds.left, mOuterTextBounds.bottom - unitHeight + unitGapHeightHalf, mOuterTextBounds.right, mOuterTextBounds.bottom); 1327 | break; 1328 | case LEFT_TOP: 1329 | case LEFT_BOTTOM: { 1330 | mUnitBounds = new RectF(mOuterTextBounds.left, mOuterTextBounds.top, mOuterTextBounds.left + unitWidth - unitGapWidthHalf, mOuterTextBounds.top + unitHeight); 1331 | break; 1332 | } 1333 | case RIGHT_TOP: 1334 | case RIGHT_BOTTOM: 1335 | default: { 1336 | mUnitBounds = new RectF(mOuterTextBounds.right - unitWidth + unitGapWidthHalf, mOuterTextBounds.top, mOuterTextBounds.right, mOuterTextBounds.top + unitHeight); 1337 | } 1338 | break; 1339 | } 1340 | 1341 | mUnitTextPaint.setTextSize(calcTextSizeForRect(mUnit, mUnitTextPaint, mUnitBounds) * mUnitScale); 1342 | mUnitBounds = calcTextBounds(mUnit, mUnitTextPaint, mUnitBounds); // center text in rectangle and reuse it 1343 | 1344 | switch (mUnitPosition) { 1345 | 1346 | 1347 | case LEFT_TOP: 1348 | case RIGHT_TOP: { 1349 | //move unite to top of text 1350 | float dy = mActualTextBounds.top - mUnitBounds.top; 1351 | mUnitBounds.offset(0, dy); 1352 | break; 1353 | } 1354 | case LEFT_BOTTOM: 1355 | case RIGHT_BOTTOM: { 1356 | //move unite to bottom of text 1357 | float dy = mActualTextBounds.bottom - mUnitBounds.bottom; 1358 | mUnitBounds.offset(0, dy); 1359 | break; 1360 | } 1361 | } 1362 | } 1363 | 1364 | private void setUnitTextBoundsAndSizeWithFixedTextSize(float unitGapWidth, float unitGapHeight) { 1365 | mUnitTextPaint.setTextSize(mUnitTextSize); 1366 | mUnitBounds = calcTextBounds(mUnit, mUnitTextPaint, mOuterTextBounds); // center text in rectangle and reuse it 1367 | 1368 | switch (mUnitPosition) { 1369 | 1370 | case TOP: 1371 | mUnitBounds.offsetTo(mUnitBounds.left, mActualTextBounds.top - unitGapHeight - mUnitBounds.height()); 1372 | break; 1373 | case BOTTOM: 1374 | mUnitBounds.offsetTo(mUnitBounds.left, mActualTextBounds.bottom + unitGapHeight); 1375 | break; 1376 | case LEFT_TOP: 1377 | case LEFT_BOTTOM: 1378 | mUnitBounds.offsetTo(mActualTextBounds.left - unitGapWidth - mUnitBounds.width(), mUnitBounds.top); 1379 | break; 1380 | case RIGHT_TOP: 1381 | case RIGHT_BOTTOM: 1382 | default: 1383 | mUnitBounds.offsetTo(mActualTextBounds.right + unitGapWidth, mUnitBounds.top); 1384 | break; 1385 | } 1386 | 1387 | switch (mUnitPosition) { 1388 | case LEFT_TOP: 1389 | case RIGHT_TOP: { 1390 | //move unite to top of text 1391 | float dy = mActualTextBounds.top - mUnitBounds.top; 1392 | mUnitBounds.offset(0, dy); 1393 | break; 1394 | } 1395 | case LEFT_BOTTOM: 1396 | case RIGHT_BOTTOM: { 1397 | //move unite to bottom of text 1398 | float dy = mActualTextBounds.bottom - mUnitBounds.bottom; 1399 | mUnitBounds.offset(0, dy); 1400 | break; 1401 | } 1402 | } 1403 | } 1404 | 1405 | 1406 | /** 1407 | * Returns the bounding rectangle of the given _text, with the size and style defined in the _textPaint centered in the middle of the _textBounds 1408 | * 1409 | * @param _text The text. 1410 | * @param _textPaint The paint defining the text size and style. 1411 | * @param _textBounds The rect where the text will be centered. 1412 | * @return The bounding box of the text centered in the _textBounds. 1413 | */ 1414 | private RectF calcTextBounds(String _text, Paint _textPaint, RectF _textBounds) { 1415 | 1416 | Rect textBoundsTmp = new Rect(); 1417 | 1418 | //get current text bounds 1419 | _textPaint.getTextBounds(_text, 0, _text.length(), textBoundsTmp); 1420 | float width = textBoundsTmp.left + textBoundsTmp.width(); 1421 | float height = textBoundsTmp.bottom + textBoundsTmp.height() * 0.93f; // the height of calcTextBounds is a bit to high, therefore * 0.93 1422 | //center in circle 1423 | RectF textRect = new RectF(); 1424 | textRect.left = (_textBounds.left + ((_textBounds.width() - width) / 2)); 1425 | textRect.top = _textBounds.top + ((_textBounds.height() - height) / 2); 1426 | textRect.right = textRect.left + width; 1427 | textRect.bottom = textRect.top + height; 1428 | 1429 | 1430 | return textRect; 1431 | } 1432 | 1433 | //endregion helper 1434 | //---------------------------------- 1435 | 1436 | //---------------------------------- 1437 | //region Setting up stuff 1438 | 1439 | /** 1440 | * Set the bounds of the component 1441 | */ 1442 | private void setupBounds() { 1443 | // Width should equal to Height, find the min value to setup the circle 1444 | int minValue = Math.min(mLayoutWidth, mLayoutHeight); 1445 | 1446 | // Calc the Offset if needed 1447 | int xOffset = mLayoutWidth - minValue; 1448 | int yOffset = mLayoutHeight - minValue; 1449 | 1450 | // Add the offset 1451 | float paddingTop = this.getPaddingTop() + (yOffset / 2); 1452 | float paddingBottom = this.getPaddingBottom() + (yOffset / 2); 1453 | float paddingLeft = this.getPaddingLeft() + (xOffset / 2); 1454 | float paddingRight = this.getPaddingRight() + (xOffset / 2); 1455 | 1456 | int width = getWidth(); //this.getLayoutParams().width; 1457 | int height = getHeight(); //this.getLayoutParams().height; 1458 | 1459 | float circleWidthHalf = mBarWidth / 2f > mRimWidth / 2f + mOuterContourSize ? mBarWidth / 2f : mRimWidth / 2f + mOuterContourSize; 1460 | 1461 | mCircleBounds = new RectF(paddingLeft + circleWidthHalf, 1462 | paddingTop + circleWidthHalf, 1463 | width - paddingRight - circleWidthHalf, 1464 | height - paddingBottom - circleWidthHalf); 1465 | 1466 | 1467 | mInnerCircleBound = new RectF(paddingLeft + (mBarWidth), 1468 | paddingTop + (mBarWidth), 1469 | width - paddingRight - (mBarWidth), 1470 | height - paddingBottom - (mBarWidth)); 1471 | mOuterTextBounds = getInnerCircleRect(mCircleBounds); 1472 | mCircleInnerContour = new RectF(mCircleBounds.left + (mRimWidth / 2.0f) + (mInnerContourSize / 2.0f), mCircleBounds.top + (mRimWidth / 2.0f) + (mInnerContourSize / 2.0f), mCircleBounds.right - (mRimWidth / 2.0f) - (mInnerContourSize / 2.0f), mCircleBounds.bottom - (mRimWidth / 2.0f) - (mInnerContourSize / 2.0f)); 1473 | mCircleOuterContour = new RectF(mCircleBounds.left - (mRimWidth / 2.0f) - (mOuterContourSize / 2.0f), mCircleBounds.top - (mRimWidth / 2.0f) - (mOuterContourSize / 2.0f), mCircleBounds.right + (mRimWidth / 2.0f) + (mOuterContourSize / 2.0f), mCircleBounds.bottom + (mRimWidth / 2.0f) + (mOuterContourSize / 2.0f)); 1474 | 1475 | mCenter = new PointF(mCircleBounds.centerX(), mCircleBounds.centerY()); 1476 | } 1477 | 1478 | private void setupBarPaint() { 1479 | if (mBarColors.length > 1) { 1480 | mBarPaint.setShader(new SweepGradient(mCircleBounds.centerX(), mCircleBounds.centerY(), mBarColors, null)); 1481 | Matrix matrix = new Matrix(); 1482 | mBarPaint.getShader().getLocalMatrix(matrix); 1483 | 1484 | matrix.postTranslate(-mCircleBounds.centerX(), -mCircleBounds.centerY()); 1485 | matrix.postRotate(mStartAngle); 1486 | matrix.postTranslate(mCircleBounds.centerX(), mCircleBounds.centerY()); 1487 | mBarPaint.getShader().setLocalMatrix(matrix); 1488 | mBarPaint.setColor(mBarColors[0]); 1489 | } else if (mBarColors.length == 1) { 1490 | mBarPaint.setColor(mBarColors[0]); 1491 | mBarPaint.setShader(null); 1492 | } else { 1493 | mBarPaint.setColor(mBarColorStandard); 1494 | mBarPaint.setShader(null); 1495 | } 1496 | 1497 | mBarPaint.setAntiAlias(true); 1498 | mBarPaint.setStrokeCap(mBarStrokeCap); 1499 | mBarPaint.setStyle(Style.STROKE); 1500 | mBarPaint.setStrokeWidth(mBarWidth); 1501 | 1502 | if (mBarStrokeCap != Paint.Cap.BUTT) { 1503 | mShaderlessBarPaint = new Paint(mBarPaint); 1504 | mShaderlessBarPaint.setShader(null); 1505 | mShaderlessBarPaint.setColor(mBarColors[0]); 1506 | } 1507 | } 1508 | 1509 | 1510 | /** 1511 | * Setup all paints. 1512 | * Call only if changes to color or size properties are not visible. 1513 | */ 1514 | public void setupPaints() { 1515 | setupBarPaint(); 1516 | setupBarSpinnerPaint(); 1517 | setupOuterContourPaint(); 1518 | setupInnerContourPaint(); 1519 | setupUnitTextPaint(); 1520 | setupTextPaint(); 1521 | setupBackgroundCirclePaint(); 1522 | setupRimPaint(); 1523 | setupBarStartEndLinePaint(); 1524 | } 1525 | 1526 | private void setupBarStartEndLinePaint() { 1527 | mBarStartEndLinePaint.setColor(mBarStartEndLineColor); 1528 | mBarStartEndLinePaint.setAntiAlias(true); 1529 | mBarStartEndLinePaint.setStyle(Style.STROKE); 1530 | mBarStartEndLinePaint.setStrokeWidth(mBarStartEndLineWidth); 1531 | } 1532 | 1533 | private void setupOuterContourPaint() { 1534 | mOuterContourPaint.setColor(mOuterContourColor); 1535 | mOuterContourPaint.setAntiAlias(true); 1536 | mOuterContourPaint.setStyle(Style.STROKE); 1537 | mOuterContourPaint.setStrokeWidth(mOuterContourSize); 1538 | } 1539 | 1540 | private void setupInnerContourPaint() { 1541 | mInnerContourPaint.setColor(mInnerContourColor); 1542 | mInnerContourPaint.setAntiAlias(true); 1543 | mInnerContourPaint.setStyle(Style.STROKE); 1544 | mInnerContourPaint.setStrokeWidth(mInnerContourSize); 1545 | } 1546 | 1547 | private void setupUnitTextPaint() { 1548 | mUnitTextPaint.setStyle(Style.FILL); 1549 | mUnitTextPaint.setAntiAlias(true); 1550 | if (unitTextTypeface != null) { 1551 | mUnitTextPaint.setTypeface(unitTextTypeface); 1552 | } 1553 | } 1554 | 1555 | private void setupTextPaint() { 1556 | mTextPaint.setSubpixelText(true); 1557 | mTextPaint.setLinearText(true); 1558 | mTextPaint.setTypeface(Typeface.MONOSPACE); 1559 | mTextPaint.setColor(mTextColor); 1560 | mTextPaint.setStyle(Style.FILL); 1561 | mTextPaint.setAntiAlias(true); 1562 | mTextPaint.setTextSize(mTextSize); 1563 | if (textTypeface != null) { 1564 | mTextPaint.setTypeface(textTypeface); 1565 | } else { 1566 | mTextPaint.setTypeface(Typeface.MONOSPACE); 1567 | } 1568 | 1569 | } 1570 | 1571 | private void setupBackgroundCirclePaint() { 1572 | mBackgroundCirclePaint.setColor(mBackgroundCircleColor); 1573 | mBackgroundCirclePaint.setAntiAlias(true); 1574 | mBackgroundCirclePaint.setStyle(Style.FILL); 1575 | } 1576 | 1577 | private void setupRimPaint() { 1578 | mRimPaint.setColor(mRimColor); 1579 | mRimPaint.setAntiAlias(true); 1580 | mRimPaint.setStyle(Style.STROKE); 1581 | mRimPaint.setStrokeWidth(mRimWidth); 1582 | } 1583 | 1584 | private void setupBarSpinnerPaint() { 1585 | mBarSpinnerPaint.setAntiAlias(true); 1586 | mBarSpinnerPaint.setStrokeCap(mSpinnerStrokeCap); 1587 | mBarSpinnerPaint.setStyle(Style.STROKE); 1588 | mBarSpinnerPaint.setStrokeWidth(mBarWidth); 1589 | mBarSpinnerPaint.setColor(mSpinnerColor); 1590 | } 1591 | 1592 | //endregion Setting up stuff 1593 | //---------------------------------- 1594 | 1595 | //---------------------------------- 1596 | //region draw all the things 1597 | 1598 | protected void onDraw(Canvas canvas) { 1599 | super.onDraw(canvas); 1600 | 1601 | if (DEBUG) { 1602 | drawDebug(canvas); 1603 | } 1604 | 1605 | float degrees = (360f / mMaxValue * mCurrentValue); 1606 | 1607 | // Draw the background circle 1608 | if (mBackgroundCircleColor != 0) { 1609 | canvas.drawArc(mInnerCircleBound, 360, 360, false, mBackgroundCirclePaint); 1610 | } 1611 | //Draw the rim 1612 | if (mRimWidth > 0) { 1613 | if (!mShowBlock) { 1614 | canvas.drawArc(mCircleBounds, 360, 360, false, mRimPaint); 1615 | } else { 1616 | drawBlocks(canvas, mCircleBounds, mStartAngle, 360, false, mRimPaint); 1617 | } 1618 | } 1619 | 1620 | //Draw outer contour 1621 | if (mOuterContourSize > 0) { 1622 | canvas.drawArc(mCircleOuterContour, 360, 360, false, mOuterContourPaint); 1623 | } 1624 | 1625 | //Draw outer contour 1626 | if (mInnerContourSize > 0) { 1627 | canvas.drawArc(mCircleInnerContour, 360, 360, false, mInnerContourPaint); 1628 | } 1629 | 1630 | //Draw spinner 1631 | if (mAnimationState == AnimationState.SPINNING || mAnimationState == AnimationState.END_SPINNING) { 1632 | drawSpinner(canvas); 1633 | if (mShowTextWhileSpinning) { 1634 | drawTextWithUnit(canvas); 1635 | } 1636 | 1637 | } else if (mAnimationState == AnimationState.END_SPINNING_START_ANIMATING) { 1638 | //draw spinning arc 1639 | drawSpinner(canvas); 1640 | 1641 | if (mDrawBarWhileSpinning) { 1642 | drawBar(canvas, degrees); 1643 | drawTextWithUnit(canvas); 1644 | } else if (mShowTextWhileSpinning) { 1645 | drawTextWithUnit(canvas); 1646 | } 1647 | 1648 | } else { 1649 | drawBar(canvas, degrees); 1650 | drawTextWithUnit(canvas); 1651 | } 1652 | 1653 | if (mClippingBitmap != null) { 1654 | canvas.drawBitmap(mClippingBitmap, 0, 0, mMaskPaint); 1655 | } 1656 | 1657 | if (mBarStartEndLineWidth > 0 && mBarStartEndLine != BarStartEndLine.NONE) { 1658 | drawStartEndLine(canvas, degrees); 1659 | } 1660 | 1661 | } 1662 | 1663 | private void drawStartEndLine(Canvas _canvas, float _degrees) { 1664 | if (_degrees == 0f) 1665 | return; 1666 | 1667 | float startAngle = mDirection == Direction.CW ? mStartAngle : mStartAngle - _degrees; 1668 | 1669 | startAngle -= mBarStartEndLineSweep / 2f; 1670 | 1671 | if (mBarStartEndLine == BarStartEndLine.START || mBarStartEndLine == BarStartEndLine.BOTH) { 1672 | _canvas.drawArc(mCircleBounds, startAngle, mBarStartEndLineSweep, false, mBarStartEndLinePaint); 1673 | } 1674 | 1675 | if (mBarStartEndLine == BarStartEndLine.END || mBarStartEndLine == BarStartEndLine.BOTH) { 1676 | _canvas.drawArc(mCircleBounds, startAngle + _degrees, mBarStartEndLineSweep, false, mBarStartEndLinePaint); 1677 | } 1678 | } 1679 | 1680 | private void drawDebug(Canvas canvas) { 1681 | Paint innerRectPaint = new Paint(); 1682 | innerRectPaint.setColor(Color.YELLOW); 1683 | canvas.drawRect(mCircleBounds, innerRectPaint); 1684 | } 1685 | 1686 | private void drawBlocks(Canvas _canvas, RectF circleBounds, float startAngle, float _degrees, boolean userCenter, Paint paint) { 1687 | float tmpDegree = 0.0f; 1688 | while (tmpDegree < _degrees) { 1689 | _canvas.drawArc(circleBounds, startAngle + tmpDegree, Math.min(mBlockScaleDegree, _degrees - tmpDegree), userCenter, paint); 1690 | tmpDegree += mBlockDegree; 1691 | } 1692 | } 1693 | 1694 | private void drawSpinner(Canvas canvas) { 1695 | if (mSpinningBarLengthCurrent < 0) { 1696 | mSpinningBarLengthCurrent = 1; 1697 | } 1698 | 1699 | float startAngle; 1700 | if (mDirection == Direction.CW) { 1701 | startAngle = mStartAngle + mCurrentSpinnerDegreeValue - mSpinningBarLengthCurrent; 1702 | } else { 1703 | startAngle = mStartAngle - mCurrentSpinnerDegreeValue; 1704 | } 1705 | 1706 | canvas.drawArc(mCircleBounds, startAngle, mSpinningBarLengthCurrent, false, 1707 | mBarSpinnerPaint); 1708 | } 1709 | 1710 | private void drawTextWithUnit(Canvas canvas) { 1711 | 1712 | final float relativeGapHeight; 1713 | final float relativeGapWidth; 1714 | final float relativeHeight; 1715 | final float relativeWidth; 1716 | 1717 | switch (mUnitPosition) { 1718 | case TOP: 1719 | case BOTTOM: 1720 | relativeGapWidth = 0.05f; //gap size between text and unit 1721 | relativeGapHeight = 0.025f; //gap size between text and unit 1722 | relativeHeight = 0.25f * mRelativeUniteSize; 1723 | relativeWidth = 0.4f * mRelativeUniteSize; 1724 | break; 1725 | default: 1726 | case LEFT_TOP: 1727 | case RIGHT_TOP: 1728 | case LEFT_BOTTOM: 1729 | case RIGHT_BOTTOM: 1730 | relativeGapWidth = 0.05f; //gap size between text and unit 1731 | relativeGapHeight = 0.025f; //gap size between text and unit 1732 | relativeHeight = 0.55f * mRelativeUniteSize; 1733 | relativeWidth = 0.3f * mRelativeUniteSize; 1734 | break; 1735 | } 1736 | 1737 | float unitGapWidthHalf = mOuterTextBounds.width() * relativeGapWidth / 2f; 1738 | float unitWidth = (mOuterTextBounds.width() * relativeWidth); 1739 | 1740 | float unitGapHeightHalf = mOuterTextBounds.height() * relativeGapHeight / 2f; 1741 | float unitHeight = (mOuterTextBounds.height() * relativeHeight); 1742 | 1743 | 1744 | boolean update = false; 1745 | //Draw Text 1746 | if (mIsAutoColorEnabled) { 1747 | mTextPaint.setColor(calcTextColor(mCurrentValue)); 1748 | } 1749 | 1750 | //set text 1751 | String text; 1752 | switch (mTextMode) { 1753 | case TEXT: 1754 | default: 1755 | text = mText != null ? mText : ""; 1756 | break; 1757 | case PERCENT: 1758 | text = decimalFormat.format(100f / mMaxValue * mCurrentValue); 1759 | break; 1760 | case VALUE: 1761 | text = decimalFormat.format(mCurrentValue); 1762 | break; 1763 | } 1764 | 1765 | 1766 | // only re-calc position and size if string length changed 1767 | if (mTextLength != text.length()) { 1768 | 1769 | update = true; 1770 | mTextLength = text.length(); 1771 | if (mTextLength == 1) { 1772 | mOuterTextBounds = getInnerCircleRect(mCircleBounds); 1773 | mOuterTextBounds = new RectF(mOuterTextBounds.left + (mOuterTextBounds.width() * 0.1f), mOuterTextBounds.top, mOuterTextBounds.right - (mOuterTextBounds.width() * 0.1f), mOuterTextBounds.bottom); 1774 | } else { 1775 | mOuterTextBounds = getInnerCircleRect(mCircleBounds); 1776 | } 1777 | if (mIsAutoTextSize) { 1778 | setTextSizeAndTextBoundsWithAutoTextSize(unitGapWidthHalf, unitWidth, unitGapHeightHalf, unitHeight, text); 1779 | 1780 | } else { 1781 | setTextSizeAndTextBoundsWithFixedTextSize(text); 1782 | } 1783 | } 1784 | 1785 | if (DEBUG) { 1786 | Paint rectPaint = new Paint(); 1787 | rectPaint.setColor(Color.MAGENTA); 1788 | canvas.drawRect(mOuterTextBounds, rectPaint); 1789 | rectPaint.setColor(Color.GREEN); 1790 | canvas.drawRect(mActualTextBounds, rectPaint); 1791 | 1792 | } 1793 | 1794 | canvas.drawText(text, mActualTextBounds.left - (mTextPaint.getTextSize() * 0.02f), mActualTextBounds.bottom, mTextPaint); 1795 | 1796 | if (mShowUnit) { 1797 | 1798 | if (mIsAutoColorEnabled) { 1799 | mUnitTextPaint.setColor(calcTextColor(mCurrentValue)); 1800 | } 1801 | if (update) { 1802 | //calc unit text position 1803 | if (mIsAutoTextSize) { 1804 | setUnitTextBoundsAndSizeWithAutoTextSize(unitGapWidthHalf, unitWidth, unitGapHeightHalf, unitHeight); 1805 | 1806 | } else { 1807 | setUnitTextBoundsAndSizeWithFixedTextSize(unitGapWidthHalf * 2f, unitGapHeightHalf * 2f); 1808 | } 1809 | } 1810 | 1811 | if (DEBUG) { 1812 | Paint rectPaint = new Paint(); 1813 | rectPaint.setColor(Color.RED); 1814 | canvas.drawRect(mUnitBounds, rectPaint); 1815 | } 1816 | 1817 | canvas.drawText(mUnit, mUnitBounds.left - (mUnitTextPaint.getTextSize() * 0.02f), mUnitBounds.bottom, mUnitTextPaint); 1818 | } 1819 | } 1820 | 1821 | private void drawBar(Canvas _canvas, float _degrees) { 1822 | float startAngle = mDirection == Direction.CW ? mStartAngle : mStartAngle - _degrees; 1823 | if (!mShowBlock) { 1824 | 1825 | if (mBarStrokeCap != Paint.Cap.BUTT && _degrees > 0 && mBarColors.length > 1) { 1826 | if (_degrees > 180) { 1827 | _canvas.drawArc(mCircleBounds, startAngle, _degrees / 2, false, mBarPaint); 1828 | _canvas.drawArc(mCircleBounds, startAngle, 1, false, mShaderlessBarPaint); 1829 | _canvas.drawArc(mCircleBounds, startAngle + (_degrees / 2), _degrees / 2, false, mBarPaint); 1830 | } else { 1831 | _canvas.drawArc(mCircleBounds, startAngle, _degrees, false, mBarPaint); 1832 | _canvas.drawArc(mCircleBounds, startAngle, 1, false, mShaderlessBarPaint); 1833 | } 1834 | 1835 | } else { 1836 | _canvas.drawArc(mCircleBounds, startAngle, _degrees, false, mBarPaint); 1837 | } 1838 | } else { 1839 | drawBlocks(_canvas, mCircleBounds, startAngle, _degrees, false, mBarPaint); 1840 | } 1841 | } 1842 | 1843 | //endregion draw 1844 | //---------------------------------- 1845 | 1846 | 1847 | /** 1848 | * Turn off spinning mode 1849 | */ 1850 | public void stopSpinning() { 1851 | setSpin(false); 1852 | mAnimationHandler.sendEmptyMessage(AnimationMsg.STOP_SPINNING.ordinal()); 1853 | } 1854 | 1855 | /** 1856 | * Puts the view in spin mode 1857 | */ 1858 | public void spin() { 1859 | setSpin(true); 1860 | mAnimationHandler.sendEmptyMessage(AnimationMsg.START_SPINNING.ordinal()); 1861 | } 1862 | 1863 | private void setSpin(boolean spin) { 1864 | mSpin = spin; 1865 | } 1866 | 1867 | //---------------------------------- 1868 | //region touch input 1869 | @Override 1870 | public boolean onTouchEvent(@NonNull MotionEvent event) { 1871 | 1872 | if (mSeekModeEnabled == false) { 1873 | return super.onTouchEvent(event); 1874 | } 1875 | 1876 | switch (event.getActionMasked()) { 1877 | case MotionEvent.ACTION_DOWN: 1878 | case MotionEvent.ACTION_UP: { 1879 | mTouchEventCount = 0; 1880 | PointF point = new PointF(event.getX(), event.getY()); 1881 | float angle = getRotationAngleForPointFromStart(point); 1882 | setValueAnimated(mMaxValue / 360f * angle, 800); 1883 | return true; 1884 | } 1885 | case MotionEvent.ACTION_MOVE: { 1886 | mTouchEventCount++; 1887 | if (mTouchEventCount > 5) { //touch/move guard 1888 | PointF point = new PointF(event.getX(), event.getY()); 1889 | float angle = getRotationAngleForPointFromStart(point); 1890 | setValue(mMaxValue / 360f * angle); 1891 | return true; 1892 | } else { 1893 | return false; 1894 | } 1895 | 1896 | } 1897 | case MotionEvent.ACTION_CANCEL: 1898 | mTouchEventCount = 0; 1899 | return false; 1900 | } 1901 | 1902 | 1903 | return super.onTouchEvent(event); 1904 | } 1905 | 1906 | private float getRotationAngleForPointFromStart(PointF point) { 1907 | long angle = Math.round(calcRotationAngleInDegrees(mCenter, point)); 1908 | float fromStart = mDirection == Direction.CW ? angle - mStartAngle : mStartAngle - angle; 1909 | return normalizeAngle(fromStart); 1910 | } 1911 | 1912 | 1913 | //endregion touch input 1914 | //---------------------------------- 1915 | 1916 | 1917 | //----------------------------------- 1918 | //region listener for progress change 1919 | 1920 | 1921 | public interface OnProgressChangedListener { 1922 | void onProgressChanged(float value); 1923 | } 1924 | 1925 | //endregion listener for progress change 1926 | //-------------------------------------- 1927 | 1928 | } 1929 | 1930 | 1931 | -------------------------------------------------------------------------------- /CircleProgressView/src/main/java/at/grabner/circleprogress/ColorUtils.java: -------------------------------------------------------------------------------- 1 | package at.grabner.circleprogress; 2 | 3 | import android.graphics.Color; 4 | import androidx.annotation.ColorInt; 5 | 6 | /** 7 | * Created by Jakob on 05.09.2015. 8 | */ 9 | public class ColorUtils { 10 | 11 | public static int getRGBGradient(@ColorInt int startColor, @ColorInt int endColor, float proportion) { 12 | 13 | int[] rgb = new int[3]; 14 | rgb[0] = interpolate(Color.red(startColor), Color.red(endColor), proportion); 15 | rgb[1] = interpolate(Color.green(startColor), Color.green(endColor), proportion); 16 | rgb[2] = interpolate(Color.blue(startColor), Color.blue(endColor), proportion); 17 | return Color.argb(255, rgb[0], rgb[1], rgb[2]); 18 | } 19 | 20 | 21 | private static int interpolate(float a, float b, float proportion) { 22 | return Math.round((a * (proportion)) + (b * (1 - proportion))); 23 | } 24 | 25 | 26 | // not finished 27 | // public static @ColorInt int getHSVGradient(@ColorInt int startColor,@ColorInt int endColor, float proportion, HSVColorDirection _direction) { 28 | // float[] startHSV = new float[3]; 29 | // float[] endHSV = new float[3]; 30 | // Color.colorToHSV(startColor, startHSV); 31 | // Color.colorToHSV(endColor, endHSV); 32 | // 33 | // float brightness = (startHSV[2] + endHSV[2]) / 2; 34 | // float saturation = (startHSV[1] + endHSV[1]) / 2; 35 | // 36 | // // determine clockwise and counter-clockwise distance between hues 37 | // float distCCW = (startHSV[0] >= endHSV[0]) ? 360 - startHSV[0] - endHSV[0] : startHSV[0] - endHSV[0]; 38 | // float distCW = (startHSV[0] >= endHSV[0]) ? endHSV[0] - startHSV[0] : 360 - endHSV[0] - startHSV[0]; 39 | // float hue = 0; 40 | // switch (_direction) { 41 | // 42 | // case ClockWise: 43 | // hue = startHSV[0] + (distCW * proportion) % 360; 44 | // break; 45 | // case CounterClockWise: 46 | // hue = startHSV[0] + (distCCW * proportion) % 360; 47 | // break; 48 | // case Shortest: 49 | // break; 50 | // case Longest: 51 | // break; 52 | // } 53 | // 54 | // // interpolate h 55 | // float hue = (float) ((distCW <= distCCW) ? startHSV[0] + (distCW * proportion) : startHSV[0] - (distCCW * proportion)); 56 | // //reuse array 57 | // endHSV[0] = hue; 58 | // endHSV[1] = saturation; 59 | // endHSV[2] = brightness; 60 | // return Color.HSVToColor(endHSV); 61 | // 62 | // } 63 | // 64 | // enum HSVColorDirection{ 65 | // ClockWise, 66 | // CounterClockWise, 67 | // Shortest, 68 | // Longest 69 | // } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /CircleProgressView/src/main/java/at/grabner/circleprogress/Direction.java: -------------------------------------------------------------------------------- 1 | package at.grabner.circleprogress; 2 | 3 | public enum Direction { 4 | /** 5 | * The bar grows clockwise from the start angle, and the spinner rotates clockwise. 6 | */ 7 | CW, 8 | 9 | /** 10 | * The bar grows counter-clockwise from the start angle, and the spinner rotates 11 | * counter-clockwise. 12 | */ 13 | CCW 14 | } 15 | -------------------------------------------------------------------------------- /CircleProgressView/src/main/java/at/grabner/circleprogress/StrokeCap.java: -------------------------------------------------------------------------------- 1 | package at.grabner.circleprogress; 2 | 3 | import android.graphics.Paint; 4 | 5 | public enum StrokeCap { 6 | /** 7 | * The stroke ends with the path, and does not project beyond it. 8 | */ 9 | BUTT(Paint.Cap.BUTT), 10 | /** 11 | * The stroke projects out as a semicircle, with the center at the 12 | * end of the path. 13 | */ 14 | ROUND(Paint.Cap.ROUND), 15 | /** 16 | * The stroke projects out as a square, with the center at the end 17 | * of the path. 18 | */ 19 | SQUARE(Paint.Cap.SQUARE); 20 | 21 | final Paint.Cap paintCap; 22 | 23 | StrokeCap(Paint.Cap paintCap) { 24 | this.paintCap = paintCap; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CircleProgressView/src/main/java/at/grabner/circleprogress/TextMode.java: -------------------------------------------------------------------------------- 1 | package at.grabner.circleprogress; 2 | 3 | public enum TextMode { 4 | /** 5 | * Show specified text 6 | */ 7 | TEXT, 8 | /** 9 | * Show percent of current value from max value 10 | */ 11 | PERCENT, 12 | /** 13 | * Show current value 14 | */ 15 | VALUE 16 | } 17 | -------------------------------------------------------------------------------- /CircleProgressView/src/main/java/at/grabner/circleprogress/UnitPosition.java: -------------------------------------------------------------------------------- 1 | package at.grabner.circleprogress; 2 | 3 | /** 4 | * Created by Jakob on 20.11.2015. 5 | */ 6 | public enum UnitPosition { 7 | TOP, 8 | BOTTOM, 9 | LEFT_TOP, 10 | RIGHT_TOP, 11 | LEFT_BOTTOM, 12 | RIGHT_BOTTOM 13 | } 14 | -------------------------------------------------------------------------------- /CircleProgressView/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /ExampleApp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /ExampleApp/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | repositories { 6 | maven { url "https://jitpack.io" } 7 | } 8 | defaultConfig { 9 | applicationId "at.grabner.example.circleprogressview" 10 | minSdkVersion 21 11 | targetSdkVersion 28 12 | versionCode 3 13 | versionName "1.2" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | debug { 21 | minifyEnabled false 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(include: ['*.jar'], dir: 'libs') 28 | implementation project(':CircleProgressView') 29 | // implementation 'com.github.jakob-grabner:Circle-Progress-View:v1+' 30 | implementation 'androidx.appcompat:appcompat:1.0.2' 31 | implementation 'com.google.android.material:material:1.1.0-alpha09' 32 | } 33 | -------------------------------------------------------------------------------- /ExampleApp/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:\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 | -------------------------------------------------------------------------------- /ExampleApp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ExampleApp/src/main/assets/fonts/ANDROID_ROBOT.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakob-grabner/Circle-Progress-View/306648f74b0cd4194536499e5ed6ff70d0408e43/ExampleApp/src/main/assets/fonts/ANDROID_ROBOT.ttf -------------------------------------------------------------------------------- /ExampleApp/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakob-grabner/Circle-Progress-View/306648f74b0cd4194536499e5ed6ff70d0408e43/ExampleApp/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /ExampleApp/src/main/java/at/grabner/example/circleprogressview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package at.grabner.example.circleprogressview; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | import androidx.appcompat.app.AppCompatActivity; 6 | import android.util.Log; 7 | import android.view.View; 8 | import android.widget.AdapterView; 9 | import android.widget.ArrayAdapter; 10 | import android.widget.CompoundButton; 11 | import android.widget.SeekBar; 12 | import android.widget.Spinner; 13 | import android.widget.Switch; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import at.grabner.circleprogress.AnimationState; 19 | import at.grabner.circleprogress.AnimationStateChangedListener; 20 | import at.grabner.circleprogress.CircleProgressView; 21 | import at.grabner.circleprogress.TextMode; 22 | import at.grabner.circleprogress.UnitPosition; 23 | 24 | 25 | public class MainActivity extends AppCompatActivity { 26 | 27 | /** 28 | * The log tag. 29 | */ 30 | private final static String TAG = "MainActivity"; 31 | 32 | CircleProgressView mCircleView; 33 | Switch mSwitchSpin; 34 | Switch mSwitchShowUnit; 35 | SeekBar mSeekBar; 36 | SeekBar mSeekBarSpinnerLength; 37 | Boolean mShowUnit = true; 38 | Spinner mSpinner; 39 | 40 | @Override 41 | protected void onCreate(Bundle savedInstanceState) { 42 | 43 | super.onCreate(savedInstanceState); 44 | setContentView(R.layout.activity_main); 45 | 46 | mCircleView = (CircleProgressView) findViewById(R.id.circleView); 47 | mCircleView.setOnProgressChangedListener(new CircleProgressView.OnProgressChangedListener() { 48 | @Override 49 | public void onProgressChanged(float value) { 50 | Log.d(TAG, "Progress Changed: " + value); 51 | } 52 | }); 53 | 54 | //value setting 55 | // mCircleView.setMaxValue(100); 56 | // mCircleView.setValue(0); 57 | // mCircleView.setValueAnimated(24); 58 | 59 | //growing/rotating counter-clockwise 60 | // mCircleView.setDirection(Direction.CCW) 61 | 62 | // //show unit 63 | // mCircleView.setUnit("%"); 64 | // mCircleView.setUnitVisible(mShowUnit); 65 | // 66 | // //text sizes 67 | // mCircleView.setTextSize(50); // text size set, auto text size off 68 | // mCircleView.setUnitSize(40); // if i set the text size i also have to set the unit size 69 | // mCircleView.setAutoTextSize(true); // enable auto text size, previous values are overwritten 70 | // //if you want the calculated text sizes to be bigger/smaller you can do so via 71 | // mCircleView.setUnitScale(0.9f); 72 | // mCircleView.setTextScale(0.9f); 73 | // 74 | //// //custom typeface 75 | //// Typeface font = Typeface.createFromAsset(getAssets(), "fonts/ANDROID_ROBOT.ttf"); 76 | //// mCircleView.setTextTypeface(font); 77 | //// mCircleView.setUnitTextTypeface(font); 78 | // 79 | // 80 | // //color 81 | // //you can use a gradient 82 | // mCircleView.setBarColor(getResources().getColor(R.color.primary), getResources().getColor(R.color.accent)); 83 | // 84 | // //colors of text and unit can be set via 85 | // mCircleView.setTextColor(Color.RED); 86 | // mCircleView.setTextColor(Color.BLUE); 87 | // //or to use the same color as in the gradient 88 | // mCircleView.setTextColorAuto(true); //previous set values are ignored 89 | // 90 | // //text mode 91 | // mCircleView.setText("Text"); //shows the given text in the circle view 92 | // mCircleView.setTextMode(TextMode.TEXT); // Set text mode to text to show text 93 | // 94 | // //in the following text modes, the text is ignored 95 | // mCircleView.setTextMode(TextMode.VALUE); // Shows the current value 96 | // mCircleView.setTextMode(TextMode.PERCENT); // Shows current percent of the current value from the max value 97 | 98 | //spinning 99 | // mCircleView.spin(); // start spinning 100 | // mCircleView.stopSpinning(); // stops spinning. Spinner gets shorter until it disappears. 101 | // mCircleView.setValueAnimated(24); // stops spinning. Spinner spins until on top. Then fills to set value. 102 | 103 | 104 | //animation callbacks 105 | 106 | //this example shows how to show a loading text if it is in spinning mode, and the current percent value otherwise. 107 | mCircleView.setShowTextWhileSpinning(true); // Show/hide text in spinning mode 108 | mCircleView.setText("Loading..."); 109 | mCircleView.setOnAnimationStateChangedListener( 110 | new AnimationStateChangedListener() { 111 | @Override 112 | public void onAnimationStateChanged(AnimationState _animationState) { 113 | switch (_animationState) { 114 | case IDLE: 115 | case ANIMATING: 116 | case START_ANIMATING_AFTER_SPINNING: 117 | mCircleView.setTextMode(TextMode.PERCENT); // show percent if not spinning 118 | mCircleView.setUnitVisible(mShowUnit); 119 | break; 120 | case SPINNING: 121 | mCircleView.setTextMode(TextMode.TEXT); // show text while spinning 122 | mCircleView.setUnitVisible(false); 123 | case END_SPINNING: 124 | break; 125 | case END_SPINNING_START_ANIMATING: 126 | break; 127 | 128 | } 129 | } 130 | } 131 | ); 132 | 133 | 134 | // region setup other ui elements 135 | //Setup Switch 136 | mSwitchSpin = (Switch) findViewById(R.id.switch1); 137 | mSwitchSpin.setOnCheckedChangeListener( 138 | new CompoundButton.OnCheckedChangeListener() { 139 | @Override 140 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 141 | if (isChecked) { 142 | mCircleView.spin(); 143 | } else { 144 | mCircleView.stopSpinning(); 145 | } 146 | } 147 | } 148 | 149 | ); 150 | 151 | mSwitchShowUnit = (Switch) findViewById(R.id.switch2); 152 | mSwitchShowUnit.setChecked(mShowUnit); 153 | mSwitchShowUnit.setOnCheckedChangeListener( 154 | new CompoundButton.OnCheckedChangeListener() { 155 | @Override 156 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 157 | mCircleView.setUnitVisible(isChecked); 158 | mShowUnit = isChecked; 159 | } 160 | } 161 | 162 | ); 163 | 164 | //Setup SeekBar 165 | mSeekBar = (SeekBar) findViewById(R.id.seekBar); 166 | 167 | mSeekBar.setMax(100); 168 | mSeekBar.setOnSeekBarChangeListener( 169 | new SeekBar.OnSeekBarChangeListener() { 170 | @Override 171 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 172 | 173 | } 174 | 175 | @Override 176 | public void onStartTrackingTouch(SeekBar seekBar) { 177 | 178 | } 179 | 180 | @Override 181 | public void onStopTrackingTouch(SeekBar seekBar) { 182 | mCircleView.setValueAnimated(seekBar.getProgress(), 1500); 183 | mSwitchSpin.setChecked(false); 184 | } 185 | } 186 | ); 187 | 188 | mSeekBarSpinnerLength = (SeekBar) findViewById(R.id.seekBar2); 189 | mSeekBarSpinnerLength.setMax(360); 190 | mSeekBarSpinnerLength.setOnSeekBarChangeListener( 191 | new SeekBar.OnSeekBarChangeListener() { 192 | @Override 193 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 194 | } 195 | 196 | @Override 197 | public void onStartTrackingTouch(SeekBar seekBar) { 198 | } 199 | 200 | @Override 201 | public void onStopTrackingTouch(SeekBar seekBar) { 202 | mCircleView.setSpinningBarLength(seekBar.getProgress()); 203 | } 204 | }); 205 | 206 | mSpinner = (Spinner) findViewById(R.id.spinner); 207 | List list = new ArrayList(); 208 | list.add("Left Top"); 209 | list.add("Left Bottom"); 210 | list.add("Right Top"); 211 | list.add("Right Bottom"); 212 | list.add("Top"); 213 | list.add("Bottom"); 214 | ArrayAdapter dataAdapter = new ArrayAdapter(this, 215 | android.R.layout.simple_spinner_item, list); 216 | mSpinner.setAdapter(dataAdapter); 217 | mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 218 | @Override 219 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 220 | switch (position) { 221 | case 0: 222 | mCircleView.setUnitPosition(UnitPosition.LEFT_TOP); 223 | break; 224 | case 1: 225 | mCircleView.setUnitPosition(UnitPosition.LEFT_BOTTOM); 226 | break; 227 | case 2: 228 | mCircleView.setUnitPosition(UnitPosition.RIGHT_TOP); 229 | break; 230 | case 3: 231 | mCircleView.setUnitPosition(UnitPosition.RIGHT_BOTTOM); 232 | break; 233 | case 4: 234 | mCircleView.setUnitPosition(UnitPosition.TOP); 235 | break; 236 | case 5: 237 | mCircleView.setUnitPosition(UnitPosition.BOTTOM); 238 | break; 239 | 240 | } 241 | } 242 | 243 | @Override 244 | public void onNothingSelected(AdapterView parent) { 245 | 246 | } 247 | }); 248 | mSpinner.setSelection(2); 249 | //endregion 250 | 251 | // new LongOperation().execute(); 252 | 253 | } 254 | 255 | 256 | @Override 257 | protected void onStart() { 258 | super.onStart(); 259 | } 260 | 261 | private class LongOperation extends AsyncTask { 262 | @Override 263 | protected Void doInBackground(Void... params) { 264 | 265 | MainActivity.this.runOnUiThread(new Runnable() { 266 | @Override 267 | public void run() { 268 | mCircleView.setValue(0); 269 | mCircleView.spin(); 270 | } 271 | }); 272 | 273 | try { 274 | Thread.sleep(2000); 275 | } catch (InterruptedException e) { 276 | e.printStackTrace(); 277 | } 278 | 279 | 280 | return null; 281 | } 282 | 283 | @Override 284 | protected void onPostExecute(Void aVoid) { 285 | mCircleView.setValueAnimated(42); 286 | } 287 | } 288 | } 289 | 290 | 291 | 292 | -------------------------------------------------------------------------------- /ExampleApp/src/main/res/drawable/mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakob-grabner/Circle-Progress-View/306648f74b0cd4194536499e5ed6ff70d0408e43/ExampleApp/src/main/res/drawable/mask.png -------------------------------------------------------------------------------- /ExampleApp/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 37 | 38 | 39 | 45 | 46 | 51 | 52 | 59 | 60 | 66 | 67 | 74 | 75 | 81 | 82 | 88 | 89 | 96 | 97 | 103 | 104 | 105 | 106 | 107 | 114 | 115 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /ExampleApp/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakob-grabner/Circle-Progress-View/306648f74b0cd4194536499e5ed6ff70d0408e43/ExampleApp/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /ExampleApp/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakob-grabner/Circle-Progress-View/306648f74b0cd4194536499e5ed6ff70d0408e43/ExampleApp/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /ExampleApp/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakob-grabner/Circle-Progress-View/306648f74b0cd4194536499e5ed6ff70d0408e43/ExampleApp/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ExampleApp/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakob-grabner/Circle-Progress-View/306648f74b0cd4194536499e5ed6ff70d0408e43/ExampleApp/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ExampleApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakob-grabner/Circle-Progress-View/306648f74b0cd4194536499e5ed6ff70d0408e43/ExampleApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ExampleApp/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | > 20 | 21 | 27 | -------------------------------------------------------------------------------- /ExampleApp/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /ExampleApp/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 7 | -------------------------------------------------------------------------------- /ExampleApp/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Circle-Progress-View 3 | 4 | -------------------------------------------------------------------------------- /ExampleApp/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #2196F3 5 | #1976D2 6 | #BBDEFB 7 | #455A64 8 | #212121 9 | #727272 10 | #FFFFFF 11 | #B6B6B6 12 | 13 | 14 | 15 | 23 | 24 | 28 | 29 |