├── .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 | 6 | 7 | 8 | 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 | 121 |
122 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 36 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | ![image](https://user-gold-cdn.xitu.io/2019/10/14/16dca390d0527b4e?w=270&h=562&f=gif&s=1830824) 9 | 10 | 11 | ## 2.DialView 12 | 13 | 效果图 14 | 15 | ![image](https://user-gold-cdn.xitu.io/2019/10/14/16dca238c506333c?w=270&h=562&f=gif&s=514741) 16 | -------------------------------------------------------------------------------- /Untitled Diagram.drawio: -------------------------------------------------------------------------------- 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 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 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 | 120 | 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 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 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 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 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 |