Compare commits

...

20 Commits

Author SHA1 Message Date
10348d2c8d <powerbell>APK 15.14.42 release Publish. 2025-12-30 18:16:29 +08:00
21cdc219c8 更新TTS语音服务朗读文本。 2025-12-30 18:14:27 +08:00
3ebf87c642 <powerbell>APK 15.14.41 release Publish. 2025-12-29 21:40:10 +08:00
347a4040cd <powerbell>Start New Stage Version. 2025-12-29 21:37:48 +08:00
6fd86a2742 添加TTS贴心服务,以免在充电时设置了服务提醒却不知道。 2025-12-29 21:36:34 +08:00
798357aedd 源码整理 2025-12-29 09:45:01 +08:00
9124303fd3 <powerbell>APK 15.14.40 release Publish. 2025-12-28 20:43:38 +08:00
414541093a <powerbell>Start New Stage Version. 2025-12-28 20:42:24 +08:00
13265be66e 添加点阵能量块风格。 2025-12-28 20:41:19 +08:00
ebd32adb68 <powerbell>APK 15.14.39 release Publish. 2025-12-28 13:54:20 +08:00
56a65cd10a 能量风格绘图方法切换回老式算法,修复能量条间隔缝隙问题。 2025-12-28 13:52:51 +08:00
e379684002 <powerbell>APK 15.14.38 release Publish. 2025-12-28 13:16:31 +08:00
4077ac18f6 <powerbell>Start New Stage Version. 2025-12-28 13:15:17 +08:00
278e690795 能量条绘图风格调试完成。 2025-12-28 13:14:24 +08:00
e81fc65b90 BatteryStyleView控件调试中。。。 2025-12-27 21:19:20 +08:00
abd956d7d0 添加BatteryStyleView控件,添加能量与斑马绘图风格。 2025-12-27 21:12:54 +08:00
fca17908b2 <powerbell>APK 15.14.37 release Publish. 2025-12-27 14:48:34 +08:00
39a3a5aeb0 更新黑白主题风格。 2025-12-27 14:45:13 +08:00
2e30f577b5 <powerbell>APK 15.14.36 release Publish. 2025-12-26 21:00:10 +08:00
dcb5355233 添加在切换主题时把主题基准色配置为背景图背景像素底色。 2025-12-26 20:58:31 +08:00
24 changed files with 1763 additions and 184 deletions

View File

