├── .gitignore
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── dictionaries
│ └── gh.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── Untitled Diagram.drawio
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── sl
│ │ └── customdemo
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── sl
│ │ │ └── customdemo
│ │ │ ├── MainActivity.kt
│ │ │ ├── chart
│ │ │ ├── DataBean.kt
│ │ │ └── TrendCurveView.kt
│ │ │ └── dial
│ │ │ └── DialView.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── sl
│ └── customdemo
│ └── ExampleUnitTest.kt
├── build.gradle
├── curve.gif
├── dial.gif
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── pathmeasure.gif
├── settings.gradle
└── 未命名绘图.drawio
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | xmlns:android
17 |
18 | ^$
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | xmlns:.*
28 |
29 | ^$
30 |
31 |
32 | BY_NAME
33 |
34 |
35 |
36 |
37 |
38 |
39 | .*:id
40 |
41 | http://schemas.android.com/apk/res/android
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | .*:name
51 |
52 | http://schemas.android.com/apk/res/android
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | name
62 |
63 | ^$
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | style
73 |
74 | ^$
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | .*
84 |
85 | ^$
86 |
87 |
88 | BY_NAME
89 |
90 |
91 |
92 |
93 |
94 |
95 | .*
96 |
97 | http://schemas.android.com/apk/res/android
98 |
99 |
100 | ANDROID_ATTRIBUTE_ORDER
101 |
102 |
103 |
104 |
105 |
106 |
107 | .*
108 |
109 | .*
110 |
111 |
112 | BY_NAME
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/dictionaries/gh.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.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 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CustomDemo
2 | ## 前言
3 | 这段时间闲了下来,决定把项目中的自定义View都用Kotlin写一遍,撸起来吧
4 | ## 1.TrendCurveView
5 |
6 | 效果图
7 |
8 | 
9 |
10 |
11 | ## 2.DialView
12 |
13 | 效果图
14 |
15 | 
16 |
--------------------------------------------------------------------------------
/Untitled Diagram.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
25 |
37 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
77 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | android {
8 | compileSdkVersion 28
9 | buildToolsVersion "28.0.2"
10 | defaultConfig {
11 | applicationId "com.sl.customdemo"
12 | minSdkVersion 18
13 | targetSdkVersion 28
14 | versionCode 1
15 | versionName "1.0"
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | }
25 |
26 | dependencies {
27 | implementation fileTree(dir: 'libs', include: ['*.jar'])
28 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
29 | implementation 'androidx.appcompat:appcompat:1.0.2'
30 | implementation 'androidx.core:core-ktx:1.0.2'
31 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
32 | testImplementation 'junit:junit:4.12'
33 | androidTestImplementation 'androidx.test:runner:1.1.1'
34 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
35 | }
36 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/sl/customdemo/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.sl.customdemo
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.sl.customdemo", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sl/customdemo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sl.customdemo
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import android.os.Bundle
5 | import com.sl.customdemo.chart.DataBean
6 | import kotlinx.android.synthetic.main.activity_main.*
7 | import kotlin.random.Random
8 |
9 | class MainActivity : AppCompatActivity() {
10 |
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | setContentView(R.layout.activity_main)
14 | trendCurveView()
15 |
16 | dialView()
17 | }
18 |
19 |
20 | /**
21 | * 贝塞尔曲线
22 | */
23 | private fun trendCurveView() {
24 | val list = (0..1000).toList()
25 | val mutableList = mutableListOf()
26 | for (i in list) {
27 | mutableList.add(
28 | DataBean(
29 | "2019-10-10",
30 | Random.nextInt(100) + 0.5
31 | )
32 | )
33 | }
34 | trendCurveView.setData(mutableList, "kg")
35 | }
36 |
37 | /**
38 | * 体重表盘
39 | */
40 | private fun dialView() {
41 | var weight = 80f
42 | weightView.setWeight(weight, 90f, 120f)
43 | addValue.setOnClickListener {
44 | weight += 10f
45 | weightView.setWeight(weight, 90f, 120f)
46 | }
47 | subtractionValue.setOnClickListener {
48 | weight -= 10f
49 | if (weight < 0) {
50 | weight = 0f
51 | }
52 | weightView.setWeight(weight, 90f, 120f)
53 | }
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sl/customdemo/chart/DataBean.kt:
--------------------------------------------------------------------------------
1 | package com.sl.customdemo.chart
2 |
3 |
4 | /**
5 | * Created by sl on 2018/3/12.
6 | */
7 |
8 | class DataBean constructor(val recordDate: String, val value: Double)
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sl/customdemo/chart/TrendCurveView.kt:
--------------------------------------------------------------------------------
1 | package com.sl.customdemo.chart
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.graphics.Color
6 | import android.graphics.LinearGradient
7 | import android.graphics.Paint
8 | import android.graphics.Path
9 | import android.graphics.PointF
10 | import android.graphics.Rect
11 | import android.graphics.RectF
12 | import android.graphics.Shader
13 | import android.text.TextPaint
14 | import android.text.TextUtils
15 | import android.util.AttributeSet
16 | import android.util.TypedValue
17 | import android.view.MotionEvent
18 | import android.view.VelocityTracker
19 | import android.view.View
20 | import android.view.ViewConfiguration
21 | import android.widget.Scroller
22 | import androidx.core.view.marginTop
23 |
24 |
25 | import java.text.ParseException
26 | import java.text.SimpleDateFormat
27 | import java.util.ArrayList
28 | import java.util.Calendar
29 | import java.util.Locale
30 | import kotlin.math.abs
31 | import kotlin.math.ceil
32 |
33 |
34 | /**
35 | * Created by taimeng on 2017/10/31.
36 | */
37 |
38 | class TrendCurveView @JvmOverloads constructor(
39 | context: Context,
40 | attrs: AttributeSet? = null,
41 | defStyleAttr: Int = 0
42 | ) : View(context, attrs, defStyleAttr) {
43 |
44 | /**
45 | * 源数据
46 | */
47 | private val mSourceData = ArrayList()
48 | /**
49 | * 总条目数
50 | */
51 | private var mTotalSize: Int = 0
52 | /**
53 | * 绘制的条目
54 | */
55 | private val mShowList = ArrayList()
56 | /**
57 | * 计算后的总条目
58 | */
59 | private val mCacheList = ArrayList()
60 | private val mMinimumFlingVelocity: Int
61 | private val mMaximumFlingVelocity: Float
62 | /**
63 | * 平滑度
64 | */
65 | private val lineSmoothness = 0.2f
66 | private var mVelocityTracker: VelocityTracker? = null
67 |
68 | private var mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
69 | /**
70 | * 文字画笔
71 | */
72 | private var mTextPaint: TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
73 | /**
74 | * 中间文字画笔
75 | */
76 | private var mTopTextPaint: TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
77 |
78 | /**
79 | * 曲线画笔
80 | */
81 | private var mCurvePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
82 | /**
83 | * 填充path画笔
84 | */
85 | private var mPathPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
86 |
87 | /**
88 | * 背景横线paint
89 | */
90 | private var mHorizontalLinePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
91 | /**
92 | * 内圆画笔
93 | */
94 | private var mInnerCirclePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
95 | /**
96 | * 外圆画笔
97 | */
98 | private var mOuterCirclePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
99 | /**
100 | * 单位画笔
101 | */
102 | private var mUnitPaint: TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
103 | /**
104 | * 单位描述 单位:kg
105 | */
106 | private var mUnitDesPaint: TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
107 | private var mRectF: RectF = RectF()
108 | private var mPath: Path = Path()
109 | private var mTopPath: Path = Path()
110 |
111 | private var mMarginRight: Float = 0.toFloat()
112 | private var mMarginTop: Float = 0.toFloat()
113 |
114 | /**
115 | * 数据展示区域的高度
116 | */
117 | private var mAvailableAreaHeight: Float = 0.toFloat()
118 | /**
119 | * 数据展示顶部 = 顶部文字高度+文字上下Margin +上margin+箭头高度+箭头底部间距
120 | */
121 | private var mAvailableAreaTop: Float = 0.toFloat()
122 |
123 | /**
124 | * 扩折线右边距
125 | */
126 | private var mBrokenLineRightMargin: Float = 0.toFloat()
127 | /**
128 | * 展示年的文字大小
129 | */
130 | private var mYearTextSize: Float = 0.toFloat()
131 |
132 | /**
133 | * 顶部文字高度
134 | */
135 | private var mTopTextHeight: Float = 0.toFloat()
136 | private var mTopTvHorizontalMargin: Float = 0.toFloat()
137 | private var mTopTvVerticalMargin: Float = 0.toFloat()
138 |
139 | /**
140 | * 顶部文字矩形的圆角
141 | */
142 | private var mTopRectRadius: Float = 0.toFloat()
143 | /**
144 | * 每个值底部的间距
145 | */
146 | private var mItemTvBottomMargin: Float = 0.toFloat()
147 |
148 | private var mBottomTextPadding: Float = 0.toFloat()
149 | private var mBottomTvHeight: Int = 0
150 | private var mBottomTextSize: Float = 0.toFloat()
151 | private var mBottomTextMargin: Float = 0.toFloat()
152 | private var mBottomHeight: Float = 0.toFloat()
153 |
154 | private var mBottomLineColor: Int = 0
155 | /**
156 | * 值的单位
157 | */
158 | private var mUnit: String? = null
159 | /**
160 | * 右边显示的文字
161 | */
162 | private var mUnitDes: String? = null
163 | /**
164 | * 右边单位描述的宽高
165 | */
166 | private var mUnitDesTextWidth: Float = 0.toFloat()
167 | private var mUnitDesTextHeight: Float = 0.toFloat()
168 |
169 | /**
170 | * 渐变色的开始颜色和结束颜色
171 | */
172 | private var mStartGradientColor: Int = 0
173 | private var mEndGradientColor: Int = 0
174 | /**
175 | * 曲线颜色
176 | */
177 | private var mCurveLineColor: Int = 0
178 | /**
179 | * 内圆半径
180 | */
181 | private var mInnerRadius: Float = 0.toFloat()
182 | /**
183 | * 外圆StrokeWidth
184 | */
185 | private var mOuterRadiusWidth: Float = 0.toFloat()
186 | /**
187 | * 当前viewWidth
188 | */
189 | private var mViewWidth: Float = 0.toFloat()
190 | /**
191 | * 总宽
192 | */
193 | private var mTotalWidth: Int = 0
194 | /**
195 | * 总高
196 | */
197 | private var mTotalHeight: Int = 0
198 |
199 | /**
200 | * 宽的中点
201 | */
202 | private var mCenterX: Int = 0
203 |
204 | /**
205 | * 每个矩形的宽度
206 | */
207 | private var mEveryRectWidth: Int = 0
208 |
209 | /**
210 | * 顶部矩形 顶部箭头的宽度 箭头高度=宽度的一半
211 | */
212 | private var mArrowWidth: Float = 0.toFloat()
213 | /**
214 | * 箭头底部的间距
215 | */
216 | private var mArrowBottomMargin: Float = 0.toFloat()
217 |
218 | private var mBottomLineHeight: Float = 0.toFloat()
219 | /**
220 | * 顶部文字baseLine 计算一次就好了
221 | */
222 | private var mTopBaseLineY: Int = 0
223 | /**
224 | * 最大的滑动距离
225 | */
226 | private var mMaxMove: Int = 0
227 | /**
228 | * 居中的条目数
229 | */
230 | private var mSelectPosition: Int = 0
231 |
232 | /**
233 | * mMove为偏移量
234 | */
235 | private var mMove = 0
236 | private var mLastFocusX: Float = 0.toFloat()
237 | private var mLastFocusY: Float = 0.toFloat()
238 | private var mDownFocusX: Float = 0.toFloat()
239 | private var mDownFocusY: Float = 0.toFloat()
240 |
241 | private val mScroller: Scroller?
242 |
243 | private var mListener: OnTableSelectListener? = null
244 |
245 |
246 | init {
247 | initSize()
248 | initPaint()
249 | mScroller = Scroller(getContext())
250 | val configuration = ViewConfiguration.get(context)
251 | mMinimumFlingVelocity = configuration.scaledMinimumFlingVelocity
252 | mMaximumFlingVelocity = configuration.scaledMaximumFlingVelocity.toFloat()
253 |
254 | }
255 |
256 | private fun initSize() {
257 | mMarginRight = dpToPx(19f)
258 | mMarginTop = dpToPx(11f)
259 |
260 | mYearTextSize = spToPx(12f)
261 | mBrokenLineRightMargin = dpToPx(5f)
262 | mTopTvHorizontalMargin = dpToPx(10f)
263 | mTopTvVerticalMargin = dpToPx(5f)
264 | mTopRectRadius = dpToPx(5f)
265 |
266 | mArrowWidth = dpToPx(9f)
267 | mArrowBottomMargin = dpToPx(12f)
268 |
269 | mBottomLineHeight = dpToPx(1f)
270 | mBottomTextMargin = dpToPx(9f)
271 | mBottomTextPadding = dpToPx(9f)
272 | mBottomTextSize = spToPx(12f)
273 | mInnerRadius = dpToPx(3f)
274 | mOuterRadiusWidth = dpToPx(2f)
275 | mItemTvBottomMargin = dpToPx(4f)
276 |
277 | mStartGradientColor = Color.parseColor("#7047DE69")
278 | mEndGradientColor = Color.parseColor("#0679D58E")
279 | mCurveLineColor = Color.parseColor("#63D798")
280 | mBottomLineColor = Color.parseColor("#EEEEEE")
281 |
282 |
283 | }
284 |
285 | private fun initPaint() {
286 | mTextPaint.textAlign = Paint.Align.CENTER
287 | mTextPaint.color = Color.parseColor("#999999")
288 |
289 | mTopTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
290 | mTopTextPaint.textSize = spToPx(16f)
291 | mTopTextPaint.color = Color.WHITE
292 | mTopTextPaint.textAlign = Paint.Align.CENTER
293 | mTopTextPaint.isFakeBoldText = true
294 | mTopTextHeight = getTextHeight(mTopTextPaint)
295 |
296 | mCurvePaint.style = Paint.Style.STROKE
297 | mCurvePaint.strokeJoin = Paint.Join.ROUND
298 | mCurvePaint.strokeWidth = dpToPx(3f)
299 | mCurvePaint.color = mCurveLineColor
300 |
301 |
302 | mHorizontalLinePaint.style = Paint.Style.STROKE
303 | mHorizontalLinePaint.strokeWidth = dpToPx(1f)
304 | mHorizontalLinePaint.color = Color.parseColor("#EEEEEE")
305 |
306 | mPathPaint
307 | mPathPaint.style = Paint.Style.FILL
308 |
309 |
310 | mInnerCirclePaint.style = Paint.Style.FILL
311 | mInnerCirclePaint.color = Color.WHITE
312 |
313 |
314 | mOuterCirclePaint.style = Paint.Style.STROKE
315 | mOuterCirclePaint.color = mCurveLineColor
316 | mOuterCirclePaint.strokeWidth = mOuterRadiusWidth
317 |
318 | mUnitPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
319 | mUnitPaint.textSize = spToPx(10f)
320 | mUnitPaint.color = Color.WHITE
321 | mUnitPaint.textAlign = Paint.Align.CENTER
322 |
323 |
324 | mUnitDesPaint.textSize = spToPx(12f)
325 | mUnitDesPaint.color = Color.parseColor("#63D798")
326 | mUnitDesPaint.textAlign = Paint.Align.CENTER
327 |
328 |
329 | mTextPaint.textSize = mBottomTextSize
330 | mBottomTvHeight = getTextHeight(mTextPaint).toInt()
331 | mBottomHeight = mBottomTvHeight + mBottomTextMargin * 2
332 | }
333 |
334 | private fun getTextHeight(paint: Paint): Float {
335 | val fm = paint.fontMetrics
336 | return ceil((fm.descent - fm.ascent).toDouble()).toFloat()
337 | }
338 |
339 |
340 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
341 | super.onSizeChanged(w, h, oldw, oldh)
342 | mTotalWidth = w
343 | mTotalHeight = h
344 | mViewWidth = w.toFloat()
345 | mEveryRectWidth = mTotalWidth / NUB
346 | mCenterX = (w / 2f).toInt()
347 | mTopBaseLineY = 0
348 | initData(mSourceData)
349 | }
350 |
351 |
352 | /**
353 | * 唯一公开方法,用于设置元数据
354 | *
355 | * @param data
356 | */
357 | fun setData(data: List?, unit: String) {
358 | if (data == null || data.isEmpty()) {
359 | return
360 | }
361 | initUnit(unit)
362 | mSourceData.clear()
363 | mSourceData.addAll(data)
364 | mTotalSize = data.size
365 | //默认选中最后一条
366 | mSelectPosition = data.size - 1
367 |
368 | mMove = 0
369 | if (mScroller != null) {
370 | mScroller.finalX = 0
371 | }
372 | initData(data)
373 |
374 | }
375 |
376 | private fun initUnit(unit: String) {
377 | if (TextUtils.isEmpty(unit)) {
378 | return
379 | }
380 | this.mUnit = unit
381 | mUnitDes = "单位:$unit"
382 | mUnitDesTextWidth = measureText(mUnitDesPaint, mUnitDes!!)[0]
383 | mUnitDesTextHeight = measureText(mUnitDesPaint, mUnitDes!!)[1]
384 | }
385 |
386 | private fun initData(data: List) {
387 | if (mTotalWidth == 0 || mTotalHeight == 0) {
388 | return
389 | }
390 | if (data.size > 1) {
391 | mTotalWidth += mEveryRectWidth * (data.size - 1)
392 | }
393 |
394 | mAvailableAreaTop =
395 | mTopTextHeight + mTopTvVerticalMargin * 2f + mArrowWidth / 2f + mArrowBottomMargin + mMarginTop
396 | mAvailableAreaHeight = (mTotalHeight.toFloat() - mAvailableAreaTop
397 | - mBottomTextPadding * 2 - mBottomLineHeight - mBottomTvHeight.toFloat())
398 |
399 | val lineGra = LinearGradient(
400 | 0f,
401 | mAvailableAreaTop - 50,
402 | 0f,
403 | mAvailableAreaTop + mAvailableAreaHeight,
404 | mStartGradientColor,
405 | mEndGradientColor,
406 | Shader.TileMode.REPEAT
407 | )
408 | mPathPaint.shader = lineGra
409 |
410 | mMaxMove = (data.size - 1) * mEveryRectWidth
411 | var max = data[0].value
412 | var min = max
413 | for (dataBean in data) {
414 | val value = dataBean.value
415 | if (value > max) {
416 | max = value
417 | }
418 | if (value < min) {
419 | min = value
420 | }
421 | }
422 |
423 | val diff = max - min
424 | val scale = if (diff == 0.0) 0.6f else mAvailableAreaHeight * 0.8f / diff.toFloat()
425 |
426 | val pm = mTextPaint.fontMetricsInt
427 | mCacheList.clear()
428 | val calendar = Calendar.getInstance()
429 | val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.CHINA)
430 |
431 | for (i in data.indices) {
432 | //计算所有点坐标
433 | val trendDataBean = data[i]
434 | //从右向左绘制的,偏移viewWidth的一半
435 | val x = (mCenterX - (data.size - 1 - i) * mEveryRectWidth).toFloat()
436 | val y = (mAvailableAreaTop + (max - trendDataBean.value) * scale).toFloat()
437 | val pointF = PointF(x, y)
438 | val recordDate = trendDataBean.recordDate
439 | try {
440 | val parse = simpleDateFormat.parse(recordDate)
441 | calendar.time = parse
442 | //计算所有文字的坐标
443 | val textBean = getTextBean(pm, trendDataBean.value.toString(), calendar, pointF)
444 | textBean.pointF = pointF
445 | mCacheList.add(textBean)
446 | } catch (e: ParseException) {
447 | e.printStackTrace()
448 | }
449 | }
450 | invalidate()
451 | }
452 |
453 |
454 | private fun getTextBean(
455 | pm: Paint.FontMetricsInt,
456 | value: String,
457 | calendar: Calendar,
458 | pointF: PointF
459 | ): TextBean {
460 | //计算字体区域
461 | val bottomTvTop = mAvailableAreaTop + mAvailableAreaHeight
462 | //内圆半径+外圆的宽+文字间距
463 | val marginTop = mInnerRadius + mOuterRadiusWidth + mItemTvBottomMargin
464 | val textBean = TextBean()
465 | val x = pointF.x
466 | val y = pointF.y
467 |
468 | // val year = calendar.get(Calendar.YEAR)
469 | val month = calendar.get(Calendar.MONTH) + 1
470 | val day = calendar.get(Calendar.DAY_OF_MONTH)
471 |
472 | val topTextWidth = mTextPaint.measureText(value)
473 | //字体居中区域
474 | mRectF.set(
475 | x - topTextWidth / 2f, y - mTopTextHeight - marginTop,
476 | x + topTextWidth / 2f, y - marginTop
477 | )
478 | val baseY =
479 | ((mRectF.bottom + mRectF.top - pm.bottom.toFloat() - pm.top.toFloat()) / 2f).toInt()
480 | textBean.centerStr = (value)
481 | textBean.centerX = (mRectF.centerX())
482 | textBean.centerY = (baseY.toFloat())
483 | //底部文字
484 | mTextPaint.textSize = mBottomTextSize
485 | val stringBuilder = StringBuilder()
486 | stringBuilder.append(if (month < 10) "0$month" else month)
487 | stringBuilder.append("-")
488 | stringBuilder.append(if (day < 10) "0$day" else day)
489 | val bottomTvWidth = mTextPaint.measureText(stringBuilder.toString())
490 | mRectF.set(
491 | x - bottomTvWidth / 2f, bottomTvTop + mBottomTextPadding,
492 | x + bottomTvWidth / 2f, bottomTvTop + mBottomTextPadding + mBottomTvHeight.toFloat()
493 | )
494 |
495 | val baseBottomY =
496 | ((mRectF.bottom + mRectF.top - pm.bottom.toFloat() - pm.top.toFloat()) / 2f).toInt()
497 | textBean.bottomStr = (stringBuilder.toString())
498 | textBean.bottomX = (mRectF.centerX())
499 | textBean.bottomY = (baseBottomY.toFloat())
500 |
501 | textBean.circleX = (x)
502 | textBean.circleY = (pointF.y)
503 |
504 | return textBean
505 | }
506 |
507 | /**
508 | *
509 | * 保证每次绘制做多nub + 3+3 三阶贝塞尔 三个控制点 左右各三个
510 | * 根据滑动距离计算展示的条目
511 | *
512 | * @param move
513 | */
514 | private fun calculateShowList(move: Int) {
515 | if (mCacheList.isEmpty()) {
516 | return
517 | }
518 | val absMove = abs(move)
519 | var start: Int
520 | var end: Int
521 | if (absMove < mCenterX) {
522 | end = mTotalSize
523 | start = mTotalSize - ((absMove + mCenterX) / mEveryRectWidth + 3)
524 | } else {
525 | val exceedStart = (absMove - mCenterX) / mEveryRectWidth
526 | end = mTotalSize - (exceedStart - 3)
527 | start = mTotalSize - (exceedStart + NUB + 3)
528 | }
529 | //越界处理
530 | end = if (mTotalSize > end) end else mTotalSize
531 | start = if (start > 0) start else 0
532 | mShowList.clear()
533 | // mShowList.addAll(mCacheList.subList(start,end));
534 | for (i in start until end) {
535 | mShowList.add(mCacheList[i])
536 | }
537 | }
538 |
539 | /**
540 | * 根据要展示的条目 计算出需要绘制path
541 | *
542 | * @param pointFList
543 | */
544 | private fun measurePath(pointFList: List) {
545 | mPath.reset()
546 | var prePreviousPointX = java.lang.Float.NaN
547 | var prePreviousPointY = java.lang.Float.NaN
548 | var previousPointX = java.lang.Float.NaN
549 | var previousPointY = java.lang.Float.NaN
550 | var currentPointX = java.lang.Float.NaN
551 | var currentPointY = java.lang.Float.NaN
552 | var nextPointX: Float
553 | var nextPointY: Float
554 |
555 | val lineSize = pointFList.size
556 | for (i in 0 until lineSize) {
557 | if (java.lang.Float.isNaN(currentPointX)) {
558 | val point = pointFList[i].pointF
559 | currentPointX = point!!.x + mMove
560 | currentPointY = point.y
561 | }
562 | if (java.lang.Float.isNaN(previousPointX)) {
563 | //是否是第一个点
564 | if (i > 0) {
565 | val point = pointFList[i - 1].pointF
566 | previousPointX = point!!.x + mMove
567 | previousPointY = point.y
568 | } else {
569 | //是的话就用当前点表示上一个点
570 | previousPointX = currentPointX
571 | previousPointY = currentPointY
572 | }
573 | }
574 |
575 | if (java.lang.Float.isNaN(prePreviousPointX)) {
576 | //是否是前两个点
577 | if (i > 1) {
578 | val point = pointFList[i - 2].pointF
579 | prePreviousPointX = point!!.x + mMove
580 | prePreviousPointY = point.y
581 | } else {
582 | //是的话就用当前点表示上上个点
583 | prePreviousPointX = previousPointX
584 | prePreviousPointY = previousPointY
585 | }
586 | }
587 |
588 | // 判断是不是最后一个点了
589 | if (i < lineSize - 1) {
590 | val point = pointFList[i + 1].pointF
591 | nextPointX = point!!.x + mMove
592 | nextPointY = point.y
593 | } else {
594 | //是的话就用当前点表示下一个点
595 | nextPointX = currentPointX
596 | nextPointY = currentPointY
597 | }
598 |
599 | if (i == 0) {
600 | // 将Path移动到开始点
601 | mPath.moveTo(currentPointX, currentPointY)
602 | } else {
603 | // 求出控制点坐标
604 | val firstDiffX = currentPointX - prePreviousPointX
605 | val firstDiffY = currentPointY - prePreviousPointY
606 | val secondDiffX = nextPointX - previousPointX
607 | val secondDiffY = nextPointY - previousPointY
608 | val firstControlPointX = previousPointX + lineSmoothness * firstDiffX
609 | val firstControlPointY = previousPointY + lineSmoothness * firstDiffY
610 | val secondControlPointX = currentPointX - lineSmoothness * secondDiffX
611 | val secondControlPointY = currentPointY - lineSmoothness * secondDiffY
612 | //画出曲线
613 | mPath.cubicTo(
614 | firstControlPointX,
615 | firstControlPointY,
616 | secondControlPointX,
617 | secondControlPointY,
618 | currentPointX,
619 | currentPointY
620 | )
621 | }
622 | // 更新值,
623 | prePreviousPointX = previousPointX
624 | prePreviousPointY = previousPointY
625 | previousPointX = currentPointX
626 | previousPointY = currentPointY
627 | currentPointX = nextPointX
628 | currentPointY = nextPointY
629 | }
630 | }
631 |
632 | override fun onDraw(canvas: Canvas) {
633 | super.onDraw(canvas)
634 | drawHorizontalLine(canvas)
635 | if (mCacheList.isEmpty()) {
636 | return
637 | }
638 | calculateShowList(mMove)
639 | measurePath(mShowList)
640 | drawUnitDes(canvas)
641 | drawCurveLineAndBgPath(canvas)
642 | drawTopAndVerticalLineView(canvas)
643 | drawValueAndPoint(canvas)
644 | }
645 |
646 | /**
647 | * 绘制背景线
648 | *
649 | * @param canvas
650 | */
651 | private fun drawHorizontalLine(canvas: Canvas) {
652 | val baseHeight = mAvailableAreaHeight / 5
653 | for (i in 0 until 6) {
654 | val startY = baseHeight * i + mAvailableAreaTop
655 | canvas.drawLine(0f, startY, mViewWidth, startY, mHorizontalLinePaint)
656 | }
657 | //画底部line
658 | mPaint.shader = null
659 | mPaint.style = Paint.Style.STROKE
660 | mPaint.strokeWidth = mBottomLineHeight
661 | mPaint.color = mBottomLineColor
662 | canvas.drawLine(
663 | 0f,
664 | mTotalHeight - mBottomLineHeight,
665 | mViewWidth,
666 | mTotalHeight - mBottomLineHeight,
667 | mPaint
668 | )
669 | }
670 |
671 | /**
672 | * 绘制右边的 单位:kg
673 | *
674 | * @param canvas
675 | */
676 | private fun drawUnitDes(canvas: Canvas) {
677 | if (!TextUtils.isEmpty(mUnitDes)) {
678 | canvas.drawText(
679 | mUnitDes!!,
680 | width.toFloat() - mMarginRight - mUnitDesTextWidth / 2,
681 | mMarginTop + mUnitDesTextHeight / 2,
682 | mUnitDesPaint
683 | )
684 | }
685 | }
686 |
687 | /**
688 | * 绘制曲线和背景填充
689 | *
690 | * @param canvas
691 | */
692 | private fun drawCurveLineAndBgPath(canvas: Canvas) {
693 | if (mShowList.size > 0) {
694 | val firstX = mShowList[0].pointF!!.x + mMove
695 | val lastX = mShowList[mShowList.size - 1].pointF!!.x + mMove
696 | //先画曲线
697 | canvas.drawPath(mPath, mCurvePaint)
698 | //再填充背景
699 | mPath.lineTo(lastX, mAvailableAreaTop + mAvailableAreaHeight)
700 | mPath.lineTo(firstX, mAvailableAreaTop + mAvailableAreaHeight)
701 | mPath.close()
702 | canvas.drawPath(mPath, mPathPaint)
703 | }
704 |
705 | }
706 |
707 | /**
708 | * 绘制顶部矩形和文字 以及垂直线
709 | *
710 | * @param canvas
711 | */
712 | private fun drawTopAndVerticalLineView(canvas: Canvas) {
713 | val scrollX = abs(mMove)
714 | val baseWidth = mEveryRectWidth / 2f
715 | //因为是从右向左滑动 最右边最大,计算的时候要反过来
716 | var nub = mTotalSize - 1 - ((scrollX + baseWidth) / mEveryRectWidth).toInt()
717 | if (nub > mTotalSize - 1) {
718 | nub = mTotalSize - 1
719 | }
720 | if (nub < 0) {
721 | nub = 0
722 | }
723 | val centerValue = mCacheList[nub].centerStr
724 | val valueWidth = mTopTextPaint.measureText(centerValue)
725 | val unitWidth = if (TextUtils.isEmpty(mUnit)) 0f else mUnitPaint.measureText(mUnit)
726 |
727 | val centerTvWidth = valueWidth + unitWidth + 1f
728 |
729 | val topRectPath = getTopRectPath(centerTvWidth)
730 | mPaint.style = Paint.Style.FILL
731 | mPaint.color = mCurveLineColor
732 | canvas.drawPath(topRectPath, mPaint)
733 | //画居中线
734 | canvas.drawLine(
735 | mCenterX.toFloat(),
736 | mAvailableAreaTop - mArrowBottomMargin,
737 | mCenterX.toFloat(),
738 | mTotalHeight.toFloat() - mBottomHeight - mBottomLineHeight,
739 | mPaint
740 | )
741 |
742 | //计算text Y坐标
743 | mRectF.set(
744 | mCenterX - centerTvWidth / 2f,
745 | mMarginTop,
746 | mCenterX + centerTvWidth / 2,
747 | mMarginTop + mTopTvVerticalMargin * 2 + mTopTextHeight
748 | )
749 | if (mTopBaseLineY == 0) {
750 | val pm = mTextPaint.fontMetricsInt
751 | mTopBaseLineY =
752 | ((mRectF.bottom + mRectF.top - pm.bottom.toFloat() - pm.top.toFloat()) / 2f).toInt()
753 | }
754 | //画居中的值
755 | canvas.drawText(
756 | centerValue!!,
757 | mRectF.centerX() - centerTvWidth / 2 + valueWidth / 2,
758 | mTopBaseLineY.toFloat(),
759 | mTopTextPaint
760 | )
761 | if (!TextUtils.isEmpty(mUnit)) {
762 | //单位
763 | canvas.drawText(
764 | mUnit!!,
765 | mRectF.centerX() + centerTvWidth / 2 - unitWidth / 2,
766 | mTopBaseLineY.toFloat(),
767 | mUnitPaint
768 | )
769 | }
770 |
771 |
772 | }
773 |
774 | /**
775 | * 顶部矩形+三角
776 | *
777 | * @param rectWidth
778 | */
779 | private fun getTopRectPath(rectWidth: Float): Path {
780 | mRectF.set(
781 | mCenterX.toFloat() - rectWidth / 2f - mTopTvHorizontalMargin,
782 | mMarginTop,
783 | mCenterX.toFloat() + rectWidth / 2f + mTopTvHorizontalMargin,
784 | mMarginTop + mTopTvVerticalMargin * 2 + mTopTextHeight
785 | )
786 | mTopPath.reset()
787 | //圆角矩形
788 | mTopPath.addRoundRect(mRectF, mTopRectRadius, mTopRectRadius, Path.Direction.CCW)
789 | //画三角
790 | mTopPath.moveTo(mRectF.centerX() - mArrowWidth / 2f, mMarginTop + mRectF.height())
791 | mTopPath.lineTo(mRectF.centerX(), mMarginTop + mRectF.height() + mArrowWidth / 2f)
792 | mTopPath.lineTo(mRectF.centerX() + mArrowWidth / 2f, mMarginTop + mRectF.height())
793 | mTopPath.close()
794 | return mTopPath
795 | }
796 |
797 |
798 | /**
799 | * 绘制每个点的值和圆
800 | *
801 | * @param canvas
802 | */
803 | private fun drawValueAndPoint(canvas: Canvas) {
804 | for (i in mShowList.indices) {
805 | val textBean = mShowList[i]
806 | val centerX = textBean.centerX + mMove
807 | //绘制值
808 | canvas.drawText(textBean.centerStr!!, centerX, textBean.centerY, mTextPaint)
809 | //绘制底部日期
810 | mTextPaint.textSize = mBottomTextSize
811 | canvas.drawText(textBean.bottomStr!!, centerX, textBean.bottomY, mTextPaint)
812 |
813 | canvas.drawCircle(centerX, textBean.circleY, mInnerRadius, mInnerCirclePaint)
814 | canvas.drawCircle(
815 | centerX,
816 | textBean.circleY,
817 | mInnerRadius + mOuterRadiusWidth / 2,
818 | mOuterCirclePaint
819 | )
820 | }
821 | }
822 |
823 | override fun onTouchEvent(event: MotionEvent): Boolean {
824 | if (mVelocityTracker == null) {
825 | mVelocityTracker = VelocityTracker.obtain()
826 | }
827 | mVelocityTracker!!.addMovement(event)
828 | val action = event.action
829 |
830 | val pointerUp = action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_POINTER_UP
831 | val skipIndex = if (pointerUp) event.actionIndex else -1
832 | // Determine focal point
833 | var sumX = 0f
834 | var sumY = 0f
835 | val count = event.pointerCount
836 | for (i in 0 until count) {
837 | if (skipIndex == i) continue
838 | sumX += event.getX(i)
839 | sumY += event.getY(i)
840 | }
841 | val div = if (pointerUp) count - 1 else count
842 | val focusX = sumX / div
843 | val focusY = sumY / div
844 |
845 | when (event.action) {
846 | MotionEvent.ACTION_DOWN -> {
847 | mLastFocusX = focusX
848 | mDownFocusX = mLastFocusX
849 | mLastFocusY = focusY
850 | mDownFocusY = mLastFocusY
851 | return true
852 | }
853 | MotionEvent.ACTION_MOVE ->
854 |
855 | if (abs(mMove) <= mMaxMove) {
856 | val scrollX = (mLastFocusX - focusX).toInt()
857 | smoothScrollBy(-scrollX, 0)
858 | mLastFocusX = focusX
859 | mLastFocusY = focusY
860 | }
861 | MotionEvent.ACTION_UP -> {
862 | mVelocityTracker!!.computeCurrentVelocity(1000, mMaximumFlingVelocity)
863 | val velocityX = mVelocityTracker!!.xVelocity
864 | //
865 | if (abs(velocityX) > mMinimumFlingVelocity) {
866 | mScroller!!.fling(
867 | mMove,
868 | 0,
869 | velocityX.toInt(),
870 | mVelocityTracker!!.yVelocity.toInt(),
871 | 0,
872 | mMaxMove,
873 | 0,
874 | 0
875 | )
876 | var finalX = mScroller.finalX
877 | val distance = abs(finalX % mEveryRectWidth)
878 | if (distance < mEveryRectWidth / 2) {
879 | finalX -= distance
880 | } else {
881 | finalX += (mEveryRectWidth - distance)
882 | }
883 | mScroller.finalX = finalX
884 |
885 | } else {
886 | setClick(event.x.toInt(), mDownFocusX)
887 | }
888 | getCurrentIndex()
889 |
890 | if (mVelocityTracker != null) {
891 | // This may have been cleared when we called out to the
892 | // application above.
893 | mVelocityTracker!!.recycle()
894 | mVelocityTracker = null
895 | }
896 | }
897 | else -> {
898 | }
899 | }// invalidate();
900 | return super.onTouchEvent(event)
901 | }
902 |
903 |
904 | private fun setClick(upX: Int, downX: Float) {
905 | var finalX = mScroller!!.finalX
906 | val distance: Int
907 | if (abs(downX - upX) > 10) {
908 | distance = abs(finalX % mEveryRectWidth)
909 | if (distance < mEveryRectWidth / 2) {
910 | finalX -= distance
911 | } else {
912 | finalX += (mEveryRectWidth - distance)
913 | }
914 |
915 | } else {
916 | val space = (mCenterX - upX).toFloat()
917 | distance = abs(space % mEveryRectWidth).toInt()
918 | val nub = (space / mEveryRectWidth).toInt()
919 | if (distance < mEveryRectWidth / 2) {
920 | if (nub != 0) {
921 | finalX = if (space > 0) {
922 | (finalX + (space - distance)).toInt()
923 | } else {
924 | (finalX + (space + distance)).toInt()
925 | }
926 | }
927 | } else {
928 | if (space > 0) {
929 | finalX += (nub + 1) * mEveryRectWidth
930 | } else {
931 | finalX = (finalX + space - (mEveryRectWidth - distance)).toInt()
932 |
933 | }
934 |
935 | }
936 | }
937 | if (finalX < 0) {
938 | finalX = 0
939 | } else if (finalX > mMaxMove) {
940 | finalX = mMaxMove
941 | }
942 | smoothScrollTo(finalX, 0)
943 | }
944 |
945 | /**
946 | * 当前居中点
947 | */
948 | private fun getCurrentIndex() {
949 | mSelectPosition = abs(mScroller!!.finalX / mEveryRectWidth)
950 | mSelectPosition = mCacheList.size - 1 - mSelectPosition
951 | if (mSelectPosition < 0) {
952 | mSelectPosition = 0
953 | }
954 | if (mSelectPosition > mCacheList.size - 1) {
955 | mSelectPosition = mCacheList.size - 1
956 | }
957 | mListener?.onTableItemSelect(mSelectPosition, mScroller.duration)
958 | }
959 |
960 |
961 | override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
962 | parent.requestDisallowInterceptTouchEvent(true)
963 | return super.dispatchTouchEvent(ev)
964 | }
965 |
966 | override fun computeScroll() {
967 | if (mScroller!!.computeScrollOffset()) {
968 | //判断左右边界
969 | mMove = mScroller.currX
970 | if (mMove > mMaxMove) {
971 | mMove = mMaxMove
972 | } else if (mMove < 0) {
973 | mMove = 0
974 | }
975 | invalidate()
976 | }
977 | }
978 |
979 | /**
980 | * 调用此方法设置滚动的相对偏移
981 | */
982 | private fun smoothScrollBy(dx: Int, dy: Int) {
983 | //设置mScroller的滚动偏移量
984 | mScroller!!.startScroll(mScroller.finalX, mScroller.finalY, dx, dy)
985 | invalidate()//这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果
986 | }
987 |
988 | /**
989 | * 调用此方法滚动到目标位置
990 | *
991 | * @param fx
992 | * @param fy
993 | */
994 | private fun smoothScrollTo(fx: Int, fy: Int) {
995 |
996 | val dx = fx - mScroller!!.finalX
997 | val dy = fy - mScroller.finalY
998 | smoothScrollBy(dx, dy)
999 | }
1000 |
1001 | fun clearData() {
1002 | mCacheList.clear()
1003 | mSourceData.clear()
1004 | mUnit = ""
1005 | mUnitDes = ""
1006 | invalidate()
1007 | }
1008 |
1009 | private fun dpToPx(dp: Float): Float {
1010 | return dp * context.resources.displayMetrics.density
1011 | }
1012 |
1013 | private fun spToPx(sp: Float): Float {
1014 | return TypedValue.applyDimension(2, sp, context.resources.displayMetrics) + 0.5f
1015 | }
1016 |
1017 | private fun measureText(paint: Paint, text: String): FloatArray {
1018 | val size = FloatArray(2)
1019 | if (TextUtils.isEmpty(text)) {
1020 | return size
1021 | }
1022 | val width = paint.measureText(text, 0, text.length)
1023 | val bounds = Rect()
1024 | paint.getTextBounds(text, 0, text.length, bounds)
1025 | size[0] = width
1026 | size[1] = bounds.height().toFloat()
1027 | return size
1028 | }
1029 |
1030 |
1031 | interface OnTableSelectListener {
1032 | fun onTableItemSelect(position: Int, durationTime: Int)
1033 | }
1034 |
1035 | fun setOnTableSelectListener(listener: OnTableSelectListener) {
1036 | this.mListener = listener
1037 | }
1038 |
1039 |
1040 | private inner class TextBean internal constructor() {
1041 | //数据文字坐标
1042 | var centerX: Float = 0.toFloat()
1043 | var centerY: Float = 0.toFloat()
1044 | //数据文字
1045 | var centerStr: String? = null
1046 | //底部日期坐标
1047 | var bottomX: Float = 0.toFloat()
1048 | var bottomY: Float = 0.toFloat()
1049 | //底部日期
1050 | var bottomStr: String? = null
1051 | //数据圆点坐标
1052 | var circleX: Float = 0.toFloat()
1053 | var circleY: Float = 0.toFloat()
1054 | //坐标点
1055 | var pointF: PointF? = null
1056 | }
1057 |
1058 | companion object {
1059 | /**
1060 | * 一屏幕展示几条
1061 | */
1062 | private const val NUB = 7
1063 | private const val TAG = "TrendCurveView"
1064 | }
1065 |
1066 |
1067 | }
1068 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sl/customdemo/dial/DialView.kt:
--------------------------------------------------------------------------------
1 | package com.sl.customdemo.dial
2 |
3 | import android.animation.Animator
4 | import android.animation.AnimatorListenerAdapter
5 | import android.animation.ValueAnimator
6 | import android.content.Context
7 | import android.graphics.Canvas
8 | import android.graphics.Color
9 | import android.graphics.Paint
10 | import android.graphics.Path
11 | import android.graphics.PathMeasure
12 | import android.graphics.RectF
13 | import android.graphics.SweepGradient
14 | import android.text.TextPaint
15 | import android.util.AttributeSet
16 | import android.util.TypedValue
17 | import android.view.View
18 | import android.view.animation.OvershootInterpolator
19 | import java.lang.Math.min
20 |
21 | import java.math.BigDecimal
22 |
23 |
24 | /**
25 | * Created by sl on 2018/3/5.
26 | */
27 |
28 | class DialView @JvmOverloads constructor(
29 | context: Context,
30 | attrs: AttributeSet? = null,
31 | defStyleAttr: Int = 0
32 | ) : View(context, attrs, defStyleAttr) {
33 |
34 | /**
35 | * 顶部文字大小
36 | */
37 | private var mTopTextSize: Float = 0.toFloat()
38 | /**
39 | * 顶部文字的下间距
40 | */
41 | private var mTopTextBottomMargin: Float = 0.toFloat()
42 | /**
43 | * 中间文字大小
44 | */
45 | private var mCenterTextSize: Float = 0.toFloat()
46 | /**
47 | * 中间文字颜色
48 | */
49 | private var mCenterTvColor: Int = Color.parseColor("#333333")
50 | /**
51 | * 中间文字高度
52 | */
53 | private var mCenterValueHeight: Int = 0
54 | /**
55 | * 中间文字单位大小
56 | */
57 | private var mCenterUnitTextSize: Float = 0.toFloat()
58 | /**
59 | * 底部文字大小
60 | */
61 | private var mBottomTextSize: Float = 0.toFloat()
62 | /**
63 | * 底部文字颜色
64 | */
65 | private var mBottomTextColor: Int = 0
66 | /**
67 | * 外圆环宽度
68 | */
69 | private var mOuterArcWidth: Float = 0.toFloat()
70 | /**
71 | * 内环宽度
72 | */
73 | private var mInnerArcWidth: Float = 0.toFloat()
74 | /**
75 | * 结束点圆半径
76 | */
77 | private var mEndCircleRadius: Float = 0.toFloat()
78 | /**
79 | * 结束点上的直线宽度
80 | */
81 | private var mEndLineWidth: Float = 0.toFloat()
82 |
83 | private var mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
84 |
85 | private var mPathPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
86 |
87 | private var mTextPaint: TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
88 | /**
89 | * 中间值
90 | */
91 | private var mCenterValue: Float = 0.toFloat()
92 | /**
93 | * 底部文字
94 | */
95 | private var mBottomText: String? = null
96 | /**
97 | * 动画时展示的值
98 | */
99 | private var mShowValue = -1f
100 |
101 | /**
102 | * 底部空白角度
103 | */
104 | private val mBottomBlankAngle = 90f
105 | /**
106 | * 中间空白角度
107 | */
108 | private val mOtherBlankAngle = 12f
109 | /**
110 | * 三端圆弧每个的角度
111 | */
112 | private var mEveryAngle: Float = 0.toFloat()
113 | /**
114 | * 开始的角度
115 | */
116 | private var mStartAngle: Float = 0.toFloat()
117 | /**
118 | * 结束的最大角度
119 | */
120 | private var mMaxEndAngle: Float = 0.toFloat()
121 | /**
122 | * 结束的角度
123 | */
124 | private var mResultAngle: Float = 0.toFloat()
125 |
126 | /**
127 | * 内环动画时候的角度
128 | */
129 | private var mEndAngle: Float = 360f
130 | /**
131 | * 内环动画时候的透明度
132 | */
133 | private var mInnerArcAlpha = 1
134 |
135 | private var mDrawBottom: Boolean = false
136 |
137 | private var maxWeight: Float = 150f
138 |
139 | /**
140 | * 内环Path
141 | */
142 | private var mInnerArcPath: Path = Path()
143 | /**
144 | * 结束点 直线的顶端Path
145 | */
146 | private var mOuterOrbitPath: Path = Path()
147 | private var mInnerArcColor: Int = 0
148 | /**
149 | * 内环结束点的坐标
150 | */
151 | private var mInnerPos: FloatArray? = null
152 |
153 | /**
154 | * 内环结束点圆上的线的另一个点的坐标
155 | */
156 | private var mOuterPos: FloatArray? = null
157 |
158 | private var mTan: FloatArray? = null
159 | /**
160 | * 外间距为外圆环宽度的一半
161 | */
162 | private var mOutsideMargin: Float = 0.toFloat()
163 | private var mWidth: Int = 0
164 | private var mHeight: Int = 0
165 |
166 | private var mRectF: RectF = RectF()
167 | /**
168 | * 内环的范围
169 | */
170 | private var mInnerArea: RectF = RectF()
171 | /**
172 | * 结束点圆环范围
173 | */
174 | private var mOuterLineArea: RectF = RectF()
175 |
176 | private var mValueAnimator: ValueAnimator? = null
177 |
178 | private val mTopStr = "体重"
179 |
180 | private val mUnit = "kg"
181 |
182 | private var mValueStr: String? = null
183 |
184 | private var mBgSweepGradient1: SweepGradient? = null
185 | private var mBgSweepGradient2: SweepGradient? = null
186 | private var mBgSweepGradient3: SweepGradient? = null
187 | private var mPathMeasure: PathMeasure = PathMeasure()
188 |
189 | private var mCenterBaseY: Int = 0
190 | private var mTopBaseY: Int = 0
191 | private var mBottomBaseY: Int = 0
192 |
193 | private val mBlueStart = Color.parseColor("#C2E9FB")
194 | private val mBlueEnd = Color.parseColor("#A1C4FD")
195 | private val mGreenStart = Color.parseColor("#58DBAE")
196 | private val mGreenEnd = Color.parseColor("#63D798")
197 | private val mRedStart = Color.parseColor("#F9C46F")
198 | private val mRedEnd = Color.parseColor("#FA8677")
199 | private val mBottomHighColor = Color.parseColor("#FA8677")
200 | private val mBottomNormalColor = Color.parseColor("#63D798")
201 | private val mBottomLowColor = Color.parseColor("#A1C4FD")
202 |
203 | init {
204 | mTopTextSize = spToPx(15f)
205 | mTopTextBottomMargin = dpToPx(3f)
206 | mCenterTextSize = spToPx(33f)
207 | mCenterUnitTextSize = spToPx(10f)
208 | mBottomTextSize = spToPx(16f)
209 | mOuterArcWidth = dpToPx(12f)
210 | mInnerArcWidth = dpToPx(3f)
211 | mEndCircleRadius = dpToPx(6f)
212 | mEndLineWidth = dpToPx(5f)
213 |
214 | mTextPaint.color = mCenterTvColor
215 | mTextPaint.textAlign = Paint.Align.CENTER
216 |
217 | mTextPaint.textSize = mCenterTextSize
218 | mCenterValueHeight = (mTextPaint.descent() - mTextPaint.ascent()).toInt()
219 |
220 | val otherAngle = 360 - mBottomBlankAngle
221 | mEveryAngle = (otherAngle - mOtherBlankAngle * 2) / 3f
222 | mStartAngle = 90 + mBottomBlankAngle / 2f
223 | mMaxEndAngle = 360 - mBottomBlankAngle
224 |
225 | mInnerPos = FloatArray(2)
226 | mOuterPos = FloatArray(2)
227 | mTan = FloatArray(2)
228 | mValueStr = getBigDecimalValue(mShowValue)
229 |
230 | mInnerArcColor = mBlueEnd
231 | }
232 |
233 | fun setWeight(value: Float, benchmarkL: Float, benchmarkH: Float) {
234 | if (value < 0) {
235 | mCenterValue = 0f
236 | } else if (value > 150) {
237 | mCenterValue = maxWeight
238 | } else {
239 | mCenterValue = value
240 | }
241 | this.mShowValue = mCenterValue
242 | mDrawBottom = false
243 | mResultAngle = calculateAngle(mCenterValue, benchmarkL, benchmarkH)
244 | when {
245 | mResultAngle <= mEveryAngle -> {
246 | this.mBottomText = "偏低" + (benchmarkL - mCenterValue)
247 | mInnerArcColor = mBlueEnd
248 | mBottomTextColor = mBottomLowColor
249 | }
250 | mResultAngle <= mEveryAngle * 2 + mOtherBlankAngle -> {
251 | this.mBottomText = "正常"
252 | mInnerArcColor = mGreenEnd
253 | mBottomTextColor = mBottomNormalColor
254 | }
255 | else -> {
256 | mInnerArcColor = mRedEnd
257 | mBottomTextColor = mBottomHighColor
258 | this.mBottomText = "偏高" + (mCenterValue - benchmarkH)
259 |
260 | }
261 | }
262 | if (mValueAnimator == null) {
263 | initAnimator()
264 | }
265 | if (mValueAnimator!!.isRunning) {
266 | mValueAnimator!!.cancel()
267 | }
268 | mValueAnimator!!.start()
269 |
270 |
271 | }
272 |
273 | /**
274 | * 计算结束的角度
275 | *
276 | * @param value
277 | * @param low
278 | * @param high
279 | * @return
280 | */
281 | private fun calculateAngle(value: Float, low: Float, high: Float): Float {
282 | var endAngle: Float
283 | endAngle = when {
284 | value < low -> mEveryAngle * if (value / low > 1) 1f else value / low
285 | value > high -> {
286 | val benchmark = (value - high) / (maxWeight - high) * mEveryAngle
287 | mEveryAngle * 2 + mOtherBlankAngle * 2 + benchmark
288 | }
289 | else -> {
290 | val benchmark = (value - low) / (high - low)
291 | mEveryAngle + mOtherBlankAngle * 1 + mEveryAngle * if (benchmark > 1) 1f else benchmark
292 |
293 | }
294 | }
295 | if (endAngle > mMaxEndAngle) {
296 | endAngle = mMaxEndAngle
297 | }
298 | return endAngle
299 | }
300 |
301 | private fun initAnimator() {
302 | mValueAnimator = ValueAnimator.ofFloat(0f, 1f)
303 | mValueAnimator!!.interpolator = OvershootInterpolator()
304 | mValueAnimator!!.duration = 2000
305 |
306 | mValueAnimator!!.addUpdateListener { animation ->
307 | val animatedValue = animation.animatedValue as Float
308 | mInnerArcAlpha = (animatedValue * 255f).toInt()
309 | mEndAngle = animatedValue * mResultAngle
310 | mShowValue = mCenterValue * animatedValue
311 | mValueStr = getBigDecimalValue(mShowValue)
312 | invalidate()
313 | }
314 |
315 | mValueAnimator!!.addListener(object : AnimatorListenerAdapter() {
316 | override fun onAnimationEnd(animation: Animator) {
317 | super.onAnimationEnd(animation)
318 | mDrawBottom = true
319 | invalidate()
320 | }
321 | })
322 |
323 | }
324 |
325 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
326 | super.onSizeChanged(w, h, oldw, oldh)
327 | mHeight = kotlin.math.min(w, h)
328 | mWidth = mHeight
329 | mOutsideMargin = mOuterArcWidth / 2f
330 |
331 | //SweepGradient的 postions beginning with 0 and ending with 1.0.而第三段明显超过一圈,所以绘制的时候画布旋转,旋转到startAngle为开始点
332 | val pos1 = floatArrayOf(0f, mEveryAngle / 360f)
333 | val pos2 = floatArrayOf(
334 | (mEveryAngle + mOtherBlankAngle) / 360f,
335 | (mEveryAngle * 2f + mOtherBlankAngle) / 360f
336 | )
337 | val pos3 = floatArrayOf(
338 | (mEveryAngle * 2f + mOtherBlankAngle * 2f) / 360f,
339 | (mEveryAngle * 3f + mOtherBlankAngle * 2f) / 360f
340 | )
341 | val colors1 = intArrayOf(mBlueStart, mBlueEnd)
342 | val colors2 = intArrayOf(mGreenStart, mGreenEnd)
343 | val colors3 = intArrayOf(mRedStart, mRedEnd)
344 |
345 | mBgSweepGradient1 = SweepGradient(w / 2f, h / 2f, colors1, pos1)
346 | mBgSweepGradient2 = SweepGradient(w / 2f, h / 2f, colors2, pos2)
347 | mBgSweepGradient3 = SweepGradient(w / 2f, h / 2f, colors3, pos3)
348 |
349 | }
350 |
351 | override fun onDraw(canvas: Canvas) {
352 | super.onDraw(canvas)
353 | if (width > height) {
354 | canvas.translate((width - height) / 2f, 0f)
355 | } else if (height > width) {
356 | canvas.translate(0f, (height - width) / 2f)
357 | }
358 | drawBgArc(canvas)
359 | drawCenterText(canvas)
360 | drawInnerAcr(canvas)
361 | }
362 |
363 | /**
364 | * 画圆弧
365 | *
366 | * @param canvas
367 | */
368 | private fun drawBgArc(canvas: Canvas) {
369 | mRectF.set(
370 | mOutsideMargin,
371 | mOutsideMargin,
372 | mWidth - mOutsideMargin,
373 | mHeight - mOutsideMargin
374 | )
375 | mPaint.color = Color.RED
376 | mPaint.style = Paint.Style.STROKE
377 | mPaint.strokeWidth = mOuterArcWidth
378 | mPaint.strokeCap = Paint.Cap.ROUND
379 |
380 | canvas.save()
381 | //正常绘制,最后一段会超过一圈,但是SweepGradient的positions的范围是【0,1】,所以旋转绘制
382 | //旋转开始位置,会使开始点的圆帽 渐变色异常,所以少旋转mOtherBlankAngle
383 | canvas.rotate(mStartAngle - mOtherBlankAngle, mWidth / 2f, mHeight / 2f)
384 | mPaint.shader = mBgSweepGradient1
385 | canvas.drawArc(mRectF, mOtherBlankAngle, mEveryAngle, false, mPaint)
386 | mPaint.shader = mBgSweepGradient2
387 | canvas.drawArc(
388 | mRectF,
389 | mEveryAngle + mOtherBlankAngle + mOtherBlankAngle,
390 | mEveryAngle,
391 | false,
392 | mPaint
393 | )
394 | mPaint.shader = mBgSweepGradient3
395 | canvas.drawArc(
396 | mRectF,
397 | mEveryAngle * 2f + mOtherBlankAngle * 2f + mOtherBlankAngle,
398 | mEveryAngle,
399 | false,
400 | mPaint
401 | )
402 | canvas.restore()
403 |
404 | }
405 |
406 | /**
407 | * 中间文字
408 | *
409 | * @param canvas
410 | */
411 | private fun drawCenterText(canvas: Canvas) {
412 | mTextPaint.textSize = mCenterTextSize
413 | mTextPaint.color = mCenterTvColor
414 | val measureCenterText = mTextPaint.measureText(mValueStr)
415 | if (mCenterBaseY == 0) {
416 | //计算中间文字的矩形
417 | mRectF.set(
418 | mWidth / 2f - measureCenterText / 2,
419 | (mHeight / 2 - mCenterValueHeight / 2).toFloat(),
420 | mWidth / 2f + measureCenterText / 2,
421 | (mHeight / 2 + mCenterValueHeight / 2).toFloat()
422 | )
423 | val fontMetrics = mTextPaint.fontMetricsInt
424 | mCenterBaseY =
425 | ((mRectF.bottom + mRectF.top - fontMetrics.bottom.toFloat() - fontMetrics.top.toFloat()) / 2).toInt()
426 | }
427 |
428 | canvas.drawText(mValueStr!!, mWidth / 2f, mCenterBaseY.toFloat(), mTextPaint)
429 | //计算单位的区域
430 | mTextPaint.textSize = mCenterUnitTextSize
431 | canvas.drawText(
432 | mUnit,
433 | mWidth / 2f + measureCenterText / 2f + mTextPaint.measureText(mUnit) / 2f,
434 | mCenterBaseY.toFloat(),
435 | mTextPaint
436 | )
437 |
438 | //画顶部的文字
439 | mTextPaint.textSize = mTopTextSize
440 | if (mTopBaseY == 0) {
441 | val topWidth = mTextPaint.measureText(mTopStr)
442 | val bottom = (mHeight / 2 - mCenterValueHeight / 2).toFloat()
443 | val top = bottom - (mOuterArcWidth + mTopTextBottomMargin + mEndCircleRadius * 2)
444 | mRectF.set(mWidth / 2f - topWidth / 2, top, mWidth / 2f + topWidth / 2f, bottom)
445 | val topFontMetrics = mTextPaint.fontMetricsInt
446 | mTopBaseY =
447 | ((mRectF.bottom + mRectF.top - topFontMetrics.bottom.toFloat() - topFontMetrics.top.toFloat()) / 2).toInt()
448 | }
449 | canvas.drawText(mTopStr, mWidth / 2f, mTopBaseY.toFloat(), mTextPaint)
450 | //画底部的值
451 | if (mShowValue > 0 && mDrawBottom) {
452 | mTextPaint.textSize = mBottomTextSize
453 | mTextPaint.color = mBottomTextColor
454 | if (mBottomBaseY == 0) {
455 | val arcRadius = mHeight / 2f
456 | mBottomBaseY =
457 | (mHeight / 2f + Math.cos(Math.toRadians((mBottomBlankAngle / 2f).toDouble())) * arcRadius).toInt()
458 | }
459 |
460 | canvas.drawText(mBottomText!!, mWidth / 2f, mBottomBaseY.toFloat(), mTextPaint)
461 |
462 | }
463 |
464 | }
465 |
466 |
467 | private fun drawInnerAcr(canvas: Canvas) {
468 | val radius =
469 | mWidth / 2f - mOuterArcWidth - mEndCircleRadius - mTopTextBottomMargin
470 | mInnerArea.set(
471 | mWidth / 2f - radius,
472 | mHeight / 2f - radius,
473 | mWidth / 2f + radius,
474 | mHeight / 2f + radius
475 | )
476 | //通过Path类画一个内切圆弧路径
477 | mInnerArcPath.reset()
478 |
479 | mInnerArcPath.addArc(
480 | mInnerArea, mStartAngle, if (mCenterValue == 0f) {
481 | 360f
482 | } else {
483 | mEndAngle
484 | }
485 | )
486 | mPathMeasure.setPath(mInnerArcPath, false)
487 | //得出切点
488 | mPathMeasure.getPosTan(mPathMeasure.length, mInnerPos, mTan)
489 |
490 | mOuterLineArea.set(
491 | mOutsideMargin,
492 | mOutsideMargin,
493 | mWidth - mOutsideMargin,
494 | mHeight - mOutsideMargin
495 | )
496 | mOuterOrbitPath.reset()
497 | mOuterOrbitPath.addArc(
498 | mOuterLineArea, mStartAngle, if (mCenterValue == 0f) {
499 | 360f
500 | } else {
501 | mEndAngle
502 | }
503 | )
504 | // 创建 PathMeasure
505 | mPathMeasure.setPath(mOuterOrbitPath, false)
506 | //得出直线的顶点
507 | mPathMeasure.getPosTan(mPathMeasure.length, mOuterPos, mTan)
508 |
509 | //绘制实心小圆圈
510 | mPathPaint.color = mInnerArcColor
511 | mPathPaint.style = Paint.Style.FILL
512 | mPathPaint.shader = null
513 | mPathPaint.alpha = 255
514 | canvas.drawCircle(mInnerPos!![0], mInnerPos!![1], mEndCircleRadius, mPathPaint)
515 | //圆中心点和线的顶端连接起来
516 | mPathPaint.strokeWidth = mEndLineWidth
517 | mPathPaint.style = Paint.Style.STROKE
518 | canvas.drawLine(
519 | mInnerPos!![0],
520 | mInnerPos!![1],
521 | mOuterPos!![0],
522 | mOuterPos!![1],
523 | mPathPaint
524 | )
525 | //有数据的时候 绘制内环
526 | if (mShowValue >= 0) {
527 | canvas.save()
528 | mPathPaint.alpha = mInnerArcAlpha
529 | mPathPaint.strokeWidth = mInnerArcWidth
530 | mPathPaint.style = Paint.Style.STROKE
531 | mInnerArcPath.reset()
532 | mInnerArcPath.addArc(mInnerArea, 0f, mEndAngle)
533 | // mOuterOrbitPath.reset()
534 | // mOuterOrbitPath.addArc(mOuterLineArea,0f,mEndAngle)
535 | canvas.rotate(mStartAngle, (mWidth / 2).toFloat(), (mHeight / 2).toFloat())
536 | canvas.drawPath(mInnerArcPath, mPathPaint)
537 | // canvas.drawPath(mOuterOrbitPath, mPathPaint)
538 | canvas.restore()
539 | }
540 |
541 | }
542 |
543 | fun cancel() {
544 | if (mValueAnimator != null && mValueAnimator!!.isRunning) {
545 | mValueAnimator!!.cancel()
546 | }
547 | }
548 |
549 |
550 | private fun getBigDecimalValue(value: Float): String {
551 | if (value <= 0) {
552 | return "--"
553 | }
554 | val bigDecimal = BigDecimal(value.toDouble())
555 | return bigDecimal.setScale(1, BigDecimal.ROUND_HALF_UP).toString()
556 | }
557 |
558 | private fun dpToPx(dp: Float): Float {
559 | return (dp * context.resources.displayMetrics.density)
560 | }
561 |
562 | private fun spToPx(sp: Float): Float {
563 | return (TypedValue.applyDimension(2, sp, context.resources.displayMetrics) + 0.5f)
564 | }
565 |
566 | companion object {
567 |
568 | private val TAG = "DialView"
569 | }
570 | }
571 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
22 |
23 |
29 |
30 |
39 |
40 |
48 |
49 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | CustomDemo
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/sl/customdemo/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.sl.customdemo
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.3.50'
5 | repositories {
6 | google()
7 | jcenter()
8 |
9 | }
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:3.5.0'
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | google()
21 | jcenter()
22 |
23 | }
24 | }
25 |
26 | task clean(type: Delete) {
27 | delete rootProject.buildDir
28 | }
29 |
--------------------------------------------------------------------------------
/curve.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/curve.gif
--------------------------------------------------------------------------------
/dial.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/dial.gif
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Oct 10 13:29:36 CST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/pathmeasure.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/songlongGithub/CustomDemo/d7438923f41beb26a5105bb443918ebd42db8dc9/pathmeasure.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name='CustomDemo'
3 |
--------------------------------------------------------------------------------