From 27deec8bf04f11cb32c2f47b35c803416ed3089e Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Sat, 29 Nov 2025 02:28:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8DLogView=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=88=B015.7.6=E7=89=88=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appbase/build.properties | 4 +- libappbase/build.properties | 4 +- .../winboll/studio/libappbase/LogUtils.java | 812 ++++++------------ .../cc/winboll/studio/libappbase/LogView.java | 778 +++++++---------- .../libappbase/TagsHorizontalListView.java | 240 ------ .../studio/libappbase/model/TagModel.java | 41 - .../libappbase/views/HorizontalListView.java | 129 +++ .../studio/libappbase/views/TagItemView.java | 107 --- libappbase/src/main/res/layout/view_log.xml | 11 +- 9 files changed, 717 insertions(+), 1409 deletions(-) delete mode 100644 libappbase/src/main/java/cc/winboll/studio/libappbase/TagsHorizontalListView.java delete mode 100644 libappbase/src/main/java/cc/winboll/studio/libappbase/model/TagModel.java create mode 100644 libappbase/src/main/java/cc/winboll/studio/libappbase/views/HorizontalListView.java delete mode 100644 libappbase/src/main/java/cc/winboll/studio/libappbase/views/TagItemView.java diff --git a/appbase/build.properties b/appbase/build.properties index b555e90f..c755271d 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -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 diff --git a/libappbase/build.properties b/libappbase/build.properties index b555e90f..c755271d 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -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 diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java index 572cf94e..a5de0115 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java @@ -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&豆包大模型 - * @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 sTagEnableTagModelList = new ArrayList(); + 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 mapTAGList = new HashMap(); - /** - * 初始化日志工具(默认日志级别: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 getTagEnableTagModelList() { - return sTagEnableTagModelList; + public static Map getMapTAGList() { + return mapTAGList; } - /** - * 加载已保存的 TAG 启用状态配置 - * 从 LogUtilsClassTAGBean 列表中读取每个 TAG 的启用状态,更新到映射表 - */ - private static void loadTagEnableSettings() { - ArrayList tagSettingList = new ArrayList(); - // 从文件加载 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 tagSettingList = new ArrayList(); - // 遍历映射表,构建配置列表(Java 7 迭代器遍历) - for (TagModel tagModel : sTagEnableTagModelList) { - tagSettingList.add(new LogUtilsClassTAGBean(tagModel.getTagName(), tagModel.isChecked())); - } - -// Iterator> iterator = sTagEnableTagModelList.entrySet().iterator(); -// while (iterator.hasNext()) { -// Map.Entry 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 classNames = dexFile.entries(); - - int totalClassCount = 0; // 总类数(调试用) - List targetClassNames = new ArrayList(); // 目标包名下的类名列表 - 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 list = new ArrayList(); + LogUtilsClassTAGBean.loadBeanList(_mContext, list, LogUtilsClassTAGBean.class); + for (int i = 0; i < list.size(); i++) { + LogUtilsClassTAGBean beanSetting = list.get(i); + for (Map.Entry 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 list = new ArrayList(); + for (Map.Entry 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 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 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> iterator = sTagEnableTagModelList.entrySet().iterator(); -// while (iterator.hasNext()) { -// Map.Entry 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> iterator = mapTAGList.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry 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> iterator = sTagEnableTagModelList.entrySet().iterator(); -// while (iterator.hasNext()) { -// Map.Entry entry = iterator.next(); -// entry.setValue(isEnable); -// } - // 保存配置到文件(持久化) - saveTagEnableSettings(); - d(TAG, String.format("所有 TAG 配置更新:%s", sTagEnableTagModelList.toString())); + public static void setALlTAGListEnable(boolean isEnable) { + Iterator> iterator = mapTAGList.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry 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()); + } } } } - diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java index ab171dca..345489fb 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java @@ -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 - * @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 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 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 adapterItems = new ArrayList<>(); + for (LogUtils.LOG_LEVEL e : LogUtils.LOG_LEVEL.values()) { + adapterItems.add(e.name()); + } + // 假设你有一个字符串数组作为选项列表 + //String[] options = {"Option 1", "Option 2", "Option 3"}; + // 创建一个ArrayAdapter来绑定数据到spinner + mLogLevelSpinnerAdapter = ArrayAdapter.createFromResource( + context, cc.winboll.studio.libappbase.R.array.enum_loglevel_array, android.R.layout.simple_spinner_item); + mLogLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + // 设置适配器并将它应用到spinner上 + mLogLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // 设置下拉视图样式 + mLogLevelSpinner.setAdapter(mLogLevelSpinnerAdapter); + // 为Spinner添加监听器 + mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + //String selectedOption = mLogLevelSpinnerAdapter.getItem(position); + // 处理选中的选项... + LogUtils.setLogLevel(LogUtils.LOG_LEVEL.values()[position]); + } + @Override + public void onNothingSelected(AdapterView parent) { + // 如果没有选择,则执行此操作... + } + }); + // 获取默认值的索引 + int defaultValueIndex = LogUtils.getLogLevel().ordinal(); + + if (defaultValueIndex != -1) { + // 如果找到了默认值,设置默认选项 + mLogLevelSpinner.setSelection(defaultValueIndex); + } + + // 加载标签列表 + Map mapTAGList = LogUtils.getMapTAGList(); + boolean isAllSelect = true; + for (Map.Entry entry : mapTAGList.entrySet()) { + if (entry.getValue() == false) { + isAllSelect = false; + break; + } + } + CheckBox cbALLTAG = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBox1); + cbALLTAG.setChecked(isAllSelect); + + // 加载标签表 + mListViewTags = findViewById(cc.winboll.studio.libappbase.R.id.tags_listview); + mListViewTags.setVerticalOffset(10); + mTAGListAdapter = new TAGListAdapter(mContext, mapTAGList); + mListViewTags.setAdapter(mTAGListAdapter); + + // 可以添加点击监听器来处理勾选框状态变化后的逻辑,比如获取当前勾选情况等 + mTAGListAdapter.notifyDataSetChanged(); + + mSelectAllTAGCheckBox = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBox1); + mSelectAllTAGCheckBox.setOnClickListener(new View.OnClickListener(){ + @Override + public void onClick(View v) { + LogUtils.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 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 tagEnableTagModelList) { - for(TagModel tagModel : tagEnableTagModelList) { - if(tagModel.isChecked()) { - return false; - } - } -// for (Map.Entry 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 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 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 originTagMap; - /** TAG 列表项数据(转换为 TAGItemModel 列表,便于排序和绑定) */ - private List tagItemList; + private Map mapOrigin; + private List itemList; - /** - * 构造方法(初始化数据并加载到列表) - * @param context 上下文 - * @param tagMap TAG 启用状态映射表 - */ - public TAGListAdapter(Context context, Map tagMap) { + public TAGListAdapter(Context context, Map map) { this.context = context; - this.originTagMap = tagMap; - loadTagData(originTagMap); // 加载并转换数据 + mapOrigin = map; + loadMap(mapOrigin); } - /** - * 获取 TAG 列表项数据(供外部定位 TAG 使用) - * @return TAGItemModel 列表 - */ public List 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 tagMap) { - tagItemList = new ArrayList<>(); - // 遍历 Map,转换为 TAGItemModel 并添加到列表 - for (Map.Entry entry : tagMap.entrySet()) { - tagItemList.add(new TAGItemModel(entry.getKey(), entry.getValue())); + void loadMap(Map map) { + itemList = new ArrayList(); + for (Map.Entry 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 { - /** 排序方向(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 { + 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()); } } } } - diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/TagsHorizontalListView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/TagsHorizontalListView.java deleted file mode 100644 index 78f30d1b..00000000 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/TagsHorizontalListView.java +++ /dev/null @@ -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&豆包大模型 - * @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 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 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 完整数据列表 - */ - public List 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; // 置空监听,避免内存泄漏 - } -} diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/model/TagModel.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/model/TagModel.java deleted file mode 100644 index 28927f86..00000000 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/model/TagModel.java +++ /dev/null @@ -1,41 +0,0 @@ -package cc.winboll.studio.libappbase.model; - -/** - * @Author ZhanGSKen&豆包大模型 - * @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; - } -} - diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/views/HorizontalListView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/views/HorizontalListView.java new file mode 100644 index 00000000..4e413579 --- /dev/null +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/views/HorizontalListView.java @@ -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); + } +} + diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/views/TagItemView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/views/TagItemView.java deleted file mode 100644 index 1094ad87..00000000 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/views/TagItemView.java +++ /dev/null @@ -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&豆包大模型 - * @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); - } -} - diff --git a/libappbase/src/main/res/layout/view_log.xml b/libappbase/src/main/res/layout/view_log.xml index 48291ede..cb03dba4 100644 --- a/libappbase/src/main/res/layout/view_log.xml +++ b/libappbase/src/main/res/layout/view_log.xml @@ -91,10 +91,15 @@ android:id="@+id/tagsearch_et"/> + 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"> -