@@ -87,7 +87,7 @@ dependencies {
//api 'cc.winboll.studio:libappbase:15.12.2'
// WinBoLL备用库 jitpack.io 地址
api 'com.github.ZhanGSKen:AES:aes-v15.12.7'
api 'com.github.ZhanGSKen:AES:aes-v15.12.8'
api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
//api fileTree(dir: 'libs', include: ['*.aar'])

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Fri Dec 26 20:13:09 HKT 2025
stageCount=36
#Tue Dec 30 18:16:29 HKT 2025
stageCount=43
libraryProject=
baseVersion=15.14
publishVersion=15.14.35
publishVersion=15.14.42
buildCount=0
baseBetaVersion=15.14.36
baseBetaVersion=15.14.43

View File

@@ -4,55 +4,84 @@
xmlns:tools="http://schemas.android.com/tools"
package="cc.winboll.studio.powerbell">
<!-- 前台服务权限 -->
<!-- 此应用可显示在其他应用上方 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<!-- 运行前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- 运行“specialUse”类型的前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<!-- 系统事件权限 -->
<!-- 开机启动 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- 通知权限 -->
<!-- 显示通知 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- 应用统计与查询权限 -->
<!-- PACKAGE_USAGE_STATS -->
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
<!-- BATTERY_STATS -->
<uses-permission android:name="android.permission.BATTERY_STATS"/>
<!-- 计算应用存储空间 -->
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<!-- 请求忽略电池优化 -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<!-- 读取您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 修改或删除您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- MANAGE_EXTERNAL_STORAGE -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!-- 拍摄照片和视频 -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions"/>
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission"/>
<!-- 电池与存储统计权限 -->
<uses-permission android:name="android.permission.BATTERY_STATS"/>
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- 硬件特性声明 -->
<uses-feature
android:name="android.hardware.camera"
android:required="false"/>
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false"/>
<!-- 应用查询 -->
<queries>
<package android:name="com.miui.securitycenter"/>
</queries>
<application
android:name=".App"
android:process=":main"
android:process=":main"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
@@ -64,7 +93,6 @@
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<!-- 主活动(主进程) -->
<activity
android:process=":main"
android:name=".MainActivity"
@@ -72,7 +100,6 @@
android:exported="true"
android:launchMode="singleTask"/>
<!-- 活动别名(启动器,主进程) -->
<activity-alias
android:name=".MainActivityEN1"
android:targetActivity=".MainActivity"
@@ -80,13 +107,19 @@
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmainen1"/>
</activity-alias>
<activity-alias
@@ -96,13 +129,19 @@
android:label="@string/app_name_cn1"
android:icon="@drawable/ic_launcher"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn1"/>
</activity-alias>
<activity-alias
@@ -112,16 +151,21 @@
android:label="@string/app_name_cn2"
android:icon="@drawable/ic_launcher"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn2"/>
</activity-alias>
<!-- 功能活动(主进程) -->
<activity
android:process=":main"
android:name=".activities.CrashActivity"
@@ -140,15 +184,25 @@
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/jpeg"/>
<data android:mimeType="image/jpg"/>
<data android:mimeType="image/png"/>
<data android:mimeType="image/webp"/>
<data android:mimeType="image/*"/>
</intent-filter>
</activity>
<activity
@@ -186,39 +240,44 @@
android:name="cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity"
android:exported="false"/>
<!-- 第三方活动(主进程) -->
<activity
android:process=":main"
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true"/>
<!-- 广播接收器(主进程) -->
<receiver
android:process=":main"
android:process=":main"
android:name=".receivers.MainReceiver"
android:enabled="true"
android:exported="true"
android:directBootAware="true">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.POWER_CONNECTED"/>
<action android:name="android.intent.action.USER_PRESENT"/>
</intent-filter>
</receiver>
<!-- 服务ControlCenterService主进程AssistantService独立进程 -->
<service
android:name=".services.ControlCenterService"
android:priority="1000"
android:enabled="true"
android:exported="false"
android:process=":main"
android:stopWithTask="false"
android:stopWithTask="false"
android:foregroundServiceType="dataSync">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="后台核心功能运行、持续保活"/>
</service>
<service
@@ -226,26 +285,41 @@
android:enabled="true"
android:exported="false"
android:process=":assistant"
android:stopWithTask="false"
android:stopWithTask="false"
android:foregroundServiceType="dataSync">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="辅助核心功能运行"/>
</service>
<!-- 内容提供者(主进程) -->
<service
android:name=".services.TTSPlayService"
android:enabled="true"
android:exported="false"
android:process=":main"
android:stopWithTask="false"/>
<service android:name=".services.ThoughtfulService"
android:enabled="true"
android:exported="false"
android:process=":main"
android:stopWithTask="false"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true"
android:process=":main">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider"/>
</provider>
<!-- 元数据 -->
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
@@ -253,4 +327,3 @@
</application>
</manifest>

View File

@@ -17,13 +17,18 @@ import cc.winboll.studio.powerbell.views.MemoryCachedBackgroundView;
/**
* 应用全局入口类适配Android API 30基于Java 7编写
* 核心策略:极致强制缓存 - 无论内存紧张程度永不自动清理任何缓存Bitmap/视图控件/路径记录)
*
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Version 1.0.0
* @Date 2025-12-25
* @Date 2025-12-29
*/
public class App extends GlobalApplication {
// ===================== 常量定义区(按功能分类排序) =====================
public static final String TAG = "App";
// ==================== 常量定义 ====================
private static final String TAG = "App";
private static final String CACHE_PROTECT_TAG = "FORCE_CACHE_PROTECT";
private static final int INVALID_BATTERY_VALUE = -1;
// 组件跳转常量
public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1";
@@ -35,286 +40,269 @@ public class App extends GlobalApplication {
public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1";
public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2";
// 缓存防护常量
private static final String CACHE_PROTECT_TAG = "FORCE_CACHE_PROTECT";
private static final int INVALID_BATTERY_VALUE = -1;
// ==================== 静态属性 ====================
// ===================== 静态属性区(按工具类优先级排序) =====================
// 应用单例
private static App sApp;
// 数据配置工具
// 配置与缓存
public static AppConfigUtils sAppConfigUtils;
private static AppCacheUtils sAppCacheUtils;
// 全局Bitmap缓存工具极致强制保持一旦初始化永不销毁
// 资源与视图缓存(强制驻留
public static BackgroundSourceUtils sBackgroundSourceUtils;
public static BitmapCacheUtils sBitmapCacheUtils;
// 全局视图控件缓存工具(极致强制保持:一旦初始化,永不销毁)
private static MemoryCachedBackgroundView sMemoryCachedBackgroundView;
// 电池状态
// 状态
public static volatile int sQuantityOfElectricity = INVALID_BATTERY_VALUE;
// 通知管理工具
// 系统工具
private static NotificationManagerUtils sNotificationManagerUtils;
// ===================== 成员属性区(按生命周期关联度排序) =====================
// 全局广播接收器
// ==================== 成员属性 ====================
private GlobalApplicationReceiver mGlobalReceiver;
// ===================== 公共静态方法区(单例/工具类实例获取) =====================
// ==================== 公共静态方法 (工具/单例) ====================
/**
* 获取应用单例实例
* 获取应用单例
*/
public static App getInstance() {
LogUtils.d(TAG, "getInstance() 调用 | 返回实例:" + sApp);
LogUtils.d(TAG, "getInstance() called | Result: " + sApp);
return sApp;
}
/**
* 获取应用配置工具实例
* 获取配置工具实例
*/
public static AppConfigUtils getAppConfigUtils(Context context) {
String contextType = context != null ? context.getClass().getSimpleName() : "null";
LogUtils.d(TAG, "getAppConfigUtils() 调用 | 入参Context类型:" + contextType);
String contextName = context != null ? context.getClass().getSimpleName() : "null";
LogUtils.d(TAG, "getAppConfigUtils() called with: context = [" + contextName + "]");
if (sAppConfigUtils == null) {
sAppConfigUtils = AppConfigUtils.getInstance(context);
LogUtils.d(TAG, "getAppConfigUtils() | AppConfigUtils实例初始化完成");
LogUtils.d(TAG, "getAppConfigUtils: Initialized new instance");
}
LogUtils.d(TAG, "getAppConfigUtils() | 返回实例:" + sAppConfigUtils);
return sAppConfigUtils;
}
/**
* 获取应用缓存工具实例
* 获取缓存工具实例
*/
public static AppCacheUtils getAppCacheUtils(Context context) {
String contextType = context != null ? context.getClass().getSimpleName() : "null";
LogUtils.d(TAG, "getAppCacheUtils() 调用 | 入参Context类型:" + contextType);
String contextName = context != null ? context.getClass().getSimpleName() : "null";
LogUtils.d(TAG, "getAppCacheUtils() called with: context = [" + contextName + "]");
if (sAppCacheUtils == null) {
sAppCacheUtils = AppCacheUtils.getInstance(context);
LogUtils.d(TAG, "getAppCacheUtils() | AppCacheUtils实例初始化完成");
LogUtils.d(TAG, "getAppCacheUtils: Initialized new instance");
}
LogUtils.d(TAG, "getAppCacheUtils() | 返回实例:" + sAppCacheUtils);
return sAppCacheUtils;
}
// ===================== 公共成员方法区(业务功能) =====================
// ==================== 公共成员方法 (业务) ====================
/**
* 清除电池历史数据
*/
public void clearBatteryHistory() {
LogUtils.d(TAG, "clearBatteryHistory() 调用");
LogUtils.d(TAG, "clearBatteryHistory() called");
if (sAppCacheUtils != null) {
sAppCacheUtils.clearBatteryHistory();
LogUtils.d(TAG, "clearBatteryHistory() | 电池历史数据清除成功");
LogUtils.d(TAG, "clearBatteryHistory: Success");
} else {
LogUtils.w(TAG, "clearBatteryHistory() | 失败:AppCacheUtils未初始化");
LogUtils.w(TAG, "clearBatteryHistory: Failed, sAppCacheUtils is null");
}
}
/**
* 手动清理所有缓存(带严格权限控制,仅主动调用生效)
* 极致强制缓存策略下,仅提供手动清理入口,永不自动调用
* 手动清理所有缓存(仅主动调用生效)
*/
public static void manualClearAllCache() {
LogUtils.w(CACHE_PROTECT_TAG, "manualClearAllCache() 调用 | 极致缓存策略下谨慎使用");
LogUtils.w(CACHE_PROTECT_TAG, "manualClearAllCache() called - Manual trigger only");
// 清理Bitmap缓存
if (sBitmapCacheUtils != null) {
sBitmapCacheUtils.clearAllCache();
LogUtils.d(CACHE_PROTECT_TAG, "manualClearAllCache() | Bitmap缓存手动清理完成");
LogUtils.d(CACHE_PROTECT_TAG, "manualClearAllCache: Bitmap cache cleared");
}
// 清理视图控件缓存(仅清除静态引用,不销毁实例
// 仅置空引用,不销毁实例(符合极致缓存策略
if (sMemoryCachedBackgroundView != null) {
LogUtils.d(CACHE_PROTECT_TAG, "manualClearAllCache() | 视图缓存保留实例,仅清除静态引用");
LogUtils.d(CACHE_PROTECT_TAG, "manualClearAllCache: View cache reference cleared");
sMemoryCachedBackgroundView = null;
}
LogUtils.w(CACHE_PROTECT_TAG, "manualClearAllCache() | 手动清理完成 | 部分缓存实例仍驻留内存");
LogUtils.w(CACHE_PROTECT_TAG, "manualClearAllCache: Manual cleanup finished");
}
/**
* 获取视图控件缓存实例非通用仅通过App实例调用避免全局直接访问
* 获取视图缓存实例
*/
public MemoryCachedBackgroundView getMemoryCachedBackgroundView() {
LogUtils.d(TAG, "getMemoryCachedBackgroundView() 调用 | 当前实例:" + sMemoryCachedBackgroundView);
LogUtils.d(TAG, "getMemoryCachedBackgroundView() called | Current: " + sMemoryCachedBackgroundView);
return sMemoryCachedBackgroundView;
}
/**
* 发送调试通知
* 发送通知消息
*/
public static void notifyMessage(String title, String content) {
LogUtils.d(TAG, "notifyMessage() 调用 | 入参title" + title + " | content" + content);
LogUtils.d(TAG, "notifyMessage() called with: title = [" + title + "], content = [" + content + "]");
boolean canNotify = isDebugging() && sApp != null && sNotificationManagerUtils != null;
if (canNotify) {
boolean canSend = isDebugging() && sApp != null && sNotificationManagerUtils != null;
if (canSend) {
NotificationMessage message = new NotificationMessage(title, content, "");
sNotificationManagerUtils.showMessageNotification(sApp, message);
LogUtils.d(TAG, "notifyMessage() | 调试通知发送成功");
LogUtils.d(TAG, "notifyMessage: Sent successfully");
} else {
LogUtils.d(TAG, "notifyMessage() | 发送失败:调试模式未开启/工具类未初始化");
LogUtils.d(TAG, "notifyMessage: Send failed, conditions not met");
}
}
// ===================== 生命周期方法区(按执行顺序排序) =====================
// ==================== 生命周期方法 ====================
@Override
public void onCreate() {
super.onCreate();
LogUtils.d(TAG, "onCreate() | 应用启动,开始初始化流程");
sApp = this;
LogUtils.d(TAG, "onCreate() called | Initializing application...");
// 初始化调试模式
sApp = this;
setIsDebugging(BuildConfig.DEBUG);
LogUtils.d(TAG, "onCreate() | 调试模式状态:" + BuildConfig.DEBUG);
LogUtils.d(TAG, "onCreate: Debug mode = " + BuildConfig.DEBUG);
// 初始化核心组件
initBaseTools();
initUtils();
initReceiver();
LogUtils.d(TAG, "onCreate() | 应用初始化完成 | 极致强制缓存策略已启用");
LogUtils.d(TAG, "onCreate: Application initialization completed. Force-cache strategy active.");
}
@Override
public void onTerminate() {
super.onTerminate();
LogUtils.d(TAG, "onTerminate() | 应用终止,释放非缓存资源");
LogUtils.d(TAG, "onTerminate() called | Releasing non-cache resources");
// 释放轻量级工具
// 释放非缓存资源
ToastUtils.release();
LogUtils.d(TAG, "onTerminate() | Toast工具资源释放完成");
releaseNotificationManager();
releaseReceiver();
// 核心策略:应用终止不清理任何缓存
LogUtils.w(CACHE_PROTECT_TAG, "onTerminate() | 极致缓存策略生效 | 所有缓存实例保持驻留");
LogUtils.d(TAG, "onTerminate() | 非缓存资源释放完成");
// 核心策略:不清理缓存
LogUtils.w(CACHE_PROTECT_TAG, "onTerminate: Force-cache active, caches remain in memory");
LogUtils.d(TAG, "onTerminate: Non-cache resources released");
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
// 极致缓存策略:拒绝系统触发的缓存清理
LogUtils.w(CACHE_PROTECT_TAG, "onTrimMemory() 调用 | 内存等级:" + level + " | 强制保持所有缓存");
LogUtils.w(CACHE_PROTECT_TAG, "onTrimMemory() called with level: " + level + " | Ignoring, caches protected");
logDetailedCacheStatus();
}
@Override
public void onLowMemory() {
super.onLowMemory();
// 低内存场景:不清理缓存,仅记录状态
LogUtils.w(CACHE_PROTECT_TAG, "onLowMemory() 调用 | 极致缓存策略:不执行任何清理操作");
LogUtils.w(CACHE_PROTECT_TAG, "onLowMemory() called | Force-cache active, no cleanup performed");
logDetailedCacheStatus();
}
// ===================== 私有初始化方法区(按初始化顺序排序) =====================
// ==================== 私有初始化方法 ====================
/**
* 初始化基础工具Activity管理、Toast、通知工具
* 初始化基础工具
*/
private void initBaseTools() {
LogUtils.d(TAG, "initBaseTools() | 开始初始化基础工具集");
LogUtils.d(TAG, "initBaseTools: Starting...");
WinBoLLActivityManager.init(this);
ToastUtils.init(this);
sNotificationManagerUtils = new NotificationManagerUtils(this);
LogUtils.d(TAG, "initBaseTools() | ActivityManager/Toast/Notification工具初始化完成");
LogUtils.d(TAG, "initBaseTools: Completed");
}
/**
* 初始化核心工具(极致强制缓存:一旦初始化永不销毁
* 初始化核心工具与缓存(极致强制驻留
*/
private void initUtils() {
LogUtils.d(TAG, "initUtils() | 开始初始化核心工具类 | 启用极致强制缓存策略");
LogUtils.d(TAG, "initUtils: Starting with force-cache strategy");
// 初始化配置与缓存工具
// 1. 配置与基础缓存
sAppConfigUtils = getAppConfigUtils(this);
sAppCacheUtils = getAppCacheUtils(this);
// 初始化背景资源与Bitmap缓存
// 2. 资源与Bitmap缓存
sBackgroundSourceUtils = BackgroundSourceUtils.getInstance(this);
sBackgroundSourceUtils.loadSettings();
sBitmapCacheUtils = BitmapCacheUtils.getInstance();
LogUtils.d(TAG, "initUtils() | BackgroundSource/BitmapCache工具初始化完成 | 永久驻留内存");
LogUtils.d(TAG, "initUtils: Resource & Bitmap tools initialized (Permanent)");
// 初始化视图控件缓存
// 3. 视图缓存
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getLastInstance(this);
if (sMemoryCachedBackgroundView == null) {
sMemoryCachedBackgroundView = MemoryCachedBackgroundView.getInstance(this, sBackgroundSourceUtils.getCurrentBackgroundBean(), true);
LogUtils.d(TAG, "initUtils() | 视图缓存工具:新建实例完成");
LogUtils.d(TAG, "initUtils: View cache - New instance created");
}
LogUtils.d(TAG, "initUtils() | MemoryCachedBackgroundView初始化完成 | 永久驻留内存");
LogUtils.d(TAG, "initUtils: View cache initialized (Permanent)");
}
/**
* 初始化全局广播接收器
* 注册全局广播接收器
*/
private void initReceiver() {
LogUtils.d(TAG, "initReceiver() | 开始初始化广播接收器");
LogUtils.d(TAG, "initReceiver: Starting...");
mGlobalReceiver = new GlobalApplicationReceiver(this);
mGlobalReceiver.registerAction();
LogUtils.d(TAG, "initReceiver() | 广播接收器注册完成");
LogUtils.d(TAG, "initReceiver: Completed");
}
// ===================== 私有释放方法区(按资源重要性排序) =====================
// ==================== 私有释放方法 ====================
/**
* 释放广播接收器资源
* 释放广播接收器
*/
private void releaseReceiver() {
LogUtils.d(TAG, "releaseReceiver() | 开始释放广播接收器");
LogUtils.d(TAG, "releaseReceiver: Starting...");
if (mGlobalReceiver != null) {
mGlobalReceiver.unregisterAction();
mGlobalReceiver = null;
LogUtils.d(TAG, "releaseReceiver() | 广播接收器资源释放完成");
} else {
LogUtils.d(TAG, "releaseReceiver() | 无需释放:广播接收器未初始化");
LogUtils.d(TAG, "releaseReceiver: Completed");
}
}
/**
* 释放通知管理工具资源
* 释放通知管理
*/
private void releaseNotificationManager() {
LogUtils.d(TAG, "releaseNotificationManager() | 开始释放通知工具");
LogUtils.d(TAG, "releaseNotificationManager: Starting...");
if (sNotificationManagerUtils != null) {
sNotificationManagerUtils.release();
sNotificationManagerUtils = null;
LogUtils.d(TAG, "releaseNotificationManager() | 通知工具资源释放完成");
} else {
LogUtils.d(TAG, "releaseNotificationManager() | 无需释放:通知工具未初始化");
LogUtils.d(TAG, "releaseNotificationManager: Completed");
}
}
// ===================== 私有工具方法区(辅助功能) =====================
// ==================== 私有辅助方法 ====================
/**
* 记录详细缓存状态(用于调试监控极致强制缓存效果
* 记录当前缓存详细状态(用于调试监控)
*/
private void logDetailedCacheStatus() {
LogUtils.d(TAG, "logDetailedCacheStatus() | 开始记录缓存状态");
LogUtils.d(TAG, "logDetailedCacheStatus: Reporting cache state");
// 记录Bitmap缓存状态
if (sBitmapCacheUtils != null) {
LogUtils.d(CACHE_PROTECT_TAG, "Bitmap缓存工具实例有效永久驻留");
LogUtils.d(CACHE_PROTECT_TAG, "Cache Status: BitmapCache [Valid]");
try {
int cacheCount = sBitmapCacheUtils.getCacheCount();
LogUtils.d(CACHE_PROTECT_TAG, "Bitmap缓存数量" + cacheCount);
LogUtils.d(CACHE_PROTECT_TAG, "Cache Detail: Bitmap Count = " + sBitmapCacheUtils.getCacheCount());
} catch (Exception e) {
LogUtils.d(CACHE_PROTECT_TAG, "Bitmap缓存数量获取失败 | 异常信息:" + e.getMessage());
LogUtils.d(CACHE_PROTECT_TAG, "Cache Detail: Failed to get bitmap count - " + e.getMessage());
}
}
// 记录视图缓存状态
if (sMemoryCachedBackgroundView != null) {
LogUtils.d(CACHE_PROTECT_TAG, "视图缓存工具:实例有效(永久驻留)");
int viewCount = MemoryCachedBackgroundView.getInstanceCount();
LogUtils.d(CACHE_PROTECT_TAG, "视图缓存实例总数:" + viewCount);
LogUtils.d(CACHE_PROTECT_TAG, "Cache Status: ViewCache [Valid]");
LogUtils.d(CACHE_PROTECT_TAG, "Cache Detail: View Instance Count = " + MemoryCachedBackgroundView.getInstanceCount());
}
LogUtils.d(TAG, "logDetailedCacheStatus() | 缓存状态记录完成");
}
}

View File

@@ -26,14 +26,17 @@ import cc.winboll.studio.powerbell.activities.ClearRecordActivity;
import cc.winboll.studio.powerbell.activities.SettingsActivity;
import cc.winboll.studio.powerbell.activities.WinBoLLActivity;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.models.BatteryStyle;
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.unittest.MainUnitTest2Activity;
import cc.winboll.studio.powerbell.unittest.MainUnitTestActivity;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.ImageUtils;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
import cc.winboll.studio.powerbell.utils.ServiceUtils;
import cc.winboll.studio.powerbell.views.BatteryStyleView;
import cc.winboll.studio.powerbell.views.MainContentView;
/**
@@ -48,6 +51,7 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
public static final String TAG = "MainActivity";
private static final int REQUEST_BACKGROUND_SETTINGS_ACTIVITY = 1001;
public static final String EXTRA_ISRELOAD_BACKGROUNDVIEW = "EXTRA_ISRELOAD_BACKGROUNDVIEW";
public static final String EXTRA_ISRELOAD_ACCENTCOLOR = "EXTRA_ISRELOAD_ACCENTCOLOR";
private static final long DELAY_LOAD_NON_CRITICAL = 500L;
// Handler 消息常量
@@ -55,6 +59,7 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
public static final int MSG_CURRENTVALUEBATTERY = 1;
public static final int MSG_LOAD_BACKGROUND = 2;
private static final int MSG_UPDATE_SERVICE_SWITCH = 3;
private static final int MSG_UPDATE_BATTERYDRAWABLE = 4;
// ======================== 静态成员区(全局共享,管控生命周期)========================
private static MainActivity sMainActivity;
@@ -215,6 +220,10 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
// 主题切换处理
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
recreate();
Intent mainIntent = new Intent(MainActivity.this, MainActivity.class);
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_BACKGROUNDVIEW, true);
mainIntent.putExtra(MainActivity.EXTRA_ISRELOAD_ACCENTCOLOR, true);
startActivity(mainIntent);
return true;
}
// 开发者功能处理
@@ -308,6 +317,9 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
break;
case MSG_UPDATE_SERVICE_SWITCH:
sMainActivity.updateServiceSwitchUI();
break;
case MSG_UPDATE_BATTERYDRAWABLE:
sMainActivity.updateBatteryDrawable();
break;
}
}
@@ -424,6 +436,13 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
LogUtils.d(TAG, "handleReloadBackgroundParam: Intent 为空");
return;
}
boolean isReloadAccentColor = intent.getBooleanExtra(EXTRA_ISRELOAD_ACCENTCOLOR, false);
if (isReloadAccentColor) {
App.sBackgroundSourceUtils.getCurrentBackgroundBean().setPixelColor(ImageUtils.getColorAccent(this));
App.sBackgroundSourceUtils.saveSettings();
}
boolean isReloadBackgroundView = intent.getBooleanExtra(EXTRA_ISRELOAD_BACKGROUNDVIEW, false);
if (isReloadBackgroundView) {
LogUtils.d(TAG, "handleReloadBackgroundParam: 接收到刷新背景视图指令");
@@ -461,6 +480,17 @@ public class MainActivity extends WinBoLLActivity implements MainContentView.OnV
LogUtils.d(TAG, "updateViewData: 视图数据已更新");
}
void updateBatteryDrawable() {
BatteryStyle batteryStyle = BatteryStyleView.getSavedBatteryStyle(this);
mMainContentView.updateBatteryDrawable(batteryStyle);
}
public static void sendUpdateBatteryDrawableMessage() {
if (sGlobalHandler != null) {
sGlobalHandler.sendEmptyMessage(MSG_UPDATE_BATTERYDRAWABLE);
}
}
private void reloadBackground() {
LogUtils.d(TAG, "reloadBackground() 调用");
if (mMainContentView == null || mBgSourceUtils == null) {

View File

@@ -1,12 +1,24 @@
package cc.winboll.studio.powerbell.activities;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.ThoughtfulServiceBean;
import java.lang.reflect.Field;
/**
* 应用设置窗口,提供应用配置项的统一入口
@@ -44,6 +56,13 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
// 初始化工具栏
initToolbar();
ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
if (thoughtfulServiceBean == null) {
thoughtfulServiceBean = new ThoughtfulServiceBean();
}
((CheckBox)findViewById(R.id.activitysettingsCheckBox1)).setChecked(thoughtfulServiceBean.isEnableUsePowerTts());
((CheckBox)findViewById(R.id.activitysettingsCheckBox2)).setChecked(thoughtfulServiceBean.isEnableChargeTts());
LogUtils.d(TAG, "【onCreate】SettingsActivity 初始化完成");
}
@@ -70,5 +89,96 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
});
LogUtils.d(TAG, "【initToolbar】工具栏初始化完成");
}
public void onCheckTTSDrawOverlaysPermission(View view) {
canDrawOverlays();
}
public void onEnableChargeTts(View view) {
ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
if (thoughtfulServiceBean == null) {
thoughtfulServiceBean = new ThoughtfulServiceBean();
}
thoughtfulServiceBean.setIsEnableChargeTts(((CheckBox)view).isChecked());
ThoughtfulServiceBean.saveBean(this, thoughtfulServiceBean);
}
public void onEnableUsePowerTts(View view) {
ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(this, ThoughtfulServiceBean.class);
if (thoughtfulServiceBean == null) {
thoughtfulServiceBean = new ThoughtfulServiceBean();
}
thoughtfulServiceBean.setIsEnableUsePowerTts(((CheckBox)view).isChecked());
ThoughtfulServiceBean.saveBean(this, thoughtfulServiceBean);
}
/**
* 悬浮窗权限检查与请求
*/
void canDrawOverlays() {
LogUtils.d(TAG, "onCanDrawOverlays: 检查悬浮窗权限");
// API6.0+校验权限
if (Build.VERSION.SDK_INT >= 23 && !Settings.canDrawOverlays(this)) {
LogUtils.d(TAG, "onCanDrawOverlays: 未开启悬浮窗权限,发起请求");
showDrawOverlayRequestDialog();
} else {
ToastUtils.show("悬浮窗权限已开启");
}
}
/**
* 显示悬浮窗权限请求对话框
*/
private void showDrawOverlayRequestDialog() {
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("权限请求")
.setMessage("为保证通话监听功能正常,需开启悬浮窗权限")
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
jumpToDrawOverlaySettings();
}
})
.setNegativeButton("稍后", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.create();
// 解决对话框焦点问题
if (dialog.getWindow() != null) {
dialog.getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}
dialog.show();
}
/**
* 跳转悬浮窗权限设置页面(反射适配低版本)
*/
private void jumpToDrawOverlaySettings() {
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转悬浮窗权限设置");
try {
// 反射获取设置页面Action避免高版本API依赖
Class<?> settingsClazz = Settings.class;
Field actionField = settingsClazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
String action = (String) actionField.get(null);
// 跳转当前应用权限设置页
Intent intent = new Intent(action);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
} catch (Exception e) {
LogUtils.e(TAG, "jumpToDrawOverlaySettings: 跳转权限设置失败", e);
Toast.makeText(this, "请手动在设置中开启悬浮窗权限", Toast.LENGTH_LONG).show();
}
}
}

