├── .github ├── FUNDING.yml └── workflows │ └── mavenPublish.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── labeledseekslider │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── labeledseekslider │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── ic_thumb_slider.xml │ │ ├── font │ │ ├── ttnorms_bold.otf │ │ └── ttnorms_regular.otf │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── example │ └── labeledseekslider │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── labeledseekslider ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── zigis │ │ └── labeledseekslider │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── zigis │ │ │ └── labeledseekslider │ │ │ ├── LabeledSeekSlider.kt │ │ │ └── custom │ │ │ ├── ContextExtensions.kt │ │ │ └── UnitPosition.kt │ └── res │ │ └── values │ │ └── attrs.xml │ └── test │ └── java │ └── com │ └── zigis │ └── labeledseekslider │ └── ExampleUnitTest.kt ├── maven ├── publish-module.gradle └── publish-root.gradle ├── sample-slide.gif └── settings.gradle /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: edgar-zigis 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/mavenPublish.yml: -------------------------------------------------------------------------------- 1 | name: Maven Publish 2 | 3 | on: 4 | release: 5 | types: [ released ] 6 | 7 | jobs: 8 | Maven-Publish: 9 | name: Publish to MavenCentral 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v1 14 | 15 | - name: Setup JDK 17 16 | uses: actions/setup-java@v3 17 | with: 18 | java-version: '17' 19 | distribution: 'temurin' 20 | 21 | - name: Publish to MavenCentral 22 | run: ./gradlew publishReleasePublicationToSonatypeRepository --max-workers 1 closeAndReleaseSonatypeStagingRepository 23 | env: 24 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 25 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 26 | SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} 27 | MAVEN_SIGNING_KEY_ID: ${{ secrets.MAVEN_SIGNING_KEY_ID }} 28 | MAVEN_SIGNING_KEY_PASSWORD: ${{ secrets.MAVEN_SIGNING_KEY_PASSWORD }} 29 | MAVEN_SIGNING_KEY: ${{ secrets.MAVEN_SIGNING_KEY }} 30 | SDK_RELEASE_VERSION: ${{ github.event.release.tag_name }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .DS_Store 5 | /build 6 | /.idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Edgar Žigis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LabeledSeekSlider [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.bio-matic/labeledseekslider/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.bio-matic/labeledseekslider) 2 | 3 | Custom & highly configurable seek slider with sliding intervals, disabled state and every possible setting to tackle! 4 | ##### Minimum target SDK: 21 5 | 6 | ![alt text](https://github.com/edgar-zigis/LabeledSeekSlider/blob/master/sample-slide.gif?raw=true) 7 | 8 | ### Gradle 9 | Make sure you have **Maven Central** included in your gradle repositories. 10 | 11 | ```gradle 12 | allprojects { 13 | repositories { 14 | mavenCentral() 15 | } 16 | } 17 | ``` 18 | ```gradle 19 | implementation 'com.bio-matic:labeledseekslider:1.3.3' 20 | ``` 21 | ### Usage 22 | ``` xml 23 | 56 | ``` 57 | if you wish to skip certain values, you can set them programatically 58 | ```kotlin 59 | seekSlider.valuesToSkip = listOf(4, 6, 10) 60 | ``` 61 | ### Remarks 62 | At the moment wrap_content height configuration falls back to **98dp**, so if you have increased default dimensions, you will also need to increase height param. 63 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | 8 | namespace 'com.example.labeledseekslider' 9 | compileSdk 35 10 | 11 | defaultConfig { 12 | applicationId "com.zigis.labeledseekslider.sample" 13 | minSdkVersion 21 14 | targetSdkVersion 35 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | 28 | buildFeatures { 29 | viewBinding true 30 | } 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_17 34 | targetCompatibility JavaVersion.VERSION_17 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '17' 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 44 | implementation 'androidx.appcompat:appcompat:1.7.0' 45 | implementation 'com.google.android.material:material:1.12.0' 46 | implementation 'androidx.constraintlayout:constraintlayout:2.2.0' 47 | implementation project(':labeledseekslider') 48 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/labeledseekslider/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.labeledseekslider 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.labeledseekslider", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/labeledseekslider/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.labeledseekslider 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.view.LayoutInflater 7 | import com.example.labeledseekslider.databinding.ActivityMainBinding 8 | 9 | class MainActivity : AppCompatActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | ActivityMainBinding.inflate(LayoutInflater.from(this)).apply { 13 | setContentView(root) 14 | seekSlider.onValueChanged = { value -> 15 | Log.d("LabeledSeekSlider", "Current slider value: $value") 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_thumb_slider.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/font/ttnorms_bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/app/src/main/res/font/ttnorms_bold.otf -------------------------------------------------------------------------------- /app/src/main/res/font/ttnorms_regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/app/src/main/res/font/ttnorms_regular.otf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | LabeledSeekSlider 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/test/java/com/example/labeledseekslider/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.labeledseekslider 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = '2.0.21' 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:8.7.3' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | plugins { 18 | id("io.github.gradle-nexus.publish-plugin") version "1.3.0" 19 | } 20 | 21 | allprojects { 22 | repositories { 23 | google() 24 | mavenCentral() 25 | } 26 | } 27 | 28 | tasks.register('clean', Delete) { 29 | delete rootProject.buildDir 30 | } 31 | 32 | apply from: "${rootDir}/maven/publish-root.gradle" -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | android.defaults.buildfeatures.buildconfig=true 21 | android.nonTransitiveRClass=false 22 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun May 01 22:06:35 EEST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 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 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /labeledseekslider/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /labeledseekslider/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.library" 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | ext { 7 | PUBLISH_ARTIFACT_ID = "labeledseekslider" 8 | } 9 | 10 | apply from: "${rootProject.projectDir}/maven/publish-module.gradle" 11 | 12 | android { 13 | 14 | namespace 'com.zigis.labeledseekslider' 15 | compileSdk 35 16 | 17 | defaultConfig { 18 | minSdkVersion 21 19 | targetSdkVersion 35 20 | 21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 22 | consumerProguardFiles "consumer-rules.pro" 23 | } 24 | 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_17 34 | targetCompatibility JavaVersion.VERSION_17 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '17' 39 | } 40 | 41 | publishing { 42 | singleVariant("release") { 43 | withSourcesJar() 44 | } 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation 'androidx.core:core-ktx:1.15.0' 50 | } -------------------------------------------------------------------------------- /labeledseekslider/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/labeledseekslider/consumer-rules.pro -------------------------------------------------------------------------------- /labeledseekslider/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /labeledseekslider/src/androidTest/java/com/zigis/labeledseekslider/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.zigis.labeledseekslider 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.zigis.labeledseekslider.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /labeledseekslider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /labeledseekslider/src/main/java/com/zigis/labeledseekslider/LabeledSeekSlider.kt: -------------------------------------------------------------------------------- 1 | package com.zigis.labeledseekslider 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.* 6 | import android.graphics.drawable.Drawable 7 | import android.os.Build 8 | import android.text.Layout 9 | import android.text.StaticLayout 10 | import android.text.TextPaint 11 | import android.util.AttributeSet 12 | import android.util.TypedValue 13 | import android.view.MotionEvent 14 | import android.view.MotionEvent.* 15 | import android.view.View 16 | import androidx.core.content.res.ResourcesCompat 17 | import com.zigis.labeledseekslider.custom.UnitPosition 18 | import com.zigis.labeledseekslider.custom.vibrate 19 | import kotlin.math.max 20 | import kotlin.math.min 21 | import kotlin.math.round 22 | 23 | /** 24 | * Highly customizable seek slider primarily designed for fintech apps. 25 | * 26 | * @author Edgar Žigis. 27 | */ 28 | @Suppress("DEPRECATION") 29 | @SuppressLint("DrawAllocation") 30 | open class LabeledSeekSlider : View { 31 | 32 | /** 33 | * Lower range value also displayed in the left corner 34 | */ 35 | var minValue: Int = 0 36 | set(value) { 37 | if (field != value) { 38 | actualXPosition = null 39 | } 40 | field = value 41 | if (actualFractionalValue < value) { 42 | actualFractionalValue = value 43 | } 44 | invalidate() 45 | } 46 | /** 47 | * Upper range value also displayed in the right corner 48 | */ 49 | var maxValue: Int = 100 50 | set(value) { 51 | if (field != value) { 52 | actualXPosition = null 53 | } 54 | field = value 55 | if (actualFractionalValue > value) { 56 | actualFractionalValue = value 57 | } 58 | invalidate() 59 | } 60 | /** 61 | * Default value which will be displayed during the initial draw 62 | */ 63 | var defaultValue: Int = 50 64 | set(value) { 65 | if (field != value || field != getDisplayValue()) { 66 | actualXPosition = null 67 | } 68 | val newValue = min(maxValue, max(minValue, value)) 69 | field = newValue 70 | actualFractionalValue = newValue 71 | invalidate() 72 | } 73 | /** 74 | * Max sliding value, must be > min && < max 75 | * Won't be applicable if null 76 | */ 77 | var limitValue: Int? = null 78 | set(value) { 79 | if (field != value) { 80 | actualXPosition = null 81 | } 82 | field = value 83 | if (value != null && actualFractionalValue > value) { 84 | actualFractionalValue = value 85 | } 86 | invalidate() 87 | } 88 | /** 89 | * Text label which indicates that the @param limitValue is reached 90 | */ 91 | var limitValueIndicator: String = "Max" 92 | set(value) { 93 | field = value 94 | invalidate() 95 | } 96 | /** 97 | * Allows sliding past @param limitValue if needed 98 | */ 99 | var allowLimitValueBypass: Boolean = false 100 | /** 101 | * Toggles vibration after @param limitValue is reached 102 | */ 103 | var vibrateOnLimitReached: Boolean = true 104 | /** 105 | * Slider title label value 106 | */ 107 | var title: String = "" 108 | set(value) { 109 | field = value 110 | invalidate() 111 | } 112 | /** 113 | * Slider unit label value 114 | * Will be set near the @param minValue and @param maxValue 115 | */ 116 | var unit: String = "" 117 | set(value) { 118 | field = value 119 | invalidate() 120 | } 121 | /** 122 | * Slider unit label position 123 | * Can be placed in front or back 124 | */ 125 | var unitPosition = UnitPosition.BACK 126 | set(value) { 127 | field = value 128 | invalidate() 129 | } 130 | /** 131 | * Will disable user interaction and grayscale whole view 132 | */ 133 | var isDisabled: Boolean = false 134 | set(value) { 135 | field = value 136 | invalidate() 137 | } 138 | /** 139 | * Already filled track color 140 | */ 141 | var activeTrackColor = Color.parseColor("#FF2400") 142 | set(value) { 143 | field = value 144 | activeTrackPaint.color = value 145 | invalidate() 146 | } 147 | /** 148 | * Yet not filled track color 149 | */ 150 | var inactiveTrackColor = Color.parseColor("#E8E8E8") 151 | set(value) { 152 | field = value 153 | inactiveTrackPaint.color = value 154 | invalidate() 155 | } 156 | /** 157 | * Thumb slider background color 158 | */ 159 | var thumbSliderBackgroundColor = Color.parseColor("#FFFFFF") 160 | set(value) { 161 | field = value 162 | thumbSliderPaint.color = value 163 | invalidate() 164 | } 165 | /** 166 | * Replaces default thumb slider if set 167 | */ 168 | var thumbSliderDrawable: Drawable? = null 169 | set(value) { 170 | field = value 171 | invalidate() 172 | } 173 | /** 174 | * Font for TextViews containing @param minValue and @param maValue 175 | */ 176 | var rangeValueTextFont = Typeface.create("sans-serif", Typeface.NORMAL) 177 | set(value) { 178 | field = value 179 | rangeTextPaint.typeface = value 180 | invalidate() 181 | } 182 | /** 183 | * Text color for TextViews containing @param minValue and @param maValue 184 | */ 185 | var rangeValueTextColor = Color.parseColor("#9FA7AD") 186 | set(value) { 187 | field = value 188 | rangeTextPaint.color = value 189 | invalidate() 190 | } 191 | /** 192 | * Text size for TextViews containing @param minValue and @param maValue 193 | */ 194 | var rangeValueTextSize = dp(12f) 195 | set(value) { 196 | field = value 197 | rangeTextPaint.textSize = value 198 | invalidate() 199 | } 200 | /** 201 | * Option to show/hide range indicators 202 | */ 203 | var isRangeIndicationHidden = false 204 | set(value) { 205 | field = value 206 | invalidate() 207 | } 208 | /** 209 | * Font for TextView containing @param title 210 | */ 211 | var titleTextFont = Typeface.create("sans-serif", Typeface.NORMAL) 212 | set(value) { 213 | field = value 214 | titleTextPaint.typeface = value 215 | invalidate() 216 | } 217 | /** 218 | * Text color for TextView containing @param title 219 | */ 220 | var titleTextColor = Color.parseColor("#9FA7AD") 221 | set(value) { 222 | field = value 223 | titleTextPaint.color = value 224 | invalidate() 225 | } 226 | /** 227 | * Text size for TextView containing @param title 228 | */ 229 | var titleTextSize = dp(12f) 230 | set(value) { 231 | field = value 232 | titleTextPaint.textSize = value 233 | invalidate() 234 | } 235 | /** 236 | * Current value bubble outline color 237 | */ 238 | var bubbleOutlineColor = Color.parseColor("#E8E8E8") 239 | set(value) { 240 | field = value 241 | bubblePaint.color = value 242 | invalidate() 243 | } 244 | /** 245 | * Current value bubble text font 246 | */ 247 | var bubbleValueTextFont = Typeface.create("sans-serif", Typeface.BOLD) 248 | set(value) { 249 | field = value 250 | bubbleTextPaint.typeface = value 251 | invalidate() 252 | } 253 | /** 254 | * Current value bubble text color 255 | */ 256 | var bubbleValueTextColor = Color.parseColor("#1A1A1A") 257 | set(value) { 258 | field = value 259 | bubbleTextPaint.color = value 260 | invalidate() 261 | } 262 | /** 263 | * Current value bubble text size 264 | */ 265 | var bubbleValueTextSize = dp(14f) 266 | set(value) { 267 | field = value 268 | bubbleTextPaint.textSize = value 269 | invalidate() 270 | } 271 | /** 272 | * Option to show/hide slider bubble 273 | */ 274 | var isBubbleHidden = false 275 | set(value) { 276 | field = value 277 | invalidate() 278 | } 279 | /** 280 | * Values which will be "jumped" through and not emitted 281 | * As well as not displayed in the UI. 282 | * For example if min is 1, max is 5 and valuesToSkip has 3 and 4 283 | * Only 1, 2 and 5 will be displayed and emitted to the user. 284 | */ 285 | var valuesToSkip: List = emptyList() 286 | /** 287 | * Sliding interval value. 288 | * For example if set to 50, sliding values will be 0, 50, 100 etc. 289 | */ 290 | var slidingInterval: Int = 1 291 | /** 292 | * Callback reporting changed values upstream 293 | */ 294 | var onValueChanged: ((Int) -> Unit)? = null 295 | /** 296 | * Read-only parameter for fetching current slider value 297 | */ 298 | var currentValue: Int = 150 299 | private set 300 | 301 | // Operational vars 302 | 303 | private var actualFractionalValue: Int = 150 304 | private var actualXPosition: Float? = null 305 | 306 | private val topPadding = dp(2f) 307 | private val bubbleHeight = dp(26f) 308 | private val minimumBubbleWidth = dp(84f) 309 | private val bubbleTextPadding = dp(16f) 310 | 311 | private var trackHeight = dp(4f) 312 | private var thumbSliderRadius = dp(12f) 313 | 314 | private val thumbSliderPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { 315 | it.style = Paint.Style.FILL 316 | it.setShadowLayer(dp(2f), 0f, 1f, Color.parseColor("#44444444")) 317 | setLayerType(LAYER_TYPE_SOFTWARE, it) 318 | } 319 | 320 | private val inactiveTrackPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { 321 | it.style = Paint.Style.FILL_AND_STROKE 322 | } 323 | private var inactiveTrackRect: RectF? = null 324 | 325 | private val activeTrackPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { 326 | it.style = Paint.Style.FILL_AND_STROKE 327 | } 328 | 329 | private val bubblePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { 330 | it.style = Paint.Style.STROKE 331 | it.strokeWidth = dp(2f) 332 | it.strokeCap = Paint.Cap.ROUND 333 | it.pathEffect = CornerPathEffect(dp(4f)) 334 | } 335 | private var bubblePath = Path() 336 | private var bubblePathWidth = 0f 337 | 338 | private var bubbleText: String = "" 339 | private val bubbleTextRect = Rect() 340 | private var bubbleTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG) 341 | 342 | private val titleTextRect = Rect() 343 | private var titleTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG) 344 | 345 | private val minRangeTextRect = Rect() 346 | private val maxRangeTextRect = Rect() 347 | private var rangeTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG) 348 | 349 | private val disabledStatePaint = Paint() 350 | 351 | // Constructors 352 | 353 | constructor(context: Context) : super(context) { 354 | init(context, null) 355 | } 356 | 357 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { 358 | init(context, attrs) 359 | } 360 | 361 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 362 | context, 363 | attrs, 364 | defStyleAttr 365 | ) { 366 | init(context, attrs) 367 | } 368 | 369 | // Initialization 370 | 371 | private fun init(context: Context, attrs: AttributeSet?) { 372 | if (isInEditMode) return 373 | 374 | initializeDisabledStatePaint() 375 | val styledAttributes = context.theme.obtainStyledAttributes( 376 | attrs, 377 | R.styleable.LabeledSeekSlider, 378 | 0, 379 | 0 380 | ) 381 | 382 | minValue = styledAttributes.getInteger( 383 | R.styleable.LabeledSeekSlider_lss_minValue, 384 | minValue 385 | ) 386 | maxValue = styledAttributes.getInteger( 387 | R.styleable.LabeledSeekSlider_lss_maxValue, 388 | maxValue 389 | ) 390 | defaultValue = styledAttributes.getInteger( 391 | R.styleable.LabeledSeekSlider_lss_defaultValue, 392 | defaultValue 393 | ).also { 394 | actualFractionalValue = min(maxValue, max(minValue, it)) 395 | } 396 | 397 | styledAttributes.getInteger(R.styleable.LabeledSeekSlider_lss_limitValue, -1).also { 398 | if (it != -1) { 399 | limitValue = it 400 | } 401 | } 402 | 403 | vibrateOnLimitReached = styledAttributes.getBoolean( 404 | R.styleable.LabeledSeekSlider_lss_vibrateOnLimitReached, 405 | true 406 | ) 407 | 408 | limitValueIndicator = styledAttributes.getString( 409 | R.styleable.LabeledSeekSlider_lss_limitValueIndicator 410 | ) ?: limitValueIndicator 411 | title = styledAttributes.getString( 412 | R.styleable.LabeledSeekSlider_lss_title 413 | ) ?: title 414 | unit = styledAttributes.getString( 415 | R.styleable.LabeledSeekSlider_lss_unit 416 | ) ?: unit 417 | 418 | unitPosition = UnitPosition.parse( 419 | styledAttributes.getInt( 420 | R.styleable.LabeledSeekSlider_lss_unitPosition, 421 | UnitPosition.BACK.value 422 | ) 423 | ) 424 | isDisabled = styledAttributes.getBoolean( 425 | R.styleable.LabeledSeekSlider_lss_isDisabled, 426 | isDisabled 427 | ) 428 | activeTrackColor = styledAttributes.getColor( 429 | R.styleable.LabeledSeekSlider_lss_activeTrackColor, 430 | activeTrackColor 431 | ) 432 | inactiveTrackColor = styledAttributes.getColor( 433 | R.styleable.LabeledSeekSlider_lss_inactiveTrackColor, 434 | inactiveTrackColor 435 | ) 436 | thumbSliderBackgroundColor = styledAttributes.getColor( 437 | R.styleable.LabeledSeekSlider_lss_thumbSliderBackgroundColor, 438 | thumbSliderBackgroundColor 439 | ) 440 | thumbSliderDrawable = styledAttributes.getDrawable( 441 | R.styleable.LabeledSeekSlider_lss_thumbSliderDrawable 442 | ) 443 | bubbleValueTextColor = styledAttributes.getColor( 444 | R.styleable.LabeledSeekSlider_lss_bubbleValueTextColor, 445 | bubbleValueTextColor 446 | ) 447 | bubbleOutlineColor = styledAttributes.getColor( 448 | R.styleable.LabeledSeekSlider_lss_bubbleOutlineColor, 449 | bubbleOutlineColor 450 | ) 451 | isBubbleHidden = styledAttributes.getBoolean( 452 | R.styleable.LabeledSeekSlider_lss_hideBubble, 453 | false 454 | ) 455 | titleTextColor = styledAttributes.getColor( 456 | R.styleable.LabeledSeekSlider_lss_titleTextColor, 457 | titleTextColor 458 | ) 459 | rangeValueTextColor = styledAttributes.getColor( 460 | R.styleable.LabeledSeekSlider_lss_rangeValueTextColor, 461 | rangeValueTextColor 462 | ) 463 | isRangeIndicationHidden = styledAttributes.getBoolean( 464 | R.styleable.LabeledSeekSlider_lss_hideRangeIndicators, 465 | false 466 | ) 467 | 468 | styledAttributes.getResourceId(R.styleable.LabeledSeekSlider_lss_titleTextFont, 0).also { 469 | if (it > 0) titleTextFont = ResourcesCompat.getFont(context, it) 470 | } 471 | styledAttributes.getResourceId(R.styleable.LabeledSeekSlider_lss_rangeValueTextFont, 0).also { 472 | if (it > 0) rangeValueTextFont = ResourcesCompat.getFont(context, it) 473 | } 474 | styledAttributes.getResourceId(R.styleable.LabeledSeekSlider_lss_bubbleValueTextFont, 0).also { 475 | if (it > 0) bubbleValueTextFont = ResourcesCompat.getFont(context, it) 476 | } 477 | 478 | slidingInterval = styledAttributes.getInteger( 479 | R.styleable.LabeledSeekSlider_lss_slidingInterval, 480 | slidingInterval 481 | ) 482 | 483 | bubbleValueTextSize = styledAttributes.getDimension( 484 | R.styleable.LabeledSeekSlider_lss_bubbleValueTextSize, 485 | bubbleValueTextSize 486 | ) 487 | titleTextSize = styledAttributes.getDimension( 488 | R.styleable.LabeledSeekSlider_lss_titleTextSize, 489 | titleTextSize 490 | ) 491 | rangeValueTextSize = styledAttributes.getDimension( 492 | R.styleable.LabeledSeekSlider_lss_rangeValueTextSize, 493 | rangeValueTextSize 494 | ) 495 | 496 | trackHeight = styledAttributes.getDimension( 497 | R.styleable.LabeledSeekSlider_lss_trackHeight, 498 | trackHeight 499 | ) 500 | thumbSliderRadius = styledAttributes.getDimension( 501 | R.styleable.LabeledSeekSlider_lss_thumbSliderRadius, 502 | thumbSliderRadius 503 | ) 504 | } 505 | 506 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 507 | val minimumDesiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight 508 | val minimumDesiredHeight = when { 509 | isBubbleHidden && isRangeIndicationHidden -> thumbSliderRadius.toInt() 510 | isRangeIndicationHidden -> dp(90f).toInt() 511 | else -> dp(120f).toInt() 512 | } 513 | setMeasuredDimension( 514 | measureDimension(minimumDesiredWidth, widthMeasureSpec), 515 | measureDimension(minimumDesiredHeight, heightMeasureSpec) 516 | ) 517 | } 518 | 519 | private fun measureDimension(desiredSize: Int, measureSpec: Int): Int { 520 | var result: Int 521 | val specMode = MeasureSpec.getMode(measureSpec) 522 | val specSize = MeasureSpec.getSize(measureSpec) 523 | if (specMode == MeasureSpec.EXACTLY) { 524 | result = specSize 525 | } else { 526 | result = desiredSize 527 | if (specMode == MeasureSpec.AT_MOST) { 528 | result = min(result, specSize) 529 | } 530 | } 531 | return result 532 | } 533 | 534 | override fun onDraw(canvas: Canvas) { 535 | super.onDraw(canvas) 536 | (actualXPosition ?: getActiveX(actualFractionalValue)).also { x -> 537 | drawBubbleValue(canvas, x) 538 | drawBubbleOutline(canvas, x) 539 | drawTitleLabelText(canvas) 540 | drawInactiveTrack(canvas) 541 | drawActiveTrack(canvas, x) 542 | drawThumbSlider(canvas, x) 543 | drawMinRangeText(canvas) 544 | drawMaxRangeText(canvas) 545 | } 546 | } 547 | 548 | override fun draw(canvas: Canvas) { 549 | if (isDisabled) { 550 | canvas.saveLayer(null, disabledStatePaint) 551 | } 552 | super.draw(canvas) 553 | if (isDisabled) { 554 | canvas.restore() 555 | } 556 | } 557 | 558 | override fun dispatchDraw(canvas: Canvas) { 559 | if (isDisabled) { 560 | canvas.saveLayer(null, disabledStatePaint) 561 | } 562 | super.dispatchDraw(canvas) 563 | if (isDisabled) { 564 | canvas.restore() 565 | } 566 | } 567 | 568 | @SuppressLint("ClickableViewAccessibility") 569 | override fun onTouchEvent(event: MotionEvent): Boolean { 570 | if (isDisabled) return false 571 | return when (event.action) { 572 | ACTION_DOWN, ACTION_MOVE, ACTION_UP -> handleSlidingMovement(event.x) 573 | else -> false 574 | } 575 | } 576 | 577 | private fun handleSlidingMovement(x: Float): Boolean { 578 | val relativeX = min(measuredWidth - thumbSliderRadius, max(-thumbSliderRadius, x)) 579 | val slidingAreaWidth = measuredWidth - thumbSliderRadius 580 | 581 | val newValue = min(maxValue, max( 582 | minValue, 583 | minValue + round((maxValue - minValue) * (relativeX / slidingAreaWidth)).toInt() 584 | )) 585 | actualFractionalValue = if (limitValue == null || allowLimitValueBypass) { 586 | newValue 587 | } else min(newValue, limitValue!!) 588 | 589 | if (limitValue != null && !allowLimitValueBypass) { 590 | if (newValue <= limitValue!!) { 591 | actualXPosition = x 592 | } else { 593 | actualXPosition = getActiveX(limitValue!!) 594 | } 595 | } else { 596 | actualXPosition = x 597 | } 598 | 599 | invalidate() 600 | return true 601 | } 602 | 603 | private fun getActiveX(currentValue: Int): Float { 604 | val slidingAreaWidth = measuredWidth - thumbSliderRadius 605 | val progress = (currentValue - minValue).toFloat() / (maxValue - minValue).toFloat() 606 | return slidingAreaWidth * progress 607 | } 608 | 609 | private fun drawThumbSlider(canvas: Canvas, x: Float) { 610 | val centerX = min( 611 | measuredWidth - thumbSliderRadius / 2, 612 | max(thumbSliderRadius / 2, x) 613 | ) 614 | if (thumbSliderDrawable != null) { 615 | thumbSliderDrawable?.setBounds( 616 | centerX.toInt() - thumbSliderRadius.toInt() / 2, 617 | inactiveTrackRect!!.centerY().toInt() - thumbSliderRadius.toInt() / 2, 618 | centerX.toInt() + thumbSliderRadius.toInt() / 2, 619 | inactiveTrackRect!!.centerY().toInt() + thumbSliderRadius.toInt() / 2 620 | ) 621 | thumbSliderDrawable?.draw(canvas) 622 | } else { 623 | canvas.drawCircle( 624 | centerX, 625 | inactiveTrackRect!!.centerY(), 626 | thumbSliderRadius, 627 | thumbSliderPaint 628 | ) 629 | } 630 | } 631 | 632 | // Track drawing 633 | 634 | private fun drawActiveTrack(canvas: Canvas, x: Float) { 635 | val activeTrackRect = RectF( 636 | 0f, 637 | getSlidingTrackVerticalOffset(), 638 | min(measuredWidth.toFloat(), max(thumbSliderRadius / 2, x)), 639 | getSlidingTrackVerticalOffset() + trackHeight 640 | ) 641 | val cornerRadius = trackHeight / 2 642 | canvas.drawRoundRect(activeTrackRect, cornerRadius, cornerRadius, activeTrackPaint) 643 | } 644 | 645 | private fun drawInactiveTrack(canvas: Canvas) { 646 | inactiveTrackRect = RectF( 647 | 0f, 648 | getSlidingTrackVerticalOffset(), 649 | measuredWidth.toFloat(), 650 | getSlidingTrackVerticalOffset() + trackHeight 651 | ) 652 | val cornerRadius = trackHeight / 2 653 | canvas.drawRoundRect(inactiveTrackRect!!, cornerRadius, cornerRadius, inactiveTrackPaint) 654 | } 655 | 656 | // Bubble drawing 657 | 658 | private fun drawBubbleOutline(canvas: Canvas, x: Float) { 659 | if (isBubbleHidden) { 660 | return 661 | } 662 | bubblePath = Path().apply { 663 | fillType = Path.FillType.EVEN_ODD 664 | moveTo(getBubbleHorizontalOffset(x), topPadding) 665 | 666 | val comparatorVar1 = x - (bubblePathWidth / 2) 667 | val comparatorVar2 = measuredWidth - bubblePathWidth 668 | 669 | val tailStart = when { 670 | 0f > comparatorVar1 -> { 671 | bubblePathWidth / 2 + min( 672 | thumbSliderRadius / 2 + dp(2f), 673 | -comparatorVar1 674 | ) 675 | } 676 | comparatorVar1 > comparatorVar2 -> { 677 | bubblePathWidth / 2 - min( 678 | thumbSliderRadius / 2 + dp(2f), 679 | comparatorVar1 - comparatorVar2 680 | ) 681 | } 682 | else -> bubblePathWidth / 2 683 | } 684 | val tailEnd = bubblePathWidth - tailStart 685 | 686 | rLineTo(bubblePathWidth, 0f) 687 | rLineTo(0f, bubbleHeight) 688 | rLineTo(-(tailStart - dp(3f)), 0f) 689 | rLineTo(-dp(3f), dp(4f)) 690 | rLineTo(-dp(3f), -dp(4f)) 691 | rLineTo(-(tailEnd - dp(3f)), 0f) 692 | rLineTo(0f, -bubbleHeight) 693 | 694 | close() 695 | } 696 | canvas.drawPath(bubblePath, bubblePaint) 697 | } 698 | 699 | // Text value drawing 700 | 701 | private fun drawBubbleValue(canvas: Canvas, x: Float) { 702 | 703 | fun drawBubbleValueOnCanvas() { 704 | canvas.apply { 705 | save() 706 | translate(getBubbleTextHorizontalOffset(x), getBubbleTextVerticalOffset()) 707 | formTextLayout(bubbleText, bubbleTextPaint).draw(this) 708 | restore() 709 | } 710 | } 711 | 712 | val displayValue = getDisplayValue() 713 | 714 | if (displayValue in valuesToSkip) { 715 | if (!isBubbleHidden) { 716 | drawBubbleValueOnCanvas() 717 | } 718 | return 719 | } 720 | 721 | val previousText = bubbleText 722 | if (actualFractionalValue == limitValue && !allowLimitValueBypass) { 723 | if (vibrateOnLimitReached) { 724 | if (!bubbleText.contains(limitValue.toString()) && previousText.isNotEmpty()) { 725 | context.vibrate(50) 726 | } 727 | } 728 | bubbleText = "$limitValueIndicator ${getUnitValue(limitValue!!)}" 729 | currentValue = limitValue!! 730 | } else { 731 | bubbleText = getUnitValue(displayValue) 732 | currentValue = displayValue 733 | } 734 | 735 | if (previousText != bubbleText && previousText.isNotEmpty()) { 736 | onValueChanged?.invoke(currentValue) 737 | } 738 | 739 | if (!isBubbleHidden) { 740 | bubbleTextPaint.getTextBounds(bubbleText, 0, bubbleText.length, bubbleTextRect) 741 | drawBubbleValueOnCanvas() 742 | } 743 | } 744 | 745 | private fun drawTitleLabelText(canvas: Canvas) { 746 | titleTextPaint.getTextBounds(title, 0, title.length, titleTextRect) 747 | canvas.apply { 748 | save() 749 | translate(0f, getTitleLabelTextVerticalOffset()) 750 | formTextLayout(title, titleTextPaint).draw(this) 751 | restore() 752 | } 753 | } 754 | 755 | private fun drawMinRangeText(canvas: Canvas) { 756 | if (isRangeIndicationHidden) { 757 | return 758 | } 759 | val textString = getUnitValue(minValue) 760 | rangeTextPaint.getTextBounds(textString, 0, textString.length, minRangeTextRect) 761 | canvas.apply { 762 | save() 763 | translate(0f, getRangeTextVerticalOffset()) 764 | formTextLayout(textString, rangeTextPaint).draw(this) 765 | restore() 766 | } 767 | } 768 | 769 | private fun drawMaxRangeText(canvas: Canvas) { 770 | if (isRangeIndicationHidden) { 771 | return 772 | } 773 | val textString = getUnitValue(maxValue) 774 | rangeTextPaint.getTextBounds(textString, 0, textString.length, maxRangeTextRect) 775 | canvas.apply { 776 | save() 777 | translate(getMaxRangeTextHorizontalOffset(), getRangeTextVerticalOffset()) 778 | formTextLayout(textString, rangeTextPaint).draw(this) 779 | restore() 780 | } 781 | } 782 | 783 | private fun getDisplayValue(): Int { 784 | return actualFractionalValue.div(slidingInterval) * slidingInterval 785 | } 786 | 787 | // Margin methods 788 | 789 | private fun getSlidingTrackVerticalOffset(): Float { 790 | return when { 791 | isBubbleHidden && isRangeIndicationHidden -> thumbSliderRadius / 2 - dp(2f) 792 | else -> bubbleHeight + dp(8f) + titleTextRect.height() + dp(8f) + thumbSliderRadius 793 | } 794 | } 795 | 796 | private fun getBubbleHorizontalOffset(x: Float): Float { 797 | return min( 798 | measuredWidth - bubblePathWidth, 799 | max(0f, x - (bubblePathWidth / 2)) 800 | ) 801 | } 802 | 803 | private fun getBubbleTextVerticalOffset(): Float { 804 | return (bubbleHeight - bubbleTextRect.height()) / 2 - dp(2f) 805 | } 806 | 807 | private fun getBubbleTextHorizontalOffset(x: Float): Float { 808 | bubblePathWidth = max(minimumBubbleWidth, bubbleTextRect.width() + bubbleTextPadding * 2) 809 | return min( 810 | measuredWidth - bubbleTextRect.width() - ((bubblePathWidth - bubbleTextRect.width()) / 2), 811 | max( 812 | bubblePathWidth / 2 - bubbleTextRect.width() / 2, 813 | x - bubbleTextRect.width() / 2 814 | ) 815 | ) 816 | } 817 | 818 | private fun getTitleLabelTextVerticalOffset(): Float { 819 | return bubbleHeight + topPadding + dp(5f) 820 | } 821 | 822 | private fun getRangeTextVerticalOffset(): Float { 823 | return inactiveTrackRect!!.bottom + thumbSliderRadius + dp(2f) 824 | } 825 | 826 | private fun getMaxRangeTextHorizontalOffset(): Float { 827 | return measuredWidth - maxRangeTextRect.width().toFloat() 828 | } 829 | 830 | // Disabled state 831 | 832 | private fun initializeDisabledStatePaint() { 833 | val colorMatrix = ColorMatrix() 834 | colorMatrix.set( 835 | floatArrayOf( 836 | 0.33f, 0.33f, 0.33f, 0f, 0f, 837 | 0.33f, 0.33f, 0.33f, 0f, 0f, 838 | 0.33f, 0.33f, 0.33f, 0f, 0f, 839 | 0f, 0f, 0f, 1f, 0f 840 | ) 841 | ) 842 | disabledStatePaint.colorFilter = ColorMatrixColorFilter(colorMatrix) 843 | } 844 | 845 | // Helper methods 846 | 847 | private fun formTextLayout(text: String, paint: TextPaint): StaticLayout { 848 | return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1) { 849 | val builder = StaticLayout.Builder.obtain(text, 0, text.length, paint, measuredWidth) 850 | .setAlignment(Layout.Alignment.ALIGN_NORMAL) 851 | builder.build() 852 | } else { 853 | StaticLayout(text, paint, width, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false) 854 | } 855 | } 856 | 857 | private fun getUnitValue(value: Int): String { 858 | return if (unitPosition == UnitPosition.FRONT) { 859 | value.toString().plus(unit) 860 | } else { 861 | value.toString().plus(" ").plus(unit) 862 | } 863 | } 864 | 865 | private fun dp(dp: Float): Float { 866 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics) 867 | } 868 | } -------------------------------------------------------------------------------- /labeledseekslider/src/main/java/com/zigis/labeledseekslider/custom/ContextExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.zigis.labeledseekslider.custom 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.os.VibrationEffect 6 | import android.os.Vibrator 7 | 8 | @Suppress("DEPRECATION") 9 | fun Context.vibrate(duration: Long) { 10 | val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator 11 | if (Build.VERSION.SDK_INT >= 26) { 12 | vibrator.vibrate(VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE)) 13 | } else { 14 | vibrator.vibrate(duration) 15 | } 16 | } -------------------------------------------------------------------------------- /labeledseekslider/src/main/java/com/zigis/labeledseekslider/custom/UnitPosition.kt: -------------------------------------------------------------------------------- 1 | package com.zigis.labeledseekslider.custom 2 | 3 | enum class UnitPosition(val value: Int) { 4 | FRONT(0), 5 | BACK(1); 6 | 7 | companion object { 8 | fun parse(value: Int?): UnitPosition { 9 | return values().firstOrNull { it.value == value } ?: BACK 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /labeledseekslider/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 | -------------------------------------------------------------------------------- /labeledseekslider/src/test/java/com/zigis/labeledseekslider/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.zigis.labeledseekslider 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /maven/publish-module.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'signing' 3 | 4 | afterEvaluate { 5 | publishing { 6 | publications { 7 | release(MavenPublication) { 8 | from components.release 9 | groupId "com.bio-matic" 10 | artifactId PUBLISH_ARTIFACT_ID 11 | version sdkVersion 12 | 13 | pom { 14 | name = PUBLISH_ARTIFACT_ID 15 | description = "LabeledSeekSlider" 16 | url = "https://github.com/edgar-zigis/LabeledSeekSlider" 17 | licenses { 18 | license { 19 | name = "MIT License" 20 | url = "http://www.opensource.org/licenses/mit-license.php" 21 | } 22 | } 23 | developers { 24 | developer { 25 | id = "biomatic" 26 | name = "Edgar Žigis" 27 | email = "admin@bio-matic.com" 28 | } 29 | } 30 | scm { 31 | connection = "scm:git:github.com/edgar-zigis/LabeledSeekSlider.git" 32 | developerConnection = "scm:git:ssh://github.com/edgar-zigis/LabeledSeekSlider.git" 33 | url = "https://github.com/edgar-zigis/LabeledSeekSlider/tree/master" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | signing { 42 | useInMemoryPgpKeys( 43 | rootProject.ext["mavenSigningKeyId"], 44 | rootProject.ext["mavenSigningKey"], 45 | rootProject.ext["mavenSigningKeyPassword"], 46 | ) 47 | sign publishing.publications 48 | } -------------------------------------------------------------------------------- /maven/publish-root.gradle: -------------------------------------------------------------------------------- 1 | ext["mavenSigningKeyId"] = '' 2 | ext["mavenSigningKeyPassword"] = '' 3 | ext["mavenSigningKey"] = '' 4 | ext["ossrhUsername"] = '' 5 | ext["ossrhPassword"] = '' 6 | ext["sonatypeStagingProfileId"] = '' 7 | ext["sdkVersion"] = '' 8 | 9 | File secretPropsFile = project.rootProject.file('local.properties') 10 | if (secretPropsFile.exists()) { 11 | Properties p = new Properties() 12 | new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } 13 | p.each { name, value -> ext[name] = value } 14 | } else { 15 | // Use system environment variables 16 | ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') 17 | ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') 18 | ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') 19 | ext["mavenSigningKeyId"] = System.getenv('MAVEN_SIGNING_KEY_ID') 20 | ext["mavenSigningKeyPassword"] = System.getenv('MAVEN_SIGNING_KEY_PASSWORD') 21 | ext["mavenSigningKey"] = System.getenv('MAVEN_SIGNING_KEY') 22 | ext["sdkVersion"] = System.getenv('SDK_RELEASE_VERSION') 23 | } 24 | 25 | nexusPublishing { 26 | repositories { 27 | sonatype { 28 | stagingProfileId = sonatypeStagingProfileId 29 | username = ossrhUsername 30 | password = ossrhPassword 31 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 32 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /sample-slide.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgar-zigis/LabeledSeekSlider/65235553840bda56c05ca4c9a13788efc09fd1f4/sample-slide.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "LabeledSeekSlider" 2 | include ':labeledseekslider' 3 | include ':app' --------------------------------------------------------------------------------