源码整理

This commit is contained in:
ZhanGSKen
2025-11-13 05:07:10 +08:00
parent 8e8359aab4
commit 0f08cf59ea
19 changed files with 935 additions and 499 deletions

View File

@@ -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

View File

@@ -1,5 +1,9 @@
package cc.winboll.studio.libappbase;
import android.util.JsonReader;
import android.util.JsonWriter;
import java.io.IOException;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @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 {
/**

View File

@@ -1,13 +1,5 @@
package cc.winboll.studio.libappbase;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 20:03
* @Describe WinBoLL JSON 数据模型基类(抽象类)
* 定义 Json Bean 的核心规范:序列化/反序列化、文件持久化、列表处理等通用逻辑,
* 子类(如 APPModel需实现抽象方法实现自身字段的 JSON 读写
* @param <T> 泛型约束,限定子类必须继承自 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&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 20:03
* @Describe WinBoLL JSON 数据模型基类(抽象类)
* 定义 Json Bean 的核心规范:序列化/反序列化、文件持久化、列表处理等通用逻辑,
* 子类(如 APPModel需实现抽象方法实现自身字段的 JSON 读写
* @param <T> 泛型约束,限定子类必须继承自 BaseBean
*/
public abstract class BaseBean<T extends BaseBean> {
/** 日志标签,用于当前基类的日志输出标识 */

View File

@@ -1,12 +1,5 @@
package cc.winboll.studio.libappbase;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @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&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 20:14
* @Describe * 应用全局崩溃处理类(单例逻辑)
* 核心功能:捕获应用未捕获异常,记录崩溃日志到文件,启动崩溃报告页面,
* 并通过「崩溃保险丝」机制防止重复崩溃,保障基础功能可用
*/
public final class CrashHandler {
/** 日志标签,用于当前类的日志输出标识 */

View File

@@ -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&豆包大模型<zhangsken@qq.com>
* @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 {
/** 日志标签 */

View File

@@ -1,12 +1,5 @@
package cc.winboll.studio.libappbase;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @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&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 19:58
* @Describe 应用异常报告观察活动窗口类
* 核心功能:应用发生未捕获崩溃时,由 CrashHandler 启动此页面,展示崩溃日志详情,
* 并提供「复制日志」「重启应用」操作入口,便于开发者定位问题和用户恢复应用
*/
public final class GlobalCrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
/** 日志标签(用于调试日志输出,唯一标识当前 Activity */

View File

@@ -1,11 +1,5 @@
package cc.winboll.studio.libappbase;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @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&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 20:21
* @Describe 全局崩溃报告视图控件
* 用于展示应用崩溃信息,包含顶部工具栏和崩溃日志文本区域,支持自定义配色
*/
public class GlobalCrashReportView extends LinearLayout {
// 日志标签

View File

@@ -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&豆包大模型<zhangsken@qq.com>
* @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";

View File

@@ -1,11 +1,5 @@
package cc.winboll.studio.libappbase;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @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&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 20:29
* @Describe 应用日志展示 Activity
* 用于单独启动窗口展示应用运行日志,依赖 LogView 控件实现日志加载与显示
*/
public class LogActivity extends Activity {
/** 日志标签,用于当前 Activity 的日志输出标识 */

View File

@@ -1,14 +1,6 @@
package cc.winboll.studio.libappbase;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @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&豆包大模型<zhangsken@qq.com>
* @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 过滤映射表keyTAG 名称value是否启用该 TAG 的日志输出) */
public static Map<String, Boolean> sTagEnableMap = new HashMap<>();
public static Map<String, Boolean> sTagEnableMap = new HashMap<String, Boolean>();
/**
* 初始化日志工具默认日志级别Off不输出日志
@@ -131,11 +131,11 @@ public class LogUtils {
* 从 LogUtilsClassTAGBean 列表中读取每个 TAG 的启用状态,更新到映射表
*/
private static void loadTagEnableSettings() {
ArrayList<LogUtilsClassTAGBean> tagSettingList = new ArrayList<>();
ArrayList<LogUtilsClassTAGBean> tagSettingList = new ArrayList<LogUtilsClassTAGBean>();
// 从文件加载 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<LogUtilsClassTAGBean> tagSettingList = new ArrayList<>();
// 遍历映射表,构建配置列表
for (Map.Entry<String, Boolean> entry : sTagEnableMap.entrySet()) {
ArrayList<LogUtilsClassTAGBean> tagSettingList = new ArrayList<LogUtilsClassTAGBean>();
// 遍历映射表,构建配置列表Java 7 迭代器遍历)
Iterator<Map.Entry<String, Boolean>> iterator = sTagEnableMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Boolean> entry = iterator.next();
tagSettingList.add(new LogUtilsClassTAGBean(entry.getKey(), entry.getValue()));
}
// 保存配置列表到文件
@@ -178,10 +180,10 @@ public class LogUtils {
Enumeration<String> classNames = dexFile.entries();
int totalClassCount = 0; // 总类数(调试用)
List<String> targetClassNames = new ArrayList<>(); // 目标包名下的类名列表
List<String> targetClassNames = new ArrayList<String>(); // 目标包名下的类名列表
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<Map.Entry<String, Boolean>> iterator = sTagEnableMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Boolean> 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<Map.Entry<String, Boolean>> iterator = sTagEnableMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Boolean> 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());

View File

@@ -1,70 +1,131 @@
package cc.winboll.studio.libappbase;
import android.util.JsonReader;
import android.util.JsonWriter;
import java.io.IOException;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @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;
}
}

View File

@@ -1,87 +1,161 @@
package cc.winboll.studio.libappbase;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/01/04 14:17:02
* @Describe 日志类class TAG 标签数据类
*/
import android.util.JsonReader;
import android.util.JsonWriter;
import java.io.IOException;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @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持久化存储时调用
* 序列化字段tagTAG 名称、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 字段初始化当前对象(读取配置时调用)
* 解析字段tagTAG 名称、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;
}
}

View File

@@ -1,10 +1,5 @@
package cc.winboll.studio.libappbase;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @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<zhangsken@qq.com>
* @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<CharSequence> 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<CharSequence> 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<String> 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<String, Boolean> mapTAGList = LogUtils.getTagEnableMap();
boolean isAllSelect = true;
for (Map.Entry<String, Boolean> 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<String, Boolean> 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<String, Boolean> tagEnableMap) {
for (Map.Entry<String, Boolean> 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<TAGItemModel> itemList = mTAGListAdapter.getItemList();
final List<TAGItemModel> 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 trueTAG 名称相同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<String, Boolean> mapOrigin;
private List<TAGItemModel> itemList;
/** 原始 TAG 启用状态映射表(来自 LogUtils */
private Map<String, Boolean> originTagMap;
/** TAG 列表项数据(转换为 TAGItemModel 列表,便于排序和绑定) */
private List<TAGItemModel> tagItemList;
public TAGListAdapter(Context context, Map<String, Boolean> map) {
/**
* 构造方法(初始化数据并加载到列表)
* @param context 上下文
* @param tagMap TAG 启用状态映射表
*/
public TAGListAdapter(Context context, Map<String, Boolean> tagMap) {
this.context = context;
mapOrigin = map;
loadMap(mapOrigin);
this.originTagMap = tagMap;
loadTagData(originTagMap); // 加载并转换数据
}
/**
* 获取 TAG 列表项数据(供外部定位 TAG 使用)
* @return TAGItemModel 列表
*/
public List<TAGItemModel> 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<String, Boolean> map) {
itemList = new ArrayList<TAGItemModel>();
for (Map.Entry<String, Boolean> entry : map.entrySet()) {
itemList.add(new TAGItemModel(entry.getKey(), entry.getValue()));
/**
* 加载 TAG 数据(将 Map 转换为 List 并排序)
* @param tagMap TAG 启用状态映射表
*/
private void loadTagData(Map<String, Boolean> tagMap) {
tagItemList = new ArrayList<>();
// 遍历 Map转换为 TAGItemModel 并添加到列表
for (Map.Entry<String, Boolean> 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<TAGItemModel> {
private boolean mIsDesc = true;
// isDesc 是否降序排列
public SortMapEntryByKeyString(boolean isDesc) {
mIsDesc = isDesc;
// ====================== 内部类TAG 排序比较器(中文兼容) ======================
/**
* TAG 名称排序比较器(实现 Comparator
* 支持中文排序(基于系统默认中文 Locale可选择升序/降序
*/
private class TagAscComparator implements Comparator<TAGItemModel> {
/** 排序方向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); // 降序
}
}
}
}

View File

@@ -1,14 +1,14 @@
package cc.winboll.studio.libappbase;
import android.os.FileObserver;
import java.lang.ref.WeakReference;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @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;

View File

@@ -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&豆包大模型<zhangsken@qq.com>
* @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用于调试输出 */

View File

@@ -1,11 +1,5 @@
package cc.winboll.studio.libappbase;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @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&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 20:45
* @Describe UTF-8 编码文件操作工具类
* 提供字符串与文件的相互转换,强制使用 UTF-8 编码,确保跨平台字符兼容性
*/
public class UTF8FileUtils {
/** 工具类日志 TAG用于调试输出 */