View File

@@ -0,0 +1,12 @@
package cc.winboll.studio.powerbell.models;
/**
* 电池绘制样式枚举 (单选选项)
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public enum BatteryStyle {
ENERGY_STYLE, // 能量样式
ZEBRA_STYLE, // 条纹样式
POINT_STYLE // 点阵样式
}

View File

@@ -0,0 +1,47 @@
package cc.winboll.studio.powerbell.models;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.Serializable;
/**
* TTS 语音播放文本内容实体类
* 适配Java7 语法规范 | Android API30 系统版本
* 特性:实现序列化接口,支持跨页面/进程传递,属性默认值初始化
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/29 19:13
*/
public class TTSSpeakTextBean implements Serializable {
// ====================================== 常量区 - 置顶排序 ======================================
/** 日志TAG 瞬态修饰,不参与序列化,减少序列化体积 */
transient public static final String TAG = "TTSSpeakTextBean";
// ====================================== 成员属性区 - 业务属性排序 ======================================
/** 延迟播放时长 单位毫秒默认值0无延迟播放 */
public int mnDelay = 0;
/** TTS语音播放文本内容默认值空字符串防止空指针 */
public String mszSpeakContent = "";
// ====================================== 构造方法区 - 无参+有参 完整实现 ======================================
/**
* 无参构造方法
* Java7序列化规范必备 + 兼容反射实例化场景
*/
public TTSSpeakTextBean() {
LogUtils.d(TAG, "【无参构造】TTSSpeakTextBean 实例化,使用默认值 | 延迟:" + mnDelay + " | 文本:" + mszSpeakContent);
}
/**
* 有参构造方法【主构造】
* @param nDelay 延迟播放时长(ms)
* @param szSpeakContent 语音播放文本内容
*/
public TTSSpeakTextBean(int nDelay, String szSpeakContent) {
LogUtils.d(TAG, "【有参构造】TTSSpeakTextBean 实例化,入参 | 延迟:" + nDelay + " | 文本:" + szSpeakContent);
this.mnDelay = nDelay;
this.mszSpeakContent = szSpeakContent;
LogUtils.d(TAG, "【有参构造】赋值完成 | 最终延迟:" + this.mnDelay + " | 最终文本:" + this.mszSpeakContent);
}
}

View File

