├── .gitignore ├── LICENSE ├── README.md ├── android-slidr ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── florent37 │ │ └── androidslidr │ │ ├── Slidr.java │ │ └── Sushi.java │ └── res │ └── values │ ├── attrs.xml │ └── attrs_non_editable.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── florent37 │ │ └── slidr │ │ ├── EditActivity.java │ │ ├── MainActivity.java │ │ ├── MainApplication.java │ │ └── NonEditActivity.java │ └── res │ ├── layout │ ├── activity_edit.xml │ ├── activity_main.xml │ └── activity_non_editable.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle ├── bintray-android-v1.gradle ├── bintray-java-v1.gradle ├── install-v1.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── medias ├── slidr1 copie.png ├── slidr1.png ├── slidr2_1 copie.png ├── slidr2_1.png ├── slidr2_2 copie.png ├── slidr2_2.png ├── slidr_region copie.png └── slidr_region.png ├── publish.sh └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea/workspace.xml 38 | 39 | # Keystore files 40 | *.jks 41 | /.idea/ 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # android-slidr 2 | 3 | Another android slider / seekbar, but different :-) 4 | 5 | 6 | 7 | Android app on Google Play 8 | 9 | 10 | 11 | # Download 12 | 13 | Buy Me a Coffee at ko-fi.com 14 | 15 | [ ![Download](https://api.bintray.com/packages/florent37/maven/android-slidr/images/download.svg) ](https://bintray.com/florent37/maven/android-slidr/_latestVersion) 16 | ```java 17 | dependencies { 18 | compile 'com.github.florent37:android-slidr:1.0.4' 19 | } 20 | ``` 21 | 22 | [![png](https://raw.githubusercontent.com/florent37/android-slidr/master/medias/slidr1.png)](https://github.com/florent37/android-slidr) 23 | 24 | ```xml 25 | 30 | ``` 31 | 32 | # Step 33 | 34 | [![png](https://raw.githubusercontent.com/florent37/android-slidr/master/medias/slidr2_1.png)](https://github.com/florent37/android-slidr) 35 | [![png](https://raw.githubusercontent.com/florent37/android-slidr/master/medias/slidr2_2.png)](https://github.com/florent37/android-slidr) 36 | 37 | ```xml 38 | 44 | ``` 45 | 46 | ```java 47 | final Slidr slidr = (Slidr) findViewById(R.id.slideure); 48 | slidr.setMax(500); 49 | slidr.addStep(new Slidr.Step("test", 250, Color.parseColor("#007E90"), Color.RED)); 50 | slidr.setTextMax("max\nvalue"); 51 | slidr.setCurrentValue(300); 52 | slidr.setListener(new Slidr.Listener() { 53 | @Override 54 | public void valueChanged(Slidr slidr, float currentValue) { 55 | 56 | } 57 | 58 | @Override 59 | public void bubbleClicked(Slidr slidr) { 60 | 61 | } 62 | }); 63 | ``` 64 | 65 | # Region 66 | 67 | [![png](https://raw.githubusercontent.com/florent37/android-slidr/master/medias/slidr_region.png)](https://github.com/florent37/android-slidr) 68 | 69 | ```xml 70 | 82 | ``` 83 | 84 | ```java 85 | final Slidr slidr = (Slidr) findViewById(R.id.slideure_regions); 86 | slidr.setMax(3000); 87 | slidr.setRegionTextFormatter(new Slidr.RegionTextFormatter() { 88 | @Override 89 | public String format(int region, float value) { 90 | return String.format("region %d : %d", region, (int) value); 91 | } 92 | }); 93 | slidr.addStep(new Slidr.Step("test", 1500, Color.parseColor("#007E90"), Color.parseColor("#111111"))); 94 | ``` 95 | 96 | 97 | # Credits 98 | 99 | Author: Florent Champigny 100 | 101 | Blog : [http://www.tutos-android-france.com/](http://www.www.tutos-android-france.com/) 102 | 103 | 104 | 105 | Android app on Google Play 106 | 107 | 108 | 109 | Follow me on Google+ 111 | 112 | 113 | Follow me on Twitter 115 | 116 | 117 | Follow me on LinkedIn 119 | 120 | 121 | 122 | License 123 | -------- 124 | 125 | Copyright 2017 Florent37, Inc. 126 | 127 | Licensed under the Apache License, Version 2.0 (the "License"); 128 | you may not use this file except in compliance with the License. 129 | You may obtain a copy of the License at 130 | 131 | http://www.apache.org/licenses/LICENSE-2.0 132 | 133 | Unless required by applicable law or agreed to in writing, software 134 | distributed under the License is distributed on an "AS IS" BASIS, 135 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 136 | See the License for the specific language governing permissions and 137 | limitations under the License. 138 | -------------------------------------------------------------------------------- /android-slidr/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /android-slidr/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion project.COMPILE_SDK 5 | buildToolsVersion project.BUILD_TOOL 6 | 7 | defaultConfig { 8 | minSdkVersion project.minSdkVersion 9 | targetSdkVersion project.TARGET_SDK 10 | versionCode 1 11 | versionName "1.0" 12 | 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | 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 'com.android.support:appcompat-v7:25.3.1' 26 | compile 'com.android.support:recyclerview-v7:25.3.1' 27 | } 28 | 29 | ext { 30 | bintrayRepo = 'maven' 31 | bintrayName = 'android-slidr' 32 | orgName = 'florent37' 33 | 34 | publishedGroupId = 'com.github.florent37' 35 | libraryName = 'AndroidSlidr' 36 | artifact = 'android-slidr' 37 | 38 | libraryDescription = 'AndroidSlidr' 39 | 40 | siteUrl = 'https://github.com/florent37/android-slidr' 41 | gitUrl = 'https://github.com/florent37/android-slidr.git' 42 | 43 | libraryVersion = rootProject.ext.libraryVersion 44 | 45 | developerId = 'florent37' 46 | developerName = 'florent37' 47 | developerEmail = 'champigny.florent@gmail.com' 48 | 49 | licenseName = 'The Apache Software License, Version 2.0' 50 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 51 | allLicenses = ["Apache-2.0"] 52 | } 53 | 54 | 55 | apply from: rootProject.file('gradle/install-v1.gradle') 56 | apply from: rootProject.file('gradle/bintray-android-v1.gradle') 57 | -------------------------------------------------------------------------------- /android-slidr/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/florentchampigny/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 | -------------------------------------------------------------------------------- /android-slidr/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android-slidr/src/main/java/com/github/florent37/androidslidr/Slidr.java: -------------------------------------------------------------------------------- 1 | package com.github.florent37.androidslidr; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.res.TypedArray; 7 | import android.graphics.Canvas; 8 | import android.graphics.Color; 9 | import android.graphics.Paint; 10 | import android.graphics.Path; 11 | import android.graphics.Rect; 12 | import android.graphics.drawable.ColorDrawable; 13 | import android.support.annotation.NonNull; 14 | import android.support.annotation.Nullable; 15 | import android.support.v4.view.GestureDetectorCompat; 16 | import android.support.v4.view.MotionEventCompat; 17 | import android.support.v4.widget.NestedScrollView; 18 | import android.support.v7.widget.AppCompatEditText; 19 | import android.support.v7.widget.RecyclerView; 20 | import android.text.Editable; 21 | import android.text.InputFilter; 22 | import android.text.InputType; 23 | import android.text.Layout; 24 | import android.text.StaticLayout; 25 | import android.text.TextPaint; 26 | import android.text.TextUtils; 27 | import android.text.TextWatcher; 28 | import android.util.AttributeSet; 29 | import android.util.TypedValue; 30 | import android.view.GestureDetector; 31 | import android.view.Gravity; 32 | import android.view.KeyEvent; 33 | import android.view.MotionEvent; 34 | import android.view.View; 35 | import android.view.ViewGroup; 36 | import android.view.ViewTreeObserver; 37 | import android.view.animation.AccelerateInterpolator; 38 | import android.view.inputmethod.InputMethodManager; 39 | import android.widget.EditText; 40 | import android.widget.FrameLayout; 41 | import android.widget.ScrollView; 42 | 43 | import java.util.ArrayList; 44 | import java.util.Collections; 45 | import java.util.List; 46 | 47 | import static android.view.MotionEvent.ACTION_UP; 48 | 49 | /** 50 | * Created by florentchampigny on 20/04/2017. 51 | */ 52 | 53 | public class Slidr extends FrameLayout { 54 | 55 | private static final float DISTANCE_TEXT_BAR = 10; 56 | private static final float BUBBLE_PADDING_HORIZONTAL = 15; 57 | private static final float BUBBLE_PADDING_VERTICAL = 10; 58 | 59 | private static final float BUBBLE_ARROW_HEIGHT = 10; 60 | private static final float BUBBLE_ARROW_WIDTH = 20; 61 | boolean moving = false; 62 | private Listener listener; 63 | private BubbleClickedListener bubbleClickedListener; 64 | private GestureDetectorCompat detector; 65 | private Settings settings; 66 | private float max = 1000; 67 | private float min = 0; 68 | private float currentValue = 0; 69 | private float oldValue = Float.MIN_VALUE; 70 | private List steps = new ArrayList<>(); 71 | private float barY; 72 | private float barWidth; 73 | private float indicatorX; 74 | private int indicatorRadius; 75 | private float barCenterY; 76 | private Bubble bubble = new Bubble(); 77 | private TextFormatter textFormatter = new EurosTextFormatter(); 78 | private RegionTextFormatter regionTextFormatter = null; 79 | 80 | private String textMax = ""; 81 | private String textMin = ""; 82 | private int calculatedHieght = 0; 83 | private boolean isEditing = false; 84 | private String textEditing = ""; 85 | private EditText editText; 86 | private TouchView touchView; 87 | private EditListener editListener; 88 | 89 | @Nullable 90 | private ViewGroup parentScroll; 91 | 92 | public Slidr(Context context) { 93 | this(context, null); 94 | } 95 | 96 | public Slidr(Context context, @Nullable AttributeSet attrs) { 97 | this(context, attrs, 0); 98 | } 99 | 100 | public Slidr(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 101 | super(context, attrs, defStyleAttr); 102 | 103 | init(context, attrs); 104 | } 105 | 106 | private void onClick(MotionEvent e) { 107 | if (bubble.clicked(e)) { 108 | onBubbleClicked(); 109 | } 110 | } 111 | 112 | @Override 113 | protected void onAttachedToWindow() { 114 | super.onAttachedToWindow(); 115 | parentScroll = (ViewGroup) getScrollableParentView(); 116 | } 117 | 118 | private void closeEditText() { 119 | editText.clearFocus(); 120 | 121 | final InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 122 | imm.hideSoftInputFromWindow(editText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); 123 | 124 | ((ViewGroup) touchView.getParent()).removeView(touchView); 125 | removeView(editText); 126 | 127 | isEditing = false; 128 | if (TextUtils.isEmpty(textEditing)) { 129 | textEditing = String.valueOf(currentValue); 130 | } 131 | Float value; 132 | try { 133 | value = Float.valueOf(textEditing); 134 | } catch (Exception e) { 135 | e.printStackTrace(); 136 | value = min; 137 | } 138 | 139 | 140 | value = Math.min(value, max); 141 | value = Math.max(value, min); 142 | final ValueAnimator valueAnimator = ValueAnimator.ofFloat(currentValue, value); 143 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 144 | @Override 145 | public void onAnimationUpdate(ValueAnimator animation) { 146 | setCurrentValueNoUpdate(((float) animation.getAnimatedValue())); 147 | postInvalidate(); 148 | } 149 | }); 150 | valueAnimator.setInterpolator(new AccelerateInterpolator()); 151 | valueAnimator.start(); 152 | editText = null; 153 | touchView = null; 154 | postInvalidate(); 155 | } 156 | 157 | private ViewGroup getActivityDecorView() { 158 | return (ViewGroup) ((Activity) getContext()).getWindow().getDecorView(); 159 | } 160 | 161 | 162 | private void editBubbleEditPosition() { 163 | if (isEditing) { 164 | editText.setX(Math.min(bubble.getX(), getWidth() - editText.getWidth())); 165 | editText.setY(bubble.getY()); 166 | 167 | final ViewGroup.LayoutParams params = editText.getLayoutParams(); 168 | params.width = (int) bubble.width; 169 | params.height = (int) bubble.getHeight(); 170 | editText.setLayoutParams(params); 171 | 172 | editText.animate().alpha(1f); 173 | } 174 | } 175 | 176 | private void onBubbleClicked() { 177 | if (settings.editOnBubbleClick) { 178 | isEditing = true; 179 | editText = new AppCompatEditText(getContext()) { 180 | @Override 181 | public boolean onKeyPreIme(int keyCode, KeyEvent event) { 182 | if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 183 | dispatchKeyEvent(event); 184 | closeEditText(); 185 | return false; 186 | } 187 | return super.onKeyPreIme(keyCode, event); 188 | } 189 | 190 | }; 191 | 192 | final int editMaxCharCount = 9; 193 | editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(editMaxCharCount)}); 194 | 195 | 196 | editText.setFocusable(true); 197 | editText.setFocusableInTouchMode(true); 198 | editText.setSelectAllOnFocus(true); 199 | 200 | editText.setSingleLine(true); 201 | editText.setGravity(Gravity.CENTER); 202 | //editText.setRawInputType(Configuration.KEYBOARD_12KEY); 203 | editText.setInputType(InputType.TYPE_CLASS_NUMBER); 204 | 205 | editText.setTextColor(settings.paintIndicator.getColor()); 206 | editText.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 207 | editText.setPadding(0, 0, 0, 0); 208 | editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, dpToPx(settings.textSizeBubbleCurrent)); 209 | 210 | textEditing = String.valueOf((int) currentValue); 211 | editText.setText(textEditing); 212 | 213 | final ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 214 | params.width = (int) bubble.width; 215 | params.height = (int) bubble.getHeight(); 216 | editText.setLayoutParams(params); 217 | 218 | final Rect rect = new Rect(); 219 | getGlobalVisibleRect(rect); 220 | this.touchView = new TouchView(getContext(), rect); 221 | getActivityDecorView().addView(touchView); 222 | 223 | editText.postDelayed(new Runnable() { 224 | @Override 225 | public void run() { 226 | final InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 227 | imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); 228 | 229 | touchView.setCallback(new TouchView.Callback() { 230 | @Override 231 | public void onClicked() { 232 | closeEditText(); 233 | } 234 | }); 235 | } 236 | }, 300); 237 | 238 | addView(editText); 239 | editText.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 240 | @Override 241 | public boolean onPreDraw() { 242 | editBubbleEditPosition(); 243 | editText.getViewTreeObserver().removeOnPreDrawListener(this); 244 | return false; 245 | } 246 | }); 247 | 248 | 249 | editText.requestFocus(); 250 | editText.requestFocusFromTouch(); 251 | editBubbleEditPosition(); 252 | 253 | if (editListener != null) { 254 | editListener.onEditStarted(editText); 255 | } 256 | 257 | editText.setOnKeyListener(new OnKeyListener() { 258 | public boolean onKey(View v, int keyCode, KeyEvent event) { 259 | if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { 260 | closeEditText(); 261 | return true; 262 | } 263 | return false; 264 | } 265 | }); 266 | 267 | editText.addTextChangedListener(new TextWatcher() { 268 | @Override 269 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 270 | 271 | } 272 | 273 | @Override 274 | public void onTextChanged(CharSequence s, int start, int before, int count) { 275 | textEditing = editText.getText().toString(); 276 | updateBubbleWidth(); 277 | invalidate(); 278 | editBubbleEditPosition(); 279 | } 280 | 281 | @Override 282 | public void afterTextChanged(Editable s) { 283 | } 284 | }); 285 | 286 | postInvalidate(); 287 | } 288 | if (bubbleClickedListener != null) { 289 | bubbleClickedListener.bubbleClicked(this); 290 | } 291 | } 292 | 293 | @Override 294 | public boolean onKeyPreIme(int keyCode, KeyEvent event) { 295 | if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 296 | dispatchKeyEvent(event); 297 | closeEditText(); 298 | return false; 299 | } 300 | return super.onKeyPreIme(keyCode, event); 301 | } 302 | 303 | 304 | private void init(Context context, @Nullable AttributeSet attrs) { 305 | setWillNotDraw(false); 306 | 307 | detector = new GestureDetectorCompat(context, new GestureDetector.SimpleOnGestureListener() { 308 | //some callbacks 309 | 310 | @Override 311 | public boolean onSingleTapConfirmed(MotionEvent e) { 312 | onClick(e); 313 | return super.onSingleTapConfirmed(e); 314 | } 315 | 316 | @Override 317 | public boolean onContextClick(MotionEvent e) { 318 | return super.onContextClick(e); 319 | } 320 | }); 321 | 322 | this.settings = new Settings(this); 323 | this.settings.init(context, attrs); 324 | } 325 | 326 | public void setListener(Listener listener) { 327 | this.listener = listener; 328 | } 329 | 330 | public void setBubbleClickedListener(BubbleClickedListener bubbleClickedListener) { 331 | this.bubbleClickedListener = bubbleClickedListener; 332 | } 333 | 334 | //region getters 335 | 336 | private float dpToPx(int size) { 337 | return size * getResources().getDisplayMetrics().density; 338 | } 339 | 340 | private float dpToPx(float size) { 341 | return size * getResources().getDisplayMetrics().density; 342 | } 343 | 344 | private float pxToDp(int size) { 345 | return size / getResources().getDisplayMetrics().density; 346 | } 347 | 348 | public float getMax() { 349 | return max; 350 | } 351 | 352 | public void setMax(float max) { 353 | this.max = max; 354 | updateValues(); 355 | update(); 356 | } 357 | 358 | public void setMin(float min) { 359 | this.min = min; 360 | updateValues(); 361 | update(); 362 | } 363 | 364 | public float getCurrentValue() { 365 | return currentValue; 366 | } 367 | 368 | public void setCurrentValue(float value) { 369 | this.currentValue = value; 370 | updateValues(); 371 | update(); 372 | } 373 | 374 | private void setCurrentValueNoUpdate(float value) { 375 | this.currentValue = value; 376 | listener.valueChanged(Slidr.this, currentValue); 377 | updateValues(); 378 | 379 | } 380 | 381 | public void setEditListener(EditListener editListener) { 382 | this.editListener = editListener; 383 | } 384 | 385 | public void addStep(List steps) { 386 | this.steps.addAll(steps); 387 | Collections.sort(steps); 388 | update(); 389 | } 390 | 391 | //endregion 392 | 393 | public void addStep(Step step) { 394 | this.steps.add(step); 395 | Collections.sort(steps); 396 | update(); 397 | } 398 | 399 | public void clearSteps() { 400 | this.steps.clear(); 401 | update(); 402 | } 403 | 404 | private View getScrollableParentView() { 405 | View view = this; 406 | while (view.getParent() != null && view.getParent() instanceof View) { 407 | view = (View) view.getParent(); 408 | if (view instanceof ScrollView || view instanceof RecyclerView || view instanceof NestedScrollView) { 409 | return view; 410 | } 411 | } 412 | return null; 413 | } 414 | 415 | @Override 416 | public boolean onTouchEvent(MotionEvent event) { 417 | return handleTouch(event); 418 | } 419 | 420 | boolean handleTouch(MotionEvent event) { 421 | if (isEditing) { 422 | return false; 423 | } 424 | boolean handledByDetector = this.detector.onTouchEvent(event); 425 | if (!handledByDetector) { 426 | 427 | final int action = MotionEventCompat.getActionMasked(event); 428 | switch (action) { 429 | case ACTION_UP: 430 | case MotionEvent.ACTION_CANCEL: 431 | if (parentScroll != null) { 432 | parentScroll.requestDisallowInterceptTouchEvent(false); 433 | } 434 | actionUp(); 435 | moving = false; 436 | break; 437 | case MotionEvent.ACTION_DOWN: 438 | final float evY = event.getY(); 439 | if (evY <= barY || evY >= (barY + barWidth)) { 440 | return true; 441 | } else { 442 | moving = true; 443 | } 444 | if (parentScroll != null) { 445 | parentScroll.requestDisallowInterceptTouchEvent(true); 446 | } 447 | case MotionEvent.ACTION_MOVE: { 448 | if (moving) { 449 | float evX = event.getX(); 450 | 451 | evX = evX - settings.paddingCorners; 452 | if (evX < 0) { 453 | evX = 0; 454 | } 455 | if (evX > barWidth) { 456 | evX = barWidth; 457 | } 458 | this.indicatorX = evX; 459 | 460 | update(); 461 | } 462 | } 463 | break; 464 | } 465 | } 466 | 467 | return true; 468 | } 469 | 470 | void actionUp() { 471 | 472 | } 473 | 474 | public void update() { 475 | if (barWidth > 0f) { 476 | float currentPercent = indicatorX / barWidth; 477 | currentValue = currentPercent * (max - min) + min; 478 | currentValue = Math.round(currentValue); 479 | 480 | if (listener != null && oldValue != currentValue) { 481 | oldValue = currentValue; 482 | listener.valueChanged(Slidr.this, currentValue); 483 | } else { 484 | 485 | } 486 | 487 | updateBubbleWidth(); 488 | editBubbleEditPosition(); 489 | } 490 | postInvalidate(); 491 | } 492 | 493 | @Override 494 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 495 | super.onSizeChanged(w, h, oldw, oldh); 496 | updateValues(); 497 | } 498 | 499 | @Override 500 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 501 | updateValues(); 502 | super.onMeasure(widthMeasureSpec, 503 | MeasureSpec.makeMeasureSpec(calculatedHieght, MeasureSpec.EXACTLY)); 504 | } 505 | 506 | private void updateBubbleWidth() { 507 | this.bubble.width = calculateBubbleTextWidth() + dpToPx(BUBBLE_PADDING_HORIZONTAL) * 2f; 508 | this.bubble.width = Math.max(150, this.bubble.width); 509 | } 510 | 511 | private boolean isRegions() { 512 | return settings.modeRegion || steps.isEmpty(); 513 | } 514 | 515 | private void updateValues() { 516 | 517 | if (currentValue < min) { 518 | currentValue = min; 519 | } 520 | 521 | settings.paddingCorners = settings.barHeight; 522 | 523 | barWidth = getWidth() - this.settings.paddingCorners * 2; 524 | 525 | if (settings.drawBubble) { 526 | updateBubbleWidth(); 527 | this.bubble.height = dpToPx(settings.textSizeBubbleCurrent) + dpToPx(BUBBLE_PADDING_VERTICAL) * 2f + dpToPx(BUBBLE_ARROW_HEIGHT); 528 | } else { 529 | this.bubble.height = 0; 530 | } 531 | 532 | this.barY = 0; 533 | if (settings.drawTextOnTop) { 534 | barY += DISTANCE_TEXT_BAR * 2; 535 | if (isRegions()) { 536 | float topTextHeight = 0; 537 | final String tmpTextLeft = formatRegionValue(0, 0); 538 | final String tmpTextRight = formatRegionValue(1, 0); 539 | topTextHeight = Math.max(topTextHeight, calculateTextMultilineHeight(tmpTextLeft, settings.paintTextTop)); 540 | topTextHeight = Math.max(topTextHeight, calculateTextMultilineHeight(tmpTextRight, settings.paintTextTop)); 541 | 542 | this.barY += topTextHeight + 3; 543 | } else { 544 | float topTextHeight = 0; 545 | 546 | for (Step step : steps) { 547 | topTextHeight = Math.max( 548 | topTextHeight, 549 | calculateTextMultilineHeight(formatValue(step.value), settings.paintTextBottom) 550 | ); 551 | } 552 | this.barY += topTextHeight; 553 | } 554 | } else { 555 | if (settings.drawBubble) { 556 | this.barY -= dpToPx(BUBBLE_ARROW_HEIGHT) / 1.5f; 557 | } 558 | } 559 | 560 | this.barY += bubble.height; 561 | 562 | this.barCenterY = barY + settings.barHeight / 2f; 563 | 564 | if (settings.indicatorInside) { 565 | this.indicatorRadius = (int) (settings.barHeight * .5f); 566 | } else { 567 | this.indicatorRadius = (int) (settings.barHeight * .9f); 568 | } 569 | 570 | for (Step step : steps) { 571 | final float stoppoverPercent = step.value / (max - min); 572 | step.xStart = stoppoverPercent * barWidth; 573 | } 574 | 575 | indicatorX = (currentValue - min) / (max - min) * barWidth; 576 | 577 | calculatedHieght = (int) (barCenterY + indicatorRadius); 578 | 579 | float bottomTextHeight = 0; 580 | if (!TextUtils.isEmpty(textMax)) { 581 | bottomTextHeight = Math.max( 582 | calculateTextMultilineHeight(textMax, settings.paintTextBottom), 583 | calculateTextMultilineHeight(textMin, settings.paintTextBottom) 584 | ); 585 | } 586 | for (Step step : steps) { 587 | bottomTextHeight = Math.max( 588 | bottomTextHeight, 589 | calculateTextMultilineHeight(step.name, settings.paintTextBottom) 590 | ); 591 | } 592 | 593 | calculatedHieght += bottomTextHeight; 594 | 595 | calculatedHieght += 10; //padding bottom 596 | 597 | } 598 | 599 | private Step findStepBeforeCustor() { 600 | for (int i = steps.size() - 1; i >= 0; i--) { 601 | final Step step = steps.get(i); 602 | if ((currentValue - min) >= step.value) { 603 | return step; 604 | } 605 | break; 606 | } 607 | return null; 608 | } 609 | 610 | private Step findStepOfCustor() { 611 | for (int i = 0; i < steps.size(); ++i) { 612 | final Step step = steps.get(i); 613 | if ((currentValue - min) <= step.value) { 614 | return step; 615 | } 616 | } 617 | return null; 618 | } 619 | 620 | public void setTextMax(String textMax) { 621 | this.textMax = textMax; 622 | postInvalidate(); 623 | } 624 | 625 | public void setTextMin(String textMin) { 626 | this.textMin = textMin; 627 | postInvalidate(); 628 | } 629 | 630 | @Override 631 | protected void onDraw(Canvas canvas) { 632 | super.onDraw(canvas); 633 | 634 | canvas.save(); 635 | { 636 | 637 | final float paddingLeft = settings.paddingCorners; 638 | final float paddingRight = settings.paddingCorners; 639 | 640 | 641 | if (isRegions()) { 642 | if (steps.isEmpty()) { 643 | settings.paintIndicator.setColor(settings.regionColorLeft); 644 | settings.paintBubble.setColor(settings.regionColorLeft); 645 | } else { 646 | settings.paintIndicator.setColor(settings.regionColorRight); 647 | settings.paintBubble.setColor(settings.regionColorRight); 648 | } 649 | } else { 650 | final Step stepBeforeCustor = findStepOfCustor(); 651 | if (stepBeforeCustor != null) { 652 | settings.paintIndicator.setColor(stepBeforeCustor.colorBefore); 653 | settings.paintBubble.setColor(stepBeforeCustor.colorBefore); 654 | } else { 655 | if (settings.step_colorizeAfterLast) { 656 | final Step beforeCustor = findStepBeforeCustor(); 657 | if (beforeCustor != null) { 658 | settings.paintIndicator.setColor(beforeCustor.colorAfter); 659 | settings.paintBubble.setColor(beforeCustor.colorAfter); 660 | } 661 | } else { 662 | settings.paintIndicator.setColor(settings.colorBackground); 663 | settings.paintBubble.setColor(settings.colorBackground); 664 | } 665 | } 666 | } 667 | 668 | final float radiusCorner = settings.barHeight / 2f; 669 | 670 | final float indicatorCenterX = indicatorX + paddingLeft; 671 | 672 | { //background 673 | final float centerCircleLeft = paddingLeft; 674 | final float centerCircleRight = getWidth() - paddingRight; 675 | 676 | //grey background 677 | if (isRegions()) { 678 | if (steps.isEmpty()) { 679 | settings.paintBar.setColor(settings.colorBackground); 680 | } else { 681 | settings.paintBar.setColor(settings.regionColorRight); 682 | } 683 | } else { 684 | settings.paintBar.setColor(settings.colorBackground); 685 | } 686 | canvas.drawCircle(centerCircleLeft, barCenterY, radiusCorner, settings.paintBar); 687 | canvas.drawCircle(centerCircleRight, barCenterY, radiusCorner, settings.paintBar); 688 | canvas.drawRect(centerCircleLeft, barY, centerCircleRight, barY + settings.barHeight, settings.paintBar); 689 | 690 | if (isRegions()) { 691 | settings.paintBar.setColor(settings.regionColorLeft); 692 | 693 | canvas.drawCircle(centerCircleLeft, barCenterY, radiusCorner, settings.paintBar); 694 | canvas.drawRect(centerCircleLeft, barY, indicatorCenterX, barY + settings.barHeight, settings.paintBar); 695 | } else { 696 | float lastX = centerCircleLeft; 697 | boolean first = true; 698 | for (Step step : steps) { 699 | settings.paintBar.setColor(step.colorBefore); 700 | if (first) { 701 | canvas.drawCircle(centerCircleLeft, barCenterY, radiusCorner, settings.paintBar); 702 | } 703 | 704 | final float x = step.xStart + paddingLeft; 705 | if (!settings.step_colorizeOnlyBeforeIndicator) { 706 | canvas.drawRect(lastX, barY, x, barY + settings.barHeight, settings.paintBar); 707 | } else { 708 | canvas.drawRect(lastX, barY, Math.min(x, indicatorCenterX), barY + settings.barHeight, settings.paintBar); 709 | } 710 | lastX = x; 711 | 712 | first = false; 713 | } 714 | 715 | 716 | if (settings.step_colorizeAfterLast) { 717 | //find the step just below currentValue 718 | for (int i = steps.size() - 1; i >= 0; i--) { 719 | final Step step = steps.get(i); 720 | if ((currentValue - min) > step.value) { 721 | settings.paintBar.setColor(step.colorAfter); 722 | canvas.drawRect(step.xStart + paddingLeft, barY, indicatorCenterX, barY + settings.barHeight, settings.paintBar); 723 | break; 724 | } 725 | } 726 | } 727 | } 728 | } 729 | 730 | 731 | { //texts top (values) 732 | if (settings.drawTextOnTop) { 733 | final float textY = barY - dpToPx(DISTANCE_TEXT_BAR); 734 | if (isRegions()) { 735 | float leftValue; 736 | float rightValue; 737 | 738 | if (settings.regions_centerText) { 739 | leftValue = currentValue; 740 | rightValue = max - leftValue; 741 | } else { 742 | leftValue = min; 743 | rightValue = max; 744 | } 745 | 746 | if (settings.regions_textFollowRegionColor) { 747 | settings.paintTextTop.setColor(settings.regionColorLeft); 748 | } 749 | 750 | float textX; 751 | if (settings.regions_centerText) { 752 | textX = (indicatorCenterX - paddingLeft) / 2f + paddingLeft; 753 | } else { 754 | textX = paddingLeft; 755 | } 756 | 757 | drawIndicatorsTextAbove(canvas, formatRegionValue(0, leftValue), settings.paintTextTop, textX, textY, Layout.Alignment.ALIGN_CENTER); 758 | 759 | if (settings.regions_textFollowRegionColor) { 760 | settings.paintTextTop.setColor(settings.regionColorRight); 761 | } 762 | 763 | if (settings.regions_centerText) { 764 | textX = indicatorCenterX + (barWidth - indicatorCenterX - paddingLeft) / 2f + paddingLeft; 765 | } else { 766 | textX = paddingLeft + barWidth; 767 | } 768 | drawIndicatorsTextAbove(canvas, formatRegionValue(1, rightValue), settings.paintTextTop, textX, textY, Layout.Alignment.ALIGN_CENTER); 769 | } else { 770 | drawIndicatorsTextAbove(canvas, formatValue(min), settings.paintTextTop, 0 + paddingLeft, textY, Layout.Alignment.ALIGN_CENTER); 771 | for (Step step : steps) { 772 | drawIndicatorsTextAbove(canvas, formatValue(step.value), settings.paintTextTop, step.xStart + paddingLeft, textY, Layout.Alignment.ALIGN_CENTER); 773 | } 774 | drawIndicatorsTextAbove(canvas, formatValue(max), settings.paintTextTop, canvas.getWidth(), textY, Layout.Alignment.ALIGN_CENTER); 775 | } 776 | } 777 | } 778 | 779 | 780 | { //steps + bottom text 781 | final float bottomTextY = barY + settings.barHeight + 15; 782 | 783 | for (Step step : steps) { 784 | if (settings.step_drawLines) { 785 | canvas.drawLine(step.xStart + paddingLeft, barY - settings.barHeight / 4f, step.xStart + paddingLeft, barY + settings.barHeight + settings.barHeight / 4f, settings.paintStep); 786 | } 787 | 788 | if (settings.drawTextOnBottom) { 789 | //drawMultilineText(canvas, maxText, canvas.getWidth() - settings.paintText.measureText(maxText), textY, settings.paintText, Layout.Alignment.ALIGN_OPPOSITE); 790 | drawMultilineText(canvas, step.name, step.xStart + paddingLeft, bottomTextY, settings.paintTextBottom, Layout.Alignment.ALIGN_CENTER); 791 | } 792 | } 793 | 794 | if (settings.drawTextOnBottom) { 795 | if (!TextUtils.isEmpty(textMax)) { 796 | drawMultilineText(canvas, textMax, canvas.getWidth(), bottomTextY, settings.paintTextBottom, Layout.Alignment.ALIGN_CENTER); 797 | } 798 | 799 | if (!TextUtils.isEmpty(textMin)) { 800 | drawMultilineText(canvas, textMin, 0, bottomTextY, settings.paintTextBottom, Layout.Alignment.ALIGN_CENTER); 801 | } 802 | } 803 | } 804 | 805 | //indicator 806 | { 807 | final int color = settings.paintIndicator.getColor(); 808 | canvas.drawCircle(indicatorCenterX, this.barCenterY, indicatorRadius, settings.paintIndicator); 809 | settings.paintIndicator.setColor(Color.WHITE); 810 | canvas.drawCircle(indicatorCenterX, this.barCenterY, indicatorRadius * 0.85f, settings.paintIndicator); 811 | settings.paintIndicator.setColor(color); 812 | } 813 | 814 | //bubble 815 | { 816 | if (settings.drawBubble) { 817 | float bubbleCenterX = indicatorCenterX; 818 | float trangleCenterX; 819 | 820 | bubble.x = bubbleCenterX - bubble.width / 2f; 821 | bubble.y = 0; 822 | 823 | if (bubbleCenterX > canvas.getWidth() - bubble.width / 2f) { 824 | bubbleCenterX = canvas.getWidth() - bubble.width / 2f; 825 | } else if (bubbleCenterX - bubble.width / 2f < 0) { 826 | bubbleCenterX = bubble.width / 2f; 827 | } 828 | 829 | trangleCenterX = (bubbleCenterX + indicatorCenterX) / 2f; 830 | 831 | drawBubble(canvas, bubbleCenterX, trangleCenterX, 0); 832 | } 833 | } 834 | } 835 | 836 | canvas.restore(); 837 | } 838 | 839 | private String formatValue(float value) { 840 | return textFormatter.format(value); 841 | } 842 | 843 | private String formatRegionValue(int region, float value) { 844 | if (regionTextFormatter != null) { 845 | return regionTextFormatter.format(region, value); 846 | } else { 847 | return formatValue(value); 848 | } 849 | } 850 | 851 | private void drawText(Canvas canvas, String text, float x, float y, TextPaint paint, Layout.Alignment aligment) { 852 | canvas.save(); 853 | { 854 | canvas.translate(x, y); 855 | final StaticLayout staticLayout = new StaticLayout(text, paint, (int) paint.measureText(text), aligment, 1.0f, 0, false); 856 | staticLayout.draw(canvas); 857 | } 858 | canvas.restore(); 859 | } 860 | 861 | private void drawMultilineText(Canvas canvas, String text, float x, float y, TextPaint paint, Layout.Alignment aligment) { 862 | final float lineHeight = paint.getTextSize(); 863 | float lineY = y; 864 | for (CharSequence line : text.split("\n")) { 865 | canvas.save(); 866 | { 867 | final float lineWidth = (int) paint.measureText(line.toString()); 868 | float lineX = x; 869 | if (aligment == Layout.Alignment.ALIGN_CENTER) { 870 | lineX -= lineWidth / 2f; 871 | } 872 | if (lineX < 0) { 873 | lineX = 0; 874 | } 875 | 876 | final float right = lineX + lineWidth; 877 | if (right > canvas.getWidth()) { 878 | lineX = canvas.getWidth() - lineWidth - settings.paddingCorners; 879 | } 880 | 881 | canvas.translate(lineX, lineY); 882 | final StaticLayout staticLayout = new StaticLayout(line, paint, (int) lineWidth, aligment, 1.0f, 0, false); 883 | staticLayout.draw(canvas); 884 | 885 | lineY += lineHeight; 886 | } 887 | canvas.restore(); 888 | } 889 | 890 | } 891 | 892 | private void drawIndicatorsTextAbove(Canvas canvas, String text, TextPaint paintText, float x, float y, Layout.Alignment alignment) { 893 | 894 | final float textHeight = calculateTextMultilineHeight(text, paintText); 895 | y -= textHeight; 896 | 897 | final int width = (int) paintText.measureText(text); 898 | if (x >= getWidth() - settings.paddingCorners) { 899 | x = (getWidth() - width - settings.paddingCorners / 2f); 900 | } else if (x <= 0) { 901 | x = width / 2f; 902 | } else { 903 | x = (x - width / 2f); 904 | } 905 | 906 | if (x < 0) { 907 | x = 0; 908 | } 909 | 910 | if (x + width > getWidth()) { 911 | x = getWidth() - width; 912 | } 913 | 914 | drawText(canvas, text, x, y, paintText, alignment); 915 | } 916 | 917 | private float calculateTextMultilineHeight(String text, TextPaint textPaint) { 918 | return text.split("\n").length * textPaint.getTextSize(); 919 | } 920 | 921 | /* 922 | private float calculateTextMultilineWidth(String text, TextPaint textPaint) { 923 | int maxLength = -1; 924 | CharSequence max = null; 925 | for (CharSequence line : text.split("\n")) { 926 | final int lineLength = line.length(); 927 | if (lineLength > maxLength) { 928 | maxLength = lineLength; 929 | max = line; 930 | } 931 | } 932 | return textPaint.measureText(max.toString()); 933 | } 934 | */ 935 | 936 | private float calculateBubbleTextWidth() { 937 | String bubbleText = formatValue(getCurrentValue()); 938 | if (isEditing) { 939 | bubbleText = textEditing; 940 | } 941 | return settings.paintBubbleTextCurrent.measureText(bubbleText); 942 | } 943 | 944 | private void drawBubblePath(Canvas canvas, float triangleCenterX, float height, float width) { 945 | final Path path = new Path(); 946 | 947 | int padding = 3; 948 | final Rect rect = new Rect(padding, padding, (int) width - padding, (int) (height - dpToPx(BUBBLE_ARROW_HEIGHT)) - padding); 949 | 950 | final float roundRectHeight = (height - dpToPx(BUBBLE_ARROW_HEIGHT)) / 2; 951 | 952 | path.moveTo(rect.left + roundRectHeight, rect.top); 953 | path.lineTo(rect.right - roundRectHeight, rect.top); 954 | path.quadTo(rect.right, rect.top, rect.right, rect.top + roundRectHeight); 955 | path.lineTo(rect.right, rect.bottom - roundRectHeight); 956 | path.quadTo(rect.right, rect.bottom, rect.right - roundRectHeight, rect.bottom); 957 | 958 | path.lineTo(triangleCenterX + dpToPx(BUBBLE_ARROW_WIDTH) / 2f, height - dpToPx(BUBBLE_ARROW_HEIGHT) - padding); 959 | path.lineTo(triangleCenterX, height - padding); 960 | path.lineTo(triangleCenterX - dpToPx(BUBBLE_ARROW_WIDTH) / 2f, height - dpToPx(BUBBLE_ARROW_HEIGHT) - padding); 961 | 962 | path.lineTo(rect.left + roundRectHeight, rect.bottom); 963 | path.quadTo(rect.left, rect.bottom, rect.left, rect.bottom - roundRectHeight); 964 | path.lineTo(rect.left, rect.top + roundRectHeight); 965 | path.quadTo(rect.left, rect.top, rect.left + roundRectHeight, rect.top); 966 | path.close(); 967 | 968 | canvas.drawPath(path, settings.paintBubble); 969 | } 970 | 971 | private void drawBubble(Canvas canvas, float centerX, float triangleCenterX, float y) { 972 | final float width = this.bubble.width; 973 | final float height = this.bubble.height; 974 | 975 | canvas.save(); 976 | { 977 | canvas.translate(centerX - width / 2f, y); 978 | triangleCenterX -= (centerX - width / 2f); 979 | 980 | if (!isEditing) { 981 | drawBubblePath(canvas, triangleCenterX, height, width); 982 | } else { 983 | final int savedColor = settings.paintBubble.getColor(); 984 | 985 | settings.paintBubble.setColor(settings.bubbleColorEditing); 986 | settings.paintBubble.setStyle(Paint.Style.FILL); 987 | drawBubblePath(canvas, triangleCenterX, height, width); 988 | 989 | settings.paintBubble.setStyle(Paint.Style.STROKE); 990 | settings.paintBubble.setColor(settings.paintIndicator.getColor()); 991 | drawBubblePath(canvas, triangleCenterX, height, width); 992 | 993 | settings.paintBubble.setStyle(Paint.Style.FILL); 994 | settings.paintBubble.setColor(savedColor); 995 | } 996 | 997 | if (!isEditing) { 998 | final String bubbleText = formatValue(getCurrentValue()); 999 | drawText(canvas, bubbleText, dpToPx(BUBBLE_PADDING_HORIZONTAL), dpToPx(BUBBLE_PADDING_VERTICAL) - 3, settings.paintBubbleTextCurrent, Layout.Alignment.ALIGN_NORMAL); 1000 | } 1001 | } 1002 | 1003 | canvas.restore(); 1004 | 1005 | } 1006 | 1007 | public void setTextFormatter(TextFormatter textFormatter) { 1008 | this.textFormatter = textFormatter; 1009 | update(); 1010 | } 1011 | 1012 | public void setRegionTextFormatter(RegionTextFormatter regionTextFormatter) { 1013 | this.regionTextFormatter = regionTextFormatter; 1014 | update(); 1015 | } 1016 | 1017 | public interface EditListener { 1018 | void onEditStarted(EditText editText); 1019 | } 1020 | 1021 | public interface Listener { 1022 | void valueChanged(Slidr slidr, float currentValue); 1023 | } 1024 | 1025 | public interface BubbleClickedListener { 1026 | void bubbleClicked(Slidr slidr); 1027 | } 1028 | 1029 | public interface TextFormatter { 1030 | String format(float value); 1031 | } 1032 | 1033 | public interface RegionTextFormatter { 1034 | String format(int region, float value); 1035 | } 1036 | 1037 | public static class Step implements Comparable { 1038 | private String name; 1039 | private float value; 1040 | 1041 | private float xStart; 1042 | private int colorBefore; 1043 | private int colorAfter = Color.parseColor("#ed5564"); 1044 | 1045 | public Step(String name, float value, int colorBefore) { 1046 | this.name = name; 1047 | this.value = value; 1048 | this.colorBefore = colorBefore; 1049 | } 1050 | 1051 | public Step(String name, float value, int colorBefore, int colorAfter) { 1052 | this(name, value, colorBefore); 1053 | this.colorAfter = colorAfter; 1054 | } 1055 | 1056 | @Override 1057 | public int compareTo(@NonNull Step o) { 1058 | return Float.compare(value, o.value); 1059 | } 1060 | } 1061 | 1062 | public static class Settings { 1063 | private Slidr slidr; 1064 | private Paint paintBar; 1065 | private Paint paintIndicator; 1066 | private Paint paintStep; 1067 | private TextPaint paintTextTop; 1068 | private TextPaint paintTextBottom; 1069 | private TextPaint paintBubbleTextCurrent; 1070 | private Paint paintBubble; 1071 | private int colorBackground = Color.parseColor("#cccccc"); 1072 | private int colorStoppover = Color.BLACK; 1073 | private int textColor = Color.parseColor("#6E6E6E"); 1074 | private int textTopSize = 12; 1075 | private int textBottomSize = 12; 1076 | private int textSizeBubbleCurrent = 16; 1077 | private float barHeight = 15; 1078 | private float paddingCorners; 1079 | private boolean step_colorizeAfterLast = false; 1080 | private boolean step_drawLines = true; 1081 | private boolean step_colorizeOnlyBeforeIndicator = true; 1082 | private boolean drawTextOnTop = true; 1083 | private boolean drawTextOnBottom = true; 1084 | private boolean drawBubble = true; 1085 | private boolean modeRegion = false; 1086 | private boolean indicatorInside = false; 1087 | private boolean regions_textFollowRegionColor = false; 1088 | private boolean regions_centerText = true; 1089 | private int regionColorLeft = Color.parseColor("#007E90"); 1090 | private int regionColorRight = Color.parseColor("#ed5564"); 1091 | private boolean editOnBubbleClick = true; 1092 | private int bubbleColorEditing = Color.WHITE; 1093 | 1094 | public Settings(Slidr slidr) { 1095 | this.slidr = slidr; 1096 | 1097 | paintIndicator = new Paint(); 1098 | paintIndicator.setAntiAlias(true); 1099 | paintIndicator.setStrokeWidth(2); 1100 | 1101 | paintBar = new Paint(); 1102 | paintBar.setAntiAlias(true); 1103 | paintBar.setStrokeWidth(2); 1104 | paintBar.setColor(colorBackground); 1105 | 1106 | paintStep = new Paint(); 1107 | paintStep.setAntiAlias(true); 1108 | paintStep.setStrokeWidth(5); 1109 | paintStep.setColor(colorStoppover); 1110 | 1111 | paintTextTop = new TextPaint(); 1112 | paintTextTop.setAntiAlias(true); 1113 | paintTextTop.setStyle(Paint.Style.FILL); 1114 | paintTextTop.setColor(textColor); 1115 | paintTextTop.setTextSize(textTopSize); 1116 | 1117 | paintTextBottom = new TextPaint(); 1118 | paintTextBottom.setAntiAlias(true); 1119 | paintTextBottom.setStyle(Paint.Style.FILL); 1120 | paintTextBottom.setColor(textColor); 1121 | paintTextBottom.setTextSize(textBottomSize); 1122 | 1123 | paintBubbleTextCurrent = new TextPaint(); 1124 | paintBubbleTextCurrent.setAntiAlias(true); 1125 | paintBubbleTextCurrent.setStyle(Paint.Style.FILL); 1126 | paintBubbleTextCurrent.setColor(Color.WHITE); 1127 | paintBubbleTextCurrent.setStrokeWidth(2); 1128 | paintBubbleTextCurrent.setTextSize(dpToPx(textSizeBubbleCurrent)); 1129 | 1130 | paintBubble = new Paint(); 1131 | paintBubble.setAntiAlias(true); 1132 | paintBubble.setStrokeWidth(3); 1133 | } 1134 | 1135 | private void init(Context context, AttributeSet attrs) { 1136 | if (attrs != null) { 1137 | final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Slidr); 1138 | setColorBackground(a.getColor(R.styleable.Slidr_slidr_backgroundColor, colorBackground)); 1139 | 1140 | this.step_colorizeAfterLast = a.getBoolean(R.styleable.Slidr_slidr_step_colorizeAfterLast, step_colorizeAfterLast); 1141 | this.step_drawLines = a.getBoolean(R.styleable.Slidr_slidr_step_drawLine, step_drawLines); 1142 | this.step_colorizeOnlyBeforeIndicator = a.getBoolean(R.styleable.Slidr_slidr_step_colorizeOnlyBeforeIndicator, step_colorizeOnlyBeforeIndicator); 1143 | 1144 | this.drawTextOnTop = a.getBoolean(R.styleable.Slidr_slidr_textTop_visible, drawTextOnTop); 1145 | setTextTopSize(a.getDimensionPixelSize(R.styleable.Slidr_slidr_textTop_size, (int) dpToPx(textTopSize))); 1146 | this.drawTextOnBottom = a.getBoolean(R.styleable.Slidr_slidr_textBottom_visible, drawTextOnBottom); 1147 | setTextBottomSize(a.getDimensionPixelSize(R.styleable.Slidr_slidr_textBottom_size, (int) dpToPx(textBottomSize))); 1148 | 1149 | this.barHeight = a.getDimensionPixelOffset(R.styleable.Slidr_slidr_barHeight, (int) dpToPx(barHeight)); 1150 | this.drawBubble = a.getBoolean(R.styleable.Slidr_slidr_draw_bubble, drawBubble); 1151 | this.modeRegion = a.getBoolean(R.styleable.Slidr_slidr_regions, modeRegion); 1152 | 1153 | this.regionColorLeft = a.getColor(R.styleable.Slidr_slidr_region_leftColor, regionColorLeft); 1154 | this.regionColorRight = a.getColor(R.styleable.Slidr_slidr_region_rightColor, regionColorRight); 1155 | 1156 | this.indicatorInside = a.getBoolean(R.styleable.Slidr_slidr_indicator_inside, indicatorInside); 1157 | this.regions_textFollowRegionColor = a.getBoolean(R.styleable.Slidr_slidr_regions_textFollowRegionColor, regions_textFollowRegionColor); 1158 | this.regions_centerText = a.getBoolean(R.styleable.Slidr_slidr_regions_centerText, regions_centerText); 1159 | 1160 | this.editOnBubbleClick = a.getBoolean(R.styleable.Slidr_slidr_edditable, editOnBubbleClick); 1161 | 1162 | a.recycle(); 1163 | } 1164 | } 1165 | 1166 | public void setStep_colorizeAfterLast(boolean step_colorizeAfterLast) { 1167 | this.step_colorizeAfterLast = step_colorizeAfterLast; 1168 | slidr.update(); 1169 | } 1170 | 1171 | public void setDrawTextOnTop(boolean drawTextOnTop) { 1172 | this.drawTextOnTop = drawTextOnTop; 1173 | slidr.update(); 1174 | } 1175 | 1176 | public void setDrawTextOnBottom(boolean drawTextOnBottom) { 1177 | this.drawTextOnBottom = drawTextOnBottom; 1178 | slidr.update(); 1179 | } 1180 | 1181 | public void setDrawBubble(boolean drawBubble) { 1182 | this.drawBubble = drawBubble; 1183 | slidr.update(); 1184 | } 1185 | 1186 | public void setModeRegion(boolean modeRegion) { 1187 | this.modeRegion = modeRegion; 1188 | slidr.update(); 1189 | } 1190 | 1191 | public void setRegionColorLeft(int regionColorLeft) { 1192 | this.regionColorLeft = regionColorLeft; 1193 | slidr.update(); 1194 | } 1195 | 1196 | public void setRegionColorRight(int regionColorRight) { 1197 | this.regionColorRight = regionColorRight; 1198 | slidr.update(); 1199 | } 1200 | 1201 | public void setColorBackground(int colorBackground) { 1202 | this.colorBackground = colorBackground; 1203 | slidr.update(); 1204 | } 1205 | 1206 | public void setTextTopSize(int textSize) { 1207 | this.textTopSize = textSize; 1208 | this.paintTextTop.setTextSize(textSize); 1209 | slidr.update(); 1210 | } 1211 | 1212 | public void setTextBottomSize(int textSize) { 1213 | this.textBottomSize = textSize; 1214 | this.paintTextBottom.setTextSize(textSize); 1215 | slidr.update(); 1216 | } 1217 | 1218 | private float dpToPx(int size) { 1219 | return size * slidr.getResources().getDisplayMetrics().density; 1220 | } 1221 | 1222 | private float dpToPx(float size) { 1223 | return size * slidr.getResources().getDisplayMetrics().density; 1224 | } 1225 | 1226 | } 1227 | 1228 | private static class TouchView extends FrameLayout { 1229 | 1230 | private final Rect viewRect; 1231 | private Callback callback; 1232 | 1233 | public TouchView(Context context, Rect viewRect) { 1234 | super(context); 1235 | this.viewRect = viewRect; 1236 | this.setBackgroundColor(Color.TRANSPARENT); 1237 | } 1238 | 1239 | public void setCallback(Callback callback) { 1240 | this.callback = callback; 1241 | } 1242 | 1243 | @Override 1244 | public boolean onTouchEvent(MotionEvent event) { 1245 | float x = event.getX(); 1246 | float y = event.getY(); 1247 | 1248 | if (x >= viewRect.left 1249 | && x <= viewRect.right 1250 | && y >= viewRect.top 1251 | && y <= viewRect.bottom) { 1252 | return false; 1253 | //return compteurChampEditable.onTouchEvent(event); 1254 | } else if (event.getAction() == ACTION_UP) { 1255 | //((ViewGroup) getParent()).removeView(this); 1256 | if (callback != null) { 1257 | callback.onClicked(); 1258 | } 1259 | } 1260 | return true; 1261 | } 1262 | 1263 | public interface Callback { 1264 | void onClicked(); 1265 | } 1266 | } 1267 | 1268 | private class Bubble { 1269 | private float height; 1270 | private float width; 1271 | private float x; 1272 | private float y; 1273 | 1274 | public boolean clicked(MotionEvent e) { 1275 | return e.getX() >= x && e.getX() <= x + width 1276 | && e.getY() >= y && e.getY() < y + height; 1277 | } 1278 | 1279 | public float getHeight() { 1280 | return height - dpToPx(BUBBLE_ARROW_HEIGHT); 1281 | } 1282 | 1283 | public float getX() { 1284 | return Math.max(x, 0); 1285 | } 1286 | 1287 | public float getY() { 1288 | return Math.max(y, 0); 1289 | } 1290 | } 1291 | 1292 | public class EurosTextFormatter implements TextFormatter { 1293 | 1294 | @Override 1295 | public String format(float value) { 1296 | return String.format("%d €", (int) value); 1297 | } 1298 | } 1299 | } 1300 | -------------------------------------------------------------------------------- /android-slidr/src/main/java/com/github/florent37/androidslidr/Sushi.java: -------------------------------------------------------------------------------- 1 | package com.github.florent37.androidslidr; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.Path; 9 | import android.graphics.Rect; 10 | import android.support.annotation.Nullable; 11 | import android.text.Layout; 12 | import android.text.StaticLayout; 13 | import android.text.TextPaint; 14 | import android.util.AttributeSet; 15 | import android.view.MotionEvent; 16 | import android.widget.FrameLayout; 17 | 18 | /** 19 | * Created by florentchampigny on 20/04/2017. 20 | */ 21 | 22 | public class Sushi extends FrameLayout { 23 | 24 | private static final float DISTANCE_TEXT_BAR = 35; 25 | private static final float BUBBLE_PADDING_HORIZONTAL = 15; 26 | private static final float BUBBLE_PADDING_VERTICAL = 3; 27 | private static final float BUBBLE_MIN_WITH = 0; 28 | 29 | private Settings settings; 30 | 31 | private float max = 1000; 32 | private float min = 0; 33 | private float currentValue = 0; 34 | 35 | private float barY; 36 | private float barWidth; 37 | private float indicatorX; 38 | private float barCenterY; 39 | private Bubble bubble = new Bubble(); 40 | private TextFormatter textFormatter = new EurosTextFormatter(); 41 | private RegionTextFormatter regionTextFormatter = null; 42 | 43 | private int calculatedHieght = 0; 44 | 45 | public Sushi(Context context) { 46 | this(context, null); 47 | } 48 | 49 | public Sushi(Context context, @Nullable AttributeSet attrs) { 50 | this(context, attrs, 0); 51 | } 52 | 53 | public Sushi(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 54 | super(context, attrs, defStyleAttr); 55 | 56 | init(context, attrs); 57 | } 58 | 59 | private void init(Context context, @Nullable AttributeSet attrs) { 60 | setWillNotDraw(false); 61 | 62 | this.settings = new Settings(this); 63 | this.settings.init(context, attrs); 64 | } 65 | 66 | //region getters 67 | 68 | private float dpToPx(int size) { 69 | return size * getResources().getDisplayMetrics().density; 70 | } 71 | 72 | private float pxToDp(int size) { 73 | return size / getResources().getDisplayMetrics().density; 74 | } 75 | 76 | public float getMax() { 77 | return max; 78 | } 79 | 80 | public void setMax(float max) { 81 | this.max = max; 82 | updateValues(); 83 | update(); 84 | } 85 | 86 | public void setMin(float min) { 87 | this.min = min; 88 | updateValues(); 89 | update(); 90 | } 91 | 92 | public float getCurrentValue() { 93 | return currentValue; 94 | } 95 | 96 | public void setCurrentValue(float value) { 97 | this.currentValue = value; 98 | updateValues(); 99 | update(); 100 | } 101 | 102 | //endregion 103 | 104 | public void update() { 105 | if (barWidth > 0f) { 106 | float currentPercent = indicatorX / barWidth; 107 | currentValue = currentPercent * (max - min) + min; 108 | 109 | updateBubbleWidth(); 110 | } 111 | postInvalidate(); 112 | } 113 | 114 | @Override 115 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 116 | super.onSizeChanged(w, h, oldw, oldh); 117 | updateValues(); 118 | } 119 | 120 | @Override 121 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 122 | updateValues(); 123 | super.onMeasure(widthMeasureSpec, 124 | MeasureSpec.makeMeasureSpec(calculatedHieght, MeasureSpec.EXACTLY)); 125 | } 126 | 127 | private void updateBubbleWidth() { 128 | this.bubble.width = calculateBubbleTextWidth() + BUBBLE_PADDING_HORIZONTAL * 2f; 129 | this.bubble.width = Math.max(BUBBLE_MIN_WITH, this.bubble.width); 130 | } 131 | 132 | private void updateValues() { 133 | 134 | if (currentValue < min) { 135 | currentValue = min; 136 | } 137 | 138 | settings.paddingCorners = settings.barHeight; 139 | 140 | barWidth = getWidth() - this.settings.paddingCorners * 2; 141 | 142 | updateBubbleWidth(); 143 | this.bubble.height = dpToPx(settings.textSizeBubble) + BUBBLE_PADDING_VERTICAL * 2f; 144 | 145 | this.barY = 0; 146 | 147 | if(settings.displayMinMax) { 148 | barY += DISTANCE_TEXT_BAR; 149 | float topTextHeight = 0; 150 | final String tmpTextLeft = formatRegionValue(0, 0); 151 | final String tmpTextRight = formatRegionValue(1, 0); 152 | topTextHeight = Math.max(topTextHeight, calculateTextMultilineHeight(tmpTextLeft, settings.paintTextTop)); 153 | topTextHeight = Math.max(topTextHeight, calculateTextMultilineHeight(tmpTextRight, settings.paintTextTop)); 154 | 155 | this.barY += topTextHeight + 3; 156 | } else { 157 | barY = 15; 158 | } 159 | 160 | this.barCenterY = barY + settings.barHeight / 2f; 161 | 162 | this.bubble.y = barCenterY - bubble.height / 2f; 163 | 164 | indicatorX = (currentValue - min) / (max - min) * barWidth; 165 | 166 | calculatedHieght = (int) (barCenterY + settings.barHeight); 167 | 168 | calculatedHieght += 10; //padding bottom 169 | 170 | } 171 | 172 | @Override 173 | protected void onDraw(Canvas canvas) { 174 | super.onDraw(canvas); 175 | 176 | canvas.save(); 177 | { 178 | 179 | final float paddingLeft = settings.paddingCorners; 180 | final float paddingRight = settings.paddingCorners; 181 | 182 | final float radiusCorner = settings.barHeight / 2f; 183 | 184 | final float indicatorCenterX = indicatorX + paddingLeft; 185 | 186 | { //background 187 | final float centerCircleLeft = paddingLeft; 188 | final float centerCircleRight = getWidth() - paddingRight; 189 | 190 | //grey background 191 | settings.paintBar.setColor(settings.colorBackground); 192 | 193 | canvas.drawCircle(centerCircleLeft, barCenterY, radiusCorner, settings.paintBar); 194 | canvas.drawCircle(centerCircleRight, barCenterY, radiusCorner, settings.paintBar); 195 | canvas.drawRect(centerCircleLeft, barY, centerCircleRight, barY + settings.barHeight, settings.paintBar); 196 | 197 | 198 | //color before indicator 199 | settings.paintBar.setColor(settings.foregroundColor); 200 | 201 | canvas.drawCircle(centerCircleLeft, barCenterY, radiusCorner, settings.paintBar); 202 | canvas.drawRect(centerCircleLeft, barY, indicatorCenterX, barY + settings.barHeight, settings.paintBar); 203 | } 204 | 205 | 206 | if (settings.displayMinMax) { //texts top (values) 207 | final float textY = barY - DISTANCE_TEXT_BAR; 208 | drawIndicatorsTextAbove(canvas, formatValue(min), settings.paintTextTop, 0 + paddingLeft, textY, Layout.Alignment.ALIGN_CENTER); 209 | drawIndicatorsTextAbove(canvas, formatValue(max), settings.paintTextTop, canvas.getWidth(), textY, Layout.Alignment.ALIGN_CENTER); 210 | } 211 | 212 | //bubble 213 | { 214 | 215 | float bubbleCenterX = indicatorCenterX; 216 | float trangleCenterX; 217 | 218 | bubble.x = bubbleCenterX - bubble.width / 2f; 219 | 220 | if (bubbleCenterX > canvas.getWidth() - bubble.width / 2f) { 221 | bubbleCenterX = canvas.getWidth() - bubble.width / 2f; 222 | } else if (bubbleCenterX - bubble.width / 2f < 0) { 223 | bubbleCenterX = bubble.width / 2f; 224 | } 225 | 226 | trangleCenterX = (bubbleCenterX + indicatorCenterX) / 2f; 227 | 228 | drawBubble(canvas, bubbleCenterX, trangleCenterX, bubble.getY()); 229 | } 230 | } 231 | 232 | canvas.restore(); 233 | } 234 | 235 | private String formatValue(float value) { 236 | return textFormatter.format(value); 237 | } 238 | 239 | private String formatRegionValue(int region, float value) { 240 | if (regionTextFormatter != null) { 241 | return regionTextFormatter.format(region, value); 242 | } else { 243 | return formatValue(value); 244 | } 245 | } 246 | 247 | private void drawText(Canvas canvas, String text, float x, float y, TextPaint paint, Layout.Alignment aligment) { 248 | canvas.save(); 249 | { 250 | canvas.translate(x, y); 251 | final StaticLayout staticLayout = new StaticLayout(text, paint, (int) paint.measureText(text), aligment, 1.0f, 0, false); 252 | staticLayout.draw(canvas); 253 | } 254 | canvas.restore(); 255 | } 256 | 257 | private void drawMultilineText(Canvas canvas, String text, float x, float y, TextPaint paint, Layout.Alignment aligment) { 258 | final float lineHeight = paint.getTextSize(); 259 | float lineY = y; 260 | for (CharSequence line : text.split("\n")) { 261 | canvas.save(); 262 | { 263 | final float lineWidth = (int) paint.measureText(line.toString()); 264 | float lineX = x; 265 | if (aligment == Layout.Alignment.ALIGN_CENTER) { 266 | lineX -= lineWidth / 2f; 267 | } 268 | if (lineX < 0) { 269 | lineX = 0; 270 | } 271 | 272 | final float right = lineX + lineWidth; 273 | if (right > canvas.getWidth()) { 274 | lineX = canvas.getWidth() - lineWidth - settings.paddingCorners; 275 | } 276 | 277 | canvas.translate(lineX, lineY); 278 | final StaticLayout staticLayout = new StaticLayout(line, paint, (int) lineWidth, aligment, 1.0f, 0, false); 279 | staticLayout.draw(canvas); 280 | 281 | lineY += lineHeight; 282 | } 283 | canvas.restore(); 284 | } 285 | 286 | } 287 | 288 | private void drawIndicatorsTextAbove(Canvas canvas, String text, TextPaint paintText, float x, float y, Layout.Alignment alignment) { 289 | 290 | final float textHeight = calculateTextMultilineHeight(text, paintText); 291 | y -= textHeight; 292 | 293 | final int width = (int) paintText.measureText(text); 294 | if (x >= getWidth() - settings.paddingCorners) { 295 | x = (getWidth() - width - settings.paddingCorners / 2f); 296 | } else if (x <= 0) { 297 | x = width / 2f; 298 | } else { 299 | x = (x - width / 2f); 300 | } 301 | 302 | if (x < 0) { 303 | x = 0; 304 | } 305 | 306 | if (x + width > getWidth()) { 307 | x = getWidth() - width; 308 | } 309 | 310 | drawText(canvas, text, x, y, paintText, alignment); 311 | } 312 | 313 | private float calculateTextMultilineHeight(String text, TextPaint textPaint) { 314 | return text.split("\n").length * textPaint.getTextSize(); 315 | } 316 | 317 | private float calculateBubbleTextWidth() { 318 | String bubbleText = formatValue(getCurrentValue()); 319 | return settings.paintTextBubble.measureText(bubbleText); 320 | } 321 | 322 | private void drawBubblePath(Canvas canvas, float triangleCenterX, float height, float width) { 323 | final Path path = new Path(); 324 | 325 | int padding = 3; 326 | final Rect rect = new Rect(padding, padding, (int) width - padding, (int) (height) - padding); 327 | 328 | final float roundRectHeight = (height) / 2; 329 | 330 | path.moveTo(rect.left + roundRectHeight, rect.top); 331 | path.lineTo(rect.right - roundRectHeight, rect.top); 332 | path.quadTo(rect.right, rect.top, rect.right, rect.top + roundRectHeight); 333 | path.lineTo(rect.right, rect.bottom - roundRectHeight); 334 | path.quadTo(rect.right, rect.bottom, rect.right - roundRectHeight, rect.bottom); 335 | 336 | path.lineTo(triangleCenterX, height - padding); 337 | path.lineTo(triangleCenterX, height - padding); 338 | path.lineTo(triangleCenterX, height - padding); 339 | 340 | path.lineTo(rect.left + roundRectHeight, rect.bottom); 341 | path.quadTo(rect.left, rect.bottom, rect.left, rect.bottom - roundRectHeight); 342 | path.lineTo(rect.left, rect.top + roundRectHeight); 343 | path.quadTo(rect.left, rect.top, rect.left + roundRectHeight, rect.top); 344 | path.close(); 345 | 346 | canvas.drawPath(path, settings.paintBubble); 347 | } 348 | 349 | private void drawBubble(Canvas canvas, float centerX, float triangleCenterX, float y) { 350 | final float width = this.bubble.width; 351 | final float height = this.bubble.height; 352 | 353 | canvas.save(); 354 | { 355 | canvas.translate(centerX - width / 2f, y); 356 | triangleCenterX -= (centerX - width / 2f); 357 | 358 | settings.paintBubble.setStyle(Paint.Style.FILL); 359 | settings.paintBubble.setColor(settings.foregroundColor); 360 | drawBubblePath(canvas, triangleCenterX, height, width); 361 | 362 | settings.paintBubble.setStyle(Paint.Style.FILL); 363 | } 364 | 365 | final String bubbleText = formatValue(getCurrentValue()); 366 | drawText(canvas, bubbleText, BUBBLE_PADDING_HORIZONTAL, bubble.getHeight() / 2f - settings.paintTextBubble.getTextSize() / 2f - BUBBLE_PADDING_VERTICAL, settings.paintTextBubble, Layout.Alignment.ALIGN_NORMAL); 367 | 368 | canvas.restore(); 369 | 370 | } 371 | 372 | public void setTextFormatter(TextFormatter textFormatter) { 373 | this.textFormatter = textFormatter; 374 | update(); 375 | } 376 | 377 | public void setRegionTextFormatter(RegionTextFormatter regionTextFormatter) { 378 | this.regionTextFormatter = regionTextFormatter; 379 | update(); 380 | } 381 | 382 | public Settings getSettings() { 383 | return settings; 384 | } 385 | 386 | public interface TextFormatter { 387 | String format(float value); 388 | } 389 | 390 | public interface RegionTextFormatter { 391 | String format(int region, float value); 392 | } 393 | 394 | public static class Settings { 395 | private Sushi slidr; 396 | private Paint paintBar; 397 | private TextPaint paintTextTop; 398 | private TextPaint paintTextBubble; 399 | private Paint paintBubble; 400 | private int colorBackground = Color.parseColor("#cccccc"); 401 | private int textColor = Color.parseColor("#6E6E6E"); 402 | 403 | private int textSize = 12; 404 | private int textSizeBubble = 16; 405 | 406 | private float barHeight = 35; 407 | private float paddingCorners; 408 | private int foregroundColor = Color.parseColor("#007E90"); 409 | 410 | 411 | private boolean displayMinMax = true; 412 | 413 | public Settings(Sushi slidr) { 414 | this.slidr = slidr; 415 | 416 | paintBar = new Paint(); 417 | paintBar.setAntiAlias(true); 418 | paintBar.setStrokeWidth(2); 419 | paintBar.setColor(colorBackground); 420 | 421 | paintTextTop = new TextPaint(); 422 | paintTextTop.setAntiAlias(true); 423 | paintTextTop.setStyle(Paint.Style.FILL); 424 | paintTextTop.setColor(textColor); 425 | paintTextTop.setTextSize(dpToPx(textSize)); 426 | 427 | paintTextBubble = new TextPaint(); 428 | paintTextBubble.setAntiAlias(true); 429 | paintTextBubble.setStyle(Paint.Style.FILL); 430 | paintTextBubble.setColor(Color.WHITE); 431 | paintTextBubble.setStrokeWidth(2); 432 | paintTextBubble.setTextSize(dpToPx(textSizeBubble)); 433 | 434 | paintBubble = new Paint(); 435 | paintBubble.setAntiAlias(true); 436 | paintBubble.setStrokeWidth(3); 437 | } 438 | 439 | private void init(Context context, AttributeSet attrs) { 440 | if (attrs != null) { 441 | final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Sushi); 442 | setColorBackground(a.getColor(R.styleable.Sushi_sushi_backgroundColor, colorBackground)); 443 | 444 | this.barHeight = a.getDimensionPixelOffset(R.styleable.Sushi_sushi_barHeight, (int) barHeight); 445 | this.foregroundColor = a.getColor(R.styleable.Sushi_sushi_foregroundColor, foregroundColor); 446 | 447 | this.textSize = a.getDimensionPixelOffset(R.styleable.Sushi_sushi_textSize, (int) dpToPx(textSize)); 448 | this.paintTextTop.setTextSize(textSize); 449 | 450 | this.textSizeBubble = a.getDimensionPixelOffset(R.styleable.Sushi_sushi_bubbleTextSize, (int) dpToPx(textSizeBubble)); 451 | this.paintTextBubble.setTextSize(textSizeBubble); 452 | 453 | this.displayMinMax = a.getBoolean(R.styleable.Sushi_sushi_displayMinMax, displayMinMax); 454 | 455 | a.recycle(); 456 | } 457 | } 458 | 459 | public void setBarHeight(int barHeight) { 460 | this.barHeight = barHeight; 461 | slidr.updateValues(); 462 | slidr.update(); 463 | } 464 | 465 | public void setForegroundColor(int foregroundColor) { 466 | this.foregroundColor = foregroundColor; 467 | slidr.update(); 468 | } 469 | 470 | public void setColorBackground(int colorBackground) { 471 | this.colorBackground = colorBackground; 472 | slidr.update(); 473 | } 474 | 475 | public void setTextSize(int textSize) { 476 | this.textSize = textSize; 477 | this.paintTextTop.setTextSize(textSize); 478 | slidr.updateValues(); 479 | slidr.update(); 480 | } 481 | 482 | public void setBubbleTextSize(int textSizeBubble) { 483 | this.textSizeBubble = textSizeBubble; 484 | this.paintTextBubble.setTextSize(textSizeBubble); 485 | slidr.updateValues(); 486 | slidr.update(); 487 | } 488 | 489 | private float dpToPx(int size) { 490 | return size * slidr.getResources().getDisplayMetrics().density; 491 | } 492 | 493 | public void setDisplayMinMax(boolean displayMinMax) { 494 | this.displayMinMax = displayMinMax; 495 | slidr.updateValues(); 496 | slidr.update(); 497 | slidr.requestLayout(); 498 | } 499 | } 500 | 501 | private class Bubble { 502 | private float height; 503 | private float width; 504 | private float x; 505 | private float y; 506 | 507 | public boolean clicked(MotionEvent e) { 508 | return e.getX() >= x && e.getX() <= x + width 509 | && e.getY() >= y && e.getY() < y + height; 510 | } 511 | 512 | public float getHeight() { 513 | return height; 514 | } 515 | 516 | public float getX() { 517 | return Math.max(x, 0); 518 | } 519 | 520 | public float getY() { 521 | return Math.max(y, 0); 522 | } 523 | } 524 | 525 | public class EurosTextFormatter implements TextFormatter { 526 | 527 | @Override 528 | public String format(float value) { 529 | return String.format("%d €", (int) value); 530 | } 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /android-slidr/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /android-slidr/src/main/res/values/attrs_non_editable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | defaultConfig { 7 | applicationId "com.github.florent37.slideure" 8 | minSdkVersion 15 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile 'com.android.support:appcompat-v7:25.3.1' 24 | compile 'com.android.support.constraint:constraint-layout:1.0.0' 25 | compile 'com.facebook.stetho:stetho:1.5.0' 26 | 27 | compile project (':android-slidr') 28 | } 29 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/florentchampigny/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 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/florent37/slidr/EditActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.florent37.slidr; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | 7 | import com.github.florent37.androidslidr.Slidr; 8 | 9 | /** 10 | * Created by florentchampigny on 24/05/2017. 11 | */ 12 | 13 | public class EditActivity extends AppCompatActivity { 14 | 15 | @Override 16 | protected void onCreate(@Nullable Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_edit); 19 | 20 | final Slidr slidr = (Slidr) findViewById(R.id.slideure); 21 | slidr.setMin(200); 22 | slidr.setBubbleClickedListener(new Slidr.BubbleClickedListener() { 23 | @Override 24 | public void bubbleClicked(Slidr slidr) { 25 | 26 | } 27 | }); 28 | slidr.setListener(new Slidr.Listener() { 29 | @Override 30 | public void valueChanged(Slidr slidr, float currentValue) { 31 | 32 | } 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/florent37/slidr/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.florent37.slidr; 2 | 3 | import android.graphics.Color; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.util.Log; 7 | import android.view.View; 8 | import android.widget.Toast; 9 | 10 | import com.github.florent37.androidslidr.Slidr; 11 | 12 | public class MainActivity extends AppCompatActivity { 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_main); 18 | 19 | { 20 | final Slidr slidr = (Slidr) findViewById(R.id.slideure); 21 | if (slidr != null) { 22 | slidr.setMax(500); 23 | slidr.addStep(new Slidr.Step("test", 250, Color.parseColor("#007E90"), Color.RED)); 24 | slidr.setTextMax("max\nvalue"); 25 | slidr.setTextMin("min\nvalue"); 26 | slidr.setCurrentValue(300); 27 | slidr.setListener(new Slidr.Listener() { 28 | @Override 29 | public void valueChanged(Slidr slidr, float currentValue) { 30 | 31 | } 32 | }); 33 | } 34 | 35 | findViewById(R.id.max).setOnClickListener(new View.OnClickListener() { 36 | @Override 37 | public void onClick(View v) { 38 | slidr.setMax(1000); 39 | } 40 | }); 41 | 42 | findViewById(R.id.current).setOnClickListener(new View.OnClickListener() { 43 | @Override 44 | public void onClick(View v) { 45 | slidr.setCurrentValue(100); 46 | } 47 | }); 48 | 49 | } 50 | 51 | { 52 | final Slidr slidr = (Slidr) findViewById(R.id.slideure2); 53 | if (slidr != null) { 54 | slidr.setMax(5000); 55 | slidr.setCurrentValue(5000); 56 | slidr.addStep(new Slidr.Step("test", 1500, Color.parseColor("#007E90"), Color.parseColor("#111111"))); 57 | } 58 | } 59 | { 60 | final Slidr slidr = (Slidr) findViewById(R.id.slideure_regions); 61 | if (slidr != null) { 62 | slidr.setMax(3000); 63 | slidr.setRegionTextFormatter(new Slidr.RegionTextFormatter() { 64 | @Override 65 | public String format(int region, float value) { 66 | return String.format("region %d : %d", region, (int) value); 67 | } 68 | }); 69 | slidr.addStep(new Slidr.Step("test", 1500, Color.parseColor("#007E90"), Color.parseColor("#111111"))); 70 | } 71 | } 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/florent37/slidr/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.florent37.slidr; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.stetho.Stetho; 6 | 7 | /** 8 | * Created by florentchampigny on 30/05/2017. 9 | */ 10 | 11 | public class MainApplication extends Application { 12 | @Override 13 | public void onCreate() { 14 | super.onCreate(); 15 | Stetho.initializeWithDefaults(this); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/florent37/slidr/NonEditActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.florent37.slidr; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | 7 | import com.github.florent37.androidslidr.Sushi; 8 | 9 | /** 10 | * Created by florentchampigny on 24/05/2017. 11 | */ 12 | 13 | public class NonEditActivity extends AppCompatActivity { 14 | 15 | @Override 16 | protected void onCreate(@Nullable Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_non_editable); 19 | 20 | final Sushi slidr = (Sushi) findViewById(R.id.slideure); 21 | slidr.getSettings().setDisplayMinMax(true); 22 | slidr.setMin(0); 23 | slidr.setMax(1000); 24 | slidr.setCurrentValue(300); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 22 | 23 | 24 | 32 | 33 | 34 | 46 | 47 | 48 | 58 | 59 |