├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── afra55
│ │ └── speedometer
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── afra55
│ │ │ └── speedometer
│ │ │ ├── MainActivity.kt
│ │ │ └── SpeedometerDialogFragment.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable-xxhdpi
│ │ ├── dialog_bg_1.png
│ │ ├── dialog_bg_2.png
│ │ ├── dialog_bg_3.png
│ │ ├── dialog_bg_5.png
│ │ ├── dialog_center_icon_1.png
│ │ ├── dialog_center_icon_3.png
│ │ ├── dialog_mask_bg_1.png
│ │ ├── dialog_mask_bg_2.png
│ │ ├── dialog_mask_bg_5.png
│ │ ├── dialog_pointer_1.png
│ │ ├── dialog_pointer_3.png
│ │ └── dialog_pointer_5.png
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── fragment_speedometer_dialog.xml
│ │ ├── view_speedometer_1.xml
│ │ ├── view_speedometer_2.xml
│ │ ├── view_speedometer_3.xml
│ │ ├── view_speedometer_4.xml
│ │ └── view_speedometer_5.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
│ └── afra55
│ └── speedometer
│ └── ExampleUnitTest.kt
├── build.gradle
├── doc
└── picture
│ ├── font.png
│ └── xfermode.png
├── gif
├── speedometer_1.gif
├── speedometer_2.gif
├── speedometer_3.gif
├── speedometer_4.gif
└── speedometer_5.gif
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── license.svg
├── schematic_diagram.png
├── settings.gradle
├── speedometer
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── afra55
│ │ └── speedometer
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── afra55
│ │ │ └── speedometer
│ │ │ ├── SpeedometerDialog.kt
│ │ │ ├── SquareFrameByHeightLayout.kt
│ │ │ ├── SquareFrameLayout.kt
│ │ │ └── ViewPager2Indicator.kt
│ └── res
│ │ └── values
│ │ └── attrs_speedometer.xml
│ └── test
│ └── java
│ └── com
│ └── afra55
│ └── speedometer
│ └── ExampleUnitTest.kt
└── speedometer_banner.jpeg
/.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 | ### JetBrains template
16 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
17 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
18 |
19 | # User-specific stuff
20 | .idea/**/workspace.xml
21 | .idea/**/tasks.xml
22 | .idea/**/usage.statistics.xml
23 | .idea/**/dictionaries
24 | .idea/**/shelf
25 |
26 | # Generated files
27 | .idea/**/contentModel.xml
28 |
29 | # Sensitive or high-churn files
30 | .idea/**/dataSources/
31 | .idea/**/dataSources.ids
32 | .idea/**/dataSources.local.xml
33 | .idea/**/sqlDataSources.xml
34 | .idea/**/dynamic.xml
35 | .idea/**/uiDesigner.xml
36 | .idea/**/dbnavigator.xml
37 |
38 | # Gradle
39 | .idea/**/gradle.xml
40 | .idea/**/libraries
41 |
42 | # Gradle and Maven with auto-import
43 | # When using Gradle or Maven with auto-import, you should exclude module files,
44 | # since they will be recreated, and may cause churn. Uncomment if using
45 | # auto-import.
46 | # .idea/artifacts
47 | # .idea/compiler.xml
48 | # .idea/modules.xml
49 | # .idea/*.iml
50 | # .idea/modules
51 | # *.iml
52 | # *.ipr
53 |
54 | # CMake
55 | cmake-build-*/
56 |
57 | # Mongo Explorer plugin
58 | .idea/**/mongoSettings.xml
59 |
60 | # File-based project format
61 | *.iws
62 |
63 | # IntelliJ
64 | out/
65 |
66 | # mpeltonen/sbt-idea plugin
67 | .idea_modules/
68 |
69 | # JIRA plugin
70 | atlassian-ide-plugin.xml
71 |
72 | # Cursive Clojure plugin
73 | .idea/replstate.xml
74 |
75 | # Crashlytics plugin (for Android Studio and IntelliJ)
76 | com_crashlytics_export_strings.xml
77 | crashlytics.properties
78 | crashlytics-build.properties
79 | fabric.properties
80 |
81 | # Editor-based Rest Client
82 | .idea/httpRequests
83 |
84 | # Android studio 3.1+ serialized cache file
85 | .idea/caches/build_file_checksums.ser
86 |
87 | ### Android template
88 | # Built application files
89 | *.apk
90 | *.aar
91 | *.ap_
92 | *.aab
93 |
94 | # Files for the ART/Dalvik VM
95 | *.dex
96 |
97 | # Java class files
98 | *.class
99 |
100 | # Generated files
101 | bin/
102 | gen/
103 | out/
104 | # Uncomment the following line in case you need and you don't have the release build type files in your app
105 | # release/
106 |
107 | # Gradle files
108 | .gradle/
109 | build/
110 |
111 | # Local configuration file (sdk path, etc)
112 | local.properties
113 |
114 | # Proguard folder generated by Eclipse
115 | proguard/
116 |
117 | # Log Files
118 | *.log
119 |
120 | # Android Studio Navigation editor temp files
121 | .navigation/
122 |
123 | # Android Studio captures folder
124 | captures/
125 |
126 | # IntelliJ
127 | *.iml
128 | .idea/workspace.xml
129 | .idea/tasks.xml
130 | .idea/gradle.xml
131 | .idea/assetWizardSettings.xml
132 | .idea/dictionaries
133 | .idea/libraries
134 | # Android Studio 3 in .gitignore file.
135 | .idea/caches
136 | .idea/modules.xml
137 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
138 | .idea/navEditor.xml
139 |
140 | # Keystore files
141 | # Uncomment the following lines if you do not want to check your keystore files in.
142 | #*.jks
143 | #*.keystore
144 |
145 | # External native build folder generated in Android Studio 2.2 and later
146 | .externalNativeBuild
147 | .cxx/
148 |
149 | # Google Services (e.g. APIs or Firebase)
150 | # google-services.json
151 |
152 | # Freeline
153 | freeline.py
154 | freeline/
155 | freeline_project_description.json
156 |
157 | # fastlane
158 | fastlane/report.xml
159 | fastlane/Preview.html
160 | fastlane/screenshots
161 | fastlane/test_output
162 | fastlane/readme.md
163 |
164 | # Version control
165 | vcs.xml
166 |
167 | # lint
168 | lint/intermediates/
169 | lint/generated/
170 | lint/outputs/
171 | lint/tmp/
172 | # lint/reports/
173 |
174 | /.idea/
175 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2020 Afra55
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 | [](https://github.com/Afra55/Speedometer/blob/master/LICENSE)
5 |
6 | [TOC]
7 |
8 |
9 |
10 | ## Speedometer
11 | 速度表盘 For Android
12 |
13 | ## 参数说明
14 |
15 | | 参数 | 说明 |
16 | | ---- | ---- |
17 | | meterDividerAreaNumber | 表盘平均划分区域数量 |
18 | | meterBg | 表盘背景 |
19 | | meterMaskBg | 表盘遮照图 |
20 | | meterPointer | 表盘指针图, 指针图要垂直向下 |
21 | | meterBottomEmptyAngle | 表盘底部0刻度与最大刻度的夹角, 比如 45度 |
22 | | meterNumberMargin | 表盘刻度距离边距的距离 |
23 | | meterNumberTextSize | 表盘刻度数字文字大小 |
24 | | meterNumberTextColor | 表盘刻度数字颜色 |
25 | | meterNumberSelectedTextColor | 指针划过区域的表盘刻度数字颜色 |
26 | | meterNumberLimitTextColor | 指针划过区域的超过限速数字的表盘刻度数字颜色 |
27 | | meterNumberFontAssetPath | 表盘刻度数字字体,asset文件夹路径,比如:`font/DIN_Condensed_Bold.ttf` |
28 | | meterCenterFontAssetPath | 表盘中心数字字体 |
29 | | meterCenterNumberTextSize | 表盘中心数字文字大小 |
30 | | meterCenterDescTextSize | 表盘中心描述文字大小 |
31 | | meterCenterTextColor | 表盘中心数字文字颜色 |
32 | | meterCenterDescTextColor | 表盘中心描述文字颜色 |
33 | | meterCenterDesc | 表盘中心描述字符串 |
34 | | meterCenterIc | 表盘中心文字的背景图 |
35 | | meterType | 表盘类型,针对特殊表盘,默认0;1:只有指针指向的刻度变颜色,中间数字在描述下面;2: 隐藏指针,隐藏刻度, 背景和蒙板图片都绘制到 mask 上 |
36 | | meterRotateX | 表盘绕x轴旋转的角度 |
37 | | meterTranslateZ | 表盘z轴移动的距离 |
38 | | meterTranslateY | 表盘Y轴移动的距离 |
39 | | meterHideCenterNumber | 是否隐藏中心数字 |
40 | | meterHideDividerNumber | 是否隐藏刻度数字 |
41 |
42 | ## 原理图
43 | 
44 |
45 |
46 | ## 效果展示
47 |
48 | ### 表盘 1
49 | ```
50 |
70 |
71 | ```
72 |
73 | 
74 |
75 | ### 表盘 2
76 |
77 | 
78 |
79 | ### 表盘 3
80 |
81 | 
82 |
83 | ### 表盘 4
84 |
85 | 
86 |
87 | ### 表盘 5
88 | 
89 |
90 | ## 基础知识
91 |
92 | ### xfermode
93 | 用来进行图像混合处理。一共有18种混合模式。[官方文档](https://developer.android.com/reference/android/graphics/PorterDuff.Mode)
94 | ```
95 | Paint paint = new Paint();
96 | // DST 图
97 | canvas.drawBitmap(destinationImage, 0, 0, paint);
98 |
99 | PorterDuff.Mode mode = // choose a mode
100 | paint.setXfermode(new PorterDuffXfermode(mode));
101 |
102 | // SRC 图
103 | canvas.drawBitmap(sourceImage, 0, 0, paint);
104 | ```
105 | 
106 |
107 | ### 测量文字大小
108 | 
109 | 获取文字的内容高度,这个高度是文字绘制的最高点到最低点的距离:
110 | ```
111 | fun TextPaint.getTextBound(str: String): Rect {
112 | val rect = Rect()
113 | getTextBounds(str, 0, str.length, rect)
114 | return rect
115 | }
116 |
117 | fun TextPaint.getCapHeight(): Int {
118 | // 获得0-9数字内容的高度
119 | return getTextBound("1234567890").height()
120 | }
121 | ```
122 | ### dp2px
123 |
124 | ```
125 | fun Resources.dp2Px(dip: Float): Float {
126 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, this.displayMetrics)
127 | }
128 | ```
129 |
130 | ### 正方形 view
131 | ```
132 | class SquareFrameLayout : FrameLayout {
133 |
134 | constructor(context: Context) : super(context) {
135 | init(null, 0)
136 | }
137 |
138 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
139 | init(attrs, 0)
140 | }
141 |
142 | constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
143 | context,
144 | attrs,
145 | defStyle
146 | ) {
147 | init(attrs, defStyle)
148 | }
149 |
150 | fun init(attrs: AttributeSet?, defStyle: Int) {
151 |
152 | }
153 |
154 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
155 | val widthMode = MeasureSpec.getMode(widthMeasureSpec)
156 | val heightMode = MeasureSpec.getMode(heightMeasureSpec)
157 |
158 | var width = MeasureSpec.getSize(widthMeasureSpec)
159 |
160 | if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.EXACTLY) {
161 | val height = MeasureSpec.getSize(heightMeasureSpec)
162 | // 如果高是 match_parent 或者指定了特定值, 则取宽高较小的值为整个view的宽高
163 | width = Math.min(width, height)
164 | }
165 |
166 | setMeasuredDimension(width, width)
167 | }
168 | }
169 | ```
170 |
171 | ## license
172 | Speedometer is available under the Apache-2.0 license. See the LICENSE file for more info.
173 | ```
174 | Copyright 2020 Afra55
175 |
176 | Licensed under the Apache License, Version 2.0 (the "License");
177 | you may not use this file except in compliance with the License.
178 | You may obtain a copy of the License at
179 |
180 | http://www.apache.org/licenses/LICENSE-2.0
181 |
182 | Unless required by applicable law or agreed to in writing, software
183 | distributed under the License is distributed on an "AS IS" BASIS,
184 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
185 | See the License for the specific language governing permissions and
186 | limitations under the License.
187 |
188 | ```
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion 29
7 | buildToolsVersion "29.0.3"
8 | compileOptions {
9 | sourceCompatibility JavaVersion.VERSION_1_8
10 | targetCompatibility JavaVersion.VERSION_1_8
11 | }
12 | kotlinOptions {
13 | jvmTarget = "1.8"
14 | }
15 | defaultConfig {
16 | applicationId "com.afra55.speedometer"
17 | minSdkVersion 21
18 | targetSdkVersion 29
19 | versionCode 1
20 | versionName "1.0"
21 |
22 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
23 | }
24 |
25 | buildTypes {
26 | release {
27 | minifyEnabled false
28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
29 | }
30 | }
31 | }
32 |
33 | dependencies {
34 | implementation fileTree(dir: "libs", include: ["*.jar"])
35 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
36 | implementation 'androidx.core:core-ktx:1.3.1'
37 | implementation 'androidx.appcompat:appcompat:1.2.0'
38 | implementation 'androidx.constraintlayout:constraintlayout:2.0.0'
39 | implementation project(path: ':speedometer')
40 | testImplementation 'junit:junit:4.12'
41 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
42 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
43 | implementation 'androidx.viewpager2:viewpager2:1.0.0'
44 |
45 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/afra55/speedometer/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.afra55.speedometer
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.afra55.speedometer", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/afra55/speedometer/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.afra55.speedometer
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import android.os.Bundle
5 | import androidx.fragment.app.Fragment
6 | import androidx.fragment.app.FragmentActivity
7 | import androidx.viewpager2.adapter.FragmentStateAdapter
8 | import kotlinx.android.synthetic.main.activity_main.*
9 |
10 | class MainActivity : AppCompatActivity() {
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | setContentView(R.layout.activity_main)
14 |
15 | val speedometerAdapter = SpeedometerAdapter(this)
16 | speedometer_vp.adapter = speedometerAdapter
17 | speedometer_vp.offscreenPageLimit = speedometerAdapter.itemCount
18 | speedometer_vp_indicator.attach(speedometer_vp)
19 | }
20 | }
21 |
22 |
23 | class SpeedometerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
24 |
25 | val itemList by lazy {
26 | val list = mutableListOf()
27 | list.add(R.layout.view_speedometer_1)
28 | list.add(R.layout.view_speedometer_2)
29 | list.add(R.layout.view_speedometer_3)
30 | list.add(R.layout.view_speedometer_4)
31 | list.add(R.layout.view_speedometer_5)
32 | list
33 | }
34 | override fun getItemCount(): Int {
35 | return itemList.size
36 | }
37 | override fun createFragment(position: Int): Fragment {
38 | return SpeedometerDialogFragment(itemList[position])
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/afra55/speedometer/SpeedometerDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package com.afra55.speedometer
2 |
3 | import android.os.Bundle
4 | import android.os.Handler
5 | import android.os.Message
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import androidx.core.util.toRange
10 | import androidx.fragment.app.Fragment
11 | import kotlinx.android.synthetic.main.fragment_speedometer_dialog.*
12 |
13 | /**
14 | * @author Afra55
15 | * @date 2020/8/28
16 | * A smile is the best business card.
17 | * 没有成绩,连呼吸都是错的。
18 | */
19 | class SpeedometerDialogFragment(val speedometerDialogResId: Int) : Fragment() {
20 |
21 | override fun onCreateView(
22 | inflater: LayoutInflater,
23 | container: ViewGroup?,
24 | savedInstanceState: Bundle?
25 | ): View? {
26 | return inflater.inflate(R.layout.fragment_speedometer_dialog, container, false)
27 | }
28 |
29 | val testHandler:Handler by lazy {
30 | Handler(Handler.Callback {
31 | if (isResume) {
32 | mySpeedometerDialog?.setCurrentNumber((0..180).random().toFloat())
33 | }
34 | testHandler.sendEmptyMessageDelayed(0, 500)
35 | false
36 |
37 | })
38 | }
39 |
40 |
41 | var mySpeedometerDialog: SpeedometerDialog? = null
42 | var isResume = false
43 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
44 | super.onViewCreated(view, savedInstanceState)
45 | val view = LayoutInflater.from(context).inflate(speedometerDialogResId, null, false)
46 | val test_speedometer: View? = view.findViewById(R.id.test_speedometer)
47 | if (test_speedometer is SpeedometerDialog) {
48 | mySpeedometerDialog = test_speedometer
49 | item_root.addView(view, 0)
50 | mySpeedometerDialog!!.setLimitNumber(120)
51 | mySpeedometerDialog!!.setMaxNumber(180F)
52 |
53 | testHandler.sendEmptyMessage(0)
54 | }
55 |
56 | }
57 |
58 | override fun onResume() {
59 | super.onResume()
60 | isResume = true
61 | }
62 |
63 | override fun onPause() {
64 | super.onPause()
65 | isResume = false
66 | }
67 |
68 | override fun onDetach() {
69 | testHandler.removeMessages(0)
70 | super.onDetach()
71 | }
72 | }
73 |
74 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
18 |
22 |
26 |
27 |
28 |
29 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/dialog_bg_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/drawable-xxhdpi/dialog_bg_1.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/dialog_bg_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/drawable-xxhdpi/dialog_bg_2.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/dialog_bg_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/drawable-xxhdpi/dialog_bg_3.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/dialog_bg_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/drawable-xxhdpi/dialog_bg_5.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/dialog_center_icon_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/drawable-xxhdpi/dialog_center_icon_1.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/dialog_center_icon_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/drawable-xxhdpi/dialog_center_icon_3.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/dialog_mask_bg_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/drawable-xxhdpi/dialog_mask_bg_1.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/dialog_mask_bg_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/drawable-xxhdpi/dialog_mask_bg_2.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/dialog_mask_bg_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/drawable-xxhdpi/dialog_mask_bg_5.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/dialog_pointer_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/drawable-xxhdpi/dialog_pointer_1.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/dialog_pointer_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/drawable-xxhdpi/dialog_pointer_3.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/dialog_pointer_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/drawable-xxhdpi/dialog_pointer_5.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
13 |
19 |
25 |
31 |
37 |
43 |
49 |
55 |
61 |
67 |
73 |
79 |
85 |
91 |
97 |
103 |
109 |
115 |
121 |
127 |
133 |
139 |
145 |
151 |
157 |
163 |
169 |
175 |
181 |
187 |
193 |
199 |
205 |
206 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
16 |
17 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_speedometer_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_speedometer_1.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_speedometer_2.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_speedometer_3.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_speedometer_4.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_speedometer_5.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 | #000000
5 | #271616
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Speedometer
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/test/java/com/afra55/speedometer/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.afra55.speedometer
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 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = "1.3.72"
4 | repositories {
5 | google()
6 | jcenter()
7 | }
8 | dependencies {
9 | classpath "com.android.tools.build:gradle:3.6.3"
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | jcenter()
21 | }
22 | }
23 |
24 | task clean(type: Delete) {
25 | delete rootProject.buildDir
26 | }
--------------------------------------------------------------------------------
/doc/picture/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/doc/picture/font.png
--------------------------------------------------------------------------------
/doc/picture/xfermode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/doc/picture/xfermode.png
--------------------------------------------------------------------------------
/gif/speedometer_1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/gif/speedometer_1.gif
--------------------------------------------------------------------------------
/gif/speedometer_2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/gif/speedometer_2.gif
--------------------------------------------------------------------------------
/gif/speedometer_3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/gif/speedometer_3.gif
--------------------------------------------------------------------------------
/gif/speedometer_4.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/gif/speedometer_4.gif
--------------------------------------------------------------------------------
/gif/speedometer_5.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/gif/speedometer_5.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=-Xmx2048m
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
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Aug 11 16:43:11 CST 2020
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.6.4-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 |
--------------------------------------------------------------------------------
/license.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/schematic_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/schematic_diagram.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':speedometer'
2 | include ':app'
3 | rootProject.name = "Speedometer"
--------------------------------------------------------------------------------
/speedometer/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/speedometer/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion 29
7 | buildToolsVersion "29.0.3"
8 | compileOptions {
9 | sourceCompatibility JavaVersion.VERSION_1_8
10 | targetCompatibility JavaVersion.VERSION_1_8
11 | }
12 | kotlinOptions {
13 | jvmTarget = "1.8"
14 | }
15 | defaultConfig {
16 | minSdkVersion 21
17 | targetSdkVersion 29
18 | versionCode 1
19 | versionName "1.0"
20 |
21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
22 | consumerProguardFiles "consumer-rules.pro"
23 | }
24 |
25 | buildTypes {
26 | release {
27 | minifyEnabled false
28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
29 | }
30 | }
31 | }
32 |
33 | dependencies {
34 | implementation fileTree(dir: "libs", include: ["*.jar"])
35 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
36 | implementation 'androidx.core:core-ktx:1.3.1'
37 | implementation 'androidx.appcompat:appcompat:1.2.0'
38 | implementation 'androidx.viewpager2:viewpager2:1.0.0'
39 | testImplementation 'junit:junit:4.12'
40 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
41 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
42 |
43 | }
--------------------------------------------------------------------------------
/speedometer/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/speedometer/consumer-rules.pro
--------------------------------------------------------------------------------
/speedometer/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
--------------------------------------------------------------------------------
/speedometer/src/androidTest/java/com/afra55/speedometer/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.afra55.speedometer
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.afra55.speedometer.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/speedometer/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/speedometer/src/main/java/com/afra55/speedometer/SpeedometerDialog.kt:
--------------------------------------------------------------------------------
1 | package com.afra55.speedometer
2 |
3 | import android.animation.ValueAnimator
4 | import android.animation.ValueAnimator.INFINITE
5 | import android.animation.ValueAnimator.REVERSE
6 | import android.content.Context
7 | import android.content.res.Resources
8 | import android.graphics.*
9 | import android.graphics.drawable.Drawable
10 | import android.text.TextPaint
11 | import android.util.AttributeSet
12 | import android.util.TypedValue
13 | import android.view.View
14 | import android.view.animation.AccelerateDecelerateInterpolator
15 | import androidx.core.graphics.drawable.toBitmap
16 | import androidx.core.text.isDigitsOnly
17 | import kotlin.math.abs
18 | import kotlin.math.cos
19 | import kotlin.math.sin
20 |
21 | class SpeedometerDialog : View {
22 |
23 | /**
24 | * 刻度数字颜色
25 | */
26 | private var meterNumberTextColor: Int = Color.RED
27 |
28 | /**
29 | * 中心速度数字颜色
30 | */
31 | private var meterCenterTextColor: Int = Color.RED
32 |
33 | /**
34 | * 中心描述文字颜色
35 | */
36 | private var meterCenterDescTextColor: Int = Color.RED
37 |
38 | /**
39 | * 指针转过区域的刻度颜色
40 | */
41 | private var meterNumberSelectedTextColor: Int = Color.RED
42 |
43 | /**
44 | * 超过限速值的刻度颜色
45 | */
46 | private var meterNumberLimitTextColor: Int = Color.RED
47 |
48 | /**
49 | * 刻度字体大小
50 | */
51 | private var meterNumberTextSize: Float = 9f
52 |
53 | /**
54 | * 刻度数字距离边界的距离
55 | */
56 | private var meterNumberMargin: Int = 0
57 |
58 | /**
59 | * 当前速度,指针指向的数字
60 | */
61 | private var currentNumber = 0F
62 |
63 | /**
64 | * 绕 X 轴旋转
65 | */
66 | private var meterRotateX = 0F
67 |
68 | /**
69 | * Z轴平移
70 | */
71 | private var meterTranslateZ = 0
72 |
73 | /**
74 | * Y轴平移
75 | */
76 | private var meterTranslateY = 0
77 |
78 | /**
79 | * 0 km/s
80 | * 1 公里/s
81 | */
82 | var meterMeasureType = 0
83 |
84 | /**
85 | * 是否隐藏中心数字
86 | */
87 | var meterHideCenterNumber = false
88 |
89 | /**
90 | * 是否隐藏刻度
91 | */
92 | var meterHideDividerNumber = false
93 |
94 | /**
95 | * 表盘特殊类型
96 | * 0: 默认
97 | * 1:只有指针指向的刻度变颜色,中间数字在描述下面
98 | * 2: 隐藏指针,隐藏刻度, 背景和蒙板图片都绘制到 mask 上
99 | */
100 | private var meterType = 0
101 |
102 | /**
103 | * 刻度最大值
104 | * 0 - maxNumber
105 | */
106 | private var maxNumber = 180F
107 |
108 | /**
109 | * 速度表底部 0 和 最大刻度值之间的夹角
110 | */
111 | private var bottomEmptyAngle = 90
112 |
113 | /**
114 | * 测试用虚线,只在编辑模式展示
115 | */
116 | private var dashCirclePaint: Paint? = null
117 |
118 | /**
119 | * 起始角度,刻度0与垂直线的夹角
120 | */
121 | private var startAngle = bottomEmptyAngle / 2F
122 |
123 | /**
124 | * 限速值,如果大于这个值,表盘开始闪烁
125 | */
126 | private var limitNumber = 120
127 |
128 | /**
129 | * 动画,指针旋转动画
130 | */
131 | var valueAnimator: ValueAnimator? = null
132 |
133 | /**
134 | * 更改透明度动画
135 | */
136 | var alphaAnimator: ValueAnimator? = null
137 |
138 | private var meterNumberTextPaint: TextPaint? = null
139 | private var meterNumberSelectedTextPaint: TextPaint? = null
140 |
141 |
142 | /**
143 | * 中心数字文字 TextPaint
144 | */
145 | private var meterCenterNumberTextPaint: TextPaint? = null
146 |
147 | /**
148 | * 中心文字Desc TextPaint
149 | */
150 | private var meterCenterDescTextPaint: TextPaint? = null
151 |
152 | /**
153 | * 中心描述文字
154 | */
155 | var meterCenterDesc: String? = null
156 |
157 | /**
158 | * 中心数字文字高度
159 | */
160 | var centerNumberTextHeight: Float = 0F
161 |
162 | /**
163 | * 中心文字描述高度
164 | */
165 | var centerDescTextHeight: Float = 0F
166 |
167 | /**
168 | * 中心描述文字大小
169 | */
170 | var meterCenterDescTextSize: Float = 9F
171 |
172 | /**
173 | * 中心数字文字大小
174 | */
175 | var meterCenterNumberTextSize: Float = 9F
176 |
177 | /**
178 | * 中心文字字体
179 | */
180 | var meterCenterFontAssetPath: String? = null
181 |
182 | /**
183 | * 数字文字字体
184 | */
185 | var meterNumberFontAssetPath: String? = null
186 |
187 |
188 | /**
189 | * 表盘背景
190 | */
191 | var meterBg: Drawable? = null
192 |
193 | /**
194 | * 表盘MaskBg
195 | */
196 | var meterMaskBg: Drawable? = null
197 |
198 | /**
199 | * 表盘中心覆盖的 icon
200 | */
201 | var meterCenterIcon: Drawable? = null
202 |
203 | /**
204 | * mask point
205 | */
206 | var translateMaskPoint: Paint? = null
207 |
208 | /**
209 | * 表盘指针
210 | */
211 | var meterPointer: Drawable? = null
212 |
213 | /**
214 | * 表盘等分区域数量
215 | */
216 | private var dividerAreaNumber = 9
217 |
218 | /**
219 | * 表盘等分刻度
220 | */
221 | var dividerNumberList = mutableListOf()
222 |
223 | /**
224 | * 需要回收的 bitmap
225 | */
226 | var needRecyclerBitmapList = mutableListOf()
227 |
228 |
229 | fun setLimitNumber(number: Int) {
230 | val tempNumber = when {
231 | // number > maxNumber -> {
232 | // maxNumber.toInt()
233 | // }
234 | number < 0 -> {
235 | 0
236 | }
237 | else -> {
238 | number
239 | }
240 | }
241 | limitNumber = tempNumber
242 |
243 | if (translateMaskPoint != null && width > 0 && height > 0) {
244 | drawMask(width, height)
245 | }
246 | }
247 |
248 | fun getLimitNumber(): Int {
249 | return limitNumber
250 | }
251 |
252 | fun setMaxNumber(maxNumber: Float) {
253 | this.maxNumber = maxNumber
254 | invalidateTextPaintAndMeasurements()
255 | if (translateMaskPoint != null && width > 0 && height > 0) {
256 | drawMask(width, height)
257 | }
258 | invalidate()
259 | }
260 |
261 | fun setCenterDesc(str: String?) {
262 | meterCenterDesc = str
263 | if (width > 0 && height > 0) {
264 | invalidate()
265 | }
266 | }
267 |
268 | fun setCurrentNumber(currentNumber: Float) {
269 | if (currentNumber == this.currentNumber) {
270 | return
271 | }
272 |
273 | val tempNumber = when {
274 | // currentNumber > maxNumber -> {
275 | // maxNumber
276 | // }
277 | currentNumber < 0 -> {
278 | 0F
279 | }
280 | else -> {
281 | currentNumber
282 | }
283 | }
284 | resetAnimator(this.currentNumber, tempNumber)
285 | }
286 |
287 | fun getCurrentNumber(): Float {
288 | return currentNumber
289 | }
290 |
291 | /**
292 | * 速度数字动画
293 | */
294 | fun resetAnimator(oldNumber: Float, nextNumber: Float) {
295 | valueAnimator?.cancel()
296 | // try {
297 | // ValueAnimator::class.java.getMethod("setDurationScale", Float::class.javaPrimitiveType).invoke(null, 1f)
298 | // } catch (e: Exception) {
299 | // }
300 | valueAnimator = ValueAnimator.ofFloat(oldNumber, nextNumber)
301 | valueAnimator?.duration = 500L
302 | valueAnimator?.interpolator = AccelerateDecelerateInterpolator()
303 | valueAnimator?.addUpdateListener {
304 | try {
305 | val v = it.animatedValue as Float
306 | currentNumber = v
307 | postInvalidate()
308 | if (limitNumber > 0 && currentNumber > limitNumber) {
309 | if (alphaAnimator == null || !alphaAnimator!!.isRunning) {
310 | alphaAnimator()
311 | }
312 | } else {
313 | if (alphaAnimator?.isRunning == true) {
314 | alphaAnimator?.removeAllUpdateListeners()
315 | alphaAnimator?.pause()
316 | alphaAnimator?.cancel()
317 | alpha = 1F
318 | }
319 | }
320 | } catch (e: Exception) {
321 | e.printStackTrace()
322 | }
323 | }
324 | valueAnimator?.start()
325 | }
326 |
327 | /**
328 | * 透明度动画
329 | */
330 | fun alphaAnimator() {
331 |
332 | alphaAnimator?.pause()
333 | alphaAnimator?.cancel()
334 | alphaAnimator = ValueAnimator.ofFloat(1F, 0.7F)
335 | alphaAnimator?.repeatCount = INFINITE
336 | alphaAnimator?.repeatMode = REVERSE
337 | alphaAnimator?.duration = 500L
338 | alphaAnimator?.addUpdateListener {
339 | try {
340 | alpha = it.animatedValue as Float
341 | } catch (e: Exception) {
342 | }
343 | }
344 | alphaAnimator?.start()
345 | }
346 |
347 | fun setDividerAreaNumber(areaNumber: Int) {
348 | if (areaNumber > 0) {
349 | dividerAreaNumber = areaNumber
350 | invalidateTextPaintAndMeasurements()
351 | invalidate()
352 | }
353 | }
354 |
355 | constructor(context: Context) : super(context) {
356 | init(null, 0)
357 | }
358 |
359 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
360 | init(attrs, 0)
361 | }
362 |
363 | constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
364 | context,
365 | attrs,
366 | defStyle
367 | ) {
368 | init(attrs, defStyle)
369 | }
370 |
371 | private fun init(attrs: AttributeSet?, defStyle: Int) {
372 | keepScreenOn = true
373 | // Load attributes
374 | val a = context.obtainStyledAttributes(
375 | attrs, R.styleable.SpeedometerDialog, defStyle, 0
376 | )
377 |
378 | meterNumberTextColor = a.getColor(
379 | R.styleable.SpeedometerDialog_meterNumberTextColor,
380 | meterNumberTextColor
381 | )
382 | meterCenterTextColor = a.getColor(
383 | R.styleable.SpeedometerDialog_meterCenterTextColor,
384 | meterCenterTextColor
385 | )
386 | meterCenterDescTextColor = a.getColor(
387 | R.styleable.SpeedometerDialog_meterCenterDescTextColor,
388 | meterCenterTextColor
389 | )
390 | meterHideCenterNumber = a.getBoolean(
391 | R.styleable.SpeedometerDialog_meterHideCenterNumber,
392 | meterHideCenterNumber
393 | )
394 | meterHideDividerNumber = a.getBoolean(
395 | R.styleable.SpeedometerDialog_meterHideDividerNumber,
396 | meterHideDividerNumber
397 | )
398 | meterNumberSelectedTextColor = a.getColor(
399 | R.styleable.SpeedometerDialog_meterNumberSelectedTextColor,
400 | meterNumberSelectedTextColor
401 | )
402 | meterNumberLimitTextColor = a.getColor(
403 | R.styleable.SpeedometerDialog_meterNumberLimitTextColor,
404 | meterNumberLimitTextColor
405 | )
406 | bottomEmptyAngle = a.getInt(
407 | R.styleable.SpeedometerDialog_meterBottomEmptyAngle,
408 | bottomEmptyAngle
409 | )
410 | meterType = a.getInt(
411 | R.styleable.SpeedometerDialog_meterType,
412 | meterType
413 | )
414 | meterRotateX = a.getFloat(
415 | R.styleable.SpeedometerDialog_meterRotateX,
416 | meterRotateX
417 | )
418 | meterTranslateZ = a.getDimensionPixelSize(
419 | R.styleable.SpeedometerDialog_meterTranslateZ,
420 | meterTranslateZ
421 | )
422 | meterTranslateY = a.getDimensionPixelSize(
423 | R.styleable.SpeedometerDialog_meterTranslateY,
424 | meterTranslateY
425 | )
426 | dividerAreaNumber =
427 | a.getInt(R.styleable.SpeedometerDialog_meterDividerAreaNumber, dividerAreaNumber)
428 | // Use getDimensionPixelSize or getDimensionPixelOffset when dealing with
429 | // values that should fall on pixel boundaries.
430 | meterNumberTextSize = a.getDimension(
431 | R.styleable.SpeedometerDialog_meterNumberTextSize,
432 | meterNumberTextSize
433 | ) // Use getDimensionPixelSize or getDimensionPixelOffset when dealing with
434 | // values that should fall on pixel boundaries.
435 | meterCenterDescTextSize = a.getDimension(
436 | R.styleable.SpeedometerDialog_meterCenterDescTextSize,
437 | meterCenterDescTextSize
438 | )
439 | meterCenterNumberTextSize = a.getDimension(
440 | R.styleable.SpeedometerDialog_meterCenterNumberTextSize,
441 | meterCenterNumberTextSize
442 | )
443 | meterCenterFontAssetPath =
444 | a.getString(R.styleable.SpeedometerDialog_meterCenterFontAssetPath)
445 | meterNumberFontAssetPath =
446 | a.getString(R.styleable.SpeedometerDialog_meterNumberFontAssetPath)
447 | meterCenterDesc = a.getString(R.styleable.SpeedometerDialog_meterCenterDesc)
448 | // Use getDimensionPixelSize or getDimensionPixelOffset when dealing with
449 | // values that should fall on pixel boundaries.
450 | meterNumberMargin = a.getDimensionPixelSize(
451 | R.styleable.SpeedometerDialog_meterNumberMargin,
452 | meterNumberMargin
453 | )
454 |
455 | if (a.hasValue(R.styleable.SpeedometerDialog_meterBg)) {
456 | meterBg = a.getDrawable(
457 | R.styleable.SpeedometerDialog_meterBg
458 | )
459 | meterBg?.callback = this
460 | }
461 |
462 | if (a.hasValue(R.styleable.SpeedometerDialog_meterMaskBg)) {
463 | meterMaskBg = a.getDrawable(
464 | R.styleable.SpeedometerDialog_meterMaskBg
465 | )
466 | meterMaskBg?.callback = this
467 | }
468 | if (a.hasValue(R.styleable.SpeedometerDialog_meterCenterIc)) {
469 | meterCenterIcon = a.getDrawable(
470 | R.styleable.SpeedometerDialog_meterCenterIc
471 | )
472 | meterCenterIcon?.callback = this
473 | }
474 | if (a.hasValue(R.styleable.SpeedometerDialog_meterPointer)) {
475 | meterPointer = a.getDrawable(
476 | R.styleable.SpeedometerDialog_meterPointer
477 | )
478 | meterPointer?.callback = this
479 | }
480 |
481 | a.recycle()
482 |
483 | startAngle = bottomEmptyAngle / 2F
484 |
485 | dashCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
486 | dashCirclePaint!!.color = Color.GREEN
487 | dashCirclePaint!!.style = Paint.Style.STROKE
488 | dashCirclePaint!!.strokeWidth = dp2Px(2F)
489 | val dashWidth = dp2Px(2F)
490 | dashCirclePaint!!.pathEffect = DashPathEffect(floatArrayOf(dashWidth, dashWidth), 0F)
491 | // Set up a default TextPaint object
492 | meterNumberTextPaint = TextPaint().apply {
493 | flags = Paint.ANTI_ALIAS_FLAG
494 | textAlign = Paint.Align.LEFT
495 | }
496 |
497 | meterCenterNumberTextPaint = TextPaint().apply {
498 | flags = Paint.ANTI_ALIAS_FLAG
499 | textAlign = Paint.Align.CENTER
500 | }
501 | meterCenterDescTextPaint = TextPaint().apply {
502 | flags = Paint.ANTI_ALIAS_FLAG
503 | textAlign = Paint.Align.CENTER
504 | }
505 | meterNumberSelectedTextPaint = TextPaint().apply {
506 | flags = Paint.ANTI_ALIAS_FLAG
507 | textAlign = Paint.Align.LEFT
508 | }
509 |
510 |
511 | // Update TextPaint and text measurements from attributes
512 | invalidateTextPaintAndMeasurements()
513 | }
514 |
515 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
516 | super.onSizeChanged(w, h, oldw, oldh)
517 | if (w > 0 && h > 0) {
518 | drawMask(w, h)
519 | }
520 | }
521 |
522 | /**
523 | * 创建指针划过区域的遮照
524 | * 这里使用的是 xfermode 的 SRC_OVER 模式
525 | * PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
526 | */
527 | private fun drawMask(w: Int, h: Int) {
528 | translateMaskPoint = Paint()
529 | translateMaskPoint?.apply {
530 |
531 | for (i in needRecyclerBitmapList) {
532 | i.recycle()
533 | }
534 | needRecyclerBitmapList.clear()
535 |
536 | flags = Paint.ANTI_ALIAS_FLAG
537 |
538 | var ca: Canvas? = null
539 | var bitmap: Bitmap? = null
540 | if (meterType == 2) { // meterType 如是是 2,表盘背景会通过指针扫过区域展示, 先绘制表盘背景到遮照上
541 | if (meterBg != null) {
542 | meterBg!!.setBounds(
543 | paddingLeft, paddingTop,
544 | paddingLeft + w, paddingTop + h
545 | )
546 | bitmap = meterBg!!.toBitmap(w, h, Bitmap.Config.ARGB_8888)
547 | ca = Canvas(bitmap)
548 | needRecyclerBitmapList.add(bitmap)
549 | }
550 | }
551 |
552 | if (meterMaskBg != null) {
553 | // 绘制遮照图
554 | meterMaskBg!!.setBounds(
555 | paddingLeft, paddingTop,
556 | paddingLeft + w, paddingTop + h
557 | )
558 | val bitmapMaskBg = meterMaskBg!!.toBitmap(w, h, Bitmap.Config.ARGB_8888)
559 | if (ca != null) {
560 | // 如果遮照已经绘制了表盘背景
561 | Paint(Paint.ANTI_ALIAS_FLAG).apply {
562 |
563 | shader = bitmapMaskBg.let {
564 | needRecyclerBitmapList.add(it)
565 | BitmapShader(it, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
566 | }
567 |
568 | // SRC_OVER 模式
569 | xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
570 |
571 | // 对 limit number 的限制
572 | var limitNumber1 = limitNumber
573 | if (limitNumber1 > maxNumber) {
574 | limitNumber1 = maxNumber.toInt()
575 | }
576 | // 绘制遮照图的范围,即表盘中心与大于限速刻度到最大刻度值的一个 arc 区域
577 | val fl =
578 | if (limitNumber1 > 0) startAngle + limitNumber1 / maxNumber * (360 - bottomEmptyAngle) else 360F
579 | ca?.drawArc(
580 | paddingLeft.toFloat(),
581 | paddingTop.toFloat(),
582 | (paddingLeft + w).toFloat(),
583 | (paddingTop + h).toFloat(),
584 | 90F,
585 | fl,
586 | true,
587 | this
588 | )
589 | }
590 |
591 | } else {
592 | // 如果表盘没有绘制在遮照上,则遮照即是设置的 meterMaskBg
593 | bitmap = bitmapMaskBg
594 | }
595 |
596 | } else {
597 | // 如果没有设置遮照,则创建一个空白图
598 | if (ca == null) {
599 | bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
600 | }
601 | }
602 |
603 |
604 | if (ca == null) {
605 | ca = Canvas(bitmap!!)
606 | }
607 | if (meterNumberSelectedTextColor != meterNumberTextColor) {
608 | // 绘制指针划过区域的刻度数字
609 | drawNumber(w, w / 2F, h / 2F, meterNumberSelectedTextPaint!!, ca, true)
610 | }
611 | shader = bitmap?.let {
612 | needRecyclerBitmapList.add(it)
613 | BitmapShader(it, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
614 | }
615 | // 给遮照 Paint 设置 SRC_OVER xfermode 模式
616 | xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
617 | }
618 | }
619 |
620 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
621 |
622 | // 这是个正方形的表盘
623 | super.onMeasure(widthMeasureSpec, widthMeasureSpec)
624 |
625 | }
626 |
627 |
628 | /**
629 | * 初始化文字相关的 TextPaint
630 | */
631 | private fun invalidateTextPaintAndMeasurements() {
632 | meterNumberTextPaint?.let {// 刻度数字Paint
633 | it.textSize = meterNumberTextSize
634 | it.color = meterNumberTextColor
635 |
636 | if (!isInEditMode) {
637 | meterNumberFontAssetPath?.let { fontPath ->
638 | it.typeface = Typeface.createFromAsset(context.assets, fontPath)
639 | }
640 | }
641 |
642 | // 获取所有刻度数字
643 | dividerNumberList.clear()
644 | val oneAreaNumber = maxNumber / dividerAreaNumber
645 | // 拿到最宽的字符,数字画出来更好看一点
646 | var maxWidth = it.measureText((dividerAreaNumber * oneAreaNumber).toInt().toString())
647 | for (i in 0..dividerAreaNumber) {
648 | val str = (i * oneAreaNumber).toInt().toString()
649 | val measureText = it.measureText(str)
650 | if (measureText > maxWidth) {
651 | maxWidth = measureText
652 | }
653 | val textWidth = maxWidth
654 | val textHeight = it.fontMetrics.bottom
655 |
656 | dividerNumberList.add(MeterNumber(str, textWidth, textHeight))
657 | }
658 | }
659 | meterNumberSelectedTextPaint?.let { // 指针扫过的刻度数字Paint
660 | it.textSize = meterNumberTextSize
661 | it.color = meterNumberSelectedTextColor
662 | if (!isInEditMode) {
663 | meterNumberFontAssetPath?.let { fontPath ->
664 | it.typeface = Typeface.createFromAsset(context.assets, fontPath)
665 | }
666 | }
667 | }
668 | meterCenterNumberTextPaint?.let {// 中心速度数字Paint
669 | it.textSize = meterCenterNumberTextSize
670 | it.color = meterCenterTextColor
671 | if (!isInEditMode) {
672 | meterCenterFontAssetPath?.let { fontPath ->
673 | it.typeface = Typeface.createFromAsset(context.assets, fontPath)
674 | }
675 | }
676 | centerNumberTextHeight = it.getCapHeight().toFloat()
677 | }
678 | meterCenterDescTextPaint?.let {// 中心描述数字Paint
679 | it.textSize = meterCenterDescTextSize
680 | it.color = meterCenterDescTextColor
681 | if (!isInEditMode) {
682 | meterCenterFontAssetPath?.let { fontPath ->
683 | it.typeface = Typeface.createFromAsset(context.assets, fontPath)
684 | }
685 | }
686 | centerDescTextHeight = it.getCapHeight().toFloat()
687 | }
688 | }
689 |
690 |
691 | var camera: Camera? = null
692 | val cameraMatrix = Matrix()
693 | override fun onDraw(canvas: Canvas) {
694 | super.onDraw(canvas)
695 |
696 | try {
697 |
698 | canvas.save()
699 | val paddingLeft = paddingLeft
700 | val paddingTop = paddingTop
701 | val paddingRight = paddingRight
702 | val paddingBottom = paddingBottom
703 |
704 | val contentWidth = width - paddingLeft - paddingRight
705 | val contentHeight = height - paddingTop - paddingBottom
706 |
707 | val cx = paddingLeft + (contentWidth.toFloat()) / 2
708 | val cy = paddingTop + (contentHeight.toFloat()) / 2
709 |
710 | if (meterRotateX != 0F) {
711 | // 如果设置了旋转,先将画布旋转
712 |
713 | if (camera == null) {
714 | camera = Camera()
715 | }
716 | camera?.let {
717 |
718 | it.save()
719 | it.rotateX(meterRotateX);
720 | it.rotateY(0F);
721 | it.rotateZ(0F); // Rotate around Z access (similar to canvas.rotate)
722 | it.translate(0F, meterTranslateY.toFloat(), meterTranslateZ.toFloat())
723 |
724 |
725 | it.getMatrix(cameraMatrix);
726 |
727 | cameraMatrix.preTranslate(-cx, -cy);
728 |
729 | cameraMatrix.postTranslate(cx, cy);
730 | it.restore();
731 |
732 | canvas.concat(cameraMatrix);
733 | }
734 | }
735 | if (meterType != 2) { // 如果 meterType 是 2 则表盘背景不绘制在画布上,它在遮照层里面
736 | meterBg?.let {
737 | it.setBounds(
738 | paddingLeft, paddingTop,
739 | paddingLeft + contentWidth, paddingTop + contentHeight
740 | )
741 | it.draw(canvas)
742 | }
743 | drawNumber(contentWidth, cx, cy, meterNumberTextPaint!!, canvas)
744 | }
745 | canvas.save()
746 | // 遮照层 start
747 |
748 |
749 | // 当前指针指向的角度
750 | val needDegreesBuyCurrentNumber = getNeedDegreesBuyCurrentNumber()
751 | translateMaskPoint?.let {
752 | if (meterType == 1) { // meterType 是1 则遮照的展示区域是指针的上下 15度
753 | canvas.drawArc(
754 | paddingLeft.toFloat(),
755 | paddingTop.toFloat(),
756 | (paddingLeft + contentWidth).toFloat(),
757 | (paddingTop + contentHeight).toFloat(),
758 | needDegreesBuyCurrentNumber + 90F - 15,
759 | 30F,
760 | true,
761 | it
762 | )
763 | } else {
764 | // 默认情况下,遮照是从最底下绘制,顺时针到指针指向的角度
765 | canvas.drawArc(
766 | paddingLeft.toFloat(),
767 | paddingTop.toFloat(),
768 | (paddingLeft + contentWidth).toFloat(),
769 | (paddingTop + contentHeight).toFloat(),
770 | 90F,
771 | needDegreesBuyCurrentNumber,
772 | true,
773 | it
774 | )
775 | }
776 | }
777 |
778 | // 遮照层 end
779 | canvas.restore()
780 |
781 |
782 | if (meterType != 2) {
783 | // 默认情况下, 绘制指针
784 | canvas.save()
785 |
786 | meterPointer?.let {
787 | it.setBounds(
788 | paddingLeft, paddingTop,
789 | paddingLeft + contentWidth, paddingTop + contentHeight
790 | )
791 | canvas.rotate(
792 | needDegreesBuyCurrentNumber,
793 | (width / 2).toFloat(), (height / 2).toFloat()
794 | )
795 | it.draw(canvas)
796 | }
797 | canvas.restore()
798 |
799 | }
800 |
801 | // 绘制中心数字的背景
802 | meterCenterIcon?.let {
803 | it.setBounds(
804 | paddingLeft, paddingTop,
805 | paddingLeft + contentWidth, paddingTop + contentHeight
806 | )
807 | it.draw(canvas)
808 | }
809 |
810 |
811 | canvas.restore()
812 |
813 | if (meterNumberTextPaint != null && !meterHideCenterNumber) {
814 | // 绘制中心数字
815 | val currentNumberString = currentNumber.toInt().toString()
816 |
817 | val dividerHeight = dp2Px(6F) / 2
818 | val offset =
819 | if (centerDescTextHeight > 0) centerNumberTextHeight - (centerNumberTextHeight + centerDescTextHeight) / 2F else centerNumberTextHeight / 2F
820 | var numberY = cy + offset - dividerHeight
821 | var descY = (cy + centerDescTextHeight) + offset + dividerHeight
822 | if (meterType == 1) {
823 | // meterType 是 1 的时候,desc 绘制在数字上面
824 | numberY = cy + centerNumberTextHeight - offset + dividerHeight
825 | descY = cy - offset - dividerHeight
826 | } else {
827 | // meterType 是 0 的时候,desc 绘制在数字下面
828 |
829 | }
830 |
831 | if (isInEditMode) {
832 | dashCirclePaint?.let { canvas.drawLine(cx - 100, cy, cx + 100, cy, it) }
833 | }
834 |
835 | meterCenterNumberTextPaint?.let {
836 | canvas.drawText(
837 | currentNumberString,
838 | (width / 2F).toFloat(),
839 | numberY,
840 | it
841 | )
842 | }
843 | if (!meterCenterDesc.isNullOrEmpty()) {
844 | meterCenterDescTextPaint?.let {
845 | canvas.drawText(
846 | meterCenterDesc!!,
847 | (width / 2F).toFloat(),
848 | descY,
849 | it
850 | )
851 | }
852 | }
853 |
854 |
855 | }
856 |
857 | if (isInEditMode) {
858 | // 编辑模式下,绘制一个圈来看表盘的位置
859 | canvas.drawCircle(cx, cy, contentWidth / 2 - 10F, dashCirclePaint!!)
860 | }
861 |
862 |
863 | } catch (e: Throwable) {
864 | }
865 | }
866 |
867 | /**
868 | * 指针指向的角度
869 | */
870 | private fun getNeedDegreesBuyCurrentNumber(): Float {
871 |
872 | var fl = currentNumber / maxNumber
873 | if (fl > 1) {
874 | fl = 1F
875 | }
876 | return startAngle + fl * (360 - bottomEmptyAngle)
877 | }
878 |
879 | /**
880 | * 绘制刻度数字
881 | */
882 | private fun drawNumber(
883 | contentWidth: Int,
884 | cx: Float,
885 | cy: Float,
886 | textPaint: TextPaint,
887 | canvas1: Canvas,
888 | showLimitNumberColor: Boolean = false
889 | ) {
890 | if (meterHideDividerNumber) {
891 | return
892 | }
893 | if (meterNumberTextPaint != null) {
894 | val perAngle = (360 - bottomEmptyAngle) / dividerAreaNumber.toDouble()
895 | val textRadius = contentWidth / 2F - meterNumberMargin
896 |
897 |
898 | if (isInEditMode) {
899 | dashCirclePaint?.let {
900 | canvas1.drawCircle(
901 | cx,
902 | cy,
903 | textRadius,
904 | it
905 | )
906 | }
907 | }
908 |
909 | dividerNumberList.forEachIndexed { i, action ->
910 | val thisAngle = startAngle + i * perAngle
911 | val realAngle = Math.toRadians(thisAngle)
912 | val mathSin = abs(sin(realAngle))
913 | val mathCos = abs(cos(realAngle))
914 | val dx = textRadius * mathSin
915 | val dy = textRadius * mathCos
916 | var strCX: Double
917 | var strCY: Double
918 | if (thisAngle < 90) {
919 | strCX = (cx - dx)
920 | strCY = (cy + dy)
921 | } else if (thisAngle >= 90 && thisAngle < 180) {
922 | strCX = (cx - dx)
923 | strCY = (cy - dy)
924 | } else if (thisAngle >= 180 && thisAngle < 270) {
925 | strCX = (cx + dx)
926 | strCY = (cy - dy)
927 | } else {
928 | strCX = (cx + dx)
929 | strCY = (cy + dy)
930 | }
931 |
932 | val str = action.str
933 | val textColor = textPaint.color
934 | if (showLimitNumberColor && str.isDigitsOnly()) {
935 | if (limitNumber > 0 && str.toInt() >= limitNumber) {
936 | textPaint.color = meterNumberLimitTextColor
937 | }
938 | }
939 | canvas1.drawText(
940 | str,
941 | (strCX - action.textWidth / 2).toFloat(),
942 | (strCY + action.textHeight / 2).toFloat(),
943 | textPaint
944 | )
945 | textPaint.color = textColor
946 | // canvas1.drawCircle(
947 | // strCX.toFloat(),
948 | // strCY.toFloat(),
949 | // dp2Px(2F),
950 | // dashCirclePaint!!
951 | // )
952 | }
953 | }
954 | }
955 |
956 | override fun onDetachedFromWindow() {
957 | release()
958 | super.onDetachedFromWindow()
959 | }
960 |
961 | /**
962 | * 释放动画
963 | */
964 | fun release() {
965 | try {
966 | for (i in needRecyclerBitmapList) {
967 | i.recycle()
968 | }
969 | needRecyclerBitmapList.clear()
970 | valueAnimator?.pause()
971 | valueAnimator?.cancel()
972 | alphaAnimator?.pause()
973 | alphaAnimator?.cancel()
974 | } catch (e: Exception) {
975 | }
976 | }
977 |
978 | }
979 |
980 | data class MeterNumber(val str: String, val textWidth: Float, val textHeight: Float)
981 |
982 | fun View.dp2Px(dip: Float): Float {
983 | return this.resources.dp2Px(dip)
984 | }
985 |
986 | fun Context.dp2Px(dip: Float): Float {
987 | return this.resources.dp2Px(dip)
988 | }
989 |
990 | fun Resources.dp2Px(dip: Float): Float {
991 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, this.displayMetrics)
992 | }
993 |
994 | fun TextPaint.getTextBound(str: String): Rect {
995 | val rect = Rect()
996 | getTextBounds(str, 0, str.length, rect)
997 | return rect
998 | }
999 |
1000 | fun TextPaint.getCapHeight(): Int {
1001 | // 获得数字高度
1002 | return getTextBound("1234567890").height()
1003 | }
--------------------------------------------------------------------------------
/speedometer/src/main/java/com/afra55/speedometer/SquareFrameByHeightLayout.kt:
--------------------------------------------------------------------------------
1 | package com.afra55.speedometer
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.widget.FrameLayout
6 |
7 | /**
8 | * @author Afra55
9 | * @date 2020/8/19
10 | * A smile is the best business card.
11 | * 没有成绩,连呼吸都是错的。
12 | */
13 | class SquareFrameByHeightLayout : FrameLayout {
14 |
15 | constructor(context: Context) : super(context) {
16 | init(null, 0)
17 | }
18 |
19 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
20 | init(attrs, 0)
21 | }
22 |
23 | constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
24 | context,
25 | attrs,
26 | defStyle
27 | ) {
28 | init(attrs, defStyle)
29 | }
30 |
31 | fun init(attrs: AttributeSet?, defStyle: Int) {
32 |
33 | }
34 |
35 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
36 | super.onMeasure(heightMeasureSpec, heightMeasureSpec)
37 | }
38 | }
--------------------------------------------------------------------------------
/speedometer/src/main/java/com/afra55/speedometer/SquareFrameLayout.kt:
--------------------------------------------------------------------------------
1 | package com.afra55.speedometer
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.widget.FrameLayout
6 |
7 | /**
8 | * @author Afra55
9 | * @date 2020/8/19
10 | * A smile is the best business card.
11 | * 没有成绩,连呼吸都是错的。
12 | */
13 | class SquareFrameLayout : FrameLayout {
14 |
15 | constructor(context: Context) : super(context) {
16 | init(null, 0)
17 | }
18 |
19 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
20 | init(attrs, 0)
21 | }
22 |
23 | constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
24 | context,
25 | attrs,
26 | defStyle
27 | ) {
28 | init(attrs, defStyle)
29 | }
30 |
31 | fun init(attrs: AttributeSet?, defStyle: Int) {
32 |
33 | }
34 |
35 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
36 | super.onMeasure(widthMeasureSpec, widthMeasureSpec)
37 | }
38 | }
--------------------------------------------------------------------------------
/speedometer/src/main/java/com/afra55/speedometer/ViewPager2Indicator.kt:
--------------------------------------------------------------------------------
1 | package com.afra55.speedometer
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.graphics.Color
6 | import android.graphics.Paint
7 | import android.util.AttributeSet
8 | import android.util.Log
9 | import android.view.View
10 | import androidx.viewpager2.widget.ViewPager2
11 |
12 | /**
13 | * @author Afra55
14 | * @date 2020/8/20
15 | * A smile is the best business card.
16 | * 没有成绩,连呼吸都是错的。
17 | */
18 | class ViewPager2Indicator :View{
19 |
20 | constructor(context: Context) : super(context) {
21 | init(null, 0)
22 | }
23 |
24 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
25 | init(attrs, 0)
26 | }
27 |
28 | constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
29 | context,
30 | attrs,
31 | defStyle
32 | ) {
33 | init(attrs, defStyle)
34 | }
35 |
36 | var selectedIndicatorWidth = 0
37 | var indicatorMargin = 0
38 | var indicatorColor = Color.WHITE
39 | val paint = Paint(Paint.ANTI_ALIAS_FLAG)
40 | var itemCount = 0
41 |
42 | var currentSelectPosition = 0
43 | var myPositionOffset = 0F
44 | val viewPager2ChangeCallback = object : ViewPager2.OnPageChangeCallback() {
45 | /**
46 | * This method will be invoked when the current page is scrolled, either as part
47 | * of a programmatically initiated smooth scroll or a user initiated touch scroll.
48 | *
49 | * @param position Position index of the first page currently being displayed.
50 | * Page position+1 will be visible if positionOffset is nonzero.
51 | * @param positionOffset Value from [0, 1) indicating the offset from the page at position.
52 | * @param positionOffsetPixels Value in pixels indicating the offset from position.
53 | */
54 | override fun onPageScrolled(
55 | position: Int,
56 | positionOffset: Float,
57 | positionOffsetPixels: Int
58 | ) {
59 | super.onPageScrolled(position, positionOffset, positionOffsetPixels)
60 | currentSelectPosition = position
61 | myPositionOffset = positionOffset
62 | invalidate()
63 | }
64 |
65 | /**
66 | * This method will be invoked when a new page becomes selected. Animation is not
67 | * necessarily complete.
68 | *
69 | * @param position Position index of the new selected page.
70 | */
71 | override fun onPageSelected(position: Int) {
72 | super.onPageSelected(position)
73 | }
74 | }
75 |
76 | private fun init(attrs: AttributeSet?, defStyle: Int) {
77 | val a = context.obtainStyledAttributes(
78 | attrs, R.styleable.ViewPager2Indicator, defStyle, 0
79 | )
80 |
81 | selectedIndicatorWidth = a.getDimensionPixelSize(R.styleable.ViewPager2Indicator_indicatorSelectedWidth, dp2Px(10F).toInt())
82 | indicatorMargin = a.getDimensionPixelSize(R.styleable.ViewPager2Indicator_indicatorMargin, dp2Px(3F).toInt())
83 | indicatorColor = a.getColor(R.styleable.ViewPager2Indicator_indicatorColor, indicatorColor)
84 |
85 | a.recycle()
86 |
87 | paint.color = indicatorColor
88 | paint.style = Paint.Style.FILL
89 |
90 |
91 | }
92 |
93 | fun attach(viewPager2: ViewPager2) {
94 | itemCount = viewPager2.adapter?.itemCount ?:0
95 | if (itemCount > 0) {
96 | viewPager2.registerOnPageChangeCallback(viewPager2ChangeCallback)
97 | post {
98 | invalidate()
99 | }
100 | }
101 | }
102 |
103 | override fun onDraw(canvas: Canvas?) {
104 | super.onDraw(canvas)
105 | if (itemCount > 0 && width > 0 && height > 0) {
106 | val paddingLeft = paddingLeft
107 | val paddingTop = paddingTop
108 | val paddingRight = paddingRight
109 | val paddingBottom = paddingBottom
110 |
111 | val contentWidth = width - paddingLeft - paddingRight
112 |
113 | // 高度即作为 indicator 的宽度
114 | val IndicatorWidth = height - paddingTop - paddingBottom
115 |
116 | val cx = paddingLeft + (contentWidth.toFloat()) / 2
117 | val cy = paddingTop + (IndicatorWidth.toFloat()) / 2
118 |
119 |
120 |
121 | var startX = cx - ((itemCount - 1) * (indicatorMargin+ IndicatorWidth ) + selectedIndicatorWidth) / 2
122 | for (i in 0 until itemCount) {
123 | val realOffset = selectedIndicatorWidth - IndicatorWidth
124 | var offset =
125 | if (currentSelectPosition == i){
126 | realOffset * (1 - myPositionOffset)
127 | } else if (currentSelectPosition + 1 == i) {
128 | realOffset * myPositionOffset
129 | } else {
130 | 0F
131 | }
132 | val left = startX
133 | val top = paddingTop.toFloat()
134 | val right = startX + IndicatorWidth + offset
135 | val bottom = paddingTop + IndicatorWidth + 0F
136 | canvas?.drawRoundRect(left, top, right, bottom, 90F, 90F, paint)
137 | startX = right + indicatorMargin
138 | }
139 | }
140 | }
141 |
142 |
143 | }
--------------------------------------------------------------------------------
/speedometer/src/main/res/values/attrs_speedometer.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 |
--------------------------------------------------------------------------------
/speedometer/src/test/java/com/afra55/speedometer/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.afra55.speedometer
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 | }
--------------------------------------------------------------------------------
/speedometer_banner.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Afra55/Android-Speedometer/8a8e9e0f4fd307f80a4032044e453e36d5b41a67/speedometer_banner.jpeg
--------------------------------------------------------------------------------