恢复LogView版本到15.7.6版。
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Fri Nov 28 13:30:32 GMT 2025
|
||||
#Fri Nov 28 18:26:25 GMT 2025
|
||||
stageCount=2
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.1
|
||||
buildCount=9
|
||||
buildCount=24
|
||||
baseBetaVersion=15.11.2
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package cc.winboll.studio.libappbase;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2024/08/12 13:44:06
|
||||
* @Describe LogUtils
|
||||
* @Describe 应用日志类
|
||||
*/
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.libappbase.model.TagModel;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import dalvik.system.DexFile;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
@@ -16,653 +22,359 @@ import java.lang.reflect.Modifier;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
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 */
|
||||
public static final String TAG = "LogUtils";
|
||||
|
||||
/**
|
||||
* 日志级别枚举(从低到高:关闭→错误→警告→信息→调试→详细)
|
||||
* 级别越高,输出的日志越详细
|
||||
*/
|
||||
public static enum LOG_LEVEL { Off, Error, Warn, Info, Debug, Verbose }
|
||||
|
||||
/** 是否初始化完成标志(volatile 保证多线程可见性) */
|
||||
private static volatile boolean sIsInited = false;
|
||||
/** 全局上下文(用于获取存储路径、包信息) */
|
||||
private static Context sContext;
|
||||
/** 日志时间格式化工具(格式:[yyyyMMdd_HHmmss_SSS],精确到毫秒) */
|
||||
private static SimpleDateFormat sSimpleDateFormat = new SimpleDateFormat("[yyyyMMdd_HHmmss_SSS]", Locale.getDefault());
|
||||
/** 日志缓存文件夹(Debug 模式下存储在外部缓存,Release 存储在内部缓存) */
|
||||
private static File sLogCacheDir;
|
||||
/** 日志配置文件夹(存储 TAG 配置文件) */
|
||||
private static File sLogDataDir;
|
||||
/** 日志存储文件(所有日志写入此文件) */
|
||||
private static File sLogFile;
|
||||
/** 日志配置文件(存储日志级别、TAG 启用状态等配置) */
|
||||
private static File sLogConfigFile;
|
||||
/** 日志配置实体类(封装日志级别等配置) */
|
||||
private static LogUtilsBean sLogConfigBean;
|
||||
/** TAG 过滤映射表(key:TAG 名称;value:是否启用该 TAG 的日志输出) */
|
||||
public static ArrayList<TagModel> sTagEnableTagModelList = new ArrayList<TagModel>();
|
||||
static volatile boolean _IsInited = false;
|
||||
static Context _mContext;
|
||||
// 日志显示时间格式
|
||||
static SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("[yyyyMMdd_HHmmss_SSS]", Locale.getDefault());
|
||||
// 应用日志文件夹
|
||||
static File _mfLogCacheDir;
|
||||
static File _mfLogDataDir;
|
||||
// 应用日志文件
|
||||
static File _mfLogCatchFile;
|
||||
static File _mfLogUtilsBeanFile;
|
||||
static LogUtilsBean _mLogUtilsBean;
|
||||
public static Map<String, Boolean> mapTAGList = new HashMap<String, Boolean>();
|
||||
|
||||
/**
|
||||
* 初始化日志工具(默认日志级别:Off,不输出日志)
|
||||
* @param context 全局上下文(建议传入 Application 实例)
|
||||
*/
|
||||
//
|
||||
// 初始化函数
|
||||
//
|
||||
public static void init(Context context) {
|
||||
sContext = context;
|
||||
_mContext = context;
|
||||
init(context, LOG_LEVEL.Off);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化日志工具(指定日志级别)
|
||||
* 1. 根据 Debug/Release 模式初始化日志存储路径;
|
||||
* 2. 加载日志配置文件;
|
||||
* 3. 扫描应用内所有类的 TAG 并初始化过滤映射表;
|
||||
* 4. 标记初始化完成。
|
||||
* @param context 全局上下文
|
||||
* @param logLevel 初始日志级别
|
||||
*/
|
||||
//
|
||||
// 初始化函数
|
||||
//
|
||||
public static void init(Context context, LOG_LEVEL logLevel) {
|
||||
sContext = context;
|
||||
// 根据 Debug 模式选择存储路径(外部/内部存储)
|
||||
if (GlobalApplication.isDebugging()) {
|
||||
// Debug 模式:存储在外部缓存目录(可通过文件管理器查看)
|
||||
sLogCacheDir = new File(context.getApplicationContext().getExternalCacheDir(), TAG);
|
||||
sLogDataDir = context.getApplicationContext().getExternalFilesDir(TAG);
|
||||
// 初始化日志缓存文件路径
|
||||
_mfLogCacheDir = new File(context.getApplicationContext().getExternalCacheDir(), TAG);
|
||||
if (!_mfLogCacheDir.exists()) {
|
||||
_mfLogCacheDir.mkdirs();
|
||||
}
|
||||
_mfLogCatchFile = new File(_mfLogCacheDir, "log.txt");
|
||||
|
||||
// 初始化日志配置文件路径
|
||||
_mfLogDataDir = context.getApplicationContext().getExternalFilesDir(TAG);
|
||||
if (!_mfLogDataDir.exists()) {
|
||||
_mfLogDataDir.mkdirs();
|
||||
}
|
||||
_mfLogUtilsBeanFile = new File(_mfLogDataDir, TAG + ".json");
|
||||
} else {
|
||||
// Release 模式:存储在内部缓存目录(仅应用自身可访问)
|
||||
sLogCacheDir = new File(context.getApplicationContext().getCacheDir(), TAG);
|
||||
sLogDataDir = new File(context.getApplicationContext().getFilesDir(), TAG);
|
||||
// 初始化日志缓存文件路径
|
||||
_mfLogCacheDir = new File(context.getApplicationContext().getCacheDir(), TAG);
|
||||
if (!_mfLogCacheDir.exists()) {
|
||||
_mfLogCacheDir.mkdirs();
|
||||
}
|
||||
_mfLogCatchFile = new File(_mfLogCacheDir, "log.txt");
|
||||
|
||||
// 初始化日志配置文件路径
|
||||
_mfLogDataDir = new File(context.getApplicationContext().getFilesDir(), TAG);
|
||||
if (!_mfLogDataDir.exists()) {
|
||||
_mfLogDataDir.mkdirs();
|
||||
}
|
||||
_mfLogUtilsBeanFile = new File(_mfLogDataDir, TAG + ".json");
|
||||
}
|
||||
|
||||
// 创建日志文件夹(不存在则创建)
|
||||
createDirIfNotExists(sLogCacheDir);
|
||||
createDirIfNotExists(sLogDataDir);
|
||||
|
||||
// 初始化日志文件和配置文件路径
|
||||
sLogFile = new File(sLogCacheDir, "log.txt");
|
||||
sLogConfigFile = new File(sLogDataDir, TAG + ".json");
|
||||
|
||||
// 加载日志配置(从文件读取,读取失败则创建默认配置)
|
||||
sLogConfigBean = LogUtilsBean.loadBeanFromFile(sLogConfigFile.getPath(), LogUtilsBean.class);
|
||||
if (sLogConfigBean == null) {
|
||||
sLogConfigBean = new LogUtilsBean();
|
||||
sLogConfigBean.setLogLevel(logLevel);
|
||||
// 保存默认配置到文件
|
||||
sLogConfigBean.saveBeanToFile(sLogConfigFile.getPath(), sLogConfigBean);
|
||||
// Toast.makeText(context,
|
||||
// "_mfLogUtilsBeanFile : " + _mfLogUtilsBeanFile
|
||||
// + "\n_mfLogCatchFile : " + _mfLogCatchFile,
|
||||
// Toast.LENGTH_SHORT).show();
|
||||
//
|
||||
_mLogUtilsBean = LogUtilsBean.loadBeanFromFile(_mfLogUtilsBeanFile.getPath(), LogUtilsBean.class);
|
||||
if (_mLogUtilsBean == null) {
|
||||
_mLogUtilsBean = new LogUtilsBean();
|
||||
_mLogUtilsBean.saveBeanToFile(_mfLogUtilsBeanFile.getPath(), _mLogUtilsBean);
|
||||
}
|
||||
|
||||
// 扫描应用内所有类的 TAG 并添加到过滤映射表
|
||||
scanAllClassTags();
|
||||
// 加载已保存的 TAG 启用状态配置
|
||||
loadTagEnableSettings();
|
||||
// 标记初始化完成
|
||||
sIsInited = true;
|
||||
|
||||
// 打印初始化日志(调试用)
|
||||
d(TAG, String.format("TAG 过滤映射表初始化完成:%s", sTagEnableTagModelList.toString()));
|
||||
// 加载当前应用下的所有类的 TAG
|
||||
addClassTAGList();
|
||||
loadTAGBeanSettings();
|
||||
_IsInited = true;
|
||||
LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 TAG 过滤映射表(外部可通过此方法获取所有 TAG 及其启用状态)
|
||||
* @return TAG 名称与启用状态的映射
|
||||
*/
|
||||
public static ArrayList<TagModel> getTagEnableTagModelList() {
|
||||
return sTagEnableTagModelList;
|
||||
public static Map<String, Boolean> getMapTAGList() {
|
||||
return mapTAGList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载已保存的 TAG 启用状态配置
|
||||
* 从 LogUtilsClassTAGBean 列表中读取每个 TAG 的启用状态,更新到映射表
|
||||
*/
|
||||
private static void loadTagEnableSettings() {
|
||||
ArrayList<LogUtilsClassTAGBean> tagSettingList = new ArrayList<LogUtilsClassTAGBean>();
|
||||
// 从文件加载 TAG 配置列表
|
||||
LogUtilsClassTAGBean.loadBeanList(sContext, tagSettingList, LogUtilsClassTAGBean.class);
|
||||
|
||||
// 遍历配置列表,更新 TAG 启用状态(Java 7 增强 for 循环)
|
||||
for (LogUtilsClassTAGBean tagSetting : tagSettingList) {
|
||||
String tag = tagSetting.getTag();
|
||||
boolean isEnable = tagSetting.getEnable();
|
||||
// 仅更新已存在的 TAG(避免无效配置)
|
||||
for (TagModel tagModel : sTagEnableTagModelList) {
|
||||
if (tagModel.equals(tag)) {
|
||||
tagModel.setChecked(isEnable);
|
||||
}
|
||||
}
|
||||
// if (sTagEnableTagModelList.containsKey(tag)) {
|
||||
// sTagEnableTagModelList.put(tag, isEnable);
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前 TAG 启用状态配置到文件
|
||||
* 将映射表中的 TAG 及其启用状态转换为 LogUtilsClassTAGBean 列表,持久化到文件
|
||||
*/
|
||||
private static void saveTagEnableSettings() {
|
||||
ArrayList<LogUtilsClassTAGBean> tagSettingList = new ArrayList<LogUtilsClassTAGBean>();
|
||||
// 遍历映射表,构建配置列表(Java 7 迭代器遍历)
|
||||
for (TagModel tagModel : sTagEnableTagModelList) {
|
||||
tagSettingList.add(new LogUtilsClassTAGBean(tagModel.getTagName(), tagModel.isChecked()));
|
||||
}
|
||||
|
||||
// Iterator<Map.Entry<String, Boolean>> iterator = sTagEnableTagModelList.entrySet().iterator();
|
||||
// while (iterator.hasNext()) {
|
||||
// Map.Entry<String, Boolean> entry = iterator.next();
|
||||
// tagSettingList.add(new LogUtilsClassTAGBean(entry.getKey(), entry.getValue()));
|
||||
// }
|
||||
|
||||
// 保存配置列表到文件
|
||||
LogUtilsClassTAGBean.saveBeanList(sContext, tagSettingList, LogUtilsClassTAGBean.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描应用内所有类的 TAG 并添加到过滤映射表
|
||||
* 1. 通过 DexFile 读取 APK 中所有类;
|
||||
* 2. 过滤指定包名前缀(cc.winboll.studio)的类;
|
||||
* 3. 反射获取类中 public static final String TAG 字段的值;
|
||||
* 4. 将 TAG 加入映射表,默认禁用(false)。
|
||||
*/
|
||||
private static void scanAllClassTags() {
|
||||
try {
|
||||
// 应用 APK 路径(通过上下文获取)
|
||||
String apkPath = sContext.getPackageCodePath();
|
||||
d(TAG, String.format("APK 路径:%s", apkPath));
|
||||
|
||||
// 读取 APK 中的所有类
|
||||
DexFile dexFile = new DexFile(apkPath);
|
||||
Enumeration<String> classNames = dexFile.entries();
|
||||
|
||||
int totalClassCount = 0; // 总类数(调试用)
|
||||
List<String> targetClassNames = new ArrayList<String>(); // 目标包名下的类名列表
|
||||
String targetPackagePrefix = "cc.winboll.studio"; // 目标包名前缀
|
||||
|
||||
// 过滤目标包名下的类(Java 7 枚举遍历)
|
||||
while (classNames.hasMoreElements()) {
|
||||
totalClassCount++;
|
||||
String className = classNames.nextElement();
|
||||
if (className.startsWith(targetPackagePrefix)) {
|
||||
targetClassNames.add(className);
|
||||
static void loadTAGBeanSettings() {
|
||||
ArrayList<LogUtilsClassTAGBean> list = new ArrayList<LogUtilsClassTAGBean>();
|
||||
LogUtilsClassTAGBean.loadBeanList(_mContext, list, LogUtilsClassTAGBean.class);
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
LogUtilsClassTAGBean beanSetting = list.get(i);
|
||||
for (Map.Entry<String, Boolean> entry : mapTAGList.entrySet()) {
|
||||
if (entry.getKey().equals(beanSetting.getTag())) {
|
||||
entry.setValue(beanSetting.getEnable());
|
||||
}
|
||||
}
|
||||
|
||||
// 打印扫描统计(调试用)
|
||||
d(TAG, String.format("APK 总类数:%d,目标包下类数:%d", totalClassCount, targetClassNames.size()));
|
||||
}
|
||||
}
|
||||
|
||||
// 反射获取每个类的 TAG 字段(Java 7 增强 for 循环)
|
||||
for (String className : targetClassNames) {
|
||||
static void saveTAGBeanSettings() {
|
||||
ArrayList<LogUtilsClassTAGBean> list = new ArrayList<LogUtilsClassTAGBean>();
|
||||
for (Map.Entry<String, Boolean> entry : mapTAGList.entrySet()) {
|
||||
list.add(new LogUtilsClassTAGBean(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
LogUtilsClassTAGBean.saveBeanList(_mContext, list, LogUtilsClassTAGBean.class);
|
||||
}
|
||||
|
||||
static void addClassTAGList() {
|
||||
//ClassLoader classLoader = getClass().getClassLoader();
|
||||
try {
|
||||
//String packageName = context.getPackageName();
|
||||
String packageNamePrefix = "cc.winboll.studio";
|
||||
List<String> classNames = new ArrayList<>();
|
||||
String apkPath = _mContext.getPackageCodePath();
|
||||
//Log.d("APK_PATH", "The APK path is: " + apkPath);
|
||||
LogUtils.d(TAG, String.format("apkPath : %s", apkPath));
|
||||
//String apkPath = "/data/app/" + packageName + "-";
|
||||
|
||||
//DexFile dexfile = new DexFile(apkPath + "1/base.apk");
|
||||
DexFile dexfile = new DexFile(apkPath);
|
||||
|
||||
int countTemp = 0;
|
||||
Enumeration<String> entries = dexfile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
countTemp++;
|
||||
String className = entries.nextElement();
|
||||
if (className.startsWith(packageNamePrefix)) {
|
||||
classNames.add(className);
|
||||
}
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, String.format("countTemp : %d\nClassNames size : %d", countTemp, classNames.size()));
|
||||
|
||||
for (String className : classNames) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(className);
|
||||
// 获取类中所有声明的字段
|
||||
Field[] fields = clazz.getDeclaredFields();
|
||||
for (Field field : fields) {
|
||||
// 过滤条件:public static String 类型,且字段名是 "TAG"
|
||||
if (Modifier.isStatic(field.getModifiers())
|
||||
&& Modifier.isPublic(field.getModifiers())
|
||||
&& field.getType() == String.class
|
||||
&& "TAG".equals(field.getName())) {
|
||||
// 获取 TAG 字段的值(静态字段,传入 null 即可)
|
||||
if (Modifier.isStatic(field.getModifiers()) && Modifier.isPublic(field.getModifiers()) && field.getType() == String.class && "TAG".equals(field.getName())) {
|
||||
String tagValue = (String) field.get(null);
|
||||
// 添加到映射表,默认禁用
|
||||
sTagEnableTagModelList.add(new TagModel(false, tagValue));
|
||||
// sTagEnableTagModelList.put(tagValue, false);
|
||||
//Log.d("TAG_INFO", "Class: " + className + ", TAG value: " + tagValue);
|
||||
//LogUtils.d(TAG, String.format("Tag Value : %s", tagValue));
|
||||
//mapTAGList.put(tagValue, true);
|
||||
mapTAGList.put(tagValue, false);
|
||||
}
|
||||
}
|
||||
} 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 (NoClassDefFoundError | ClassNotFoundException | IllegalAccessException e) {
|
||||
LogUtils.d(TAG, e.getMessage(), Thread.currentThread().getStackTrace());
|
||||
//LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
//Toast.makeText(context, TAG + " : " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 捕获 APK 读取异常
|
||||
d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
//Toast.makeText(context, TAG + " : " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单个 TAG 的启用状态
|
||||
* @param tag TAG 名称
|
||||
* @param isEnable 是否启用(true:输出该 TAG 的日志;false:不输出)
|
||||
*/
|
||||
public static void setTagEnable(String tag, boolean isEnable) {
|
||||
// 遍历映射表,更新目标 TAG 的状态(Java 7 迭代器遍历)
|
||||
for (TagModel tagModel : sTagEnableTagModelList) {
|
||||
if (tagModel.getTagName().equals(tag)) {
|
||||
tagModel.setChecked(isEnable);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Iterator<Map.Entry<String, Boolean>> iterator = sTagEnableTagModelList.entrySet().iterator();
|
||||
// while (iterator.hasNext()) {
|
||||
// Map.Entry<String, Boolean> entry = iterator.next();
|
||||
// if (tag.equals(entry.getKey())) {
|
||||
// entry.setValue(isEnable);
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// 保存配置到文件(持久化)
|
||||
saveTagEnableSettings();
|
||||
d(TAG, String.format("TAG 配置更新:%s", sTagEnableTagModelList.toString()));
|
||||
public static void setTAGListEnable(String tag, boolean isEnable) {
|
||||
Iterator<Map.Entry<String, Boolean>> iterator = mapTAGList.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, Boolean> entry = iterator.next();
|
||||
if (tag.equals(entry.getKey())) {
|
||||
entry.setValue(isEnable);
|
||||
//System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
saveTAGBeanSettings();
|
||||
LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置所有 TAG 的启用状态(批量控制)
|
||||
* @param isEnable 是否启用(true:所有 TAG 均输出日志;false:所有 TAG 均不输出)
|
||||
*/
|
||||
public static void setAllTagsEnable(boolean isEnable) {
|
||||
// 遍历映射表,批量更新所有 TAG 的状态(Java 7 迭代器遍历)
|
||||
for (TagModel tagModel : sTagEnableTagModelList) {
|
||||
tagModel.setChecked(isEnable);
|
||||
}
|
||||
// Iterator<Map.Entry<String, Boolean>> iterator = sTagEnableTagModelList.entrySet().iterator();
|
||||
// while (iterator.hasNext()) {
|
||||
// Map.Entry<String, Boolean> entry = iterator.next();
|
||||
// entry.setValue(isEnable);
|
||||
// }
|
||||
// 保存配置到文件(持久化)
|
||||
saveTagEnableSettings();
|
||||
d(TAG, String.format("所有 TAG 配置更新:%s", sTagEnableTagModelList.toString()));
|
||||
public static void setALlTAGListEnable(boolean isEnable) {
|
||||
Iterator<Map.Entry<String, Boolean>> iterator = mapTAGList.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, Boolean> entry = iterator.next();
|
||||
entry.setValue(isEnable);
|
||||
//System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
|
||||
}
|
||||
saveTAGBeanSettings();
|
||||
LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全局日志级别(控制日志输出的详细程度)
|
||||
* @param logLevel 目标日志级别
|
||||
*/
|
||||
public static void setLogLevel(LOG_LEVEL logLevel) {
|
||||
if (sLogConfigBean != null) {
|
||||
sLogConfigBean.setLogLevel(logLevel);
|
||||
// 保存配置到文件(持久化)
|
||||
sLogConfigBean.saveBeanToFile(sLogConfigFile.getPath(), sLogConfigBean);
|
||||
}
|
||||
LogUtils._mLogUtilsBean.setLogLevel(logLevel);
|
||||
_mLogUtilsBean.saveBeanToFile(_mfLogUtilsBeanFile.getPath(), _mLogUtilsBean);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前全局日志级别
|
||||
* @return 当前日志级别
|
||||
*/
|
||||
public static LOG_LEVEL getLogLevel() {
|
||||
return sLogConfigBean != null ? sLogConfigBean.getLogLevel() : LOG_LEVEL.Off;
|
||||
return LogUtils._mLogUtilsBean.getLogLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前日志是否可输出(校验初始化状态、TAG 启用状态、日志级别)
|
||||
* @param tag 日志 TAG
|
||||
* @param logLevel 日志级别
|
||||
* @return true:可输出;false:不可输出
|
||||
*/
|
||||
private static boolean isLoggable(String tag, LOG_LEVEL logLevel) {
|
||||
// 未初始化:不输出
|
||||
if (!sIsInited) {
|
||||
static boolean isLoggable(String tag, LOG_LEVEL logLevel) {
|
||||
if (!_IsInited) {
|
||||
return false;
|
||||
}
|
||||
if(sTagEnableTagModelList == null || sTagEnableTagModelList.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
boolean isTagExist = false;
|
||||
for (TagModel tagModel : sTagEnableTagModelList) {
|
||||
if(tagModel.equals(tag)) {
|
||||
isTagExist = true;
|
||||
}
|
||||
}
|
||||
// TAG 未配置或未启用:不输出
|
||||
if (!isTagExist) {
|
||||
}
|
||||
if (mapTAGList.get(tag) == null
|
||||
|| !mapTAGList.get(tag)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 日志级别未达到:不输出
|
||||
if (!isLevelMatched(logLevel)) {
|
||||
}
|
||||
if (!isInTheLevel(logLevel)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断日志级别是否匹配(当前全局级别 >= 目标级别时可输出)
|
||||
* 例:全局级别为 Debug(4),则 Error(1)、Warn(2)、Info(3)、Debug(4)均可输出
|
||||
* @param logLevel 目标日志级别
|
||||
* @return true:级别匹配;false:不匹配
|
||||
*/
|
||||
private static boolean isLevelMatched(LOG_LEVEL logLevel) {
|
||||
if (sLogConfigBean == null) {
|
||||
return false;
|
||||
}
|
||||
// 枚举的 ordinal() 方法返回索引(Off=0,Error=1,...,Verbose=5)
|
||||
return sLogConfigBean.getLogLevel().ordinal() >= logLevel.ordinal();
|
||||
static boolean isInTheLevel(LOG_LEVEL logLevel) {
|
||||
return (LogUtils._mLogUtilsBean.getLogLevel().ordinal() == logLevel.ordinal()
|
||||
|| LogUtils._mLogUtilsBean.getLogLevel().ordinal() > logLevel.ordinal());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志缓存文件夹路径(外部可通过此方法获取日志存储目录)
|
||||
* @return 日志缓存文件夹
|
||||
*/
|
||||
//
|
||||
// 获取应用日志文件夹
|
||||
//
|
||||
public static File getLogCacheDir() {
|
||||
return sLogCacheDir;
|
||||
return _mfLogCacheDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 Error 级别日志
|
||||
* @param tag TAG 名称
|
||||
* @param message 日志内容
|
||||
*/
|
||||
public static void e(String tag, String message) {
|
||||
if (isLoggable(tag, LOG_LEVEL.Error)) {
|
||||
saveLog(tag, LOG_LEVEL.Error, message);
|
||||
//
|
||||
// 调试日志写入函数
|
||||
//
|
||||
public static void e(String szTAG, String szMessage) {
|
||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Error)) {
|
||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Error, szMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 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());
|
||||
//
|
||||
// 调试日志写入函数
|
||||
//
|
||||
public static void w(String szTAG, String szMessage) {
|
||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Warn)) {
|
||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Warn, szMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 Warn 级别日志
|
||||
* @param tag TAG 名称
|
||||
* @param message 日志内容
|
||||
*/
|
||||
public static void w(String tag, String message) {
|
||||
if (isLoggable(tag, LOG_LEVEL.Warn)) {
|
||||
saveLog(tag, LOG_LEVEL.Warn, message);
|
||||
//
|
||||
// 调试日志写入函数
|
||||
//
|
||||
public static void i(String szTAG, String szMessage) {
|
||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Info)) {
|
||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Info, szMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 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());
|
||||
//
|
||||
// 调试日志写入函数
|
||||
//
|
||||
public static void d(String szTAG, String szMessage) {
|
||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
|
||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, szMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 Info 级别日志
|
||||
* @param tag TAG 名称
|
||||
* @param message 日志内容
|
||||
*/
|
||||
public static void i(String tag, String message) {
|
||||
if (isLoggable(tag, LOG_LEVEL.Info)) {
|
||||
saveLog(tag, LOG_LEVEL.Info, message);
|
||||
//
|
||||
// 调试日志写入函数
|
||||
// 包含线程调试堆栈信息
|
||||
//
|
||||
public static void d(String szTAG, String szMessage, StackTraceElement[] listStackTrace) {
|
||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
|
||||
StringBuilder sbMessage = new StringBuilder(szMessage);
|
||||
sbMessage.append(" \nAt ");
|
||||
sbMessage.append(listStackTrace[2].getMethodName());
|
||||
sbMessage.append(" (");
|
||||
sbMessage.append(listStackTrace[2].getFileName());
|
||||
sbMessage.append(":");
|
||||
sbMessage.append(listStackTrace[2].getLineNumber());
|
||||
sbMessage.append(")");
|
||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sbMessage.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 Debug 级别日志(基础版)
|
||||
* @param tag TAG 名称
|
||||
* @param message 日志内容
|
||||
*/
|
||||
public static void d(String tag, String message) {
|
||||
if (isLoggable(tag, LOG_LEVEL.Debug)) {
|
||||
saveLog(tag, LOG_LEVEL.Debug, message);
|
||||
//
|
||||
// 调试日志写入函数
|
||||
// 包含异常信息和线程调试堆栈信息
|
||||
//
|
||||
public static void d(String szTAG, Exception e, StackTraceElement[] listStackTrace) {
|
||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) {
|
||||
StringBuilder sbMessage = new StringBuilder(e.getClass().toGenericString());
|
||||
sbMessage.append(" : ");
|
||||
sbMessage.append(e.getMessage());
|
||||
sbMessage.append(" \nAt ");
|
||||
sbMessage.append(listStackTrace[2].getMethodName());
|
||||
sbMessage.append(" (");
|
||||
sbMessage.append(listStackTrace[2].getFileName());
|
||||
sbMessage.append(":");
|
||||
sbMessage.append(listStackTrace[2].getLineNumber());
|
||||
sbMessage.append(")");
|
||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sbMessage.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 Debug 级别日志(带调用栈信息)
|
||||
* 包含调用方法名、文件名、行号,便于调试定位
|
||||
* @param tag TAG 名称
|
||||
* @param message 日志内容
|
||||
* @param stackTrace 线程调用栈(通常传入 Thread.currentThread().getStackTrace())
|
||||
*/
|
||||
public static void d(String tag, String message, StackTraceElement[] stackTrace) {
|
||||
if (isLoggable(tag, LOG_LEVEL.Debug)) {
|
||||
StringBuilder sb = new StringBuilder(message);
|
||||
// 拼接调用栈信息(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());
|
||||
//
|
||||
// 调试日志写入函数
|
||||
//
|
||||
public static void v(String szTAG, String szMessage) {
|
||||
if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Verbose)) {
|
||||
saveLog(szTAG, LogUtils.LOG_LEVEL.Verbose, szMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 Debug 级别日志(带异常信息和调用栈)
|
||||
* 包含异常类型、异常信息、调用位置,便于异常定位
|
||||
* @param tag TAG 名称
|
||||
* @param e 异常对象
|
||||
* @param stackTrace 线程调用栈
|
||||
*/
|
||||
public static void d(String tag, Exception e, StackTraceElement[] stackTrace) {
|
||||
if (isLoggable(tag, LOG_LEVEL.Debug)) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
// 拼接异常信息
|
||||
sb.append(e.getClass().getSimpleName())
|
||||
.append(" : ")
|
||||
.append(e.getMessage() != null ? e.getMessage() : "无异常消息")
|
||||
// 拼接调用栈信息
|
||||
.append(" \nAt ")
|
||||
.append(stackTrace[2].getMethodName())
|
||||
.append(" (")
|
||||
.append(stackTrace[2].getFileName())
|
||||
.append(":")
|
||||
.append(stackTrace[2].getLineNumber())
|
||||
.append(")");
|
||||
saveLog(tag, LOG_LEVEL.Debug, sb.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出 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 名称
|
||||
* @param message 日志内容
|
||||
*/
|
||||
public static void v(String tag, String message) {
|
||||
if (isLoggable(tag, LOG_LEVEL.Verbose)) {
|
||||
saveLog(tag, LOG_LEVEL.Verbose, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心日志保存方法(将日志写入文件)
|
||||
* 日志格式:[级别] [时间戳] [TAG]
|
||||
* 日志内容
|
||||
* @param tag TAG 名称
|
||||
* @param logLevel 日志级别
|
||||
* @param message 日志内容
|
||||
*/
|
||||
private static void saveLog(String tag, LOG_LEVEL logLevel, String message) {
|
||||
BufferedWriter writer = null;
|
||||
//
|
||||
// 日志文件保存函数
|
||||
//
|
||||
static void saveLog(String szTAG, LogUtils.LOG_LEVEL logLevel, String szMessage) {
|
||||
try {
|
||||
// 以追加模式打开日志文件,UTF-8 编码
|
||||
writer = new BufferedWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(sLogFile, true),
|
||||
"UTF-8"
|
||||
)
|
||||
);
|
||||
// 拼接日志内容(级别 + 时间 + TAG + 消息)
|
||||
String logContent = String.format(
|
||||
"[%s] %s [%s]\n%s\n",
|
||||
logLevel.name(), // 日志级别(如 Debug)
|
||||
sSimpleDateFormat.format(System.currentTimeMillis()), // 时间戳
|
||||
tag, // TAG 名称
|
||||
message // 日志内容
|
||||
);
|
||||
// 写入文件
|
||||
writer.write(logContent);
|
||||
BufferedWriter out = null;
|
||||
out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(_mfLogCatchFile, true), "UTF-8"));
|
||||
out.write("[" + logLevel + "] " + mSimpleDateFormat.format(System.currentTimeMillis()) + " [" + szTAG + "]\n" + szMessage + "\n");
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
// 日志写入失败时,输出内部调试日志
|
||||
d(TAG, "日志写入失败:" + (e.getMessage() != null ? e.getMessage() : "未知错误"));
|
||||
} finally {
|
||||
// 关闭流,避免资源泄漏(Java 7 手动关闭,不使用 try-with-resources)
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "IOException : " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载历史日志(读取日志文件所有内容)
|
||||
* @return 历史日志字符串(空字符串表示文件不存在或读取失败)
|
||||
*/
|
||||
//
|
||||
// 历史日志加载函数
|
||||
//
|
||||
public static String loadLog() {
|
||||
// 日志文件不存在,返回空
|
||||
if (sLogFile == null || !sLogFile.exists()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder logContent = new StringBuilder();
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
// 以 UTF-8 编码读取日志文件
|
||||
reader = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(sLogFile),
|
||||
"UTF-8"
|
||||
)
|
||||
);
|
||||
String line;
|
||||
// 逐行读取并拼接(Java 7 普通 while 循环)
|
||||
while ((line = reader.readLine()) != null) {
|
||||
logContent.append(line).append("\n");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 读取失败时,输出内部调试日志
|
||||
d(TAG, "日志读取失败:" + (e.getMessage() != null ? e.getMessage() : "未知错误"));
|
||||
} finally {
|
||||
// 关闭流,避免资源泄漏
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
if (_mfLogCatchFile.exists()) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
try {
|
||||
BufferedReader in = null;
|
||||
in = new BufferedReader(new InputStreamReader(new FileInputStream(_mfLogCatchFile), "UTF-8"));
|
||||
String line = "";
|
||||
while ((line = in.readLine()) != null) {
|
||||
sb.append(line);
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, "IOException : " + e.getMessage());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
return logContent.toString();
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理历史日志(清空日志文件内容)
|
||||
*/
|
||||
//
|
||||
// 清理日志函数
|
||||
//
|
||||
public static void cleanLog() {
|
||||
if (sLogFile == null || !sLogFile.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 写入空字符串到文件,实现清空(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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:创建文件夹(不存在则创建)
|
||||
* @param dir 目标文件夹
|
||||
*/
|
||||
private static void createDirIfNotExists(File dir) {
|
||||
if (dir != null && !dir.exists()) {
|
||||
dir.mkdirs();
|
||||
if (_mfLogCatchFile.exists()) {
|
||||
try {
|
||||
UTF8FileUtils.writeStringToFile(_mfLogCatchFile.getPath(), "");
|
||||
//LogUtils.d(TAG, "cleanLog");
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package cc.winboll.studio.libappbase;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@QQ.COM
|
||||
* @Date 2024/08/12 14:36:18
|
||||
* @Describe 日志视图类,继承 RelativeLayout 类。
|
||||
*/
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
@@ -21,57 +26,37 @@ 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 cc.winboll.studio.libappbase.views.HorizontalListView;
|
||||
import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import cc.winboll.studio.libappbase.model.TagModel;
|
||||
|
||||
/**
|
||||
* @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";
|
||||
|
||||
/** 日志处理中标志(避免并发刷新,volatile 保证多线程可见性) */
|
||||
private volatile boolean mIsHandling;
|
||||
/** 新日志添加标志(标记有未处理的新日志,volatile 保证多线程可见性) */
|
||||
private volatile boolean mIsAddNewLog;
|
||||
public volatile boolean mIsHandling;
|
||||
public volatile boolean mIsAddNewLog;
|
||||
|
||||
/** 上下文对象(用于布局加载、系统服务获取) */
|
||||
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 TagsHorizontalListView mTagsHorizontalListView;
|
||||
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;
|
||||
|
||||
// ====================== 构造方法(初始化视图) ======================
|
||||
public LogView(Context context) {
|
||||
super(context);
|
||||
initView(context);
|
||||
@@ -92,318 +77,258 @@ public class LogView extends RelativeLayout {
|
||||
initView(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动日志监听与展示
|
||||
* 1. 初始化并启动 LogViewThread(监听日志文件变化);
|
||||
* 2. 初始加载并展示日志内容。
|
||||
*/
|
||||
public void start() {
|
||||
mLogViewThread = new LogViewThread(this);
|
||||
mLogViewThread = new LogViewThread(LogView.this);
|
||||
mLogViewThread.start();
|
||||
showAndScrollLogView(); // 初始显示日志并滚动到底部
|
||||
// 显示日志
|
||||
showAndScrollLogView();
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动日志到底部(确保最新日志可见)
|
||||
* 运行在主线程,通过 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化视图组件(加载布局、绑定控件、设置监听)
|
||||
* @param context 上下文对象
|
||||
*/
|
||||
private void initView(Context context) {
|
||||
void initView(Context context) {
|
||||
mContext = context;
|
||||
mLogViewHandler = new LogViewHandler(); // 初始化主线程 Handler
|
||||
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);
|
||||
|
||||
// 加载日志视图布局(R.layout.view_log 为自定义布局文件)
|
||||
View rootView = LayoutInflater.from(context).inflate(R.layout.view_log, this, true);
|
||||
// 绑定布局控件(通过 ID 找到对应组件)
|
||||
bindViews(rootView);
|
||||
metTagSearch.addTextChangedListener(new TextWatcher() {
|
||||
|
||||
// 设置 TAG 搜索输入框监听(实时搜索并定位 TAG)
|
||||
setupTagSearchListener();
|
||||
// 设置功能按钮监听(清理日志、复制日志)
|
||||
setupFunctionButtonListeners(rootView);
|
||||
// 设置文本选择开关监听(控制日志文本是否可选中)
|
||||
setupTextSelectableListener();
|
||||
// 初始化日志级别下拉框(绑定级别数据,设置默认值)
|
||||
initLogLevelSpinner();
|
||||
// 初始化 TAG 列表(加载所有 TAG,设置全选状态)
|
||||
initTagListView();
|
||||
// 设置默认交互模式(默认禁止子视图获取焦点,避免误触)
|
||||
@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.getMapTAGList();
|
||||
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.setALlTAGListEnable(mSelectAllTAGCheckBox.isChecked());
|
||||
//LogUtils.setALlTAGListEnable(false);
|
||||
//mTAGListAdapter.notifyDataSetChanged();
|
||||
mTAGListAdapter.reload();
|
||||
//ToastUtils.show(String.format("onClick\nmSelectAllTAGCheckBox.isChecked() : %s", mSelectAllTAGCheckBox.isChecked()));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 设置滚动时不聚焦日志
|
||||
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);
|
||||
mTagsHorizontalListView = 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);
|
||||
// 定位匹配的 TAG
|
||||
scrollToTargetTag(searchText);
|
||||
// if (!searchText.isEmpty()) {
|
||||
// // 搜索文本非空,定位匹配的 TAG
|
||||
// scrollToTargetTag(searchText);
|
||||
// } else {
|
||||
// // 搜索文本为空,重置滚动位置
|
||||
// HorizontalScrollView parentHs = findViewById(R.id.viewlogHorizontalScrollView1);
|
||||
// parentHs.smoothScrollTo(0, 0);
|
||||
// mTagsHorizontalListView.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 启用状态映射表
|
||||
ArrayList<TagModel> tagEnableTagModel = LogUtils.getTagEnableTagModelList();
|
||||
// 判断是否所有 TAG 都已启用(初始化全选开关状态)
|
||||
boolean isAllTagEnabled = isAllTagsEnabled(tagEnableTagModel);
|
||||
mSelectAllTagCb.setChecked(isAllTagEnabled);
|
||||
|
||||
// 初始化 TAG 水平列表(设置垂直偏移,绑定适配器)
|
||||
//mTagHorizontalListView.setVerticalOffset(10);
|
||||
//mTagListAdapter = new TAGListAdapter(mContext, tagEnableMap);
|
||||
mTagsHorizontalListView.setTagList(tagEnableTagModel);
|
||||
//mTagListAdapter.notifyDataSetChanged(); // 刷新列表数据
|
||||
|
||||
// 全选 TAG 开关监听(点击时启用/禁用所有 TAG)
|
||||
mSelectAllTagCb.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
boolean isSelectAll = mSelectAllTagCb.isChecked();
|
||||
LogUtils.setAllTagsEnable(isSelectAll); // 批量更新所有 TAG 状态
|
||||
|
||||
LogUtils.d(TAG, "mTagListAdapter.reload() not yet.");
|
||||
//mTagListAdapter.reload(); // 重新加载 TAG 数据并刷新视图
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否所有 TAG 都已启用
|
||||
* @param tagEnableMap TAG 启用状态映射表
|
||||
* @return true:所有 TAG 均启用;false:存在未启用的 TAG
|
||||
*/
|
||||
private boolean isAllTagsEnabled(ArrayList<TagModel> tagEnableTagModelList) {
|
||||
for(TagModel tagModel : tagEnableTagModelList) {
|
||||
if(tagModel.isChecked()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// for (Map.Entry<String, Boolean> entry : tagEnableTagModelList.entrySet()) {
|
||||
// if (!entry.getValue()) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新日志视图(由 LogViewThread 触发,通知有新日志)
|
||||
* 避免并发刷新:正在处理时标记新日志,处理完成后再次刷新
|
||||
*/
|
||||
public void updateLogView() {
|
||||
if (mLogViewHandler.isHandling()) {
|
||||
// 正在处理日志刷新,标记有新日志待处理
|
||||
if (mLogViewHandler.isHandling() == true) {
|
||||
// 正在处理日志显示,
|
||||
// 就先设置一个新日志标志位
|
||||
// 以便日志显示完后,再次显示新日志内容
|
||||
mLogViewHandler.setIsAddNewLog(true);
|
||||
} else {
|
||||
// 发送刷新消息到主线程
|
||||
Message refreshMsg = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOG_REFRESH);
|
||||
mLogViewHandler.sendMessage(refreshMsg);
|
||||
//LogUtils.d(TAG, "LogListener showLog(String path)");
|
||||
Message message = mLogViewHandler.obtainMessage(LogViewHandler.MSG_LOGVIEW_UPDATE);
|
||||
mLogViewHandler.sendMessage(message);
|
||||
mLogViewHandler.setIsAddNewLog(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示日志并滚动到底部
|
||||
* 1. 从 LogUtils 加载所有历史日志;
|
||||
* 2. 设置到文本控件并滚动到底部。
|
||||
*/
|
||||
private void showAndScrollLogView() {
|
||||
mLogTextView.setText(LogUtils.loadLog()); // 加载并显示日志
|
||||
scrollLogToBottom(); // 滚动到底部,显示最新日志
|
||||
void showAndScrollLogView() {
|
||||
mTextView.setText(LogUtils.loadLog());
|
||||
scrollLogUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到目标 TAG(根据搜索文本定位匹配的 TAG 并滚动显示)
|
||||
* @param prefix 搜索文本(TAG 前缀)
|
||||
*/
|
||||
private void scrollToTargetTag(final String prefix) {
|
||||
if (prefix == null || prefix.isEmpty()) {
|
||||
LogUtils.d(TAG, "TAG 搜索参数为空,无法定位");
|
||||
public void scrollToTag(final String prefix) {
|
||||
if (mTAGListAdapter == null || prefix == null || prefix.length() == 0) {
|
||||
LogUtils.d(TAG, "参数为空,无法滚动");
|
||||
return;
|
||||
}
|
||||
|
||||
final List<TagModel> tagItemList = mTagsHorizontalListView.getTagList();
|
||||
mTagsHorizontalListView.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).getTagName();
|
||||
if (tag != null && tag.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
targetPosition = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final List<TAGItemModel> itemList = mTAGListAdapter.getItemList();
|
||||
|
||||
if (targetPosition != -1) {
|
||||
final int targetPositionFinal = targetPosition;
|
||||
// 延迟滚动(确保布局完成,避免滚动失效)
|
||||
mTagsHorizontalListView.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LogUtils.d(TAG, "定位到 TAG 位置:" + targetPositionFinal);
|
||||
|
||||
LogUtils.d(TAG, "mTagsHorizontalListView.scrollToItem(targetPositionFinal); not yet.");
|
||||
//mTagsHorizontalListView.scrollToItem(targetPositionFinal);
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
LogUtils.d(TAG, "未找到匹配前缀的 TAG:" + prefix);
|
||||
}
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====================== 内部类:日志视图 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;
|
||||
|
||||
|
||||
class LogViewHandler extends Handler {
|
||||
|
||||
final static int MSG_LOGVIEW_UPDATE = 0;
|
||||
volatile boolean isHandling;
|
||||
volatile boolean isAddNewLog;
|
||||
|
||||
public LogViewHandler() {
|
||||
setIsHandling(false);
|
||||
@@ -426,32 +351,24 @@ public class LogView extends RelativeLayout {
|
||||
return isAddNewLog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
switch (msg.what) {
|
||||
case MSG_LOG_REFRESH:
|
||||
// 未处理日志刷新时,标记为处理中并触发显示
|
||||
if (!isHandling()) {
|
||||
setIsHandling(true);
|
||||
showAndScrollLogView();
|
||||
case MSG_LOGVIEW_UPDATE:{
|
||||
if (isHandling() == false) {
|
||||
setIsHandling(true);
|
||||
showAndScrollLogView();
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 内部类:TAG 数据模型(封装 TAG 名称与状态) ======================
|
||||
/**
|
||||
* TAG 列表项数据模型
|
||||
* 封装单个 TAG 的名称及其启用状态(用于 Adapter 数据绑定)
|
||||
*/
|
||||
private class TAGItemModel {
|
||||
/** TAG 名称(如 "LogViewThread"、"LogUtils") */
|
||||
public class TAGItemModel {
|
||||
private String tag;
|
||||
/** TAG 启用状态(true:启用;false:禁用) */
|
||||
private boolean isChecked;
|
||||
|
||||
public TAGItemModel(String tag, boolean isChecked) {
|
||||
@@ -475,17 +392,18 @@ public class LogView extends RelativeLayout {
|
||||
isChecked = checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写 equals 方法(按 TAG 名称判断相等)
|
||||
* @param o 比较对象
|
||||
* @return true:TAG 名称相同;false:不同
|
||||
*/
|
||||
// getter/setter...
|
||||
|
||||
@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 7,不依赖 Objects.equals)
|
||||
// 手动处理空值比较(Java 6 不支持 Objects.equals)
|
||||
if (tag == null) {
|
||||
return that.tag == null;
|
||||
} else {
|
||||
@@ -493,174 +411,106 @@ 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(); // 手动处理空值
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 内部类:TAG 列表适配器(绑定数据与视图) ======================
|
||||
/**
|
||||
* TAG 水平列表适配器(继承 BaseAdapter)
|
||||
* 负责 TAG 数据与列表项视图的绑定,处理勾选状态变化
|
||||
*/
|
||||
private class TAGListAdapter extends BaseAdapter {
|
||||
/** 上下文对象(用于加载列表项布局) */
|
||||
|
||||
public class TAGListAdapter extends BaseAdapter {
|
||||
|
||||
private Context context;
|
||||
/** 原始 TAG 启用状态映射表(来自 LogUtils) */
|
||||
private Map<String, Boolean> originTagMap;
|
||||
/** TAG 列表项数据(转换为 TAGItemModel 列表,便于排序和绑定) */
|
||||
private List<TAGItemModel> tagItemList;
|
||||
private Map<String, Boolean> mapOrigin;
|
||||
private List<TAGItemModel> itemList;
|
||||
|
||||
/**
|
||||
* 构造方法(初始化数据并加载到列表)
|
||||
* @param context 上下文
|
||||
* @param tagMap TAG 启用状态映射表
|
||||
*/
|
||||
public TAGListAdapter(Context context, Map<String, Boolean> tagMap) {
|
||||
public TAGListAdapter(Context context, Map<String, Boolean> map) {
|
||||
this.context = context;
|
||||
this.originTagMap = tagMap;
|
||||
loadTagData(originTagMap); // 加载并转换数据
|
||||
mapOrigin = map;
|
||||
loadMap(mapOrigin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 TAG 列表项数据(供外部定位 TAG 使用)
|
||||
* @return TAGItemModel 列表
|
||||
*/
|
||||
public List<TAGItemModel> getItemList() {
|
||||
return tagItemList;
|
||||
return itemList;
|
||||
}
|
||||
|
||||
// ====================== BaseAdapter 抽象方法实现 ======================
|
||||
@Override
|
||||
public int getCount() {
|
||||
return tagItemList == null ? 0 : tagItemList.size();
|
||||
return itemList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return tagItemList.get(position);
|
||||
public Object getItem(int p) {
|
||||
return itemList.get(p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
public long getItemId(int p) {
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 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()));
|
||||
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 名称升序排序(中文排序兼容)
|
||||
Collections.sort(tagItemList, new TagAscComparator(true));
|
||||
// 添加排序功能,按照tag进行升序排序
|
||||
Collections.sort(itemList, new SortMapEntryByKeyString(true));
|
||||
//Collections.sort(itemList, new SortMapEntryByKeyString(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载 TAG 数据(用于全选/反选后刷新列表)
|
||||
*/
|
||||
public void reload() {
|
||||
loadTagData(originTagMap); // 重新加载数据
|
||||
notifyDataSetChanged(); // 通知视图刷新
|
||||
loadMap(mapOrigin);
|
||||
super.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();
|
||||
// 绑定列表项控件(TAG 文本和勾选框)
|
||||
holder.tagTv = convertView.findViewById(R.id.viewlogtagTextView1);
|
||||
holder.tagCb = convertView.findViewById(R.id.viewlogtagCheckBox1);
|
||||
convertView.setTag(holder); // 保存 ViewHolder 到视图
|
||||
holder.tvText = convertView.findViewById(R.id.viewlogtagTextView1);
|
||||
holder.cbChecked = convertView.findViewById(R.id.viewlogtagCheckBox1);
|
||||
convertView.setTag(holder);
|
||||
} else {
|
||||
holder = (ViewHolder) convertView.getTag(); // 复用 ViewHolder
|
||||
holder = (ViewHolder) convertView.getTag();
|
||||
}
|
||||
|
||||
// 绑定数据到视图
|
||||
final TAGItemModel item = tagItemList.get(position);
|
||||
holder.tagTv.setText(item.getTag()); // 设置 TAG 名称
|
||||
holder.tagCb.setChecked(item.isChecked()); // 设置勾选状态
|
||||
final TAGItemModel item = itemList.get(position);
|
||||
holder.tvText.setText(item.getTag());
|
||||
holder.cbChecked.setChecked(item.isChecked());
|
||||
holder.cbChecked.setOnClickListener(new View.OnClickListener(){
|
||||
|
||||
// 勾选框点击监听(更新 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);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.setTAGListEnable(item.getTag(), ((CheckBox)v).isChecked());
|
||||
}
|
||||
});
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表项 ViewHolder(缓存控件,提升列表滑动性能)
|
||||
*/
|
||||
private class ViewHolder {
|
||||
TextView tagTv; // TAG 名称文本控件
|
||||
CheckBox tagCb; // TAG 启用状态勾选框
|
||||
public class ViewHolder {
|
||||
TextView tvText;
|
||||
CheckBox cbChecked;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 内部类: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;
|
||||
class SortMapEntryByKeyString implements Comparator<TAGItemModel> {
|
||||
private boolean mIsDesc = true;
|
||||
// isDesc 是否降序排列
|
||||
public SortMapEntryByKeyString(boolean isDesc) {
|
||||
mIsDesc = isDesc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个 TAGItemModel(按 TAG 名称排序)
|
||||
* @param o1 第一个比较对象
|
||||
* @param o2 第二个比较对象
|
||||
* @return 比较结果(正数:o1 在 o2 后;负数:o1 在 o2 前;0:相等)
|
||||
*/
|
||||
Collator cmp = Collator.getInstance(java.util.Locale.CHINA);
|
||||
@Override
|
||||
public int compare(TAGItemModel o1, TAGItemModel o2) {
|
||||
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); // 升序
|
||||
if (mIsDesc) {
|
||||
return o1.getTag().compareTo(o2.getTag());
|
||||
} else {
|
||||
return chineseCollator.compare(tag2, tag1); // 降序
|
||||
return o2.getTag().compareTo(o1.getTag());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
package cc.winboll.studio.libappbase;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.LinearLayout;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.model.TagModel;
|
||||
import cc.winboll.studio.libappbase.views.TagItemView;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/11 20:26
|
||||
* @Describe 水平排列 TagItemView 列表控件(继承 HorizontalScrollView)
|
||||
* 核心:以 HorizontalScrollView 为父容器,内部用 LinearLayout 水平承载 TagItemView,支持左右拉动滚动
|
||||
*/
|
||||
public class TagsHorizontalListView extends HorizontalScrollView {
|
||||
public static final String TAG = "TagsHorizontalListView";
|
||||
|
||||
// 内部水平容器(承载所有 TagItemView,核心子布局)
|
||||
private LinearLayout mTagContainer;
|
||||
// Tag 数据列表(存储所有 Tag 数据,与视图联动)
|
||||
private List<TagModel> mTagList;
|
||||
// Tag 选中状态全局监听(供外部获取所有 Tag 选中变化)
|
||||
private OnTagCheckedChangeListener mGlobalCheckedListener;
|
||||
// Tag 项之间的水平间距(默认 10dp,可外部设置)
|
||||
private int mTagHorizontalSpacing = 10;
|
||||
|
||||
// 构造方法(Java 7 完整兼容)
|
||||
public TagsHorizontalListView(Context context) {
|
||||
super(context);
|
||||
initView();
|
||||
}
|
||||
|
||||
public TagsHorizontalListView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public TagsHorizontalListView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initView();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化视图:创建内部水平容器,配置 ScrollView 基础属性
|
||||
*/
|
||||
private void initView() {
|
||||
// 1. 配置 HorizontalScrollView 基础属性
|
||||
setHorizontalScrollBarEnabled(true); // 显示水平滚动条
|
||||
setVerticalScrollBarEnabled(false); // 禁用垂直滚动条
|
||||
setOverScrollMode(OVER_SCROLL_NEVER); // 禁用过度滚动效果(避免边缘阴影)
|
||||
|
||||
// 2. 创建内部水平容器(LinearLayout,承载所有 TagItemView)
|
||||
mTagContainer = new LinearLayout(getContext());
|
||||
mTagContainer.setOrientation(LinearLayout.HORIZONTAL); // 水平排列
|
||||
mTagContainer.setGravity(Gravity.CENTER_VERTICAL); // 子项垂直居中
|
||||
// 设置容器内边距(左右 16dp,上下 8dp,避免 Tag 贴边)
|
||||
int padding = dp2px(16);
|
||||
mTagContainer.setPadding(padding, dp2px(8), padding, dp2px(8));
|
||||
// 将容器添加到 HorizontalScrollView 中(ScrollView 只能有一个直接子View)
|
||||
addView(mTagContainer, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
|
||||
|
||||
// 3. 初始化 Tag 数据列表
|
||||
mTagList = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Tag 数据列表(核心方法:数据驱动视图,批量创建 TagItemView)
|
||||
* @param tagList 所有 Tag 数据(含 isChecked + tagName)
|
||||
*/
|
||||
public void setTagList(List<TagModel> tagList) {
|
||||
// 清空原有数据和视图,避免重复添加
|
||||
clearAllTags();
|
||||
|
||||
if (tagList == null || tagList.isEmpty()) {
|
||||
LogUtils.d(TAG, "Tag 数据列表为空,不创建视图");
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存新数据
|
||||
mTagList.addAll(tagList);
|
||||
|
||||
// 批量创建 TagItemView 并添加到容器
|
||||
for (int i = 0; i < mTagList.size(); i++) {
|
||||
final int position = i;
|
||||
TagModel tagModel = mTagList.get(position);
|
||||
// 创建单个 Tag 控件
|
||||
TagItemView tagItemView = new TagItemView(getContext());
|
||||
// 绑定数据(自动显示标签名称和选中状态)
|
||||
tagItemView.setTagModel(tagModel);
|
||||
// 设置 Tag 项布局参数(添加水平间距)
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
// 除了最后一个 Tag,其余都添加右侧间距
|
||||
if (position != mTagList.size() - 1) {
|
||||
params.rightMargin = dp2px(mTagHorizontalSpacing);
|
||||
}
|
||||
tagItemView.setLayoutParams(params);
|
||||
|
||||
// 绑定单个 Tag 选中状态监听(同步到全局监听)
|
||||
tagItemView.setOnTagCheckedChangeListener(new TagItemView.OnTagCheckedChangeListener() {
|
||||
@Override
|
||||
public void onTagCheckedChanged(TagItemView tagItemView, TagModel tagModel, boolean isChecked) {
|
||||
// 触发全局监听,传递当前 Tag 位置、控件、数据
|
||||
if (mGlobalCheckedListener != null) {
|
||||
mGlobalCheckedListener.onTagCheckedChanged(position, tagItemView, tagModel, isChecked);
|
||||
}
|
||||
// 同步数据到列表(确保数据与视图一致)
|
||||
mTagList.set(position, tagModel);
|
||||
}
|
||||
});
|
||||
|
||||
// 将 Tag 控件添加到内部水平容器
|
||||
mTagContainer.addView(tagItemView);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前所有 Tag 数据列表(含最新选中状态)
|
||||
* @return List<TagModel> 完整数据列表
|
||||
*/
|
||||
public List<TagModel> getTagList() {
|
||||
return mTagList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有 Tag 数据和视图(避免内存泄漏)
|
||||
*/
|
||||
public void clearAllTags() {
|
||||
// 清空视图
|
||||
if (mTagContainer != null) {
|
||||
mTagContainer.removeAllViews();
|
||||
}
|
||||
// 清空数据
|
||||
if (mTagList != null) {
|
||||
mTagList.clear();
|
||||
}
|
||||
// 重置滚动位置到最左侧
|
||||
scrollTo(0, 0);
|
||||
LogUtils.d(TAG, "已清空所有 Tag 数据和视图");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Tag 项之间的水平间距(单位:dp,外部调用更直观)
|
||||
* @param spacingDp 水平间距(dp)
|
||||
*/
|
||||
public void setTagHorizontalSpacing(int spacingDp) {
|
||||
this.mTagHorizontalSpacing = spacingDp;
|
||||
// 重新布局(生效间距)
|
||||
if (mTagContainer != null) {
|
||||
mTagContainer.requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到指定位置的 Tag 项(水平平滑滚动)
|
||||
* @param position Tag 索引(从 0 开始)
|
||||
*/
|
||||
public void scrollToTag(int position) {
|
||||
// 校验索引有效性
|
||||
if (position < 0 || position >= mTagList.size() || mTagContainer == null) {
|
||||
LogUtils.d(TAG, "无效的 Tag 索引: " + position);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取目标 Tag 控件
|
||||
View targetTag = mTagContainer.getChildAt(position);
|
||||
if (targetTag == null) {
|
||||
LogUtils.e(TAG, "获取目标 Tag 控件失败,无法滚动");
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算滚动目标坐标(目标 Tag 左边界 - 容器左内边距,确保 Tag 左对齐显示)
|
||||
int scrollX = targetTag.getLeft() - mTagContainer.getPaddingLeft();
|
||||
// 平滑滚动到目标位置(Java 7 兼容,使用 ScrollView 原生方法)
|
||||
smoothScrollTo(scrollX, 0);
|
||||
|
||||
LogUtils.d(TAG, "已平滑滚动到 Tag 索引: " + position);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置滚动到最左侧
|
||||
*/
|
||||
public void resetScrollToStart() {
|
||||
smoothScrollTo(0, 0);
|
||||
LogUtils.d(TAG, "已重置滚动到最左侧");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全局 Tag 选中状态监听(供外部获取所有 Tag 变化)
|
||||
* @param listener 全局监听接口
|
||||
*/
|
||||
public void setOnTagCheckedChangeListener(OnTagCheckedChangeListener listener) {
|
||||
this.mGlobalCheckedListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局 Tag 选中状态监听接口
|
||||
* 携带索引、控件、数据、选中状态,便于外部批量处理
|
||||
*/
|
||||
public interface OnTagCheckedChangeListener {
|
||||
/**
|
||||
* 选中状态变化回调
|
||||
* @param position 当前 Tag 索引(从 0 开始)
|
||||
* @param tagItemView 当前 Tag 控件
|
||||
* @param tagModel 绑定的数据模型(含最新状态)
|
||||
* @param isChecked 最新选中状态
|
||||
*/
|
||||
void onTagCheckedChanged(int position, TagItemView tagItemView, TagModel tagModel, boolean isChecked);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具方法:dp 转 px(适配不同分辨率屏幕)
|
||||
* @param dpValue dp 值
|
||||
* @return 对应的 px 值
|
||||
*/
|
||||
private int dp2px(float dpValue) {
|
||||
if (dpValue <= 0) {
|
||||
return 0;
|
||||
}
|
||||
final float scale = getContext().getResources().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f); // 四舍五入,确保精度
|
||||
}
|
||||
|
||||
/**
|
||||
* 生命周期方法:控件销毁时清空资源,避免内存泄漏
|
||||
*/
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
clearAllTags();
|
||||
mGlobalCheckedListener = null; // 置空监听,避免内存泄漏
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package cc.winboll.studio.libappbase.model;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/28 20:36
|
||||
* @Describe Tag 数据模型
|
||||
* 存储 TagItemView 所需的核心数据:选中状态 + 标签名称
|
||||
*/
|
||||
public class TagModel {
|
||||
public static final String TAG = "TagModel";
|
||||
private boolean isChecked; // 选中状态(true:选中,false:未选中)
|
||||
private String tagName; // 标签名称(显示文本)
|
||||
|
||||
// Java 7 无参构造(便于外部实例化)
|
||||
public TagModel() {
|
||||
}
|
||||
|
||||
// Java 7 有参构造(快速初始化数据)
|
||||
public TagModel(boolean isChecked, String tagName) {
|
||||
this.isChecked = isChecked;
|
||||
this.tagName = tagName;
|
||||
}
|
||||
|
||||
// Getter/Setter 方法(Java 7 标准写法,无 lambda 简化)
|
||||
public boolean isChecked() {
|
||||
return isChecked;
|
||||
}
|
||||
|
||||
public void setChecked(boolean checked) {
|
||||
isChecked = checked;
|
||||
}
|
||||
|
||||
public String getTagName() {
|
||||
return tagName;
|
||||
}
|
||||
|
||||
public void setTagName(String tagName) {
|
||||
this.tagName = tagName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package cc.winboll.studio.libappbase.views;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen@AliYun.Com
|
||||
* @Date 2025/03/12 12:29:01
|
||||
* @Describe 水平布局的 ListView
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Scroller;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class HorizontalListView extends ListView {
|
||||
public static final String TAG = "HorizontalListView";
|
||||
private int verticalOffset = 0;
|
||||
private Scroller scroller;
|
||||
private int totalWidth;
|
||||
|
||||
public HorizontalListView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public HorizontalListView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public HorizontalListView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
scroller = new Scroller(getContext());
|
||||
setHorizontalScrollBarEnabled(true);
|
||||
setVerticalScrollBarEnabled(false);
|
||||
}
|
||||
|
||||
public void setVerticalOffset(int verticalOffset) {
|
||||
this.verticalOffset = verticalOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
int childCount = getChildCount();
|
||||
int left = getPaddingLeft();
|
||||
int viewHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
|
||||
totalWidth = left;
|
||||
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
View child = getChildAt(i);
|
||||
int width = child.getMeasuredWidth();
|
||||
int height = child.getMeasuredHeight();
|
||||
child.layout(left, verticalOffset, left + width, verticalOffset + height);
|
||||
left += width;
|
||||
}
|
||||
totalWidth = left + getPaddingRight();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
|
||||
int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
|
||||
super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void computeScroll() {
|
||||
if (scroller.computeScrollOffset()) {
|
||||
scrollTo(scroller.getCurrX(), scroller.getCurrY());
|
||||
postInvalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void smoothScrollTo(int x, int y) {
|
||||
int dx = x - getScrollX();
|
||||
int dy = y - getScrollY();
|
||||
scroller.startScroll(getScrollX(), getScrollY(), dx, dy, 300); // 300ms平滑动画
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeHorizontalScrollRange() {
|
||||
return totalWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeHorizontalScrollOffset() {
|
||||
return getScrollX();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int computeHorizontalScrollExtent() {
|
||||
return getWidth();
|
||||
}
|
||||
|
||||
public void scrollToItem(int position) {
|
||||
if (position < 0 || position >= getChildCount()) {
|
||||
LogUtils.d(TAG, "无效的position: " + position);
|
||||
return;
|
||||
}
|
||||
|
||||
View targetView = getChildAt(position);
|
||||
int targetLeft = targetView.getLeft();
|
||||
int scrollX = targetLeft - getPaddingLeft();
|
||||
|
||||
// 修正最大滚动范围计算
|
||||
int maxScrollX = totalWidth;
|
||||
scrollX = Math.max(0, Math.min(scrollX, maxScrollX));
|
||||
|
||||
// 强制重新布局和绘制
|
||||
requestLayout();
|
||||
invalidateViews();
|
||||
smoothScrollTo(scrollX, 0);
|
||||
LogUtils.d(TAG, String.format("滚动到position: %d, scrollX: %d computeHorizontalScrollRange() %d", position, scrollX, computeHorizontalScrollRange()));
|
||||
}
|
||||
|
||||
public void resetScrollToStart() {
|
||||
// 强制重新布局和绘制
|
||||
requestLayout();
|
||||
invalidateViews();
|
||||
smoothScrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package cc.winboll.studio.libappbase.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import cc.winboll.studio.libappbase.model.TagModel;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/28 20:37
|
||||
* @Describe Tag 单项展示控件(继承 CheckBox)
|
||||
* 单独承载单个 Tag 数据,关联 TagModel,实现选中状态+标签名称的联动展示
|
||||
*/
|
||||
public class TagItemView extends CheckBox {
|
||||
public static final String TAG = "TagItemView";
|
||||
|
||||
private TagModel mTagModel; // 绑定的 Tag 数据模型
|
||||
// 选中状态变化监听(供外部回调)
|
||||
private OnTagCheckedChangeListener mCheckedChangeListener;
|
||||
|
||||
// 构造方法(Java 7 完整兼容,覆盖所有重载)
|
||||
public TagItemView(Context context) {
|
||||
super(context);
|
||||
initView();
|
||||
}
|
||||
|
||||
public TagItemView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public TagItemView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initView();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化控件(默认样式 + 事件监听)
|
||||
*/
|
||||
private void initView() {
|
||||
// 初始化默认样式(可根据需求调整,如字体大小、内边距)
|
||||
setTextSize(14); // 标签字体大小
|
||||
setPadding(20, 10, 20, 10); // 内边距(避免文本贴边)
|
||||
|
||||
// 绑定 CheckBox 选中状态变化事件(联动 TagModel)
|
||||
setOnCheckedChangeListener(new OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
// 同步选中状态到数据模型
|
||||
if (mTagModel != null) {
|
||||
mTagModel.setChecked(isChecked);
|
||||
}
|
||||
// 触发外部监听回调
|
||||
if (mCheckedChangeListener != null) {
|
||||
mCheckedChangeListener.onTagCheckedChanged(TagItemView.this, mTagModel, isChecked);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定 Tag 数据模型(核心方法:数据驱动视图)
|
||||
* @param tagModel 单个 Tag 数据(含 isChecked + tagName)
|
||||
*/
|
||||
public void setTagModel(TagModel tagModel) {
|
||||
if (tagModel == null) {
|
||||
return;
|
||||
}
|
||||
this.mTagModel = tagModel;
|
||||
// 同步数据到视图:标签名称 + 选中状态
|
||||
setText(tagModel.getTagName());
|
||||
setChecked(tagModel.isChecked());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前绑定的 Tag 数据模型
|
||||
* @return TagModel (含最新选中状态和标签名称)
|
||||
*/
|
||||
public TagModel getTagModel() {
|
||||
return mTagModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选中状态变化监听(供外部获取选中事件)
|
||||
* @param listener 监听接口实例
|
||||
*/
|
||||
public void setOnTagCheckedChangeListener(OnTagCheckedChangeListener listener) {
|
||||
this.mCheckedChangeListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义监听接口:Tag 选中状态变化时回调
|
||||
* 携带当前控件、数据模型、选中状态,便于外部处理
|
||||
*/
|
||||
public interface OnTagCheckedChangeListener {
|
||||
/**
|
||||
* 选中状态变化回调
|
||||
* @param tagItemView 当前 Tag 控件
|
||||
* @param tagModel 绑定的数据模型(含最新状态)
|
||||
* @param isChecked 最新选中状态
|
||||
*/
|
||||
void onTagCheckedChanged(TagItemView tagItemView, TagModel tagModel, boolean isChecked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,10 +91,15 @@
|
||||
android:id="@+id/tagsearch_et"/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_border"
|
||||
android:scrollbars="none"
|
||||
android:padding="5dp"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/viewlogHorizontalScrollView1">
|
||||
|
||||
<cc.winboll.studio.libappbase.TagsHorizontalListView
|
||||
<cc.winboll.studio.libappbase.views.HorizontalListView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/tags_listview"/>
|
||||
|
||||
Reference in New Issue
Block a user