@@ -0,0 +1,156 @@
package cc.winboll.studio.powerbell.models;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.JsonReader;
import android.util.JsonWriter;
import java.io.IOException;
import java.io.Serializable;
import cc.winboll.studio.libappbase.BaseBean;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/29 20:59
* @Describe 贴心服务配置实体类 (适配API30 / Java7)
*/
public class ThoughtfulServiceBean extends BaseBean implements Parcelable, Serializable {
// ====================== 常量区 - 置顶统一管理 ======================
public static final String TAG = ThoughtfulServiceBean.class.getSimpleName();
private static final long serialVersionUID = 1L; // Serializable 序列化兼容必备
// JSON序列化字段常量 杜绝硬编码
public static final String JSON_FIELD_IS_ENABLE_CHARGE_TTS = "isEnableChargeTts";
public static final String JSON_FIELD_IS_ENABLE_USE_POWER_TTS = "isEnableUsePowerTts";
// ====================== 核心成员变量 - 私有封装 ======================
private boolean isEnableChargeTts = false; // 是否启用 充电TTS贴心语音服务
private boolean isEnableUsePowerTts = false; // 是否启用 用电TTS贴心语音服务
// ====================== Parcelable 静态创建器 (API30标准写法 必须public static final) ======================
public static final Creator<ThoughtfulServiceBean> CREATOR = new Creator<ThoughtfulServiceBean>() {
@Override
public ThoughtfulServiceBean createFromParcel(Parcel source) {
return new ThoughtfulServiceBean(source);
}
@Override
public ThoughtfulServiceBean[] newArray(int size) {
LogUtils.d(TAG, "newArray: 初始化数组size = " + size);
return new ThoughtfulServiceBean[size];
}
};
// ====================== 构造方法区 (无参+有参+Parcel构造 全覆盖) ======================
/**
* 无参构造 - JSON解析/反射实例化 必备
*/
public ThoughtfulServiceBean() {
LogUtils.d(TAG, "ThoughtfulServiceBean: 无参构造初始化默认禁用所有TTS服务");
}
/**
* 全参构造 - 手动配置所有服务状态
* @param isEnableChargeTts 充电TTS服务开关
* @param isEnableUsePowerTts 用电TTS服务开关
*/
public ThoughtfulServiceBean(boolean isEnableChargeTts, boolean isEnableUsePowerTts) {
this.isEnableChargeTts = isEnableChargeTts;
this.isEnableUsePowerTts = isEnableUsePowerTts;
LogUtils.d(TAG, "ThoughtfulServiceBean: 全参构造 | isEnableChargeTts=" + isEnableChargeTts + " | isEnableUsePowerTts=" + isEnableUsePowerTts);
}
/**
* Parcel反序列化构造 - Parcelable必备 私有私有化
*/
private ThoughtfulServiceBean(Parcel in) {
this.isEnableChargeTts = in.readByte() != 0;
this.isEnableUsePowerTts = in.readByte() != 0;
LogUtils.d(TAG, "ThoughtfulServiceBean: Parcel构造解析完成 | isEnableChargeTts=" + isEnableChargeTts + " | isEnableUsePowerTts=" + isEnableUsePowerTts);
}
// ====================== Getter/Setter 方法区 (封装成员变量 统一访问) ======================
public boolean isEnableChargeTts() {
return isEnableChargeTts;
}
public void setIsEnableChargeTts(boolean isEnableChargeTts) {
LogUtils.d(TAG, "setIsEnableChargeTts: 旧值=" + this.isEnableChargeTts + " 新值=" + isEnableChargeTts);
this.isEnableChargeTts = isEnableChargeTts;
}
public boolean isEnableUsePowerTts() {
return isEnableUsePowerTts;
}
public void setIsEnableUsePowerTts(boolean isEnableUsePowerTts) {
LogUtils.d(TAG, "setIsEnableUsePowerTts: 旧值=" + this.isEnableUsePowerTts + " 新值=" + isEnableUsePowerTts);
this.isEnableUsePowerTts = isEnableUsePowerTts;
}
// ====================== 重写父类 BaseBean 核心方法 (JSON序列化/反序列化 业务核心) ======================
@Override
public String getName() {
String className = ThoughtfulServiceBean.class.getName();
LogUtils.d(TAG, "getName: 返回当前实体类名 = " + className);
return className;
}
/**
* JSON序列化 - 写入所有字段 适配持久化/网络传输
*/
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
jsonWriter.name(JSON_FIELD_IS_ENABLE_CHARGE_TTS).value(this.isEnableChargeTts);
jsonWriter.name(JSON_FIELD_IS_ENABLE_USE_POWER_TTS).value(this.isEnableUsePowerTts);
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成所有TTS服务状态已写入");
}
/**
* JSON反序列化 - 读取字段生成实体 适配数据恢复
*/
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
ThoughtfulServiceBean bean = new ThoughtfulServiceBean();
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String fieldName = jsonReader.nextName();
switch (fieldName) {
case JSON_FIELD_IS_ENABLE_CHARGE_TTS:
bean.setIsEnableChargeTts(jsonReader.nextBoolean());
break;
case JSON_FIELD_IS_ENABLE_USE_POWER_TTS:
bean.setIsEnableUsePowerTts(jsonReader.nextBoolean());
break;
default:
jsonReader.skipValue();
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未知JSON字段 = " + fieldName);
break;
}
}
jsonReader.endObject();
LogUtils.d(TAG, "readBeanFromJsonReader: JSON反序列化完成生成实体对象");
return bean;
}
// ====================== 实现 Parcelable 接口方法 (组件间Intent传递必备 API30/Java7完美适配) ======================
@Override
public int describeContents() {
return 0; // 无文件描述符等特殊内容固定返回0即可
}
/**
* Parcel序列化 - boolean用byte存储(Java7/API30标准写法 避免兼容性问题)
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByte((byte) (isEnableChargeTts ? 1 : 0));
dest.writeByte((byte) (isEnableUsePowerTts ? 1 : 0));
LogUtils.d(TAG, "writeToParcel: Parcel序列化完成所有TTS服务状态已写入");
}
}

View File

@@ -13,6 +13,7 @@ import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BatteryUtils;
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
import java.lang.ref.WeakReference;
import cc.winboll.studio.powerbell.services.ThoughtfulService;
/**
* 控制中心广播接收器
@@ -110,6 +111,15 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
LogUtils.d(TAG, "handleBatteryStateChanged() 跳过 | 电池状态无变化");
return;
}
// 在插拔充电线时,执行贴心服务
if(currentCharging != sIsCharging) {
if(currentCharging) {
ThoughtfulService.startServiceWithType(service, ThoughtfulService.ServiceType.CHARGE_STATE);
} else {
ThoughtfulService.startServiceWithType(service, ThoughtfulService.ServiceType.DISCHARGE_STATE);
}
}
// 3. 更新静态缓存状态,保证多线程可见
sIsCharging = currentCharging;

View File

@@ -0,0 +1,83 @@
package cc.winboll.studio.powerbell.services;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.TTSSpeakTextBean;
import cc.winboll.studio.powerbell.utils.TextToSpeechUtils;
import java.util.ArrayList;
/**
* TTS 语音播放后台服务组件
* 适配Java7 语法规范 | Android API30 系统版本
* 功能后台承载TTS语音播放解耦页面生命周期避免页面销毁中断播放
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/29 19:12
*/
public class TTSPlayService extends Service {
// ====================================== 常量区 - 静态全局常量 置顶排序 ======================================
public static final String TAG = "TTSPlayService";
public static final String EXTRA_SPEAKDATA = "EXTRA_SPEAKDATA";
// ====================================== 对外公开静态快捷调用方法【新增核心】======================================
/**
* 公开静态方法一键启动TTS播放服务播放指定文本内容
* @param context 上下文对象
* @param speakText 需要播放的语音文本内容
*/
public static void startPlayTTS(Context context, String speakText) {
LogUtils.d(TAG, "【startPlayTTS】静态快捷调用方法 | 入参Context=" + context + " | 播放文本=" + speakText);
if (context != null && speakText != null && !speakText.isEmpty()) {
// 初始化播放数据集合
ArrayList<TTSSpeakTextBean> ttsBeanList = new ArrayList<>();
// 添加播放文本延迟时间为0无延迟立即播放
ttsBeanList.add(new TTSSpeakTextBean(0, speakText));
LogUtils.d(TAG, "【startPlayTTS】封装播放数据完成创建启动服务意图");
// 创建意图并封装序列化参数
Intent intent = new Intent(context, TTSPlayService.class);
intent.putExtra(EXTRA_SPEAKDATA, ttsBeanList);
// 启动当前服务
context.startService(intent);
LogUtils.d(TAG, "【startPlayTTS】已调用startServiceTTS播放服务启动成功");
} else {
LogUtils.d(TAG, "【startPlayTTS】上下文为空 或 播放文本为空/空字符串,跳过启动服务");
}
}
// ====================================== 生命周期方法 - 绑定服务 (无绑定逻辑) ======================================
@Override
public IBinder onBind(Intent intent) {
LogUtils.d(TAG, "【onBind】服务绑定方法调用入参Intent" + intent);
return null;
}
// ====================================== 生命周期方法 - 启动服务【核心方法】 ======================================
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.d(TAG, "【onStartCommand】服务启动方法调用 | 入参Intent" + intent + " | flags" + flags + " | startId" + startId);
// 解析播放数据并执行播放
if (intent != null) {
LogUtils.d(TAG, "【onStartCommand】Intent不为空开始解析序列化播放数据");
ArrayList<TTSSpeakTextBean> listTTSSpeakTextBean = (ArrayList<TTSSpeakTextBean>) intent.getSerializableExtra(EXTRA_SPEAKDATA);
if (listTTSSpeakTextBean != null && listTTSSpeakTextBean.size() > 0) {
LogUtils.d(TAG, "【onStartCommand】解析播放数据成功队列长度" + listTTSSpeakTextBean.size() + "调用TTS播放工具类");
TextToSpeechUtils.getInstance(this).speekTTSList(listTTSSpeakTextBean);
} else {
LogUtils.d(TAG, "【onStartCommand】播放数据为空/长度0跳过语音播放逻辑");
}
} else {
LogUtils.d(TAG, "【onStartCommand】Intent为空无播放数据可解析");
}
// 返回默认值,保持原服务启动策略不变
int result = super.onStartCommand(intent, flags, startId);
LogUtils.d(TAG, "【onStartCommand】方法执行完成返回值" + result);
return result;
}
}

View File

