源码整理
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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> {
|
||||
|
||||
/** 日志标签,用于当前基类的日志输出标识 */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/** 日志标签,用于当前类的日志输出标识 */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/** 日志标签 */
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
// 日志标签
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 的日志输出标识 */
|
||||
|
||||
@@ -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 过滤映射表(key:TAG 名称;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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(持久化存储时调用)
|
||||
* 序列化字段: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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 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<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); // 降序
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(用于调试输出) */
|
||||
|
||||
@@ -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(用于调试输出) */
|
||||
|
||||
Reference in New Issue
Block a user