diff --git a/appbase/build.properties b/appbase/build.properties index 3c4621e..a163812 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Wed Nov 12 20:28:00 GMT 2025 +#Wed Nov 12 21:06:14 GMT 2025 stageCount=10 libraryProject=libappbase baseVersion=15.10 publishVersion=15.10.9 -buildCount=22 +buildCount=27 baseBetaVersion=15.10.10 diff --git a/appbase/src/main/java/cc/winboll/studio/appbase/App.java b/appbase/src/main/java/cc/winboll/studio/appbase/App.java index 025983f..888d581 100644 --- a/appbase/src/main/java/cc/winboll/studio/appbase/App.java +++ b/appbase/src/main/java/cc/winboll/studio/appbase/App.java @@ -1,27 +1,40 @@ package cc.winboll.studio.appbase; +import cc.winboll.studio.libappbase.GlobalApplication; +import cc.winboll.studio.libappbase.ToastUtils; + /** * @Author ZhanGSKen * @Date 2025/01/05 09:54:42 - * @Describe APPbase 应用类 + * @Describe 应用全局入口类(继承基础库 GlobalApplication) + * 负责应用初始化、全局资源管理与生命周期回调处理,是整个应用的核心入口 */ -import cc.winboll.studio.libappbase.GlobalApplication; -import cc.winboll.studio.libappbase.ToastUtils; - public class App extends GlobalApplication { + /** 当前应用类的日志 TAG(用于调试输出,标识日志来源) */ public static final String TAG = "App"; - + + /** + * 应用创建时回调(全局初始化入口) + * 在应用进程启动时执行,仅调用一次,用于初始化全局工具类、第三方库等 + */ @Override public void onCreate() { - super.onCreate(); - ToastUtils.init(getApplicationContext()); - + super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置) + // 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用) + ToastUtils.init(getApplicationContext()); } - @Override - public void onTerminate() { - super.onTerminate(); - ToastUtils.release(); - } + /** + * 应用终止时回调(资源释放入口) + * 仅在模拟环境(如 Android Studio 模拟器)中可靠触发,真机上可能因系统回收进程不执行 + * 用于释放全局资源,避免内存泄漏 + */ + @Override + public void onTerminate() { + super.onTerminate(); // 调用父类终止逻辑(如基础库资源释放) + // 释放 Toast 工具类资源(销毁全局 Toast 实例,避免内存泄漏) + ToastUtils.release(); + } } + diff --git a/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java b/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java index 4588491..387fab3 100644 --- a/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java +++ b/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java @@ -14,75 +14,127 @@ import cc.winboll.studio.libappbase.LogActivity; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; +/** + * @Author ZhanGSKen + * @Date 未标注(建议补充创建日期) + * @Describe 应用主界面 Activity(入口界面) + * 包含功能测试按钮(崩溃测试、日志查看、Toast测试)、顶部工具栏(菜单功能),是应用交互的核心入口 + */ public class MainActivity extends Activity { + /** 当前 Activity 的日志 TAG(用于调试输出,标识日志来源) */ public static final String TAG = "MainActivity"; - Toolbar mToolbar; + /** 顶部工具栏(用于展示标题、菜单,绑定布局中的 Toolbar 控件) */ + private Toolbar mToolbar; + /** + * Activity 创建时回调(初始化界面) + * 在 Activity 首次创建时执行,用于加载布局、初始化控件、设置事件监听 + * @param savedInstanceState 保存 Activity 状态的 Bundle(如屏幕旋转时的数据恢复) + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ToastUtils.show("onCreate"); - setContentView(R.layout.activity_main); + ToastUtils.show("onCreate"); // 显示 Activity 创建提示(调试用) + setContentView(R.layout.activity_main); // 加载主界面布局 + // 初始化 Toolbar 并设置为 ActionBar mToolbar = findViewById(R.id.toolbar); - setActionBar(mToolbar); + setActionBar(mToolbar); // 将 Toolbar 替代系统默认 ActionBar } + /** + * 创建菜单时回调(加载工具栏菜单) + * 初始化 ActionBar 菜单,加载自定义菜单布局 + * @param menu 菜单对象(用于承载菜单项) + * @return true:显示菜单;false:不显示菜单 + */ @Override public boolean onCreateOptionsMenu(Menu menu) { + // 加载菜单布局(R.menu.toolbar_main 为自定义菜单文件) getMenuInflater().inflate(R.menu.toolbar_main, menu); return super.onCreateOptionsMenu(menu); } + /** + * 菜单 item 点击时回调(处理菜单事件) + * 响应 Toolbar 菜单项的点击事件,执行对应业务逻辑 + * @param item 被点击的菜单项 + * @return true:消费点击事件;false:不消费(传递给父类) + */ @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.item_home : { - openWebsiteInBrowser(this); - } - } - // 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。 + switch (item.getItemId()) { + case R.id.item_home: + // 点击 "首页/官网" 菜单项,唤起浏览器打开指定网站 + openWebsiteInBrowser(this); + break; + // 可扩展其他菜单项(如设置、关于等)的处理逻辑 + } return super.onOptionsItemSelected(item); } + /** + * 崩溃测试按钮点击事件(触发应用崩溃,用于调试异常捕获) + * 故意执行非法操作(循环获取不存在的字符串资源),强制应用崩溃 + * @param view 触发事件的 View(对应布局中的崩溃测试按钮) + */ public void onCrashTest(View view) { - for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) { - getString(i); - } + // 循环从 Integer.MIN_VALUE 到 Integer.MAX_VALUE,获取不存在的字符串资源 ID,触发崩溃 + for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) { + getString(i); // i 超出资源 ID 范围,抛出 Resources.NotFoundException 导致崩溃 + } } + /** + * 日志测试按钮点击事件(打开日志查看界面) + * 启动 LogActivity,用于查看应用运行日志 + * @param view 触发事件的 View(对应布局中的日志测试按钮) + */ public void onLogTest(View view) { + // 启动日志查看 Activity(通过静态方法传入上下文,简化跳转逻辑) LogActivity.startLogActivity(this); } - - public void onToastUtilsTest(View view) { - LogUtils.d(TAG, "onToastUtilsTest"); - ToastUtils.show("Hello, WinBoLL!"); - new Thread(new Runnable(){ + + /** + * Toast 工具测试按钮点击事件(测试全局 Toast 功能) + * 测试主线程、子线程中 Toast 的显示效果,验证 ToastUtils 的可用性 + * @param view 触发事件的 View(对应布局中的 Toast 测试按钮) + */ + public void onToastUtilsTest(View view) { + LogUtils.d(TAG, "onToastUtilsTest"); // 打印调试日志,标识进入 Toast 测试 + ToastUtils.show("Hello, WinBoLL!"); // 主线程显示 Toast + + // 开启子线程,延迟 2 秒后显示 Toast(测试子线程 Toast 兼容性) + new Thread(new Runnable() { @Override public void run() { try { - Thread.sleep(2000); + Thread.sleep(2000); // 线程休眠 2 秒 + // 若 ToastUtils 已处理主线程切换,此处可直接调用;否则需通过 Handler 切换到主线程 ToastUtils.show("Thread.sleep(2000);ToastUtils.show..."); - } catch (InterruptedException e) {} + } catch (InterruptedException e) { + // 捕获线程中断异常(如线程被销毁时),不做处理(测试场景) + e.printStackTrace(); + } } }).start(); } - /** - * 唤起默认浏览器打开指定网站 - * @param context 上下文(如 Activity.this) - */ - public void openWebsiteInBrowser(Context context) { - // 目标网站地址 - String url = "https://www.winboll.cc"; - // 构建打开浏览器的意图 - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - // 设置标志:避免创建新的任务栈(可选,按需求调整) - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - - } + /** + * 唤起系统默认浏览器打开指定网站(跳转至应用官网) + * 通过 Intent.ACTION_VIEW 隐式意图,触发浏览器打开目标 URL + * @param context 上下文对象(如 Activity、Application,此处为 MainActivity) + */ + public void openWebsiteInBrowser(Context context) { + String url = "https://www.winboll.cc"; // 目标网站 URL(应用官网) + // 构建隐式意图:ACTION_VIEW 表示查看指定数据(Uri 为网站地址) + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + // 设置标志:在新的任务栈中启动 Activity(避免与当前应用任务栈混淆) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 启动意图(唤起浏览器) + context.startActivity(intent); + } } + diff --git a/libappbase/build.properties b/libappbase/build.properties index 3c4621e..a163812 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Wed Nov 12 20:28:00 GMT 2025 +#Wed Nov 12 21:06:14 GMT 2025 stageCount=10 libraryProject=libappbase baseVersion=15.10 publishVersion=15.10.9 -buildCount=22 +buildCount=27 baseBetaVersion=15.10.10 diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/APPModel.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/APPModel.java index 46b2ee6..d4dc9ca 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/APPModel.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/APPModel.java @@ -1,5 +1,9 @@ package cc.winboll.studio.libappbase; +import android.util.JsonReader; +import android.util.JsonWriter; +import java.io.IOException; + /** * @Author ZhanGSKen&豆包大模型 * @Date 2025/11/11 20:01 @@ -7,10 +11,6 @@ package cc.winboll.studio.libappbase; * 继承自 BaseBean,用于存储和管理应用的核心配置信息(如调试状态), * 支持 JSON 序列化/反序列化,便于数据持久化或跨组件传递 */ -import android.util.JsonReader; -import android.util.JsonWriter; -import java.io.IOException; - public class APPModel extends BaseBean { /** diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/BaseBean.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/BaseBean.java index b8b1f2f..372e6ef 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/BaseBean.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/BaseBean.java @@ -1,13 +1,5 @@ package cc.winboll.studio.libappbase; -/** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/11/11 20:03 - * @Describe WinBoLL JSON 数据模型基类(抽象类) - * 定义 Json Bean 的核心规范:序列化/反序列化、文件持久化、列表处理等通用逻辑, - * 子类(如 APPModel)需实现抽象方法,实现自身字段的 JSON 读写 - * @param 泛型约束,限定子类必须继承自 BaseBean - */ import android.content.Context; import android.util.JsonReader; import android.util.JsonWriter; @@ -17,6 +9,14 @@ import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/11 20:03 + * @Describe WinBoLL JSON 数据模型基类(抽象类) + * 定义 Json Bean 的核心规范:序列化/反序列化、文件持久化、列表处理等通用逻辑, + * 子类(如 APPModel)需实现抽象方法,实现自身字段的 JSON 读写 + * @param 泛型约束,限定子类必须继承自 BaseBean + */ public abstract class BaseBean { /** 日志标签,用于当前基类的日志输出标识 */ diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java index 4594662..2432cd9 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java @@ -1,12 +1,5 @@ package cc.winboll.studio.libappbase; -/** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/11/11 20:14 - * @Describe * 应用全局崩溃处理类(单例逻辑) - * 核心功能:捕获应用未捕获异常,记录崩溃日志到文件,启动崩溃报告页面, - * 并通过「崩溃保险丝」机制防止重复崩溃,保障基础功能可用 - */ import android.app.Activity; import android.app.Application; import android.content.ActivityNotFoundException; @@ -43,6 +36,13 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/11 20:14 + * @Describe * 应用全局崩溃处理类(单例逻辑) + * 核心功能:捕获应用未捕获异常,记录崩溃日志到文件,启动崩溃报告页面, + * 并通过「崩溃保险丝」机制防止重复崩溃,保障基础功能可用 + */ public final class CrashHandler { /** 日志标签,用于当前类的日志输出标识 */ diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java index b0018c8..2c734af 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java @@ -1,17 +1,17 @@ package cc.winboll.studio.libappbase; +import android.app.Application; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; + /** * @Author ZhanGSKen&豆包大模型 * @Date 2025/11/11 19:56 * @Describe 全局 Application 类,用于初始化应用核心组件、管理全局状态(如调试模式) * 需在 AndroidManifest.xml 中配置 android:name=".GlobalApplication" 使其生效 */ -import android.app.Application; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; - public class GlobalApplication extends Application { /** 日志标签 */ diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java index 5fcd340..1d0519d 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashActivity.java @@ -1,12 +1,5 @@ package cc.winboll.studio.libappbase; -/** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/11/11 19:58 - * @Describe 应用异常报告观察活动窗口类 - * 核心功能:应用发生未捕获崩溃时,由 CrashHandler 启动此页面,展示崩溃日志详情, - * 并提供「复制日志」「重启应用」操作入口,便于开发者定位问题和用户恢复应用 - */ import android.app.Activity; import android.content.ClipData; import android.content.ClipboardManager; @@ -19,6 +12,13 @@ import android.view.MenuItem; import android.widget.Toast; import cc.winboll.studio.libappbase.R; +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/11 19:58 + * @Describe 应用异常报告观察活动窗口类 + * 核心功能:应用发生未捕获崩溃时,由 CrashHandler 启动此页面,展示崩溃日志详情, + * 并提供「复制日志」「重启应用」操作入口,便于开发者定位问题和用户恢复应用 + */ public final class GlobalCrashActivity extends Activity implements MenuItem.OnMenuItemClickListener { /** 日志标签(用于调试日志输出,唯一标识当前 Activity) */ diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashReportView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashReportView.java index cb34e7b..9151822 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashReportView.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalCrashReportView.java @@ -1,11 +1,5 @@ package cc.winboll.studio.libappbase; -/** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/11/11 20:21 - * @Describe 全局崩溃报告视图控件 - * 用于展示应用崩溃信息,包含顶部工具栏和崩溃日志文本区域,支持自定义配色 - */ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; @@ -19,6 +13,12 @@ import android.widget.TextView; import android.widget.Toolbar; import cc.winboll.studio.libappbase.R; +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/11 20:21 + * @Describe 全局崩溃报告视图控件 + * 用于展示应用崩溃信息,包含顶部工具栏和崩溃日志文本区域,支持自定义配色 + */ public class GlobalCrashReportView extends LinearLayout { // 日志标签 diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/HorizontalListView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/HorizontalListView.java index 4881a25..1a48fb6 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/HorizontalListView.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/HorizontalListView.java @@ -1,17 +1,17 @@ package cc.winboll.studio.libappbase; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ListView; +import android.widget.Scroller; + /** * @Author ZhanGSKen&豆包大模型 * @Date 2025/11/11 20:26 * @Describe 水平滚动 ListView 控件 * 继承自 ListView,重写布局和测量逻辑,实现子项水平排列和滚动,替代默认垂直布局 */ -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ListView; -import android.widget.Scroller; - public class HorizontalListView extends ListView { /** 日志标签,用于当前控件的日志输出标识 */ public static final String TAG = "HorizontalListView"; diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogActivity.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogActivity.java index 53e80ba..e182a1b 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogActivity.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogActivity.java @@ -1,11 +1,5 @@ package cc.winboll.studio.libappbase; -/** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/11/11 20:29 - * @Describe 应用日志展示 Activity - * 用于单独启动窗口展示应用运行日志,依赖 LogView 控件实现日志加载与显示 - */ import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -13,6 +7,12 @@ import android.os.Bundle; import cc.winboll.studio.libappbase.LogView; import cc.winboll.studio.libappbase.R; +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/11 20:29 + * @Describe 应用日志展示 Activity + * 用于单独启动窗口展示应用运行日志,依赖 LogView 控件实现日志加载与显示 + */ public class LogActivity extends Activity { /** 日志标签,用于当前 Activity 的日志输出标识 */ diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java index 2318ae9..ca0f29d 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java @@ -1,14 +1,6 @@ package cc.winboll.studio.libappbase; -/** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/11/11 20:36 - * @Describe WinBoLl 应用日志管理工具类(单例逻辑) - * 核心功能:日志分级控制、日志文件读写、TAG 过滤配置、应用内所有 TAG 自动扫描 - * 支持 Debug/Release 模式区分存储路径,日志持久化与清理 - */ import android.content.Context; -import cc.winboll.studio.libappbase.GlobalApplication; import dalvik.system.DexFile; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -29,6 +21,14 @@ import java.util.List; import java.util.Locale; import java.util.Map; +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/11 20:36 + * @Describe WinBoLl 应用日志管理工具类(单例逻辑) + * 核心功能:日志分级控制、日志文件读写、TAG 过滤配置、应用内所有 TAG 自动扫描 + * 支持 Debug/Release 模式区分存储路径,日志持久化与清理 + * 适配 Java 7 语法,移除 Lambda、Stream 等 Java 8+ 特性 + */ public class LogUtils { /** 当前工具类的日志 TAG */ @@ -57,7 +57,7 @@ public class LogUtils { /** 日志配置实体类(封装日志级别等配置) */ private static LogUtilsBean sLogConfigBean; /** TAG 过滤映射表(key:TAG 名称;value:是否启用该 TAG 的日志输出) */ - public static Map sTagEnableMap = new HashMap<>(); + public static Map sTagEnableMap = new HashMap(); /** * 初始化日志工具(默认日志级别:Off,不输出日志) @@ -131,11 +131,11 @@ public class LogUtils { * 从 LogUtilsClassTAGBean 列表中读取每个 TAG 的启用状态,更新到映射表 */ private static void loadTagEnableSettings() { - ArrayList tagSettingList = new ArrayList<>(); + ArrayList tagSettingList = new ArrayList(); // 从文件加载 TAG 配置列表 LogUtilsClassTAGBean.loadBeanList(sContext, tagSettingList, LogUtilsClassTAGBean.class); - // 遍历配置列表,更新 TAG 启用状态 + // 遍历配置列表,更新 TAG 启用状态(Java 7 增强 for 循环) for (LogUtilsClassTAGBean tagSetting : tagSettingList) { String tag = tagSetting.getTag(); boolean isEnable = tagSetting.getEnable(); @@ -151,9 +151,11 @@ public class LogUtils { * 将映射表中的 TAG 及其启用状态转换为 LogUtilsClassTAGBean 列表,持久化到文件 */ private static void saveTagEnableSettings() { - ArrayList tagSettingList = new ArrayList<>(); - // 遍历映射表,构建配置列表 - for (Map.Entry entry : sTagEnableMap.entrySet()) { + ArrayList tagSettingList = new ArrayList(); + // 遍历映射表,构建配置列表(Java 7 迭代器遍历) + Iterator> iterator = sTagEnableMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); tagSettingList.add(new LogUtilsClassTAGBean(entry.getKey(), entry.getValue())); } // 保存配置列表到文件 @@ -178,10 +180,10 @@ public class LogUtils { Enumeration classNames = dexFile.entries(); int totalClassCount = 0; // 总类数(调试用) - List targetClassNames = new ArrayList<>(); // 目标包名下的类名列表 + List targetClassNames = new ArrayList(); // 目标包名下的类名列表 String targetPackagePrefix = "cc.winboll.studio"; // 目标包名前缀 - // 过滤目标包名下的类 + // 过滤目标包名下的类(Java 7 枚举遍历) while (classNames.hasMoreElements()) { totalClassCount++; String className = classNames.nextElement(); @@ -193,7 +195,7 @@ public class LogUtils { // 打印扫描统计(调试用) d(TAG, String.format("APK 总类数:%d,目标包下类数:%d", totalClassCount, targetClassNames.size())); - // 反射获取每个类的 TAG 字段 + // 反射获取每个类的 TAG 字段(Java 7 增强 for 循环) for (String className : targetClassNames) { try { Class clazz = Class.forName(className); @@ -211,9 +213,13 @@ public class LogUtils { sTagEnableMap.put(tagValue, false); } } - } catch (NoClassDefFoundError | ClassNotFoundException | IllegalAccessException e) { + } catch (NoClassDefFoundError e) { // 捕获反射异常,避免单个类扫描失败影响整体 d(TAG, e.getMessage(), Thread.currentThread().getStackTrace()); + } catch (ClassNotFoundException e) { + d(TAG, e.getMessage(), Thread.currentThread().getStackTrace()); + } catch (IllegalAccessException e) { + d(TAG, e.getMessage(), Thread.currentThread().getStackTrace()); } } } catch (IOException e) { @@ -228,7 +234,7 @@ public class LogUtils { * @param isEnable 是否启用(true:输出该 TAG 的日志;false:不输出) */ public static void setTagEnable(String tag, boolean isEnable) { - // 遍历映射表,更新目标 TAG 的状态 + // 遍历映射表,更新目标 TAG 的状态(Java 7 迭代器遍历) Iterator> iterator = sTagEnableMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry entry = iterator.next(); @@ -247,7 +253,7 @@ public class LogUtils { * @param isEnable 是否启用(true:所有 TAG 均输出日志;false:所有 TAG 均不输出) */ public static void setAllTagsEnable(boolean isEnable) { - // 遍历映射表,批量更新所有 TAG 的状态 + // 遍历映射表,批量更新所有 TAG 的状态(Java 7 迭代器遍历) Iterator> iterator = sTagEnableMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry entry = iterator.next(); @@ -333,6 +339,34 @@ public class LogUtils { } } + /** + * 输出 Error 级别日志(带异常信息和调用栈) + * 错误级别专用,包含完整异常详情,便于错误定位和排查 + * @param tag TAG 名称 + * @param message 日志内容 + * @param e 异常对象(存储异常信息和调用栈) + */ + public static void e(String tag, String message, Exception e) { + if (isLoggable(tag, LOG_LEVEL.Error)) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + StringBuilder sb = new StringBuilder(message); + // 拼接异常信息(类型 + 消息) + sb.append(" \nException: ") + .append(e.getClass().getSimpleName()) + .append(" : ") + .append(e.getMessage() != null ? e.getMessage() : "无异常消息"); + // 拼接调用栈信息(stackTrace[2] 为实际调用处) + sb.append(" \nAt ") + .append(stackTrace[2].getMethodName()) + .append(" (") + .append(stackTrace[2].getFileName()) + .append(":") + .append(stackTrace[2].getLineNumber()) + .append(")"); + saveLog(tag, LOG_LEVEL.Error, sb.toString()); + } + } + /** * 输出 Warn 级别日志 * @param tag TAG 名称 @@ -344,6 +378,34 @@ public class LogUtils { } } + /** + * 输出 Warn 级别日志(带异常信息和调用栈) + * 包含日志内容、异常详情、调用位置,便于警告场景下的问题定位 + * @param tag TAG 名称 + * @param message 日志内容 + * @param e 异常对象(存储异常信息和调用栈) + */ + public static void w(String tag, String message, Exception e) { + if (isLoggable(tag, LOG_LEVEL.Warn)) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + StringBuilder sb = new StringBuilder(message); + // 拼接异常信息(类型 + 消息) + sb.append(" \nException: ") + .append(e.getClass().getSimpleName()) + .append(" : ") + .append(e.getMessage() != null ? e.getMessage() : "无异常消息"); + // 拼接调用栈信息(stackTrace[2] 为实际调用处) + sb.append(" \nAt ") + .append(stackTrace[2].getMethodName()) + .append(" (") + .append(stackTrace[2].getFileName()) + .append(":") + .append(stackTrace[2].getLineNumber()) + .append(")"); + saveLog(tag, LOG_LEVEL.Warn, sb.toString()); + } + } + /** * 输出 Info 级别日志 * @param tag TAG 名称 @@ -399,9 +461,9 @@ public class LogUtils { if (isLoggable(tag, LOG_LEVEL.Debug)) { StringBuilder sb = new StringBuilder(); // 拼接异常信息 - sb.append(e.getClass().toGenericString()) + sb.append(e.getClass().getSimpleName()) .append(" : ") - .append(e.getMessage()) + .append(e.getMessage() != null ? e.getMessage() : "无异常消息") // 拼接调用栈信息 .append(" \nAt ") .append(stackTrace[2].getMethodName()) @@ -414,6 +476,35 @@ public class LogUtils { } } + /** + * 输出 Debug 级别日志(带日志内容+异常对象,简化调用) + * 无需手动传入调用栈,内部自动获取,适配常见调试场景 + * @param tag TAG 名称 + * @param message 日志内容 + * @param e 异常对象(存储异常信息和调用栈) + */ + public static void d(String tag, String message, Exception e) { + if (isLoggable(tag, LOG_LEVEL.Debug)) { + // 自动获取当前线程调用栈,简化外部调用 + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + StringBuilder sb = new StringBuilder(message); + // 拼接异常信息(类型 + 消息) + sb.append(" \nException: ") + .append(e.getClass().getSimpleName()) + .append(" : ") + .append(e.getMessage() != null ? e.getMessage() : "无异常消息"); + // 拼接调用栈信息(stackTrace[2] 为实际调用处) + sb.append(" \nAt ") + .append(stackTrace[2].getMethodName()) + .append(" (") + .append(stackTrace[2].getFileName()) + .append(":") + .append(stackTrace[2].getLineNumber()) + .append(")"); + saveLog(tag, LOG_LEVEL.Debug, sb.toString()); + } + } + /** * 输出 Verbose 级别日志(最详细级别) * @param tag TAG 名称 @@ -455,9 +546,9 @@ public class LogUtils { writer.write(logContent); } catch (IOException e) { // 日志写入失败时,输出内部调试日志 - d(TAG, "日志写入失败:" + e.getMessage()); + d(TAG, "日志写入失败:" + (e.getMessage() != null ? e.getMessage() : "未知错误")); } finally { - // 关闭流,避免资源泄漏 + // 关闭流,避免资源泄漏(Java 7 手动关闭,不使用 try-with-resources) if (writer != null) { try { writer.close(); @@ -489,13 +580,13 @@ public class LogUtils { ) ); String line; - // 逐行读取并拼接 + // 逐行读取并拼接(Java 7 普通 while 循环) while ((line = reader.readLine()) != null) { logContent.append(line).append("\n"); } } catch (IOException e) { // 读取失败时,输出内部调试日志 - d(TAG, "日志读取失败:" + e.getMessage()); + d(TAG, "日志读取失败:" + (e.getMessage() != null ? e.getMessage() : "未知错误")); } finally { // 关闭流,避免资源泄漏 if (reader != null) { @@ -518,8 +609,15 @@ public class LogUtils { } try { - // 写入空字符串到文件,实现清空 - UTF8FileUtils.writeStringToFile(sLogFile.getPath(), ""); + // 写入空字符串到文件,实现清空(Java 7 手动处理流) + BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter( + new FileOutputStream(sLogFile), + "UTF-8" + ) + ); + writer.write(""); + writer.close(); } catch (IOException e) { // 清空失败时,输出内部调试日志(带调用栈) d(TAG, e, Thread.currentThread().getStackTrace()); diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsBean.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsBean.java index 1a9d211..b8556b9 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsBean.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsBean.java @@ -1,70 +1,131 @@ package cc.winboll.studio.libappbase; +import android.util.JsonReader; +import android.util.JsonWriter; +import java.io.IOException; + /** * @Author ZhanGSKen * @Date 2024/08/23 15:39:07 - * @Describe LogUtils 数据配置类。 + * @Describe LogUtils 配置数据模型(继承 BaseBean,实现 JSON 序列化/反序列化) + * 封装 LogUtils 的核心配置参数(当前仅日志级别),用于配置的持久化存储与读取 */ -import android.util.JsonReader; -import android.util.JsonWriter; -import java.io.IOException; public class LogUtilsBean extends BaseBean { + /** 当前类的日志 TAG(用于调试输出) */ public static final String TAG = "LogUtilsBean"; - LogUtils.LOG_LEVEL logLevel; + /** + * 全局日志级别(默认值:Off,即不输出任何日志) + * 关联 LogUtils.LOG_LEVEL 枚举,存储日志输出的级别阈值 + */ + private LogUtils.LOG_LEVEL logLevel; + /** + * 无参构造方法(默认初始化日志级别为 Off) + * 用于 JSON 反序列化时的实例创建 + */ public LogUtilsBean() { this.logLevel = LogUtils.LOG_LEVEL.Off; } + /** + * 有参构造方法(指定初始日志级别) + * @param logLevel 初始日志级别(如 LogUtils.LOG_LEVEL.Debug) + */ public LogUtilsBean(LogUtils.LOG_LEVEL logLevel) { this.logLevel = logLevel; } + /** + * 设置日志级别(更新配置时使用) + * @param logLevel 目标日志级别 + */ public void setLogLevel(LogUtils.LOG_LEVEL logLevel) { this.logLevel = logLevel; } + /** + * 获取当前日志级别(读取配置时使用) + * @return 当前配置的日志级别 + */ public LogUtils.LOG_LEVEL getLogLevel() { return logLevel; } + /** + * 重写父类方法:获取当前类的全限定名(用于 BaseBean 反射识别) + * @return 类全限定名(如 "cc.winboll.studio.libappbase.LogUtilsBean") + */ @Override public String getName() { return LogUtilsBean.class.getName(); } + /** + * 重写父类方法:将当前配置对象序列化为 JSON(持久化存储时调用) + * 序列化字段:logLevel(存储枚举的 ordinal 值,确保反序列化一致性) + * @param jsonWriter JSON 写入器(用于输出 JSON 数据) + * @throws IOException JSON 写入异常(如流关闭、格式错误) + */ @Override public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + // 调用父类序列化逻辑(若 BaseBean 有公共字段,需优先处理) super.writeThisToJsonWriter(jsonWriter); - LogUtilsBean bean = this; - jsonWriter.name("logLevel").value(bean.getLogLevel().ordinal()); + // 序列化日志级别:存储枚举的索引值(如 Off=0、Error=1...),比存储名称更高效 + jsonWriter.name("logLevel").value(this.getLogLevel().ordinal()); } + /** + * 重写父类方法:从 JSON 字段初始化当前对象(读取配置时调用) + * 解析字段:logLevel(通过索引值恢复 LogUtils.LOG_LEVEL 枚举) + * @param jsonReader JSON 读取器(用于读取 JSON 数据) + * @param name JSON 字段名(当前解析的字段) + * @return true:字段解析成功;false:字段不匹配(需父类处理或跳过) + * @throws IOException JSON 读取异常(如字段类型不匹配、流中断) + */ @Override public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { - if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { - if (name.equals("logLevel")) { - setLogLevel(LogUtils.LOG_LEVEL.values()[jsonReader.nextInt()]); - } else { - return false; - } + // 先让父类处理公共字段,处理成功则直接返回 + if (super.initObjectsFromJsonReader(jsonReader, name)) { + return true; } + // 解析当前类专属字段 + if ("logLevel".equals(name)) { + // 通过枚举索引值恢复枚举实例(确保与序列化时的 ordinal 对应) + int levelOrdinal = jsonReader.nextInt(); + this.setLogLevel(LogUtils.LOG_LEVEL.values()[levelOrdinal]); + } else { + // 字段不匹配,返回 false 表示需要跳过该字段 + return false; + } + // 字段解析成功 return true; } + /** + * 重写父类方法:从 JSON 读取器完整解析配置对象(入口方法) + * 负责 JSON 对象的开始/结束解析,遍历所有字段并调用 initObjectsFromJsonReader 处理 + * @param jsonReader JSON 读取器(传入待解析的 JSON 流) + * @return 解析后的当前 LogUtilsBean 实例(支持链式调用) + * @throws IOException JSON 解析异常(如格式错误、字段缺失) + */ @Override public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + // 开始解析 JSON 对象(必须与 writeThisToJsonWriter 中的结构对应) jsonReader.beginObject(); + // 遍历 JSON 中的所有字段 while (jsonReader.hasNext()) { - String name = jsonReader.nextName(); - if (!initObjectsFromJsonReader(jsonReader, name)) { + String fieldName = jsonReader.nextName(); + // 解析字段,若字段不匹配则跳过该值(避免解析失败) + if (!this.initObjectsFromJsonReader(jsonReader, fieldName)) { jsonReader.skipValue(); } } - // 结束 JSON 对象 + // 结束 JSON 对象解析(必须调用,否则会导致流异常) jsonReader.endObject(); + // 返回当前实例,支持链式调用(如 new LogUtilsBean().readBeanFromJsonReader(reader)) return this; } } + diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsClassTAGBean.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsClassTAGBean.java index af9c57d..83ce79e 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsClassTAGBean.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtilsClassTAGBean.java @@ -1,87 +1,161 @@ package cc.winboll.studio.libappbase; -/** - * @Author ZhanGSKen - * @Date 2025/01/04 14:17:02 - * @Describe 日志类class TAG 标签数据类 - */ import android.util.JsonReader; import android.util.JsonWriter; import java.io.IOException; +/** + * @Author ZhanGSKen + * @Date 2025/01/04 14:17:02 + * @Describe 日志 TAG 过滤配置模型(继承 BaseBean,实现 JSON 序列化/反序列化) + * 封装单个日志 TAG 的名称及其启用状态,用于 LogUtils 的 TAG 过滤规则持久化存储与读取 + */ public class LogUtilsClassTAGBean extends BaseBean { + /** 当前类的日志 TAG(用于调试输出) */ public static final String TAG = "LogUtilsClassTAGBean"; - // 标签名 - String tag; - // 是否启用 - Boolean enable; + /** + * 日志 TAG 名称(如 "LogViewThread"、"ToastUtils") + * 与 LogUtils 中扫描的应用内 TAG 一一对应 + */ + private String tag; + /** + * TAG 启用状态(控制该 TAG 的日志是否输出) + * true:启用(输出该 TAG 的日志);false:禁用(不输出该 TAG 的日志) + */ + private Boolean enable; + + /** + * 无参构造方法(默认初始化:TAG 为当前类 TAG,启用状态为 true) + * 用于 JSON 反序列化时的实例创建,或默认配置生成 + */ public LogUtilsClassTAGBean() { - this.tag = TAG; - this.enable = true; + this.tag = TAG; // 默认 TAG 为当前类的 TAG + this.enable = true; // 默认启用该 TAG 的日志输出 } + /** + * 有参构造方法(指定 TAG 名称和启用状态) + * 用于主动创建 TAG 过滤配置实例 + * @param tag 日志 TAG 名称 + * @param enable TAG 启用状态(true/false) + */ public LogUtilsClassTAGBean(String tag, Boolean enable) { this.tag = tag; this.enable = enable; } + /** + * 设置日志 TAG 名称 + * @param tag 目标 TAG 名称 + */ public void setTag(String tag) { this.tag = tag; } + /** + * 获取日志 TAG 名称 + * @return 当前配置的 TAG 名称 + */ public String getTag() { return tag; } + /** + * 设置 TAG 启用状态 + * @param enable 目标启用状态(true:启用;false:禁用) + */ public void setEnable(Boolean enable) { this.enable = enable; } + /** + * 获取 TAG 启用状态 + * @return 当前 TAG 的启用状态 + */ public Boolean getEnable() { return enable; } + /** + * 重写父类方法:获取当前类的全限定名(用于 BaseBean 反射识别) + * @return 类全限定名(如 "cc.winboll.studio.libappbase.LogUtilsClassTAGBean") + */ @Override public String getName() { return LogUtilsClassTAGBean.class.getName(); } + /** + * 重写父类方法:将当前 TAG 配置对象序列化为 JSON(持久化存储时调用) + * 序列化字段:tag(TAG 名称)、enable(启用状态) + * @param jsonWriter JSON 写入器(用于输出 JSON 数据) + * @throws IOException JSON 写入异常(如流关闭、格式错误) + */ @Override public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + // 调用父类序列化逻辑(若 BaseBean 有公共字段,需优先处理) super.writeThisToJsonWriter(jsonWriter); - LogUtilsClassTAGBean bean = this; - jsonWriter.name("tag").value(bean.getTag()); - jsonWriter.name("enable").value(bean.getEnable()); + // 序列化 TAG 名称 + jsonWriter.name("tag").value(this.getTag()); + // 序列化启用状态 + jsonWriter.name("enable").value(this.getEnable()); } + /** + * 重写父类方法:从 JSON 字段初始化当前对象(读取配置时调用) + * 解析字段:tag(TAG 名称)、enable(启用状态) + * @param jsonReader JSON 读取器(用于读取 JSON 数据) + * @param name JSON 字段名(当前解析的字段) + * @return true:字段解析成功;false:字段不匹配(需父类处理或跳过) + * @throws IOException JSON 读取异常(如字段类型不匹配、流中断) + */ @Override public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { - if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { - if (name.equals("tag")) { - setTag(jsonReader.nextString()); - } else if (name.equals("enable")) { - setEnable(jsonReader.nextBoolean()); - } else { - return false; - } + // 先让父类处理公共字段,处理成功则直接返回 + if (super.initObjectsFromJsonReader(jsonReader, name)) { + return true; } + // 解析当前类专属字段 + if ("tag".equals(name)) { + // 读取 TAG 名称并设置 + this.setTag(jsonReader.nextString()); + } else if ("enable".equals(name)) { + // 读取启用状态并设置 + this.setEnable(jsonReader.nextBoolean()); + } else { + // 字段不匹配,返回 false 表示需要跳过该字段 + return false; + } + // 字段解析成功 return true; } + /** + * 重写父类方法:从 JSON 读取器完整解析配置对象(入口方法) + * 负责 JSON 对象的开始/结束解析,遍历所有字段并调用 initObjectsFromJsonReader 处理 + * @param jsonReader JSON 读取器(传入待解析的 JSON 流) + * @return 解析后的当前 LogUtilsClassTAGBean 实例(支持链式调用) + * @throws IOException JSON 解析异常(如格式错误、字段缺失) + */ @Override public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + // 开始解析 JSON 对象(必须与 writeThisToJsonWriter 中的结构对应) jsonReader.beginObject(); + // 遍历 JSON 中的所有字段 while (jsonReader.hasNext()) { - String name = jsonReader.nextName(); - if (!initObjectsFromJsonReader(jsonReader, name)) { + String fieldName = jsonReader.nextName(); + // 解析字段,若字段不匹配则跳过该值(避免解析失败) + if (!this.initObjectsFromJsonReader(jsonReader, fieldName)) { jsonReader.skipValue(); } } - // 结束 JSON 对象 + // 结束 JSON 对象解析(必须调用,否则会导致流异常) jsonReader.endObject(); + // 返回当前实例,支持链式调用(如 new LogUtilsClassTAGBean().readBeanFromJsonReader(reader)) return this; } } + diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java index db7675e..af50d0f 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java @@ -1,10 +1,5 @@ package cc.winboll.studio.libappbase; -/** - * @Author ZhanGSKen - * @Date 2024/08/12 14:36:18 - * @Describe 日志视图类,继承 RelativeLayout 类。 - */ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; @@ -26,8 +21,6 @@ import android.widget.RelativeLayout; import android.widget.ScrollView; import android.widget.Spinner; import android.widget.TextView; -import cc.winboll.studio.libappbase.LogUtils; -import cc.winboll.studio.libappbase.R; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; @@ -35,27 +28,49 @@ import java.util.Comparator; import java.util.List; import java.util.Map; +/** + * @Author ZhanGSKen + * @Date 2024/08/12 14:36:18 + * @Describe 日志可视化自定义 View(继承 RelativeLayout) + * 核心功能:日志展示、日志级别筛选、TAG 过滤(启用/禁用)、TAG 搜索定位、日志清理/复制、视图交互控制 + * 依赖 LogUtils 进行日志读写,通过 LogViewThread 监听日志文件变化并自动刷新 + */ public class LogView extends RelativeLayout { + /** 当前 View 的日志 TAG(用于调试输出) */ public static final String TAG = "LogView"; - public volatile boolean mIsHandling; - public volatile boolean mIsAddNewLog; + /** 日志处理中标志(避免并发刷新,volatile 保证多线程可见性) */ + private volatile boolean mIsHandling; + /** 新日志添加标志(标记有未处理的新日志,volatile 保证多线程可见性) */ + private volatile boolean mIsAddNewLog; - Context mContext; - ScrollView mScrollView; - TextView mTextView; - EditText metTagSearch; - CheckBox mSelectableCheckBox; - CheckBox mSelectAllTAGCheckBox; - TAGListAdapter mTAGListAdapter; - LogViewThread mLogViewThread; - LogViewHandler mLogViewHandler; - Spinner mLogLevelSpinner; - ArrayAdapter mLogLevelSpinnerAdapter; - // 标签列表 - HorizontalListView mListViewTags; + /** 上下文对象(用于布局加载、系统服务获取) */ + private Context mContext; + /** 日志滚动视图(包裹日志文本,支持垂直滚动) */ + private ScrollView mLogScrollView; + /** 日志文本展示控件(显示所有日志内容) */ + private TextView mLogTextView; + /** TAG 搜索输入框(用于搜索并定位目标 TAG) */ + private EditText mTagSearchEt; + /** 文本选择开关(控制是否允许选中日志文本) */ + private CheckBox mTextSelectableCb; + /** 全选 TAG 开关(控制所有 TAG 的启用/禁用) */ + private CheckBox mSelectAllTagCb; + /** TAG 列表适配器(绑定 TAG 数据与视图,处理勾选状态) */ + private TAGListAdapter mTagListAdapter; + /** 日志监听线程(监听日志文件变化,触发视图刷新) */ + private LogViewThread mLogViewThread; + /** 日志视图 Handler(主线程更新 UI,避免跨线程操作) */ + private LogViewHandler mLogViewHandler; + /** 日志级别选择下拉框(用于切换全局日志输出级别) */ + private Spinner mLogLevelSpinner; + /** 日志级别适配器(绑定 LogUtils.LOG_LEVEL 枚举与 Spinner) */ + private ArrayAdapter mLogLevelAdapter; + /** TAG 水平列表视图(横向展示所有 TAG,支持滚动) */ + private HorizontalListView mTagHorizontalListView; + // ====================== 构造方法(初始化视图) ====================== public LogView(Context context) { super(context); initView(context); @@ -76,258 +91,307 @@ public class LogView extends RelativeLayout { initView(context); } + /** + * 启动日志监听与展示 + * 1. 初始化并启动 LogViewThread(监听日志文件变化); + * 2. 初始加载并展示日志内容。 + */ public void start() { - mLogViewThread = new LogViewThread(LogView.this); + mLogViewThread = new LogViewThread(this); mLogViewThread.start(); - // 显示日志 - showAndScrollLogView(); + showAndScrollLogView(); // 初始显示日志并滚动到底部 } - public void scrollLogUp() { - mScrollView.post(new Runnable() { - @Override - public void run() { - mScrollView.fullScroll(ScrollView.FOCUS_DOWN); - // 日志显示结束 - mLogViewHandler.setIsHandling(false); - // 检查是否添加了新日志 - if (mLogViewHandler.isAddNewLog()) { - // 有新日志添加,先更改新日志标志 - mLogViewHandler.setIsAddNewLog(false); - // 再次发送显示日志的显示 - Message message = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOGVIEW_UPDATE); - mLogViewHandler.sendMessage(message); - } - } - }); + /** + * 滚动日志到底部(确保最新日志可见) + * 运行在主线程,通过 post 提交 Runnable 避免 UI 线程阻塞 + */ + private void scrollLogToBottom() { + mLogScrollView.post(new Runnable() { + @Override + public void run() { + // 滚动到 ScrollView 底部(FOCUS_DOWN 表示聚焦到底部) + mLogScrollView.fullScroll(ScrollView.FOCUS_DOWN); + // 标记日志处理完成 + mLogViewHandler.setIsHandling(false); + // 检查是否有未处理的新日志,有则再次触发刷新 + if (mLogViewHandler.isAddNewLog()) { + mLogViewHandler.setIsAddNewLog(false); + Message refreshMsg = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOG_REFRESH); + mLogViewHandler.sendMessage(refreshMsg); + } + } + }); } - void initView(Context context) { + /** + * 初始化视图组件(加载布局、绑定控件、设置监听) + * @param context 上下文对象 + */ + private void initView(Context context) { mContext = context; - mLogViewHandler = new LogViewHandler(); - // 加载视图布局 - addView(inflate(mContext, cc.winboll.studio.libappbase.R.layout.view_log, null)); - // 初始化日志子控件视图 - // - mScrollView = findViewById(cc.winboll.studio.libappbase.R.id.viewlogScrollViewLog); - mTextView = findViewById(cc.winboll.studio.libappbase.R.id.viewlogTextViewLog); - metTagSearch = findViewById(cc.winboll.studio.libappbase.R.id.tagsearch_et); - // 获取Log Level spinner实例 - mLogLevelSpinner = findViewById(cc.winboll.studio.libappbase.R.id.viewlogSpinner1); + mLogViewHandler = new LogViewHandler(); // 初始化主线程 Handler - metTagSearch.addTextChangedListener(new TextWatcher() { + // 加载日志视图布局(R.layout.view_log 为自定义布局文件) + View rootView = LayoutInflater.from(context).inflate(R.layout.view_log, this, true); + // 绑定布局控件(通过 ID 找到对应组件) + bindViews(rootView); - @Override - public void afterTextChanged(Editable editable) { - } - - @Override - public void beforeTextChanged(CharSequence charSequence, int p, int p1, int p2) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - LogUtils.d(TAG, s.toString()); - if (s.length() > 0) { - scrollToTag(s.toString()); - } else { - HorizontalScrollView hsRoot = findViewById(R.id.viewlogHorizontalScrollView1); - hsRoot.smoothScrollTo(0, 0); - mListViewTags.resetScrollToStart(); - } -// mListViewTags.postDelayed(new Runnable() { -// @Override -// public void run() { -// mListViewTags.scrollToItem(5); -// } -// }, 100); - } - // 其他方法留空或按需实现 - }); - - - (findViewById(cc.winboll.studio.libappbase.R.id.viewlogButtonClean)).setOnClickListener(new View.OnClickListener(){ - - @Override - public void onClick(View v) { - LogUtils.cleanLog(); - LogUtils.d(TAG, "Log is cleaned."); - } - }); - (findViewById(cc.winboll.studio.libappbase.R.id.viewlogButtonCopy)).setOnClickListener(new View.OnClickListener(){ - - @Override - public void onClick(View v) { - - ClipboardManager cm = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); - cm.setPrimaryClip(ClipData.newPlainText(mContext.getPackageName(), LogUtils.loadLog())); - LogUtils.d(TAG, "Log is copied."); - } - }); - mSelectableCheckBox = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBoxSelectable); - mSelectableCheckBox.setOnClickListener(new View.OnClickListener(){ - @Override - public void onClick(View v) { - if (mSelectableCheckBox.isChecked()) { - setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); - } else { - setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); - } - } - }); - - // 设置日志级别列表 - ArrayList adapterItems = new ArrayList<>(); - for (LogUtils.LOG_LEVEL e : LogUtils.LOG_LEVEL.values()) { - adapterItems.add(e.name()); - } - // 假设你有一个字符串数组作为选项列表 - //String[] options = {"Option 1", "Option 2", "Option 3"}; - // 创建一个ArrayAdapter来绑定数据到spinner - mLogLevelSpinnerAdapter = ArrayAdapter.createFromResource( - context, cc.winboll.studio.libappbase.R.array.enum_loglevel_array, android.R.layout.simple_spinner_item); - mLogLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - - // 设置适配器并将它应用到spinner上 - mLogLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // 设置下拉视图样式 - mLogLevelSpinner.setAdapter(mLogLevelSpinnerAdapter); - // 为Spinner添加监听器 - mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - //String selectedOption = mLogLevelSpinnerAdapter.getItem(position); - // 处理选中的选项... - LogUtils.setLogLevel(LogUtils.LOG_LEVEL.values()[position]); - } - @Override - public void onNothingSelected(AdapterView parent) { - // 如果没有选择,则执行此操作... - } - }); - // 获取默认值的索引 - int defaultValueIndex = LogUtils.getLogLevel().ordinal(); - - if (defaultValueIndex != -1) { - // 如果找到了默认值,设置默认选项 - mLogLevelSpinner.setSelection(defaultValueIndex); - } - - // 加载标签列表 - Map mapTAGList = LogUtils.getTagEnableMap(); - boolean isAllSelect = true; - for (Map.Entry entry : mapTAGList.entrySet()) { - if (entry.getValue() == false) { - isAllSelect = false; - break; - } - } - CheckBox cbALLTAG = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBox1); - cbALLTAG.setChecked(isAllSelect); - - // 加载标签表 - mListViewTags = findViewById(cc.winboll.studio.libappbase.R.id.tags_listview); - mListViewTags.setVerticalOffset(10); - mTAGListAdapter = new TAGListAdapter(mContext, mapTAGList); - mListViewTags.setAdapter(mTAGListAdapter); - - // 可以添加点击监听器来处理勾选框状态变化后的逻辑,比如获取当前勾选情况等 - mTAGListAdapter.notifyDataSetChanged(); - - mSelectAllTAGCheckBox = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBox1); - mSelectAllTAGCheckBox.setOnClickListener(new View.OnClickListener(){ - @Override - public void onClick(View v) { - LogUtils.setAllTagsEnable(mSelectAllTAGCheckBox.isChecked()); - //LogUtils.setALlTAGListEnable(false); - //mTAGListAdapter.notifyDataSetChanged(); - mTAGListAdapter.reload(); - //ToastUtils.show(String.format("onClick\nmSelectAllTAGCheckBox.isChecked() : %s", mSelectAllTAGCheckBox.isChecked())); - } - }); - - - // 设置滚动时不聚焦日志 + // 设置 TAG 搜索输入框监听(实时搜索并定位 TAG) + setupTagSearchListener(); + // 设置功能按钮监听(清理日志、复制日志) + setupFunctionButtonListeners(rootView); + // 设置文本选择开关监听(控制日志文本是否可选中) + setupTextSelectableListener(); + // 初始化日志级别下拉框(绑定级别数据,设置默认值) + initLogLevelSpinner(); + // 初始化 TAG 列表(加载所有 TAG,设置全选状态) + initTagListView(); + // 设置默认交互模式(默认禁止子视图获取焦点,避免误触) setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); } + /** + * 绑定布局控件(通过 ID 查找并初始化所有子组件) + * @param rootView 根布局视图 + */ + private void bindViews(View rootView) { + mLogScrollView = rootView.findViewById(R.id.viewlogScrollViewLog); + mLogTextView = rootView.findViewById(R.id.viewlogTextViewLog); + mTagSearchEt = rootView.findViewById(R.id.tagsearch_et); + mLogLevelSpinner = rootView.findViewById(R.id.viewlogSpinner1); + mTextSelectableCb = rootView.findViewById(R.id.viewlogCheckBoxSelectable); + mSelectAllTagCb = rootView.findViewById(R.id.viewlogCheckBox1); + mTagHorizontalListView = rootView.findViewById(R.id.tags_listview); + } + + /** + * 设置 TAG 搜索输入框监听(文本变化时触发 TAG 定位) + */ + private void setupTagSearchListener() { + mTagSearchEt.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + String searchText = s.toString().trim(); + LogUtils.d(TAG, "TAG 搜索内容:" + searchText); + if (!searchText.isEmpty()) { + // 搜索文本非空,定位匹配的 TAG + scrollToTargetTag(searchText); + } else { + // 搜索文本为空,重置滚动位置 + HorizontalScrollView parentHs = findViewById(R.id.viewlogHorizontalScrollView1); + parentHs.smoothScrollTo(0, 0); + mTagHorizontalListView.resetScrollToStart(); + } + } + + @Override + public void afterTextChanged(Editable s) {} + }); + } + + /** + * 设置功能按钮监听(清理日志、复制日志) + */ + private void setupFunctionButtonListeners(View rootView) { + // 清理日志按钮(点击清空所有历史日志) + rootView.findViewById(R.id.viewlogButtonClean).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.cleanLog(); + LogUtils.d(TAG, "日志已清理"); + } + }); + + // 复制日志按钮(点击复制所有日志到剪贴板) + rootView.findViewById(R.id.viewlogButtonCopy).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); + // 将日志内容复制到剪贴板(标签为应用包名) + clipboard.setPrimaryClip(ClipData.newPlainText(mContext.getPackageName(), LogUtils.loadLog())); + LogUtils.d(TAG, "日志已复制到剪贴板"); + } + }); + } + + /** + * 设置文本选择开关监听(控制日志文本是否可选中复制) + */ + private void setupTextSelectableListener() { + mTextSelectableCb.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mTextSelectableCb.isChecked()) { + // 允许文本选择:子视图优先获取焦点 + setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); + } else { + // 禁止文本选择:阻止子视图获取焦点 + setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); + } + } + }); + } + + /** + * 初始化日志级别下拉框(Spinner) + * 1. 绑定 LogUtils.LOG_LEVEL 枚举数据; + * 2. 设置默认选中当前全局日志级别; + * 3. 监听级别变化,更新 LogUtils 全局配置。 + */ + private void initLogLevelSpinner() { + // 从资源文件加载日志级别数组(R.array.enum_loglevel_array 与 LOG_LEVEL 枚举对应) + mLogLevelAdapter = ArrayAdapter.createFromResource( + mContext, R.array.enum_loglevel_array, android.R.layout.simple_spinner_item); + // 设置下拉列表样式 + mLogLevelAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mLogLevelSpinner.setAdapter(mLogLevelAdapter); + + // 监听下拉框选择变化 + mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + // 根据选择的位置设置全局日志级别(position 与 LOG_LEVEL 枚举索引对应) + LogUtils.setLogLevel(LogUtils.LOG_LEVEL.values()[position]); + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); + + // 设置默认选中当前日志级别 + int defaultLevelIndex = LogUtils.getLogLevel().ordinal(); + if (defaultLevelIndex >= 0) { + mLogLevelSpinner.setSelection(defaultLevelIndex); + } + } + + /** + * 初始化 TAG 水平列表 + * 1. 加载 LogUtils 中的所有 TAG 及其启用状态; + * 2. 初始化 TAG 列表适配器; + * 3. 设置全选 TAG 开关监听。 + */ + private void initTagListView() { + // 获取 LogUtils 中的 TAG 启用状态映射表 + Map tagEnableMap = LogUtils.getTagEnableMap(); + // 判断是否所有 TAG 都已启用(初始化全选开关状态) + boolean isAllTagEnabled = isAllTagsEnabled(tagEnableMap); + mSelectAllTagCb.setChecked(isAllTagEnabled); + + // 初始化 TAG 水平列表(设置垂直偏移,绑定适配器) + mTagHorizontalListView.setVerticalOffset(10); + mTagListAdapter = new TAGListAdapter(mContext, tagEnableMap); + mTagHorizontalListView.setAdapter(mTagListAdapter); + mTagListAdapter.notifyDataSetChanged(); // 刷新列表数据 + + // 全选 TAG 开关监听(点击时启用/禁用所有 TAG) + mSelectAllTagCb.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + boolean isSelectAll = mSelectAllTagCb.isChecked(); + LogUtils.setAllTagsEnable(isSelectAll); // 批量更新所有 TAG 状态 + mTagListAdapter.reload(); // 重新加载 TAG 数据并刷新视图 + } + }); + } + + /** + * 判断是否所有 TAG 都已启用 + * @param tagEnableMap TAG 启用状态映射表 + * @return true:所有 TAG 均启用;false:存在未启用的 TAG + */ + private boolean isAllTagsEnabled(Map tagEnableMap) { + for (Map.Entry entry : tagEnableMap.entrySet()) { + if (!entry.getValue()) { + return false; + } + } + return true; + } + + /** + * 更新日志视图(由 LogViewThread 触发,通知有新日志) + * 避免并发刷新:正在处理时标记新日志,处理完成后再次刷新 + */ public void updateLogView() { - if (mLogViewHandler.isHandling() == true) { - // 正在处理日志显示, - // 就先设置一个新日志标志位 - // 以便日志显示完后,再次显示新日志内容 + if (mLogViewHandler.isHandling()) { + // 正在处理日志刷新,标记有新日志待处理 mLogViewHandler.setIsAddNewLog(true); } else { - //LogUtils.d(TAG, "LogListener showLog(String path)"); - Message message = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOGVIEW_UPDATE); - mLogViewHandler.sendMessage(message); + // 发送刷新消息到主线程 + Message refreshMsg = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOG_REFRESH); + mLogViewHandler.sendMessage(refreshMsg); mLogViewHandler.setIsAddNewLog(false); } } - void showAndScrollLogView() { - mTextView.setText(LogUtils.loadLog()); - scrollLogUp(); + /** + * 显示日志并滚动到底部 + * 1. 从 LogUtils 加载所有历史日志; + * 2. 设置到文本控件并滚动到底部。 + */ + private void showAndScrollLogView() { + mLogTextView.setText(LogUtils.loadLog()); // 加载并显示日志 + scrollLogToBottom(); // 滚动到底部,显示最新日志 } - public void scrollToTag(final String prefix) { - if (mTAGListAdapter == null || prefix == null || prefix.length() == 0) { - LogUtils.d(TAG, "参数为空,无法滚动"); + /** + * 滚动到目标 TAG(根据搜索文本定位匹配的 TAG 并滚动显示) + * @param prefix 搜索文本(TAG 前缀) + */ + private void scrollToTargetTag(final String prefix) { + if (mTagListAdapter == null || prefix == null || prefix.isEmpty()) { + LogUtils.d(TAG, "TAG 搜索参数为空,无法定位"); return; } - final List itemList = mTAGListAdapter.getItemList(); + final List tagItemList = mTagListAdapter.getItemList(); + mTagHorizontalListView.post(new Runnable() { + @Override + public void run() { + int targetPosition = -1; + // 遍历 TAG 列表,查找前缀匹配的 TAG(忽略大小写) + for (int i = 0; i < tagItemList.size(); i++) { + String tag = tagItemList.get(i).getTag(); + if (tag != null && tag.toLowerCase().startsWith(prefix.toLowerCase())) { + targetPosition = i; + break; + } + } - mListViewTags.post(new Runnable() { - @Override - public void run() { - // 查找匹配的标签位置 - int targetPosition = -1; - - for (int i = 0; i < itemList.size(); i++) { - String tag = itemList.get(i).getTag(); - if (tag != null && tag.toLowerCase().startsWith(prefix.toLowerCase())) { - targetPosition = i; - - break; - } - } - - if (targetPosition != -1) { - // 优化滚动逻辑 - //mListViewTags.setSelection(targetPosition); - //mListViewTags.invalidateViews(); // 强制刷新所有可见项 - - // 单独刷新目标视图 -// View targetView = mListViewTags.getChildAt(targetPosition); -// if (targetView != null) { -// targetView.requestLayout(); -// targetView.requestFocus(); -// } - - final int scrollPosition = targetPosition; - - // 延迟滚动确保布局完成 - mListViewTags.postDelayed(new Runnable() { - @Override - public void run() { - LogUtils.d(TAG, String.format("scrollPosition %d", scrollPosition)); - mListViewTags.scrollToItem(scrollPosition); - } - }, 100); - } else { - LogUtils.d(TAG, "未找到匹配的标签前缀:" + prefix); - } - } - }); + if (targetPosition != -1) { + final int targetPositionFinal = targetPosition; + // 延迟滚动(确保布局完成,避免滚动失效) + mTagHorizontalListView.postDelayed(new Runnable() { + @Override + public void run() { + LogUtils.d(TAG, "定位到 TAG 位置:" + targetPositionFinal); + mTagHorizontalListView.scrollToItem(targetPositionFinal); + } + }, 100); + } else { + LogUtils.d(TAG, "未找到匹配前缀的 TAG:" + prefix); + } + } + }); } - - - class LogViewHandler extends Handler { - - final static int MSG_LOGVIEW_UPDATE = 0; - volatile boolean isHandling; - volatile boolean isAddNewLog; + // ====================== 内部类:日志视图 Handler(主线程更新 UI) ====================== + /** + * 日志视图 Handler(运行在主线程,处理日志刷新消息) + * 避免跨线程操作 UI,通过标志位控制并发刷新 + */ + private class LogViewHandler extends Handler { + /** 日志刷新消息标识 */ + private static final int MSG_LOG_REFRESH = 0; + /** 日志处理中标志(与外部 mIsHandling 同步) */ + private volatile boolean isHandling; + /** 新日志添加标志(与外部 mIsAddNewLog 同步) */ + private volatile boolean isAddNewLog; public LogViewHandler() { setIsHandling(false); @@ -350,24 +414,32 @@ public class LogView extends RelativeLayout { return isAddNewLog; } + @Override public void handleMessage(Message msg) { + super.handleMessage(msg); switch (msg.what) { - case MSG_LOGVIEW_UPDATE:{ - if (isHandling() == false) { - setIsHandling(true); - showAndScrollLogView(); - } - break; + case MSG_LOG_REFRESH: + // 未处理日志刷新时,标记为处理中并触发显示 + if (!isHandling()) { + setIsHandling(true); + showAndScrollLogView(); } + break; default: break; } - super.handleMessage(msg); } } - public class TAGItemModel { + // ====================== 内部类:TAG 数据模型(封装 TAG 名称与状态) ====================== + /** + * TAG 列表项数据模型 + * 封装单个 TAG 的名称及其启用状态(用于 Adapter 数据绑定) + */ + private class TAGItemModel { + /** TAG 名称(如 "LogViewThread"、"LogUtils") */ private String tag; + /** TAG 启用状态(true:启用;false:禁用) */ private boolean isChecked; public TAGItemModel(String tag, boolean isChecked) { @@ -391,18 +463,17 @@ public class LogView extends RelativeLayout { isChecked = checked; } - // getter/setter... - + /** + * 重写 equals 方法(按 TAG 名称判断相等) + * @param o 比较对象 + * @return true:TAG 名称相同;false:不同 + */ @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; TAGItemModel that = (TAGItemModel) o; - // 手动处理空值比较(Java 6 不支持 Objects.equals) + // 手动处理空值比较(兼容 Java 7,不依赖 Objects.equals) if (tag == null) { return that.tag == null; } else { @@ -410,106 +481,174 @@ public class LogView extends RelativeLayout { } } + /** + * 重写 hashCode 方法(基于 TAG 名称生成哈希值) + * @return 哈希值(空 TAG 返回 0) + */ @Override public int hashCode() { - return tag == null ? 0 : tag.hashCode(); // 手动处理空值 + return tag == null ? 0 : tag.hashCode(); } } - - public class TAGListAdapter extends BaseAdapter { - + // ====================== 内部类:TAG 列表适配器(绑定数据与视图) ====================== + /** + * TAG 水平列表适配器(继承 BaseAdapter) + * 负责 TAG 数据与列表项视图的绑定,处理勾选状态变化 + */ + private class TAGListAdapter extends BaseAdapter { + /** 上下文对象(用于加载列表项布局) */ private Context context; - private Map mapOrigin; - private List itemList; + /** 原始 TAG 启用状态映射表(来自 LogUtils) */ + private Map originTagMap; + /** TAG 列表项数据(转换为 TAGItemModel 列表,便于排序和绑定) */ + private List tagItemList; - public TAGListAdapter(Context context, Map map) { + /** + * 构造方法(初始化数据并加载到列表) + * @param context 上下文 + * @param tagMap TAG 启用状态映射表 + */ + public TAGListAdapter(Context context, Map tagMap) { this.context = context; - mapOrigin = map; - loadMap(mapOrigin); + this.originTagMap = tagMap; + loadTagData(originTagMap); // 加载并转换数据 } + /** + * 获取 TAG 列表项数据(供外部定位 TAG 使用) + * @return TAGItemModel 列表 + */ public List getItemList() { - return itemList; + return tagItemList; } + // ====================== BaseAdapter 抽象方法实现 ====================== @Override public int getCount() { - return itemList.size(); + return tagItemList == null ? 0 : tagItemList.size(); } @Override - public Object getItem(int p) { - return itemList.get(p); + public Object getItem(int position) { + return tagItemList.get(position); } @Override - public long getItemId(int p) { - return p; + public long getItemId(int position) { + return position; } - void loadMap(Map map) { - itemList = new ArrayList(); - for (Map.Entry entry : map.entrySet()) { - itemList.add(new TAGItemModel(entry.getKey(), entry.getValue())); + /** + * 加载 TAG 数据(将 Map 转换为 List 并排序) + * @param tagMap TAG 启用状态映射表 + */ + private void loadTagData(Map tagMap) { + tagItemList = new ArrayList<>(); + // 遍历 Map,转换为 TAGItemModel 并添加到列表 + for (Map.Entry entry : tagMap.entrySet()) { + tagItemList.add(new TAGItemModel(entry.getKey(), entry.getValue())); } - // 添加排序功能,按照tag进行升序排序 - Collections.sort(itemList, new SortMapEntryByKeyString(true)); - //Collections.sort(itemList, new SortMapEntryByKeyString(false)); + // 按 TAG 名称升序排序(中文排序兼容) + Collections.sort(tagItemList, new TagAscComparator(true)); } + /** + * 重新加载 TAG 数据(用于全选/反选后刷新列表) + */ public void reload() { - loadMap(mapOrigin); - super.notifyDataSetChanged(); + loadTagData(originTagMap); // 重新加载数据 + notifyDataSetChanged(); // 通知视图刷新 } + /** + * 创建/复用列表项视图(优化性能,避免重复 inflate) + * @param position 列表项位置 + * @param convertView 复用视图(可为 null) + * @param parent 父容器 + * @return 列表项视图 + */ @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; + // 复用视图(减少布局加载开销) if (convertView == null) { + // 加载列表项布局(R.layout.item_logtag 为 TAG 项自定义布局) convertView = LayoutInflater.from(context).inflate(R.layout.item_logtag, parent, false); holder = new ViewHolder(); - holder.tvText = convertView.findViewById(R.id.viewlogtagTextView1); - holder.cbChecked = convertView.findViewById(R.id.viewlogtagCheckBox1); - convertView.setTag(holder); + // 绑定列表项控件(TAG 文本和勾选框) + holder.tagTv = convertView.findViewById(R.id.viewlogtagTextView1); + holder.tagCb = convertView.findViewById(R.id.viewlogtagCheckBox1); + convertView.setTag(holder); // 保存 ViewHolder 到视图 } else { - holder = (ViewHolder) convertView.getTag(); + holder = (ViewHolder) convertView.getTag(); // 复用 ViewHolder } - final TAGItemModel item = itemList.get(position); - holder.tvText.setText(item.getTag()); - holder.cbChecked.setChecked(item.isChecked()); - holder.cbChecked.setOnClickListener(new View.OnClickListener(){ + // 绑定数据到视图 + final TAGItemModel item = tagItemList.get(position); + holder.tagTv.setText(item.getTag()); // 设置 TAG 名称 + holder.tagCb.setChecked(item.isChecked()); // 设置勾选状态 - @Override - public void onClick(View v) { - LogUtils.setTagEnable(item.getTag(), ((CheckBox)v).isChecked()); - } - }); + // 勾选框点击监听(更新 TAG 启用状态) + holder.tagCb.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + boolean isChecked = ((CheckBox) v).isChecked(); + // 调用 LogUtils 更新该 TAG 的启用状态 + LogUtils.setTagEnable(item.getTag(), isChecked); + // 同步更新本地模型状态(避免刷新后状态不一致) + item.setChecked(isChecked); + } + }); return convertView; } - public class ViewHolder { - TextView tvText; - CheckBox cbChecked; + /** + * 列表项 ViewHolder(缓存控件,提升列表滑动性能) + */ + private class ViewHolder { + TextView tagTv; // TAG 名称文本控件 + CheckBox tagCb; // TAG 启用状态勾选框 } } - class SortMapEntryByKeyString implements Comparator { - private boolean mIsDesc = true; - // isDesc 是否降序排列 - public SortMapEntryByKeyString(boolean isDesc) { - mIsDesc = isDesc; + // ====================== 内部类:TAG 排序比较器(中文兼容) ====================== + /** + * TAG 名称排序比较器(实现 Comparator) + * 支持中文排序(基于系统默认中文 Locale),可选择升序/降序 + */ + private class TagAscComparator implements Comparator { + /** 排序方向(true:升序;false:降序) */ + private boolean isAsc; + /** 中文排序器(兼容中文汉字排序) */ + private Collator chineseCollator = Collator.getInstance(java.util.Locale.CHINA); + + public TagAscComparator(boolean isAsc) { + this.isAsc = isAsc; } - Collator cmp = Collator.getInstance(java.util.Locale.CHINA); + + /** + * 比较两个 TAGItemModel(按 TAG 名称排序) + * @param o1 第一个比较对象 + * @param o2 第二个比较对象 + * @return 比较结果(正数:o1 在 o2 后;负数:o1 在 o2 前;0:相等) + */ @Override public int compare(TAGItemModel o1, TAGItemModel o2) { - if (mIsDesc) { - return o1.getTag().compareTo(o2.getTag()); + String tag1 = o1.getTag(); + String tag2 = o2.getTag(); + // 处理空值(空 TAG 排在最前) + if (tag1 == null) return -1; + if (tag2 == null) return 1; + + // 根据排序方向返回比较结果 + if (isAsc) { + return chineseCollator.compare(tag1, tag2); // 升序 } else { - return o2.getTag().compareTo(o1.getTag()); + return chineseCollator.compare(tag2, tag1); // 降序 } } } } + diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogViewThread.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogViewThread.java index 56ad4c9..3941e45 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogViewThread.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogViewThread.java @@ -1,14 +1,14 @@ package cc.winboll.studio.libappbase; +import android.os.FileObserver; +import java.lang.ref.WeakReference; + /** * @Author ZhanGSKen * @Date 2024/08/12 14:43:50 * @Describe 日志视图线程类 * 独立线程监听日志文件目录变化(如写入、删除),触发日志视图更新,避免阻塞主线程 */ -import android.os.FileObserver; -import java.lang.ref.WeakReference; - public class LogViewThread extends Thread { /** 日志标签(用于调试输出) */ @@ -105,7 +105,6 @@ public class LogViewThread extends Thread { switch (eventType) { // 事件:文件写入完成(如日志写入结束并关闭文件) case FileObserver.CLOSE_WRITE: - LogUtils.d(TAG, "日志文件写入完成,文件名:" + (path != null ? path : "未知")); // 触发日志视图更新(需先判断 LogView 是否未被回收) updateLogView(); break; diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java index 869f267..f4586ff 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java @@ -1,17 +1,17 @@ package cc.winboll.studio.libappbase; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.widget.Toast; + /** * @Author ZhanGSKen&豆包大模型 * @Date 2025/11/11 20:51 * @Describe 吐司工具类(单例模式) * 简化 Android 吐司的创建与展示,通过独立线程 + Handler 处理消息,最终切换到主线程显示吐司,避免内存泄漏 */ -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.widget.Toast; - public class ToastUtils { /** 工具类日志 TAG(用于调试输出) */ diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/UTF8FileUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/UTF8FileUtils.java index a9d034f..c7407fb 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/UTF8FileUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/UTF8FileUtils.java @@ -1,11 +1,5 @@ package cc.winboll.studio.libappbase; -/** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/11/11 20:45 - * @Describe UTF-8 编码文件操作工具类 - * 提供字符串与文件的相互转换,强制使用 UTF-8 编码,确保跨平台字符兼容性 - */ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -14,6 +8,12 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/11 20:45 + * @Describe UTF-8 编码文件操作工具类 + * 提供字符串与文件的相互转换,强制使用 UTF-8 编码,确保跨平台字符兼容性 + */ public class UTF8FileUtils { /** 工具类日志 TAG(用于调试输出) */