├── .gitignore ├── LICENSE.md ├── README.md ├── VolumeControlView-Sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── agilie │ │ └── controller │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── agilie │ │ │ └── controller │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable │ │ ├── gradient_circle.png │ │ └── shape.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── percent_counter.xml │ │ ├── menu │ │ └── menu_dialog_pick.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_ex.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_ex.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_ex.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_ex.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_ex.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── agilie │ └── controller │ └── ExampleUnitTest.java ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── volume-control-view ├── .gitignore ├── bintray.gradle ├── build.gradle ├── install.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── agilie │ └── volumecontrol │ └── ExampleInstrumentedTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── agilie │ │ └── volumecontrol │ │ ├── Utils.kt │ │ ├── animation │ │ ├── controller │ │ │ ├── Controller.kt │ │ │ └── ControllerImpl.kt │ │ └── painter │ │ │ ├── BackgroundShiningImpl.kt │ │ │ ├── InnerCircle.kt │ │ │ ├── InnerCircleImpl.kt │ │ │ ├── MainCircleImpl.kt │ │ │ ├── MovableCircle.kt │ │ │ ├── MovableCircleImpl.kt │ │ │ ├── Painter.kt │ │ │ ├── SimpleLineImpl.kt │ │ │ └── SplinePath.kt │ │ └── view │ │ └── VolumeControlView.kt └── res │ └── values │ ├── attrs.xml │ └── strings.xml └── test └── java └── com └── agilie └── volumecontrol └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | /*/build/ 3 | /build/gi 4 | # Crashlytics configuations 5 | com_crashlytics_export_strings.xml 6 | 7 | # Local configuration file (sdk splinelPath, etc) 8 | local.properties 9 | 10 | # Gradle generated files 11 | .gradle/ 12 | 13 | # Signing files 14 | .signing/ 15 | 16 | # User-specific configurations 17 | .idea/libraries/ 18 | .idea/workspace.xml 19 | .idea/tasks.xml 20 | .idea/.name 21 | .idea/compiler.xml 22 | .idea/copyright/profiles_settings.xml 23 | .idea/encodings.xml 24 | .idea/misc.xml 25 | .idea/modules.xml 26 | .idea/scopes/scope_settings.xml 27 | .idea/vcs.xml 28 | *.iml 29 | 30 | # OS-specific files 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | ehthumbs.db 37 | Thumbs.db 38 | 39 | .gradle 40 | /local.properties 41 | /.idea/workspace.xml 42 | /.idea/libraries 43 | /build 44 | */build 45 | /captures 46 | .externalNativeBuild 47 | gradle.properties 48 | .idea/codeStyleSettings.xml 49 | 50 | app/build 51 | intermediates 52 | /.idea/ 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [2015] [Agilie] 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 |

2 | 3 | VolumeControlView 4 |

5 | 6 |

7 | 8 |

9 | 10 | 11 | Made by Agilie 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | GitHub license 20 | 21 | 22 |

