├── fluid-slider ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ └── values │ │ │ ├── strings.xml │ │ │ └── attrs.xml │ │ └── kotlin │ │ └── com │ │ └── ramotion │ │ └── fluidslider │ │ └── FluidSlider.kt ├── proguard-rules.pro └── build.gradle ├── fluid-slider-example-simple ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── 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 │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── layout │ │ │ └── activity_main.xml │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ └── drawable │ │ │ └── ic_launcher_background.xml │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── com │ │ └── ramotion │ │ └── fluidslider │ │ └── example │ │ └── simple │ │ └── MainActivity.kt ├── proguard-rules.pro └── build.gradle ├── fluid-slider-example-simple-java ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── 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 │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── layout │ │ │ └── activity_main.xml │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ └── drawable │ │ │ └── ic_launcher_background.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── ramotion │ │ └── fluidslider │ │ └── example │ │ └── simple │ │ └── java │ │ └── MainActivity.java ├── proguard-rules.pro └── build.gradle ├── header.png ├── Fluid_slider.gif ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle ├── gradle.properties ├── .gitignore ├── LICENSE ├── gradlew.bat ├── gradlew └── README.md /fluid-slider/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /fluid-slider-example-simple/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/header.png -------------------------------------------------------------------------------- /fluid-slider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Fluid_slider.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/Fluid_slider.gif -------------------------------------------------------------------------------- /fluid-slider/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | fluid-slider 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':fluid-slider-example-simple', 2 | ':fluid-slider', 3 | ':fluid-slider-example-simple-java' 4 | -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple-java/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple-java/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple-java/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple-java/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple-java/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple-java/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple-java/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple-java/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple-java/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/fluid-slider-android/HEAD/fluid-slider-example-simple-java/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Mar 03 10:12:16 MSK 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip 7 | -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | fluid-slider-android 3 | Kotlin is a great fit for developing Android applications, bringing all of the advantages of a modern language to the Android platform without introducing any new restrictions. 4 | 5 | -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | fluid-slider-example-simple-java 3 | Kotlin is a great fit for developing Android applications, bringing all of the advantages of a modern language to the Android platform without introducing any new restrictions. 4 | 5 | -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /fluid-slider/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 22 | -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /fluid-slider-example-simple/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 22 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | android.useAndroidX=true 11 | android.enableJetifier=true 12 | 13 | # Specifies the JVM arguments used for the daemon process. 14 | # The setting is particularly useful for tweaking memory settings. 15 | org.gradle.jvmargs=-Xmx1536m 16 | 17 | # When configured, Gradle will run in incubating parallel mode. 18 | # This option should only be used with decoupled projects. More details, visit 19 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 20 | # org.gradle.parallel=true 21 | -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/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 22 | -------------------------------------------------------------------------------- /fluid-slider/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Java compiled classes # 2 | # Package Files # 3 | # IDEA Project files # 4 | *.iws 5 | *.iml 6 | *.ipr 7 | .idea/ 8 | 9 | # Build directory # 10 | target/ 11 | build/ 12 | 13 | # Generated files # 14 | bin/ 15 | gen/ 16 | out/ 17 | 18 | # Local props file # 19 | local.properties 20 | 21 | # Gradle cache # 22 | .gradle 23 | 24 | # OSX files # 25 | .DS_Store 26 | 27 | # NDK # 28 | # files for the dex VM # 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | 35 | # Android Studio Navigation editor temp files 36 | .navigation/ 37 | 38 | # Android Studio captures folder 39 | captures/ 40 | 41 | # Keystore files 42 | *.jks 43 | 44 | # External native build folder generated in Android Studio 2.2 and later 45 | .externalNativeBuild 46 | 47 | # Google Services (e.g. APIs or Firebase) 48 | google-services.json 49 | 50 | # Freeline 51 | freeline.py 52 | freeline/ 53 | freeline_project_description.json 54 | fluid-slider-simple-example/libs/ 55 | fluid-slider/libs/ 56 | fluid-slider/src/main/java/ 57 | fluid-slider/src/main/res/drawable/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ramotion 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 | -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/kotlin/com/ramotion/fluidslider/example/simple/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.fluidslider.example.simple 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.widget.TextView 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.ramotion.fluidslider.FluidSlider 8 | 9 | class MainActivity : AppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.activity_main) 14 | 15 | val textView = findViewById(R.id.textView) 16 | 17 | val max = 45 18 | val min = 10 19 | val total = max - min 20 | 21 | val slider = findViewById(R.id.fluidSlider) 22 | slider.positionListener = { pos -> slider.bubbleText = "${min + (total * pos).toInt()}" } 23 | slider.position = 0.3f 24 | slider.startText ="$min" 25 | slider.endText = "$max" 26 | 27 | slider.beginTrackingListener = { textView.visibility = View.INVISIBLE } 28 | slider.endTrackingListener = { textView.visibility = View.VISIBLE } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 29 6 | 7 | defaultConfig { 8 | applicationId "com.ramotion.fluidslider.example.simple.java" 9 | minSdkVersion 19 10 | targetSdkVersion 29 11 | versionCode 2 12 | versionName "1.1" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation fileTree(dir: 'libs', include: ['*.jar']) 33 | implementation project(':fluid-slider') 34 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 35 | implementation 'androidx.appcompat:appcompat:1.1.0' 36 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 37 | testImplementation 'junit:junit:4.12' 38 | androidTestImplementation 'androidx.test:runner:1.2.0' 39 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 40 | } -------------------------------------------------------------------------------- /fluid-slider-example-simple/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | 8 | defaultConfig { 9 | applicationId "com.ramotion.fluidslider.example.simple" 10 | minSdkVersion 16 11 | targetSdkVersion 29 12 | versionCode 2 13 | versionName "1.1" 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | sourceSets { 25 | main.java.srcDirs += 'src/main/kotlin' 26 | } 27 | 28 | buildToolsVersion '28.0.3' 29 | } 30 | 31 | dependencies { 32 | implementation fileTree(include: ['*.jar'], dir: 'libs') 33 | implementation project(':fluid-slider') 34 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 35 | implementation 'androidx.appcompat:appcompat:1.1.0' 36 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 37 | 38 | testImplementation 'junit:junit:4.12' 39 | androidTestImplementation 'androidx.test:runner:1.2.0' 40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 41 | } -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 23 | 24 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/java/com/ramotion/fluidslider/example/simple/java/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.ramotion.fluidslider.example.simple.java; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | import android.widget.TextView; 6 | 7 | import com.ramotion.fluidslider.FluidSlider; 8 | 9 | import androidx.appcompat.app.AppCompatActivity; 10 | import kotlin.Unit; 11 | import kotlin.jvm.functions.Function0; 12 | 13 | public class MainActivity extends AppCompatActivity { 14 | 15 | @SuppressWarnings("Convert2Lambda") 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_main); 20 | 21 | final TextView textView = findViewById(R.id.textView); 22 | 23 | final int max = 45; 24 | final int min = 10; 25 | final int total = max - min; 26 | 27 | final FluidSlider slider = findViewById(R.id.fluidSlider); 28 | slider.setBeginTrackingListener(new Function0() { 29 | @Override 30 | public Unit invoke() { 31 | textView.setVisibility(View.INVISIBLE); 32 | return Unit.INSTANCE; 33 | } 34 | }); 35 | 36 | slider.setEndTrackingListener(new Function0() { 37 | @Override 38 | public Unit invoke() { 39 | textView.setVisibility(View.VISIBLE); 40 | return Unit.INSTANCE; 41 | } 42 | }); 43 | 44 | // Java 8 lambda 45 | slider.setPositionListener(pos -> { 46 | final String value = String.valueOf( (int)(min + total * pos) ); 47 | slider.setBubbleText(value); 48 | return Unit.INSTANCE; 49 | }); 50 | 51 | 52 | slider.setPosition(0.3f); 53 | slider.setStartText(String.valueOf(min)); 54 | slider.setEndText(String.valueOf(max)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 24 | 25 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /fluid-slider-example-simple/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /fluid-slider/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'signing' 4 | apply plugin: 'com.bmuschko.nexus' 5 | 6 | group = 'com.ramotion.fluidslider' 7 | version = '0.3.1' 8 | 9 | android { 10 | compileSdkVersion 29 11 | defaultConfig { 12 | minSdkVersion 16 13 | targetSdkVersion 29 14 | versionCode 5 15 | versionName version 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | 19 | } 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | sourceSets { 27 | main.java.srcDirs += 'src/main/kotlin' 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation fileTree(include: ['*.jar'], dir: 'libs') 33 | implementation 'androidx.appcompat:appcompat:1.1.0' 34 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 35 | 36 | testImplementation 'junit:junit:4.12' 37 | androidTestImplementation 'androidx.test:runner:1.2.0' 38 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 39 | } 40 | 41 | repositories { 42 | mavenCentral() 43 | } 44 | 45 | modifyPom { 46 | project { 47 | name 'Fluid Slider for Android' 48 | description 'A slider widget with a popup bubble displaying the precise value selected. http://ramotion.com' 49 | url 'https://github.com/Ramotion/fluid-slider-android' 50 | inceptionYear '2017' 51 | 52 | scm { 53 | url 'https://github.com/Ramotion/fluid-slider-android' 54 | connection 'scm:git@github.com:Ramotion/fluid-slider-android.git' 55 | developerConnection 'scm:git@github.com:Ramotion/fluid-slider-android.git' 56 | } 57 | 58 | licenses { 59 | license { 60 | name 'The MIT License (MIT)' 61 | url 'https://opensource.org/licenses/mit-license.php' 62 | distribution 'repo' 63 | } 64 | } 65 | 66 | developers { 67 | developer { 68 | id 'dvg4000' 69 | name 'Dmitry Grishechkin' 70 | email 'dvgrishechkin@yandex.ru' 71 | } 72 | } 73 | } 74 | } 75 | 76 | nexus { 77 | sign = true 78 | repositoryUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' 79 | snapshotRepositoryUrl = 'https://oss.sonatype.org/content/repositories/snapshots/' 80 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /fluid-slider-example-simple/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 | -------------------------------------------------------------------------------- /fluid-slider-example-simple-java/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

