├── .idea
├── .name
├── copyright
│ └── profiles_settings.xml
├── encodings.xml
├── compiler.xml
├── AndroidProjectSystem.xml
├── migrations.xml
├── deploymentTargetSelector.xml
├── vcs.xml
├── gradle.xml
├── runConfigurations.xml
├── misc.xml
└── caches
│ └── deviceStreaming.xml
├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── attrs.xml
│ │ │ │ └── styles.xml
│ │ │ ├── drawable
│ │ │ │ ├── gradient_background.xml
│ │ │ │ ├── card_background.xml
│ │ │ │ ├── battery_display_background.xml
│ │ │ │ └── button_background.xml
│ │ │ ├── values-w820dp
│ │ │ │ └── dimens.xml
│ │ │ └── layout
│ │ │ │ ├── dialog_gradient_settings.xml
│ │ │ │ ├── dialog_power_settings.xml
│ │ │ │ ├── dialog_color_picker.xml
│ │ │ │ ├── dialog_basic_settings.xml
│ │ │ │ ├── dialog_text_settings.xml
│ │ │ │ ├── dialog_size_settings.xml
│ │ │ │ ├── dialog_stripe_settings.xml
│ │ │ │ ├── dialog_appearance_settings.xml
│ │ │ │ └── activity_main.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── donkor
│ │ │ └── demo
│ │ │ ├── BatteryView.kt
│ │ │ └── MainActivity.kt
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── donkor
│ │ │ └── demo
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── donkor
│ │ └── demo
│ │ └── ApplicationTest.java
├── proguard-rules.pro
└── build.gradle
├── screenshot
├── 1.webp
├── 2.webp
├── 3.webp
├── 4.webp
├── 5.webp
├── 6.webp
└── 7.webp
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── settings.gradle
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── gradlew
└── README.md
/.idea/.name:
--------------------------------------------------------------------------------
1 | BatteryView
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/screenshot/1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/screenshot/1.webp
--------------------------------------------------------------------------------
/screenshot/2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/screenshot/2.webp
--------------------------------------------------------------------------------
/screenshot/3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/screenshot/3.webp
--------------------------------------------------------------------------------
/screenshot/4.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/screenshot/4.webp
--------------------------------------------------------------------------------
/screenshot/5.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/screenshot/5.webp
--------------------------------------------------------------------------------
/screenshot/6.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/screenshot/6.webp
--------------------------------------------------------------------------------
/screenshot/7.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/screenshot/7.webp
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Donkor798/BatteryView/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 电池视图演示
4 |
5 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/AndroidProjectSystem.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 28 10:00:20 PST 2015
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-8.7-all.zip
7 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/gradient_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/test/java/com/donkor/demo/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.donkor.demo;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
9 | */
10 | public class ExampleUnitTest {
11 | @Test
12 | public void addition_isCorrect() throws Exception {
13 | assertEquals(4, 2 + 2);
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/card_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | }
15 | }
16 |
17 | rootProject.name = "BatteryView"
18 | include ':app'
19 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/donkor/demo/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.donkor.demo;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/battery_display_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in E:\Users\Android\sdk1/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/button_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
8 |
9 |
10 | -
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
10 |
11 | # ========== Android / Gradle 生成产物 ==========
12 | # 忽略所有模块的 build 目录
13 | **/build/
14 |
15 | # Gradle 本地缓存
16 | .gradle/
17 |
18 | # NDK/外部构建输出
19 | .externalNativeBuild/
20 | .cxx/
21 |
22 | # AS 截图与临时输出
23 | captures/
24 |
25 | # ========== 开发工具配置 ==========
26 | # Android Studio/IntelliJ
27 | .idea/
28 | *.iml
29 |
30 | # VS Code
31 | .vscode/
32 |
33 | # ========== 本地环境与敏感文件 ==========
34 | # 本地 SDK/NDK 路径
35 | local.properties
36 |
37 | # 签名与密钥(请勿提交)
38 | *.keystore
39 | *.jks
40 | keystore.properties
41 |
42 | # Google 服务(若使用,避免泄露)
43 | google-services.json
44 |
45 | # ========== 二进制与日志 ==========
46 | *.apk
47 | *.ap_
48 | *.aab
49 | *.dex
50 | *.class
51 | *.log
52 |
53 | # ========== 操作系统文件 ==========
54 | .DS_Store
55 | Thumbs.db
56 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | compileSdkVersion 35
8 |
9 | namespace 'com.donkor.demo'
10 |
11 | defaultConfig {
12 | applicationId "com.donkor.demo"
13 | minSdkVersion 24
14 | targetSdkVersion 35
15 | versionCode 1
16 | versionName "1.0"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 |
26 | compileOptions {
27 | sourceCompatibility JavaVersion.VERSION_17
28 | targetCompatibility JavaVersion.VERSION_17
29 | }
30 |
31 | kotlinOptions {
32 | jvmTarget = '17'
33 | }
34 | }
35 |
36 | dependencies {
37 | implementation fileTree(dir: 'libs', include: ['*.jar'])
38 | implementation 'androidx.core:core-ktx:1.12.0'
39 | implementation 'androidx.appcompat:appcompat:1.6.1'
40 | testImplementation 'junit:junit:4.13.2'
41 | }
42 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | org.gradle.parallel=true
18 |
19 | # AndroidX package structure to make it clearer which packages are bundled with the
20 | # Android operating system, and which are packaged with your app's APK
21 | android.useAndroidX=true
22 |
23 | # Enables namespacing of each library's R class so that its R class includes only the
24 | # resources declared in the library itself and none from the library's dependencies,
25 | # thereby reducing the size of the R class for that library
26 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | #6366F1
6 | #4F46E5
7 | #8B5CF6
8 |
9 |
10 | #F8FAFC
11 | #1E293B
12 | #FFFFFF
13 | #334155
14 |
15 |
16 | #0F172A
17 | #64748B
18 | #94A3B8
19 | #FFFFFF
20 |
21 |
22 | #E2E8F0
23 |
24 |
25 | #10B981
26 | #F59E0B
27 | #EF4444
28 | #3B82F6
29 |
30 |
31 | #6366F1
32 | #8B5CF6
33 |
34 |
35 | #1A000000
36 |
37 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/misc.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 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_gradient_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
18 |
19 |
20 |
28 |
29 |
30 |
37 |
38 |
46 |
47 |
48 |
55 |
56 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_power_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
11 |
12 |
20 |
21 |
22 |
28 |
29 |
35 |
36 |
40 |
41 |
49 |
50 |
51 |
52 |
59 |
60 |
61 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_color_picker.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
17 |
18 |
19 |
25 |
26 |
32 |
33 |
39 |
40 |
41 |
42 |
48 |
49 |
55 |
56 |
62 |
63 |
64 |
65 |
70 |
71 |
77 |
78 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_basic_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
11 |
12 |
20 |
21 |
22 |
31 |
32 |
33 |
42 |
43 |
44 |
53 |
54 |
55 |
64 |
65 |
66 |
75 |
76 |
77 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
15 |
23 |
24 |
25 |
33 |
34 |
35 |
42 |
43 |
44 |
52 |
53 |
54 |
61 |
62 |
63 |
77 |
78 |
79 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_text_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
18 |
19 |
20 |
27 |
28 |
36 |
37 |
38 |
45 |
46 |
53 |
54 |
55 |
62 |
63 |
68 |
69 |
76 |
77 |
84 |
85 |
92 |
93 |
100 |
101 |
108 |
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_size_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
13 |
14 |
22 |
23 |
24 |
32 |
33 |
40 |
41 |
48 |
49 |
56 |
57 |
64 |
65 |
66 |
74 |
75 |
82 |
83 |
90 |
91 |
98 |
99 |
105 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_stripe_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
18 |
19 |
20 |
27 |
28 |
34 |
35 |
42 |
43 |
50 |
51 |
58 |
59 |
60 |
61 |
69 |
70 |
71 |
78 |
79 |
86 |
87 |
88 |
96 |
97 |
98 |
104 |
105 |
113 |
114 |
121 |
122 |
129 |
130 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_appearance_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
18 |
19 |
20 |
28 |
29 |
30 |
37 |
38 |
46 |
47 |
48 |
55 |
56 |
63 |
64 |
65 |
72 |
73 |
80 |
81 |
82 |
87 |
88 |
89 |
97 |
98 |
99 |
106 |
107 |
115 |
116 |
117 |
124 |
125 |
132 |
133 |
134 |
141 |
142 |
148 |
149 |
150 |
151 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BatteryView
2 |
3 | 一个功能丰富的 Android 自定义电池视图控件,支持水平和垂直两种方向,提供完整的电池状态展示和自定义功能。
4 |
5 | ## ✨ 特性
6 |
7 | ### 基础功能
8 | - 🔋 支持水平/垂直两种方向
9 | - 📊 实时显示电池电量百分比
10 | - 🎨 根据电量自动变色(红/黄/绿)
11 | - ⚡ 充电动画效果
12 | - 🔄 充电循环动画(0-100%)
13 |
14 | ### 高级功能
15 | - 🎨 **渐变填充**:支持横向/纵向渐变,自定义起始色和结束色
16 | - 💫 **低电量闪烁**:电量低于阈值时闪烁提醒
17 | - 📳 **低电量震动**:电量低于阈值时震动提醒
18 | - 🎯 **电量平滑过渡**:电量变化时平滑动画
19 |
20 | ### 外观自定义
21 | - 🎭 **电池身设置**
22 | - 边框颜色
23 | - 圆角半径(0-32dp,独立控制)
24 | - 边框宽度(0-12dp)
25 |
26 | - 🔋 **电池头设置**
27 | - 电池头颜色(独立于边框颜色)
28 | - 电池头圆角半径(0-32dp,独立控制)
29 | - 电池头比例(0.2-0.8)
30 | - **电池头紧贴设计**:贴近电池身的边为直角,远离的边使用圆角,实现无缝贴合
31 |
32 | ### 文本自定义
33 | - 📝 百分比文字大小
34 | - 🎨 百分比文字颜色
35 | - 📍 百分比文字位置(居中/顶部/底部/左侧/右侧)
36 |
37 | ## 🎯 设计亮点
38 |
39 | ### 1. 电池头紧贴设计
40 | 电池头与电池身无缝贴合,贴近电池身的边为直角,远离的边使用圆角
41 |
42 | ### 2. 独立圆角控制
43 | - 电池身和电池头可以设置不同的圆角半径
44 | - 支持更丰富的视觉效果组合
45 | - 向后兼容旧的 `batteryCornerRadius` 属性
46 |
47 | ### 3. 充电循环动画
48 | - 0→100% 单向循环(3秒周期)
49 | - 平滑的电量过渡动画
50 | - 可独立控制是否启用
51 |
52 | ### 4. 低电量智能提醒
53 | - 闪烁提醒:电量低于阈值时自动闪烁
54 | - 震动提醒:电量低于阈值时震动(需权限)
55 | - 可自定义低电量阈值
56 |
57 | ## 📱 演示应用
58 |
59 | 应用提供了完整的演示界面,包含:
60 | - 实时电量调节
61 | - 6个功能设置弹窗(电量、基础、文本、渐变、尺寸、外观)
62 | - 实时预览效果
63 | - 一键重置功能
64 | - 水平和垂直电池同步显示
65 |
66 |
67 | ## 🖼 截图预览
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | ## 🚀 快速开始
83 |
84 | ### 1. 在布局文件中使用
85 |
86 | ```xml
87 |
107 | ```
108 |
109 | ### 2. 在代码中使用
110 |
111 | ```kotlin
112 | val batteryView = findViewById(R.id.batteryView)
113 |
114 | // 基础设置
115 | batteryView.setPower(75) // 设置电量
116 | batteryView.setCharging(true) // 设置充电状态
117 | batteryView.setChargingCycle(true) // 设置充电循环动画
118 |
119 | // 外观设置 - 电池身
120 | batteryView.setBodyCornerRadius(16f) // 设置电池身圆角(像素)
121 | batteryView.setBorderWidth(4f) // 设置边框宽度(像素)
122 | batteryView.setBorderColor(Color.BLACK) // 设置边框颜色
123 |
124 | // 外观设置 - 电池头
125 | batteryView.setHeadCornerRadius(8f) // 设置电池头圆角(像素)
126 | batteryView.setHeadColor(Color.RED) // 设置电池头颜色
127 | batteryView.setHeadScale(0.6f) // 设置电池头比例
128 |
129 | // 渐变设置
130 | batteryView.setGradientEnabled(true) // 启用渐变
131 | batteryView.setGradientVertical(true) // 设置垂直渐变
132 | batteryView.setGradientColors(Color.GREEN, Color.YELLOW) // 设置渐变颜色
133 |
134 | // 文本设置
135 | batteryView.setShowPercent(true) // 显示百分比
136 | batteryView.setPercentSize(14f * density) // 设置文字大小(像素)
137 | batteryView.setPercentColor(Color.WHITE) // 设置文字颜色
138 | batteryView.setPercentPosition(0) // 设置文字位置(0=居中)
139 |
140 | // 低电量提醒
141 | batteryView.setLowBlink(true) // 启用低电量闪烁
142 | batteryView.setVibrateOnLow(true) // 启用低电量震动
143 |
144 | // 动画设置
145 | batteryView.setAnimatePower(true) // 启用电量平滑过渡
146 | ```
147 |
148 | ## 📐 XML 属性
149 |
150 | ### 基础属性
151 |
152 | | 属性 | 类型 | 说明 | 默认值 |
153 | |------|------|------|--------|
154 | | `batteryOrientation` | enum | 电池方向(horizontal/vertical) | horizontal |
155 | | `batteryPower` | integer | 电池电量(0-100) | 100 |
156 | | `batteryColor` | color | 基础颜色 | - |
157 | | `batteryCharging` | boolean | 是否充电 | false |
158 |
159 | ### 百分比文本属性
160 |
161 | | 属性 | 类型 | 说明 | 默认值 |
162 | |------|------|------|--------|
163 | | `batteryShowPercent` | boolean | 是否显示百分比 | false |
164 | | `batteryPercentColor` | color | 百分比文字颜色 | BLACK |
165 | | `batteryPercentSize` | dimension | 百分比文字大小 | 12sp |
166 | | `batteryPercentPosition` | enum | 百分比位置(center/top/bottom/start/end) | center |
167 |
168 | ### 外观属性 - 电池身
169 |
170 | | 属性 | 类型 | 说明 | 默认值 |
171 | |------|------|------|--------|
172 | | `batteryCornerRadius` | dimension | 圆角半径(同时设置电池身和电池头) | 3dp |
173 | | `batteryBodyCornerRadius` | dimension | 电池身圆角半径 | 3dp |
174 | | `batteryBorderWidth` | dimension | 边框宽度 | 2dp |
175 | | `batteryBorderColor` | color | 边框颜色 | BLACK |
176 |
177 | ### 外观属性 - 电池头
178 |
179 | | 属性 | 类型 | 说明 | 默认值 |
180 | |------|------|------|--------|
181 | | `batteryHeadCornerRadius` | dimension | 电池头圆角半径 | 3dp |
182 | | `batteryHeadScale` | float | 电池头比例(0.2-0.8) | 0.5 |
183 | | `batteryHeadColor` | color | 电池头颜色 | BLACK |
184 |
185 | ### 渐变属性
186 |
187 | | 属性 | 类型 | 说明 | 默认值 |
188 | |------|------|------|--------|
189 | | `batteryGradientEnabled` | boolean | 是否启用渐变 | false |
190 | | `batteryGradientStartColor` | color | 渐变起始颜色 | GREEN |
191 | | `batteryGradientEndColor` | color | 渐变结束颜色 | YELLOW |
192 | | `batteryGradientVertical` | boolean | 是否垂直渐变 | false |
193 |
194 | ### 低电量提醒属性
195 |
196 | | 属性 | 类型 | 说明 | 默认值 |
197 | |------|------|------|--------|
198 | | `batteryLowThreshold` | integer | 低电量阈值(0-100) | 20 |
199 | | `batteryLowBlink` | boolean | 低电量闪烁 | false |
200 | | `batteryVibrateOnLow` | boolean | 低电量震动 | false |
201 |
202 | ### 动画属性
203 |
204 | | 属性 | 类型 | 说明 | 默认值 |
205 | |------|------|------|--------|
206 | | `batteryAnimatePower` | boolean | 电量平滑过渡 | false |
207 |
208 | ## 🎨 API 方法
209 |
210 | ### 电量控制
211 | ```kotlin
212 | setPower(power: Int) // 设置电量(0-100)
213 | getPower(): Int // 获取当前电量
214 | setAnimatePower(enable: Boolean) // 设置电量平滑过渡动画
215 | ```
216 |
217 | ### 充电控制
218 | ```kotlin
219 | setCharging(charging: Boolean) // 设置充电状态
220 | setChargingCycle(enable: Boolean) // 设置充电循环动画(0-100%循环)
221 | ```
222 |
223 | ### 外观控制 - 电池身
224 | ```kotlin
225 | setBodyCornerRadius(radiusPx: Float) // 设置电池身圆角(像素)
226 | setCornerRadius(radiusPx: Float) // 同时设置电池身和电池头圆角(像素)
227 | setBorderWidth(widthPx: Float) // 设置边框宽度(像素)
228 | setBorderColor(color: Int) // 设置边框颜色
229 | ```
230 |
231 | ### 外观控制 - 电池头
232 | ```kotlin
233 | setHeadCornerRadius(radiusPx: Float) // 设置电池头圆角(像素)
234 | setHeadColor(color: Int) // 设置电池头颜色
235 | setHeadScale(scale: Float) // 设置电池头比例(0.2-0.8)
236 | ```
237 |
238 | ### 渐变控制
239 | ```kotlin
240 | setGradientEnabled(enabled: Boolean) // 启用/禁用渐变填充
241 | setGradientVertical(vertical: Boolean) // 设置渐变方向(true=垂直,false=横向)
242 | setGradientColors(start: Int, end: Int) // 设置渐变起始色和结束色
243 | ```
244 |
245 | ### 文本控制
246 | ```kotlin
247 | setShowPercent(show: Boolean) // 显示/隐藏百分比文字
248 | setPercentSize(sizePx: Float) // 设置文字大小(像素)
249 | setPercentColor(color: Int) // 设置文字颜色
250 | setPercentPosition(position: Int) // 设置文字位置(0=居中,1=顶部,2=底部,3=左侧,4=右侧)
251 | ```
252 |
253 | ### 低电量提醒
254 | ```kotlin
255 | setLowBlink(enabled: Boolean) // 启用/禁用低电量闪烁
256 | setVibrateOnLow(enabled: Boolean) // 启用/禁用低电量震动
257 | setLowThreshold(threshold: Int) // 设置低电量阈值(0-100)
258 | ```
259 |
260 | ## 💡 使用示例
261 |
262 | ### 示例1:简单的电池显示
263 |
264 | ```xml
265 |
272 | ```
273 |
274 | ### 示例2:带渐变效果的充电电池
275 |
276 | ```xml
277 |
288 | ```
289 |
290 | ### 示例3:自定义外观的电池
291 |
292 | ```xml
293 |
308 | ```
309 |
310 | ### 示例4:低电量提醒电池
311 |
312 | ```kotlin
313 | val batteryView = findViewById(R.id.batteryView)
314 | batteryView.apply {
315 | setPower(15) // 低电量
316 | setLowBlink(true) // 启用闪烁
317 | setVibrateOnLow(true) // 启用震动
318 | setLowThreshold(20) // 低于20%触发
319 | setShowPercent(true)
320 | setPercentColor(Color.RED)
321 | }
322 | ```
323 |
324 | ### 示例5:充电循环动画
325 |
326 | ```kotlin
327 | val batteryView = findViewById(R.id.batteryView)
328 | batteryView.apply {
329 | setCharging(true) // 启用充电
330 | setChargingCycle(true) // 启用循环动画(0-100%)
331 | setAnimatePower(true) // 平滑过渡
332 | setGradientEnabled(true) // 渐变效果
333 | }
334 | ```
335 |
336 | ## 📦 技术栈
337 |
338 | - **语言**:Kotlin
339 | - **最低 SDK**:21 (Android 5.0)
340 | - **目标 SDK**:35 (Android 15)
341 | - **构建工具**:Gradle 8.3.2
342 |
343 | ## ❓ 常见问题
344 |
345 | ### Q1: 如何设置电池头和电池身不同的圆角?
346 | ```kotlin
347 | batteryView.setBodyCornerRadius(16f) // 电池身大圆角
348 | batteryView.setHeadCornerRadius(4f) // 电池头小圆角
349 | ```
350 |
351 | ### Q2: 如何让电池头和电池身使用不同的颜色?
352 | ```kotlin
353 | batteryView.setBorderColor(Color.BLACK) // 电池身边框黑色
354 | batteryView.setHeadColor(Color.RED) // 电池头红色
355 | ```
356 |
357 | ### Q3: 充电循环动画如何工作?
358 | 启用充电循环动画后,电量会从0%平滑上升到100%,然后立即跳回0%,周期为3秒:
359 | ```kotlin
360 | batteryView.setCharging(true) // 启用充电
361 | batteryView.setChargingCycle(true) // 启用循环动画
362 | ```
363 |
364 | ### Q4: 如何自定义低电量阈值?
365 | ```kotlin
366 | batteryView.setLowThreshold(15) // 设置低电量阈值为15%
367 | batteryView.setLowBlink(true) // 启用闪烁
368 | batteryView.setVibrateOnLow(true) // 启用震动
369 | ```
370 |
371 | ### Q5: 渐变方向如何控制?
372 | ```kotlin
373 | // 横向渐变(从左到右或从上到下)
374 | batteryView.setGradientVertical(false)
375 |
376 | // 纵向渐变(从下到上或从右到左)
377 | batteryView.setGradientVertical(true)
378 | ```
379 |
380 | ### Q6: 如何在XML中同时设置电池身和电池头圆角?
381 | 使用 `batteryCornerRadius` 会同时设置两者:
382 | ```xml
383 | app:batteryCornerRadius="8dp"
384 | ```
385 | 或者分别设置:
386 | ```xml
387 | app:batteryBodyCornerRadius="12dp"
388 | app:batteryHeadCornerRadius="6dp"
389 | ```
390 |
391 | ### Q7: 电量平滑过渡动画的作用是什么?
392 | 启用后,调用 `setPower()` 时电量会平滑过渡到目标值,而不是立即跳变:
393 | ```kotlin
394 | batteryView.setAnimatePower(true)
395 | batteryView.setPower(80) // 从当前电量平滑过渡到80%
396 | ```
397 |
398 | ## 📄 许可证
399 |
400 | ```
401 | Copyright 2025 Donkor
402 |
403 | Licensed under the Apache License, Version 2.0 (the "License");
404 | you may not use this file except in compliance with the License.
405 | You may obtain a copy of the License at
406 |
407 | http://www.apache.org/licenses/LICENSE-2.0
408 |
409 | Unless required by applicable law or agreed to in writing, software
410 | distributed under the License is distributed on an "AS IS" BASIS,
411 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
412 | See the License for the specific language governing permissions and
413 | limitations under the License.
414 | ```
415 |
416 | ## 👨💻 作者
417 |
418 | **Donkor**
419 |
420 | ---
421 |
422 | ⭐ 如果这个项目对你有帮助,请给个 Star!
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
11 |
12 |
13 |
18 |
19 |
20 |
25 |
26 |
27 |
32 |
33 |
41 |
42 |
50 |
51 |
52 |
53 |
69 |
70 |
71 |
72 |
84 |
85 |
86 |
95 |
96 |
104 |
105 |
106 |
112 |
113 |
114 |
121 |
122 |
131 |
132 |
153 |
154 |
155 |
156 |
161 |
162 |
163 |
170 |
171 |
180 |
181 |
201 |
202 |
203 |
204 |
205 |
206 |
216 |
217 |
218 |
223 |
224 |
234 |
235 |
246 |
247 |
257 |
258 |
259 |
260 |
264 |
265 |
275 |
276 |
287 |
288 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
--------------------------------------------------------------------------------
/app/src/main/java/com/donkor/demo/BatteryView.kt:
--------------------------------------------------------------------------------
1 | package com.donkor.demo
2 |
3 | import android.animation.ValueAnimator
4 | import android.content.Context
5 | import android.content.res.TypedArray
6 | import android.graphics.*
7 | import android.util.AttributeSet
8 | import android.util.TypedValue
9 | import android.view.View
10 | import androidx.annotation.ColorInt
11 | import android.os.Build
12 | import android.os.VibrationEffect
13 | import android.os.Vibrator
14 | import android.os.VibratorManager
15 |
16 | import kotlin.math.max
17 | import kotlin.math.min
18 |
19 | /**
20 | * 页面:BatteryView(Kotlin版,增强)
21 | * 说明:支持水平/垂直电池,圆角、电量百分比文本、充电动画、自定义边框宽度与电池头比例
22 | * author : Donkor , 创建的日期(2025-10-13)
23 | */
24 | class BatteryView @JvmOverloads constructor(
25 | context: Context,
26 | attrs: AttributeSet? = null
27 | ) : View(context, attrs) {
28 |
29 | // 属性:电量/方向/颜色
30 | private var power: Int = 100 // 0..100
31 | private var orientation: Int = 0 // 0: horizontal, 1: vertical
32 | @ColorInt private var baseColor: Int = Color.GREEN
33 |
34 | // 增强属性
35 | private var showPercent: Boolean = false
36 | // 百分比文本配置
37 | private var percentColor: Int? = null
38 | private var percentSizePx: Float = sp(12f)
39 | // 位置:0 center, 1 top, 2 bottom, 3 left, 4 right
40 | private var percentPosition: Int = 0
41 |
42 | // 渐变配置
43 | private var gradientEnabled: Boolean = false
44 | private var gradientStartColor: Int = Color.GREEN
45 | private var gradientEndColor: Int = Color.YELLOW
46 | private var gradientVertical: Boolean = false
47 |
48 | // 低电量配置
49 | private var lowThreshold: Int = 15
50 | private var lowBlink: Boolean = false
51 | private var vibrateOnLow: Boolean = false
52 | private var blinkAlpha: Float = 1f
53 | private var blinkAnimator: ValueAnimator? = null
54 | private var lastVibrateTime: Long = 0L
55 |
56 | // 电量动画过渡(显示电量)
57 | private var animatePower: Boolean = false
58 | private var displayPower: Float = 100f // 初始化为100,稍后会在init中更新
59 | private var powerAnimator: ValueAnimator? = null
60 |
61 | private var charging: Boolean = false
62 |
63 | // 充电动画:电量循环变化
64 | private var chargingCycle: Boolean = false // 是否启用充电循环动画(0-100%)
65 | private var chargingAnimator: ValueAnimator? = null
66 |
67 | private var bodyCornerRadius: Float = dp(3f) // 电池身圆角半径
68 | private var headCornerRadius: Float = dp(3f) // 电池头圆角半径
69 | private var borderWidth: Float = dp(2f)
70 | private var headScale: Float = 0.5f // 电池头尺寸比例(相对电池高/宽的比例)
71 | private var headColor: Int = Color.BLACK // 电池头颜色,默认与边框颜色一致
72 |
73 | // 画笔
74 | private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
75 | style = Paint.Style.STROKE
76 | color = Color.BLACK
77 | }
78 | private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
79 | style = Paint.Style.FILL
80 | color = baseColor
81 | }
82 | private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
83 | color = Color.BLACK
84 | textAlign = Paint.Align.CENTER
85 | textSize = sp(12f)
86 | }
87 |
88 | // 几何区域
89 | private val bodyRect = RectF()
90 | private val headRect = RectF()
91 |
92 | init {
93 | if (attrs != null) {
94 | val ta: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.Battery)
95 | baseColor = ta.getColor(R.styleable.Battery_batteryColor, Color.WHITE)
96 | orientation = ta.getInt(R.styleable.Battery_batteryOrientation, 0)
97 | power = clamp(ta.getInt(R.styleable.Battery_batteryPower, 100), 0, 100)
98 |
99 | // 新增可选属性
100 | if (ta.hasValue(R.styleable.Battery_batteryShowPercent))
101 | showPercent = ta.getBoolean(R.styleable.Battery_batteryShowPercent, false)
102 | if (ta.hasValue(R.styleable.Battery_batteryCharging))
103 | charging = ta.getBoolean(R.styleable.Battery_batteryCharging, false)
104 | // 外观配置 - 电池身
105 | if (ta.hasValue(R.styleable.Battery_batteryCornerRadius)) {
106 | val radius = ta.getDimension(R.styleable.Battery_batteryCornerRadius, bodyCornerRadius)
107 | bodyCornerRadius = radius
108 | headCornerRadius = radius // 默认电池头和电池身圆角相同
109 | }
110 | if (ta.hasValue(R.styleable.Battery_batteryBodyCornerRadius))
111 | bodyCornerRadius = ta.getDimension(R.styleable.Battery_batteryBodyCornerRadius, bodyCornerRadius)
112 | if (ta.hasValue(R.styleable.Battery_batteryBorderWidth))
113 | borderWidth = ta.getDimension(R.styleable.Battery_batteryBorderWidth, borderWidth)
114 | if (ta.hasValue(R.styleable.Battery_batteryBorderColor))
115 | borderPaint.color = ta.getColor(R.styleable.Battery_batteryBorderColor, Color.BLACK)
116 |
117 | // 外观配置 - 电池头
118 | if (ta.hasValue(R.styleable.Battery_batteryHeadCornerRadius))
119 | headCornerRadius = ta.getDimension(R.styleable.Battery_batteryHeadCornerRadius, headCornerRadius)
120 | if (ta.hasValue(R.styleable.Battery_batteryHeadScale))
121 | headScale = ta.getFloat(R.styleable.Battery_batteryHeadScale, headScale)
122 | if (ta.hasValue(R.styleable.Battery_batteryHeadColor))
123 | headColor = ta.getColor(R.styleable.Battery_batteryHeadColor, Color.BLACK)
124 |
125 | // 百分比文本
126 | if (ta.hasValue(R.styleable.Battery_batteryPercentColor))
127 | percentColor = ta.getColor(R.styleable.Battery_batteryPercentColor, Color.BLACK)
128 | if (ta.hasValue(R.styleable.Battery_batteryPercentSize))
129 | percentSizePx = ta.getDimension(R.styleable.Battery_batteryPercentSize, percentSizePx)
130 | if (ta.hasValue(R.styleable.Battery_batteryPercentPosition))
131 | percentPosition = ta.getInt(R.styleable.Battery_batteryPercentPosition, 0)
132 |
133 | // 渐变与动画过渡
134 | if (ta.hasValue(R.styleable.Battery_batteryGradientEnabled))
135 | gradientEnabled = ta.getBoolean(R.styleable.Battery_batteryGradientEnabled, false)
136 | if (ta.hasValue(R.styleable.Battery_batteryGradientStartColor))
137 | gradientStartColor = ta.getColor(R.styleable.Battery_batteryGradientStartColor, gradientStartColor)
138 | if (ta.hasValue(R.styleable.Battery_batteryGradientEndColor))
139 | gradientEndColor = ta.getColor(R.styleable.Battery_batteryGradientEndColor, gradientEndColor)
140 | if (ta.hasValue(R.styleable.Battery_batteryGradientVertical))
141 | gradientVertical = ta.getBoolean(R.styleable.Battery_batteryGradientVertical, false)
142 | if (ta.hasValue(R.styleable.Battery_batteryAnimatePower))
143 | animatePower = ta.getBoolean(R.styleable.Battery_batteryAnimatePower, false)
144 |
145 | // 低电量
146 | if (ta.hasValue(R.styleable.Battery_batteryLowThreshold))
147 | lowThreshold = ta.getInt(R.styleable.Battery_batteryLowThreshold, lowThreshold)
148 | if (ta.hasValue(R.styleable.Battery_batteryLowBlink))
149 | lowBlink = ta.getBoolean(R.styleable.Battery_batteryLowBlink, lowBlink)
150 | if (ta.hasValue(R.styleable.Battery_batteryVibrateOnLow))
151 | vibrateOnLow = ta.getBoolean(R.styleable.Battery_batteryVibrateOnLow, vibrateOnLow)
152 |
153 | ta.recycle()
154 | }
155 | borderPaint.strokeWidth = borderWidth
156 | // 初始化displayPower为当前power值
157 | displayPower = power.toFloat()
158 | updateCharging(charging)
159 | }
160 |
161 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
162 | // 支持 wrap_content 的默认尺寸
163 | val defW = dp(if (orientation == 0) 60f else 24f).toInt()
164 | val defH = dp(if (orientation == 0) 24f else 60f).toInt()
165 | val w = resolveSize(defW, widthMeasureSpec)
166 | val h = resolveSize(defH, heightMeasureSpec)
167 | setMeasuredDimension(w, h)
168 | }
169 |
170 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
171 | super.onSizeChanged(w, h, oldw, oldh)
172 | val left = paddingLeft.toFloat()
173 | val top = paddingTop.toFloat()
174 | val right = (w - paddingRight).toFloat()
175 | val bottom = (h - paddingBottom).toFloat()
176 |
177 | // 更新边框宽度(随尺寸缩放下限1px)
178 | val size = if (orientation == 0) (right - left) else (bottom - top)
179 | borderWidth = max(1f, size / 60f)
180 | borderPaint.strokeWidth = borderWidth
181 |
182 | if (orientation == 0) {
183 | // 水平:右侧预留电池头
184 | val headW = max(borderWidth, (bottom - top) * 0.25f)
185 | bodyRect.set(
186 | left + borderWidth / 2,
187 | top + borderWidth / 2,
188 | right - headW - borderWidth / 2,
189 | bottom - borderWidth / 2
190 | )
191 | val headH = (bodyRect.height() * headScale).coerceAtLeast(borderWidth)
192 | val cy = bodyRect.centerY()
193 | headRect.set(bodyRect.right, cy - headH / 2, right, cy + headH / 2)
194 | } else {
195 | // 垂直:上方预留电池头
196 | val headH = max(borderWidth, (right - left) * 0.25f)
197 | bodyRect.set(
198 | left + borderWidth / 2,
199 | top + headH + borderWidth / 2,
200 | right - borderWidth / 2,
201 | bottom - borderWidth / 2
202 | )
203 | val headW = (bodyRect.width() * headScale).coerceAtLeast(borderWidth)
204 | val cx = bodyRect.centerX()
205 | headRect.set(cx - headW / 2, top, cx + headW / 2, top + headH)
206 | }
207 | }
208 |
209 | override fun onDraw(canvas: Canvas) {
210 | super.onDraw(canvas)
211 | // 外框(使用电池身圆角)
212 | canvas.drawRoundRect(bodyRect, bodyCornerRadius, bodyCornerRadius, borderPaint)
213 |
214 | // 内部可用区域(考虑边框)
215 | val inner = RectF(
216 | bodyRect.left + borderWidth / 2,
217 | bodyRect.top + borderWidth / 2,
218 | bodyRect.right - borderWidth / 2,
219 | bodyRect.bottom - borderWidth / 2
220 | )
221 |
222 | // 低电量闪烁:通过 alpha 控制填充透明度
223 | val isLow = displayPower <= lowThreshold
224 | val effectiveAlpha = if (lowBlink && isLow) (blinkAlpha * 255).toInt() else 255
225 |
226 | // 颜色或渐变
227 | val baseFillColor = when {
228 | displayPower < 30f -> Color.RED
229 | displayPower < 50f -> Color.YELLOW
230 | else -> baseColor.takeIf { it != Color.WHITE } ?: Color.GREEN
231 | }
232 | fillPaint.color = baseFillColor
233 | fillPaint.alpha = effectiveAlpha
234 | if (gradientEnabled) {
235 | fillPaint.shader = if (!gradientVertical) {
236 | LinearGradient(inner.left, inner.centerY(), inner.right, inner.centerY(),
237 | intArrayOf(gradientStartColor, gradientEndColor), floatArrayOf(0f, 1f), Shader.TileMode.CLAMP)
238 | } else {
239 | LinearGradient(inner.centerX(), inner.top, inner.centerX(), inner.bottom,
240 | intArrayOf(gradientStartColor, gradientEndColor), floatArrayOf(0f, 1f), Shader.TileMode.CLAMP)
241 | }
242 | } else {
243 | fillPaint.shader = null
244 | }
245 |
246 | // 建立圆角裁剪路径,确保内填充与外框圆角贴合(使用电池身圆角)
247 | val path = Path()
248 | path.addRoundRect(inner, bodyCornerRadius, bodyCornerRadius, Path.Direction.CW)
249 | canvas.save()
250 | canvas.clipPath(path)
251 |
252 | // 根据电量绘制填充矩形(被圆角路径裁剪)
253 | if (orientation == 0) {
254 | val widthUsable = inner.right - inner.left
255 | val r = inner.left + widthUsable * (displayPower / 100f)
256 | if (r > inner.left) canvas.drawRect(inner.left, inner.top, r, inner.bottom, fillPaint)
257 | } else {
258 | val heightUsable = inner.bottom - inner.top
259 | val t = inner.bottom - heightUsable * (displayPower / 100f)
260 | if (t < inner.bottom) canvas.drawRect(inner.left, t, inner.right, inner.bottom, fillPaint)
261 | }
262 |
263 | canvas.restore()
264 |
265 | // 电池头:使用独立的headColor,可与边框颜色不同
266 | // 贴近长框的两个角为直角,远离长框的两个角为圆角
267 | val headPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
268 | style = Paint.Style.FILL
269 | color = headColor
270 | }
271 |
272 | // 使用Path绘制电池头,实现部分圆角
273 | // 贴近电池身的边为直角(紧贴),远离电池身的边使用headCornerRadius
274 | val headPath = Path()
275 | if (orientation == 0) {
276 | // 水平电池:电池头在右侧
277 | // 左侧两个角为直角(紧贴电池身),右侧两个角使用headCornerRadius
278 | headPath.moveTo(headRect.left, headRect.top) // 左上角(直角)
279 | headPath.lineTo(headRect.right - headCornerRadius, headRect.top) // 顶边
280 | headPath.arcTo(
281 | headRect.right - headCornerRadius * 2, headRect.top,
282 | headRect.right, headRect.top + headCornerRadius * 2,
283 | -90f, 90f, false
284 | ) // 右上角(圆角)
285 | headPath.lineTo(headRect.right, headRect.bottom - headCornerRadius) // 右边
286 | headPath.arcTo(
287 | headRect.right - headCornerRadius * 2, headRect.bottom - headCornerRadius * 2,
288 | headRect.right, headRect.bottom,
289 | 0f, 90f, false
290 | ) // 右下角(圆角)
291 | headPath.lineTo(headRect.left, headRect.bottom) // 底边
292 | headPath.close() // 左下角(直角)
293 | } else {
294 | // 垂直电池:电池头在上侧
295 | // 下侧两个角为直角(紧贴电池身),上侧两个角使用headCornerRadius
296 | headPath.moveTo(headRect.left, headRect.bottom) // 左下角(直角)
297 | headPath.lineTo(headRect.left, headRect.top + headCornerRadius) // 左边
298 | headPath.arcTo(
299 | headRect.left, headRect.top,
300 | headRect.left + headCornerRadius * 2, headRect.top + headCornerRadius * 2,
301 | 180f, 90f, false
302 | ) // 左上角(圆角)
303 | headPath.lineTo(headRect.right - headCornerRadius, headRect.top) // 顶边
304 | headPath.arcTo(
305 | headRect.right - headCornerRadius * 2, headRect.top,
306 | headRect.right, headRect.top + headCornerRadius * 2,
307 | -90f, 90f, false
308 | ) // 右上角(圆角)
309 | headPath.lineTo(headRect.right, headRect.bottom) // 右边
310 | headPath.close() // 右下角(直角)
311 | }
312 |
313 | canvas.drawPath(headPath, headPaint)
314 |
315 | // 百分比文本
316 | if (showPercent) drawPercentText(canvas, inner)
317 | }
318 |
319 |
320 |
321 | private fun drawPercentText(canvas: Canvas, inner: RectF) {
322 | val pInt = displayPower.toInt().coerceIn(0, 100)
323 | val txt = "$pInt%"
324 | // 文本大小与颜色:优先使用自定义
325 | textPaint.textSize = percentSizePx
326 | textPaint.color = percentColor ?: Color.BLACK
327 | val fm = textPaint.fontMetrics
328 | val centerBaseline = inner.centerY() - (fm.ascent + fm.descent) / 2
329 | when (percentPosition) {
330 | 0 -> {
331 | // 居中
332 | textPaint.textAlign = Paint.Align.CENTER
333 | canvas.drawText(txt, inner.centerX(), centerBaseline, textPaint)
334 | }
335 | 1 -> {
336 | // 顶部
337 | textPaint.textAlign = Paint.Align.CENTER
338 | canvas.drawText(txt, inner.centerX(), inner.top - fm.ascent, textPaint)
339 | }
340 | 2 -> {
341 | // 底部
342 | textPaint.textAlign = Paint.Align.CENTER
343 | canvas.drawText(txt, inner.centerX(), inner.bottom - fm.descent, textPaint)
344 | }
345 | 3 -> {
346 | // 左侧:无论水平/垂直,均贴左并垂直居中
347 | textPaint.textAlign = Paint.Align.LEFT
348 | canvas.drawText(txt, inner.left + dp(6f), centerBaseline, textPaint)
349 | }
350 | 4 -> {
351 | // 右侧:无论水平/垂直,均贴右并垂直居中
352 | textPaint.textAlign = Paint.Align.RIGHT
353 | canvas.drawText(txt, inner.right - dp(6f), centerBaseline, textPaint)
354 | }
355 | }
356 | }
357 |
358 | // Kotlin 公开 API
359 | fun setPower(value: Int) {
360 | val newVal = clamp(value, 0, 100)
361 | if (newVal != power) {
362 | val prev = power
363 | power = newVal
364 | if (animatePower) animateDisplayPower(prev.toFloat(), newVal.toFloat()) else {
365 | displayPower = newVal.toFloat()
366 | postInvalidateOnAnimation()
367 | }
368 | //
369 | handleLowBatteryTransition(prev, newVal)
370 | }
371 | }
372 | private fun handleLowBatteryTransition(prev: Int, now: Int) {
373 | val crossedToLow = prev > lowThreshold && now <= lowThreshold
374 | val stillLow = now <= lowThreshold
375 | if (lowBlink) updateBlink(stillLow)
376 | if (vibrateOnLow && crossedToLow) vibrateOnce()
377 | }
378 |
379 | private fun updateBlink(enable: Boolean) {
380 | blinkAnimator?.cancel()
381 | if (enable) {
382 | blinkAnimator = ValueAnimator.ofFloat(0.4f, 1f).apply {
383 | duration = 600
384 | repeatMode = ValueAnimator.REVERSE
385 | repeatCount = ValueAnimator.INFINITE
386 | addUpdateListener {
387 | blinkAlpha = it.animatedValue as Float
388 | postInvalidateOnAnimation()
389 | }
390 | start()
391 | }
392 | } else {
393 | blinkAlpha = 1f
394 | }
395 | }
396 |
397 | private fun vibrateOnce() {
398 | val now = System.currentTimeMillis()
399 | if (now - lastVibrateTime < 3000) return //
400 | lastVibrateTime = now
401 | try {
402 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
403 | val vm = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
404 | vm.defaultVibrator.vibrate(VibrationEffect.createOneShot(120, VibrationEffect.DEFAULT_AMPLITUDE))
405 | } else {
406 | val v = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
407 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
408 | v.vibrate(VibrationEffect.createOneShot(120, VibrationEffect.DEFAULT_AMPLITUDE))
409 | } else {
410 | @Suppress("DEPRECATION") v.vibrate(120)
411 | }
412 | }
413 | } catch (_: Throwable) { }
414 | }
415 |
416 | private fun animateDisplayPower(from: Float, to: Float) {
417 | powerAnimator?.cancel()
418 | powerAnimator = ValueAnimator.ofFloat(from, to).apply {
419 | duration = (300 + kotlin.math.abs(to - from) * 5).toLong()
420 | addUpdateListener {
421 | displayPower = (it.animatedValue as Float).coerceIn(0f, 100f)
422 | postInvalidateOnAnimation()
423 | }
424 | start()
425 | }
426 | }
427 |
428 |
429 | fun getPower(): Int = power
430 |
431 | fun setColor(@ColorInt color: Int) {
432 | baseColor = color
433 | postInvalidateOnAnimation()
434 | }
435 |
436 | // 新增公开 Setter:便于在 Activity 中实时控制
437 | fun setGradientEnabled(enabled: Boolean) {
438 | if (gradientEnabled != enabled) {
439 | gradientEnabled = enabled
440 | postInvalidateOnAnimation()
441 | }
442 | }
443 |
444 | fun setLowBlink(enabled: Boolean) {
445 | if (lowBlink != enabled) {
446 | lowBlink = enabled
447 | updateBlink(enabled && power <= lowThreshold)
448 | postInvalidateOnAnimation()
449 | }
450 | }
451 |
452 | // 控制:是否启用电量动画过渡(外部可动态切换)
453 | fun setAnimatePower(enabled: Boolean) {
454 | animatePower = enabled
455 | }
456 |
457 | fun setVibrateOnLow(enabled: Boolean) {
458 | vibrateOnLow = enabled
459 | }
460 |
461 | // 百分比文本动态配置
462 | fun setPercentColorInt(@ColorInt color: Int) {
463 | percentColor = color
464 | postInvalidateOnAnimation()
465 | }
466 | fun clearPercentColor() {
467 | percentColor = null
468 | postInvalidateOnAnimation()
469 | }
470 | fun setPercentSizeSp(sizeSp: Float) {
471 | percentSizePx = sp(sizeSp)
472 | postInvalidateOnAnimation()
473 | }
474 | fun setPercentPosition(pos: Int) {
475 | percentPosition = pos.coerceIn(0, 4)
476 | postInvalidateOnAnimation()
477 | }
478 |
479 | // 渐变动态配置
480 | fun setGradientStartColor(@ColorInt color: Int) { gradientStartColor = color; postInvalidateOnAnimation() }
481 | fun setGradientEndColor(@ColorInt color: Int) { gradientEndColor = color; postInvalidateOnAnimation() }
482 | fun setGradientVertical(enabled: Boolean) { gradientVertical = enabled; postInvalidateOnAnimation() }
483 |
484 |
485 |
486 | fun setOrientation(orientation: Int) {
487 | val newVal = if (orientation == 1) 1 else 0
488 | if (newVal != this.orientation) {
489 | this.orientation = newVal
490 | requestLayout()
491 | }
492 | }
493 |
494 | fun setShowPercent(show: Boolean) {
495 | if (showPercent != show) {
496 | showPercent = show
497 | postInvalidateOnAnimation()
498 | }
499 | }
500 |
501 | fun setCharging(enable: Boolean) {
502 | if (charging != enable) {
503 | charging = enable
504 | updateCharging(enable)
505 | postInvalidateOnAnimation()
506 | }
507 | }
508 |
509 | /**
510 | * 设置充电循环动画开关
511 | * @param enable true=启用0-100循环动画,false=关闭循环动画
512 | */
513 | fun setChargingCycle(enable: Boolean) {
514 | if (chargingCycle != enable) {
515 | chargingCycle = enable
516 | // 如果当前正在充电,需要重新启动/停止动画
517 | if (charging) {
518 | updateCharging(true)
519 | }
520 | }
521 | }
522 |
523 | fun setBodyCornerRadius(radiusPx: Float) {
524 | bodyCornerRadius = max(0f, radiusPx)
525 | postInvalidateOnAnimation()
526 | }
527 |
528 | fun setHeadCornerRadius(radiusPx: Float) {
529 | headCornerRadius = max(0f, radiusPx)
530 | postInvalidateOnAnimation()
531 | }
532 |
533 | // 兼容旧方法,同时设置电池身和电池头圆角
534 | fun setCornerRadius(radiusPx: Float) {
535 | bodyCornerRadius = max(0f, radiusPx)
536 | headCornerRadius = max(0f, radiusPx)
537 | postInvalidateOnAnimation()
538 | }
539 |
540 | fun setBorderWidth(widthPx: Float) {
541 | borderWidth = max(1f, widthPx)
542 | borderPaint.strokeWidth = borderWidth
543 | requestLayout()
544 | }
545 |
546 | fun setBorderColor(@ColorInt color: Int) {
547 | borderPaint.color = color
548 | postInvalidateOnAnimation()
549 | }
550 |
551 | fun setHeadScale(scale: Float) {
552 | headScale = scale.coerceIn(0.2f, 0.8f)
553 | requestLayout()
554 | }
555 |
556 | fun setHeadColor(@ColorInt color: Int) {
557 | headColor = color
558 | postInvalidateOnAnimation()
559 | }
560 |
561 | /**
562 | * 更新充电动画
563 | * 支持两种模式:
564 | * 1. chargingCycle=false: 不显示循环动画
565 | * 2. chargingCycle=true: 电量从0到100循环变化(单向,不下降)
566 | */
567 | private fun updateCharging(enable: Boolean) {
568 | chargingAnimator?.cancel()
569 | if (enable && chargingCycle) {
570 | // 电量循环动画:0% -> 100%(单向循环,到100后立即跳回0)
571 | chargingAnimator = ValueAnimator.ofInt(0, 100).apply {
572 | duration = 3000L // 3秒从0到100
573 | repeatMode = ValueAnimator.RESTART // 重新开始(跳回0)
574 | repeatCount = ValueAnimator.INFINITE
575 | interpolator = android.view.animation.LinearInterpolator() // 线性变化
576 | addUpdateListener {
577 | val animatedPower = it.animatedValue as Int
578 | // 直接更新displayPower,不触发setPower
579 | displayPower = animatedPower.toFloat()
580 | postInvalidateOnAnimation()
581 | }
582 | start()
583 | }
584 | } else {
585 | // 停止充电或关闭循环时,恢复到实际电量
586 | displayPower = power.toFloat()
587 | postInvalidateOnAnimation()
588 | }
589 | }
590 |
591 | private fun dp(v: Float): Float = TypedValue.applyDimension(
592 | TypedValue.COMPLEX_UNIT_DIP, v, resources.displayMetrics
593 | )
594 |
595 | private fun sp(v: Float): Float = TypedValue.applyDimension(
596 | TypedValue.COMPLEX_UNIT_SP, v, resources.displayMetrics
597 | )
598 |
599 | private fun clamp(v: Int, minV: Int, maxV: Int) = max(minV, min(maxV, v))
600 | }
601 |
602 |
--------------------------------------------------------------------------------
/app/src/main/java/com/donkor/demo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.donkor.demo
2 |
3 | import android.graphics.Color
4 | import android.os.Bundle
5 | import android.os.Handler
6 | import android.os.Looper
7 | import android.widget.SeekBar
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.appcompat.widget.SwitchCompat
10 |
11 | import android.view.LayoutInflater
12 | import android.view.View
13 | import android.widget.Button
14 | import android.widget.TextView
15 | import androidx.appcompat.app.AlertDialog
16 |
17 | /**
18 | * 页面:MainActivity(Kotlin版)- 简洁弹窗设计
19 | * 说明:演示水平与垂直 BatteryView 的使用,采用简洁主界面+弹窗设置的设计
20 | * author : Donkor , 创建的日期(2025-10-14)
21 | */
22 | class MainActivity : AppCompatActivity() {
23 |
24 | // 主界面UI控件
25 | private lateinit var tvPowerValue: TextView
26 | private lateinit var switchShowPercent: SwitchCompat
27 | private lateinit var switchCharging: SwitchCompat
28 | private lateinit var switchGradient: SwitchCompat
29 | private lateinit var switchLowBlink: SwitchCompat
30 | private lateinit var switchVibrate: SwitchCompat
31 | private lateinit var seekPower: SeekBar
32 |
33 | // 充电循环动画状态(仅在弹窗中控制)
34 | private var chargingCycleEnabled: Boolean = false
35 |
36 | // 设置按钮
37 | private lateinit var btnPowerSettings: Button
38 | private lateinit var btnBasicSettings: Button
39 | private lateinit var btnTextSettings: Button
40 | private lateinit var btnGradientSettings: Button
41 | private lateinit var btnSizeSettings: Button
42 | private lateinit var btnAppearanceSettings: Button
43 | private lateinit var btnResetAll: Button
44 |
45 | // 隐藏的控件引用(用于兼容性)
46 | private lateinit var seekPercentSize: SeekBar
47 | private lateinit var rgPercentPos: android.widget.RadioGroup
48 | private lateinit var switchGradientVertical: SwitchCompat
49 | private lateinit var btnPickPercentColor: Button
50 | private lateinit var btnPickGradStartColor: Button
51 | private lateinit var btnPickGradEndColor: Button
52 | private lateinit var btnPickBorderColor: Button
53 | private lateinit var btnPickHeadColor: Button
54 | private lateinit var seekCornerRadius: SeekBar
55 | private lateinit var seekHeadCornerRadius: SeekBar
56 | private lateinit var seekBorderWidth: SeekBar
57 | private lateinit var seekHeadScale: SeekBar
58 | private lateinit var seekHBatteryWidth: SeekBar
59 | private lateinit var seekHBatteryHeight: SeekBar
60 | private lateinit var seekVBatteryWidth: SeekBar
61 | private lateinit var seekVBatteryHeight: SeekBar
62 |
63 | private lateinit var horizontalBattery: BatteryView
64 | private lateinit var verticalBattery: BatteryView
65 |
66 | private var power: Int = 0
67 | private val handler = Handler(Looper.getMainLooper())
68 |
69 |
70 | private val ticker = object : Runnable {
71 | override fun run() {
72 | power += 5
73 | if (power > 100) power = 0
74 | horizontalBattery.setPower(power)
75 | // 100ms 刷新
76 | handler.postDelayed(this, 100)
77 | }
78 | }
79 |
80 | override fun onCreate(savedInstanceState: Bundle?) {
81 | super.onCreate(savedInstanceState)
82 | setContentView(R.layout.activity_main)
83 |
84 | // 初始化视图
85 | initViews()
86 |
87 | // 初始化监听器
88 | setupListeners()
89 | }
90 |
91 | /**
92 | * 初始化所有视图控件
93 | */
94 | private fun initViews() {
95 | horizontalBattery = findViewById(R.id.horizontalBattery)
96 | verticalBattery = findViewById(R.id.verticalBattery)
97 | tvPowerValue = findViewById(R.id.tvPowerValue)
98 |
99 | // 主界面按钮
100 | btnPowerSettings = findViewById(R.id.btnPowerSettings)
101 | btnBasicSettings = findViewById(R.id.btnBasicSettings)
102 | btnTextSettings = findViewById(R.id.btnTextSettings)
103 | btnGradientSettings = findViewById(R.id.btnGradientSettings)
104 | btnSizeSettings = findViewById(R.id.btnSizeSettings)
105 | btnAppearanceSettings = findViewById(R.id.btnAppearanceSettings)
106 | btnResetAll = findViewById(R.id.btnResetAll)
107 |
108 | // 控件:控制水平电池视图
109 | switchShowPercent = findViewById(R.id.switchShowPercent)
110 | switchCharging = findViewById(R.id.switchCharging)
111 | switchGradient = findViewById(R.id.switchGradient)
112 | switchLowBlink = findViewById(R.id.switchLowBlink)
113 | switchVibrate = findViewById(R.id.switchVibrate)
114 | seekPower = findViewById(R.id.seekPower)
115 |
116 | // 初始化:先临时关闭电量动画,确保初始文本与进度条完全一致
117 | horizontalBattery.setAnimatePower(false)
118 | verticalBattery.setAnimatePower(false)
119 |
120 | // 初始化可视状态(与 XML 默认值一致)
121 | switchShowPercent.isChecked = true
122 | switchCharging.isChecked = true
123 | switchGradient.isChecked = true
124 | switchLowBlink.isChecked = true
125 | switchVibrate.isChecked = true
126 | seekPower.progress = horizontalBattery.getPower()
127 | verticalBattery.setPower(seekPower.progress)
128 |
129 | // 更新电量显示文本
130 | updatePowerValueText(seekPower.progress)
131 |
132 | horizontalBattery.setAnimatePower(true)
133 | verticalBattery.setAnimatePower(true)
134 |
135 | // 确保垂直电池使用正确的颜色(不设置统一颜色,让它根据电量自动变色)
136 | // 移除之前的 verticalBattery.setColor(Color.BLACK) 调用
137 | }
138 |
139 | /**
140 | * 设置所有监听器
141 | */
142 | private fun setupListeners() {
143 | // 监听:实时改变 BatteryView
144 | switchShowPercent.setOnCheckedChangeListener { _, checked ->
145 | horizontalBattery.setShowPercent(checked)
146 | verticalBattery.setShowPercent(checked)
147 | }
148 | switchCharging.setOnCheckedChangeListener { _, checked ->
149 | horizontalBattery.setCharging(checked)
150 | verticalBattery.setCharging(checked)
151 | }
152 |
153 | switchGradient.setOnCheckedChangeListener { _, checked ->
154 | horizontalBattery.setGradientEnabled(checked)
155 | verticalBattery.setGradientEnabled(checked)
156 | }
157 | switchLowBlink.setOnCheckedChangeListener { _, checked ->
158 | horizontalBattery.setLowBlink(checked)
159 | verticalBattery.setLowBlink(checked)
160 | }
161 | switchVibrate.setOnCheckedChangeListener { _, checked ->
162 | horizontalBattery.setVibrateOnLow(checked)
163 | verticalBattery.setVibrateOnLow(checked)
164 | }
165 |
166 | // 电量SeekBar监听
167 | seekPower.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
168 | override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
169 | horizontalBattery.setPower(progress)
170 | verticalBattery.setPower(progress)
171 | updatePowerValueText(progress)
172 | }
173 | override fun onStartTrackingTouch(sb: SeekBar?) {}
174 | override fun onStopTrackingTouch(sb: SeekBar?) {}
175 | })
176 |
177 | // 绑定:电池大小(分开控制)
178 | seekHBatteryWidth = findViewById(R.id.seekHBatteryWidth)
179 | seekHBatteryHeight = findViewById(R.id.seekHBatteryHeight)
180 | seekVBatteryWidth = findViewById(R.id.seekVBatteryWidth)
181 | seekVBatteryHeight = findViewById(R.id.seekVBatteryHeight)
182 |
183 | // 工具:px/dp 转换
184 | val density = resources.displayMetrics.density
185 | fun px2dp(px: Int): Int = (px / density).toInt()
186 | fun dp2px(dp: Int): Int = (dp * density).toInt()
187 |
188 | // 初始化:以各自当前尺寸为初值
189 | val hLp = horizontalBattery.layoutParams
190 | val vLp = verticalBattery.layoutParams
191 | val hWdp = if (hLp.width > 0) px2dp(hLp.width) else 120
192 | val hHdp = if (hLp.height > 0) px2dp(hLp.height) else 36
193 | val vWdp = if (vLp.width > 0) px2dp(vLp.width) else 24
194 | val vHdp = if (vLp.height > 0) px2dp(vLp.height) else 72
195 | listOf(seekHBatteryWidth, seekHBatteryHeight, seekVBatteryWidth, seekVBatteryHeight).forEach { it.max = 300 }
196 | seekHBatteryWidth.progress = hWdp.coerceIn(16, 300)
197 | seekHBatteryHeight.progress = hHdp.coerceIn(16, 300)
198 | seekVBatteryWidth.progress = vWdp.coerceIn(16, 300)
199 | seekVBatteryHeight.progress = vHdp.coerceIn(16, 300)
200 |
201 | // 应用:仅影响对应电池
202 | fun applyHSize(wDp: Int? = null, hDp: Int? = null) {
203 | val lp = horizontalBattery.layoutParams
204 | wDp?.coerceIn(16, 300)?.let { lp.width = dp2px(it) }
205 | hDp?.coerceIn(16, 300)?.let { lp.height = dp2px(it) }
206 | horizontalBattery.layoutParams = lp
207 | horizontalBattery.requestLayout()
208 | }
209 | fun applyVSize(wDp: Int? = null, hDp: Int? = null) {
210 | val lp = verticalBattery.layoutParams
211 | wDp?.coerceIn(16, 300)?.let { lp.width = dp2px(it) }
212 | hDp?.coerceIn(16, 300)?.let { lp.height = dp2px(it) }
213 | verticalBattery.layoutParams = lp
214 | verticalBattery.requestLayout()
215 | }
216 |
217 | // 监听:水平电池
218 | seekHBatteryWidth.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
219 | override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
220 | if (fromUser) applyHSize(wDp = progress)
221 | }
222 | override fun onStartTrackingTouch(sb: SeekBar?) {}
223 | override fun onStopTrackingTouch(sb: SeekBar?) {}
224 | })
225 | seekHBatteryHeight.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
226 | override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
227 | if (fromUser) applyHSize(hDp = progress)
228 | }
229 | override fun onStartTrackingTouch(sb: SeekBar?) {}
230 | override fun onStopTrackingTouch(sb: SeekBar?) {}
231 | })
232 |
233 | // 监听:垂直电池
234 | seekVBatteryWidth.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
235 | override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
236 | if (fromUser) applyVSize(wDp = progress)
237 | }
238 | override fun onStartTrackingTouch(sb: SeekBar?) {}
239 | override fun onStopTrackingTouch(sb: SeekBar?) {}
240 | })
241 | seekVBatteryHeight.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
242 | override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
243 | if (fromUser) applyVSize(hDp = progress)
244 | }
245 | override fun onStartTrackingTouch(sb: SeekBar?) {}
246 | override fun onStopTrackingTouch(sb: SeekBar?) {}
247 | })
248 |
249 | // 绑定更多控制项
250 | seekPercentSize = findViewById(R.id.seekPercentSize)
251 | rgPercentPos = findViewById(R.id.rgPercentPos)
252 |
253 | switchGradientVertical = findViewById(R.id.switchGradientVertical)
254 | // 颜色选择按钮
255 | btnPickPercentColor = findViewById(R.id.btnPickPercentColor)
256 | btnPickBorderColor = findViewById(R.id.btnPickBorderColor)
257 | btnPickGradStartColor = findViewById(R.id.btnPickGradStartColor)
258 | btnPickGradEndColor = findViewById(R.id.btnPickGradEndColor)
259 |
260 | // 外观
261 | seekCornerRadius = findViewById(R.id.seekCornerRadius)
262 | seekHeadCornerRadius = findViewById(R.id.seekHeadCornerRadius)
263 | seekBorderWidth = findViewById(R.id.seekBorderWidth)
264 | seekHeadScale = findViewById(R.id.seekHeadScale)
265 |
266 | // 初始化默认值(对齐精简控制)
267 | seekPercentSize.progress = 12 // sp
268 | // 文本位置默认:居中
269 | rgPercentPos.check(R.id.rbPosCenter)
270 |
271 | // 渐变方向默认:横向(false)
272 | switchGradientVertical.isChecked = false
273 |
274 | seekCornerRadius.progress = 6 // dp
275 | seekHeadCornerRadius.progress = 6 // dp
276 | seekBorderWidth.progress = 2 // dp
277 | seekHeadScale.progress = 50 // 0.5
278 |
279 | seekPercentSize.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
280 | override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
281 | val sp = progress.coerceAtLeast(8)
282 | horizontalBattery.setPercentSizeSp(sp.toFloat())
283 | verticalBattery.setPercentSizeSp(sp.toFloat())
284 | }
285 | override fun onStartTrackingTouch(sb: SeekBar?) {}
286 | override fun onStopTrackingTouch(sb: SeekBar?) {}
287 | })
288 | // 百分比文本位置:单选映射到 position 索引
289 | rgPercentPos.setOnCheckedChangeListener { _, checkedId ->
290 | val pos = when (checkedId) {
291 | R.id.rbPosCenter -> 0
292 | R.id.rbPosTop -> 1
293 | R.id.rbPosBottom -> 2
294 | R.id.rbPosStart -> 3
295 | R.id.rbPosEnd -> 4
296 | else -> 0
297 | }
298 | horizontalBattery.setPercentPosition(pos)
299 | verticalBattery.setPercentPosition(pos)
300 | }
301 |
302 | // 监听:渐变配置
303 | switchGradientVertical.setOnCheckedChangeListener { _, checked ->
304 | horizontalBattery.setGradientVertical(checked)
305 | verticalBattery.setGradientVertical(checked)
306 | }
307 |
308 | // 监听:外观
309 | seekCornerRadius.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
310 | override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
311 | horizontalBattery.setCornerRadius(progress.toFloat())
312 | verticalBattery.setCornerRadius(progress.toFloat())
313 | }
314 | override fun onStartTrackingTouch(sb: SeekBar?) {}
315 | override fun onStopTrackingTouch(sb: SeekBar?) {}
316 | })
317 |
318 | btnPickPercentColor.setOnClickListener {
319 | val init = Color.BLACK
320 | showColorPicker("选择百分比颜色", init) { color ->
321 | horizontalBattery.setPercentColorInt(color)
322 | verticalBattery.setPercentColorInt(color)
323 | }
324 | }
325 | btnPickGradStartColor.setOnClickListener {
326 | val init = Color.GREEN
327 | showColorPicker("选择渐变起始色", init) { color ->
328 | horizontalBattery.setGradientStartColor(color)
329 | verticalBattery.setGradientStartColor(color)
330 | }
331 | }
332 | btnPickGradEndColor.setOnClickListener {
333 | val init = Color.YELLOW
334 | showColorPicker("选择渐变结束色", init) { color ->
335 |
336 | horizontalBattery.setGradientEndColor(color)
337 | verticalBattery.setGradientEndColor(color)
338 | }
339 | }
340 |
341 |
342 | seekBorderWidth.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
343 | override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
344 | horizontalBattery.setBorderWidth(progress.toFloat())
345 | verticalBattery.setBorderWidth(progress.toFloat())
346 | }
347 | override fun onStartTrackingTouch(sb: SeekBar?) {}
348 | override fun onStopTrackingTouch(sb: SeekBar?) {}
349 | })
350 |
351 |
352 | btnPickBorderColor.setOnClickListener {
353 | val init = Color.BLACK
354 | showColorPicker("选择边框颜色", init) { color ->
355 | horizontalBattery.setBorderColor(color)
356 | verticalBattery.setBorderColor(color)
357 | }
358 | }
359 |
360 |
361 | seekHeadScale.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
362 | override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
363 | val scale = (progress.coerceIn(20, 80)) / 100f
364 | horizontalBattery.setHeadScale(scale)
365 | verticalBattery.setHeadScale(scale)
366 | }
367 | override fun onStartTrackingTouch(sb: SeekBar?) {}
368 | override fun onStopTrackingTouch(sb: SeekBar?) {}
369 | })
370 |
371 | switchGradient.setOnCheckedChangeListener { _, checked ->
372 | horizontalBattery.setGradientEnabled(checked)
373 | verticalBattery.setGradientEnabled(checked)
374 | }
375 | switchLowBlink.setOnCheckedChangeListener { _, checked ->
376 | horizontalBattery.setLowBlink(checked)
377 | verticalBattery.setLowBlink(checked)
378 | }
379 | switchVibrate.setOnCheckedChangeListener { _, checked ->
380 | horizontalBattery.setVibrateOnLow(checked)
381 | verticalBattery.setVibrateOnLow(checked)
382 | }
383 | seekPower.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
384 | override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
385 | if (fromUser) {
386 | horizontalBattery.setPower(progress)
387 | verticalBattery.setPower(progress)
388 | updatePowerValueText(progress)
389 | }
390 | }
391 | override fun onStartTrackingTouch(sb: SeekBar?) {}
392 | override fun onStopTrackingTouch(sb: SeekBar?) {}
393 | })
394 |
395 | // 设置按钮点击事件
396 | btnPowerSettings.setOnClickListener { showPowerSettingsDialog() }
397 | btnBasicSettings.setOnClickListener { showBasicSettingsDialog() }
398 | btnTextSettings.setOnClickListener { showTextSettingsDialog() }
399 | btnGradientSettings.setOnClickListener { showGradientSettingsDialog() }
400 | btnSizeSettings.setOnClickListener { showSizeSettingsDialog() }
401 | btnAppearanceSettings.setOnClickListener { showAppearanceSettingsDialog() }
402 |
403 | // 重置所有设置
404 | btnResetAll.setOnClickListener {
405 | AlertDialog.Builder(this)
406 | .setTitle("重置确认")
407 | .setMessage("确定要重置所有设置到默认值吗?")
408 | .setPositiveButton("确定") { _, _ ->
409 | // 重置电量
410 | horizontalBattery.setPower(75)
411 | verticalBattery.setPower(75)
412 | seekPower.progress = 75
413 | tvPowerValue.text = "75%"
414 |
415 | // 重置基础功能
416 | switchShowPercent.isChecked = true
417 | horizontalBattery.setShowPercent(true)
418 | verticalBattery.setShowPercent(true)
419 |
420 | switchCharging.isChecked = true
421 | horizontalBattery.setCharging(true)
422 | verticalBattery.setCharging(true)
423 |
424 | chargingCycleEnabled = false
425 | horizontalBattery.setChargingCycle(false)
426 | verticalBattery.setChargingCycle(false)
427 |
428 | switchGradient.isChecked = true
429 | horizontalBattery.setGradientEnabled(true)
430 | verticalBattery.setGradientEnabled(true)
431 |
432 | switchLowBlink.isChecked = true
433 | horizontalBattery.setLowBlink(true)
434 | verticalBattery.setLowBlink(true)
435 |
436 | switchVibrate.isChecked = true
437 | horizontalBattery.setVibrateOnLow(true)
438 | verticalBattery.setVibrateOnLow(true)
439 |
440 | // 提示
441 | android.widget.Toast.makeText(this, "已重置所有设置", android.widget.Toast.LENGTH_SHORT).show()
442 | }
443 | .setNegativeButton("取消", null)
444 | .show()
445 | }
446 | }
447 |
448 | override fun onDestroy() {
449 | super.onDestroy()
450 | handler.removeCallbacks(ticker)
451 | }
452 |
453 | /**
454 | * 更新电量显示文本
455 | */
456 | private fun updatePowerValueText(power: Int) {
457 | tvPowerValue.text = "$power%"
458 | // 电量显示在渐变背景上,使用白色文本
459 | tvPowerValue.setTextColor(Color.WHITE)
460 | }
461 |
462 | /**
463 | * 显示电量设置弹窗
464 | */
465 | private fun showPowerSettingsDialog() {
466 | val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_power_settings, null)
467 | val tvPowerValueDialog = dialogView.findViewById(R.id.tvPowerValueDialog)
468 | val seekPowerDialog = dialogView.findViewById(R.id.seekPowerDialog)
469 |
470 | // 同步当前值
471 | seekPowerDialog.progress = seekPower.progress
472 | tvPowerValueDialog.text = "${seekPower.progress}%"
473 |
474 | // 电量变化
475 | seekPowerDialog.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
476 | override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
477 | seekPower.progress = progress
478 | tvPowerValueDialog.text = "$progress%"
479 | horizontalBattery.setPower(progress)
480 | verticalBattery.setPower(progress)
481 | updatePowerValueText(progress)
482 | }
483 | override fun onStartTrackingTouch(sb: SeekBar?) {}
484 | override fun onStopTrackingTouch(sb: SeekBar?) {}
485 | })
486 |
487 | AlertDialog.Builder(this)
488 | .setView(dialogView)
489 | .setPositiveButton("确定", null)
490 | .show()
491 | }
492 |
493 | /**
494 | * 显示基础功能设置弹窗
495 | */
496 | private fun showBasicSettingsDialog() {
497 | val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_basic_settings, null)
498 | val switchShowPercentDialog = dialogView.findViewById(R.id.switchShowPercentDialog)
499 | val switchChargingDialog = dialogView.findViewById(R.id.switchChargingDialog)
500 | val switchChargingCycleDialog = dialogView.findViewById(R.id.switchChargingCycleDialog)
501 | val switchGradientDialog = dialogView.findViewById(R.id.switchGradientDialog)
502 | val switchLowBlinkDialog = dialogView.findViewById(R.id.switchLowBlinkDialog)
503 | val switchVibrateDialog = dialogView.findViewById(R.id.switchVibrateDialog)
504 |
505 | // 同步当前值
506 | switchShowPercentDialog.isChecked = switchShowPercent.isChecked
507 | switchChargingDialog.isChecked = switchCharging.isChecked
508 | switchChargingCycleDialog.isChecked = chargingCycleEnabled
509 | switchGradientDialog.isChecked = switchGradient.isChecked
510 | switchLowBlinkDialog.isChecked = switchLowBlink.isChecked
511 | switchVibrateDialog.isChecked = switchVibrate.isChecked
512 |
513 | // 显示百分比
514 | switchShowPercentDialog.setOnCheckedChangeListener { _, checked ->
515 | switchShowPercent.isChecked = checked
516 | horizontalBattery.setShowPercent(checked)
517 | verticalBattery.setShowPercent(checked)
518 | }
519 |
520 | // 充电动画
521 | switchChargingDialog.setOnCheckedChangeListener { _, checked ->
522 | switchCharging.isChecked = checked
523 | horizontalBattery.setCharging(checked)
524 | verticalBattery.setCharging(checked)
525 | }
526 |
527 | // 充电循环动画(0-100%)
528 | switchChargingCycleDialog.setOnCheckedChangeListener { _, checked ->
529 | chargingCycleEnabled = checked
530 | horizontalBattery.setChargingCycle(checked)
531 | verticalBattery.setChargingCycle(checked)
532 | }
533 |
534 | // 渐变填充
535 | switchGradientDialog.setOnCheckedChangeListener { _, checked ->
536 | switchGradient.isChecked = checked
537 | horizontalBattery.setGradientEnabled(checked)
538 | verticalBattery.setGradientEnabled(checked)
539 | }
540 |
541 | // 低电量闪烁
542 | switchLowBlinkDialog.setOnCheckedChangeListener { _, checked ->
543 | switchLowBlink.isChecked = checked
544 | horizontalBattery.setLowBlink(checked)
545 | verticalBattery.setLowBlink(checked)
546 | }
547 |
548 | // 低电量震动
549 | switchVibrateDialog.setOnCheckedChangeListener { _, checked ->
550 | switchVibrate.isChecked = checked
551 | horizontalBattery.setVibrateOnLow(checked)
552 | verticalBattery.setVibrateOnLow(checked)
553 | }
554 |
555 | AlertDialog.Builder(this)
556 | .setView(dialogView)
557 | .setPositiveButton("确定", null)
558 | .show()
559 | }
560 |
561 | /**
562 | * 显示文本设置弹窗
563 | */
564 | private fun showTextSettingsDialog() {
565 | val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_text_settings, null)
566 | val btnColor = dialogView.findViewById