├── 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 | [](https://github.com/tankery/CircularSeekBar/actions/workflows/ci-check.yml)
4 | [](https://github.com/tankery/CircularSeekBar/releases)
5 | [](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 |
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 |
--------------------------------------------------------------------------------