@@ -0,0 +1,164 @@
package cc.winboll.studio.powerbell.services;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.models.ThoughtfulServiceBean;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
/**
* 智能电池服务(充电/放电状态处理)
* 适配Java7 语法规范 | Android API30 系统版本
* 功能:接收充电/放电状态指令,根据不同状态执行对应业务任务
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/29 19:29
*/
public class ThoughtfulService extends Service {
// ====================================== 常量区 - 置顶排序 ======================================
public static final String TAG = "ThoughtfulService";
/** Intent传递 服务类型 的Key值 */
public static final String EXTRA_SERVICE_TYPE = "EXTRA_SERVICE_TYPE";
// ====================================== 枚举类 - 服务类型 充电/放电状态 ======================================
/**
* 服务执行类型枚举
* CHARGE_STATE : 充电状态服务
* DISCHARGE_STATE : 放电(耗电)状态服务
*/
public enum ServiceType {
CHARGE_STATE, //充电状态服务
DISCHARGE_STATE //放电状态服务
}
// ====================================== 对外公开静态启动函数【新增核心】入参Context + 枚举 ======================================
/**
* 公开静态方法:传入上下文+服务类型枚举,一键构建意图并启动当前服务
* @param context 上下文对象
* @param serviceType 服务类型枚举【充电/放电】
*/
public static void startServiceWithType(Context context, ServiceType serviceType) {
LogUtils.d(TAG, "【startServiceWithType】静态启动方法调用 | Context=" + context + " | ServiceType=" + (serviceType == null ? "null" : serviceType.name()));
ThoughtfulServiceBean thoughtfulServiceBean = ThoughtfulServiceBean.loadBean(context, ThoughtfulServiceBean.class);
if (thoughtfulServiceBean == null) {
thoughtfulServiceBean = new ThoughtfulServiceBean();
}
// 对应TTS服务提醒没有启用就退出
if((serviceType == ServiceType.CHARGE_STATE && !thoughtfulServiceBean.isEnableChargeTts())
||(serviceType == ServiceType.DISCHARGE_STATE && !thoughtfulServiceBean.isEnableUsePowerTts())){
return;
}
// 判空健壮性校验
if (context != null && serviceType != null) {
// 构建意图 + 封装枚举参数
Intent intent = new Intent(context, ThoughtfulService.class);
intent.putExtra(EXTRA_SERVICE_TYPE, serviceType);
// 启动服务
context.startService(intent);
LogUtils.d(TAG, "【startServiceWithType】服务启动成功执行[" + serviceType.name() + "]任务");
} else {
LogUtils.d(TAG, "【startServiceWithType】上下文为空 或 服务类型枚举为空,跳过启动服务");
}
}
// ====================================== 生命周期方法 - 绑定服务 (原逻辑保留) ======================================
@Override
public IBinder onBind(Intent intent) {
LogUtils.d(TAG, "【onBind】服务绑定方法调用入参Intent" + intent);
return null;
}
// ====================================== 生命周期方法 - 启动服务【核心逻辑】接收枚举+分支执行任务 ======================================
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.d(TAG, "【onStartCommand】服务启动方法调用 | intent=" + intent + " | flags=" + flags + " | startId=" + startId);
// 判断意图非空,解析服务类型参数
if (intent != null) {
LogUtils.d(TAG, "【onStartCommand】Intent不为空开始解析服务类型枚举参数");
// 获取传递的服务类型枚举
ServiceType serviceType = (ServiceType) intent.getSerializableExtra(EXTRA_SERVICE_TYPE);
// 根据服务类型,执行对应任务
if (serviceType != null) {
LogUtils.d(TAG, "【onStartCommand】解析到服务类型" + serviceType.name());
switch (serviceType) {
case CHARGE_STATE:
// 执行【充电状态】对应的业务任务
executeChargeStateTask();
break;
case DISCHARGE_STATE:
// 执行【放电状态】对应的业务任务
executeDischargeStateTask();
break;
default:
LogUtils.d(TAG, "【onStartCommand】未知的服务类型不执行任何任务");
break;
}
} else {
LogUtils.d(TAG, "【onStartCommand】未解析到有效服务类型参数参数为空");
}
} else {
LogUtils.d(TAG, "【onStartCommand】启动服务的Intent为空直接返回");
}
// 返回默认策略,与原生逻辑一致
int result = super.onStartCommand(intent, flags, startId);
LogUtils.d(TAG, "【onStartCommand】服务执行完成返回值" + result);
return result;
}
// ====================================== 私有业务方法 充电/放电 分任务执行 ======================================
/**
* 执行【充电状态】的业务任务
* 可在此方法内编写 充电时的逻辑(语音提醒/电量监控/弹窗等)
*/
private void executeChargeStateTask() {
LogUtils.d(TAG, "【executeChargeStateTask】执行【充电状态】业务任务 >>> ");
//ToastUtils.show("【executeChargeStateTask】执行【充电状态】业务任务 >>> ");
// TODO 此处添加充电状态需要执行的业务逻辑代码
// 加载最新配置
AppConfigBean latestConfig = AppConfigUtils.getInstance(this).loadAppConfig();
if (latestConfig == null) {
LogUtils.e(TAG, "handleNotifyAppConfigUpdate() 终止 | 最新配置为空");
return;
}
if (latestConfig.isEnableChargeReminder()) {
int nChargeReminderValue = latestConfig.getChargeReminderValue();
String szRemind = String.format("限量充电提醒已启用,限量值为百分之%d。", nChargeReminderValue);
szRemind = szRemind + szRemind + szRemind;
TTSPlayService.startPlayTTS(this, szRemind);
}
}
/**
* 执行【放电(耗电)状态】的业务任务
* 可在此方法内编写 放电时的逻辑(语音提醒/电量监控/弹窗等)
*/
private void executeDischargeStateTask() {
LogUtils.d(TAG, "【executeDischargeStateTask】执行【放电状态】业务任务 >>> ");
//ToastUtils.show("【executeDischargeStateTask】执行【放电状态】业务任务 >>> ");
// TODO 此处添加放电状态需要执行的业务逻辑代码
// 加载最新配置
AppConfigBean latestConfig = AppConfigUtils.getInstance(this).loadAppConfig();
if (latestConfig == null) {
LogUtils.e(TAG, "handleNotifyAppConfigUpdate() 终止 | 最新配置为空");
return;
}
if (latestConfig.isEnableUsageReminder()) {
int nUsageReminderValue = latestConfig.getUsageReminderValue();
String szRemind = String.format("电量不足提醒已启用,低电值为百分之%d。", nUsageReminderValue);
//szRemind = szRemind + szRemind + szRemind;
TTSPlayService.startPlayTTS(this, szRemind);
}
}
}

View File

@@ -0,0 +1,251 @@
package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Build;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.LinearLayout;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.TTSSpeakTextBean;
import java.util.ArrayList;
/**
* TTS语音播放工具类 (单例实现)
* 适配Java7 语法规范 | Android API36 系统版本【修复崩溃】
* 功能:队列播放语音文本 + 播放悬浮窗展示 + 点击悬浮窗停止播放/关闭悬浮窗
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/29 19:03
*/
public class TextToSpeechUtils {
// ====================================== 常量区 - 静态全局常量 (置顶) ======================================
public static final String TAG = "TextToSpeechUtils";
public static final String UNIQUE_ID = "UNIQUE_ID";
// ====================================== 单例实例 - 静态私有 (饿汉式优化) ======================================
private static volatile TextToSpeechUtils sTextToSpeechUtils;
// ====================================== 成员属性区 - 私有成员变量 (按功能归类 有序排列) ======================================
private Context mContext;
private WindowManager mWindowManager;
private TextToSpeech mTextToSpeech;
private View mView;
private volatile boolean isExist = false;
private UtteranceProgressListener mUtteranceProgressListener;
// ====================================== 构造方法 - 私有私有化 (单例模式) ======================================
private TextToSpeechUtils(Context context) {
LogUtils.d(TAG, "【构造方法】初始化TextToSpeechUtil实例");
this.mContext = context.getApplicationContext();
this.mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
this.initUtteranceProgressListener();
LogUtils.d(TAG, "【构造方法】初始化完成获取WindowManager实例"+mWindowManager);
}
// ====================================== 对外暴露方法 - 单例获取入口 (线程安全) ======================================
public static synchronized TextToSpeechUtils getInstance(Context context) {
LogUtils.d(TAG, "【getInstance】获取单例实例入参Context" + context);
if (sTextToSpeechUtils == null) {
LogUtils.d(TAG, "【getInstance】实例为空创建新的TextToSpeechUtil对象");
sTextToSpeechUtils = new TextToSpeechUtils(context);
}
return sTextToSpeechUtils;
}
// ====================================== 核心对外业务方法 - 播放TTS语音队列 【主入口】 ======================================
public void speekTTSList(final ArrayList<TTSSpeakTextBean> listTTSSpeakTextBean) {
LogUtils.d(TAG, "【speekTTSList】播放语音队列调用入参队列长度" + (listTTSSpeakTextBean == null ? 0 : listTTSSpeakTextBean.size()));
// 重置播放退出标志位
isExist = false;
LogUtils.d(TAG, "【speekTTSList】重置播放退出标志位 isExist = " + isExist);
// TTS实例为空 → 初始化TTS后重放
if (mTextToSpeech == null) {
LogUtils.d(TAG, "【speekTTSList】TextToSpeech实例为空开始初始化TTS");
mTextToSpeech = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int initStatus) {
LogUtils.d(TAG, "【onInit】TTS初始化回调初始化状态码" + initStatus);
if (initStatus == TextToSpeech.SUCCESS) {
LogUtils.d(TAG, "【onInit】TTS初始化成功重新调用语音播放方法");
speekTTSList(listTTSSpeakTextBean);
} else {
LogUtils.d(TAG, "【onInit】TTS init failed : " + initStatus + ". The app [https://play.google.com/store/apps/details?id=com.google.android.tts] maybe fix this TTS probrem. ");
}
}
});
mTextToSpeech.setOnUtteranceProgressListener(mUtteranceProgressListener);
LogUtils.d(TAG, "【speekTTSList】已为TTS绑定播放进度监听器");
} else {
// TTS实例就绪 → 执行播放逻辑
if (listTTSSpeakTextBean != null && listTTSSpeakTextBean.size() > 0) {
LogUtils.d(TAG, "【speekTTSList】TTS实例就绪语音队列数据有效开始播放逻辑处理");
// 清理过期的悬浮窗 - 防止内存泄漏/重复添加
clearFloatWindow();
// ========== 修复1添加悬浮窗权限检查有权限才初始化悬浮窗无权限则只播语音不崩溃 ==========
if (checkOverlayPermission()) {
initWindow();
LogUtils.d(TAG, "【speekTTSList】悬浮窗初始化并显示完成");
} else {
LogUtils.d(TAG, "【speekTTSList】悬浮窗权限未授予跳过悬浮窗显示仅播放语音");
}
// 获取第一条语音的延迟时间并休眠
int nDelay = listTTSSpeakTextBean.get(0).mnDelay;
LogUtils.d(TAG, "【speekTTSList】获取播放延迟时间" + nDelay + "ms开始休眠等待");
try {
Thread.sleep(nDelay);
} catch (InterruptedException e) {
LogUtils.d(TAG, "【speekTTSList】休眠等待被中断", e);
}
LogUtils.d(TAG, "【speekTTSList】休眠等待完成开始循环播放语音队列");
// 循环播放语音队列
for (int speakPosition = 0; speakPosition < listTTSSpeakTextBean.size() && !isExist; speakPosition++) {
String szSpeakContent = listTTSSpeakTextBean.get(speakPosition).mszSpeakContent;
isExist = (listTTSSpeakTextBean.size() - 2 < speakPosition);
LogUtils.d(TAG, "【speekTTSList】播放索引" + speakPosition + " | 播放文本:" + szSpeakContent + " | 当前退出标记位:" + isExist);
// 第一条语音清空队列播放,后续语音追加播放
if (speakPosition == 0) {
mTextToSpeech.speak(szSpeakContent, TextToSpeech.QUEUE_FLUSH, null, UNIQUE_ID);
LogUtils.d(TAG, "【speekTTSList】执行清空队列播放 → QUEUE_FLUSH");
} else {
mTextToSpeech.speak(szSpeakContent, TextToSpeech.QUEUE_ADD, null, UNIQUE_ID);
LogUtils.d(TAG, "【speekTTSList】执行追加队列播放 → QUEUE_ADD");
}
}
LogUtils.d(TAG, "【speekTTSList】语音队列循环播放逻辑执行完毕");
} else {
LogUtils.d(TAG, "【speekTTSList】语音队列为空/长度0跳过播放逻辑");
}
}
}
// ====================================== 私有工具方法 - 初始化播放监听器 ======================================
private void initUtteranceProgressListener() {
LogUtils.d(TAG, "【initUtteranceProgressListener】初始化TTS播放进度监听器");
mUtteranceProgressListener = new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
LogUtils.d(TAG, "【onStart】TTS语音播放开始唯一标识ID" + utteranceId);
}
@Override
public void onDone(String utteranceId) {
LogUtils.d(TAG, "【onDone】TTS语音播放结束唯一标识ID" + utteranceId + " | 退出标志位:" + isExist);
// 播放完成 关闭悬浮窗
if (isExist && mWindowManager != null && mView != null) {
LogUtils.d(TAG, "【onDone】满足关闭条件执行悬浮窗移除操作");
clearFloatWindow();
}
}
@Override
public void onError(String utteranceId) {
LogUtils.d(TAG, "【onError】TTS语音播放出错唯一标识ID" + utteranceId);
}
};
}
// ====================================== 私有核心方法 - 初始化并添加悬浮窗 【核心修复 根治崩溃】 ======================================
private void initWindow() {
LogUtils.d(TAG, "【initWindow】开始初始化播放悬浮窗");
// 创建Window布局参数
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
// ========== 修复2 重中之重Android 12(API31)+ 彻底废弃TYPE_PHONE统一用TYPE_APPLICATION_OVERLAY 适配API36 ==========
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
LogUtils.d(TAG, "【initWindow】系统版本>=API26悬浮窗类型TYPE_APPLICATION_OVERLAY");
} else {
// 仅低版本用TYPE_PHONE高版本不再走这里
params.type = WindowManager.LayoutParams.TYPE_PHONE;
LogUtils.d(TAG, "【initWindow】系统版本<API26悬浮窗类型TYPE_PHONE");
}
// 悬浮窗样式配置
params.alpha = 0.9f;
params.gravity = Gravity.RIGHT | Gravity.BOTTOM;
params.x = 20;
params.y = 20;
params.format = PixelFormat.RGBA_8888;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
// 核心Flag无焦点+不阻塞触摸事件穿透
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
// 加载悬浮窗布局
mView = View.inflate(mContext, R.layout.view_tts_back, null);
LinearLayout llMain = mView.findViewById(R.id.viewttsbackLinearLayout1);
llMain.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view) {
LogUtils.d(TAG, "【onClick】悬浮窗被点击执行停止播放+关闭悬浮窗");
isExist = true;
if (mTextToSpeech != null) {
mTextToSpeech.stop();
LogUtils.d(TAG, "【onClick】已调用TTS.stop()停止播放");
}
clearFloatWindow();
}
});
// ========== 修复3添加异常捕获+重复添加判断防止addView时报错导致整个TTS播放崩溃 ==========
try {
if (mWindowManager != null && mView != null && mView.getWindowToken() == null) {
mWindowManager.addView(mView, params);
LogUtils.d(TAG, "【initWindow】悬浮窗添加到Window成功布局加载完成");
}
} catch (Exception e) {
LogUtils.d(TAG, "【initWindow】悬浮窗添加异常(权限/系统限制),不影响语音播放:", e);
mView = null;
}
}
// ====================================== 私有工具方法 - 清理悬浮窗 (通用封装) 【优化修复】 ======================================
private void clearFloatWindow() {
LogUtils.d(TAG, "【clearFloatWindow】执行悬浮窗清理操作");
if (mWindowManager != null && mView != null) {
try {
mWindowManager.removeView(mView);
LogUtils.d(TAG, "【clearFloatWindow】悬浮窗移除成功");
} catch (Exception e) {
LogUtils.d(TAG, "【clearFloatWindow】悬浮窗移除异常", e);
} finally {
mView = null;
}
} else {
LogUtils.d(TAG, "【clearFloatWindow】无需清理WindowManager或View为空");
}
}
// ====================================== ✅ 新增悬浮窗权限检查【必须】Android6.0+ 强制校验 防止崩溃 ✅ ======================================
private boolean checkOverlayPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
boolean hasPermission = android.provider.Settings.canDrawOverlays(mContext);
LogUtils.d(TAG, "【checkOverlayPermission】Android6.0+ 悬浮窗权限校验结果:" + hasPermission);
return hasPermission;
} else {
// 低版本默认有权限
return true;
}
}
// ====================================== ✅ 新增释放资源方法【根治内存泄漏】建议在Service/Activity销毁时调用 ✅ ======================================
public void release() {
LogUtils.d(TAG, "【release】释放TTS资源和悬浮窗");
clearFloatWindow();
if (mTextToSpeech != null) {
mTextToSpeech.stop();
mTextToSpeech.shutdown();
mTextToSpeech = null;
}
sTextToSpeechUtils = null;
}
}

