├── app ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── values │ │ │ ├── styles.xml │ │ │ ├── strings.xml │ │ │ └── dimens.xml │ │ ├── values-w820dp │ │ │ └── dimens.xml │ │ └── layout │ │ │ └── activity_main.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── me │ │ └── tankery │ │ └── app │ │ └── circularseekbar │ │ └── MainActivity.kt ├── proguard-rules.pro └── build.gradle ├── circularSeekBar ├── .gitignore ├── gradle.properties ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ └── values │ │ │ └── attrs.xml │ │ └── java │ │ └── me │ │ └── tankery │ │ └── lib │ │ └── circularseekbar │ │ └── CircularSeekBar.kt ├── build.gradle └── proguard-rules.pro ├── settings.gradle ├── art └── capture.jpg ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── mvn-push.gradle ├── .gitignore ├── RELEASING.md ├── .github └── workflows │ ├── ci-check.yml │ └── release.yml ├── gradle.properties ├── CHANGELOG.md ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /circularSeekBar/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':circularSeekBar' 3 | -------------------------------------------------------------------------------- /art/capture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tankery/CircularSeekBar/HEAD/art/capture.jpg -------------------------------------------------------------------------------- /circularSeekBar/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=circularSeekBar 2 | POM_NAME=CircularSeekBar 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tankery/CircularSeekBar/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tankery/CircularSeekBar/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tankery/CircularSeekBar/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tankery/CircularSeekBar/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tankery/CircularSeekBar/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /circularSeekBar/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CircularSeekBar 3 | 4 | Hello CircularSeekBar! 5 | Settings 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat May 14 23:20:54 BST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | 4dp 7 | 4dp 8 | 200dp 9 | 120dp 10 | 120dp 11 | 12 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/tankery/lib/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 | -------------------------------------------------------------------------------- /circularSeekBar/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'maven-publish' 4 | 5 | android { 6 | compileSdkVersion rootProject.compileSdkVersion 7 | resourcePrefix "cs_" 8 | 9 | defaultConfig { 10 | minSdkVersion 10 11 | targetSdkVersion 32 12 | versionName version 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" 24 | } 25 | 26 | apply from: rootProject.file('gradle/mvn-push.gradle') 27 | -------------------------------------------------------------------------------- /circularSeekBar/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 /Users/tankery/lib/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Java files 2 | bin 3 | gen 4 | 5 | # NDK files 6 | libs/*/ 7 | obj 8 | 9 | # eclipse project files 10 | .project 11 | .cproject 12 | .settings/ 13 | 14 | # Annotaion Process 15 | .apt_generated/ 16 | 17 | # ProGuard 18 | proguard 19 | 20 | # Android Tools file 21 | build.xml 22 | local.properties 23 | 24 | # VIM temp file 25 | *.swp 26 | 27 | # Encryption files 28 | *.x 29 | 30 | # built application files 31 | *.apk 32 | *.ap_ 33 | 34 | # files for the dex VM 35 | *.dex 36 | 37 | # Java class files 38 | *.class 39 | 40 | # Local configuration file (sdk path, etc) 41 | local.properties 42 | 43 | # Android Studio 44 | *.iml 45 | .idea 46 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 47 | .gradle 48 | build/ 49 | 50 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | ## I. Bump version 4 | 5 | 1. Update version to `X.Y.Z` in `gradle.properties`. 6 | 2. Update the `CHANGELOG.md` for the impending release. 7 | 3. Update version name in `README.md` 8 | 4. Execute `git commit -m "Upgrade to vX.Y.Z"` (where X.Y.Z is the new version). 9 | 5. Create a PR by execute `git push origin master:release/X.Y.Z`. 10 | 6. Review PR and merge branch to master. 11 | 7. Fetch the repo to update working tree. 12 | 13 | ## II. Publishing 14 | 15 | 1. Execute `git tag vX.Y.Z` (where X.Y.Z is the new version) 16 | 2. Execute `git push --tags` 17 | 3. The GitHub workflow will automatically create a new release for this version, and upload to Sonatype. 18 | 4. After published, visit [Sonatype Nexus](https://s01.oss.sonatype.org/) and promote the artifact. 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/ci-check.yml: -------------------------------------------------------------------------------- 1 | name: CI Check 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ master ] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: set up JDK 11 16 | uses: actions/setup-java@v2 17 | with: 18 | java-version: '11' 19 | distribution: 'adopt' 20 | 21 | - name: Grant execute permission for gradlew 22 | run: chmod +x gradlew 23 | - name: Build demo APK 24 | run: | 25 | ./gradlew clean assemble --stacktrace 26 | check: 27 | 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: set up JDK 11 33 | uses: actions/setup-java@v2 34 | with: 35 | java-version: '11' 36 | distribution: 'adopt' 37 | 38 | - name: Grant execute permission for gradlew 39 | run: chmod +x gradlew 40 | - name: Check with Gradle 41 | run: ./gradlew check --stacktrace 42 | 43 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | // Define some common closures (methods) for code share 5 | ext.getTagCount = { 6 | String cmd = "git tag -l \"v*\"" 7 | String result = cmd.execute((String[])null, project.projectDir).text 8 | return result.trim().split("\n").length 9 | } 10 | ext.getLatestTaggedVersion = { 11 | String cmd = "git describe --abbrev=0 --tags" 12 | String result = cmd.execute((String[])null, project.projectDir).text 13 | return result.trim().replaceAll("v_?", "") 14 | } 15 | 16 | android { 17 | compileSdkVersion rootProject.compileSdkVersion 18 | 19 | defaultConfig { 20 | applicationId "me.tankery.app.circularseekbar" 21 | minSdkVersion 15 22 | targetSdkVersion 32 23 | versionCode getTagCount() 24 | versionName getLatestTaggedVersion() 25 | } 26 | buildTypes { 27 | release { 28 | signingConfig signingConfigs.debug 29 | minifyEnabled false 30 | setProperty("archivesBaseName", "CircularSeekBar-demo-${defaultConfig.versionName}") 31 | } 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation project(':circularSeekBar') 37 | implementation "com.android.support:support-annotations:$supportLibVersion" 38 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" 39 | } 40 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | GROUP=me.tankery.lib 21 | VERSION_NAME=1.4.2 22 | 23 | POM_DESCRIPTION=Android CircularSeekBar view/widget 24 | 25 | POM_URL=https://github.com/tankery/CircularSeekBar 26 | POM_SCM_URL=https://github.com/tankery/CircularSeekBar 27 | POM_SCM_CONNECTION=scm:git:git://github.com/tankery/CircularSeekBar.git 28 | POM_SCM_DEV_CONNECTION=scm:git:git://github.com/tankery/CircularSeekBar.git 29 | 30 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 31 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 32 | POM_LICENCE_DIST=repo 33 | 34 | POM_DEVELOPER_ID=tankery 35 | POM_DEVELOPER_NAME=Tankery Chen 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## v1.4.2 4 | 5 | Allow disable pointer at runtime. Thanks [@tejada's PR](https://github.com/tankery/CircularSeekBar/pull/55). 6 | 7 | ## v1.4.1 8 | 9 | Add new method for apply custom Paint. Thanks [@ttpho's PR](https://github.com/tankery/CircularSeekBar/pull/50). 10 | 11 | ## v1.4.0 12 | 13 | Migrate library source code to Kotlin. Thanks [@jonathan-livly's PR](https://github.com/tankery/CircularSeekBar/pull/45). 14 | 15 | ## v1.3.2 16 | 17 | Support Sonatype Maven Central uploading. 18 | 19 | ## v1.3.1 20 | 21 | Change to using GitHub workflow for releasing. 22 | 23 | ## v1.3.0 24 | 25 | - API Change: Set glow effect disabled by default. 26 | - Upgrade gradle and sdk. 27 | - Fix drawing bug when glow effect enabled. 28 | 29 | ## v1.2.0 30 | 31 | - Upgrade gradle and sdk. 32 | - Add support to hide progress when empty 33 | - Fix some progress drawing bug 34 | 35 | ## v1.1.7 36 | 37 | - Upgrade gradle and sdk. 38 | 39 | ## v1.1.6 40 | 41 | - Add support for disabling progress glow. 42 | 43 | ## v1.1.5 44 | 45 | - Fix progress is hard to set to 0 or 100 when touch. 46 | 47 | ## v1.1.4 48 | 49 | - Upgrade gradle and sdk version 50 | - Fix Progress bar missing when progress is 100 on Android O. 51 | 52 | ## v1.1.3 53 | 54 | Remove unnecessary new instance of Path, and fix some bugs. 55 | 56 | ## v1.1.2 57 | 58 | Use dimension & color type for attrs instead of float & string... 59 | 60 | ## v1.1.1 61 | 62 | Fix resource name prefix error. 63 | 64 | ## v1.1.0 65 | 66 | add some Java object APIs. 67 | -------------------------------------------------------------------------------- /app/src/main/java/me/tankery/app/circularseekbar/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.tankery.app.circularseekbar 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.widget.TextView 8 | import me.tankery.app.circularseekbar.R.id 9 | import me.tankery.app.circularseekbar.R.layout 10 | import me.tankery.lib.circularseekbar.CircularSeekBar 11 | 12 | class MainActivity : Activity() { 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | 17 | setContentView(layout.activity_main) 18 | 19 | val textEvent = findViewById(id.text_event) 20 | val textProgress = findViewById(id.text_progress) 21 | val seekBar = findViewById(id.seek_bar) 22 | 23 | seekBar.setOnSeekBarChangeListener(object : 24 | CircularSeekBar.OnCircularSeekBarChangeListener { 25 | override fun onProgressChanged(circularSeekBar: CircularSeekBar?, progress: Float, fromUser: Boolean) { 26 | val message = String.format("Progress changed to %.2f, fromUser %s", progress, fromUser) 27 | Log.d("Main", message) 28 | textProgress.text = message 29 | } 30 | 31 | override fun onStopTrackingTouch(seekBar: CircularSeekBar?) { 32 | Log.d("Main", "onStopTrackingTouch") 33 | textEvent.text = "" 34 | } 35 | 36 | @SuppressLint("SetTextI18n") 37 | override fun onStartTrackingTouch(seekBar: CircularSeekBar?) { 38 | Log.d("Main", "onStartTrackingTouch") 39 | textEvent.text = "touched | " 40 | } 41 | }) 42 | } 43 | } -------------------------------------------------------------------------------- /circularSeekBar/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 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | # Push events to matching v*, i.e. v1.0, v20.15.10 7 | - 'v*' 8 | 9 | jobs: 10 | release-demo: 11 | name: Release Demo APK to GitHub 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: set up JDK 11 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '11' 20 | distribution: 'adopt' 21 | 22 | - name: Build demo APK 23 | run: | 24 | ./gradlew clean assemble --stacktrace 25 | - name: Set output file 26 | id: listFile 27 | run: | 28 | echo "::set-output name=demoPath::$(find . -name 'CircularSeekBar-demo-*-release.apk')" 29 | echo "::set-output name=libPath::$(find . -name 'circularSeekBar-release.aar')" 30 | 31 | - name: Create Release 32 | id: create_release 33 | uses: actions/create-release@v1 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | with: 37 | tag_name: ${{ github.ref }} 38 | release_name: Release ${{ github.ref }} 39 | draft: false 40 | prerelease: false 41 | - name: Upload Demo APK 42 | id: upload_demo_apk 43 | uses: actions/upload-release-asset@v1 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | with: 47 | # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 48 | upload_url: ${{ steps.create_release.outputs.upload_url }} 49 | asset_path: ${{ steps.listFile.outputs.demoPath }} 50 | asset_name: CircularSeekBar-demo.apk 51 | asset_content_type: application/zip 52 | - name: Upload Library AAR 53 | id: upload_library_aar 54 | uses: actions/upload-release-asset@v1 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | with: 58 | upload_url: ${{ steps.create_release.outputs.upload_url }} 59 | asset_path: ${{ steps.listFile.outputs.libPath }} 60 | asset_name: circularSeekBar-release.aar 61 | asset_content_type: application/zip 62 | 63 | release-library: 64 | name: Release library to Sonatype maven repository 65 | runs-on: ubuntu-latest 66 | steps: 67 | - name: Checkout code 68 | uses: actions/checkout@v2 69 | - name: Build library 70 | run: | 71 | ./gradlew clean publish --stacktrace 72 | env: 73 | SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 74 | SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 75 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 76 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 77 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 78 | -------------------------------------------------------------------------------- /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: 'com.android.library' 18 | apply plugin: 'maven-publish' 19 | apply plugin: 'signing' 20 | 21 | ext.isReleaseVersion = !VERSION_NAME.contains("SNAPSHOT") 22 | 23 | File secretPropsFile = project.rootProject.file('local.properties') 24 | if (secretPropsFile.exists()) { 25 | // Read local.properties file first if it exists 26 | Properties p = new Properties() 27 | new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } 28 | p.each { name, value -> ext[name] = value } 29 | } else { 30 | // Use system environment variables 31 | ext["ossrhUsername"] = System.getenv('SONATYPE_NEXUS_USERNAME') 32 | ext["ossrhPassword"] = System.getenv('SONATYPE_NEXUS_PASSWORD') 33 | ext["signingKeyId"] = System.getenv('SIGNING_KEY_ID') 34 | ext["signingPassword"] = System.getenv('SIGNING_PASSWORD') 35 | ext["signingKey"] = System.getenv('SIGNING_KEY') 36 | } 37 | 38 | android { 39 | publishing { 40 | singleVariant('release') { 41 | withSourcesJar() 42 | withJavadocJar() 43 | } 44 | } 45 | } 46 | 47 | afterEvaluate { 48 | publishing { 49 | publications { 50 | maven(MavenPublication) { 51 | groupId = GROUP 52 | artifactId = POM_ARTIFACT_ID 53 | version = VERSION_NAME 54 | 55 | from components.release 56 | 57 | repositories { 58 | maven { 59 | def releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" 60 | def snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" 61 | url = isReleaseVersion ? releasesRepoUrl : snapshotsRepoUrl 62 | credentials { 63 | username = ossrhUsername 64 | password = ossrhPassword 65 | } 66 | } 67 | } 68 | 69 | pom { 70 | name = POM_NAME 71 | packaging = POM_PACKAGING 72 | description = POM_DESCRIPTION 73 | url = POM_URL 74 | 75 | scm { 76 | url = POM_SCM_URL 77 | connection = POM_SCM_CONNECTION 78 | developerConnection = POM_SCM_DEV_CONNECTION 79 | } 80 | 81 | licenses { 82 | license { 83 | name = POM_LICENCE_NAME 84 | url = POM_LICENCE_URL 85 | distribution = POM_LICENCE_DIST 86 | } 87 | } 88 | 89 | developers { 90 | developer { 91 | id = POM_DEVELOPER_ID 92 | name = POM_DEVELOPER_NAME 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | signing { 101 | required { isReleaseVersion && gradle.taskGraph.hasTask("publishing") } 102 | useInMemoryPgpKeys( 103 | signingKey, 104 | signingPassword, 105 | ) 106 | sign publishing.publications.maven 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CircularSeekBar 2 | 3 | [![Build Status](https://github.com/tankery/CircularSeekBar/actions/workflows/ci-check.yml/badge.svg?event=push&branch=master)](https://github.com/tankery/CircularSeekBar/actions/workflows/ci-check.yml) 4 | [![GitHub release](https://img.shields.io/github/release/tankery/CircularSeekBar.svg?label=demo)](https://github.com/tankery/CircularSeekBar/releases) 5 | [![Maven Central](https://img.shields.io/maven-central/v/me.tankery.lib/circularSeekBar)](https://search.maven.org/artifact/me.tankery.lib/circularSeekBar) 6 | 7 | Rich feature Circular SeekBar (Circle, Semi-circle, and Ellipse) for Android. 8 | 9 | This library is based on [CircularSeekBar of Matt Joseph (devadvance)](https://github.com/devadvance/circularseekbar). 10 | But the original author seems to have stopped maintaining (last commit was in 2016), so I decide to take it up by myself. 11 | 12 | I made it build on Android Studio (Gradle), then fix & add more features to the library. 13 | 14 | CircularSeekBar Screenshot 15 | 16 | ## The features I add 17 | 18 | 1. Support float progress, instead of integer only. 19 | 2. Refactor the name of the attributes, to avoid conflicts. 20 | 3. Disable the seek pointer, make it work like a circular progress bar. 21 | 4. Some other bug fixes for Matt's CircularSeekBar. 22 | 5. Customize the shape of progress end (butt, round, square) 23 | 6. Use a arc to represent the pointer, you can custom the arc angle. 24 | 7. Negative progress support. 25 | 8. Other small features. 26 | 27 | ## Setup 28 | 29 | ### Gradle 30 | 31 | ``` Gradle 32 | dependencies { 33 | implementation 'me.tankery.lib:circularSeekBar:1.4.2' 34 | } 35 | ``` 36 | 37 | ### Source 38 | 39 | Copy sources and `attrs.xml` in module `circularSeekBar` to your project. 40 | 41 | > Old style Java fan? Checkout version [v1.3.2](https://github.com/tankery/CircularSeekBar/releases/tag/v1.3.2), which is the latest version that still using Java. 42 | 43 | ## Usage 44 | 45 | CircularSeekBar support following attributes: 46 | ``` 47 | app:cs_circle_style = "butt|round|square" 48 | app:cs_progress = "integer" 49 | app:cs_max = "integer" 50 | app:cs_negative_enabled = "boolean" 51 | app:cs_move_outside_circle = "boolean" 52 | app:cs_maintain_equal_circle = "boolean" 53 | app:cs_use_custom_radii = "boolean" 54 | app:cs_lock_enabled = "boolean" 55 | app:cs_circle_x_radius = "dimension" 56 | app:cs_circle_y_radius = "dimension" 57 | app:cs_circle_stroke_width = "dimension" 58 | app:cs_disable_pointer = "boolean" 59 | app:cs_pointer_stroke_width = "dimension" 60 | app:cs_pointer_halo_width = "dimension" 61 | app:cs_pointer_halo_border_width = "dimension" 62 | app:cs_circle_fill = "color" 63 | app:cs_circle_color = "color" 64 | app:cs_circle_progress_color = "color" 65 | app:cs_pointer_color = "color" 66 | app:cs_pointer_halo_color = "color" 67 | app:cs_pointer_halo_color_ontouch = "color" 68 | app:cs_pointer_alpha_ontouch = "integer" 69 | app:cs_pointer_angle = "float" 70 | app:cs_start_angle = "float" 71 | app:cs_end_angle = "float" 72 | app:cs_disable_progress_glow = "boolean" 73 | app:cs_hide_progress_when_empty = "boolean" 74 | ``` 75 | 76 | Apply custom Paint 77 | 78 | ```kotlin 79 | val dashWidth = 16.0f 80 | val dashGap = dashWidth / 2.0f 81 | circularSeekBar?.applyCustomCirclePaint { 82 | it.pathEffect = DashPathEffect(floatArrayOf(dashWidth, dashGap), 0.0f) 83 | } 84 | ``` 85 | 86 | ## Appreciation 87 | 88 | This library is based on [CircularSeekBar of Matt Joseph (devadvance)](https://github.com/devadvance/circularseekbar). 89 | But the original author seems to have stopped maintaining (last commit was in 2016), so I decide to take it up by myself. Thanks to Matt for the work! 90 | 91 | ### Automation 92 | 93 | This part provides general solution to any types of libraries: 94 | 95 | - GitHub Actions: [Quick Start](https://docs.github.com/en/actions/quickstart), [Workflow commands](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions) and [Upload Release Asset repo](https://github.com/actions/upload-release-asset) are official documents I use to write the CI check and releasing workflows. 96 | - [Márton B.: Publishing Android libraries to MavenCentral in 2021](https://getstream.io/blog/publishing-libraries-to-mavencentral-2021/): It is a great learning material for how to upload library to sonatype maven central. Also took the official doc as an reference: [Maven Publish Plugin](https://docs.gradle.org/current/userguide/publishing_maven.html). For Android, the config can be a little different, could also checkout [Android library distribution with maven-publish](https://proandroiddev.com/android-library-distribution-with-maven-publish-28ac59b8ecb8) for a reference. 97 | - Legacy: Another post ([LINK](https://proandroiddev.com/publishing-your-first-android-library-to-mavencentral-be2c51330b88)) also could used as a reference. 98 | - Official Maven Central document [Gradle](https://central.sonatype.org/publish/publish-gradle/) to learn how to publish artifacts. 99 | - Signature: Maven Central requires signature for library to release. [GPG](https://central.sonatype.org/publish/requirements/gpg/#distributing-your-public-key) and [The Signing Plugin](https://docs.gradle.org/current/userguide/signing_plugin.html) are two official document for how to sign the library, this will correct some error or outdated information in Márton's article. 100 | - Also [Timber](https://github.com/JakeWharton/timber) of Jake Wharton is a great library to learn how to use a generic solution for public libraries. My [mvn-push.gradle](https://github.com/tankery/CircularSeekBar/blob/master/gradle/mvn-push.gradle) is actually forked from Timber repo, and it's really universal that could be used on many different types of libraries. 101 | 102 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 17 | 18 | 28 | 29 | 33 | 34 | 39 | 40 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 55 | 56 | 57 | 69 | 70 | 75 | 76 | 77 | 78 | 79 | 80 | 92 | 93 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 122 | 123 | 129 | 130 | 131 | 132 | 133 | 134 | 148 | 149 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /circularSeekBar/src/main/java/me/tankery/lib/circularseekbar/CircularSeekBar.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2013 Matt Joseph 4 | * Copyright 2018 Tankery Chen 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * 20 | * This custom view/widget was inspired and guided by: 21 | * 22 | * HoloCircleSeekBar - Copyright 2012 Jes�s Manzano 23 | * HoloColorPicker - Copyright 2012 Lars Werkman (Designed by Marie Schweiz) 24 | * 25 | * Although I did not used the code from either project directly, they were both used as 26 | * reference material, and as a result, were extremely helpful. 27 | */ 28 | package me.tankery.lib.circularseekbar 29 | 30 | import android.content.Context 31 | import android.content.res.TypedArray 32 | import android.graphics.Paint 33 | import android.graphics.Path 34 | import android.graphics.Paint.Cap 35 | import android.graphics.RectF 36 | import android.graphics.Color 37 | import android.graphics.BlurMaskFilter 38 | import android.graphics.PathMeasure 39 | import android.graphics.Canvas 40 | import android.os.Build 41 | import android.os.Bundle 42 | import android.os.Parcelable 43 | import android.util.AttributeSet 44 | import android.view.MotionEvent 45 | import android.view.View 46 | 47 | class CircularSeekBar @JvmOverloads constructor( 48 | context: Context, 49 | attrs: AttributeSet? = null, 50 | defStyleAttr: Int = 0, 51 | defStyleRes: Int = 0 52 | ) : View(context, attrs, defStyleAttr) { 53 | /** 54 | * Used to scale the dp units to pixels 55 | */ 56 | private val DPTOPX_SCALE = resources.displayMetrics.density 57 | 58 | /** 59 | * `Paint` instance used to draw the inactive circle. 60 | */ 61 | private val circlePaint: Paint = Paint() 62 | 63 | /** 64 | * `Paint` instance used to draw the circle fill. 65 | */ 66 | private val circleFillPaint: Paint = Paint() 67 | 68 | /** 69 | * `Paint` instance used to draw the active circle (represents progress). 70 | */ 71 | private val circleProgressPaint: Paint = Paint() 72 | 73 | /** 74 | * If progress glow is disabled, there is no glow from the progress bar when filled 75 | * 76 | * NOTE: To enable glow effect, please make sure this view is rendering with hardware 77 | * accelerate disabled. (Checkout this doc for details of hardware accelerate: 78 | * https://developer.android.com/guide/topics/graphics/hardware-accel) 79 | */ 80 | private var disableProgressGlow = false 81 | 82 | /** 83 | * `Paint` instance used to draw the glow from the active circle. 84 | */ 85 | private val circleProgressGlowPaint: Paint = Paint() 86 | 87 | /** 88 | * `Paint` instance used to draw the center of the pointer. 89 | * Note: This is broken on 4.0+, as BlurMasks do not work with hardware acceleration. 90 | */ 91 | private val pointerPaint: Paint = Paint() 92 | 93 | /** 94 | * `Paint` instance used to draw the halo of the pointer. 95 | * Note: The halo is the part that changes transparency. 96 | */ 97 | private val pointerHaloPaint: Paint = Paint() 98 | 99 | /** 100 | * `Paint` instance used to draw the border of the pointer, outside of the halo. 101 | */ 102 | private val pointerHaloBorderPaint: Paint = Paint() 103 | 104 | /** 105 | * The style of the circle, can be butt, round or square. 106 | */ 107 | var circleStyle: Cap = Cap.ROUND 108 | set(style) { 109 | field = style 110 | initPaints() 111 | recalculateAll() 112 | invalidate() 113 | } 114 | 115 | /** 116 | * current in negative half cycle. 117 | */ 118 | private var isInNegativeHalf = false 119 | 120 | /** 121 | * The width of the circle (in pixels). 122 | * Sets the circle stroke width. 123 | * @param width the width of the circle 124 | */ 125 | var circleStrokeWidth = 0f 126 | set(width) { 127 | field = width 128 | initPaints() 129 | recalculateAll() 130 | invalidate() 131 | } 132 | 133 | /** 134 | * The X radius of the circle (in pixels). 135 | */ 136 | private var circleXRadius = 0f 137 | 138 | /** 139 | * The Y radius of the circle (in pixels). 140 | */ 141 | private var circleYRadius = 0f 142 | 143 | /** 144 | * If disable pointer, we can't seek the progress. 145 | */ 146 | var disablePointer = false 147 | set(value) { 148 | field = value 149 | invalidate() 150 | } 151 | 152 | /** 153 | * The radius of the pointer (in pixels). 154 | * 155 | * Sets the pointer pointer stroke width. 156 | * @param width the width of the pointer 157 | */ 158 | var pointerStrokeWidth = 0f 159 | set(width) { 160 | field = width 161 | initPaints() 162 | recalculateAll() 163 | invalidate() 164 | } 165 | 166 | /** 167 | * The width of the pointer halo (in pixels). 168 | */ 169 | private var pointerHaloWidth = 0f 170 | 171 | /** 172 | * The width of the pointer halo border (in pixels). 173 | */ 174 | private var pointerHaloBorderWidth = 0f 175 | 176 | /** 177 | * Angle of the pointer arc. 178 | * Default is 0, the pointer is a circle when angle is 0 and the style is round. 179 | * Can not be less than 0. can not be greater than 360. 180 | * 181 | * Gets the pointer angle. 182 | * @return Angle for the pointer (0..360) 183 | */ 184 | var pointerAngle = 0f 185 | set(angle) { 186 | // Modulo 360 right now to avoid constant conversion 187 | var normalizedAngle = (360f + angle % 360f) % 360f 188 | if (normalizedAngle == 0f) { 189 | normalizedAngle = SMALL_DEGREE_BIAS 190 | } 191 | if (normalizedAngle != field) { 192 | field = normalizedAngle 193 | recalculateAll() 194 | invalidate() 195 | } 196 | } 197 | 198 | /** 199 | * Start angle of the CircularSeekBar. 200 | * Note: If mStartAngle and mEndAngle are set to the same angle, 0.1 is subtracted 201 | * from the mEndAngle to make the circle function properly. 202 | */ 203 | var startAngle = 0f 204 | set(angle) { 205 | field = angle 206 | if (angle % 360f == endAngle % 360f) { 207 | //mStartAngle = mStartAngle + 1f; 208 | endAngle -= SMALL_DEGREE_BIAS 209 | } 210 | recalculateAll() 211 | invalidate() 212 | } 213 | 214 | /** 215 | * End angle of the CircularSeekBar. 216 | * Note: If mStartAngle and mEndAngle are set to the same angle, 0.1 is subtracted 217 | * from the mEndAngle to make the circle function properly. 218 | */ 219 | var endAngle = 0f 220 | set(angle) { 221 | field = if (startAngle % 360f == endAngle % 360f) { 222 | //mStartAngle = mStartAngle + 1f; 223 | angle - SMALL_DEGREE_BIAS 224 | } else { 225 | angle 226 | } 227 | recalculateAll() 228 | invalidate() 229 | } 230 | 231 | /** 232 | * `RectF` that represents the circle (or ellipse) of the seekbar. 233 | */ 234 | val pathCircle: RectF = RectF() 235 | 236 | /** 237 | * Holds the color value for `mPointerPaint` before the `Paint` instance is created. 238 | * 239 | * Gets the pointer color. 240 | * 241 | * Sets the pointer color. 242 | * @param color the color of the pointer 243 | * @return An integer color value for the pointer 244 | */ 245 | var pointerColor = DEFAULT_POINTER_COLOR 246 | set(color) { 247 | field = color 248 | pointerPaint.color = color 249 | invalidate() 250 | } 251 | 252 | /** 253 | * Holds the color value for `mPointerHaloPaint` before the `Paint` instance is created. 254 | * 255 | * Gets the pointer halo color. 256 | * 257 | * Sets the pointer halo color. 258 | * 259 | * @param color the color of the pointer halo 260 | * @return An integer color value for the pointer halo 261 | */ 262 | var pointerHaloColor = DEFAULT_POINTER_HALO_COLOR 263 | set(color) { 264 | field = color 265 | pointerHaloPaint.color = field 266 | invalidate() 267 | } 268 | 269 | /** 270 | * Holds the color value for `mPointerHaloPaint` before the `Paint` instance is created. 271 | */ 272 | private var pointerHaloColorOnTouch = DEFAULT_POINTER_HALO_COLOR_ONTOUCH 273 | 274 | /** 275 | * Holds the color value for `mCirclePaint` before the `Paint` instance is created. 276 | * 277 | * Gets the circle color. 278 | * 279 | * Sets the circle color. 280 | * @param color the color of the circle 281 | * @return An integer color value for the circle 282 | */ 283 | var circleColor = DEFAULT_CIRCLE_COLOR 284 | set(color) { 285 | field = color 286 | circlePaint.color = color 287 | invalidate() 288 | } 289 | 290 | /** 291 | * Holds the color value for `mCircleFillPaint` before the `Paint` instance is created. 292 | * 293 | * Gets the circle fill color. 294 | * 295 | * Sets the circle fill color. 296 | * @param color the color of the circle fill 297 | * @return An integer color value for the circle fill 298 | */ 299 | var circleFillColor = DEFAULT_CIRCLE_FILL_COLOR 300 | set(color) { 301 | field = color 302 | circleFillPaint.color = color 303 | invalidate() 304 | } 305 | 306 | /** 307 | * Holds the color value for `mCircleProgressPaint` before the `Paint` instance is created. 308 | * 309 | * Gets the circle progress color. 310 | * 311 | * Sets the circle progress color. 312 | * @param color the color of the circle progress 313 | * @return An integer color value for the circle progress 314 | */ 315 | var circleProgressColor = DEFAULT_CIRCLE_PROGRESS_COLOR 316 | set(color) { 317 | field = color 318 | circleProgressPaint.color = color 319 | invalidate() 320 | } 321 | 322 | /** 323 | * Holds the alpha value for `mPointerHaloPaint`. 324 | * 325 | * Gets the pointer alpha value. 326 | * 327 | * Sets the pointer alpha. 328 | * @param alpha the alpha of the pointer 329 | * @return An integer alpha value for the pointer (0..255) 330 | */ 331 | var pointerAlpha = DEFAULT_POINTER_ALPHA 332 | set(alpha) { 333 | if (alpha in 0..255) { 334 | field = alpha 335 | pointerHaloPaint.alpha = alpha 336 | invalidate() 337 | } 338 | } 339 | 340 | /** 341 | * Holds the OnTouch alpha value for `mPointerHaloPaint`. 342 | * 343 | * Gets the pointer alpha value when touched. 344 | * 345 | * Sets the pointer alpha when touched. 346 | * @param alpha the alpha of the pointer (0..255) when touched 347 | * @return An integer alpha value for the pointer (0..255) when touched 348 | */ 349 | var pointerAlphaOnTouch = DEFAULT_POINTER_ALPHA_ONTOUCH 350 | set(alpha) { 351 | if (alpha in 0..255) { 352 | field = alpha 353 | } 354 | } 355 | 356 | /** 357 | * Distance (in degrees) that the the circle/semi-circle makes up. 358 | * This amount represents the max of the circle in degrees. 359 | */ 360 | private var totalCircleDegrees = 0f 361 | 362 | /** 363 | * Distance (in degrees) that the current progress makes up in the circle. 364 | */ 365 | private var progressDegrees = 0f 366 | 367 | /** 368 | * `Path` used to draw the circle/semi-circle. 369 | */ 370 | private val circlePath: Path = Path() 371 | 372 | /** 373 | * `Path` used to draw the progress on the circle. 374 | */ 375 | private val circleProgressPath: Path = Path() 376 | 377 | /** 378 | * `Path` used to draw the pointer arc on the circle. 379 | */ 380 | private val circlePointerPath: Path = Path() 381 | 382 | /** 383 | * Max value that this CircularSeekBar is representing. 384 | * 385 | * Get the current max of the CircularSeekBar. 386 | * 387 | * Set the max of the CircularSeekBar. 388 | * If the new max is less than the current progress, then the progress will be set to zero. 389 | * If the progress is changed as a result, then any listener will receive a onProgressChanged event. 390 | * @param max The new max for the CircularSeekBar. 391 | * @return Synchronized integer value of the max. 392 | */ 393 | @get:Synchronized 394 | var max = 0f 395 | set(max) { 396 | if (max > 0) { 397 | if (max <= progressActual) { 398 | progressActual = 0f // If the new max is less than current progress, set progress to zero 399 | onCircularSeekBarChangeListener?.onProgressChanged( 400 | this, 401 | if (isInNegativeHalf) -progressActual else progressActual, 402 | false 403 | ) 404 | } 405 | field = max 406 | recalculateAll() 407 | invalidate() 408 | } 409 | } 410 | 411 | /** 412 | * Progress value that this CircularSeekBar is representing. 413 | */ 414 | private var progressActual = 0f 415 | 416 | /** 417 | * Get the progress of the CircularSeekBar. 418 | * 419 | * Set the progress of the CircularSeekBar. 420 | * If the progress is the same, then any listener will not receive a onProgressChanged event. 421 | * 422 | * @param progress The progress to set the CircularSeekBar to. 423 | * @return The progress of the CircularSeekBar. 424 | */ 425 | var progress: Float 426 | get() { 427 | val progress = max * progressDegrees / totalCircleDegrees 428 | return if (isInNegativeHalf) -progress else progress 429 | } 430 | set(progress) { 431 | if (progressActual != progress) { 432 | if (isNegativeEnabled) { 433 | if (progress < 0) { 434 | progressActual = -progress 435 | isInNegativeHalf = true 436 | } else { 437 | progressActual = progress 438 | isInNegativeHalf = false 439 | } 440 | } else { 441 | progressActual = progress 442 | } 443 | 444 | onCircularSeekBarChangeListener?.onProgressChanged(this, progress, false) 445 | 446 | recalculateAll() 447 | invalidate() 448 | } 449 | } 450 | 451 | /** 452 | * Used for enabling/disabling the negative progress bar. 453 | */ 454 | var isNegativeEnabled = false 455 | 456 | /** 457 | * If true, then the user can specify the X and Y radii. 458 | * If false, then the View itself determines the size of the CircularSeekBar. 459 | */ 460 | private var customRadii = false 461 | 462 | /** 463 | * Maintain a perfect circle (equal x and y radius), regardless of view or custom attributes. 464 | * The smaller of the two radii will always be used in this case. 465 | * The default is to be a circle and not an ellipse, due to the behavior of the ellipse. 466 | */ 467 | private var maintainEqualCircle = false 468 | 469 | /** 470 | * Once a user has touched the circle, this determines if moving outside the circle is able 471 | * to change the position of the pointer (and in turn, the progress). 472 | */ 473 | private var moveOutsideCircle = false 474 | 475 | /** 476 | * Used for enabling/disabling the lock option for easier hitting of the 0 progress mark. 477 | */ 478 | var isLockEnabled = true 479 | 480 | /** 481 | * Used for when the user moves beyond the start of the circle when moving counter clockwise. 482 | * Makes it easier to hit the 0 progress mark. 483 | */ 484 | private var lockAtStart = true 485 | 486 | /** 487 | * Used for when the user moves beyond the end of the circle when moving clockwise. 488 | * Makes it easier to hit the 100% (max) progress mark. 489 | */ 490 | private var lockAtEnd = false 491 | 492 | /** 493 | * If progress is zero, hide the progress bar. 494 | */ 495 | private var hideProgressWhenEmpty = false 496 | 497 | /** 498 | * When the user is touching the circle on ACTION_DOWN, this is set to true. 499 | * Used when touching the CircularSeekBar. 500 | */ 501 | private var userIsMovingPointer = false 502 | 503 | /** 504 | * The width of the circle used in the `RectF` that is used to draw it. 505 | * Based on either the View width or the custom X radius. 506 | */ 507 | private var circleWidth = 0f 508 | 509 | /** 510 | * The height of the circle used in the `RectF` that is used to draw it. 511 | * Based on either the View width or the custom Y radius. 512 | */ 513 | private var circleHeight = 0f 514 | 515 | /** 516 | * Represents the progress mark on the circle, in geometric degrees. 517 | * This is not provided by the user; it is calculated; 518 | */ 519 | private var pointerPosition = 0f 520 | 521 | /** 522 | * Pointer position in terms of X and Y coordinates. 523 | */ 524 | private val pointerPositionXY = FloatArray(2) 525 | 526 | /** 527 | * Listener. 528 | */ 529 | private var onCircularSeekBarChangeListener: OnCircularSeekBarChangeListener? = null 530 | 531 | init { 532 | val attrArray = context.theme.obtainStyledAttributes(attrs, R.styleable.cs_CircularSeekBar, defStyleAttr, defStyleRes) 533 | initPaints() 534 | initAttributes(attrArray) 535 | attrArray.recycle() 536 | } 537 | 538 | /** 539 | * Initialize the CircularSeekBar with the attributes from the XML style. 540 | * Uses the defaults defined at the top of this file when an attribute is not specified by the user. 541 | * @param attrArray TypedArray containing the attributes. 542 | */ 543 | private fun initAttributes(attrArray: TypedArray) { 544 | circleXRadius = attrArray.getDimension( 545 | R.styleable.cs_CircularSeekBar_cs_circle_x_radius, 546 | DEFAULT_CIRCLE_X_RADIUS 547 | ) 548 | circleYRadius = attrArray.getDimension( 549 | R.styleable.cs_CircularSeekBar_cs_circle_y_radius, 550 | DEFAULT_CIRCLE_Y_RADIUS 551 | ) 552 | pointerStrokeWidth = attrArray.getDimension( 553 | R.styleable.cs_CircularSeekBar_cs_pointer_stroke_width, 554 | DEFAULT_POINTER_STROKE_WIDTH 555 | ) 556 | pointerHaloWidth = attrArray.getDimension( 557 | R.styleable.cs_CircularSeekBar_cs_pointer_halo_width, 558 | DEFAULT_POINTER_HALO_WIDTH 559 | ) 560 | pointerHaloBorderWidth = attrArray.getDimension( 561 | R.styleable.cs_CircularSeekBar_cs_pointer_halo_border_width, 562 | DEFAULT_POINTER_HALO_BORDER_WIDTH 563 | ) 564 | circleStrokeWidth = attrArray.getDimension( 565 | R.styleable.cs_CircularSeekBar_cs_circle_stroke_width, 566 | DEFAULT_CIRCLE_STROKE_WIDTH 567 | ) 568 | val circleStyleAttribute = 569 | attrArray.getInt(R.styleable.cs_CircularSeekBar_cs_circle_style, DEFAULT_CIRCLE_STYLE) 570 | circleStyle = Cap.values()[circleStyleAttribute] 571 | pointerColor = attrArray.getColor( 572 | R.styleable.cs_CircularSeekBar_cs_pointer_color, 573 | DEFAULT_POINTER_COLOR 574 | ) 575 | pointerHaloColor = attrArray.getColor( 576 | R.styleable.cs_CircularSeekBar_cs_pointer_halo_color, 577 | DEFAULT_POINTER_HALO_COLOR 578 | ) 579 | pointerHaloColorOnTouch = attrArray.getColor( 580 | R.styleable.cs_CircularSeekBar_cs_pointer_halo_color_ontouch, 581 | DEFAULT_POINTER_HALO_COLOR_ONTOUCH 582 | ) 583 | circleColor = 584 | attrArray.getColor(R.styleable.cs_CircularSeekBar_cs_circle_color, DEFAULT_CIRCLE_COLOR) 585 | circleProgressColor = attrArray.getColor( 586 | R.styleable.cs_CircularSeekBar_cs_circle_progress_color, 587 | DEFAULT_CIRCLE_PROGRESS_COLOR 588 | ) 589 | circleFillColor = attrArray.getColor( 590 | R.styleable.cs_CircularSeekBar_cs_circle_fill, 591 | DEFAULT_CIRCLE_FILL_COLOR 592 | ) 593 | pointerAlpha = Color.alpha(pointerHaloColor) 594 | pointerAlphaOnTouch = attrArray.getInt( 595 | R.styleable.cs_CircularSeekBar_cs_pointer_alpha_ontouch, 596 | DEFAULT_POINTER_ALPHA_ONTOUCH 597 | ) 598 | if (pointerAlphaOnTouch > 255 || pointerAlphaOnTouch < 0) { 599 | pointerAlphaOnTouch = DEFAULT_POINTER_ALPHA_ONTOUCH 600 | } 601 | max = attrArray.getInt(R.styleable.cs_CircularSeekBar_cs_max, DEFAULT_MAX).toFloat() 602 | progressActual = 603 | attrArray.getInt(R.styleable.cs_CircularSeekBar_cs_progress, DEFAULT_PROGRESS).toFloat() 604 | customRadii = attrArray.getBoolean( 605 | R.styleable.cs_CircularSeekBar_cs_use_custom_radii, 606 | DEFAULT_USE_CUSTOM_RADII 607 | ) 608 | maintainEqualCircle = attrArray.getBoolean( 609 | R.styleable.cs_CircularSeekBar_cs_maintain_equal_circle, 610 | DEFAULT_MAINTAIN_EQUAL_CIRCLE 611 | ) 612 | moveOutsideCircle = attrArray.getBoolean( 613 | R.styleable.cs_CircularSeekBar_cs_move_outside_circle, 614 | DEFAULT_MOVE_OUTSIDE_CIRCLE 615 | ) 616 | isLockEnabled = attrArray.getBoolean( 617 | R.styleable.cs_CircularSeekBar_cs_lock_enabled, 618 | DEFAULT_LOCK_ENABLED 619 | ) 620 | disablePointer = attrArray.getBoolean( 621 | R.styleable.cs_CircularSeekBar_cs_disable_pointer, 622 | DEFAULT_DISABLE_POINTER 623 | ) 624 | isNegativeEnabled = attrArray.getBoolean( 625 | R.styleable.cs_CircularSeekBar_cs_negative_enabled, 626 | DEFAULT_NEGATIVE_ENABLED 627 | ) 628 | isInNegativeHalf = false 629 | disableProgressGlow = attrArray.getBoolean( 630 | R.styleable.cs_CircularSeekBar_cs_disable_progress_glow, 631 | DEFAULT_DISABLE_PROGRESS_GLOW 632 | ) 633 | hideProgressWhenEmpty = attrArray.getBoolean( 634 | R.styleable.cs_CircularSeekBar_cs_hide_progress_when_empty, 635 | DEFAULT_CS_HIDE_PROGRESS_WHEN_EMPTY 636 | ) 637 | 638 | // Modulo 360 right now to avoid constant conversion 639 | startAngle = (360f + attrArray.getFloat( 640 | R.styleable.cs_CircularSeekBar_cs_start_angle, 641 | DEFAULT_START_ANGLE 642 | ) % 360f) % 360f 643 | endAngle = (360f + attrArray.getFloat( 644 | R.styleable.cs_CircularSeekBar_cs_end_angle, 645 | DEFAULT_END_ANGLE 646 | ) % 360f) % 360f 647 | 648 | // Disable negative progress if is semi-oval. 649 | if (startAngle != endAngle) { 650 | isNegativeEnabled = false 651 | } 652 | if (startAngle % 360f == endAngle % 360f) { 653 | //mStartAngle = mStartAngle + 1f; 654 | endAngle = endAngle - SMALL_DEGREE_BIAS 655 | } 656 | 657 | // Modulo 360 right now to avoid constant conversion 658 | pointerAngle = (360f + attrArray.getFloat( 659 | R.styleable.cs_CircularSeekBar_cs_pointer_angle, 660 | DEFAULT_POINTER_ANGLE 661 | ) % 360f) % 360f 662 | if (pointerAngle == 0f) { 663 | pointerAngle = SMALL_DEGREE_BIAS 664 | } 665 | if (disablePointer) { 666 | pointerStrokeWidth = 0f 667 | pointerHaloWidth = 0f 668 | pointerHaloBorderWidth = 0f 669 | } 670 | } 671 | 672 | /** 673 | * Initializes the `Paint` objects with the appropriate styles. 674 | */ 675 | private fun initPaints() { 676 | circlePaint.isAntiAlias = true 677 | circlePaint.isDither = true 678 | circlePaint.color = circleColor 679 | circlePaint.strokeWidth = circleStrokeWidth 680 | circlePaint.style = Paint.Style.STROKE 681 | circlePaint.strokeJoin = Paint.Join.ROUND 682 | circlePaint.strokeCap = circleStyle 683 | 684 | circleFillPaint.isAntiAlias = true 685 | circleFillPaint.isDither = true 686 | circleFillPaint.color = circleFillColor 687 | circleFillPaint.style = Paint.Style.FILL 688 | 689 | circleProgressPaint.isAntiAlias = true 690 | circleProgressPaint.isDither = true 691 | circleProgressPaint.color = circleProgressColor 692 | circleProgressPaint.strokeWidth = circleStrokeWidth 693 | circleProgressPaint.style = Paint.Style.STROKE 694 | circleProgressPaint.strokeJoin = Paint.Join.ROUND 695 | circleProgressPaint.strokeCap = circleStyle 696 | if (!disableProgressGlow) { 697 | circleProgressGlowPaint.set(circleProgressPaint) 698 | circleProgressGlowPaint.maskFilter = BlurMaskFilter( 699 | PROGRESS_GLOW_RADIUS_DP * DPTOPX_SCALE, 700 | BlurMaskFilter.Blur.NORMAL 701 | ) 702 | } 703 | 704 | pointerPaint.isAntiAlias = true 705 | pointerPaint.isDither = true 706 | pointerPaint.color = pointerColor 707 | pointerPaint.strokeWidth = pointerStrokeWidth 708 | pointerPaint.style = Paint.Style.STROKE 709 | pointerPaint.strokeJoin = Paint.Join.ROUND 710 | pointerPaint.strokeCap = circleStyle 711 | 712 | pointerHaloPaint.set(pointerPaint) 713 | pointerHaloPaint.color = pointerHaloColor 714 | pointerHaloPaint.alpha = pointerAlpha 715 | pointerHaloPaint.strokeWidth = pointerStrokeWidth + pointerHaloWidth * 2f 716 | 717 | pointerHaloBorderPaint.set(pointerPaint) 718 | pointerHaloBorderPaint.strokeWidth = pointerHaloBorderWidth 719 | pointerHaloBorderPaint.style = Paint.Style.STROKE 720 | } 721 | 722 | /** 723 | * Calculates the total degrees between mStartAngle and mEndAngle, and sets mTotalCircleDegrees 724 | * to this value. 725 | */ 726 | private fun calculateTotalDegrees() { 727 | totalCircleDegrees = 728 | (360f - (startAngle - endAngle)) % 360f // Length of the entire circle/arc 729 | if (totalCircleDegrees <= 0f) { 730 | totalCircleDegrees = 360f 731 | } 732 | } 733 | 734 | /** 735 | * Calculate the degrees that the progress represents. Also called the sweep angle. 736 | * Sets mProgressDegrees to that value. 737 | */ 738 | private fun calculateProgressDegrees() { 739 | progressDegrees = 740 | if (isInNegativeHalf) startAngle - pointerPosition else pointerPosition - startAngle // Verified 741 | progressDegrees = 742 | if (progressDegrees < 0) 360f + progressDegrees else progressDegrees // Verified 743 | } 744 | 745 | /** 746 | * Calculate the pointer position (and the end of the progress arc) in degrees. 747 | * Sets mPointerPosition to that value. 748 | */ 749 | private fun calculatePointerPosition() { 750 | val progressPercent = progressActual / max 751 | val progressDegree = progressPercent * totalCircleDegrees 752 | pointerPosition = startAngle + if (isInNegativeHalf) -progressDegree else progressDegree 753 | pointerPosition = 754 | (if (pointerPosition < 0) 360f + pointerPosition else pointerPosition) % 360f 755 | } 756 | 757 | private fun calculatePointerXYPosition() { 758 | var pm = PathMeasure(circleProgressPath, false) 759 | val returnValue = pm.getPosTan(pm.length, pointerPositionXY, null) 760 | if (!returnValue) { 761 | pm = PathMeasure(circlePath, false) 762 | pm.getPosTan(0f, pointerPositionXY, null) 763 | } 764 | } 765 | 766 | /** 767 | * Reset the `Path` objects with the appropriate values. 768 | */ 769 | private fun resetPaths() { 770 | if (isInNegativeHalf) { 771 | circlePath.reset() 772 | circlePath.addArc(pathCircle, startAngle - totalCircleDegrees, totalCircleDegrees) 773 | 774 | // beside progress path it self, we also draw a extend arc to math the pointer arc. 775 | val extendStart = startAngle - progressDegrees - pointerAngle / 2.0f 776 | var extendDegrees = progressDegrees + pointerAngle 777 | if (extendDegrees >= 360f) { 778 | extendDegrees = 360f - SMALL_DEGREE_BIAS 779 | } 780 | circleProgressPath.reset() 781 | circleProgressPath.addArc(pathCircle, extendStart, extendDegrees) 782 | val pointerStart = pointerPosition - pointerAngle / 2.0f 783 | circlePointerPath.reset() 784 | circlePointerPath.addArc(pathCircle, pointerStart, pointerAngle) 785 | } else { 786 | circlePath.reset() 787 | circlePath.addArc(pathCircle, startAngle, totalCircleDegrees) 788 | 789 | // beside progress path it self, we also draw a extend arc to math the pointer arc. 790 | val extendStart = startAngle - pointerAngle / 2.0f 791 | var extendDegrees = progressDegrees + pointerAngle 792 | if (extendDegrees >= 360f) { 793 | extendDegrees = 360f - SMALL_DEGREE_BIAS 794 | } 795 | circleProgressPath.reset() 796 | circleProgressPath.addArc(pathCircle, extendStart, extendDegrees) 797 | val pointerStart = pointerPosition - pointerAngle / 2.0f 798 | circlePointerPath.reset() 799 | circlePointerPath.addArc(pathCircle, pointerStart, pointerAngle) 800 | } 801 | } 802 | 803 | /** 804 | * Initialize the `RectF` objects with the appropriate values. 805 | */ 806 | private fun resetRects() { 807 | pathCircle[-circleWidth, -circleHeight, circleWidth] = circleHeight 808 | } 809 | 810 | override fun onDraw(canvas: Canvas) { 811 | super.onDraw(canvas) 812 | canvas.translate(width / 2f, height / 2f) 813 | canvas.drawPath(circlePath, circleFillPaint) 814 | canvas.drawPath(circlePath, circlePaint) 815 | val ableToGoNegative = 816 | isNegativeEnabled && Math.abs(totalCircleDegrees - 360f) < SMALL_DEGREE_BIAS * 2 817 | // Hide progress bar when progress is 0 818 | // Also make sure we still draw progress when has pointer or able to go negative 819 | val shouldHideProgress = hideProgressWhenEmpty && progressDegrees == 0f && 820 | disablePointer && !ableToGoNegative 821 | if (!shouldHideProgress) { 822 | if (!disableProgressGlow) { 823 | canvas.drawPath(circleProgressPath, circleProgressGlowPaint) 824 | } 825 | canvas.drawPath(circleProgressPath, circleProgressPaint) 826 | } 827 | if (!disablePointer) { 828 | if (userIsMovingPointer) { 829 | canvas.drawPath(circlePointerPath, pointerHaloPaint) 830 | } 831 | canvas.drawPath(circlePointerPath, pointerPaint) 832 | // TODO, find a good way to draw halo border. 833 | // if (mUserIsMovingPointer) { 834 | // canvas.drawCircle(mPointerPositionXY[0], mPointerPositionXY[1], 835 | // (mPointerStrokeWidth /2f) + mPointerHaloWidth + (mPointerHaloBorderWidth / 2f), 836 | // mPointerHaloBorderPaint); 837 | // } 838 | } 839 | } 840 | 841 | private fun setProgressBasedOnAngle(angle: Float) { 842 | pointerPosition = angle 843 | calculateProgressDegrees() 844 | progressActual = max * progressDegrees / totalCircleDegrees 845 | } 846 | 847 | private fun recalculateAll() { 848 | calculateTotalDegrees() 849 | calculatePointerPosition() 850 | calculateProgressDegrees() 851 | resetRects() 852 | resetPaths() 853 | calculatePointerXYPosition() 854 | } 855 | 856 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 857 | var height = getDefaultSize(suggestedMinimumHeight, heightMeasureSpec) 858 | var width = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec) 859 | if (height == 0) height = width 860 | if (width == 0) width = height 861 | if (maintainEqualCircle) { 862 | val min = Math.min(width, height) 863 | setMeasuredDimension(min, min) 864 | } else { 865 | setMeasuredDimension(width, height) 866 | } 867 | val isHardwareAccelerated = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && 868 | isHardwareAccelerated && layerType != LAYER_TYPE_SOFTWARE 869 | val hasGlowEffect = !disableProgressGlow && !isHardwareAccelerated 870 | 871 | // Set the circle width and height based on the view for the moment 872 | val padding = Math.max( 873 | circleStrokeWidth / 2f, 874 | pointerStrokeWidth / 2 + pointerHaloWidth + pointerHaloBorderWidth 875 | ) + 876 | if (hasGlowEffect) PROGRESS_GLOW_RADIUS_DP * DPTOPX_SCALE else 0f 877 | circleHeight = height / 2f - padding 878 | circleWidth = width / 2f - padding 879 | 880 | // If it is not set to use custom 881 | if (customRadii) { 882 | // Check to make sure the custom radii are not out of the view. If they are, just use the view values 883 | if (circleYRadius - padding < circleHeight) { 884 | circleHeight = circleYRadius - padding 885 | } 886 | if (circleXRadius - padding < circleWidth) { 887 | circleWidth = circleXRadius - padding 888 | } 889 | } 890 | if (maintainEqualCircle) { // Applies regardless of how the values were determined 891 | val min = Math.min(circleHeight, circleWidth) 892 | circleHeight = min 893 | circleWidth = min 894 | } 895 | recalculateAll() 896 | } 897 | 898 | override fun onTouchEvent(event: MotionEvent): Boolean { 899 | if (disablePointer || !isEnabled) return false 900 | 901 | // Convert coordinates to our internal coordinate system 902 | val x = event.x - width / 2 903 | val y = event.y - height / 2 904 | 905 | // Get the distance from the center of the circle in terms of x and y 906 | val distanceX = pathCircle.centerX() - x 907 | val distanceY = pathCircle.centerY() - y 908 | 909 | // Get the distance from the center of the circle in terms of a radius 910 | val touchEventRadius = 911 | Math.sqrt(Math.pow(distanceX.toDouble(), 2.0) + Math.pow(distanceY.toDouble(), 2.0)) 912 | .toFloat() 913 | val minimumTouchTarget = 914 | MIN_TOUCH_TARGET_DP * DPTOPX_SCALE // Convert minimum touch target into px 915 | var additionalRadius: Float // Either uses the minimumTouchTarget size or larger if the ring/pointer is larger 916 | additionalRadius = 917 | if (circleStrokeWidth < minimumTouchTarget) { // If the width is less than the minimumTouchTarget, use the minimumTouchTarget 918 | minimumTouchTarget / 2 919 | } else { 920 | circleStrokeWidth / 2 // Otherwise use the width 921 | } 922 | val outerRadius = Math.max( 923 | circleHeight, 924 | circleWidth 925 | ) + additionalRadius // Max outer radius of the circle, including the minimumTouchTarget or wheel width 926 | val innerRadius = Math.min( 927 | circleHeight, 928 | circleWidth 929 | ) - additionalRadius // Min inner radius of the circle, including the minimumTouchTarget or wheel width 930 | additionalRadius = 931 | if (pointerStrokeWidth < minimumTouchTarget / 2) { // If the pointer radius is less than the minimumTouchTarget, use the minimumTouchTarget 932 | minimumTouchTarget / 2 933 | } else { 934 | pointerStrokeWidth // Otherwise use the radius 935 | } 936 | var touchAngle: Float 937 | touchAngle = 938 | (Math.atan2(y.toDouble(), x.toDouble()) / Math.PI * 180 % 360).toFloat() // Verified 939 | touchAngle = if (touchAngle < 0) 360 + touchAngle else touchAngle // Verified 940 | 941 | /* 942 | Represents the clockwise distance from {@code mStartAngle} to the touch angle. 943 | Used when touching the CircularSeekBar. 944 | */ 945 | var cwDistanceFromStart: Float 946 | 947 | /* 948 | Represents the counter-clockwise distance from {@code mStartAngle} to the touch angle. 949 | Used when touching the CircularSeekBar. 950 | */ 951 | val ccwDistanceFromStart: Float 952 | 953 | /* 954 | Represents the clockwise distance from {@code mEndAngle} to the touch angle. 955 | Used when touching the CircularSeekBar. 956 | */ 957 | var cwDistanceFromEnd: Float 958 | 959 | /* 960 | Represents the counter-clockwise distance from {@code mEndAngle} to the touch angle. 961 | Used when touching the CircularSeekBar. 962 | Currently unused, but kept just in case. 963 | */ 964 | @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") 965 | val ccwDistanceFromEnd: Float 966 | 967 | /* 968 | Represents the clockwise distance from {@code mPointerPosition} to the touch angle. 969 | Used when touching the CircularSeekBar. 970 | */ 971 | var cwDistanceFromPointer: Float 972 | 973 | /* 974 | Represents the counter-clockwise distance from {@code mPointerPosition} to the touch angle. 975 | Used when touching the CircularSeekBar. 976 | */ 977 | val ccwDistanceFromPointer: Float 978 | cwDistanceFromStart = touchAngle - startAngle // Verified 979 | cwDistanceFromStart = 980 | if (cwDistanceFromStart < 0) 360f + cwDistanceFromStart else cwDistanceFromStart // Verified 981 | ccwDistanceFromStart = 360f - cwDistanceFromStart // Verified 982 | cwDistanceFromEnd = touchAngle - endAngle // Verified 983 | cwDistanceFromEnd = 984 | if (cwDistanceFromEnd < 0) 360f + cwDistanceFromEnd else cwDistanceFromEnd // Verified 985 | ccwDistanceFromEnd = 360f - cwDistanceFromEnd // Verified 986 | when (event.action) { 987 | MotionEvent.ACTION_DOWN -> { 988 | // These are only used for ACTION_DOWN for handling if the pointer was the part that was touched 989 | val pointerRadiusDegrees = (pointerStrokeWidth * 180 / (Math.PI * Math.max( 990 | circleHeight, 991 | circleWidth 992 | ))).toFloat() 993 | val pointerDegrees = Math.max(pointerRadiusDegrees, pointerAngle / 2f) 994 | cwDistanceFromPointer = touchAngle - pointerPosition 995 | cwDistanceFromPointer = 996 | if (cwDistanceFromPointer < 0) 360f + cwDistanceFromPointer else cwDistanceFromPointer 997 | ccwDistanceFromPointer = 360f - cwDistanceFromPointer 998 | // This is for if the first touch is on the actual pointer. 999 | if (touchEventRadius >= innerRadius && touchEventRadius <= outerRadius && 1000 | (cwDistanceFromPointer <= pointerDegrees || ccwDistanceFromPointer <= pointerDegrees) 1001 | ) { 1002 | setProgressBasedOnAngle(pointerPosition) 1003 | pointerHaloPaint.alpha = pointerAlphaOnTouch 1004 | pointerHaloPaint.color = pointerHaloColorOnTouch 1005 | recalculateAll() 1006 | invalidate() 1007 | onCircularSeekBarChangeListener?.onStartTrackingTouch(this) 1008 | userIsMovingPointer = true 1009 | lockAtEnd = false 1010 | lockAtStart = false 1011 | } else if (cwDistanceFromStart > totalCircleDegrees) { // If the user is touching outside of the start AND end 1012 | userIsMovingPointer = false 1013 | return false 1014 | } else if (touchEventRadius >= innerRadius && touchEventRadius <= outerRadius) { // If the user is touching near the circle 1015 | setProgressBasedOnAngle(touchAngle) 1016 | pointerHaloPaint.alpha = pointerAlphaOnTouch 1017 | pointerHaloPaint.color = pointerHaloColorOnTouch 1018 | recalculateAll() 1019 | invalidate() 1020 | onCircularSeekBarChangeListener?.onStartTrackingTouch(this) 1021 | onCircularSeekBarChangeListener?.onProgressChanged(this, progress, true) 1022 | userIsMovingPointer = true 1023 | lockAtEnd = false 1024 | lockAtStart = false 1025 | } else { // If the user is not touching near the circle 1026 | userIsMovingPointer = false 1027 | return false 1028 | } 1029 | } 1030 | MotionEvent.ACTION_MOVE -> if (userIsMovingPointer) { 1031 | val smallInCircle = totalCircleDegrees / 3f 1032 | var cwPointerFromStart = pointerPosition - startAngle 1033 | cwPointerFromStart = 1034 | if (cwPointerFromStart < 0) cwPointerFromStart + 360f else cwPointerFromStart 1035 | val touchOverStart = ccwDistanceFromStart < smallInCircle 1036 | val touchOverEnd = cwDistanceFromEnd < smallInCircle 1037 | val pointerNearStart = cwPointerFromStart < smallInCircle 1038 | val pointerNearEnd = cwPointerFromStart > totalCircleDegrees - smallInCircle 1039 | val progressNearZero = progressActual < max / 3f 1040 | val progressNearMax = progressActual > max / 3f * 2f 1041 | if (progressNearMax) { // logic for end lock. 1042 | if (pointerNearStart) { // negative end 1043 | lockAtEnd = touchOverStart 1044 | } else if (pointerNearEnd) { // positive end 1045 | lockAtEnd = touchOverEnd 1046 | } 1047 | } else if (progressNearZero && isNegativeEnabled) { // logic for negative flip 1048 | if (touchOverEnd) isInNegativeHalf = false else if (touchOverStart) { 1049 | isInNegativeHalf = true 1050 | } 1051 | } else if (progressNearZero) { // logic for start lock 1052 | if (pointerNearStart) { 1053 | lockAtStart = touchOverStart 1054 | } 1055 | } 1056 | if (lockAtStart && isLockEnabled) { 1057 | // TODO: Add a check if mProgress is already 0, in which case don't call the listener 1058 | progressActual = 0f 1059 | recalculateAll() 1060 | invalidate() 1061 | onCircularSeekBarChangeListener?.onProgressChanged(this, progress, true) 1062 | } else if (lockAtEnd && isLockEnabled) { 1063 | progressActual = max 1064 | recalculateAll() 1065 | invalidate() 1066 | onCircularSeekBarChangeListener?.onProgressChanged(this, progress, true) 1067 | } else if (moveOutsideCircle || touchEventRadius <= outerRadius) { 1068 | if (cwDistanceFromStart <= totalCircleDegrees) { 1069 | setProgressBasedOnAngle(touchAngle) 1070 | } 1071 | recalculateAll() 1072 | invalidate() 1073 | onCircularSeekBarChangeListener?.onProgressChanged(this, progress, true) 1074 | } 1075 | } else { 1076 | return false 1077 | } 1078 | MotionEvent.ACTION_UP -> { 1079 | pointerHaloPaint.alpha = pointerAlpha 1080 | pointerHaloPaint.color = pointerHaloColor 1081 | if (userIsMovingPointer) { 1082 | userIsMovingPointer = false 1083 | invalidate() 1084 | onCircularSeekBarChangeListener?.onStopTrackingTouch(this) 1085 | } else { 1086 | return false 1087 | } 1088 | } 1089 | MotionEvent.ACTION_CANCEL -> { 1090 | pointerHaloPaint.alpha = pointerAlpha 1091 | pointerHaloPaint.color = pointerHaloColor 1092 | userIsMovingPointer = false 1093 | invalidate() 1094 | } 1095 | } 1096 | if (event.action == MotionEvent.ACTION_MOVE && parent != null) { 1097 | parent.requestDisallowInterceptTouchEvent(true) 1098 | } 1099 | return true 1100 | } 1101 | 1102 | override fun onSaveInstanceState(): Parcelable { 1103 | val superState = super.onSaveInstanceState() 1104 | val state = Bundle() 1105 | state.putParcelable("PARENT", superState) 1106 | state.putFloat("MAX", max) 1107 | state.putFloat("PROGRESS", progressActual) 1108 | state.putInt("circleColor", circleColor) 1109 | state.putInt("circleProgressColor", circleProgressColor) 1110 | state.putInt("pointerColor", pointerColor) 1111 | state.putInt("pointerHaloColor", pointerHaloColor) 1112 | state.putInt("pointerHaloColorOnTouch", pointerHaloColorOnTouch) 1113 | state.putInt("pointerAlpha", pointerAlpha) 1114 | state.putInt("pointerAlphaOnTouch", pointerAlphaOnTouch) 1115 | state.putFloat("pointerAngle", pointerAngle) 1116 | state.putBoolean("disablePointer", disablePointer) 1117 | state.putBoolean("lockEnabled", isLockEnabled) 1118 | state.putBoolean("negativeEnabled", isNegativeEnabled) 1119 | state.putBoolean("disableProgressGlow", disableProgressGlow) 1120 | state.putBoolean("isInNegativeHalf", isInNegativeHalf) 1121 | state.putInt("circleStyle", circleStyle.ordinal) 1122 | state.putBoolean("hideProgressWhenEmpty", hideProgressWhenEmpty) 1123 | return state 1124 | } 1125 | 1126 | override fun onRestoreInstanceState(state: Parcelable) { 1127 | val savedState = state as Bundle 1128 | val superState = savedState.getParcelable("PARENT") 1129 | super.onRestoreInstanceState(superState) 1130 | max = savedState.getFloat("MAX") 1131 | progressActual = savedState.getFloat("PROGRESS") 1132 | circleColor = savedState.getInt("circleColor") 1133 | circleProgressColor = savedState.getInt("circleProgressColor") 1134 | pointerColor = savedState.getInt("pointerColor") 1135 | pointerHaloColor = savedState.getInt("pointerHaloColor") 1136 | pointerHaloColorOnTouch = savedState.getInt("pointerHaloColorOnTouch") 1137 | pointerAlpha = savedState.getInt("pointerAlpha") 1138 | pointerAlphaOnTouch = savedState.getInt("pointerAlphaOnTouch") 1139 | pointerAngle = savedState.getFloat("pointerAngle") 1140 | disablePointer = savedState.getBoolean("disablePointer") 1141 | isLockEnabled = savedState.getBoolean("lockEnabled") 1142 | isNegativeEnabled = savedState.getBoolean("negativeEnabled") 1143 | disableProgressGlow = savedState.getBoolean("disableProgressGlow") 1144 | isInNegativeHalf = savedState.getBoolean("isInNegativeHalf") 1145 | circleStyle = Cap.values()[savedState.getInt("circleStyle")] 1146 | hideProgressWhenEmpty = savedState.getBoolean("hideProgressWhenEmpty") 1147 | initPaints() 1148 | recalculateAll() 1149 | } 1150 | 1151 | fun setOnSeekBarChangeListener(l: OnCircularSeekBarChangeListener?) { 1152 | onCircularSeekBarChangeListener = l 1153 | } 1154 | 1155 | /** 1156 | * custom any `Paint` 1157 | */ 1158 | private fun applyCustomPaint(paint: Paint, block: (Paint) -> Unit) { 1159 | block.invoke(paint) 1160 | invalidate() 1161 | } 1162 | 1163 | /** 1164 | * custom `Paint` instance used to draw the circle fill. 1165 | */ 1166 | fun applyCustomCircleFillPaint(block: (Paint) -> Unit) { 1167 | applyCustomPaint(circleFillPaint, block) 1168 | } 1169 | 1170 | /** 1171 | * custom `Paint` instance used to draw the inactive circle. 1172 | */ 1173 | fun applyCustomCirclePaint(block: (Paint) -> Unit) { 1174 | applyCustomPaint(circlePaint, block) 1175 | } 1176 | 1177 | /** 1178 | * custom `Paint` instance used to draw the active circle (represents progress). 1179 | */ 1180 | fun applyCustomCircleProgressPaint(block: (Paint) -> Unit) { 1181 | applyCustomPaint(circleProgressPaint, block) 1182 | } 1183 | 1184 | /** 1185 | * custom `Paint` instance used to draw the center of the pointer. 1186 | */ 1187 | fun applyCustomPointerPaint(block: (Paint) -> Unit) { 1188 | applyCustomPaint(pointerPaint, block) 1189 | } 1190 | 1191 | /** 1192 | * custom `Paint` instance used to draw the glow from the active circle. 1193 | */ 1194 | fun applyCustomCircleProgressGlowPaint(block: (Paint) -> Unit) { 1195 | applyCustomPaint(circleProgressGlowPaint, block) 1196 | } 1197 | 1198 | /** 1199 | * custom `Paint` instance used to draw the halo of the pointer. 1200 | */ 1201 | fun applyCustomPointerHaloPaint(block: (Paint) -> Unit) { 1202 | applyCustomPaint(pointerHaloPaint, block) 1203 | } 1204 | 1205 | /** 1206 | * custom `Paint` instance used to draw the border of the pointer, outside of the halo. 1207 | */ 1208 | fun applyCustomPointerHaloBorderPaint(block: (Paint) -> Unit) { 1209 | applyCustomPaint(pointerHaloBorderPaint, block) 1210 | } 1211 | 1212 | /** 1213 | * Listener for the CircularSeekBar. Implements the same methods as the normal OnSeekBarChangeListener. 1214 | */ 1215 | interface OnCircularSeekBarChangeListener { 1216 | fun onProgressChanged( 1217 | circularSeekBar: CircularSeekBar?, 1218 | progress: Float, 1219 | fromUser: Boolean 1220 | ) 1221 | 1222 | fun onStopTrackingTouch(seekBar: CircularSeekBar?) 1223 | fun onStartTrackingTouch(seekBar: CircularSeekBar?) 1224 | } 1225 | 1226 | companion object { 1227 | /** 1228 | * Minimum touch target size in DP. 48dp is the Android design recommendation 1229 | */ 1230 | private const val MIN_TOUCH_TARGET_DP = 48f 1231 | 1232 | /** 1233 | * For some case we need the degree to have small bias to avoid overflow. 1234 | */ 1235 | private const val SMALL_DEGREE_BIAS = .1f 1236 | 1237 | /** 1238 | * Radius of progress glow, in dp unit. 1239 | */ 1240 | private const val PROGRESS_GLOW_RADIUS_DP = 5f 1241 | 1242 | // Default values 1243 | private val DEFAULT_CIRCLE_STYLE = Cap.ROUND.ordinal 1244 | private const val DEFAULT_CIRCLE_X_RADIUS = 30f 1245 | private const val DEFAULT_CIRCLE_Y_RADIUS = 30f 1246 | private const val DEFAULT_POINTER_STROKE_WIDTH = 14f 1247 | private const val DEFAULT_POINTER_HALO_WIDTH = 6f 1248 | private const val DEFAULT_POINTER_HALO_BORDER_WIDTH = 0f 1249 | private const val DEFAULT_CIRCLE_STROKE_WIDTH = 5f 1250 | private const val DEFAULT_START_ANGLE = 270f // Geometric (clockwise, relative to 3 o'clock) 1251 | private const val DEFAULT_END_ANGLE = 270f // Geometric (clockwise, relative to 3 o'clock) 1252 | private const val DEFAULT_POINTER_ANGLE = 0f 1253 | private const val DEFAULT_MAX = 100 1254 | private const val DEFAULT_PROGRESS = 0 1255 | private const val DEFAULT_CIRCLE_COLOR = Color.DKGRAY 1256 | private val DEFAULT_CIRCLE_PROGRESS_COLOR = Color.argb(235, 74, 138, 255) 1257 | private val DEFAULT_POINTER_COLOR = Color.argb(235, 74, 138, 255) 1258 | private val DEFAULT_POINTER_HALO_COLOR = Color.argb(135, 74, 138, 255) 1259 | private val DEFAULT_POINTER_HALO_COLOR_ONTOUCH = Color.argb(135, 74, 138, 255) 1260 | private const val DEFAULT_CIRCLE_FILL_COLOR = Color.TRANSPARENT 1261 | private const val DEFAULT_POINTER_ALPHA = 135 1262 | private const val DEFAULT_POINTER_ALPHA_ONTOUCH = 100 1263 | private const val DEFAULT_USE_CUSTOM_RADII = false 1264 | private const val DEFAULT_MAINTAIN_EQUAL_CIRCLE = true 1265 | private const val DEFAULT_MOVE_OUTSIDE_CIRCLE = false 1266 | private const val DEFAULT_LOCK_ENABLED = true 1267 | private const val DEFAULT_DISABLE_POINTER = false 1268 | private const val DEFAULT_NEGATIVE_ENABLED = false 1269 | private const val DEFAULT_DISABLE_PROGRESS_GLOW = true 1270 | private const val DEFAULT_CS_HIDE_PROGRESS_WHEN_EMPTY = false 1271 | } 1272 | } 1273 | --------------------------------------------------------------------------------