├── .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 [](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 | 
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'
--------------------------------------------------------------------------------