FLUID SLIDER [KOTLIN]

7 | 8 |

A slider widget with a popup bubble displaying the precise value selected

9 | 10 | 11 | ___ 12 | 13 | 14 |

We specialize in the designing and coding of custom UI for Mobile Apps and Websites.
15 | 16 | 17 |

18 |

Stay tuned for the latest updates:
19 | 20 |

21 | 22 | Inspired by [Virgil Pana](https://dribbble.com/virgilpana) [shot](https://dribbble.com/shots/3868232-Fluid-Slider) 23 | 24 |
25 | 26 | [![Twitter](https://img.shields.io/badge/Twitter-@Ramotion-blue.svg?style=flat)](http://twitter.com/Ramotion) 27 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/92bd2e49f7e543cd8748c670b9e52ca7)](https://www.codacy.com/app/dvg4000/fluid-slider-android?utm_source=github.com&utm_medium=referral&utm_content=Ramotion/fluid-slider-android&utm_campaign=Badge_Grade) 28 | [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/Ramotion) 29 | 30 | ## Requirements 31 | 32 | - Android 4.1 Jelly Bean (API lvl 16) or greater 33 | - Your favorite IDE 34 | 35 | ## Installation 36 | ​ 37 | Just download the package from [here](http://central.maven.org/maven2/com/ramotion/fluidslider/fluid-slider/0.3.1/fluid-slider-0.3.1.aar) and add it to your project classpath, or just use the maven repo: 38 | 39 | Gradle: 40 | ```groovy 41 | implementation 'com.ramotion.fluidslider:fluid-slider:0.3.1' 42 | ``` 43 | SBT: 44 | ```scala 45 | libraryDependencies += "com.ramotion.fluidslider" % "fluid-slider" % "0.3.1" 46 | ``` 47 | Maven: 48 | ```xml 49 | 50 | com.ramotion.fluidslider 51 | fluid-slider 52 | 0.3.1 53 | aar 54 | 55 | ``` 56 | 57 | ## Basic usage 58 | 59 | Place the `FluidSlider` in your layout. 60 | 61 | To track the current position of the slider, set the `positionListener`, as shown below: 62 | ``` 63 | val slider = findViewById(R.id.fluidSlider) 64 | slider.positionListener = { p -> Log.d("MainActivity", "current position is: $p" )} 65 | ``` 66 | 67 | You can also track the beginning and completion of the movement of the slider, using the following properties: 68 | `beginTrackingListener` and` endTrackingListener`. Example below: 69 | ``` 70 | slider.beginTrackingListener = { /* action on slider touched */ } 71 | slider.endTrackingListener = { /* action on slider released */ } 72 | ``` 73 | 74 | Here is simple example, how to change `FluidSlider` range. 75 | ```kotlin 76 | // Kotlin 77 | val max = 45 78 | val min = 10 79 | val total = max - min 80 | 81 | val slider = findViewById(R.id.fluidSlider) 82 | slider.positionListener = { pos -> slider.bubbleText = "${min + (total * pos).toInt()}" } 83 | slider.position = 0.3f 84 | slider.startText ="$min" 85 | slider.endText = "$max" 86 | 87 | // Java 88 | final FluidSlider slider = findViewById(R.id.fluidSlider); 89 | slider.setBeginTrackingListener(new Function0() { 90 | @Override 91 | public Unit invoke() { 92 | Log.d("D", "setBeginTrackingListener"); 93 | return Unit.INSTANCE; 94 | } 95 | }); 96 | 97 | slider.setEndTrackingListener(new Function0() { 98 | @Override 99 | public Unit invoke() { 100 | Log.d("D", "setEndTrackingListener"); 101 | return Unit.INSTANCE; 102 | } 103 | }); 104 | 105 | // Or Java 8 lambda 106 | slider.setPositionListener(pos -> { 107 | final String value = String.valueOf( (int)((1 - pos) * 100) ); 108 | slider.setBubbleText(value); 109 | return Unit.INSTANCE; 110 | }); 111 | ``` 112 | 113 | Here are the attributes you can specify through XML or related setters: 114 | * `bar_color` - Color of slider. 115 | * `bubble_color` - Color of circle "bubble" inside bar. 116 | * `bar_text_color` - Color of `start` and `end` texts of slider. 117 | * `bubble_text_color` - Color of text inside "bubble". 118 | * `start_text` - Start (left) text of slider. 119 | * `end_text` - End (right) text of slider. 120 | * `text_size` - Text size. 121 | * `duration` - Duration of "bubble" rise in milliseconds. 122 | * `initial_position` - Initial positon of "bubble" in range form `0.0` to `1.0`. 123 | * `size` - Height of slider. Can be `small` (40dp) and `normal` (56dp). 124 | 125 | 126 | This library is a part of a selection of our best UI open-source projects. 127 | 128 | ## Third Party Bindings 129 | ### React Native 130 | You may now use this library with [React Native](https://github.com/facebook/react-native) via the module [here](https://github.com/prscX/react-native-fluidic-slider) 131 | 132 | ## 🗂 Check this library on other language: 133 | 134 | 135 | 136 | 137 | ## 📄 License 138 | 139 | Fluid Slider Android is released under the MIT license. 140 | See [LICENSE](./LICENSE) for details. 141 | 142 | This library is a part of a selection of our best UI open-source projects 143 | 144 | If you use the open-source library in your project, please make sure to credit and backlink to www.ramotion.com 145 | 146 | ## 📱 Get the Showroom App for Android to give it a try 147 | Try this UI component and more like this in our Android app. Contact us if interested. 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /fluid-slider/src/main/kotlin/com/ramotion/fluidslider/FluidSlider.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.fluidslider 2 | 3 | import android.animation.ValueAnimator 4 | import android.annotation.SuppressLint 5 | import android.content.Context 6 | import android.graphics.* 7 | import android.os.Build 8 | import android.os.Parcel 9 | import android.os.Parcelable 10 | import android.util.AttributeSet 11 | import android.view.MotionEvent 12 | import android.view.View 13 | import android.view.ViewOutlineProvider 14 | import android.view.animation.OvershootInterpolator 15 | import com.ramotion.fluidslider.FluidSlider.Size.NORMAL 16 | import com.ramotion.fluidslider.FluidSlider.Size.SMALL 17 | import kotlin.math.* 18 | 19 | 20 | class FluidSlider @JvmOverloads constructor( 21 | context: Context, 22 | attrs: AttributeSet? = null, 23 | defStyleAttr: Int = 0, 24 | size: Size = Size.NORMAL) : View(context, attrs, defStyleAttr) { 25 | 26 | /** 27 | * Sizes that can be used. 28 | * @see NORMAL 29 | * @see SMALL 30 | */ 31 | enum class Size(val value: Int) { 32 | /** 33 | * Default size - 56dp. 34 | */ 35 | NORMAL(56), 36 | 37 | /** 38 | * Small size - 40dp. 39 | */ 40 | SMALL(40) 41 | } 42 | 43 | private companion object { 44 | const val BAR_CORNER_RADIUS = 2 45 | const val BAR_VERTICAL_OFFSET = 1.5f 46 | const val BAR_INNER_HORIZONTAL_OFFSET = 0 47 | 48 | const val SLIDER_WIDTH = 4 49 | const val SLIDER_HEIGHT = 1 + BAR_VERTICAL_OFFSET 50 | 51 | const val TOP_CIRCLE_DIAMETER = 1 52 | const val BOTTOM_CIRCLE_DIAMETER = 25.0f 53 | const val TOUCH_CIRCLE_DIAMETER = 1 54 | const val LABEL_CIRCLE_DIAMETER = 10 55 | 56 | const val ANIMATION_DURATION = 400 57 | const val TOP_SPREAD_FACTOR = 0.4f 58 | const val BOTTOM_START_SPREAD_FACTOR = 0.25f 59 | const val BOTTOM_END_SPREAD_FACTOR = 0.1f 60 | const val METABALL_HANDLER_FACTOR = 2.4f 61 | const val METABALL_MAX_DISTANCE = 15.0f 62 | const val METABALL_RISE_DISTANCE = 1.1f 63 | 64 | const val TEXT_SIZE = 12 65 | const val TEXT_OFFSET = 8 66 | const val TEXT_START = "0" 67 | const val TEXT_END = "100" 68 | 69 | const val COLOR_BAR = 0xff6168e7.toInt() 70 | const val COLOR_LABEL = Color.WHITE 71 | const val COLOR_LABEL_TEXT = Color.BLACK 72 | const val COLOR_BAR_TEXT = Color.WHITE 73 | 74 | const val INITIAL_POSITION = 0.5f 75 | } 76 | 77 | private val barHeight: Float 78 | 79 | private val desiredWidth: Int 80 | private val desiredHeight: Int 81 | 82 | private val topCircleDiameter: Float 83 | private val bottomCircleDiameter: Float 84 | private val touchRectDiameter: Float 85 | private val labelRectDiameter: Float 86 | 87 | private val metaballMaxDistance: Float 88 | private val metaballRiseDistance: Float 89 | private val textOffset: Float 90 | 91 | private val barVerticalOffset: Float 92 | private val barCornerRadius: Float 93 | private val barInnerOffset: Float 94 | 95 | private val rectBar = RectF() 96 | private val rectTopCircle = RectF() 97 | private val rectBottomCircle = RectF() 98 | private val rectTouch = RectF() 99 | private val rectLabel = RectF() 100 | private val rectText = Rect() 101 | private val pathMetaball = Path() 102 | 103 | private val paintBar: Paint 104 | private val paintLabel: Paint 105 | private val paintText: Paint 106 | 107 | private var maxMovement = 0f 108 | private var touchX: Float? = null 109 | 110 | /** 111 | * Duration of "bubble" rise in milliseconds. 112 | */ 113 | var duration = ANIMATION_DURATION.toLong() 114 | set(value) { 115 | field = abs(value) 116 | } 117 | 118 | /** 119 | * Color of text inside "bubble". 120 | */ 121 | @Suppress("MemberVisibilityCanBePrivate") 122 | var colorBubbleText = COLOR_LABEL_TEXT 123 | 124 | /** 125 | * Color of `start` and `end` texts of slider. 126 | */ 127 | @Suppress("MemberVisibilityCanBePrivate") 128 | var colorBarText = COLOR_BAR_TEXT 129 | 130 | /** 131 | * Color of slider. 132 | */ 133 | @Suppress("MemberVisibilityCanBePrivate") 134 | var colorBar: Int 135 | get() = paintBar.color 136 | set(value) { 137 | paintBar.color = value 138 | } 139 | 140 | /** 141 | * Color of circle "bubble" inside bar. 142 | */ 143 | @Suppress("MemberVisibilityCanBePrivate") 144 | var colorBubble: Int 145 | get() = paintLabel.color 146 | set(value) { 147 | paintLabel.color = value 148 | } 149 | 150 | /** 151 | * Text size. 152 | */ 153 | var textSize: Float 154 | get() = paintText.textSize 155 | set(value) { 156 | paintText.textSize = value 157 | } 158 | 159 | /** 160 | * Bubble text. 161 | */ 162 | var bubbleText: String? = null 163 | 164 | /** 165 | * Start (left) text of slider. 166 | */ 167 | var startText: String? = TEXT_START 168 | 169 | /** 170 | * End (right) text of slider. 171 | */ 172 | var endText: String? = TEXT_END 173 | 174 | /** 175 | * Initial position of "bubble" in range form `0.0` to `1.0`. 176 | */ 177 | var position = INITIAL_POSITION 178 | set(value) { 179 | field = max(0f, min(1f, value)) 180 | invalidate() 181 | positionListener?.invoke(field) 182 | } 183 | 184 | /** 185 | * Current position tracker. Receive current position, in range from `0.0f` to `1.0f`. 186 | */ 187 | var positionListener: ((Float) -> Unit)? = null 188 | 189 | /** 190 | * Called on slider touch. 191 | */ 192 | var beginTrackingListener: (() -> Unit)? = null 193 | 194 | /** 195 | * Called when slider is released. 196 | */ 197 | var endTrackingListener: (() -> Unit)? = null 198 | 199 | @SuppressLint("NewApi") 200 | inner class OutlineProvider : ViewOutlineProvider() { 201 | override fun getOutline(v: View?, outline: Outline?) { 202 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 203 | val rect = Rect(rectBar.left.toInt(), rectBar.top.toInt(), rectBar.right.toInt(), rectBar.bottom.toInt()) 204 | outline?.setRoundRect(rect, barCornerRadius) 205 | } 206 | } 207 | } 208 | 209 | class State : BaseSavedState { 210 | companion object { 211 | @JvmField 212 | @Suppress("unused") 213 | val CREATOR = object : Parcelable.Creator { 214 | override fun createFromParcel(parcel: Parcel): State = State(parcel) 215 | override fun newArray(size: Int): Array = arrayOfNulls(size) 216 | } 217 | } 218 | 219 | val position: Float 220 | val startText: String? 221 | val endText: String? 222 | val textSize: Float 223 | val colorLabel: Int 224 | val colorBar: Int 225 | val colorBarText: Int 226 | val colorLabelText: Int 227 | val duration: Long 228 | 229 | constructor(superState: Parcelable?, 230 | position: Float, 231 | startText: String?, 232 | endText: String?, 233 | textSize: Float, 234 | colorLabel: Int, 235 | colorBar: Int, 236 | colorBarText: Int, 237 | colorLabelText: Int, 238 | duration: Long) : super(superState) { 239 | this.position = position 240 | this.startText = startText 241 | this.endText = endText 242 | this.textSize = textSize 243 | this.colorLabel = colorLabel 244 | this.colorBar = colorBar 245 | this.colorBarText = colorBarText 246 | this.colorLabelText = colorLabelText 247 | this.duration = duration 248 | } 249 | 250 | private constructor(parcel: Parcel) : super(parcel) { 251 | this.position = parcel.readFloat() 252 | this.startText = parcel.readString() 253 | this.endText = parcel.readString() 254 | this.textSize = parcel.readFloat() 255 | this.colorLabel = parcel.readInt() 256 | this.colorBar = parcel.readInt() 257 | this.colorBarText = parcel.readInt() 258 | this.colorLabelText = parcel.readInt() 259 | this.duration = parcel.readLong() 260 | } 261 | 262 | override fun writeToParcel(parcel: Parcel, i: Int) { 263 | super.writeToParcel(parcel, i) 264 | parcel.writeFloat(position) 265 | parcel.writeString(startText) 266 | parcel.writeString(endText) 267 | parcel.writeFloat(textSize) 268 | parcel.writeInt(colorLabel) 269 | parcel.writeInt(colorBar) 270 | parcel.writeInt(colorBarText) 271 | parcel.writeInt(colorLabelText) 272 | parcel.writeLong(duration) 273 | } 274 | 275 | override fun describeContents(): Int = 0 276 | } 277 | 278 | init { 279 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 280 | outlineProvider = OutlineProvider() 281 | } 282 | 283 | paintBar = Paint(Paint.ANTI_ALIAS_FLAG) 284 | paintBar.style = Paint.Style.FILL 285 | 286 | paintLabel = Paint(Paint.ANTI_ALIAS_FLAG) 287 | paintLabel.style = Paint.Style.FILL 288 | 289 | paintText = Paint(Paint.ANTI_ALIAS_FLAG) 290 | 291 | val density = context.resources.displayMetrics.density 292 | 293 | if (attrs != null) { 294 | val a = context.theme.obtainStyledAttributes(attrs, R.styleable.FluidSlider, defStyleAttr, 0) 295 | try { 296 | colorBar = a.getColor(R.styleable.FluidSlider_bar_color, COLOR_BAR) 297 | colorBubble = a.getColor(R.styleable.FluidSlider_bubble_color, COLOR_LABEL) 298 | colorBarText = a.getColor(R.styleable.FluidSlider_bar_text_color, COLOR_BAR_TEXT) 299 | colorBubbleText = a.getColor(R.styleable.FluidSlider_bubble_text_color, COLOR_LABEL_TEXT) 300 | 301 | position = max(0f, min(1f, a.getFloat(R.styleable.FluidSlider_initial_position, INITIAL_POSITION))) 302 | textSize = a.getDimension(R.styleable.FluidSlider_text_size, TEXT_SIZE * density) 303 | duration = abs(a.getInteger(R.styleable.FluidSlider_duration, ANIMATION_DURATION)).toLong() 304 | 305 | a.getString(R.styleable.FluidSlider_start_text)?.also { startText = it } 306 | a.getString(R.styleable.FluidSlider_end_text)?.also { endText = it } 307 | 308 | val defaultBarHeight = if (a.getInteger(R.styleable.FluidSlider_size, 1) == 1) Size.NORMAL.value else Size.SMALL.value 309 | barHeight = defaultBarHeight * density 310 | } finally { 311 | a.recycle() 312 | } 313 | } else { 314 | colorBar = COLOR_BAR 315 | colorBubble = COLOR_LABEL 316 | textSize = TEXT_SIZE * density 317 | barHeight = size.value * density 318 | } 319 | 320 | desiredWidth = (barHeight * SLIDER_WIDTH).toInt() 321 | desiredHeight = (barHeight * SLIDER_HEIGHT).toInt() 322 | 323 | topCircleDiameter = barHeight * TOP_CIRCLE_DIAMETER 324 | bottomCircleDiameter = barHeight * BOTTOM_CIRCLE_DIAMETER 325 | touchRectDiameter = barHeight * TOUCH_CIRCLE_DIAMETER 326 | labelRectDiameter = barHeight - LABEL_CIRCLE_DIAMETER * density 327 | 328 | metaballMaxDistance = barHeight * METABALL_MAX_DISTANCE 329 | metaballRiseDistance = barHeight * METABALL_RISE_DISTANCE 330 | 331 | barVerticalOffset = barHeight * BAR_VERTICAL_OFFSET 332 | barCornerRadius = BAR_CORNER_RADIUS * density 333 | barInnerOffset = BAR_INNER_HORIZONTAL_OFFSET * density 334 | textOffset = TEXT_OFFSET * density 335 | } 336 | 337 | /** 338 | * Additional constructor that can be used to create FluidSlider programmatically. 339 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 340 | * @param size Size of FluidSlider. 341 | * @see Size 342 | */ 343 | constructor(context: Context, size: Size) : this(context, null, 0, size) 344 | 345 | override fun onSaveInstanceState(): Parcelable { 346 | return State(super.onSaveInstanceState(), 347 | position, startText, endText, textSize, 348 | colorBubble, colorBar, colorBarText, colorBubbleText, duration) 349 | } 350 | 351 | override fun onRestoreInstanceState(state: Parcelable) { 352 | if (state is State) { 353 | super.onRestoreInstanceState(state.superState) 354 | position = state.position 355 | startText = state.startText 356 | endText = state.endText 357 | textSize = state.textSize 358 | colorBubble = state.colorLabel 359 | colorBar = state.colorBar 360 | colorBarText = state.colorBarText 361 | colorBubbleText = state.colorLabelText 362 | duration = state.duration 363 | } else { 364 | super.onRestoreInstanceState(state) 365 | } 366 | } 367 | 368 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 369 | val w = resolveSizeAndState(desiredWidth, widthMeasureSpec, 0) 370 | val h = resolveSizeAndState(desiredHeight, heightMeasureSpec, 0) 371 | setMeasuredDimension(w, h) 372 | } 373 | 374 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 375 | super.onSizeChanged(w, h, oldw, oldh) 376 | 377 | val width = w.toFloat() 378 | rectBar.set(0f, barVerticalOffset, width, barVerticalOffset + barHeight) 379 | rectTopCircle.set(0f, barVerticalOffset, topCircleDiameter, barVerticalOffset + topCircleDiameter) 380 | rectBottomCircle.set(0f, barVerticalOffset, bottomCircleDiameter, barVerticalOffset + bottomCircleDiameter) 381 | rectTouch.set(0f, barVerticalOffset, touchRectDiameter, barVerticalOffset + touchRectDiameter) 382 | 383 | val vOffset = barVerticalOffset + (topCircleDiameter - labelRectDiameter) / 2f 384 | rectLabel.set(0f, vOffset, labelRectDiameter, vOffset + labelRectDiameter) 385 | 386 | maxMovement = width - touchRectDiameter - barInnerOffset * 2 387 | } 388 | 389 | override fun onDraw(canvas: Canvas) { 390 | super.onDraw(canvas) 391 | 392 | // Draw slider bar and text 393 | canvas.drawRoundRect(rectBar, barCornerRadius, barCornerRadius, paintBar) 394 | 395 | startText?.let { drawText(canvas, paintText, it, Paint.Align.LEFT, colorBarText, textOffset, rectBar, rectText) } 396 | endText?.let { drawText(canvas, paintText, it, Paint.Align.RIGHT, colorBarText, textOffset, rectBar, rectText) } 397 | 398 | // Draw metaball 399 | val x = barInnerOffset + touchRectDiameter / 2 + maxMovement * position 400 | offsetRectToPosition(x, rectTouch, rectTopCircle, rectBottomCircle, rectLabel) 401 | 402 | drawMetaball(canvas, paintBar, pathMetaball, rectBottomCircle, rectTopCircle, rectBar.top) 403 | 404 | // Draw label and text 405 | canvas.drawOval(rectLabel, paintLabel) 406 | 407 | val text = bubbleText ?: (position * 100).toInt().toString() 408 | drawText(canvas, paintText, text, Paint.Align.CENTER, colorBubbleText, 0f, rectLabel, rectText) 409 | } 410 | 411 | override fun performClick(): Boolean { 412 | super.performClick() 413 | return true 414 | } 415 | 416 | override fun onTouchEvent(event: MotionEvent): Boolean = when (event.actionMasked) { 417 | MotionEvent.ACTION_DOWN -> { 418 | val x = event.x 419 | val y = event.y 420 | if (rectBar.contains(x, y)) { 421 | if (!rectTouch.contains(x, y)) { 422 | position = max(0f, min(1f, (x - rectTouch.width() / 2) / maxMovement)) 423 | } 424 | touchX = x 425 | beginTrackingListener?.invoke() 426 | showLabel(metaballRiseDistance) 427 | parent.requestDisallowInterceptTouchEvent(true) 428 | true 429 | } else { 430 | false 431 | } 432 | } 433 | MotionEvent.ACTION_MOVE -> { 434 | touchX?.let { 435 | touchX = event.x 436 | val newPos = max(0f, min(1f, position + (event.x - it) / maxMovement)) 437 | position = newPos 438 | true 439 | } == true 440 | } 441 | MotionEvent.ACTION_UP, 442 | MotionEvent.ACTION_CANCEL -> { 443 | touchX?.let { 444 | touchX = null 445 | endTrackingListener?.invoke() 446 | hideLabel() 447 | performClick() 448 | parent.requestDisallowInterceptTouchEvent(false) 449 | true 450 | } == true 451 | } 452 | else -> false 453 | } 454 | 455 | private fun offsetRectToPosition(position: Float, vararg rects: RectF) { 456 | for (rect in rects) { 457 | rect.offsetTo(position - rect.width() / 2f, rect.top) 458 | } 459 | } 460 | 461 | private fun getVector(radians: Float, length: Float): Pair { 462 | val x = (cos(radians) * length) 463 | val y = (sin(radians) * length) 464 | return x to y 465 | } 466 | 467 | private fun getVectorLength(x1: Float, y1: Float, x2: Float, y2: Float): Float { 468 | val x = x1 - x2 469 | val y = y1 - y2 470 | return sqrt(x * x + y * y) 471 | } 472 | 473 | private fun drawMetaball(canvas: Canvas, 474 | paint: Paint, 475 | path: Path, 476 | circle1: RectF, 477 | circle2: RectF, 478 | topBorder: Float, 479 | riseDistance: Float = metaballRiseDistance, 480 | maxDistance: Float = metaballMaxDistance, 481 | cornerRadius: Float = barCornerRadius, 482 | topSpreadFactor: Float = TOP_SPREAD_FACTOR, 483 | bottomStartSpreadFactor: Float = BOTTOM_START_SPREAD_FACTOR, 484 | bottomEndSpreadFactor: Float = BOTTOM_END_SPREAD_FACTOR, 485 | handleRate: Float = METABALL_HANDLER_FACTOR) { 486 | val radius1 = circle1.width() / 2.0f 487 | val radius2 = circle2.width() / 2.0f 488 | 489 | if (radius1 == 0.0f || radius2 == 0.0f) { 490 | return 491 | } 492 | 493 | val d = getVectorLength(circle1.centerX(), circle1.centerY(), circle2.centerX(), circle2.centerY()) 494 | if (d > maxDistance || d <= abs(radius1 - radius2)) { 495 | return 496 | } 497 | 498 | val riseRatio = min(1f, max(0f, topBorder - circle2.top) / riseDistance) 499 | 500 | val u1: Float 501 | val u2: Float 502 | if (d < radius1 + radius2) { // case circles are overlapping 503 | u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d)) 504 | u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d)) 505 | } else { 506 | u1 = 0.0f 507 | u2 = 0.0f 508 | } 509 | 510 | val centerXMin = circle2.centerX() - circle1.centerX() 511 | val centerYMin = circle2.centerY() - circle1.centerY() 512 | 513 | val bottomSpreadDiff = bottomStartSpreadFactor - bottomEndSpreadFactor 514 | val bottomSpreadFactor = bottomStartSpreadFactor - bottomSpreadDiff * riseRatio 515 | 516 | val fPI = PI.toFloat() 517 | val angle1 = atan2(centerYMin, centerXMin) 518 | val angle2 = acos((radius1 - radius2) / d) 519 | val angle1a = angle1 + u1 + (angle2 - u1) * bottomSpreadFactor 520 | val angle1b = angle1 - u1 - (angle2 - u1) * bottomSpreadFactor 521 | val angle2a = (angle1 + fPI - u2 - (fPI - u2 - angle2) * topSpreadFactor) 522 | val angle2b = (angle1 - fPI + u2 + (fPI - u2 - angle2) * topSpreadFactor) 523 | 524 | val p1a = getVector(angle1a, radius1).let { (it.first + circle1.centerX()) to (it.second + circle1.centerY()) }.toList() 525 | val p1b = getVector(angle1b, radius1).let { (it.first + circle1.centerX()) to (it.second + circle1.centerY()) }.toList() 526 | val p2a = getVector(angle2a, radius2).let { (it.first + circle2.centerX()) to (it.second + circle2.centerY()) }.toList() 527 | val p2b = getVector(angle2b, radius2).let { (it.first + circle2.centerX()) to (it.second + circle2.centerY()) }.toList() 528 | 529 | val totalRadius = (radius1 + radius2) 530 | val d2Base = min( 531 | max(topSpreadFactor, bottomSpreadFactor) * handleRate, 532 | getVectorLength(p1a[0], p1a[1], p2a[0], p2a[1]) / totalRadius) 533 | 534 | // case circles are overlapping: 535 | val d2 = d2Base * min(1.0f, d * 2 / (radius1 + radius2)) 536 | 537 | val r1 = radius1 * d2 538 | val r2 = radius2 * d2 539 | 540 | val pi2 = fPI / 2 541 | val sp1 = getVector(angle1a - pi2, r1).toList() 542 | val sp2 = getVector(angle2a + pi2, r2).toList() 543 | val sp3 = getVector(angle2b - pi2, r2).toList() 544 | val sp4 = getVector(angle1b + pi2, r1).toList() 545 | 546 | // move bottom point to bar top border 547 | val yOffset = (abs(topBorder - p1a[1]) * riseRatio) - 1 548 | val fp1a = p1a.let { l -> listOf(l[0], l[1] - yOffset) } 549 | val fp1b = p1b.let { l -> listOf(l[0], l[1] - yOffset) } 550 | 551 | with(path) { 552 | reset() 553 | moveTo(fp1a[0], fp1a[1] + cornerRadius) 554 | lineTo(fp1a[0], fp1a[1]) 555 | cubicTo(fp1a[0] + sp1[0], fp1a[1] + sp1[1], p2a[0] + sp2[0], p2a[1] + sp2[1], p2a[0], p2a[1]) 556 | lineTo(circle2.centerX(), circle2.centerY()) 557 | lineTo(p2b[0], p2b[1]) 558 | cubicTo(p2b[0] + sp3[0], p2b[1] + sp3[1], fp1b[0] + sp4[0], fp1b[1] + sp4[1], fp1b[0], fp1b[1]) 559 | lineTo(fp1b[0], fp1b[1] + cornerRadius) 560 | close() 561 | } 562 | 563 | with(canvas) { 564 | drawPath(path, paint) 565 | drawOval(circle2, paint) 566 | } 567 | } 568 | 569 | private fun drawText(canvas: Canvas, paint: Paint, 570 | text: String, align: Paint.Align, color: Int, offset: Float, 571 | holderRect: RectF, textRect: Rect) { 572 | paint.color = color 573 | paint.textAlign = align 574 | paint.getTextBounds(text, 0, text.length, textRect) 575 | val x = when (align) { 576 | Paint.Align.LEFT -> offset 577 | Paint.Align.CENTER -> holderRect.centerX() 578 | Paint.Align.RIGHT -> holderRect.right - offset 579 | } 580 | val y = holderRect.centerY() + textRect.height() / 2f - textRect.bottom 581 | canvas.drawText(text, 0, text.length, x, y, paint) 582 | } 583 | 584 | private fun showLabel(distance: Float) { 585 | val top = barVerticalOffset - distance 586 | val labelVOffset = (topCircleDiameter - labelRectDiameter) / 2f 587 | 588 | val animation = ValueAnimator.ofFloat(rectTopCircle.top, top) 589 | animation.addUpdateListener { 590 | val value = it.animatedValue as Float 591 | rectTopCircle.offsetTo(rectTopCircle.left, value) 592 | rectLabel.offsetTo(rectLabel.left, value + labelVOffset) 593 | invalidate() 594 | } 595 | animation.duration = duration 596 | animation.interpolator = OvershootInterpolator() 597 | animation.start() 598 | } 599 | 600 | private fun hideLabel() { 601 | val labelVOffset = (topCircleDiameter - labelRectDiameter) / 2f 602 | val animation = ValueAnimator.ofFloat(rectTopCircle.top, barVerticalOffset) 603 | animation.addUpdateListener { 604 | val value = it.animatedValue as Float 605 | rectTopCircle.offsetTo(rectTopCircle.left, value) 606 | rectLabel.offsetTo(rectLabel.left, value + labelVOffset) 607 | invalidate() 608 | } 609 | animation.duration = duration 610 | animation.start() 611 | } 612 | 613 | } --------------------------------------------------------------------------------