View File

@@ -7,6 +7,7 @@ import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.models.BatteryStyle;
/**
* 电池电量Drawable适配API30兼容小米机型支持能量/条纹两种绘制风格切换
@@ -31,7 +32,7 @@ public class BatteryDrawable extends Drawable {
private final Paint mBatteryPaint;
// 业务控制变量
private int mBatteryValue = -1; // 当前电量0-100-1=未初始化)
private boolean mIsEnergyStyle = true; // 绘制风格true=能量false=条纹)
private BatteryStyle mBatteryStyle = BatteryStyle.ENERGY_STYLE; // 绘制风格true=能量false=条纹)
// ====================================== 构造方法(重载适配,优先暴露常用构造) ======================================
/**
@@ -49,13 +50,16 @@ public class BatteryDrawable extends Drawable {
* @param batteryColor 电量显示颜色
* @param isEnergyStyle 是否启用能量风格
*/
public BatteryDrawable(int batteryColor, boolean isEnergyStyle) {
LogUtils.d(TAG, "【BatteryDrawable】构造器2调用 | 颜色=" + Integer.toHexString(batteryColor) + " | 风格=" + (isEnergyStyle ? "能量" : "条纹"));
public BatteryDrawable(int batteryColor, BatteryStyle batteryStyle) {
mBatteryPaint = new Paint();
mIsEnergyStyle = isEnergyStyle;
mBatteryStyle = batteryStyle;
initPaintConfig(batteryColor);
}
public void setIsEnergyStyle(BatteryStyle batteryStyle) {
this.mBatteryStyle = batteryStyle;
}
// ====================================== 私有初始化方法(封装复用,隐藏内部逻辑) ======================================
/**
* 初始化画笔配置适配API30渲染特性优化小米机型兼容性
@@ -74,8 +78,7 @@ public class BatteryDrawable extends Drawable {
// ====================================== 核心绘制方法Drawable抽象方法优先级最高 ======================================
@Override
public void draw(Canvas canvas) {
LogUtils.d(TAG, "【draw】绘制开始 | 当前电量=" + mBatteryValue + " | 风格=" + (mIsEnergyStyle ? "能量" : "条纹"));
// 未初始化/异常电量,直接跳过,避免无效绘制
// 未初始化/异常电量,直接跳过,避免无效绘制
if (mBatteryValue < 0) {
LogUtils.w(TAG, "【draw】电量未初始化跳过绘制");
return;
@@ -99,10 +102,12 @@ public class BatteryDrawable extends Drawable {
LogUtils.d(TAG, "【draw】绘制参数校准 | 左边界=" + left + " | 右边界=" + right + " | 高度=" + drawHeight);
// 按风格执行绘制
if (mIsEnergyStyle) {
if (mBatteryStyle == BatteryStyle.ENERGY_STYLE) {
drawEnergyStyle(canvas, validBattery, left, right, drawHeight);
} else {
drawStripeStyle(canvas, validBattery, left, right, drawHeight);
} else if (mBatteryStyle == BatteryStyle.ZEBRA_STYLE) {
drawZebraStyle(canvas, validBattery, left, right, drawHeight);
} else if (mBatteryStyle == BatteryStyle.POINT_STYLE) {
drawPointStyle(canvas, validBattery, left, right, drawHeight);
}
LogUtils.d(TAG, "【draw】绘制完成");
}
@@ -118,9 +123,25 @@ public class BatteryDrawable extends Drawable {
*/
private void drawEnergyStyle(Canvas canvas, int battery, int left, int right, int height) {
LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制开始 | 电量=" + battery);
int top = height - (height * battery / BATTERY_MAX); // 计算电量对应顶部坐标
canvas.drawRect(new Rect(left, top, right, height), mBatteryPaint);
LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制完成 | 顶部坐标=" + top);
// int top = height - (height * battery / BATTERY_MAX); // 计算电量对应顶部坐标
// canvas.drawRect(new Rect(left, top, right, height), mBatteryPaint);
// LogUtils.d(TAG, "【drawEnergyStyle】能量风格绘制完成 | 顶部坐标=" + top);
int nWidth = getBounds().width();
int nHeight = getBounds().height();
int mnDx = nHeight / 203;
// 绘制耗电电量提醒值电量
// 能量绘图风格
int nTop;
int nLeft = 0;
int nBottom;
int nRight = nWidth;
//for (int i = 0; i < mnValue; i ++) {
nBottom = nHeight;
nTop = nHeight - (nHeight * mBatteryValue / 100);
canvas.drawRect(new Rect(nLeft, nTop, nRight, nBottom), mBatteryPaint);
}
/**
@@ -131,15 +152,75 @@ public class BatteryDrawable extends Drawable {
* @param right 右边界
* @param height 绘制高度
*/
private void drawStripeStyle(Canvas canvas, int battery, int left, int right, int height) {
private void drawZebraStyle(Canvas canvas, int battery, int left, int right, int height) {
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制开始 | 电量=" + battery);
int stripeHeight = height / STRIPE_COUNT; // 单条条纹高度(均匀拆分)
// 从底部向上绘制对应电量条纹
for (int i = 0; i < battery; i++) {
int bottom = height - (stripeHeight * i);
int top = bottom - stripeHeight;
canvas.drawRect(new Rect(left, top, right, bottom), mBatteryPaint);
}
// int stripeHeight = height / STRIPE_COUNT; // 单条条纹高度(均匀拆分)
// // 从底部向上绘制对应电量条纹
// for (int i = 0; i < battery; i++) {
// int bottom = height - (stripeHeight * i);
// int top = bottom - stripeHeight;
// canvas.drawRect(new Rect(left, top, right, bottom), mBatteryPaint);
// }
int nWidth = getBounds().width();
int nHeight = getBounds().height();
int mnDx = nHeight / 203;
// 意兴阑珊绘图风格
int nTop;
int nLeft = 0;
int nBottom;
int nRight = nWidth;
for (int i = 0; i < mBatteryValue; i ++) {
nBottom = (nHeight * (100 - i) / 100) - mnDx;
nTop = nBottom + mnDx;
canvas.drawRect(new Rect(nLeft, nTop, nRight, nBottom), mBatteryPaint);
}
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制完成 | 条纹数量=" + battery);
}
/**
* 点阵风格绘制
* @param canvas 绘制画布
* @param battery 有效电量0-100
* @param left 左边界
* @param right 右边界
* @param height 绘制高度
*/
private void drawPointStyle(Canvas canvas, int battery, int left, int right, int height) {
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制开始 | 电量=" + battery);
int nWidth = getBounds().width();
int nHeight = getBounds().height();
int mnDx = nHeight / 203;
// 意兴阑珊绘图风格
int nTop;
int nLeft = 0;
int nBottom;
int nRight = nWidth;
int nLineWidth = nRight - nLeft;
int radius_horizontal = (nLineWidth / 10) / 2;
int radius_vertical = mnDx/2;
int radius = Math.min(radius_horizontal, radius_vertical);
for (int i = 0; i < mBatteryValue; i ++) {
nBottom = (nHeight * (100 - i) / 100) - mnDx;
nTop = nBottom + mnDx;
//canvas.drawRect(new Rect(nLeft, nTop, nRight, nBottom), mBatteryPaint);
for (int j = 0; j < 10; j++) {
// cx, cy 圆心坐标radius 半径paint 画笔
int cx = radius_horizontal + radius_horizontal * j * 2;
int cy = nTop + radius_vertical;
canvas.drawCircle(cx, cy, radius, mBatteryPaint);
}
}
LogUtils.d(TAG, "【drawStripeStyle】条纹风格绘制完成 | 条纹数量=" + battery);
}
@@ -159,9 +240,8 @@ public class BatteryDrawable extends Drawable {
* 切换绘制风格
* @param isEnergyStyle true=能量风格false=条纹风格
*/
public void switchDrawStyle(boolean isEnergyStyle) {
LogUtils.d(TAG, "【switchDrawStyle】风格切换 | 旧风格=" + (mIsEnergyStyle ? "能量" : "条纹") + " | 新风格=" + (isEnergyStyle ? "能量" : "条纹"));
mIsEnergyStyle = isEnergyStyle;
public void setDrawStyle(BatteryStyle batteryStyle) {
mBatteryStyle = batteryStyle;
invalidateSelf();
LogUtils.d(TAG, "【switchDrawStyle】已触发重绘");
}
@@ -188,12 +268,10 @@ public class BatteryDrawable extends Drawable {
return mBatteryValue;
}
/**
* 获取当前绘制风格
* @return true=能量风格false=条纹风格
*/
public boolean isEnergyStyle() {
return mIsEnergyStyle;
public BatteryStyle getEnergyStyle() {
return mBatteryStyle;
}
// ====================================== Drawable抽象方法必须实现精简逻辑 ======================================

View File

@@ -0,0 +1,285 @@
package cc.winboll.studio.powerbell.views;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.RelativeLayout;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.BatteryStyle;
/**
* 电池样式单选视图水平展示所有BatteryStyle枚举选项
* 每个选项 = RadioButton单选按钮 + BatteryDrawable预览控件
* 适配API30、Java7规范联动BatteryDrawable绘制样式
* 包含SP持久化存储 + 公共静态方法读取SP枚举值 + 彻底修复点击不回调+单选失效
* 默认选中BatteryStyle.ENERGY_STYLE
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
*/
public class BatteryStyleView extends LinearLayout implements RadioGroup.OnCheckedChangeListener {
// ====================== 常量区 ======================
public static final String TAG = "BatteryStyleView";
private static final int DEFAULT_BATTERY_COLOR = Color.parseColor("#FF4CAF50");
private static final int DEFAULT_CHECKED_STYLE_INDEX = 1; // ✅ 修改默认选中下标 1 = ENERGY_STYLE
public static final String SP_NAME = "sp_battery_style_config";
public static final String SP_KEY_BATTERY_STYLE = "key_selected_battery_style";
// ====================== 控件变量 ======================
private RadioGroup rgBatteryStyle;
private RadioButton rbZebraStyle;
private RadioButton rbEnergyStyle;
private RadioButton rbPointStyle; // ✅ 新增:圆点样式单选按钮
private RelativeLayout rlZebraPreview;
private RelativeLayout rlEnergyPreview;
private RelativeLayout rlPointPreview; // ✅ 新增:圆点样式预览布局
private BatteryDrawable mZebraDrawable;
private BatteryDrawable mEnergyDrawable;
private BatteryDrawable mPointDrawable; // ✅ 新增圆点样式Drawable实例
// ====================== 业务变量 ======================
private BatteryStyle mCurrentStyle = BatteryStyle.ENERGY_STYLE; // ✅ 修改默认样式为 能量样式
private OnBatteryStyleSelectedListener mStyleSelectedListener;
private int mBatteryColor = DEFAULT_BATTERY_COLOR;
private int mBatteryValue = 100;
private SharedPreferences mSp;
// ====================== 构造方法 ======================
public BatteryStyleView(Context context) {
super(context);
initSP(context);
initView(context, null);
}
public BatteryStyleView(Context context, AttributeSet attrs) {
super(context, attrs);
initSP(context);
initAttrs(context, attrs);
initView(context, attrs);
}
public BatteryStyleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initSP(context);
initAttrs(context, attrs);
initView(context, attrs);
}
// ====================== 初始化SP持久化 ======================
private void initSP(Context context) {
mSp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
LogUtils.d(TAG, "【initSP】SharedPreferences初始化完成文件名称 = " + SP_NAME);
}
// ====================== 初始化方法 ======================
private void initAttrs(Context context, AttributeSet attrs) {
if (attrs == null) return;
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BatteryStyleView);
mBatteryColor = typedArray.getColor(R.styleable.BatteryStyleView_batteryPreviewColor, DEFAULT_BATTERY_COLOR);
mBatteryValue = typedArray.getInt(R.styleable.BatteryStyleView_previewBatteryValue, 100);
int styleIndex = typedArray.getInt(R.styleable.BatteryStyleView_defaultSelectedStyle, DEFAULT_CHECKED_STYLE_INDEX);
mCurrentStyle = getStyleFromSP() == null ? (styleIndex == 0 ? BatteryStyle.ENERGY_STYLE : styleIndex ==1 ? BatteryStyle.ZEBRA_STYLE : BatteryStyle.POINT_STYLE) : getStyleFromSP();
typedArray.recycle();
LogUtils.d(TAG, "【initAttrs】解析属性完成 电量颜色=" + Integer.toHexString(mBatteryColor) + " 预览电量=" + mBatteryValue + " 默认样式=" + mCurrentStyle.name());
}
private void initView(Context context, AttributeSet attrs) {
LayoutInflater.from(context).inflate(R.layout.view_battery_style, this, true);
rgBatteryStyle = findViewById(R.id.rg_battery_style);
rbZebraStyle = findViewById(R.id.rb_zebra_style);
rbEnergyStyle = findViewById(R.id.rb_energy_style);
rbPointStyle = findViewById(R.id.rb_point_style); // ✅ 新增:绑定圆点样式单选按钮
rlZebraPreview = findViewById(R.id.rl_zebra_preview);
rlEnergyPreview = findViewById(R.id.rl_energy_preview);
rlPointPreview = findViewById(R.id.rl_point_preview); // ✅ 新增:绑定圆点样式预览布局
initPreviewDrawable();
rgBatteryStyle.setOnCheckedChangeListener(this);
addRadioBtnClickLister();
setDefaultChecked();
LogUtils.d(TAG, "【initView】视图初始化完成");
}
private void initPreviewDrawable() {
mZebraDrawable = new BatteryDrawable(mBatteryColor, BatteryStyle.ZEBRA_STYLE);
mZebraDrawable.setBatteryValue(mBatteryValue);
rlZebraPreview.setBackground(mZebraDrawable);
mEnergyDrawable = new BatteryDrawable(mBatteryColor, BatteryStyle.ENERGY_STYLE);
mEnergyDrawable.setBatteryValue(mBatteryValue);
rlEnergyPreview.setBackground(mEnergyDrawable);
// ✅ 新增初始化圆点样式Drawable + 绑定预览布局 + 设置电量值
mPointDrawable = new BatteryDrawable(mBatteryColor, BatteryStyle.POINT_STYLE);
mPointDrawable.setBatteryValue(mBatteryValue);
rlPointPreview.setBackground(mPointDrawable);
LogUtils.d(TAG, "【initPreviewDrawable】Drawable预览初始化完成");
}
private void setDefaultChecked() {
// ✅ 新增:圆点样式的默认选中判断
if (mCurrentStyle == BatteryStyle.ZEBRA_STYLE) {
rbZebraStyle.setChecked(true);
} else if (mCurrentStyle == BatteryStyle.POINT_STYLE) {
rbPointStyle.setChecked(true);
} else {
rbEnergyStyle.setChecked(true);
}
LogUtils.d(TAG, "【setDefaultChecked】默认选中样式 = " + mCurrentStyle.name());
}
private void addRadioBtnClickLister() {
rbZebraStyle.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
rbZebraStyle.setChecked(true);
rbEnergyStyle.setChecked(false);
rbPointStyle.setChecked(false); // ✅ 新增:取消圆点样式选中
handleStyleSelect(BatteryStyle.ZEBRA_STYLE);
}
});
rbEnergyStyle.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
rbEnergyStyle.setChecked(true);
rbZebraStyle.setChecked(false);
rbPointStyle.setChecked(false); // ✅ 新增:取消圆点样式选中
handleStyleSelect(BatteryStyle.ENERGY_STYLE);
}
});
// ✅ 新增:圆点样式单选按钮点击事件
rbPointStyle.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
rbPointStyle.setChecked(true);
rbZebraStyle.setChecked(false);
rbEnergyStyle.setChecked(false);
handleStyleSelect(BatteryStyle.POINT_STYLE);
}
});
}
// ====================== RadioGroup 选中回调 (点击必触发) ======================
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
ToastUtils.show("onCheckedChanged");
if (checkedId == R.id.rb_zebra_style) {
handleStyleSelect(BatteryStyle.ZEBRA_STYLE);
} else if (checkedId == R.id.rb_energy_style) {
handleStyleSelect(BatteryStyle.ENERGY_STYLE);
} else if (checkedId == R.id.rb_point_style) { // ✅ 新增:圆点样式选中回调
handleStyleSelect(BatteryStyle.POINT_STYLE);
}
}
private void handleStyleSelect(BatteryStyle style) {
mCurrentStyle = style;
saveStyle2SP(mCurrentStyle);
MainActivity.sendUpdateBatteryDrawableMessage();
LogUtils.d(TAG, "【handleStyleSelect】选中样式 → " + mCurrentStyle.name() + "已存入SP");
if (mStyleSelectedListener != null) {
mStyleSelectedListener.onStyleSelected(mCurrentStyle);
}
}
// ====================== SP持久化 存储+读取 封装方法 ======================
private void saveStyle2SP(BatteryStyle style) {
mSp.edit().putString(SP_KEY_BATTERY_STYLE, style.name()).commit();
}
private BatteryStyle getStyleFromSP() {
String styleStr = mSp.getString(SP_KEY_BATTERY_STYLE, null);
if (styleStr == null) return null;
try {
return BatteryStyle.valueOf(styleStr);
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "【getStyleFromSP】SP读取样式异常 = " + e.getMessage());
return null;
}
}
// ====================== 公共静态方法 读取SP存储的枚举值 ======================
public static BatteryStyle getSavedBatteryStyle(Context context) {
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String styleStr = sp.getString(SP_KEY_BATTERY_STYLE, null);
if (styleStr == null) {
LogUtils.w(TAG, "【getSavedBatteryStyle】SP无存储值返回默认样式 ENERGY_STYLE");
return BatteryStyle.ENERGY_STYLE; // ✅ 静态方法默认值同步修改为能量样式
}
try {
BatteryStyle style = BatteryStyle.valueOf(styleStr);
LogUtils.d(TAG, "【getSavedBatteryStyle】SP读取成功 → " + style.name());
return style;
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "【getSavedBatteryStyle】SP读取异常 = " + e.getMessage() + ",返回默认样式 ENERGY_STYLE");
return BatteryStyle.ENERGY_STYLE; // ✅ 异常兜底值同步修改为能量样式
}
}
public static BatteryStyle getSavedBatteryStyle(Context context, BatteryStyle defaultStyle) {
SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String styleStr = sp.getString(SP_KEY_BATTERY_STYLE, null);
if (styleStr == null) {
LogUtils.w(TAG, "【getSavedBatteryStyle】SP无存储值返回自定义默认样式 → " + defaultStyle.name());
return defaultStyle;
}
try {
BatteryStyle style = BatteryStyle.valueOf(styleStr);
LogUtils.d(TAG, "【getSavedBatteryStyle】SP读取成功 → " + style.name());
return style;
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "【getSavedBatteryStyle】SP读取异常 = " + e.getMessage() + ",返回自定义默认样式 → " + defaultStyle.name());
return defaultStyle;
}
}
// ====================== 对外暴露方法 ======================
public void setSelectedStyle(BatteryStyle style) {
mCurrentStyle = style;
// ✅ 新增:圆点样式的手动选中赋值
rbZebraStyle.setChecked(style == BatteryStyle.ZEBRA_STYLE);
rbEnergyStyle.setChecked(style == BatteryStyle.ENERGY_STYLE);
rbPointStyle.setChecked(style == BatteryStyle.POINT_STYLE);
saveStyle2SP(style);
LogUtils.d(TAG, "【setSelectedStyle】手动设置选中样式 → " + style.name() + "已存入SP");
}
public BatteryStyle getCurrentStyle() {
return mCurrentStyle;
}
public void setPreviewBatteryValue(int batteryValue) {
this.mBatteryValue = batteryValue;
mZebraDrawable.setBatteryValue(batteryValue);
mEnergyDrawable.setBatteryValue(batteryValue);
mPointDrawable.setBatteryValue(batteryValue); // ✅ 新增:圆点样式同步电量值
}
public void setPreviewBatteryColor(int color) {
this.mBatteryColor = color;
mZebraDrawable.updateBatteryColor(color);
mEnergyDrawable.updateBatteryColor(color);
mPointDrawable.updateBatteryColor(color); // ✅ 新增:圆点样式同步颜色值
}
public void setOnBatteryStyleSelectedListener(OnBatteryStyleSelectedListener listener) {
this.mStyleSelectedListener = listener;
}
// ====================== 选中回调接口 ======================
public interface OnBatteryStyleSelectedListener {
void onStyleSelected(BatteryStyle batteryStyle);
}
}

