├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── circularprogressview ├── .gitignore ├── build.gradle ├── gradle-mvn-push.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── rahatarmanahmed │ │ └── cpv │ │ ├── CircularProgressView.java │ │ ├── CircularProgressViewAdapter.java │ │ └── CircularProgressViewListener.java │ └── res │ └── values │ ├── attrs.xml │ ├── strings.xml │ └── values.xml ├── example ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── rahatarmanahmed │ │ └── cpv │ │ └── example │ │ └── MainActivity.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── layout │ └── activity_main.xml │ ├── menu │ └── menu_main.xml │ ├── values-v21 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── gif ├── sampleDeterminate.gif └── sampleIndeterminate.gif ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Android ### 4 | # Built application files 5 | *.apk 6 | *.ap_ 7 | 8 | # Files for the Dalvik VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | /*/build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | ### Android Patch ### 33 | gen-external-apklibs 34 | 35 | 36 | ### Intellij ### 37 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 38 | 39 | *.iml 40 | 41 | ## Directory-based project format: 42 | .idea/ 43 | # if you remove the above rule, at least ignore the following: 44 | 45 | # User-specific stuff: 46 | # .idea/workspace.xml 47 | # .idea/tasks.xml 48 | # .idea/dictionaries 49 | 50 | # Sensitive or high-churn files: 51 | # .idea/dataSources.ids 52 | # .idea/dataSources.xml 53 | # .idea/sqlDataSources.xml 54 | # .idea/dynamic.xml 55 | # .idea/uiDesigner.xml 56 | 57 | # Gradle: 58 | # .idea/gradle.xml 59 | # .idea/libraries 60 | 61 | # Mongo Explorer plugin: 62 | # .idea/mongoSettings.xml 63 | 64 | ## File-based project format: 65 | *.ipr 66 | *.iws 67 | 68 | ## Plugin-specific files: 69 | 70 | # IntelliJ 71 | /out/ 72 | 73 | # mpeltonen/sbt-idea plugin 74 | .idea_modules/ 75 | 76 | # JIRA plugin 77 | atlassian-ide-plugin.xml 78 | 79 | # Crashlytics plugin (for Android Studio and IntelliJ) 80 | com_crashlytics_export_strings.xml 81 | crashlytics.properties 82 | crashlytics-build.properties 83 | 84 | 85 | ### Gradle ### 86 | .gradle 87 | 88 | # Ignore Gradle GUI config 89 | gradle-app.setting 90 | 91 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 92 | !gradle-wrapper.jar 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rahat Ahmed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Material CircularProgressView 2 | 3 | | Indeterminate | Determinate | 4 | |:-------------:|:-----------:| 5 | | ![Sample Indeterminate GIF](https://raw.github.com/rahatarmanahmed/CircularProgressView/master/gif/sampleIndeterminate.gif) | ![Sample Determinate GIF](https://raw.github.com/rahatarmanahmed/CircularProgressView/master/gif/sampleDeterminate.gif) | 6 | 7 | ## Description 8 | 9 | This CircularProgressView is a (surprisingly) circular progress bar Android View that is designed to imitate the Material versions of ProgressBar. These versions can be seen on [this page](http://www.google.com/design/spec/components/progress-activity.html#progress-activity-types-of-indicators) of the Material design spec under Circular indicators. 10 | 11 | ## Usage 12 | 13 | To use CircularProgressView you must add it as a dependency in your Gradle build: 14 | 15 | ```groovy 16 | dependencies { 17 | compile 'com.github.rahatarmanahmed:circularprogressview:2.5.0' 18 | } 19 | ``` 20 | 21 | Then add the view to your layout: 22 | 23 | ```xml 24 | 31 | ``` 32 | 33 | That's all you need! If you don't want the CircularProgressView to automatically start animating, omit the app:cpv_animAutostart option and start it manually yourself: 34 | 35 | ```java 36 | CircularProgressView progressView = (CircularProgressView) findViewById(R.id.progress_view); 37 | progressView.startAnimation(); 38 | ``` 39 | 40 | ## XML attributes 41 | 42 | | Name | Type | Default | Description | 43 | |:----:|:----:|:-------:|:-----------:| 44 | | cpv_progress | float | 0 | The current progress of the progress bar. | 45 | | cpv_maxProgress | float | 100 | The maximum progress of the progress bar; what's considered as 100% of the bar. | 46 | | cpv_thickness | dimension | 4px | The thickness of the progress bar. | 47 | | cpv_color | color | Theme's accent color. If not available, Material Blue 500 (#2196F3) | The color of the progress bar. | 48 | | cpv_indeterminate | boolean | false | Whether this progress bar is indeterminate or not. If indeterminate, the progress set on this view will not have any effect. | 49 | | cpv_animDuration | integer | 4000 | The duration of the indeterminate progress bar animation in milliseconds. It is the duration of all "steps" of the indeterminate animation. (Indeterminate only) | 50 | | cpv_animSwoopDuration | integer | 5000 | The duration of the initial swoop of the determinate animation. (Determinate only) | 51 | | cpv_animSyncDuration | integer | 500 | The duration of the determinate progress update animation. When you use `setUpdate(int)`, this is how long it takes for the view to finish animating to that progress. (Determinate only) | 52 | | cpv_animSteps | integer | 3 | The number of "steps" in the indeterminate animation (how many times it does the loopy thing before returning to its original position). It is recommended to use an odd number, as even numbers of steps look the same after half the number of steps. | 53 | | cpv_animAutostart | boolean | false | Whether this progress bar should automatically start animating once it is initialized. | 54 | | cpv_startAngle | float | 0 | The starting angle for progress bar. (Determinate only) | 55 | 56 | ## Public Methods 57 | 58 | | Name | Description | 59 | |:----:|:-----------:| 60 | | isIndeterminate() | Returns true if the progress bar is indeterminate, false if determinate. | 61 | | setIndeterminate(boolean) | Set whether this progress bar is indeterminate or not. Will reset the animation if the value changes | 62 | | getThickness() | Gets the thickness of the progress bar. | 63 | | setThickness(int) | Sets thickness of the progress bar. | 64 | | getColor() | Gets the color of the progress bar. | 65 | | setColor(int) | Sets the color of the progress bar. | 66 | | getMaxProgress() | Gets the maximum progress of the progress bar. | 67 | | setMaxProgress(float) | Sets the maximum progress of the progress bar. | 68 | | getProgress() | Gets the current progress of the progress bar. | 69 | | setProgress(float) | Sets the current progress of the progress bar. (Will linearly animate the update.) | 70 | | startAnimation() | Starts the animation of the progress bar. (Alias of resetAnimation().) | 71 | | resetAnimation() | Resets the animation of the progress bar. | 72 | | stopAnimation() | Stops the animation of the progress bar. | 73 | | addListener(CircularProgressViewListener) | Registers a CircularProgressViewListener with this view. | 74 | | removeListener(CircularProgressViewListener) | Unregisters a CircularProgressViewListener with this view. | 75 | 76 | ## Listener Events. 77 | 78 | A [`CircularProgressViewListener`](circularprogressview/src/main/java/com/github/rahatarmanahmed/cpv/CircularProgressViewListener.java) class is available for listening to some events (as well as a [`CircularProgressViewAdapter`](circularprogressview/src/main/java/com/github/rahatarmanahmed/cpv/CircularProgressViewAdapter.java)). 79 | 80 | | Event | Description | 81 | |:----:|:-----------:| 82 | | onProgressUpdate(float) | Called when setProgress is called. (Determinate only) | 83 | | onProgressUpdateEnd(float) | Called when this view finishes animating to the updated progress. (Determinate only) | 84 | | onAnimationReset() | Called when resetAnimation() is called. | 85 | | onModeChange(boolean) | Called when you switch between indeterminate and determinate modes. | 86 | 87 | 88 | ## Known Issues 89 | 90 | ### CircularProgressView flickers when phone is in battery saving mode 91 | This happens because battery saving mode automatically ends all Animators, but the ones in CPV run in an endless loop. The best way to work around this right now is to use the native ProgressBar for API >21, since that is when the battery saver mode was introduced. See [this](https://github.com/rahatarmanahmed/CircularProgressView/issues/16#issuecomment-109169040) issue comment on how to accomplish this. 92 | 93 | 94 | ## Changelog 95 | 96 | ### v2.5.0 97 | 98 | * Added `stopAnimation()` method 99 | * Fixed view animating while not visible. Setting visibility to GONE or INVISIBLE will stop the animation. Setting to VISIBLE will restart it. 100 | 101 | ### v2.4.0 102 | 103 | * Added cpv_startAngle attribute 104 | 105 | ### v2.3.2 106 | 107 | * Fixed CPV stopping when View is recycled 108 | 109 | ### v2.3.1 110 | 111 | * Fixed memory leak 112 | 113 | ### v2.3.0 114 | 115 | * Removed application tag from manifest 116 | * Added [`CircularProgressViewListener`](circularprogressview/src/main/java/com/github/rahatarmanahmed/cpv/CircularProgressViewListener.java) and [`CircularProgressViewAdapter`](circularprogressview/src/main/java/com/github/rahatarmanahmed/cpv/CircularProgressViewAdapter.java) 117 | * Added animation duration options for determinate swoop and sync animations 118 | 119 | ### v2.2.1 120 | 121 | * Fixed ignoring the color #FFFFFF 122 | 123 | ### v2.2.0 124 | 125 | * Now it uses the actual theme's accent color if possible by default. 126 | 127 | ### v2.1.0 128 | 129 | * Fixed default thickness using 4px instead of 4dp 130 | 131 | ### v2.0.1 132 | 133 | * Possible fix for drawArc NullPointerError 134 | * Slight performance improvements 135 | 136 | ### v2.0.0 137 | 138 | * Removed unnecessary appcompat dependency from example 139 | * Fixed repaint issue by drawing smaller arcs 140 | 141 | ### v1.0.0 142 | 143 | * Initial release 144 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.1.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /circularprogressview/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /circularprogressview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 22 10 | versionCode VERSION_CODE.toInteger() 11 | versionName VERSION_NAME 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | 20 | lintOptions { 21 | abortOnError false 22 | } 23 | } 24 | 25 | dependencies { 26 | compile fileTree(dir: 'libs', include: ['*.jar']) 27 | } 28 | 29 | apply from: './gradle-mvn-push.gradle' -------------------------------------------------------------------------------- /circularprogressview/gradle-mvn-push.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Chris Banes 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'maven' 18 | apply plugin: 'signing' 19 | 20 | def isReleaseBuild() { 21 | return VERSION_NAME.contains("SNAPSHOT") == false 22 | } 23 | 24 | def getReleaseRepositoryUrl() { 25 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 26 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 27 | } 28 | 29 | def getSnapshotRepositoryUrl() { 30 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 31 | : "https://oss.sonatype.org/content/repositories/snapshots/" 32 | } 33 | 34 | def getRepositoryUsername() { 35 | return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "" 36 | } 37 | 38 | def getRepositoryPassword() { 39 | return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "" 40 | } 41 | 42 | afterEvaluate { project -> 43 | uploadArchives { 44 | repositories { 45 | mavenDeployer { 46 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 47 | 48 | pom.groupId = GROUP 49 | pom.artifactId = POM_ARTIFACT_ID 50 | pom.version = VERSION_NAME 51 | 52 | repository(url: getReleaseRepositoryUrl()) { 53 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 54 | } 55 | snapshotRepository(url: getSnapshotRepositoryUrl()) { 56 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 57 | } 58 | 59 | pom.project { 60 | name POM_NAME 61 | packaging POM_PACKAGING 62 | description POM_DESCRIPTION 63 | url POM_URL 64 | 65 | scm { 66 | url POM_SCM_URL 67 | connection POM_SCM_CONNECTION 68 | developerConnection POM_SCM_DEV_CONNECTION 69 | } 70 | 71 | licenses { 72 | license { 73 | name POM_LICENCE_NAME 74 | url POM_LICENCE_URL 75 | distribution POM_LICENCE_DIST 76 | } 77 | } 78 | 79 | developers { 80 | developer { 81 | id POM_DEVELOPER_ID 82 | name POM_DEVELOPER_NAME 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | signing { 91 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 92 | sign configurations.archives 93 | } 94 | 95 | task androidJavadocs(type: Javadoc) { 96 | source = android.sourceSets.main.java.srcDirs 97 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 98 | } 99 | 100 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 101 | classifier = 'javadoc' 102 | from androidJavadocs.destinationDir 103 | } 104 | 105 | task androidSourcesJar(type: Jar) { 106 | classifier = 'sources' 107 | from android.sourceSets.main.java.sourceFiles 108 | } 109 | 110 | artifacts { 111 | archives androidSourcesJar 112 | archives androidJavadocsJar 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /circularprogressview/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=CircularProgressView 2 | POM_ARTIFACT_ID=circularprogressview 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /circularprogressview/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-studio-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 | -------------------------------------------------------------------------------- /circularprogressview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /circularprogressview/src/main/java/com/github/rahatarmanahmed/cpv/CircularProgressView.java: -------------------------------------------------------------------------------- 1 | package com.github.rahatarmanahmed.cpv; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.AnimatorSet; 6 | import android.animation.ValueAnimator; 7 | import android.content.Context; 8 | import android.content.res.Resources; 9 | import android.content.res.TypedArray; 10 | import android.graphics.Canvas; 11 | import android.graphics.Paint; 12 | import android.graphics.RectF; 13 | import android.os.Build; 14 | import android.util.AttributeSet; 15 | import android.util.Log; 16 | import android.util.TypedValue; 17 | import android.view.View; 18 | import android.view.animation.DecelerateInterpolator; 19 | import android.view.animation.LinearInterpolator; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | 25 | /** 26 | * TODO: document your custom view class. 27 | */ 28 | public class CircularProgressView extends View { 29 | 30 | private static final float INDETERMINANT_MIN_SWEEP = 15f; 31 | 32 | private Paint paint; 33 | private int size = 0; 34 | private RectF bounds; 35 | 36 | private boolean isIndeterminate, autostartAnimation; 37 | private float currentProgress, maxProgress, indeterminateSweep, indeterminateRotateOffset; 38 | private int thickness, color, animDuration, animSwoopDuration, animSyncDuration, animSteps; 39 | 40 | private List listeners; 41 | // Animation related stuff 42 | private float startAngle; 43 | private float actualProgress; 44 | private ValueAnimator startAngleRotate; 45 | private ValueAnimator progressAnimator; 46 | private AnimatorSet indeterminateAnimator; 47 | private float initialStartAngle; 48 | 49 | public CircularProgressView(Context context) { 50 | super(context); 51 | init(null, 0); 52 | } 53 | 54 | public CircularProgressView(Context context, AttributeSet attrs) { 55 | super(context, attrs); 56 | init(attrs, 0); 57 | } 58 | 59 | public CircularProgressView(Context context, AttributeSet attrs, int defStyle) { 60 | super(context, attrs, defStyle); 61 | init(attrs, defStyle); 62 | } 63 | 64 | protected void init(AttributeSet attrs, int defStyle) { 65 | listeners = new ArrayList<>(); 66 | 67 | initAttributes(attrs, defStyle); 68 | 69 | paint = new Paint(Paint.ANTI_ALIAS_FLAG); 70 | updatePaint(); 71 | 72 | bounds = new RectF(); 73 | } 74 | 75 | private void initAttributes(AttributeSet attrs, int defStyle) 76 | { 77 | final TypedArray a = getContext().obtainStyledAttributes( 78 | attrs, R.styleable.CircularProgressView, defStyle, 0); 79 | 80 | Resources resources = getResources(); 81 | 82 | // Initialize attributes from styleable attributes 83 | currentProgress = a.getFloat(R.styleable.CircularProgressView_cpv_progress, 84 | resources.getInteger(R.integer.cpv_default_progress)); 85 | maxProgress = a.getFloat(R.styleable.CircularProgressView_cpv_maxProgress, 86 | resources.getInteger(R.integer.cpv_default_max_progress)); 87 | thickness = a.getDimensionPixelSize(R.styleable.CircularProgressView_cpv_thickness, 88 | resources.getDimensionPixelSize(R.dimen.cpv_default_thickness)); 89 | isIndeterminate = a.getBoolean(R.styleable.CircularProgressView_cpv_indeterminate, 90 | resources.getBoolean(R.bool.cpv_default_is_indeterminate)); 91 | autostartAnimation = a.getBoolean(R.styleable.CircularProgressView_cpv_animAutostart, 92 | resources.getBoolean(R.bool.cpv_default_anim_autostart)); 93 | initialStartAngle = a.getFloat(R.styleable.CircularProgressView_cpv_startAngle, 94 | resources.getInteger(R.integer.cpv_default_start_angle)); 95 | startAngle = initialStartAngle; 96 | 97 | int accentColor = getContext().getResources().getIdentifier("colorAccent", "attr", getContext().getPackageName()); 98 | 99 | // If color explicitly provided 100 | if (a.hasValue(R.styleable.CircularProgressView_cpv_color)) { 101 | color = a.getColor(R.styleable.CircularProgressView_cpv_color, resources.getColor(R.color.cpv_default_color)); 102 | } 103 | // If using support library v7 accentColor 104 | else if(accentColor != 0) { 105 | TypedValue t = new TypedValue(); 106 | getContext().getTheme().resolveAttribute(accentColor, t, true); 107 | color = t.data; 108 | } 109 | // If using native accentColor (SDK >21) 110 | else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 111 | TypedArray t = getContext().obtainStyledAttributes(new int[] { android.R.attr.colorAccent }); 112 | color = t.getColor(0, resources.getColor(R.color.cpv_default_color)); 113 | } 114 | else { 115 | //Use default color 116 | color = resources.getColor(R.color.cpv_default_color); 117 | } 118 | 119 | animDuration = a.getInteger(R.styleable.CircularProgressView_cpv_animDuration, 120 | resources.getInteger(R.integer.cpv_default_anim_duration)); 121 | animSwoopDuration = a.getInteger(R.styleable.CircularProgressView_cpv_animSwoopDuration, 122 | resources.getInteger(R.integer.cpv_default_anim_swoop_duration)); 123 | animSyncDuration = a.getInteger(R.styleable.CircularProgressView_cpv_animSyncDuration, 124 | resources.getInteger(R.integer.cpv_default_anim_sync_duration)); 125 | animSteps = a.getInteger(R.styleable.CircularProgressView_cpv_animSteps, 126 | resources.getInteger(R.integer.cpv_default_anim_steps)); 127 | a.recycle(); 128 | } 129 | 130 | @Override 131 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 132 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 133 | int xPad = getPaddingLeft() + getPaddingRight(); 134 | int yPad = getPaddingTop() + getPaddingBottom(); 135 | int width = getMeasuredWidth() - xPad; 136 | int height = getMeasuredHeight() - yPad; 137 | size = (width < height) ? width : height; 138 | setMeasuredDimension(size + xPad, size + yPad); 139 | 140 | } 141 | 142 | @Override 143 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 144 | super.onSizeChanged(w, h, oldw, oldh); 145 | size = (w < h) ? w : h; 146 | updateBounds(); 147 | } 148 | 149 | private void updateBounds() 150 | { 151 | int paddingLeft = getPaddingLeft(); 152 | int paddingTop = getPaddingTop(); 153 | bounds.set(paddingLeft + thickness, paddingTop + thickness, size - paddingLeft - thickness, size - paddingTop - thickness); 154 | } 155 | 156 | private void updatePaint() 157 | { 158 | paint.setColor(color); 159 | paint.setStyle(Paint.Style.STROKE); 160 | paint.setStrokeWidth(thickness); 161 | paint.setStrokeCap(Paint.Cap.BUTT); 162 | } 163 | 164 | @Override 165 | protected void onDraw(Canvas canvas) { 166 | super.onDraw(canvas); 167 | 168 | // Draw the arc 169 | float sweepAngle = (isInEditMode()) ? currentProgress/maxProgress*360 : actualProgress/maxProgress*360; 170 | if(!isIndeterminate) 171 | canvas.drawArc(bounds, startAngle, sweepAngle, false, paint); 172 | else 173 | canvas.drawArc(bounds, startAngle + indeterminateRotateOffset, indeterminateSweep, false, paint); 174 | } 175 | 176 | /** 177 | * Returns the mode of this view (determinate or indeterminate). 178 | * @return true if this view is in indeterminate mode. 179 | */ 180 | public boolean isIndeterminate() { 181 | return isIndeterminate; 182 | } 183 | 184 | /** 185 | * Sets whether this CircularProgressView is indeterminate or not. 186 | * It will reset the animation if the mode has changed. 187 | * @param isIndeterminate True if indeterminate. 188 | */ 189 | public void setIndeterminate(boolean isIndeterminate) { 190 | boolean old = this.isIndeterminate; 191 | boolean reset = this.isIndeterminate != isIndeterminate; 192 | this.isIndeterminate = isIndeterminate; 193 | if (reset) 194 | resetAnimation(); 195 | if(old != isIndeterminate) { 196 | for(CircularProgressViewListener listener : listeners) { 197 | listener.onModeChanged(isIndeterminate); 198 | } 199 | } 200 | } 201 | 202 | /** 203 | * Get the thickness of the progress bar arc. 204 | * @return the thickness of the progress bar arc 205 | */ 206 | public int getThickness() { 207 | return thickness; 208 | } 209 | 210 | /** 211 | * Sets the thickness of the progress bar arc. 212 | * @param thickness the thickness of the progress bar arc 213 | */ 214 | public void setThickness(int thickness) { 215 | this.thickness = thickness; 216 | updatePaint(); 217 | updateBounds(); 218 | invalidate(); 219 | } 220 | 221 | /** 222 | * 223 | * @return the color of the progress bar 224 | */ 225 | public int getColor() { 226 | return color; 227 | } 228 | 229 | /** 230 | * Sets the color of the progress bar. 231 | * @param color the color of the progress bar 232 | */ 233 | public void setColor(int color) { 234 | this.color = color; 235 | updatePaint(); 236 | invalidate(); 237 | } 238 | 239 | /** 240 | * Gets the progress value considered to be 100% of the progress bar. 241 | * @return the maximum progress 242 | */ 243 | public float getMaxProgress() { 244 | return maxProgress; 245 | } 246 | 247 | /** 248 | * Sets the progress value considered to be 100% of the progress bar. 249 | * @param maxProgress the maximum progress 250 | */ 251 | public void setMaxProgress(float maxProgress) { 252 | this.maxProgress = maxProgress; 253 | invalidate(); 254 | } 255 | 256 | /** 257 | * @return current progress 258 | */ 259 | public float getProgress() { 260 | return currentProgress; 261 | } 262 | 263 | /** 264 | * Sets the progress of the progress bar. 265 | * 266 | * @param currentProgress the new progress. 267 | */ 268 | public void setProgress(final float currentProgress) { 269 | this.currentProgress = currentProgress; 270 | // Reset the determinate animation to approach the new currentProgress 271 | if (!isIndeterminate) { 272 | if (progressAnimator != null && progressAnimator.isRunning()) 273 | progressAnimator.cancel(); 274 | progressAnimator = ValueAnimator.ofFloat(actualProgress, currentProgress); 275 | progressAnimator.setDuration(animSyncDuration); 276 | progressAnimator.setInterpolator(new LinearInterpolator()); 277 | progressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 278 | @Override 279 | public void onAnimationUpdate(ValueAnimator animation) { 280 | actualProgress = (Float) animation.getAnimatedValue(); 281 | invalidate(); 282 | } 283 | }); 284 | progressAnimator.addListener(new AnimatorListenerAdapter() { 285 | @Override 286 | public void onAnimationEnd(Animator animation) { 287 | for(CircularProgressViewListener listener : listeners) { 288 | listener.onProgressUpdateEnd(currentProgress); 289 | } 290 | } 291 | }); 292 | 293 | progressAnimator.start(); 294 | } 295 | invalidate(); 296 | for(CircularProgressViewListener listener : listeners) { 297 | listener.onProgressUpdate(currentProgress); 298 | } 299 | } 300 | 301 | /** 302 | * Register a CircularProgressViewListener with this View 303 | * @param listener The listener to register 304 | */ 305 | public void addListener(CircularProgressViewListener listener) { 306 | if(listener != null) 307 | listeners.add(listener); 308 | } 309 | 310 | /** 311 | * Unregister a CircularProgressViewListener with this View 312 | * @param listener The listener to unregister 313 | */ 314 | public void removeListener(CircularProgressViewListener listener) { 315 | listeners.remove(listener); 316 | } 317 | 318 | /** 319 | * Starts the progress bar animation. 320 | * (This is an alias of resetAnimation() so it does the same thing.) 321 | */ 322 | public void startAnimation() { 323 | resetAnimation(); 324 | } 325 | 326 | /** 327 | * Resets the animation. 328 | */ 329 | public void resetAnimation() { 330 | // Cancel all the old animators 331 | if(startAngleRotate != null && startAngleRotate.isRunning()) 332 | startAngleRotate.cancel(); 333 | if(progressAnimator != null && progressAnimator.isRunning()) 334 | progressAnimator.cancel(); 335 | if(indeterminateAnimator != null && indeterminateAnimator.isRunning()) 336 | indeterminateAnimator.cancel(); 337 | 338 | // Determinate animation 339 | if(!isIndeterminate) 340 | { 341 | // The cool 360 swoop animation at the start of the animation 342 | startAngle = initialStartAngle; 343 | startAngleRotate = ValueAnimator.ofFloat(startAngle, startAngle + 360); 344 | startAngleRotate.setDuration(animSwoopDuration); 345 | startAngleRotate.setInterpolator(new DecelerateInterpolator(2)); 346 | startAngleRotate.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 347 | @Override 348 | public void onAnimationUpdate(ValueAnimator animation) { 349 | startAngle = (Float) animation.getAnimatedValue(); 350 | invalidate(); 351 | } 352 | }); 353 | startAngleRotate.start(); 354 | 355 | // The linear animation shown when progress is updated 356 | actualProgress = 0f; 357 | progressAnimator = ValueAnimator.ofFloat(actualProgress, currentProgress); 358 | progressAnimator.setDuration(animSyncDuration); 359 | progressAnimator.setInterpolator(new LinearInterpolator()); 360 | progressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 361 | @Override 362 | public void onAnimationUpdate(ValueAnimator animation) { 363 | actualProgress = (Float) animation.getAnimatedValue(); 364 | invalidate(); 365 | } 366 | }); 367 | progressAnimator.start(); 368 | } 369 | // Indeterminate animation 370 | else 371 | { 372 | indeterminateSweep = INDETERMINANT_MIN_SWEEP; 373 | // Build the whole AnimatorSet 374 | indeterminateAnimator = new AnimatorSet(); 375 | AnimatorSet prevSet = null, nextSet; 376 | for(int k=0;k 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /circularprogressview/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CircularProgressView 3 | 4 | -------------------------------------------------------------------------------- /circularprogressview/src/main/res/values/values.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2196F3 4 | 4dp 5 | 0 6 | 100 7 | false 8 | false 9 | 4000 10 | 5000 11 | 500 12 | 3 13 | -90 14 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.github.rahatarmanahmed.cpv.example" 9 | minSdkVersion 14 10 | targetSdkVersion 22 11 | versionCode VERSION_CODE.toInteger() 12 | versionName VERSION_NAME 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | 21 | lintOptions { 22 | abortOnError false 23 | } 24 | } 25 | 26 | dependencies { 27 | compile fileTree(dir: 'libs', include: ['*.jar']) 28 | compile project(':circularprogressview') 29 | } 30 | -------------------------------------------------------------------------------- /example/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-studio-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 | -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/src/main/java/com/github/rahatarmanahmed/cpv/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.rahatarmanahmed.cpv.example; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.os.Handler; 6 | import android.os.SystemClock; 7 | import android.util.Log; 8 | import android.view.View; 9 | import android.widget.Button; 10 | 11 | import com.github.rahatarmanahmed.cpv.CircularProgressView; 12 | import com.github.rahatarmanahmed.cpv.CircularProgressViewAdapter; 13 | 14 | 15 | public class MainActivity extends Activity { 16 | 17 | CircularProgressView progressView; 18 | Thread updateThread; 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | setContentView(R.layout.activity_main); 24 | progressView = (CircularProgressView) findViewById(R.id.progress_view); 25 | 26 | // Test the listener with logcat messages 27 | progressView.addListener(new CircularProgressViewAdapter() { 28 | @Override 29 | public void onProgressUpdate(float currentProgress) { 30 | Log.d("CPV", "onProgressUpdate: " + currentProgress); 31 | } 32 | 33 | @Override 34 | public void onProgressUpdateEnd(float currentProgress) { 35 | Log.d("CPV", "onProgressUpdateEnd: " + currentProgress); 36 | } 37 | 38 | @Override 39 | public void onAnimationReset() { 40 | Log.d("CPV", "onAnimationReset"); 41 | } 42 | 43 | @Override 44 | public void onModeChanged(boolean isIndeterminate) { 45 | Log.d("CPV", "onModeChanged: " + (isIndeterminate ? "indeterminate" : "determinate")); 46 | } 47 | }); 48 | 49 | // Test loading animation 50 | startAnimationThreadStuff(1000); 51 | final Button button = (Button) findViewById(R.id.button); 52 | button.setOnClickListener(new View.OnClickListener() { 53 | @Override 54 | public void onClick(View v) { 55 | if(progressView.isIndeterminate()) 56 | { 57 | progressView.setIndeterminate(false); 58 | button.setText("Switch to indeterminate"); 59 | } 60 | else 61 | { 62 | progressView.setIndeterminate(true); 63 | button.setText("Switch to determinate"); 64 | } 65 | startAnimationThreadStuff(0); 66 | } 67 | }); 68 | } 69 | 70 | private void startAnimationThreadStuff(long delay) { 71 | if (updateThread != null && updateThread.isAlive()) 72 | updateThread.interrupt(); 73 | // Start animation after a delay so there's no missed frames while the app loads up 74 | final Handler handler = new Handler(); 75 | handler.postDelayed(new Runnable() { 76 | @Override 77 | public void run() { 78 | if(!progressView.isIndeterminate()) { 79 | progressView.setProgress(0f); 80 | // Run thread to update progress every quarter second until full 81 | updateThread = new Thread(new Runnable() { 82 | @Override 83 | public void run() { 84 | while (progressView.getProgress() < progressView.getMaxProgress() && !Thread.interrupted()) { 85 | // Must set progress in UI thread 86 | handler.post(new Runnable() { 87 | @Override 88 | public void run() { 89 | progressView.setProgress(progressView.getProgress() + 10); 90 | } 91 | }); 92 | SystemClock.sleep(250); 93 | } 94 | } 95 | }); 96 | updateThread.start(); 97 | } 98 | // Alias for resetAnimation, it's all the same 99 | progressView.startAnimation(); 100 | } 101 | }, delay); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /example/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahatarmanahmed/CircularProgressView/3620aff9a889fe426ff9270e15166ad4c062922b/example/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahatarmanahmed/CircularProgressView/3620aff9a889fe426ff9270e15166ad4c062922b/example/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahatarmanahmed/CircularProgressView/3620aff9a889fe426ff9270e15166ad4c062922b/example/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahatarmanahmed/CircularProgressView/3620aff9a889fe426ff9270e15166ad4c062922b/example/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 20 | 26 | 27 | 37 | 38 | 45 | 46 |