├── .gitignore
├── README.md
├── README_zh.md
├── apk
└── sample.apk
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── xw
│ │ └── samlpe
│ │ └── bubbleseekbar
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── xw
│ │ │ └── samlpe
│ │ │ └── bubbleseekbar
│ │ │ ├── DemoFragment1.java
│ │ │ ├── DemoFragment2.java
│ │ │ ├── DemoFragment3.java
│ │ │ ├── DemoFragment4.java
│ │ │ ├── MainActivity.java
│ │ │ └── ObservableScrollView.java
│ └── res
│ │ ├── drawable
│ │ ├── selector_radio_text_color.xml
│ │ ├── shape_divider_shadow.xml
│ │ └── shape_divider_vertical.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── fragment_demo_1.xml
│ │ ├── fragment_demo_2.xml
│ │ ├── fragment_demo_3.xml
│ │ └── fragment_demo_4.xml
│ │ ├── menu
│ │ └── menu_main.xml
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ │ ├── values-v21
│ │ └── styles.xml
│ │ ├── values-w820dp
│ │ └── dimens.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── xw
│ └── samlpe
│ └── bubbleseekbar
│ └── ExampleUnitTest.java
├── bubbleseekbar
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── xw
│ │ └── repo
│ │ ├── BubbleConfigBuilder.java
│ │ ├── BubbleSeekBar.java
│ │ └── BubbleUtils.java
│ └── res
│ └── values
│ ├── attr.xml
│ ├── colors.xml
│ └── strings.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── screenshot
├── demo1.gif
├── demo2.gif
├── demo3.gif
└── demo4.gif
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://bintray.com/woxingxiao/maven/bubbleseekbar/3.0/link)
2 | [](https://bintray.com/woxingxiao/maven/bubbleseekbar/3.0-lite/link)
3 | [](https://opensource.org/licenses/Apache-2.0)
4 |
5 | [**中文说明**](https://github.com/woxingxiao/BubbleSeekBar/blob/master/README_zh.md)
6 |
7 | **A beautiful Android custom seek bar, which has a bubble view with progress appearing upon when seeking. Highly customizable, mostly demands has been considered. `star` or `pull request` will be welcomed**
8 | ****
9 | ## Screenshot
10 | 
11 | 
12 | ******
13 | 
14 | 
15 |
16 | ## Download
17 | The **LATEST_VERSION**: [](https://bintray.com/woxingxiao/maven/bubbleseekbar/_latestVersion)
18 | ```groovy
19 | dependencies {
20 | // lite version (recommended)
21 | // e.g. compile 'com.xw.repo:bubbleseekbar:3.4-lite'
22 | compile 'com.xw.repo:bubbleseekbar:${LATEST_VERSION}-lite'
23 |
24 | // enhanced version
25 | // e.g. compile 'com.xw.repo:bubbleseekbar:3.4'
26 | // compile 'com.xw.repo:bubbleseekbar:${LATEST_VERSION}'
27 | }
28 | ```
29 |
30 | ## Usage
31 | ### xml
32 | ```xml
33 |
50 | ```
51 | ```xml
52 |
65 | ```
66 | ### java (not for **_lite_** version)
67 | ```java
68 | mBbubbleSeekBar.getConfigBuilder()
69 | .min(0.0)
70 | .max(50)
71 | .progress(20)
72 | .sectionCount(5)
73 | .trackColor(ContextCompat.getColor(getContext(), R.color.color_gray))
74 | .secondTrackColor(ContextCompat.getColor(getContext(), R.color.color_blue))
75 | .thumbColor(ContextCompat.getColor(getContext(), R.color.color_blue))
76 | .showSectionText()
77 | .sectionTextColor(ContextCompat.getColor(getContext(), R.color.colorPrimary))
78 | .sectionTextSize(18)
79 | .showThumbText()
80 | .thumbTextColor(ContextCompat.getColor(getContext(), R.color.color_red))
81 | .thumbTextSize(18)
82 | .bubbleColor(ContextCompat.getColor(getContext(), R.color.color_green))
83 | .bubbleTextSize(18)
84 | .showSectionMark()
85 | .seekBySection()
86 | .autoAdjustSectionMark()
87 | .sectionTextPosition(BubbleSeekBar.TextPosition.BELOW_SECTION_MARK)
88 | .build();
89 | ```
90 | Check out the demo for more details. Or download the apk: [**sample.apk**](https://github.com/woxingxiao/BubbleSeekBar/raw/master/apk/sample.apk)
91 | ## Attentions
92 | - There are two versions of this library.The differences as follow:
93 |
94 | version | init | getter/setter
95 | -------- | ---|---
96 | lite|xml|min, max, progress
97 | enhanced|xml, java|all attrs
98 |
99 | **_lite_** version is recommended.
100 |
101 | - You must correct the offsets by setting `ScrollListener` when `BubbleSeekBar`'s parent view is scrollable, otherwise the position of bubble appears maybe be wrong. For example:
102 | ```java
103 | mContainer.setOnYourContainerScrollListener(new OnYourContainerScrollListener() {
104 | @Override
105 | public void onScroll() {
106 | // call this method to correct offsets
107 | mBubbleSeekBar.correctOffsetWhenContainerOnScrolling();
108 | }
109 | });
110 | ```
111 | - When set `bsb_touch_to_seek` attribute to be `true` , you better not to click **too fast** because the animation may not have enough time to play.
112 | - This library depends `support:appcompat-v7` via **`provided`**, so you don't need to worry about compatibility.
113 |
114 | ## Attributes
115 | ```xml
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | nce"/>
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | ```
151 | ## License
152 | ```
153 | Copyright 2017 woxingxiao
154 |
155 | Licensed under the Apache License, Version 2.0 (the "License");
156 | you may not use this file except in compliance with the License.
157 | You may obtain a copy of the License at
158 |
159 | http://www.apache.org/licenses/LICENSE-2.0
160 |
161 | Unless required by applicable law or agreed to in writing, software
162 | distributed under the License is distributed on an "AS IS" BASIS,
163 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
164 | See the License for the specific language governing permissions and
165 | limitations under the License.
166 | ```
167 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | [](https://bintray.com/woxingxiao/maven/bubbleseekbar/3.0/link)
2 | [](https://bintray.com/woxingxiao/maven/bubbleseekbar/3.0-lite/link)
3 | [](https://opensource.org/licenses/Apache-2.0)
4 |
5 | **自定义`SeekBar`,进度变化由可视化气泡样式呈现,定制化程度较高,适合大部分需求。欢迎`star` or `pull request`**
6 | ****
7 | ## Screenshot
8 | 
9 | 
10 | *******
11 | 
12 | 
13 | ## Download
14 | The **LATEST_VERSION**:[](https://bintray.com/woxingxiao/maven/bubbleseekbar/_latestVersion)
15 | ```groovy
16 | dependencies {
17 | // lite version 轻量版(推荐)
18 | // 例如:compile 'com.xw.repo:bubbleseekbar:3.4-lite'
19 | compile 'com.xw.repo:bubbleseekbar:${LATEST_VERSION}-lite'
20 |
21 | // enhanced version 增强版
22 | // 例如:compile 'com.xw.repo:bubbleseekbar:3.4'
23 | // compile 'com.xw.repo:bubbleseekbar:${LATEST_VERSION}'
24 | }
25 | ```
26 | ## Usage
27 | ### xml
28 | ```xml
29 |
46 | ```
47 | ```xml
48 |
61 | ```
62 | ### java (not for **_lite_** version)
63 | ```java
64 | mBbubbleSeekBar.getConfigBuilder()
65 | .min(0.0)
66 | .max(50)
67 | .progress(20)
68 | .sectionCount(5)
69 | .trackColor(ContextCompat.getColor(getContext(), R.color.color_gray))
70 | .secondTrackColor(ContextCompat.getColor(getContext(), R.color.color_blue))
71 | .thumbColor(ContextCompat.getColor(getContext(), R.color.color_blue))
72 | .showSectionText()
73 | .sectionTextColor(ContextCompat.getColor(getContext(), R.color.colorPrimary))
74 | .sectionTextSize(18)
75 | .showThumbText()
76 | .thumbTextColor(ContextCompat.getColor(getContext(), R.color.color_red))
77 | .thumbTextSize(18)
78 | .bubbleColor(ContextCompat.getColor(getContext(), R.color.color_green))
79 | .bubbleTextSize(18)
80 | .showSectionMark()
81 | .seekBySection()
82 | .autoAdjustSectionMark()
83 | .sectionTextPosition(BubbleSeekBar.TextPosition.BELOW_SECTION_MARK)
84 | .build();
85 | ```
86 | 查看demo获知更多使用细节。或者下载安装apk:[**sample.apk**](https://github.com/woxingxiao/BubbleSeekBar/raw/master/apk/sample.apk)
87 |
88 | ## Attentions
89 | - 下列是两个版本的差异对比:
90 |
91 | version | init | getter/setter
92 | -------- | ---|---
93 | lite|xml|min, max, progress
94 | enhanced|xml, java|all attrs
95 |
96 | 推荐使用 **_lite_** 版本。
97 |
98 | - 如果`BubbleSeekBar`的外部容器是可滑动的控件,需要设置滑动监听来修正气泡的偏移,否则滑动后气泡出现位置可能错乱。方法如下:
99 | ```java
100 | mContainer.setOnYourContainerScrollListener(new OnYourContainerScrollListener() {
101 | @Override
102 | public void onScroll() {
103 | // 调用修正偏移方法
104 | mBubbleSeekBar.correctOffsetWhenContainerOnScrolling();
105 | }
106 | });
107 | ```
108 | - 当设置`bsb_touch_to_seek`属性为`true`时, 最好不要点击**太快**去seek进度,否则动画可能没有足够时间播放。
109 | - 本库依赖`support:appcompat-v7`通过 **`provided`** 的方式,所以不必担心兼容性。
110 |
111 | ## Attributes
112 | ```xml
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | nce"/>
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | ```
148 | --------
149 | > **人生苦短,请选择科学上网。推荐一下本人正在使用的,稳定高速,便宜好用。[推介链接](https://portal.shadowsocks.com.hk/aff.php?aff=8881)**
150 |
151 | ## License
152 | ```
153 | Copyright 2017 woxingxiao
154 |
155 | Licensed under the Apache License, Version 2.0 (the "License");
156 | you may not use this file except in compliance with the License.
157 | You may obtain a copy of the License at
158 |
159 | http://www.apache.org/licenses/LICENSE-2.0
160 |
161 | Unless required by applicable law or agreed to in writing, software
162 | distributed under the License is distributed on an "AS IS" BASIS,
163 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
164 | See the License for the specific language governing permissions and
165 | limitations under the License.
166 | ```
167 |
--------------------------------------------------------------------------------
/apk/sample.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sathishmscict/BubbleSeekBar/d3796790eb8d9329a84b3f0fe4100c3e5542778d/apk/sample.apk
--------------------------------------------------------------------------------
/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.xw.samlpe.bubbleseekbar"
8 | minSdkVersion 14
9 | targetSdkVersion 25
10 | versionCode 6
11 | versionName "1.5"
12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13 | }
14 |
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 |
22 | android.applicationVariants.all { variant ->
23 | variant.outputs.each { output ->
24 | if (output.outputFile != null && output.outputFile.name.endsWith('.apk')) {
25 | def name = "${rootDir}/apk/sample.apk"
26 | output.outputFile = file(name)
27 | }
28 | }
29 | }
30 | }
31 |
32 | dependencies {
33 | compile fileTree(include: ['*.jar'], dir: 'libs')
34 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
35 | exclude group: 'com.android.support', module: 'support-annotations'
36 | })
37 | compile 'com.android.support:appcompat-v7:25.3.1'
38 | compile 'com.android.support:design:25.3.1'
39 | testCompile 'junit:junit:4.12'
40 | compile project(':bubbleseekbar')
41 | }
42 |
--------------------------------------------------------------------------------
/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 D:\Android\android-sdk-windows/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 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/xw/samlpe/bubbleseekbar/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.xw.samlpe.bubbleseekbar;
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.xw.samlpe.bubbleseekbar", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xw/samlpe/bubbleseekbar/DemoFragment1.java:
--------------------------------------------------------------------------------
1 | package com.xw.samlpe.bubbleseekbar;
2 |
3 | import android.os.Bundle;
4 | import android.support.annotation.Nullable;
5 | import android.support.design.widget.Snackbar;
6 | import android.support.v4.app.Fragment;
7 | import android.view.LayoutInflater;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 | import android.widget.Button;
11 |
12 | import com.xw.repo.BubbleSeekBar;
13 |
14 | import java.util.Random;
15 |
16 | /**
17 | * DemoFragment1
18 | * <>
19 | * Created by woxingxiao on 2017-03-11.
20 | */
21 |
22 | public class DemoFragment1 extends Fragment {
23 |
24 | public static DemoFragment1 newInstance() {
25 | return new DemoFragment1();
26 | }
27 |
28 | @Nullable
29 | @Override
30 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
31 |
32 | View view = inflater.inflate(R.layout.fragment_demo_1, container, false);
33 |
34 | final BubbleSeekBar bubbleSeekBar = (BubbleSeekBar) view.findViewById(R.id.demo_1_seek_bar);
35 | bubbleSeekBar.setProgress(20);
36 | Button button = (Button) view.findViewById(R.id.demo_1_button);
37 |
38 | button.setOnClickListener(new View.OnClickListener() {
39 | @Override
40 | public void onClick(View v) {
41 | int progress = new Random().nextInt((int) bubbleSeekBar.getMax());
42 | bubbleSeekBar.setProgress(progress);
43 | Snackbar.make(v, "set random progress = " + progress, Snackbar.LENGTH_SHORT).show();
44 | }
45 | });
46 |
47 | return view;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xw/samlpe/bubbleseekbar/DemoFragment2.java:
--------------------------------------------------------------------------------
1 | package com.xw.samlpe.bubbleseekbar;
2 |
3 | import android.os.Bundle;
4 | import android.support.annotation.Nullable;
5 | import android.support.v4.app.Fragment;
6 | import android.view.LayoutInflater;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 |
10 | /**
11 | * DemoFragment2
12 | * <>
13 | * Created by woxingxiao on 2017-03-11.
14 | */
15 |
16 | public class DemoFragment2 extends Fragment {
17 |
18 | public static DemoFragment2 newInstance() {
19 | return new DemoFragment2();
20 | }
21 |
22 | @Nullable
23 | @Override
24 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
25 | return inflater.inflate(R.layout.fragment_demo_2, container, false);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xw/samlpe/bubbleseekbar/DemoFragment3.java:
--------------------------------------------------------------------------------
1 | package com.xw.samlpe.bubbleseekbar;
2 |
3 | import android.os.Bundle;
4 | import android.support.annotation.Nullable;
5 | import android.support.v4.app.Fragment;
6 | import android.support.v4.content.ContextCompat;
7 | import android.view.LayoutInflater;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 |
11 | import com.xw.repo.BubbleSeekBar;
12 |
13 | /**
14 | * DemoFragment3
15 | * <>
16 | * Created by woxingxiao on 2017-03-11.
17 | */
18 |
19 | public class DemoFragment3 extends Fragment {
20 |
21 | public static DemoFragment3 newInstance() {
22 | return new DemoFragment3();
23 | }
24 |
25 | @Nullable
26 | @Override
27 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
28 |
29 | View view = inflater.inflate(R.layout.fragment_demo_3, container, false);
30 | BubbleSeekBar bubbleSeekBar1 = (BubbleSeekBar) view.findViewById(R.id.demo_3_seek_bar_1);
31 | BubbleSeekBar bubbleSeekBar2 = (BubbleSeekBar) view.findViewById(R.id.demo_3_seek_bar_2);
32 | BubbleSeekBar bubbleSeekBar3 = (BubbleSeekBar) view.findViewById(R.id.demo_3_seek_bar_3);
33 | BubbleSeekBar bubbleSeekBar4 = (BubbleSeekBar) view.findViewById(R.id.demo_3_seek_bar_4);
34 |
35 | bubbleSeekBar1.getConfigBuilder()
36 | .min(0)
37 | .max(50)
38 | .progress(20)
39 | .sectionCount(5)
40 | .trackColor(ContextCompat.getColor(getContext(), R.color.color_gray))
41 | .secondTrackColor(ContextCompat.getColor(getContext(), R.color.color_blue))
42 | .thumbColor(ContextCompat.getColor(getContext(), R.color.color_blue))
43 | .showSectionText()
44 | .sectionTextColor(ContextCompat.getColor(getContext(), R.color.colorPrimary))
45 | .sectionTextSize(18)
46 | .showThumbText()
47 | .thumbTextColor(ContextCompat.getColor(getContext(), R.color.color_red))
48 | .thumbTextSize(18)
49 | .bubbleColor(ContextCompat.getColor(getContext(), R.color.color_green))
50 | .bubbleTextSize(18)
51 | .showSectionMark()
52 | .seekBySection()
53 | .autoAdjustSectionMark()
54 | .sectionTextPosition(BubbleSeekBar.TextPosition.BELOW_SECTION_MARK)
55 | .build();
56 |
57 | bubbleSeekBar2.getConfigBuilder()
58 | .min(-50)
59 | .max(50)
60 | .sectionCount(10)
61 | .sectionTextInterval(2)
62 | .trackColor(ContextCompat.getColor(getContext(), R.color.color_red_light))
63 | .secondTrackColor(ContextCompat.getColor(getContext(), R.color.color_red))
64 | .seekBySection()
65 | .showSectionText()
66 | .sectionTextPosition(BubbleSeekBar.TextPosition.BELOW_SECTION_MARK)
67 | .build();
68 |
69 | bubbleSeekBar3.getConfigBuilder()
70 | .min(1)
71 | .max(1.5f)
72 | .floatType()
73 | .sectionCount(10)
74 | .secondTrackColor(ContextCompat.getColor(getContext(), R.color.color_green))
75 | .showSectionText()
76 | .showThumbText()
77 | .build();
78 |
79 | bubbleSeekBar4.getConfigBuilder()
80 | .min(-0.4f)
81 | .max(0.4f)
82 | .progress(0)
83 | .floatType()
84 | .sectionCount(10)
85 | .sectionTextInterval(2)
86 | .showSectionText()
87 | .sectionTextPosition(BubbleSeekBar.TextPosition.BELOW_SECTION_MARK)
88 | .autoAdjustSectionMark()
89 | .build();
90 |
91 | return view;
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xw/samlpe/bubbleseekbar/DemoFragment4.java:
--------------------------------------------------------------------------------
1 | package com.xw.samlpe.bubbleseekbar;
2 |
3 | import android.os.Bundle;
4 | import android.support.annotation.Nullable;
5 | import android.support.v4.app.Fragment;
6 | import android.view.LayoutInflater;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 | import android.widget.TextView;
10 |
11 | import com.xw.repo.BubbleSeekBar;
12 |
13 | import java.util.Locale;
14 |
15 | /**
16 | * DemoFragment4
17 | * <>
18 | * Created by woxingxiao on 2017-03-11.
19 | */
20 |
21 | public class DemoFragment4 extends Fragment {
22 |
23 | public static DemoFragment4 newInstance() {
24 | return new DemoFragment4();
25 | }
26 |
27 | @Nullable
28 | @Override
29 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
30 |
31 | View view = inflater.inflate(R.layout.fragment_demo_4, container, false);
32 | ObservableScrollView mObsScrollView = (ObservableScrollView) view.findViewById(R.id.demo_4_obs_scroll_view);
33 | final BubbleSeekBar bubbleSeekBar1 = (BubbleSeekBar) view.findViewById(R.id.demo_4_seek_bar_1);
34 | final BubbleSeekBar bubbleSeekBar2 = (BubbleSeekBar) view.findViewById(R.id.demo_4_seek_bar_2);
35 | final BubbleSeekBar bubbleSeekBar3 = (BubbleSeekBar) view.findViewById(R.id.demo_4_seek_bar_3);
36 | final TextView progressText1 = (TextView) view.findViewById(R.id.demo_4_progress_text_1);
37 | final TextView progressText2 = (TextView) view.findViewById(R.id.demo_4_progress_text_2);
38 | final TextView progressText3 = (TextView) view.findViewById(R.id.demo_4_progress_text_3);
39 |
40 | mObsScrollView.setOnScrollChangedListener(new ObservableScrollView.OnScrollChangedListener() {
41 | @Override
42 | public void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy) {
43 | bubbleSeekBar1.correctOffsetWhenContainerOnScrolling();
44 | }
45 | });
46 | bubbleSeekBar2.setOnProgressChangedListener(new BubbleSeekBar.OnProgressChangedListenerAdapter() {
47 | @Override
48 | public void onProgressChanged(int progress, float progressFloat) {
49 | String s = String.format(Locale.CHINA, "onChanged int:%d, float:%.1f", progress, progressFloat);
50 | progressText1.setText(s);
51 | }
52 |
53 | @Override
54 | public void getProgressOnActionUp(int progress, float progressFloat) {
55 | String s = String.format(Locale.CHINA, "onActionUp int:%d, float:%.1f", progress, progressFloat);
56 | progressText2.setText(s);
57 | }
58 |
59 | @Override
60 | public void getProgressOnFinally(int progress, float progressFloat) {
61 | String s = String.format(Locale.CHINA, "onFinally int:%d, float:%.1f", progress, progressFloat);
62 | progressText3.setText(s);
63 | }
64 | });
65 |
66 | // trigger by set progress or seek by finger
67 | bubbleSeekBar3.setProgress(bubbleSeekBar3.getMax());
68 |
69 | return view;
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xw/samlpe/bubbleseekbar/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.xw.samlpe.bubbleseekbar;
2 |
3 | import android.os.Bundle;
4 | import android.support.v4.app.Fragment;
5 | import android.support.v4.app.FragmentManager;
6 | import android.support.v4.app.FragmentTransaction;
7 | import android.support.v7.app.AppCompatActivity;
8 | import android.support.v7.widget.Toolbar;
9 | import android.view.Menu;
10 | import android.view.MenuItem;
11 | import android.view.View;
12 |
13 | public class MainActivity extends AppCompatActivity implements View.OnClickListener {
14 |
15 | private String mTag;
16 |
17 | @Override
18 | protected void onCreate(Bundle savedInstanceState) {
19 | super.onCreate(savedInstanceState);
20 | setContentView(R.layout.activity_main);
21 |
22 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
23 | setSupportActionBar(toolbar);
24 |
25 | findViewById(R.id.main_tab_btn_1).setOnClickListener(this);
26 | findViewById(R.id.main_tab_btn_2).setOnClickListener(this);
27 | findViewById(R.id.main_tab_btn_3).setOnClickListener(this);
28 | findViewById(R.id.main_tab_btn_4).setOnClickListener(this);
29 |
30 | if (savedInstanceState == null) {
31 | FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
32 | ft.add(R.id.container, DemoFragment1.newInstance(), "demo1");
33 | ft.commit();
34 | mTag = "demo1";
35 | }
36 | }
37 |
38 | @Override
39 | public void onClick(View v) {
40 | switch (v.getId()) {
41 | case R.id.main_tab_btn_1:
42 | switchContent("demo1");
43 | break;
44 | case R.id.main_tab_btn_2:
45 | switchContent("demo2");
46 | break;
47 | case R.id.main_tab_btn_3:
48 | switchContent("demo3");
49 | break;
50 | case R.id.main_tab_btn_4:
51 | switchContent("demo4");
52 | break;
53 | }
54 | }
55 |
56 | public void switchContent(String toTag) {
57 | if (mTag.equals(toTag))
58 | return;
59 |
60 | FragmentManager fm = getSupportFragmentManager();
61 | Fragment from = fm.findFragmentByTag(mTag);
62 | Fragment to = fm.findFragmentByTag(toTag);
63 |
64 | FragmentTransaction ft = fm.beginTransaction();
65 | if (to == null) {
66 | if ("demo1".equals(toTag)) {
67 | to = DemoFragment1.newInstance();
68 | } else if ("demo2".equals(toTag)) {
69 | to = DemoFragment2.newInstance();
70 | } else if ("demo3".equals(toTag)) {
71 | to = DemoFragment3.newInstance();
72 | } else {
73 | to = DemoFragment4.newInstance();
74 | }
75 | }
76 | if (!to.isAdded()) {
77 | ft.hide(from).add(R.id.container, to, toTag);
78 | } else {
79 | ft.hide(from).show(to);
80 | }
81 | ft.commit();
82 |
83 | mTag = toTag;
84 | }
85 |
86 | @Override
87 | public boolean onCreateOptionsMenu(Menu menu) {
88 | // Inflate the menu; this adds items to the action bar if it is present.
89 | getMenuInflater().inflate(R.menu.menu_main, menu);
90 | return true;
91 | }
92 |
93 | @Override
94 | public boolean onOptionsItemSelected(MenuItem item) {
95 | // Handle action bar item clicks here. The action bar will
96 | // automatically handle clicks on the Home/Up button, so long
97 | // as you specify a parent activity in AndroidManifest.xml.
98 | int id = item.getItemId();
99 |
100 | //noinspection SimplifiableIfStatement
101 | if (id == R.id.action_settings) {
102 | return true;
103 | }
104 |
105 | return super.onOptionsItemSelected(item);
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xw/samlpe/bubbleseekbar/ObservableScrollView.java:
--------------------------------------------------------------------------------
1 | package com.xw.samlpe.bubbleseekbar;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.MotionEvent;
6 | import android.widget.ScrollView;
7 |
8 | public class ObservableScrollView extends ScrollView {
9 |
10 | private OnScrollChangedListener mOnScrollChangedListener;
11 |
12 | public ObservableScrollView(Context context) {
13 | super(context);
14 | }
15 |
16 | public ObservableScrollView(Context context, AttributeSet attrs, int defStyle) {
17 | super(context, attrs, defStyle);
18 | }
19 |
20 | public ObservableScrollView(Context context, AttributeSet attrs) {
21 | super(context, attrs);
22 | }
23 |
24 | public void setOnScrollChangedListener(OnScrollChangedListener onScrollChangedListener) {
25 | this.mOnScrollChangedListener = onScrollChangedListener;
26 | }
27 |
28 | @Override
29 | protected void onScrollChanged(int x, int y, int oldx, int oldy) {
30 | super.onScrollChanged(x, y, oldx, oldy);
31 | if (mOnScrollChangedListener != null) {
32 | mOnScrollChangedListener.onScrollChanged(this, x, y, oldx, oldy);
33 | }
34 | }
35 |
36 | @Override
37 | public boolean onInterceptTouchEvent(MotionEvent ev) {
38 | return false; // 此处为了演示,阻止ScrollView拦截Touch事件
39 | }
40 |
41 | interface OnScrollChangedListener {
42 |
43 | void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy);
44 |
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selector_radio_text_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_divider_shadow.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_divider_vertical.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
22 |
23 |
27 |
28 |
37 |
38 |
48 |
49 |
58 |
59 |
68 |
69 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_demo_1.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
21 |
22 |
28 |
29 |
37 |
38 |
44 |
45 |
54 |
55 |
60 |
61 |
74 |
75 |
81 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_demo_2.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
23 |
24 |
29 |
30 |
47 |
48 |
54 |
55 |
67 |
68 |
74 |
75 |
88 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_demo_3.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
21 |
22 |
28 |
29 |
34 |
35 |
41 |
42 |
47 |
48 |
54 |
55 |
60 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_demo_4.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
20 |
21 |
28 |
29 |
36 |
37 |
47 |
48 |
56 |
57 |
66 |
67 |
68 |
69 |
70 |
76 |
77 |
91 |
92 |
97 |
98 |
103 |
104 |
109 |
110 |
116 |
117 |
132 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
5 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sathishmscict/BubbleSeekBar/d3796790eb8d9329a84b3f0fe4100c3e5542778d/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sathishmscict/BubbleSeekBar/d3796790eb8d9329a84b3f0fe4100c3e5542778d/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sathishmscict/BubbleSeekBar/d3796790eb8d9329a84b3f0fe4100c3e5542778d/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sathishmscict/BubbleSeekBar/d3796790eb8d9329a84b3f0fe4100c3e5542778d/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sathishmscict/BubbleSeekBar/d3796790eb8d9329a84b3f0fe4100c3e5542778d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #607D8B
4 | #455A64
5 | #FF9800
6 | #B4B4B4
7 | #00b4aa
8 | #FF6A6E
9 | #FFB4B6
10 | #03A9F4
11 | #F5F5F5
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 16dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BubbleSeekBar
3 | Settings
4 | set random progress
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/test/java/com/xw/samlpe/bubbleseekbar/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.xw.samlpe.bubbleseekbar;
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 | }
--------------------------------------------------------------------------------
/bubbleseekbar/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/bubbleseekbar/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | def VERSION_NAME = "3.5"
4 |
5 | android {
6 | compileSdkVersion 25
7 | buildToolsVersion "25.0.2"
8 |
9 | defaultConfig {
10 | minSdkVersion 14
11 | targetSdkVersion 25
12 | versionCode 21
13 | versionName VERSION_NAME
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 fileTree(dir: 'libs', include: ['*.jar'])
26 | provided 'com.android.support:appcompat-v7:25.3.1'
27 | }
28 |
29 | apply plugin: 'com.novoda.bintray-release'
30 | publish {
31 | userOrg = 'woxingxiao'
32 | groupId = 'com.xw.repo'
33 | artifactId = 'bubbleseekbar'
34 | publishVersion = VERSION_NAME
35 | desc = 'A beautiful Android custom seekbar, which has a bubble view with progress appearing upon when seeking.'
36 | website = 'https://github.com/woxingxiao/BubbleSeekBar'
37 | }
--------------------------------------------------------------------------------
/bubbleseekbar/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 D:\Android\android-sdk-windows/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 |
--------------------------------------------------------------------------------
/bubbleseekbar/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/bubbleseekbar/src/main/java/com/xw/repo/BubbleConfigBuilder.java:
--------------------------------------------------------------------------------
1 | package com.xw.repo;
2 |
3 | import android.support.annotation.ColorInt;
4 | import android.support.annotation.IntRange;
5 |
6 | import static com.xw.repo.BubbleUtils.dp2px;
7 | import static com.xw.repo.BubbleUtils.sp2px;
8 |
9 | /**
10 | * config BubbleSeekBar's attributes
11 | *
12 | * Created by woxingxiao on 2017-03-14.
13 | */
14 | public class BubbleConfigBuilder {
15 |
16 | float min;
17 | float max;
18 | float progress;
19 | boolean floatType;
20 | int trackSize;
21 | int secondTrackSize;
22 | int thumbRadius;
23 | int thumbRadiusOnDragging;
24 | int trackColor;
25 | int secondTrackColor;
26 | int thumbColor;
27 | int sectionCount;
28 | boolean showSectionMark;
29 | boolean autoAdjustSectionMark;
30 | boolean showSectionText;
31 | int sectionTextSize;
32 | int sectionTextColor;
33 | @BubbleSeekBar.TextPosition
34 | int sectionTextPosition;
35 | int sectionTextInterval;
36 | boolean showThumbText;
37 | int thumbTextSize;
38 | int thumbTextColor;
39 | boolean showProgressInFloat;
40 | boolean touchToSeek;
41 | boolean seekBySection;
42 | int bubbleColor;
43 | int bubbleTextSize;
44 | int bubbleTextColor;
45 | boolean alwaysShowBubble;
46 |
47 | private BubbleSeekBar mBubbleSeekBar;
48 |
49 | BubbleConfigBuilder(BubbleSeekBar bubbleSeekBar) {
50 | mBubbleSeekBar = bubbleSeekBar;
51 | }
52 |
53 | public void build() {
54 | mBubbleSeekBar.config(this);
55 | }
56 |
57 | public BubbleConfigBuilder min(float min) {
58 | this.min = min;
59 | this.progress = min;
60 | return this;
61 | }
62 |
63 | public BubbleConfigBuilder max(float max) {
64 | this.max = max;
65 | return this;
66 | }
67 |
68 | public BubbleConfigBuilder progress(float progress) {
69 | this.progress = progress;
70 | return this;
71 | }
72 |
73 | public BubbleConfigBuilder floatType() {
74 | this.floatType = true;
75 | return this;
76 | }
77 |
78 | public BubbleConfigBuilder trackSize(int dp) {
79 | this.trackSize = dp2px(dp);
80 | return this;
81 | }
82 |
83 | public BubbleConfigBuilder secondTrackSize(int dp) {
84 | this.secondTrackSize = dp2px(dp);
85 | return this;
86 | }
87 |
88 | public BubbleConfigBuilder thumbRadius(int dp) {
89 | this.thumbRadius = dp2px(dp);
90 | return this;
91 | }
92 |
93 | public BubbleConfigBuilder thumbRadiusOnDragging(int dp) {
94 | this.thumbRadiusOnDragging = dp2px(dp);
95 | return this;
96 | }
97 |
98 | public BubbleConfigBuilder trackColor(@ColorInt int color) {
99 | this.trackColor = color;
100 | this.sectionTextColor = color;
101 | return this;
102 | }
103 |
104 | public BubbleConfigBuilder secondTrackColor(@ColorInt int color) {
105 | this.secondTrackColor = color;
106 | this.thumbColor = color;
107 | this.thumbTextColor = color;
108 | this.bubbleColor = color;
109 | return this;
110 | }
111 |
112 | public BubbleConfigBuilder thumbColor(@ColorInt int color) {
113 | this.thumbColor = color;
114 | return this;
115 | }
116 |
117 | public BubbleConfigBuilder sectionCount(@IntRange(from = 1) int count) {
118 | this.sectionCount = count;
119 | return this;
120 | }
121 |
122 | public BubbleConfigBuilder showSectionMark() {
123 | this.showSectionMark = true;
124 | return this;
125 | }
126 |
127 | public BubbleConfigBuilder autoAdjustSectionMark() {
128 | this.autoAdjustSectionMark = true;
129 | return this;
130 | }
131 |
132 | public BubbleConfigBuilder showSectionText() {
133 | this.showSectionText = true;
134 | return this;
135 | }
136 |
137 | public BubbleConfigBuilder sectionTextSize(int sp) {
138 | this.sectionTextSize = sp2px(sp);
139 | return this;
140 | }
141 |
142 | public BubbleConfigBuilder sectionTextColor(@ColorInt int color) {
143 | this.sectionTextColor = color;
144 | return this;
145 | }
146 |
147 | public BubbleConfigBuilder sectionTextPosition(@BubbleSeekBar.TextPosition int position) {
148 | this.sectionTextPosition = position;
149 | return this;
150 | }
151 |
152 | public BubbleConfigBuilder sectionTextInterval(@IntRange(from = 1) int interval) {
153 | this.sectionTextInterval = interval;
154 | return this;
155 | }
156 |
157 | public BubbleConfigBuilder showThumbText() {
158 | this.showThumbText = true;
159 | return this;
160 | }
161 |
162 | public BubbleConfigBuilder thumbTextSize(int sp) {
163 | this.thumbTextSize = sp2px(sp);
164 | return this;
165 | }
166 |
167 | public BubbleConfigBuilder thumbTextColor(@ColorInt int color) {
168 | thumbTextColor = color;
169 | return this;
170 | }
171 |
172 | public BubbleConfigBuilder showProgressInFloat() {
173 | this.showProgressInFloat = true;
174 | return this;
175 | }
176 |
177 | public BubbleConfigBuilder touchToSeek() {
178 | this.touchToSeek = true;
179 | return this;
180 | }
181 |
182 | public BubbleConfigBuilder seekBySection() {
183 | this.seekBySection = true;
184 | return this;
185 | }
186 |
187 | public BubbleConfigBuilder bubbleColor(@ColorInt int color) {
188 | this.bubbleColor = color;
189 | return this;
190 | }
191 |
192 | public BubbleConfigBuilder bubbleTextSize(int sp) {
193 | this.bubbleTextSize = sp2px(sp);
194 | return this;
195 | }
196 |
197 | public BubbleConfigBuilder bubbleTextColor(@ColorInt int color) {
198 | this.bubbleTextColor = color;
199 | return this;
200 | }
201 |
202 | public BubbleConfigBuilder alwaysShowBubble() {
203 | this.alwaysShowBubble = true;
204 | return this;
205 | }
206 |
207 | public float getMin() {
208 | return min;
209 | }
210 |
211 | public float getMax() {
212 | return max;
213 | }
214 |
215 | public float getProgress() {
216 | return progress;
217 | }
218 |
219 | public boolean isFloatType() {
220 | return floatType;
221 | }
222 |
223 | public int getTrackSize() {
224 | return trackSize;
225 | }
226 |
227 | public int getSecondTrackSize() {
228 | return secondTrackSize;
229 | }
230 |
231 | public int getThumbRadius() {
232 | return thumbRadius;
233 | }
234 |
235 | public int getThumbRadiusOnDragging() {
236 | return thumbRadiusOnDragging;
237 | }
238 |
239 | public int getTrackColor() {
240 | return trackColor;
241 | }
242 |
243 | public int getSecondTrackColor() {
244 | return secondTrackColor;
245 | }
246 |
247 | public int getThumbColor() {
248 | return thumbColor;
249 | }
250 |
251 | public int getSectionCount() {
252 | return sectionCount;
253 | }
254 |
255 | public boolean isShowSectionMark() {
256 | return showSectionMark;
257 | }
258 |
259 | public boolean isAutoAdjustSectionMark() {
260 | return autoAdjustSectionMark;
261 | }
262 |
263 | public boolean isShowSectionText() {
264 | return showSectionText;
265 | }
266 |
267 | public int getSectionTextSize() {
268 | return sectionTextSize;
269 | }
270 |
271 | public int getSectionTextColor() {
272 | return sectionTextColor;
273 | }
274 |
275 | public int getSectionTextPosition() {
276 | return sectionTextPosition;
277 | }
278 |
279 | public int getSectionTextInterval() {
280 | return sectionTextInterval;
281 | }
282 |
283 | public boolean isShowThumbText() {
284 | return showThumbText;
285 | }
286 |
287 | public int getThumbTextSize() {
288 | return thumbTextSize;
289 | }
290 |
291 | public int getThumbTextColor() {
292 | return thumbTextColor;
293 | }
294 |
295 | public boolean isShowProgressInFloat() {
296 | return showProgressInFloat;
297 | }
298 |
299 | public boolean isTouchToSeek() {
300 | return touchToSeek;
301 | }
302 |
303 | public boolean isSeekBySection() {
304 | return seekBySection;
305 | }
306 |
307 | public int getBubbleColor() {
308 | return bubbleColor;
309 | }
310 |
311 | public int getBubbleTextSize() {
312 | return bubbleTextSize;
313 | }
314 |
315 | public int getBubbleTextColor() {
316 | return bubbleTextColor;
317 | }
318 |
319 | public boolean isAlwaysShowBubble() {
320 | return alwaysShowBubble;
321 | }
322 | }
323 |
--------------------------------------------------------------------------------
/bubbleseekbar/src/main/java/com/xw/repo/BubbleSeekBar.java:
--------------------------------------------------------------------------------
1 | package com.xw.repo;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.animation.AnimatorSet;
6 | import android.animation.ObjectAnimator;
7 | import android.animation.ValueAnimator;
8 | import android.content.Context;
9 | import android.content.res.TypedArray;
10 | import android.graphics.Canvas;
11 | import android.graphics.Color;
12 | import android.graphics.Paint;
13 | import android.graphics.Path;
14 | import android.graphics.PixelFormat;
15 | import android.graphics.Rect;
16 | import android.graphics.RectF;
17 | import android.os.Build;
18 | import android.os.Bundle;
19 | import android.os.Parcelable;
20 | import android.support.annotation.IntDef;
21 | import android.support.annotation.NonNull;
22 | import android.support.v4.content.ContextCompat;
23 | import android.util.AttributeSet;
24 | import android.view.Gravity;
25 | import android.view.MotionEvent;
26 | import android.view.View;
27 | import android.view.ViewGroup;
28 | import android.view.WindowManager;
29 | import android.view.animation.LinearInterpolator;
30 |
31 | import com.xw.repo.bubbleseekbar.R;
32 |
33 | import java.lang.annotation.Retention;
34 | import java.lang.annotation.RetentionPolicy;
35 | import java.math.BigDecimal;
36 |
37 | import static com.xw.repo.BubbleSeekBar.TextPosition.BELOW_SECTION_MARK;
38 | import static com.xw.repo.BubbleSeekBar.TextPosition.BOTTOM_SIDES;
39 | import static com.xw.repo.BubbleSeekBar.TextPosition.SIDES;
40 | import static com.xw.repo.BubbleUtils.dp2px;
41 | import static com.xw.repo.BubbleUtils.sp2px;
42 |
43 | /**
44 | * A beautiful and powerful Android custom seek bar, which has a bubble view with progress
45 | * appearing upon when seeking. Highly customizable, mostly demands has been considered.
46 | *
47 | * Created by woxingxiao on 2016-10-27.
48 | */
49 | public class BubbleSeekBar extends View {
50 |
51 | static final int NONE = -1;
52 |
53 | @IntDef({NONE, SIDES, BOTTOM_SIDES, BELOW_SECTION_MARK})
54 | @Retention(RetentionPolicy.SOURCE)
55 | public @interface TextPosition {
56 | int SIDES = 0, BOTTOM_SIDES = 1, BELOW_SECTION_MARK = 2;
57 | }
58 |
59 | private float mMin; // min
60 | private float mMax; // max
61 | private float mProgress; // real time value
62 | private boolean isFloatType; // support for float type output
63 | private int mTrackSize; // height of right-track(on the right of thumb)
64 | private int mSecondTrackSize; // height of left-track(on the left of thumb)
65 | private int mThumbRadius; // radius of thumb
66 | private int mThumbRadiusOnDragging; // radius of thumb when be dragging
67 | private int mTrackColor; // color of right-track
68 | private int mSecondTrackColor; // color of left-track
69 | private int mThumbColor; // color of thumb
70 | private int mSectionCount; // shares of whole progress(max - min)
71 | private boolean isShowSectionMark; // show demarcation points or not
72 | private boolean isAutoAdjustSectionMark; // auto scroll to the nearest section_mark or not
73 | private boolean isShowSectionText; // show section-text or not
74 | private int mSectionTextSize; // text size of section-text
75 | private int mSectionTextColor; // text color of section-text
76 | @TextPosition
77 | private int mSectionTextPosition = NONE; // text position of section-text relative to track
78 | private int mSectionTextInterval; // the interval of two section-text
79 | private boolean isShowThumbText; // show real time progress-text under thumb or not
80 | private int mThumbTextSize; // text size of progress-text
81 | private int mThumbTextColor; // text color of progress-text
82 | private boolean isShowProgressInFloat; // show bubble-progress in float or not
83 | private boolean isTouchToSeek; // touch anywhere on track to quickly seek
84 | private boolean isSeekBySection; // seek by section, the progress may not be linear
85 | private long mAnimDuration; // duration of animation
86 | private boolean isAlwaysShowBubble; // bubble shows all time
87 |
88 | private int mBubbleColor;// color of bubble
89 | private int mBubbleTextSize; // text size of bubble-progress
90 | private int mBubbleTextColor; // text color of bubble-progress
91 |
92 | private float mDelta; // max - min
93 | private float mSectionValue; // (mDelta / mSectionCount)
94 | private float mThumbCenterX; // X coordinate of thumb's center
95 | private float mTrackLength; // pixel length of whole track
96 | private float mSectionOffset; // pixel length of one section
97 | private boolean isThumbOnDragging; // is thumb on dragging or not
98 | private int mTextSpace; // space between text and track
99 | private boolean triggerBubbleShowing;
100 | private boolean triggerSeekBySection;
101 |
102 | private OnProgressChangedListener mProgressListener; // progress changing listener
103 | private float mLeft; // space between left of track and left of the view
104 | private float mRight; // space between right of track and left of the view
105 | private Paint mPaint;
106 | private Rect mRectText;
107 |
108 | private WindowManager mWindowManager;
109 | private BubbleView mBubbleView;
110 | private int mBubbleRadius;
111 | private float mBubbleCenterRawSolidX;
112 | private float mBubbleCenterRawSolidY;
113 | private float mBubbleCenterRawX;
114 | private WindowManager.LayoutParams mLayoutParams;
115 | private int[] mPoint = new int[2];
116 | private boolean isTouchToSeekAnimEnd = true;
117 | private float mPreSecValue; // previous SectionValue
118 | private BubbleConfigBuilder mConfigBuilder; // config attributes
119 |
120 | public BubbleSeekBar(Context context) {
121 | this(context, null);
122 | }
123 |
124 | public BubbleSeekBar(Context context, AttributeSet attrs) {
125 | this(context, attrs, 0);
126 | }
127 |
128 | public BubbleSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
129 | super(context, attrs, defStyleAttr);
130 |
131 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BubbleSeekBar, defStyleAttr, 0);
132 | mMin = a.getFloat(R.styleable.BubbleSeekBar_bsb_min, 0.0f);
133 | mMax = a.getFloat(R.styleable.BubbleSeekBar_bsb_max, 100.0f);
134 | mProgress = a.getFloat(R.styleable.BubbleSeekBar_bsb_progress, mMin);
135 | isFloatType = a.getBoolean(R.styleable.BubbleSeekBar_bsb_is_float_type, false);
136 | mTrackSize = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_track_size, dp2px(2));
137 | mSecondTrackSize = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_second_track_size,
138 | mTrackSize + dp2px(2));
139 | mThumbRadius = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_thumb_radius,
140 | mSecondTrackSize + dp2px(2));
141 | mThumbRadiusOnDragging = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_thumb_radius,
142 | mSecondTrackSize * 2);
143 | mSectionCount = a.getInteger(R.styleable.BubbleSeekBar_bsb_section_count, 10);
144 | mTrackColor = a.getColor(R.styleable.BubbleSeekBar_bsb_track_color,
145 | ContextCompat.getColor(context, R.color.colorPrimary));
146 | mSecondTrackColor = a.getColor(R.styleable.BubbleSeekBar_bsb_second_track_color,
147 | ContextCompat.getColor(context, R.color.colorAccent));
148 | mThumbColor = a.getColor(R.styleable.BubbleSeekBar_bsb_thumb_color, mSecondTrackColor);
149 | isShowSectionText = a.getBoolean(R.styleable.BubbleSeekBar_bsb_show_section_text, false);
150 | mSectionTextSize = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_section_text_size, sp2px(14));
151 | mSectionTextColor = a.getColor(R.styleable.BubbleSeekBar_bsb_section_text_color, mTrackColor);
152 | isSeekBySection = a.getBoolean(R.styleable.BubbleSeekBar_bsb_seek_by_section, false);
153 | int pos = a.getInteger(R.styleable.BubbleSeekBar_bsb_section_text_position, NONE);
154 | if (pos == 0) {
155 | mSectionTextPosition = SIDES;
156 | } else if (pos == 1) {
157 | mSectionTextPosition = TextPosition.BOTTOM_SIDES;
158 | } else if (pos == 2) {
159 | mSectionTextPosition = TextPosition.BELOW_SECTION_MARK;
160 | } else {
161 | mSectionTextPosition = NONE;
162 | }
163 | mSectionTextInterval = a.getInteger(R.styleable.BubbleSeekBar_bsb_section_text_interval, 1);
164 | isShowThumbText = a.getBoolean(R.styleable.BubbleSeekBar_bsb_show_thumb_text, false);
165 | mThumbTextSize = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_thumb_text_size, sp2px(14));
166 | mThumbTextColor = a.getColor(R.styleable.BubbleSeekBar_bsb_thumb_text_color, mSecondTrackColor);
167 | mBubbleColor = a.getColor(R.styleable.BubbleSeekBar_bsb_bubble_color, mSecondTrackColor);
168 | mBubbleTextSize = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_bubble_text_size, sp2px(14));
169 | mBubbleTextColor = a.getColor(R.styleable.BubbleSeekBar_bsb_bubble_text_color, Color.WHITE);
170 | isShowSectionMark = a.getBoolean(R.styleable.BubbleSeekBar_bsb_show_section_mark, false);
171 | isAutoAdjustSectionMark = a.getBoolean(R.styleable.BubbleSeekBar_bsb_auto_adjust_section_mark, false);
172 | isShowProgressInFloat = a.getBoolean(R.styleable.BubbleSeekBar_bsb_show_progress_in_float, false);
173 | int duration = a.getInteger(R.styleable.BubbleSeekBar_bsb_anim_duration, -1);
174 | mAnimDuration = duration < 0 ? 200 : duration;
175 | isTouchToSeek = a.getBoolean(R.styleable.BubbleSeekBar_bsb_touch_to_seek, false);
176 | isAlwaysShowBubble = a.getBoolean(R.styleable.BubbleSeekBar_bsb_always_show_bubble, false);
177 | a.recycle();
178 |
179 | mPaint = new Paint();
180 | mPaint.setAntiAlias(true);
181 | mPaint.setStrokeCap(Paint.Cap.ROUND);
182 | mPaint.setTextAlign(Paint.Align.CENTER);
183 |
184 | mRectText = new Rect();
185 |
186 | mTextSpace = dp2px(2);
187 | mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
188 |
189 | // init BubbleView
190 | mBubbleView = new BubbleView(context);
191 | mBubbleView.setProgressText(isShowProgressInFloat ?
192 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
193 |
194 | initConfigByPriority();
195 | calculateRadiusOfBubble();
196 | }
197 |
198 | private void initConfigByPriority() {
199 | if (mMin == mMax) {
200 | mMin = 0.0f;
201 | mMax = 100.0f;
202 | }
203 | if (mMin > mMax) {
204 | float tmp = mMax;
205 | mMax = mMin;
206 | mMin = tmp;
207 | }
208 | if (mProgress < mMin) {
209 | mProgress = mMin;
210 | }
211 | if (mProgress > mMax) {
212 | mProgress = mMax;
213 | }
214 | if (mSecondTrackSize < mTrackSize) {
215 | mSecondTrackSize = mTrackSize + dp2px(2);
216 | }
217 | if (mThumbRadius <= mSecondTrackSize) {
218 | mThumbRadius = mSecondTrackSize + dp2px(2);
219 | }
220 | if (mThumbRadiusOnDragging <= mSecondTrackSize) {
221 | mThumbRadiusOnDragging = mSecondTrackSize * 2;
222 | }
223 | if (mSectionCount <= 0) {
224 | mSectionCount = 10;
225 | }
226 | mDelta = mMax - mMin;
227 | mSectionValue = mDelta / mSectionCount;
228 |
229 | if (mSectionValue < 1) {
230 | isFloatType = true;
231 | }
232 | if (isFloatType) {
233 | isShowProgressInFloat = true;
234 | }
235 | if (mSectionTextPosition != NONE) {
236 | isShowSectionText = true;
237 | }
238 | if (isShowSectionText) {
239 | if (mSectionTextPosition == NONE) {
240 | mSectionTextPosition = TextPosition.SIDES;
241 | }
242 | if (mSectionTextPosition == TextPosition.BELOW_SECTION_MARK) {
243 | isShowSectionMark = true;
244 | }
245 | }
246 | if (mSectionTextInterval < 1) {
247 | mSectionTextInterval = 1;
248 | }
249 | if (isAutoAdjustSectionMark && !isShowSectionMark) {
250 | isAutoAdjustSectionMark = false;
251 | }
252 | if (isSeekBySection) {
253 | mPreSecValue = mMin;
254 | if (mProgress != mMin) {
255 | mPreSecValue = mSectionValue;
256 | }
257 | isShowSectionMark = true;
258 | isAutoAdjustSectionMark = true;
259 | isTouchToSeek = false;
260 | }
261 | if (isAlwaysShowBubble) {
262 | setProgress(mProgress);
263 | }
264 |
265 | mThumbTextSize = isFloatType || isSeekBySection || (isShowSectionText && mSectionTextPosition ==
266 | TextPosition.BELOW_SECTION_MARK) ? mSectionTextSize : mThumbTextSize;
267 | }
268 |
269 | /**
270 | * Calculate radius of bubble according to the Min and the Max
271 | */
272 | private void calculateRadiusOfBubble() {
273 | mPaint.setTextSize(mBubbleTextSize);
274 |
275 | // 计算滑到两端气泡里文字需要显示的宽度,比较取最大值为气泡的半径
276 | String text;
277 | if (isShowProgressInFloat) {
278 | text = float2String(mMin);
279 | } else {
280 | text = getMinText();
281 | }
282 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
283 | int w1 = (mRectText.width() + mTextSpace * 2) >> 1;
284 |
285 | if (isShowProgressInFloat) {
286 | text = float2String(mMax);
287 | } else {
288 | text = getMaxText();
289 | }
290 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
291 | int w2 = (mRectText.width() + mTextSpace * 2) >> 1;
292 |
293 | mBubbleRadius = dp2px(14); // default 14dp
294 | int max = Math.max(mBubbleRadius, Math.max(w1, w2));
295 | mBubbleRadius = max + mTextSpace;
296 | }
297 |
298 | @Override
299 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
300 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
301 |
302 | int height = mThumbRadiusOnDragging * 2; // 默认高度为拖动时thumb圆的直径
303 | if (isShowThumbText) {
304 | mPaint.setTextSize(mThumbTextSize);
305 | mPaint.getTextBounds("j", 0, 1, mRectText); // “j”是字母和阿拉伯数字中最高的
306 | height += mRectText.height() + mTextSpace; // 如果显示实时进度,则原来基础上加上进度文字高度和间隔
307 | }
308 | if (isShowSectionText && mSectionTextPosition >= TextPosition.BOTTOM_SIDES) { // 如果Section值在track之下显示,比较取较大值
309 | mPaint.setTextSize(mSectionTextSize);
310 | mPaint.getTextBounds("j", 0, 1, mRectText);
311 | height = Math.max(height, mThumbRadiusOnDragging * 2 + mRectText.height() + mTextSpace);
312 | }
313 | setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec), height);
314 |
315 | mLeft = getPaddingLeft() + mThumbRadiusOnDragging;
316 | mRight = getMeasuredWidth() - getPaddingRight() - mThumbRadiusOnDragging;
317 |
318 | if (isShowSectionText) {
319 | mPaint.setTextSize(mSectionTextSize);
320 |
321 | if (mSectionTextPosition == TextPosition.SIDES) {
322 | String text = getMinText();
323 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
324 | mLeft += (mRectText.width() + mTextSpace);
325 |
326 | text = getMaxText();
327 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
328 | mRight -= (mRectText.width() + mTextSpace);
329 | } else if (mSectionTextPosition >= TextPosition.BOTTOM_SIDES) {
330 | String text = getMinText();
331 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
332 | float max = Math.max(mThumbRadiusOnDragging, mRectText.width() / 2f);
333 | mLeft = getPaddingLeft() + max + mTextSpace;
334 |
335 | text = getMaxText();
336 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
337 | max = Math.max(mThumbRadiusOnDragging, mRectText.width() / 2f);
338 | mRight = getMeasuredWidth() - getPaddingRight() - max - mTextSpace;
339 | }
340 | } else if (isShowThumbText && mSectionTextPosition == NONE) {
341 | mPaint.setTextSize(mThumbTextSize);
342 |
343 | String text = getMinText();
344 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
345 | float max = Math.max(mThumbRadiusOnDragging, mRectText.width() / 2f);
346 | mLeft = getPaddingLeft() + max + mTextSpace;
347 |
348 | text = getMaxText();
349 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
350 | max = Math.max(mThumbRadiusOnDragging, mRectText.width() / 2f);
351 | mRight = getMeasuredWidth() - getPaddingRight() - max - mTextSpace;
352 | }
353 |
354 | mTrackLength = mRight - mLeft;
355 | mSectionOffset = mTrackLength * 1f / mSectionCount;
356 |
357 | mBubbleView.measure(widthMeasureSpec, heightMeasureSpec);
358 |
359 | locatePositionOnScreen();
360 | }
361 |
362 | /**
363 | * In fact there two parts of the BubbleSeeBar, they are the BubbleView and the SeekBar.
364 | *
365 | * The BubbleView is added to Window by the WindowManager, so the only connection between
366 | * BubbleView and SeekBar is their origin raw coordinates on the screen.
367 | *
368 | * It's easy to compute the coordinates(mBubbleCenterRawSolidX, mBubbleCenterRawSolidY) of point
369 | * when the Progress equals the Min. Then compute the pixel length increment when the Progress is
370 | * changing, the result is mBubbleCenterRawX. At last the WindowManager calls updateViewLayout()
371 | * to update the LayoutParameter.x of the BubbleView.
372 | *
373 | * 气泡BubbleView实际是通过WindowManager动态添加的一个视图,因此与SeekBar唯一的位置联系就是它们在屏幕上的
374 | * 绝对坐标。
375 | * 先计算进度mProgress为mMin时BubbleView的中心坐标(mBubbleCenterRawSolidX,mBubbleCenterRawSolidY),
376 | * 然后根据进度来增量计算横坐标mBubbleCenterRawX,再动态设置LayoutParameter.x,就实现了气泡跟随滑动移动。
377 | */
378 | private void locatePositionOnScreen() {
379 | getLocationOnScreen(mPoint);
380 |
381 | mBubbleCenterRawSolidX = mPoint[0] + mLeft - mBubbleView.getMeasuredWidth() / 2f;
382 | mBubbleCenterRawX = mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta;
383 | mBubbleCenterRawSolidY = mPoint[1] - mBubbleView.getMeasuredHeight();
384 | mBubbleCenterRawSolidY -= dp2px(24);
385 | if (BubbleUtils.isMIUI()) {
386 | mBubbleCenterRawSolidY += dp2px(4);
387 | }
388 | }
389 |
390 | @Override
391 | protected void onDraw(Canvas canvas) {
392 | super.onDraw(canvas);
393 |
394 | float xLeft = getPaddingLeft();
395 | float xRight = getMeasuredWidth() - getPaddingRight();
396 | float yTop = getPaddingTop() + mThumbRadiusOnDragging;
397 |
398 | // draw sectionText SIDES or BOTTOM_SIDES
399 | if (isShowSectionText) {
400 | mPaint.setTextSize(mSectionTextSize);
401 | mPaint.setColor(mSectionTextColor);
402 |
403 | if (mSectionTextPosition == TextPosition.SIDES) {
404 | float y_ = yTop + mRectText.height() / 2f;
405 |
406 | String text = getMinText();
407 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
408 | canvas.drawText(text, xLeft + mRectText.width() / 2f, y_, mPaint);
409 | xLeft += mRectText.width() + mTextSpace;
410 |
411 | text = getMaxText();
412 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
413 | canvas.drawText(text, xRight - mRectText.width() / 2f, y_, mPaint);
414 | xRight -= (mRectText.width() + mTextSpace);
415 |
416 | } else if (mSectionTextPosition >= TextPosition.BOTTOM_SIDES) {
417 | float y_ = yTop + mThumbRadiusOnDragging + mTextSpace;
418 |
419 | String text = getMinText();
420 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
421 | y_ += mRectText.height();
422 | xLeft = mLeft;
423 | if (mSectionTextPosition == TextPosition.BOTTOM_SIDES) {
424 | canvas.drawText(text, xLeft, y_, mPaint);
425 | }
426 |
427 | text = getMaxText();
428 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
429 | xRight = mRight;
430 | if (mSectionTextPosition == TextPosition.BOTTOM_SIDES) {
431 | canvas.drawText(text, xRight, y_, mPaint);
432 | }
433 | }
434 | } else if (isShowThumbText && mSectionTextPosition == NONE) {
435 | xLeft = mLeft;
436 | xRight = mRight;
437 | }
438 |
439 | if ((!isShowSectionText && !isShowThumbText) || mSectionTextPosition == TextPosition.SIDES) {
440 | xLeft += mThumbRadiusOnDragging;
441 | xRight -= mThumbRadiusOnDragging;
442 | }
443 |
444 | boolean isShowTextBelowSectionMark = isShowSectionText && mSectionTextPosition ==
445 | TextPosition.BELOW_SECTION_MARK;
446 | boolean conditionInterval = mSectionCount % 2 == 0;
447 |
448 | // draw sectionMark & sectionText BELOW_SECTION_MARK
449 | if (isShowTextBelowSectionMark || isShowSectionMark) {
450 | float r = (mThumbRadiusOnDragging - dp2px(2)) / 2f;
451 | float junction = mTrackLength / mDelta * Math.abs(mProgress - mMin) + mLeft; // 交汇点
452 | mPaint.setTextSize(mSectionTextSize);
453 | mPaint.getTextBounds("0123456789", 0, "0123456789".length(), mRectText); // compute solid height
454 |
455 | float x_;
456 | float y_ = yTop + mRectText.height() + mThumbRadiusOnDragging + mTextSpace;
457 |
458 | for (int i = 0; i <= mSectionCount; i++) {
459 | x_ = xLeft + i * mSectionOffset;
460 | mPaint.setColor(x_ <= junction ? mSecondTrackColor : mTrackColor);
461 | // sectionMark
462 | canvas.drawCircle(x_, yTop, r, mPaint);
463 |
464 | // sectionText belows section
465 | if (isShowTextBelowSectionMark) {
466 | mPaint.setColor(mSectionTextColor);
467 |
468 | if (mSectionTextInterval > 1) {
469 | if (conditionInterval && i % mSectionTextInterval == 0) {
470 | float m = mMin + mSectionValue * i;
471 | canvas.drawText(isFloatType ? float2String(m) : (int) m + "", x_, y_, mPaint);
472 | }
473 | } else {
474 | float m = mMin + mSectionValue * i;
475 | canvas.drawText(isFloatType ? float2String(m) : (int) m + "", x_, y_, mPaint);
476 | }
477 | }
478 | }
479 | }
480 |
481 | if (!isThumbOnDragging || isAlwaysShowBubble) {
482 | mThumbCenterX = mTrackLength / mDelta * (mProgress - mMin) + xLeft;
483 | }
484 |
485 | // draw thumbText
486 | if (isShowThumbText && !isThumbOnDragging && isTouchToSeekAnimEnd) {
487 | mPaint.setColor(mThumbTextColor);
488 | mPaint.setTextSize(mThumbTextSize);
489 | mPaint.getTextBounds("0123456789", 0, "0123456789".length(), mRectText); // compute solid height
490 | float y_ = yTop + mRectText.height() + mThumbRadiusOnDragging + mTextSpace;
491 |
492 | if (isFloatType || (isShowProgressInFloat && mSectionTextPosition == TextPosition.BOTTOM_SIDES &&
493 | mProgress != mMin && mProgress != mMax)) {
494 | canvas.drawText(String.valueOf(getProgressFloat()), mThumbCenterX, y_, mPaint);
495 | } else {
496 | canvas.drawText(String.valueOf(getProgress()), mThumbCenterX, y_, mPaint);
497 | }
498 | }
499 |
500 | // draw track
501 | mPaint.setColor(mSecondTrackColor);
502 | mPaint.setStrokeWidth(mSecondTrackSize);
503 | canvas.drawLine(xLeft, yTop, mThumbCenterX, yTop, mPaint);
504 |
505 | // draw second track
506 | mPaint.setColor(mTrackColor);
507 | mPaint.setStrokeWidth(mTrackSize);
508 | canvas.drawLine(mThumbCenterX, yTop, xRight, yTop, mPaint);
509 |
510 | // draw thumb
511 | mPaint.setColor(mThumbColor);
512 | canvas.drawCircle(mThumbCenterX, yTop, isThumbOnDragging ? mThumbRadiusOnDragging : mThumbRadius, mPaint);
513 | }
514 |
515 | @Override
516 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
517 | super.onSizeChanged(w, h, oldw, oldh);
518 |
519 | post(new Runnable() {
520 | @Override
521 | public void run() {
522 | requestLayout();
523 | }
524 | });
525 | }
526 |
527 | @Override
528 | protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
529 | if (!isAlwaysShowBubble)
530 | return;
531 |
532 | if (visibility != VISIBLE) {
533 | hideBubble();
534 | } else {
535 | if (triggerBubbleShowing) {
536 | showBubble();
537 | }
538 | }
539 | super.onVisibilityChanged(changedView, visibility);
540 | }
541 |
542 | @Override
543 | protected void onDetachedFromWindow() {
544 | hideBubble();
545 | mBubbleView = null;
546 | super.onDetachedFromWindow();
547 | }
548 |
549 | float dx;
550 |
551 | @Override
552 | public boolean onTouchEvent(MotionEvent event) {
553 | switch (event.getActionMasked()) {
554 | case MotionEvent.ACTION_DOWN:
555 | isThumbOnDragging = isThumbTouched(event);
556 | if (isThumbOnDragging) {
557 | if (isSeekBySection && !triggerSeekBySection) {
558 | triggerSeekBySection = true;
559 | }
560 | if (isAlwaysShowBubble && !triggerBubbleShowing) {
561 | triggerBubbleShowing = true;
562 | }
563 | showBubble();
564 | invalidate();
565 | } else if (isTouchToSeek && isTrackTouched(event)) {
566 | if (isAlwaysShowBubble) {
567 | hideBubble();
568 | triggerBubbleShowing = true;
569 | }
570 |
571 | mThumbCenterX = event.getX();
572 | if (mThumbCenterX < mLeft) {
573 | mThumbCenterX = mLeft;
574 | }
575 | if (mThumbCenterX > mRight) {
576 | mThumbCenterX = mRight;
577 | }
578 | mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
579 | mBubbleCenterRawX = mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta;
580 |
581 | showBubble();
582 | invalidate();
583 | }
584 |
585 | dx = mThumbCenterX - event.getX();
586 |
587 | break;
588 | case MotionEvent.ACTION_MOVE:
589 | if (isThumbOnDragging) {
590 | mThumbCenterX = event.getX() + dx;
591 | if (mThumbCenterX < mLeft) {
592 | mThumbCenterX = mLeft;
593 | }
594 | if (mThumbCenterX > mRight) {
595 | mThumbCenterX = mRight;
596 | }
597 | mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
598 |
599 | mBubbleCenterRawX = mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta;
600 | mLayoutParams.x = (int) (mBubbleCenterRawX + 0.5f);
601 | mWindowManager.updateViewLayout(mBubbleView, mLayoutParams);
602 | mBubbleView.setProgressText(isShowProgressInFloat ?
603 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
604 |
605 | invalidate();
606 |
607 | if (mProgressListener != null) {
608 | mProgressListener.onProgressChanged(getProgress(), getProgressFloat());
609 | }
610 | }
611 |
612 | break;
613 | case MotionEvent.ACTION_UP:
614 | case MotionEvent.ACTION_CANCEL:
615 | if (isAutoAdjustSectionMark) {
616 | if (isTouchToSeek) {
617 | mBubbleView.postDelayed(new Runnable() {
618 | @Override
619 | public void run() {
620 | isTouchToSeekAnimEnd = false;
621 | autoAdjustSection();
622 | }
623 | }, isThumbOnDragging ? 0 : 300);
624 | } else {
625 | autoAdjustSection();
626 | }
627 | } else if (isThumbOnDragging || isTouchToSeek) {
628 | mBubbleView.postDelayed(new Runnable() {
629 | @Override
630 | public void run() {
631 | mBubbleView.animate()
632 | .alpha(isAlwaysShowBubble ? 1f : 0f)
633 | .setDuration(mAnimDuration)
634 | .setListener(new AnimatorListenerAdapter() {
635 | @Override
636 | public void onAnimationEnd(Animator animation) {
637 | if (!isAlwaysShowBubble) {
638 | hideBubble();
639 | }
640 |
641 | isThumbOnDragging = false;
642 | invalidate();
643 |
644 | if (mProgressListener != null) {
645 | mProgressListener.onProgressChanged(getProgress(),
646 | getProgressFloat());
647 | }
648 | }
649 |
650 | @Override
651 | public void onAnimationCancel(Animator animation) {
652 | if (!isAlwaysShowBubble) {
653 | hideBubble();
654 | }
655 |
656 | isThumbOnDragging = false;
657 | invalidate();
658 | }
659 | })
660 | .start();
661 |
662 | }
663 | }, !isThumbOnDragging && isTouchToSeek ? 300 : 0);
664 | }
665 |
666 | if (mProgressListener != null) {
667 | mProgressListener.getProgressOnActionUp(getProgress(), getProgressFloat());
668 | }
669 |
670 | break;
671 | }
672 |
673 | return isThumbOnDragging || isTouchToSeek || super.onTouchEvent(event);
674 | }
675 |
676 | /**
677 | * Detect effective touch of thumb
678 | */
679 | private boolean isThumbTouched(MotionEvent event) {
680 | if (!isEnabled())
681 | return false;
682 |
683 | float x = mTrackLength / mDelta * (mProgress - mMin) + mLeft;
684 | float y = getMeasuredHeight() / 2f;
685 | return (event.getX() - x) * (event.getX() - x) + (event.getY() - y) * (event.getY() - y)
686 | <= (mLeft + dp2px(8)) * (mLeft + dp2px(8));
687 | }
688 |
689 | /**
690 | * Detect effective touch of track
691 | */
692 | private boolean isTrackTouched(MotionEvent event) {
693 | if (!isEnabled())
694 | return false;
695 |
696 | return event.getX() >= getPaddingLeft() && event.getX() <= getMeasuredWidth() - getPaddingRight()
697 | && event.getY() >= getPaddingTop() && event.getY() <= getPaddingTop() + mThumbRadiusOnDragging * 2;
698 | }
699 |
700 | /**
701 | * Showing the Bubble depends the way that the WindowManager adds a Toast type view to the Window.
702 | *
703 | * 显示气泡
704 | * 原理是利用WindowManager动态添加一个与Toast相同类型的BubbleView,消失时再移除
705 | */
706 | private void showBubble() {
707 | if (mBubbleView == null || mBubbleView.getParent() != null) {
708 | return;
709 | }
710 |
711 | if (mLayoutParams == null) {
712 | mLayoutParams = new WindowManager.LayoutParams();
713 | mLayoutParams.gravity = Gravity.START | Gravity.TOP;
714 | mLayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
715 | mLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
716 | mLayoutParams.format = PixelFormat.TRANSLUCENT;
717 | mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
718 | | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
719 | | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
720 | // MIUI禁止了开发者使用TYPE_TOAST,Android 7.1.1 对TYPE_TOAST的使用更严格
721 | if (BubbleUtils.isMIUI() || Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
722 | mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
723 | } else {
724 | mLayoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
725 | }
726 | }
727 | mLayoutParams.x = (int) (mBubbleCenterRawX + 0.5f);
728 | mLayoutParams.y = (int) (mBubbleCenterRawSolidY + 0.5f);
729 |
730 | mBubbleView.setAlpha(0);
731 | mBubbleView.setVisibility(VISIBLE);
732 | mBubbleView.animate().alpha(1f).setDuration(mAnimDuration)
733 | .setListener(new AnimatorListenerAdapter() {
734 | @Override
735 | public void onAnimationStart(Animator animation) {
736 | mWindowManager.addView(mBubbleView, mLayoutParams);
737 | }
738 | }).start();
739 | mBubbleView.setProgressText(isShowProgressInFloat ?
740 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
741 | }
742 |
743 | /**
744 | * Auto scroll to the nearest section mark
745 | */
746 | private void autoAdjustSection() {
747 | int i;
748 | float x = 0;
749 | for (i = 0; i <= mSectionCount; i++) {
750 | x = i * mSectionOffset + mLeft;
751 | if (x <= mThumbCenterX && mThumbCenterX - x <= mSectionOffset) {
752 | break;
753 | }
754 | }
755 |
756 | BigDecimal bigDecimal = BigDecimal.valueOf(mThumbCenterX);
757 | float x_ = bigDecimal.setScale(1, BigDecimal.ROUND_HALF_UP).floatValue();
758 | boolean onSection = x_ == x; // 就在section处,不作valueAnim,优化性能
759 |
760 | AnimatorSet animatorSet = new AnimatorSet();
761 |
762 | ValueAnimator valueAnim = null;
763 | if (!onSection) {
764 | if (mThumbCenterX - x <= mSectionOffset / 2f) {
765 | valueAnim = ValueAnimator.ofFloat(mThumbCenterX, x);
766 | } else {
767 | valueAnim = ValueAnimator.ofFloat(mThumbCenterX, (i + 1) * mSectionOffset + mLeft);
768 | }
769 | valueAnim.setInterpolator(new LinearInterpolator());
770 | valueAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
771 | @Override
772 | public void onAnimationUpdate(ValueAnimator animation) {
773 | mThumbCenterX = (float) animation.getAnimatedValue();
774 | mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
775 |
776 | mBubbleCenterRawX = mBubbleCenterRawSolidX + mThumbCenterX - mLeft;
777 | mLayoutParams.x = (int) (mBubbleCenterRawX + 0.5f);
778 | if (mBubbleView.getParent() != null) {
779 | mWindowManager.updateViewLayout(mBubbleView, mLayoutParams);
780 | }
781 | mBubbleView.setProgressText(isShowProgressInFloat ?
782 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
783 |
784 | invalidate();
785 |
786 | if (mProgressListener != null) {
787 | mProgressListener.onProgressChanged(getProgress(), getProgressFloat());
788 | }
789 | }
790 | });
791 | }
792 |
793 | ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mBubbleView, View.ALPHA, isAlwaysShowBubble ? 1 : 0);
794 |
795 | if (onSection) {
796 | animatorSet.setDuration(mAnimDuration).play(alphaAnim);
797 | } else {
798 | animatorSet.setDuration(mAnimDuration).playTogether(valueAnim, alphaAnim);
799 | }
800 | animatorSet.addListener(new AnimatorListenerAdapter() {
801 | @Override
802 | public void onAnimationEnd(Animator animation) {
803 | if (!isAlwaysShowBubble) {
804 | hideBubble();
805 | }
806 |
807 | mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
808 | isThumbOnDragging = false;
809 | isTouchToSeekAnimEnd = true;
810 | invalidate();
811 |
812 | if (mProgressListener != null) {
813 | mProgressListener.getProgressOnFinally(getProgress(), getProgressFloat());
814 | }
815 | }
816 |
817 | @Override
818 | public void onAnimationCancel(Animator animation) {
819 | if (!isAlwaysShowBubble) {
820 | hideBubble();
821 | }
822 |
823 | mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
824 | isThumbOnDragging = false;
825 | isTouchToSeekAnimEnd = true;
826 | invalidate();
827 | }
828 | });
829 | animatorSet.start();
830 | }
831 |
832 | /**
833 | * The WindowManager removes the BubbleView from the Window.
834 | */
835 | private void hideBubble() {
836 | mBubbleView.setVisibility(GONE); // 防闪烁
837 | if (mBubbleView.getParent() != null) {
838 | mWindowManager.removeViewImmediate(mBubbleView);
839 | }
840 | }
841 |
842 | /**
843 | * When BubbleSeekBar's parent view is scrollable, must listener to it's scrolling and call this
844 | * method to correct the offsets.
845 | */
846 | public void correctOffsetWhenContainerOnScrolling() {
847 | locatePositionOnScreen();
848 |
849 | if (mBubbleView.getParent() != null) {
850 | postInvalidate();
851 | }
852 | }
853 |
854 | private String getMinText() {
855 | return isFloatType ? float2String(mMin) : String.valueOf((int) mMin);
856 | }
857 |
858 | private String getMaxText() {
859 | return isFloatType ? float2String(mMax) : String.valueOf((int) mMax);
860 | }
861 |
862 | public float getMin() {
863 | return mMin;
864 | }
865 |
866 | public float getMax() {
867 | return mMax;
868 | }
869 |
870 | public void setProgress(float progress) {
871 | mProgress = progress;
872 |
873 | mBubbleCenterRawX = mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta;
874 |
875 | if (mProgressListener != null) {
876 | mProgressListener.onProgressChanged(getProgress(), getProgressFloat());
877 | mProgressListener.getProgressOnFinally(getProgress(), getProgressFloat());
878 | }
879 | if (isAlwaysShowBubble) {
880 | hideBubble();
881 |
882 | int[] location = new int[2];
883 | getLocationOnScreen(location);
884 | postDelayed(new Runnable() {
885 | @Override
886 | public void run() {
887 | showBubble();
888 | triggerBubbleShowing = true;
889 | }
890 | }, location[0] == 0 && location[1] == 0 ? 200 : 0);
891 | }
892 |
893 | postInvalidate();
894 | }
895 |
896 | public int getProgress() {
897 | if (isSeekBySection && triggerSeekBySection) {
898 | float half = mSectionValue / 2;
899 |
900 | if (mProgress >= mPreSecValue) { // increasing
901 | if (mProgress >= mPreSecValue + half) {
902 | mPreSecValue += mSectionValue;
903 | return Math.round(mPreSecValue);
904 | } else {
905 | return Math.round(mPreSecValue);
906 | }
907 | } else { // reducing
908 | if (mProgress >= mPreSecValue - half) {
909 | return Math.round(mPreSecValue);
910 | } else {
911 | mPreSecValue -= mSectionValue;
912 | return Math.round(mPreSecValue);
913 | }
914 | }
915 | }
916 |
917 | return Math.round(mProgress);
918 | }
919 |
920 | public float getProgressFloat() {
921 | return formatFloat(mProgress);
922 | }
923 |
924 | public OnProgressChangedListener getOnProgressChangedListener() {
925 | return mProgressListener;
926 | }
927 |
928 | public void setOnProgressChangedListener(OnProgressChangedListener onProgressChangedListener) {
929 | mProgressListener = onProgressChangedListener;
930 | }
931 |
932 | void config(BubbleConfigBuilder builder) {
933 | mMin = builder.min;
934 | mMax = builder.max;
935 | mProgress = builder.progress;
936 | isFloatType = builder.floatType;
937 | mTrackSize = builder.trackSize;
938 | mSecondTrackSize = builder.secondTrackSize;
939 | mThumbRadius = builder.thumbRadius;
940 | mThumbRadiusOnDragging = builder.thumbRadiusOnDragging;
941 | mTrackColor = builder.trackColor;
942 | mSecondTrackColor = builder.secondTrackColor;
943 | mThumbColor = builder.thumbColor;
944 | mSectionCount = builder.sectionCount;
945 | isShowSectionMark = builder.showSectionMark;
946 | isAutoAdjustSectionMark = builder.autoAdjustSectionMark;
947 | isShowSectionText = builder.showSectionText;
948 | mSectionTextSize = builder.sectionTextSize;
949 | mSectionTextColor = builder.sectionTextColor;
950 | mSectionTextPosition = builder.sectionTextPosition;
951 | mSectionTextInterval = builder.sectionTextInterval;
952 | isShowThumbText = builder.showThumbText;
953 | mThumbTextSize = builder.thumbTextSize;
954 | mThumbTextColor = builder.thumbTextColor;
955 | isShowProgressInFloat = builder.showProgressInFloat;
956 | isTouchToSeek = builder.touchToSeek;
957 | isSeekBySection = builder.seekBySection;
958 | mBubbleColor = builder.bubbleColor;
959 | mBubbleTextSize = builder.bubbleTextSize;
960 | mBubbleTextColor = builder.bubbleTextColor;
961 | isAlwaysShowBubble = builder.alwaysShowBubble;
962 |
963 | initConfigByPriority();
964 | calculateRadiusOfBubble();
965 |
966 | if (mProgressListener != null) {
967 | mProgressListener.onProgressChanged(getProgress(), getProgressFloat());
968 | mProgressListener.getProgressOnFinally(getProgress(), getProgressFloat());
969 | }
970 |
971 | mConfigBuilder = null;
972 |
973 | requestLayout();
974 | }
975 |
976 | public BubbleConfigBuilder getConfigBuilder() {
977 | if (mConfigBuilder == null) {
978 | mConfigBuilder = new BubbleConfigBuilder(this);
979 | }
980 |
981 | mConfigBuilder.min = mMin;
982 | mConfigBuilder.max = mMax;
983 | mConfigBuilder.progress = mProgress;
984 | mConfigBuilder.floatType = isFloatType;
985 | mConfigBuilder.trackSize = mTrackSize;
986 | mConfigBuilder.secondTrackSize = mSecondTrackSize;
987 | mConfigBuilder.thumbRadius = mThumbRadius;
988 | mConfigBuilder.thumbRadiusOnDragging = mThumbRadiusOnDragging;
989 | mConfigBuilder.trackColor = mTrackColor;
990 | mConfigBuilder.secondTrackColor = mSecondTrackColor;
991 | mConfigBuilder.thumbColor = mThumbColor;
992 | mConfigBuilder.sectionCount = mSectionCount;
993 | mConfigBuilder.showSectionMark = isShowSectionMark;
994 | mConfigBuilder.autoAdjustSectionMark = isAutoAdjustSectionMark;
995 | mConfigBuilder.showSectionText = isShowSectionText;
996 | mConfigBuilder.sectionTextSize = mSectionTextSize;
997 | mConfigBuilder.sectionTextColor = mSectionTextColor;
998 | mConfigBuilder.sectionTextPosition = mSectionTextPosition;
999 | mConfigBuilder.sectionTextInterval = mSectionTextInterval;
1000 | mConfigBuilder.showThumbText = isShowThumbText;
1001 | mConfigBuilder.thumbTextSize = mThumbTextSize;
1002 | mConfigBuilder.thumbTextColor = mThumbTextColor;
1003 | mConfigBuilder.showProgressInFloat = isShowProgressInFloat;
1004 | mConfigBuilder.touchToSeek = isTouchToSeek;
1005 | mConfigBuilder.seekBySection = isSeekBySection;
1006 | mConfigBuilder.bubbleColor = mBubbleColor;
1007 | mConfigBuilder.bubbleTextSize = mBubbleTextSize;
1008 | mConfigBuilder.bubbleTextColor = mBubbleTextColor;
1009 | mConfigBuilder.alwaysShowBubble = isAlwaysShowBubble;
1010 |
1011 | return mConfigBuilder;
1012 | }
1013 |
1014 | @Override
1015 | protected Parcelable onSaveInstanceState() {
1016 | Bundle bundle = new Bundle();
1017 | bundle.putParcelable("save_instance", super.onSaveInstanceState());
1018 | bundle.putFloat("progress", mProgress);
1019 |
1020 | return bundle;
1021 | }
1022 |
1023 | @Override
1024 | protected void onRestoreInstanceState(Parcelable state) {
1025 | if (state instanceof Bundle) {
1026 | Bundle bundle = (Bundle) state;
1027 | mProgress = bundle.getFloat("progress");
1028 | super.onRestoreInstanceState(bundle.getParcelable("save_instance"));
1029 | mBubbleView.setProgressText(isShowProgressInFloat ?
1030 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
1031 | if (isAlwaysShowBubble) {
1032 | setProgress(mProgress);
1033 | }
1034 |
1035 | return;
1036 | }
1037 |
1038 | super.onRestoreInstanceState(state);
1039 | }
1040 |
1041 | private String float2String(float value) {
1042 | return String.valueOf(formatFloat(value));
1043 | }
1044 |
1045 | private float formatFloat(float value) {
1046 | BigDecimal bigDecimal = BigDecimal.valueOf(value);
1047 | return bigDecimal.setScale(1, BigDecimal.ROUND_HALF_UP).floatValue();
1048 | }
1049 |
1050 | /**
1051 | * Listen to progress onChanged, onActionUp, onFinally
1052 | */
1053 | public interface OnProgressChangedListener {
1054 |
1055 | void onProgressChanged(int progress, float progressFloat);
1056 |
1057 | void getProgressOnActionUp(int progress, float progressFloat);
1058 |
1059 | void getProgressOnFinally(int progress, float progressFloat);
1060 | }
1061 |
1062 | /**
1063 | * Listener adapter
1064 | *
1065 | * usage like {@link AnimatorListenerAdapter}
1066 | */
1067 | public static abstract class OnProgressChangedListenerAdapter implements OnProgressChangedListener {
1068 |
1069 | @Override
1070 | public void onProgressChanged(int progress, float progressFloat) {
1071 | }
1072 |
1073 | @Override
1074 | public void getProgressOnActionUp(int progress, float progressFloat) {
1075 | }
1076 |
1077 | @Override
1078 | public void getProgressOnFinally(int progress, float progressFloat) {
1079 | }
1080 | }
1081 |
1082 | /*******************************************************************************************
1083 | * ************************************ custom bubble view ************************************
1084 | *******************************************************************************************/
1085 | private class BubbleView extends View {
1086 |
1087 | private Paint mBubblePaint;
1088 | private Path mBubblePath;
1089 | private RectF mBubbleRectF;
1090 | private Rect mRect;
1091 | private String mProgressText = "";
1092 |
1093 | BubbleView(Context context) {
1094 | this(context, null);
1095 | }
1096 |
1097 | BubbleView(Context context, AttributeSet attrs) {
1098 | this(context, attrs, 0);
1099 | }
1100 |
1101 | BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
1102 | super(context, attrs, defStyleAttr);
1103 |
1104 | mBubblePaint = new Paint();
1105 | mBubblePaint.setAntiAlias(true);
1106 | mBubblePaint.setTextAlign(Paint.Align.CENTER);
1107 |
1108 | mBubblePath = new Path();
1109 | mBubbleRectF = new RectF();
1110 | mRect = new Rect();
1111 | }
1112 |
1113 | @Override
1114 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1115 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1116 |
1117 | setMeasuredDimension(3 * mBubbleRadius, 3 * mBubbleRadius);
1118 |
1119 | mBubbleRectF.set(getMeasuredWidth() / 2f - mBubbleRadius, 0,
1120 | getMeasuredWidth() / 2f + mBubbleRadius, 2 * mBubbleRadius);
1121 | }
1122 |
1123 | @Override
1124 | protected void onDraw(Canvas canvas) {
1125 | super.onDraw(canvas);
1126 |
1127 | mBubblePath.reset();
1128 | float x0 = getMeasuredWidth() / 2f;
1129 | float y0 = getMeasuredHeight() - mBubbleRadius / 3f;
1130 | mBubblePath.moveTo(x0, y0);
1131 | float x1 = (float) (getMeasuredWidth() / 2f - Math.sqrt(3) / 2f * mBubbleRadius);
1132 | float y1 = 3 / 2f * mBubbleRadius;
1133 | mBubblePath.quadTo(
1134 | x1 - dp2px(2), y1 - dp2px(2),
1135 | x1, y1
1136 | );
1137 | mBubblePath.arcTo(mBubbleRectF, 150, 240);
1138 |
1139 | float x2 = (float) (getMeasuredWidth() / 2f + Math.sqrt(3) / 2f * mBubbleRadius);
1140 | mBubblePath.quadTo(
1141 | x2 + dp2px(2), y1 - dp2px(2),
1142 | x0, y0
1143 | );
1144 | mBubblePath.close();
1145 |
1146 | mBubblePaint.setColor(mBubbleColor);
1147 | canvas.drawPath(mBubblePath, mBubblePaint);
1148 |
1149 | mBubblePaint.setTextSize(mBubbleTextSize);
1150 | mBubblePaint.setColor(mBubbleTextColor);
1151 | mBubblePaint.getTextBounds(mProgressText, 0, mProgressText.length(), mRect);
1152 | Paint.FontMetrics fm = mBubblePaint.getFontMetrics();
1153 | float baseline = mBubbleRadius + (fm.descent - fm.ascent) / 2f - fm.descent;
1154 | canvas.drawText(mProgressText, getMeasuredWidth() / 2f, baseline, mBubblePaint);
1155 | }
1156 |
1157 | void setProgressText(String progressText) {
1158 | if (progressText != null && !mProgressText.equals(progressText)) {
1159 | mProgressText = progressText;
1160 | invalidate();
1161 | }
1162 | }
1163 | }
1164 |
1165 | }
--------------------------------------------------------------------------------
/bubbleseekbar/src/main/java/com/xw/repo/BubbleUtils.java:
--------------------------------------------------------------------------------
1 | package com.xw.repo;
2 |
3 | import android.content.res.Resources;
4 | import android.os.Environment;
5 | import android.util.TypedValue;
6 |
7 | import java.io.File;
8 | import java.io.FileInputStream;
9 | import java.io.IOException;
10 | import java.util.Properties;
11 |
12 | class BubbleUtils {
13 |
14 | private static final File BUILD_PROP_FILE = new File(Environment.getRootDirectory(), "build.prop");
15 | private static Properties sBuildProperties;
16 | private static final Object sBuildPropertiesLock = new Object();
17 |
18 | private static Properties getBuildProperties() {
19 | synchronized (sBuildPropertiesLock) {
20 | if (sBuildProperties == null) {
21 | sBuildProperties = new Properties();
22 | try {
23 | sBuildProperties.load(new FileInputStream(BUILD_PROP_FILE));
24 | } catch (IOException e) {
25 | e.printStackTrace();
26 | }
27 | }
28 | }
29 | return sBuildProperties;
30 | }
31 |
32 | static boolean isMIUI() {
33 | return getBuildProperties().containsKey("ro.miui.ui.version.name");
34 | }
35 |
36 | static int dp2px(int dp) {
37 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
38 | Resources.getSystem().getDisplayMetrics());
39 | }
40 |
41 | static int sp2px(int sp) {
42 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
43 | Resources.getSystem().getDisplayMetrics());
44 | }
45 | }
--------------------------------------------------------------------------------
/bubbleseekbar/src/main/res/values/attr.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/bubbleseekbar/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #607D8B
4 | #455A64
5 | #FF9800
6 |
--------------------------------------------------------------------------------
/bubbleseekbar/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | bubbleseekbar
3 |
4 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.3.0'
9 | classpath 'com.novoda:bintray-release:0.4.0'
10 |
11 | // NOTE: Do not place your application dependencies here; they belong
12 | // in the individual module build.gradle files
13 | }
14 | }
15 |
16 | allprojects {
17 | repositories {
18 | jcenter()
19 | }
20 |
21 | tasks.withType(Javadoc) {
22 | options.addStringOption('Xdoclint:none', '-quiet')
23 | options.addStringOption('encoding', 'UTF-8')
24 | }
25 | }
26 |
27 | task clean(type: Delete) {
28 | delete rootProject.buildDir
29 | }
30 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sathishmscict/BubbleSeekBar/d3796790eb8d9329a84b3f0fe4100c3e5542778d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Mar 10 21:58:07 CST 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 [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/screenshot/demo1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sathishmscict/BubbleSeekBar/d3796790eb8d9329a84b3f0fe4100c3e5542778d/screenshot/demo1.gif
--------------------------------------------------------------------------------
/screenshot/demo2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sathishmscict/BubbleSeekBar/d3796790eb8d9329a84b3f0fe4100c3e5542778d/screenshot/demo2.gif
--------------------------------------------------------------------------------
/screenshot/demo3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sathishmscict/BubbleSeekBar/d3796790eb8d9329a84b3f0fe4100c3e5542778d/screenshot/demo3.gif
--------------------------------------------------------------------------------
/screenshot/demo4.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sathishmscict/BubbleSeekBar/d3796790eb8d9329a84b3f0fe4100c3e5542778d/screenshot/demo4.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':bubbleseekbar'
2 |
--------------------------------------------------------------------------------