View File

@@ -15,18 +15,20 @@ import android.widget.RelativeLayout;
import android.widget.Switch;
import android.widget.TextView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.models.BatteryStyle;
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
/**
* 主页面核心视图封装类:统一管理视图绑定、数据更新、事件监听,解耦 Activity 逻辑
* 适配Java7 | API30 | 小米手机,优化性能与资源回收,杜绝内存泄漏,配置变更确认对话框
* 新增:拖动进度条时实时预览 sbUsageReminder 与 sbChargeReminder 比值
* 修复updateBatteryDrawable() 电池样式切换后重绘失效问题
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/17 13:14
*/
@@ -43,7 +45,7 @@ public class MainContentView {
private static final int BATTERY_MIN = 0;
private static final int BATTERY_MAX = 100;
// ====================================== 内部静态类(临时数据载体,避免外部依赖 ======================================
// ====================================== 内部缓存类(解耦,避免冗余 ======================================
/**
* 临时配置数据实体(缓存变更信息,取消时恢复)
*/
@@ -89,6 +91,8 @@ public class MainContentView {
public RelativeLayout mainLayout;
public MemoryCachedBackgroundView backgroundView;
private LinearLayout mllBackgroundView;
private volatile BatteryStyle mBatteryStyle = BatteryStyle.ENERGY_STYLE;
// 容器布局控件
public LinearLayout llLeftSeekBar;
public LinearLayout llRightSeekBar;
@@ -133,6 +137,7 @@ public class MainContentView {
this.mContext = context;
this.mActionListener = actionListener;
this.mAppConfigUtils = AppConfigUtils.getInstance(context.getApplicationContext());
mBatteryStyle = BatteryStyleView.getSavedBatteryStyle(context);
// 执行核心初始化流程(按顺序执行,避免依赖空指针)
bindViews(rootView);
@@ -202,7 +207,6 @@ public class MainContentView {
App.sBackgroundSourceUtils.loadSettings();
BackgroundBean backgroundBean = App.sBackgroundSourceUtils.getCurrentBackgroundBean();
backgroundView.loadByBackgroundBean(backgroundBean, true);
//App.notifyMessage(TAG, "reloadBackgroundView");
}
}
@@ -210,19 +214,60 @@ public class MainContentView {
* 初始化电池 Drawable集成 BatteryDrawable默认能量风格适配小米机型渲染
*/
private void initBatteryDrawables() {
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化开始");
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化开始 | style="+mBatteryStyle.name());
// 当前电量 Drawable颜色从资源读取适配 API30 主题)
int colorCurrent = getResourceColor(R.color.colorCurrent);
mCurrentBatteryDrawable = new BatteryDrawable(colorCurrent);
mCurrentBatteryDrawable.setDrawStyle(mBatteryStyle);
// 充电提醒 Drawable
int colorCharge = getResourceColor(R.color.colorCharge);
mChargeReminderBatteryDrawable = new BatteryDrawable(colorCharge);
// 耗电提醒 Drawable
mChargeReminderBatteryDrawable.setDrawStyle(mBatteryStyle);
// 耗电提醒 Drawable
int colorUsage = getResourceColor(R.color.colorUsege);
mUsageReminderBatteryDrawable = new BatteryDrawable(colorUsage);
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化完成");
mUsageReminderBatteryDrawable.setDrawStyle(mBatteryStyle);
LogUtils.d(TAG, "【initBatteryDrawables】电池Drawable初始化完成");
}
/**
* ✅ 核心修复:电池样式切换+强制重绘刷新 完整版
* 修复点1重新创建Drawable后重新给ImageView赋值Drawable
* 修复点2重置所有Drawable的电量值保证样式切换后数值不变
* 修复点3调用ImageView.invalidate()强制触发重绘API30必加
* 修复点4Drawable.invalidateSelf() 双保险刷新绘制内容
* @param batteryStyle 切换后的电池样式
*/
public void updateBatteryDrawable(BatteryStyle batteryStyle) {
if(batteryStyle == null || batteryStyle == mBatteryStyle){
LogUtils.d(TAG, "【updateBatteryDrawable】样式无变化跳过刷新");
return;
}
// 1. 更新样式标记
mBatteryStyle = batteryStyle;
// 2. 重新初始化Drawable并设置新样式
initBatteryDrawables();
// 3. 重置所有Drawable的电量值 → 保证样式切换后数值不变
mCurrentBatteryDrawable.setBatteryValue(mAppConfigUtils.getCurrentBatteryValue());
mChargeReminderBatteryDrawable.setBatteryValue(mCurrentChargeProgress);
mUsageReminderBatteryDrawable.setBatteryValue(mCurrentUsageProgress);
// 4. 重新给ImageView赋值Drawable → 核心修复:之前缺失这一步
if(ivCurrentBattery != null) ivCurrentBattery.setImageDrawable(mCurrentBatteryDrawable);
if(ivChargeReminderBattery != null) ivChargeReminderBattery.setImageDrawable(mChargeReminderBatteryDrawable);
if(ivUsageReminderBattery != null) ivUsageReminderBattery.setImageDrawable(mUsageReminderBatteryDrawable);
// 5. Drawable自身刷新 → 双保险
mCurrentBatteryDrawable.invalidateSelf();
mChargeReminderBatteryDrawable.invalidateSelf();
mUsageReminderBatteryDrawable.invalidateSelf();
// 6. ✅ API30关键修复ImageView强制重绘解决绘制缓存不刷新问题
if(ivCurrentBattery != null) ivCurrentBattery.invalidate();
if(ivChargeReminderBattery != null) ivChargeReminderBattery.invalidate();
if(ivUsageReminderBattery != null) ivUsageReminderBattery.invalidate();
LogUtils.d(TAG, "【updateBatteryDrawable】样式切换完成"+mBatteryStyle.name() + " | 重绘触发成功");
ToastUtils.show("电池样式已切换为:"+mBatteryStyle.name());
}
/**
* 初始化配置变更确认对话框(核心优化:保存 Builder 实例,解决消息不生效问题)
*/

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<!-- 阴影部分 -->
<!-- 个人觉得更形象的表达top代表下边的阴影高度left代表右边的阴影宽度。其实也就是相对应的offsetsolid中的颜色是阴影的颜色也可以设置角度等等 -->
<item
android:left="2dp"
android:top="2dp"
android:right="2dp"
android:bottom="2dp">
<shape android:shape="rectangle" >
<gradient
android:angle="270"
android:endColor="#0FFFFFFF"
android:startColor="#0FFFFFFF" />
<corners
android:bottomLeftRadius="6dip"
android:bottomRightRadius="6dip"
android:topLeftRadius="6dip"
android:topRightRadius="6dip" />
</shape>
</item>
<!-- 背景部分 -->
<!-- 形象的表达bottom代表背景部分在上边缘超出阴影的高度right代表背景部分在左边超出阴影的宽度相对应的offset -->
<item
android:left="3dp"
android:top="3dp"
android:right="3dp"
android:bottom="5dp">
<shape android:shape="rectangle" >
<gradient
android:angle="270"
android:endColor="#CFFFFFFF"
android:startColor="#CFFFFFFF" />
<corners
android:bottomLeftRadius="6dip"
android:bottomRightRadius="6dip"
android:topLeftRadius="6dip"
android:topRightRadius="6dip" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#ff000000"
android:pathData="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z"/>
</vector>

View File

@@ -113,8 +113,9 @@
<LinearLayout
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="match_parent">
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0">
<TextView
android:layout_width="match_parent"
@@ -127,7 +128,7 @@
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:id="@+id/fragmentandroidviewImageView2"
android:layout_weight="1.0"/>
@@ -135,7 +136,7 @@
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0">
@@ -150,7 +151,7 @@
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:id="@+id/fragmentandroidviewImageView1"
android:layout_weight="1.0"/>
@@ -158,8 +159,9 @@
<LinearLayout
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="match_parent">
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0">
<TextView
android:layout_width="match_parent"
@@ -172,7 +174,7 @@
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:id="@+id/fragmentandroidviewImageView3"
android:layout_weight="1.0"/>

View File

@@ -14,17 +14,46 @@
style="@style/DefaultAToolbar"/>
<LinearLayout
android:orientation="horizontal"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<Button
android:layout_width="wrap_content"
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用电提醒启用时的TTS贴心服务"
android:onClick="onEnableUsePowerTts"
android:id="@+id/activitysettingsCheckBox1"/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="充电提醒启用时的TTS贴心服务"
android:onClick="onEnableChargeTts"
android:id="@+id/activitysettingsCheckBox2"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="CheckPermission"
android:padding="10dp"
android:onClick="onCheckPermission"/>
android:gravity="right">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="检查TTS语音悬浮窗权限"
android:padding="10dp"
android:onClick="onCheckTTSDrawOverlaysPermission"/>
</LinearLayout>
</LinearLayout>
@@ -33,5 +62,20 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0">
<cc.winboll.studio.powerbell.views.BatteryStyleView
android:id="@+id/battery_style_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:batteryPreviewColor="@color/colorPrimary"
app:previewBatteryValue="100"
app:defaultSelectedStyle="zebra_style"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<RadioGroup
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rg_battery_style"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp"
android:spacing="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="4dp">
<RadioButton
android:id="@+id/rb_energy_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/energy_style"
android:textSize="14sp"
android:buttonTint="@color/colorPrimary"
android:textColor="@color/colorPrimary"/>
<RelativeLayout
android:id="@+id/rl_energy_preview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#F5F5F5"
android:padding="1dp"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="4dp">
<RadioButton
android:id="@+id/rb_zebra_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/zebra_style"
android:textSize="14sp"
android:buttonTint="@color/colorPrimary"
android:textColor="@color/colorPrimary"/>
<RelativeLayout
android:id="@+id/rl_zebra_preview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#F5F5F5"
android:padding="1dp"
android:layout_weight="1.0"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="4dp">
<RadioButton
android:id="@+id/rb_point_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/point_style"
android:textSize="14sp"
android:buttonTint="@color/colorPrimary"
android:textColor="@color/colorPrimary"/>
<RelativeLayout
android:id="@+id/rl_point_preview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#F5F5F5"
android:padding="1dp"
android:layout_weight="1.0"/>
</LinearLayout>
</RadioGroup>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_frame_white"
android:layout_gravity="center_vertical|center_horizontal"
android:id="@+id/viewttsbackLinearLayout1">
<LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
android:gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textColor="#FF000000"
android:textStyle="bold"
android:textAppearance="?android:attr/textAppearanceLarge"/>
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:textAllCaps="false"
android:background="@drawable/speaker"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click To Stop TTS Play"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="#FF000000"/>
</LinearLayout>
</LinearLayout>

View File

@@ -1,3 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- BatteryStyleView 自定义属性 -->
<declare-styleable name="BatteryStyleView">
<attr name="batteryPreviewColor" format="color"/>
<attr name="previewBatteryValue" format="integer"/>
<attr name="defaultSelectedStyle" format="integer">
<enum name="zebra_style" value="0"/>
<enum name="energy_style" value="1"/>
</attr>
</declare-styleable>
</resources>

View File

@@ -32,6 +32,9 @@
<string name="subtitle_activity_about">About The APP</string>
<string name="msg_AOHPCTCSeekBar_ClearRecord">&gt;&gt;&gt;Seek 100% Right Is Clean Record.&gt;&gt;&gt;</string>
<string name="msg_no_battery_record">No Battery Record</string>
<string name="zebra_style">Zebra Style</string>
<string name="energy_style">Energy Style</string>
<string name="point_style">Point Style</string>
<!-- 权限申请相关字符串(统一管理,避免硬编码) -->
<string name="permission_title">权限申请</string>