23 | 24 | We’re happy to introduce you a new free regulator VolumeControlView based on our lightweight open-source visual component that doesn't require extra lines of code and can be easily integrated into your project. Visual regulator can be connected to a player or other smart house’s device making the process of controlling the level of a particular characteristic much easier. 25 | 26 | ### Demo 27 | 28 | 29 | 30 | ## Link to iOS repo 31 | 32 | Check out our iOS [VolumeControlView](https://github.com/agilie/AGVolumeControlView) also! 33 | 34 | ## Example 35 | To run the example project, clone the repo, and run sample. 36 | ### How to use 37 | 38 | Just add VolumeControlView to your layout file: 39 | ```kotlin 40 | 44 | ```` 45 | 46 | The visual display of this regulator can be easily customized. One has a possibility to choose colors, the gradient style and background according to the wishes: 47 | ```kotlin 48 | var colors : intArrayOf 49 | var backgroundLayoutColor : Color 50 | var minShiningRadius : Float 51 | var maxShiningRadius : Float 52 | var shiningFrequency : Float 53 | ```` 54 | ````xml 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ```` 67 | ## Usage 68 | 69 | ### Gradle 70 | 71 | Add dependency in your `build.gradle` file: 72 | ````gradle 73 | compile 'com.agilie:volume-control-view:1.0' 74 | ```` 75 | 76 | ### Maven 77 | Add dependency in your `.pom` file: 78 | ````xml 79 | 80 | com.agilie 81 | volume-control-view 82 | 1.0 83 | pom 84 | 85 | ```` 86 | 87 | ## Requirements 88 | 89 | VolumeControlView works on Android API 19+ 90 | 91 | ## Troubleshooting 92 | 93 | Problems? Check the [Issues](https://github.com/agilie/VolumeControlView/issues) block 94 | to find the solution or create an new issue that we will fix asap. 95 | 96 | ## Author 97 | 98 | This library is open-sourced by [Agilie Team](https://www.agilie.com?utm_source=github&utm_medium=referral&utm_campaign=Git_Android_Kotlin&utm_term=VolumeControlView) 99 | 100 | ## Contributors 101 | 102 | - [Eugene Surkov](https://github.com/ukevgen) 103 | 104 | ## Contact us 105 | If you have any questions, suggestions or just need a help with web or mobile development, please email us at
106 |
107 | You can ask us anything from basic to complex questions.
108 | We will continue publishing new open-source projects. Stay with us, more updates will follow!
109 | 110 | ## License 111 | 112 | The [MIT](LICENSE.md) License (MIT) Copyright © 2017 [Agilie Team](https://www.agilie.com?utm_source=github&utm_medium=referral&utm_campaign=Git_Android_Kotlin&utm_term=VolumeControlView) 113 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | !/src/main/java/com/agilie/controller/ControllerActivity.kt 3 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 25 7 | buildToolsVersion "25.0.2" 8 | defaultConfig { 9 | applicationId "com.agilie.controller" 10 | minSdkVersion 19 11 | targetSdkVersion 25 12 | versionCode 1 13 | versionName "1.0" 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | compile fileTree(dir: 'libs', include: ['*.jar']) 26 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 27 | exclude group: 'com.android.support', module: 'support-annotations' 28 | }) 29 | compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" 30 | compile 'com.android.support:appcompat-v7:25.3.1' 31 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 32 | 33 | compile 'com.agilie:volume-control-view:1.0' 34 | testCompile 'junit:junit:4.12' 35 | } 36 | repositories { 37 | mavenCentral() 38 | } 39 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/eugene/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include splinePath and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the linesMap number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the linesMap number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/androidTest/java/com/agilie/controller/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.agilie.controller; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.agilie.controller", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/java/com/agilie/controller/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.controller 2 | 3 | import android.graphics.Color 4 | import android.os.Bundle 5 | import android.support.v7.app.AppCompatActivity 6 | import android.view.LayoutInflater 7 | import android.widget.SeekBar 8 | import com.agilie.volumecontrol.animation.controller.ControllerImpl 9 | 10 | import kotlinx.android.synthetic.main.activity_main.* 11 | 12 | 13 | class MainActivity : AppCompatActivity() { 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | setContentView(R.layout.activity_main) 18 | 19 | initController() 20 | initSeekBars() 21 | } 22 | 23 | private fun initController() { 24 | value.setFactory { LayoutInflater.from(this).inflate(R.layout.percent_counter, value, false) } 25 | 26 | controllerView.setStartPercent(50) 27 | controllerView.backgroundLayoutColor = Color.BLACK 28 | controllerView.controller?.onTouchControllerListener = (object : ControllerImpl.OnTouchControllerListener { 29 | override fun onControllerDown(angle: Int, percent: Int) { 30 | // nothing here 31 | } 32 | 33 | override fun onControllerMove(angle: Int, percent: Int) { 34 | // nothing here 35 | } 36 | 37 | override fun onAngleChange(angle: Int, percent: Int) { 38 | value.setText(percent.toString() + "%") 39 | 40 | when (angle) { 41 | in 0..45 -> controllerView.setBackgroundShiningColor(Color.parseColor("#FF7F00")) 42 | in 46..90 -> controllerView.setBackgroundShiningColor(Color.parseColor("#9FFF00")) 43 | in 91..135 -> controllerView.setBackgroundShiningColor(Color.parseColor("#FACC00")) 44 | in 136..180 -> controllerView.setBackgroundShiningColor(Color.parseColor("#3B9800")) 45 | in 181..225 -> controllerView.setBackgroundShiningColor(Color.parseColor("#00493D")) 46 | in 226..270 -> controllerView.setBackgroundShiningColor(Color.parseColor("#E7FBE1")) 47 | in 271..315 -> controllerView.setBackgroundShiningColor(Color.parseColor("#53FFFF")) 48 | in 316..360 -> controllerView.setBackgroundShiningColor(Color.parseColor("#FF7F00")) 49 | } 50 | } 51 | }) 52 | } 53 | 54 | private fun initSeekBars() { 55 | seekBarMinRadius.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 56 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 57 | controllerView.setShiningMinRadius(1 + progress / 100f) 58 | if (seekBarMaxRadius.progress - progress < 10) { 59 | seekBarMaxRadius.progress = (progress + controllerView.getShiningMaxRadius()!! * 10).toInt() 60 | } 61 | } 62 | 63 | override fun onStartTrackingTouch(seekBar: SeekBar) { 64 | } 65 | 66 | override fun onStopTrackingTouch(seekBar: SeekBar) { 67 | } 68 | }) 69 | 70 | seekBarMaxRadius.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 71 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 72 | controllerView.setShiningMaxRadius(1 + progress / 100f) 73 | if (progress - seekBarMinRadius.progress < 10) { 74 | seekBarMinRadius.progress = (progress - controllerView.getShiningMaxRadius()!! * 10).toInt() 75 | } 76 | } 77 | 78 | override fun onStartTrackingTouch(seekBar: SeekBar) { 79 | } 80 | 81 | override fun onStopTrackingTouch(seekBar: SeekBar) { 82 | } 83 | }) 84 | 85 | frequency.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 86 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 87 | controllerView.setShiningFrequency(progress / 10000f) 88 | } 89 | 90 | override fun onStartTrackingTouch(seekBar: SeekBar) { 91 | } 92 | 93 | override fun onStopTrackingTouch(seekBar: SeekBar) { 94 | } 95 | }) 96 | 97 | } 98 | } 99 | 100 | 101 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/drawable/gradient_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/drawable/gradient_circle.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/drawable/shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 29 | 30 | 31 | 32 | 33 | 41 | 42 | 50 | 51 | 60 | 61 | 70 | 71 | 79 | 80 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/layout/percent_counter.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/menu/menu_dialog_pick.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-hdpi/ic_launcher_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-hdpi/ic_launcher_ex.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-mdpi/ic_launcher_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-mdpi/ic_launcher_ex.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-xhdpi/ic_launcher_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-xhdpi/ic_launcher_ex.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-xxhdpi/ic_launcher_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-xxhdpi/ic_launcher_ex.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-xxxhdpi/ic_launcher_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-xxxhdpi/ic_launcher_ex.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/VolumeControlView-Sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #6000FF 5 | #C467FF 6 | #FFB6C2 7 | #E7FBE1 8 | #53FFFF 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | #FFFFFF 7 | 8 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | VolumeControlView Example 3 | 4 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /VolumeControlView-Sample/src/test/java/com/agilie/controller/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.agilie.controller; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 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 | 3 | buildscript { 4 | ext.kotlin_version = '1.1.2-4' 5 | repositories { 6 | jcenter() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:2.3.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2' 12 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | jcenter() 21 | } 22 | 23 | tasks.withType(Javadoc) { 24 | // Ignores errors from mavenAndroidJavadocs task 25 | //options.addStringOption('Xdoclint:none', '-quiet') 26 | //options.addStringOption('encoding', 'UTF-8') 27 | excludes = ['**/*.kt'] 28 | } 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agilie/VolumeControlView/948f179d84e2c0b3e59b13893f1ed49cd4846411/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon May 22 10:33:59 EEST 2017 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-3.3-all.zip 7 | -------------------------------------------------------------------------------- /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 [ -x0 "$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 [ ! -x0 "$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 --splinelPath --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --splinelPath --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 --splinelPath --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 | -------------------------------------------------------------------------------- /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 initAttrs 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 initAttrs 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 | :initAttrs 49 | @rem Get command-linesMap 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 linesMap arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x0%~1" == "x0" 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 linesMap 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 /backGroundCircle 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':VolumeControlView-Sample', ':volume-control-view' 2 | -------------------------------------------------------------------------------- /volume-control-view/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /volume-control-view/bintray.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.jfrog.bintray' 2 | 3 | task sourcesJar(type: Jar) { 4 | from android.sourceSets.main.java.srcDirs 5 | classifier = 'sources' 6 | } 7 | 8 | task javadoc(type: Javadoc) { 9 | source = android.sourceSets.main.java.srcDirs 10 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 11 | } 12 | 13 | task javadocJar(type: Jar, dependsOn: javadoc) { 14 | classifier = 'javadoc' 15 | from javadoc.destinationDir 16 | } 17 | artifacts { 18 | archives javadocJar 19 | archives sourcesJar 20 | } 21 | 22 | // Bintray https://github.com/bintray/gradle-bintray-plugin 23 | Properties properties = new Properties() 24 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 25 | 26 | 27 | bintray { 28 | user = properties.getProperty("bintray.user") 29 | key = properties.getProperty("bintray.apikey") 30 | 31 | configurations = ['archives'] 32 | pkg { 33 | repo = 'maven' 34 | name = 'VolumeControlView' //YOUR PACKAGE NAME 35 | userOrg = 'agilie' 36 | //An optional organization name when the repo belongs to one of the user's orgs 37 | desc = 'Visual regulator can be connected to a player or other smart house’s device making the process of controlling the level of a particular characteristic much easier.' // YOUR LIBRARY DESCRIPTION 38 | websiteUrl = 'https://github.com/agilie/VolumeControlView' // YOUR SITE 39 | vcsUrl = 'https://github.com/agilie/VolumeControlView' // YOUR GIT REPO 40 | licenses = ["Apache-2.0"] // A LIST OF YOUR LICENCES 41 | publish = true 42 | publicDownloadNumbers = true 43 | } 44 | 45 | version = '1.0' //YOUR LIBRARY VERSION 46 | } 47 | 48 | -------------------------------------------------------------------------------- /volume-control-view/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 25 7 | buildToolsVersion "25.0.3" 8 | 9 | defaultConfig { 10 | minSdkVersion 19 11 | targetSdkVersion 25 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | compile fileTree(dir: 'libs', include: ['*.jar']) 28 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 29 | exclude group: 'com.android.support', module: 'support-annotations' 30 | }) 31 | compile 'com.android.support:appcompat-v7:25.3.1' 32 | testCompile 'junit:junit:4.12' 33 | compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" 34 | } 35 | repositories { 36 | mavenCentral() 37 | } 38 | 39 | apply from: 'install.gradle' 40 | apply from: 'bintray.gradle' -------------------------------------------------------------------------------- /volume-control-view/install.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.github.dcendents.android-maven' 2 | group = 'com.agilie' // CREATE A GROUP ID FOR YOUR LIBRARY 3 | 4 | install { 5 | repositories.mavenInstaller { 6 | pom { 7 | project { 8 | packaging 'VolumeControlView' 9 | groupId 'com.agilie' // CREATE A GROUP ID FOR YOUR LIBRARY 10 | artifactId 'volume-control-view' // THE NAME OF YOUR MODULE 11 | 12 | name 'VolumeControlView' // YOUR LIBRARY NAME 13 | description 'Visual regulator can be connected to a player or other smart house’s device making the process of controlling the level of a particular characteristic much easier.' // YOUR LIBRARY DESCRIPTION 14 | url 'https://github.com/agilie/VolumeControlView' // YOUR SITE 15 | 16 | licenses { 17 | license { 18 | name 'The Apache Software License, Version 2.0' 19 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 20 | } 21 | } 22 | developers { 23 | developer { 24 | 25 | } 26 | } 27 | scm { 28 | connection 'https://github.com/agilie/VolumeControlView.git' // YOUR GIT REPO 29 | developerConnection 'https://github.com/agilie/VolumeControlView.git' // YOUR GIT REPO 30 | url 'https://github.com/agilie/VolumeControlView' // YOUR SITE 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /volume-control-view/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/eugene/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /volume-control-view/src/androidTest/java/com/agilie/volumecontrol/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.agilie.spline_controller.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /volume-control-view/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol 2 | 3 | import android.graphics.PointF 4 | 5 | fun getPointOnBorderLineOfCircle(point: PointF?, radius: Float, alfa: Int = 0) = 6 | PointF().apply { 7 | if (point != null) { 8 | x = (radius * Math.cos(Math.toRadians(alfa - 90.0)) + point.x).toFloat() 9 | y = (radius * Math.sin(Math.toRadians(alfa - 90.0)) + point.y).toFloat() 10 | } 11 | } 12 | 13 | fun calculateAngleWithTwoVectors(touch: PointF?, center: PointF?): Double { 14 | var angle = 0.0 15 | if (touch != null && center != null) { 16 | val x2 = touch.x - center.x 17 | val y2 = touch.y - center.y 18 | val d1 = Math.sqrt((center.y * center.y).toDouble()) 19 | val d2 = Math.sqrt((x2 * x2 + y2 * y2).toDouble()) 20 | if (touch.x >= center.x) { 21 | angle = Math.toDegrees(Math.acos((-center.y * y2) / (d1 * d2))) 22 | } else 23 | angle = 360 - Math.toDegrees(Math.acos((-center.y * y2) / (d1 * d2))) 24 | } 25 | return angle 26 | } 27 | 28 | fun pointInCircle(point: PointF, pointCenter: PointF, radius: Float) = 29 | Math.pow((point.x - pointCenter.x).toDouble(), 2.0) + 30 | Math.pow((point.y - pointCenter.y).toDouble(), 2.0) <= radius * radius 31 | 32 | 33 | fun getPointOnBorderLineOfCircle(innerX: Float, innerY: Float, innerRadius: Float, alfa: Double = 0.0) = 34 | PointF().apply { 35 | x = (innerRadius * Math.cos(Math.toRadians(alfa - 90.0)) + innerX).toFloat() 36 | y = (innerRadius * Math.sin(Math.toRadians(alfa - 90.0)) + innerY).toFloat() 37 | } 38 | 39 | fun calculateAngleWithTwoVectors(touchX: Float, touchY: Float, centerX: Float, centerY: Float): Double { 40 | val angle: Double 41 | val x2 = touchX - centerX 42 | val y2 = touchY - centerY 43 | val d1 = Math.sqrt((centerY * centerY).toDouble()) 44 | val d2 = Math.sqrt((x2 * x2 + y2 * y2).toDouble()) 45 | if (touchX >= centerX) { 46 | angle = Math.toDegrees(Math.acos((-centerY * y2) / (d1 * d2))) 47 | } else 48 | angle = 360 - Math.toDegrees(Math.acos((-centerY * y2) / (d1 * d2))) 49 | return angle 50 | } 51 | 52 | fun closestValue(value: Double, closestValue: Int): Int { 53 | var j = (Math.round(value)).toInt() 54 | while (true) { 55 | if (j > 0 && closestValue > 0) { 56 | if (j % closestValue == 0) 57 | return j 58 | else 59 | ++j 60 | } else 61 | return j 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/animation/controller/Controller.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol.animation.controller 2 | 3 | import android.graphics.Canvas 4 | 5 | interface Controller { 6 | 7 | fun onDraw(canvas: Canvas) 8 | fun onSizeChanged(width: Int, height: Int) 9 | } -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/animation/controller/ControllerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol.animation.controller 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.PointF 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.MotionEvent 8 | import com.agilie.volumecontrol.animation.painter.* 9 | import com.agilie.volumecontrol.calculateAngleWithTwoVectors 10 | import com.agilie.volumecontrol.closestValue 11 | import com.agilie.volumecontrol.getPointOnBorderLineOfCircle 12 | import com.agilie.volumecontrol.view.VolumeControlView.Companion.CONTROLLER_SPACE 13 | import com.agilie.volumecontrol.view.VolumeControlView.Companion.INNER_CIRCLE_STROKE_WIDTH 14 | import com.agilie.volumecontrol.view.VolumeControlView.Companion.MOVABLE_CIRCLE_RADIUS 15 | import com.agilie.volumecontrol.view.VolumeControlView.Companion.SECTOR_STEP 16 | import java.util.* 17 | 18 | 19 | class ControllerImpl(val innerCircleImpl: InnerCircleImpl, 20 | val movableCircleImpl: MovableCircleImpl, 21 | val splinePath: SplinePath, 22 | val mainCircleImpl: MainCircleImpl, 23 | var backgroundShiningImpl: BackgroundShiningImpl) : Controller { 24 | 25 | interface OnTouchControllerListener { 26 | fun onControllerDown(angle: Int, percent: Int) 27 | fun onControllerMove(angle: Int, percent: Int) 28 | fun onAngleChange(angle: Int, percent: Int) 29 | } 30 | 31 | private var width = 0 32 | private var height = 0 33 | private var eventRadius: Float = 0f 34 | private var distance: Float = 0f 35 | private var controllerCenter: PointF = PointF() 36 | private var controllerRadius = 0f 37 | private var linesList = ArrayList() 38 | 39 | var onTouchControllerListener: OnTouchControllerListener? = null 40 | 41 | /** Draw all object after void onSizeChange*/ 42 | override fun onDraw(canvas: Canvas) { 43 | backgroundShiningImpl.onDraw(canvas) 44 | mainCircleImpl.onDraw(canvas) 45 | linesList.forEach { it.onDraw(canvas) } 46 | splinePath.onDraw(canvas) 47 | innerCircleImpl.onDraw(canvas) 48 | movableCircleImpl.onDraw(canvas) 49 | } 50 | 51 | /** When call onSizeChanged we set init all radius and coordinates of the centers*/ 52 | override fun onSizeChanged(w: Int, h: Int) { 53 | this.width = w 54 | this.height = h 55 | setCircleRadius(w, h) 56 | setCenterCoordinates(w, h) 57 | createSplinePath() 58 | initLines() 59 | } 60 | 61 | fun onTouchEvent(event: MotionEvent) { 62 | when (event.action) { 63 | MotionEvent.ACTION_DOWN -> { 64 | onActionDown(PointF(event.x, event.y)) 65 | } 66 | MotionEvent.ACTION_MOVE -> { 67 | onActionMove(PointF(event.x, event.y)) 68 | } 69 | MotionEvent.ACTION_UP -> { 70 | //nothing here 71 | } 72 | } 73 | } 74 | 75 | /**Save state of previousAngle*/ 76 | fun onSaveInstanceState(bundle: Bundle) { 77 | bundle.putInt("previousAngle", previousAngle) 78 | bundle.putBoolean("onRestore", true) 79 | bundle.putBoolean("firstLaunch", firstLaunch) 80 | } 81 | 82 | /**Restore state*/ 83 | fun onRestoreInstanceState(bundle: Bundle) { 84 | previousAngle = bundle.getInt("previousAngle") 85 | onRestore = bundle.getBoolean("onRestore") 86 | firstLaunch = bundle.getBoolean("firstLaunch") 87 | } 88 | 89 | /** Move shapes to new position*/ 90 | private fun onActionDown(touchPointF: PointF) { 91 | //if (firstLaunch) actionDownAngle = startAngle else getClosestAngle(touchPointF) 92 | actionDownAngle = getClosestAngle(touchPointF) 93 | 94 | val startAngle = getStartAngle(touchPointF) 95 | val point = getPointOnBorderLineOfCircle(controllerCenter, eventRadius, startAngle) 96 | 97 | previousAngle = actionDownAngle 98 | direction = Direction.UNDEFINED 99 | angleDelta = 0 100 | 101 | val percentage = calculatePercent(actionDownAngle) 102 | onTouchControllerListener?.onControllerDown(actionDownAngle, percentage) 103 | onTouchControllerListener?.onAngleChange(actionDownAngle, percentage) 104 | 105 | movableCircleImpl.onActionMove(point) 106 | backgroundShiningImpl.gradientAngle = actionDownAngle 107 | splinePath.onReset() 108 | splinePath.onDrawBigSpline(actionDownAngle, startAngle) 109 | } 110 | 111 | private var actionDownAngle: Int = 0 112 | private var angleDelta = 0 113 | 114 | private var previousAngle = 0 115 | private var direction: Direction = Direction.UNDEFINED 116 | 117 | enum class Direction { 118 | UNDEFINED, CLOCKWISE, CCLOCKWISE 119 | } 120 | 121 | 122 | /**In order to correctly move all the figures relative to the touch point, 123 | * we need to calculate the nearest sector to the touch point. 124 | * After calculating the coordinates of the new points to which you must move all the shapes. 125 | * Also we need to detected full and empty circle*/ 126 | private fun onActionMove(touchPointF: PointF) { 127 | val currentAngle = getClosestAngle(touchPointF) 128 | //val startAngle = getStartAngle(touchPointF) 129 | val moveToPoint = getPointOnBorderLineOfCircle(controllerCenter, eventRadius, currentAngle) 130 | val startPoint = getPointOnBorderLineOfCircle(controllerCenter, eventRadius, 0) 131 | 132 | val angleChanged = previousAngle != currentAngle 133 | 134 | if (angleChanged) { 135 | if (overlappedClockwise(direction, previousAngle, currentAngle)) { 136 | angleDelta += (360 - previousAngle + currentAngle) 137 | } else if (overlappedCclockwise(direction, previousAngle, currentAngle)) { 138 | angleDelta -= (360 - currentAngle + previousAngle) 139 | } else if (previousAngle < currentAngle) { 140 | direction = Direction.CLOCKWISE 141 | angleDelta += (currentAngle - previousAngle) 142 | } else { 143 | direction = Direction.CCLOCKWISE 144 | angleDelta -= (previousAngle - currentAngle) 145 | } 146 | } 147 | 148 | val angle = Math.max(Math.min(actionDownAngle + angleDelta, 360), 0) 149 | val percentage = calculatePercent(angle) 150 | 151 | if (moveMovableCircle(angle)) { 152 | movableCircleImpl.onActionMove(moveToPoint) 153 | backgroundShiningImpl.gradientAngle = currentAngle 154 | } else { 155 | movableCircleImpl.onActionMove(startPoint) 156 | } 157 | 158 | onTouchControllerListener?.onControllerMove(angle, percentage) 159 | 160 | if (angleChanged) { 161 | onTouchControllerListener?.onAngleChange(angle, percentage) 162 | } 163 | 164 | splinePath.onReset() 165 | splinePath.onDrawBigSpline(angle, currentAngle) 166 | 167 | previousAngle = currentAngle 168 | } 169 | 170 | private fun moveMovableCircle(angle: Int): Boolean { 171 | if (angle == 360 || angle == 0) { 172 | return false 173 | } 174 | return true 175 | } 176 | 177 | private var onRestore = false 178 | 179 | /**Draw spline. If we draw this for the first time than call void onCreateSpiralPath */ 180 | var startAngle = 0 181 | var firstLaunch = true 182 | private fun createSplinePath() { 183 | if (onRestore) { 184 | Log.d("Restore", "-----------------------------------------------------------") 185 | val restoreTouchPoint = getPointOnBorderLineOfCircle(controllerCenter, controllerRadius, previousAngle) 186 | onActionDown(restoreTouchPoint) 187 | } else { 188 | if (!firstLaunch) splinePath.onCreateSpiralPath(drawToAngle = 0, startAngle = 0) 189 | else { 190 | splinePath.onCreateSpiralPath(drawToAngle = 0, startAngle = startAngle) 191 | onActionDown(getPointOnBorderLineOfCircle(controllerCenter, controllerRadius, startAngle)) 192 | firstLaunch = false 193 | } 194 | } 195 | } 196 | 197 | /**Set all centers coordinates */ 198 | private fun setCenterCoordinates(w: Int, h: Int) { 199 | controllerCenter.apply { 200 | x = w / 2f 201 | y = h / 2f 202 | } 203 | 204 | innerCircleImpl.center = controllerCenter 205 | movableCircleImpl.center.apply { 206 | x = controllerCenter.x 207 | y = controllerCenter.y - eventRadius 208 | } 209 | 210 | mainCircleImpl.center = controllerCenter 211 | backgroundShiningImpl.center = controllerCenter 212 | 213 | splinePath.spiralStartPoint = getPointOnBorderLineOfCircle(controllerCenter, 214 | innerCircleImpl.radius + INNER_CIRCLE_STROKE_WIDTH, 0) 215 | 216 | splinePath.innerCircleCenter = innerCircleImpl.center 217 | splinePath.center = controllerCenter 218 | } 219 | 220 | /** Set all radius. The area of ​​the controller depends on the variable 221 | * CONTROLLER_SPACE. The area of ​​the inner circle half of controller radius */ 222 | private fun setCircleRadius(w: Int, h: Int) { 223 | controllerRadius = if (w > h) h / CONTROLLER_SPACE else w / CONTROLLER_SPACE 224 | mainCircleImpl.radius = controllerRadius 225 | 226 | backgroundShiningImpl.radius = controllerRadius 227 | 228 | innerCircleImpl.radius = controllerRadius / 2 229 | movableCircleImpl.radius = MOVABLE_CIRCLE_RADIUS 230 | 231 | splinePath.innerCircleRadius = innerCircleImpl.radius 232 | splinePath.radius = controllerRadius 233 | 234 | eventRadius = innerCircleImpl.radius - movableCircleImpl.radius * 2 235 | distance = (controllerRadius - innerCircleImpl.radius) / 360 236 | 237 | splinePath.distance = distance 238 | } 239 | 240 | /** Draw sector lines */ 241 | private fun initLines() { 242 | for (i in 0..360 step SECTOR_STEP) { 243 | val line = SimpleLineImpl(splinePath.splinePaint) 244 | line.startPoint = controllerCenter 245 | val endPoint = getPointOnBorderLineOfCircle(controllerCenter.x, 246 | controllerCenter.y, controllerRadius, i.toDouble()) 247 | line.endPoint = endPoint 248 | linesList.add(line) 249 | } 250 | } 251 | 252 | private fun overlappedCclockwise(direction: Direction, previousAngle: Int, currentAngle: Int) = direction == Direction.CCLOCKWISE && (currentAngle - previousAngle) > 45 253 | 254 | private fun overlappedClockwise(direction: Direction, previousAngle: Int, currentAngle: Int) = direction == Direction.CLOCKWISE && (previousAngle - currentAngle) > 45 255 | 256 | private fun getClosestAngle(touchPointF: PointF) = 257 | closestValue(calculateAngleWithTwoVectors(touchPointF, controllerCenter), SECTOR_STEP) 258 | 259 | private fun getStartAngle(touchPointF: PointF) = 260 | (Math.round(calculateAngleWithTwoVectors(touchPointF, controllerCenter))).toInt() 261 | 262 | 263 | private fun calculatePercent(angle: Int) = Math.round(angle / 360f * 100) 264 | 265 | } -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/animation/painter/BackgroundShiningImpl.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol.animation.painter 2 | 3 | import android.graphics.* 4 | import com.agilie.volumecontrol.getPointOnBorderLineOfCircle 5 | 6 | class BackgroundShiningImpl(val paint: Paint, 7 | val paint2: Paint, 8 | var colors: IntArray, 9 | var colors2: IntArray, 10 | var minShiningRadius: Float, 11 | var maxShiningRadius: Float, 12 | var frequency: Float) : Painter { 13 | 14 | private var incrementer: Incrementer? = null 15 | 16 | init { 17 | incrementer = Incrementer() 18 | incrementer?.start() 19 | } 20 | 21 | @Volatile 22 | private var currentSplash = 1.3f 23 | var radius: Float = 0f 24 | var center = PointF() 25 | var gradientAngle = 0 26 | 27 | override fun onDraw(canvas: Canvas) { 28 | 29 | paint.shader = RadialGradient(center.x, center.y, radius * currentSplash, colors, 30 | floatArrayOf(0.01F, 0.99F), Shader.TileMode.CLAMP) 31 | 32 | val startPoint = getPointOnBorderLineOfCircle(center, radius, gradientAngle + 180) 33 | val endPoint = getPointOnBorderLineOfCircle(center, radius, gradientAngle) 34 | 35 | paint2.shader = LinearGradient(startPoint.x, startPoint.y, endPoint.x, endPoint.y, colors2, 36 | null, 37 | Shader.TileMode.CLAMP) 38 | 39 | canvas.drawCircle(center.x, center.y, radius * currentSplash, paint) 40 | canvas.drawCircle(center.x, center.y, radius * currentSplash, paint2) 41 | 42 | } 43 | 44 | override fun onSizeChanged(w: Int, h: Int) { 45 | } 46 | 47 | /**Class for implementing shining logic */ 48 | inner class Incrementer : Thread() { 49 | 50 | @Volatile 51 | private var isIncrement = true 52 | private var time = 0f 53 | 54 | override fun run() { 55 | 56 | do { 57 | sleep(16) 58 | when (isIncrement) { 59 | true -> onIncrement() 60 | false -> onDecrement() 61 | } 62 | 63 | } while (!Thread.interrupted()) 64 | } 65 | 66 | private fun onDecrement() { 67 | if (currentSplash >= minShiningRadius) { 68 | time += frequency 69 | currentSplash -= time 70 | } else { 71 | time = 0f 72 | isIncrement = true 73 | } 74 | } 75 | 76 | private fun onIncrement() { 77 | if (currentSplash <= maxShiningRadius) { 78 | currentSplash += time 79 | time += frequency 80 | } else { 81 | time = 0f 82 | isIncrement = false 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/animation/painter/InnerCircle.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol.animation.painter 2 | 3 | interface InnerCircle : Painter { 4 | } -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/animation/painter/InnerCircleImpl.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol.animation.painter 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import android.graphics.PointF 6 | 7 | class InnerCircleImpl(val paint: Paint) : InnerCircle { 8 | 9 | var radius: Float = 0f 10 | var center = PointF() 11 | 12 | override fun onDraw(canvas: Canvas) { 13 | canvas.drawCircle(center.x, center.y, radius, paint) 14 | } 15 | 16 | override fun onSizeChanged(w: Int, h: Int) { 17 | } 18 | } -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/animation/painter/MainCircleImpl.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol.animation.painter 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import android.graphics.PointF 6 | import android.graphics.SweepGradient 7 | 8 | class MainCircleImpl(val paint: Paint, val colors: IntArray) : Painter { 9 | 10 | var radius: Float = 0f 11 | var center = PointF() 12 | 13 | override fun onDraw(canvas: Canvas) { 14 | paint.shader = SweepGradient(center.x, center.y, colors, null) 15 | canvas.drawCircle(center.x, center.y, radius, paint) 16 | 17 | } 18 | 19 | override fun onSizeChanged(w: Int, h: Int) { 20 | } 21 | } -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/animation/painter/MovableCircle.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol.animation.painter 2 | 3 | import android.graphics.PointF 4 | 5 | interface MovableCircle : Painter { 6 | 7 | fun onActionMove(mainCenter: PointF) 8 | fun onActionDown(pointF: PointF) 9 | fun onActionUp(pointF: PointF) 10 | } -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/animation/painter/MovableCircleImpl.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol.animation.painter 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import android.graphics.PointF 6 | import com.agilie.volumecontrol.pointInCircle 7 | 8 | class MovableCircleImpl(val paint: Paint) : MovableCircle { 9 | 10 | var center = PointF() 11 | var radius = 0f 12 | 13 | override fun onDraw(canvas: Canvas) { 14 | canvas.drawCircle(center.x, center.y, radius, paint) 15 | } 16 | 17 | override fun onSizeChanged(w: Int, h: Int) { 18 | } 19 | 20 | override fun onActionDown(pointF: PointF) { 21 | if (!pointInCircle(pointF, center, radius * 2)) 22 | return 23 | // TODO start light show 24 | } 25 | 26 | override fun onActionMove(point: PointF) { 27 | center.apply { 28 | x = point.x 29 | y = point.y 30 | } 31 | } 32 | 33 | override fun onActionUp(pointF: PointF) { 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/animation/painter/Painter.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol.animation.painter 2 | 3 | import android.graphics.Canvas 4 | 5 | interface Painter { 6 | 7 | fun onDraw(canvas: Canvas) 8 | fun onSizeChanged(w: Int, h: Int) 9 | 10 | } -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/animation/painter/SimpleLineImpl.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol.animation.painter 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import android.graphics.PointF 6 | 7 | class SimpleLineImpl(val paint: Paint) : Painter { 8 | 9 | var startPoint = PointF() 10 | var endPoint = PointF() 11 | 12 | override fun onDraw(canvas: Canvas) { 13 | canvas.drawLine(startPoint.x, 14 | startPoint.y, endPoint.x, endPoint.y, paint) 15 | } 16 | 17 | override fun onSizeChanged(w: Int, h: Int) { 18 | } 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/animation/painter/SplinePath.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol.animation.painter 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import android.graphics.Path 6 | import android.graphics.PointF 7 | import com.agilie.volumecontrol.getPointOnBorderLineOfCircle 8 | 9 | class SplinePath(val splinePath: Path, val splinePaint: Paint) : Painter { 10 | 11 | var spiralStartPoint: PointF? = null 12 | var center: PointF? = null 13 | var innerCircleCenter: PointF? = null 14 | var radius = 0f 15 | var innerCircleRadius = 0f 16 | var distance = 0f 17 | 18 | override fun onDraw(canvas: Canvas) { 19 | canvas.drawPath(splinePath, splinePaint) 20 | } 21 | 22 | override fun onSizeChanged(w: Int, h: Int) { 23 | } 24 | 25 | fun onReset() { 26 | splinePath.reset() 27 | } 28 | 29 | fun onCreateSpiralPath(drawToAngle: Int, startAngle: Int) { 30 | drawBigSpline(drawToAngle, startAngle) 31 | } 32 | 33 | fun onDrawBigSpline(angle: Int, startAngle: Int) { 34 | drawBigSpline(angle, startAngle) 35 | } 36 | 37 | /** Our custom spline consists of line path. 38 | *In order to correctly draw a spline it is necessary to pass four control points. 39 | From the starting point, which is a point on the inner circle lying at an angle of 40 | zero degrees relative to the circle circle. 41 | Second point it is on the outer circle lies at the same angle as the first point. 42 | The third point determines which area is to be shown 43 | The fourth point closes our circle * */ 44 | private fun drawBigSpline(angle: Int, startAngle: Int) { 45 | 46 | val startPoint = getPointOnBorderLineOfCircle(innerCircleCenter, innerCircleRadius, angle) 47 | val controlPoint2 = getPointOnBorderLineOfCircle(center, radius, angle) 48 | 49 | val radius = innerCircleRadius + distance * angle 50 | val controlPoint3 = getPointOnBorderLineOfCircle(center, radius, angle) 51 | //Move to point 1,2 52 | splinePath.moveTo(startPoint.x, startPoint.y) 53 | splinePath.lineTo(controlPoint2.x, controlPoint2.y) 54 | //Describe the outer circle to point 2 55 | (angle..360 + angle step 6) 56 | .map { getPointOnBorderLineOfCircle(center, this.radius + 5, it) } 57 | .forEach { splinePath.lineTo(it.x, it.y) } 58 | //Move to control point 3 59 | splinePath.lineTo(controlPoint3.x, controlPoint3.y) 60 | angle.downTo(0).forEach { 61 | val radius = innerCircleRadius + distance * it 62 | val point = getPointOnBorderLineOfCircle(center, radius, it) 63 | splinePath.lineTo(point.x, point.y) 64 | } 65 | //Move to point 4 66 | for (i in 0..360 step 20) { 67 | val point = getPointOnBorderLineOfCircle(innerCircleCenter, innerCircleRadius, i) 68 | if (i == startAngle) { 69 | splinePath.lineTo(point.x, point.y) 70 | } 71 | splinePath.lineTo(point.x, point.y) 72 | } 73 | 74 | splinePath.close() 75 | } 76 | } -------------------------------------------------------------------------------- /volume-control-view/src/main/java/com/agilie/volumecontrol/view/VolumeControlView.kt: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol.view 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.graphics.Path 8 | import android.os.Bundle 9 | import android.os.Parcelable 10 | import android.util.AttributeSet 11 | import android.util.Log 12 | import android.view.MotionEvent 13 | import android.view.View 14 | import android.view.ViewGroup 15 | import com.agilie.volomecontrolview.R 16 | import com.agilie.volomecontrolview.R.styleable.* 17 | import com.agilie.volumecontrol.animation.controller.ControllerImpl 18 | import com.agilie.volumecontrol.animation.painter.* 19 | 20 | 21 | 22 | class VolumeControlView : View, View.OnTouchListener { 23 | 24 | companion object { 25 | val INNER_CIRCLE_STROKE_WIDTH = 4f 26 | var SECTOR_STEP = 6 27 | var CONTROLLER_SPACE = 3f 28 | var MOVABLE_CIRCLE_RADIUS = 10f 29 | } 30 | 31 | var backgroundLayoutColor = Color.parseColor("#E3E4E5") 32 | private var splineColor = Color.BLACK 33 | private var movableCircleColor = Color.rgb(80, 254, 253) 34 | private var innerCircleColor = Color.rgb(80, 254, 253) 35 | private var minShiningRadius = 1.3f 36 | private var maxShiningRadius = 1.5f 37 | private var shiningStep = 0.004f 38 | 39 | var controller: ControllerImpl? = null 40 | var colors = intArrayOf( 41 | Color.parseColor("#0080ff"), 42 | Color.parseColor("#6000FF"), 43 | Color.parseColor("#0533FF"), 44 | Color.parseColor("#C467FF"), 45 | Color.parseColor("#FFB6C2"), 46 | Color.parseColor("#E7FBE1"), 47 | Color.parseColor("#53FFFF"), 48 | Color.parseColor("#0080ff")) 49 | 50 | private var backgroundColors = intArrayOf( 51 | Color.parseColor("#FF4081"), 52 | Color.parseColor("#000000")) 53 | 54 | private var backgroundColorsLine = intArrayOf( 55 | Color.parseColor("#000000"), 56 | Color.parseColor("#00000000")) 57 | 58 | 59 | constructor(context: Context) : super(context) { 60 | initAttrs(null) 61 | initController() 62 | } 63 | 64 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { 65 | initAttrs(attrs) 66 | initController() 67 | } 68 | 69 | fun setStartAngle(angle: Int) { 70 | controller?.startAngle = angle 71 | } 72 | 73 | fun setStartPercent(percent: Int) { 74 | when (percent) { 75 | in 100..Int.MAX_VALUE -> { 76 | controller?.startAngle = 360 77 | Log.d("tag", "") 78 | } 79 | else -> controller?.startAngle = (percent * 360) / 100 80 | } 81 | } 82 | 83 | override fun onDraw(canvas: Canvas) { 84 | super.onDraw(canvas) 85 | controller?.onDraw(canvas) 86 | invalidate() 87 | } 88 | 89 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 90 | controller?.onSizeChanged(w, h) 91 | super.onSizeChanged(w, h, oldw, oldh) 92 | } 93 | 94 | override fun onTouch(v: View, event: MotionEvent): Boolean { 95 | controller?.onTouchEvent(event) 96 | return true 97 | } 98 | 99 | override fun onSaveInstanceState(): Parcelable { 100 | val bundle = Bundle() 101 | bundle.putParcelable("superState", super.onSaveInstanceState()) 102 | controller?.onSaveInstanceState(bundle) 103 | return bundle 104 | } 105 | 106 | override fun onRestoreInstanceState(state: Parcelable) { 107 | val bundle = state as Bundle 108 | controller?.onRestoreInstanceState(bundle) 109 | super.onRestoreInstanceState(bundle.getParcelable("superState")) 110 | } 111 | 112 | fun setBackgroundShiningColor(fillColor: Int, backgroundColor: Int = backgroundLayoutColor) { 113 | backgroundColors = intArrayOf(fillColor, backgroundColor) 114 | backgroundColorsLine = intArrayOf(backgroundColor, Color.parseColor("#00000000")) 115 | 116 | controller?.backgroundShiningImpl?.colors = backgroundColors 117 | controller?.backgroundShiningImpl?.colors2 = backgroundColorsLine 118 | } 119 | 120 | fun setShiningMaxRadius(radius: Float) { 121 | controller?.backgroundShiningImpl?.maxShiningRadius = radius 122 | } 123 | 124 | fun setShiningMinRadius(radius: Float) { 125 | controller?.backgroundShiningImpl?.minShiningRadius = radius 126 | } 127 | 128 | fun setShiningFrequency(step: Float) { 129 | controller?.backgroundShiningImpl?.frequency = step 130 | } 131 | 132 | fun getShiningMaxRadius() = controller?.backgroundShiningImpl?.maxShiningRadius 133 | fun getShiningMinRadius() = controller?.backgroundShiningImpl?.minShiningRadius 134 | fun getFrequency() = controller?.backgroundShiningImpl?.frequency 135 | 136 | private fun initAttrs(attrs: AttributeSet?) { 137 | 138 | val attributes = context 139 | .obtainStyledAttributes(attrs, R.styleable.VolumeControlView) 140 | //Colors attrs 141 | innerCircleColor = attributes.getColor(VolumeControlView_innerCircleColor, innerCircleColor) 142 | movableCircleColor = attributes.getColor(VolumeControlView_movableCircleColor, movableCircleColor) 143 | splineColor = attributes.getColor(VolumeControlView_splineCircleColor, splineColor) 144 | 145 | //Shining attrs 146 | minShiningRadius = attributes.getFloat(VolumeControlView_minShiningRadius, minShiningRadius) 147 | maxShiningRadius = attributes.getFloat(VolumeControlView_maxShiningRadius, maxShiningRadius) 148 | shiningStep = attributes.getFloat(VolumeControlView_shiningFrequency, shiningStep) 149 | 150 | val step = attributes.getInt(VolumeControlView_sectorRadius, SECTOR_STEP) 151 | val controllerSpace = attributes.getFloat(VolumeControlView_controllerSpace, CONTROLLER_SPACE) 152 | val movableCircleRadius = attributes.getFloat(VolumeControlView_movableCircleRadius, MOVABLE_CIRCLE_RADIUS) 153 | 154 | //Checks start values 155 | SECTOR_STEP = if (step > 0) step else SECTOR_STEP 156 | CONTROLLER_SPACE = if (controllerSpace > 0) controllerSpace else CONTROLLER_SPACE 157 | MOVABLE_CIRCLE_RADIUS = if (movableCircleRadius > 0) movableCircleRadius else MOVABLE_CIRCLE_RADIUS 158 | 159 | attributes.recycle() 160 | setLayerType(ViewGroup.LAYER_TYPE_SOFTWARE, null) 161 | setWillNotDraw(false) 162 | setOnTouchListener(this) 163 | } 164 | 165 | private fun initController() { 166 | controller = ControllerImpl( 167 | InnerCircleImpl(setInnerCirclePaint()), 168 | MovableCircleImpl(setMovableCirclePaint()), 169 | SplinePath(Path(), setSplinePathPaint()), 170 | MainCircleImpl(setMainCirclePaint(), colors), 171 | BackgroundShiningImpl(Paint(), 172 | Paint(), 173 | backgroundColors, 174 | backgroundColorsLine, 175 | minShiningRadius, 176 | maxShiningRadius, 177 | shiningStep)) 178 | } 179 | 180 | private fun setInnerCirclePaint() = Paint().apply { 181 | color = innerCircleColor 182 | isAntiAlias = true 183 | style = Paint.Style.STROKE 184 | strokeWidth = INNER_CIRCLE_STROKE_WIDTH 185 | } 186 | 187 | private fun setMovableCirclePaint() = Paint().apply { 188 | color = movableCircleColor 189 | isAntiAlias = true 190 | style = Paint.Style.FILL 191 | } 192 | 193 | private fun setSplinePathPaint() = Paint().apply { 194 | color = splineColor 195 | isAntiAlias = true 196 | style = Paint.Style.FILL 197 | strokeWidth = 2f 198 | } 199 | 200 | private fun setMainCirclePaint() = Paint().apply { 201 | strokeCap = Paint.Cap.SQUARE 202 | strokeWidth = 1F 203 | style = Paint.Style.FILL 204 | } 205 | 206 | } 207 | 208 | 209 | -------------------------------------------------------------------------------- /volume-control-view/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /volume-control-view/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | volume-control-view 3 | 4 | -------------------------------------------------------------------------------- /volume-control-view/src/test/java/com/agilie/volumecontrol/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.agilie.volumecontrol; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } --------------------------------------------------------------------------------