diff --git a/appbase/build.properties b/appbase/build.properties index f956dcc..c1f31c4 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat Dec 06 14:24:56 HKT 2025 +#Sat Dec 06 06:54:32 GMT 2025 stageCount=2 libraryProject=libappbase baseVersion=15.12 publishVersion=15.12.1 -buildCount=0 +buildCount=2 baseBetaVersion=15.12.2 diff --git a/appbase/src/main/AndroidManifest.xml b/appbase/src/main/AndroidManifest.xml index 284d7af..2b21bf8 100644 --- a/appbase/src/main/AndroidManifest.xml +++ b/appbase/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ - + - - + - + + + - + \ No newline at end of file diff --git a/appbase/src/main/java/cc/winboll/studio/appbase/App.java b/appbase/src/main/java/cc/winboll/studio/appbase/App.java index 888d581..b834044 100644 --- a/appbase/src/main/java/cc/winboll/studio/appbase/App.java +++ b/appbase/src/main/java/cc/winboll/studio/appbase/App.java @@ -2,6 +2,7 @@ package cc.winboll.studio.appbase; import cc.winboll.studio.libappbase.GlobalApplication; import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.libappbase.BuildConfig; /** * @Author ZhanGSKen @@ -21,6 +22,8 @@ public class App extends GlobalApplication { @Override public void onCreate() { super.onCreate(); // 调用父类初始化逻辑(如基础库配置、全局上下文设置) + //setIsDebugging(false); + setIsDebugging(BuildConfig.DEBUG); // 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用) ToastUtils.init(getApplicationContext()); } diff --git a/libappbase/build.properties b/libappbase/build.properties index 6dc9053..c1f31c4 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat Dec 06 14:24:33 HKT 2025 +#Sat Dec 06 06:54:32 GMT 2025 stageCount=2 libraryProject=libappbase baseVersion=15.12 publishVersion=15.12.1 -buildCount=0 +buildCount=2 baseBetaVersion=15.12.2 diff --git a/libappbase/src/main/AndroidManifest.xml b/libappbase/src/main/AndroidManifest.xml index 61a602b..857b928 100644 --- a/libappbase/src/main/AndroidManifest.xml +++ b/libappbase/src/main/AndroidManifest.xml @@ -3,8 +3,9 @@ xmlns:android="http://schemas.android.com/apk/res/android" package="cc.winboll.studio.libappbase"> - + + - * @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 Map sTagEnableMap = new HashMap(); + 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); + // 初始化日志缓存文件路径(debug模式:外部存储) + _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); + // 初始化日志缓存文件路径(release模式:内部存储) + _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); + _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", sTagEnableMap.toString())); + // 加载当前应用下的所有类的 TAG + addClassTAGList(); + loadTAGBeanSettings(); + _IsInited = true; + LogUtils.d(TAG, String.format("mapTAGList : %s", mapTAGList.toString())); } - /** - * 获取 TAG 过滤映射表(外部可通过此方法获取所有 TAG 及其启用状态) - * @return TAG 名称与启用状态的映射 - */ - public static Map getTagEnableMap() { - return sTagEnableMap; + 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(避免无效配置) - if (sTagEnableMap.containsKey(tag)) { - sTagEnableMap.put(tag, isEnable); + 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()); + } } } } - /** - * 保存当前 TAG 启用状态配置到文件 - * 将映射表中的 TAG 及其启用状态转换为 LogUtilsClassTAGBean 列表,持久化到文件 - */ - private static void saveTagEnableSettings() { - ArrayList tagSettingList = new ArrayList(); - // 遍历映射表,构建配置列表(Java 7 迭代器遍历) - Iterator> iterator = sTagEnableMap.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - tagSettingList.add(new LogUtilsClassTAGBean(entry.getKey(), entry.getValue())); + static void saveTAGBeanSettings() { + ArrayList list = new ArrayList(); + for (Map.Entry entry : mapTAGList.entrySet()) { + list.add(new LogUtilsClassTAGBean(entry.getKey(), entry.getValue())); } - // 保存配置列表到文件 - LogUtilsClassTAGBean.saveBeanList(sContext, tagSettingList, LogUtilsClassTAGBean.class); + LogUtilsClassTAGBean.saveBeanList(_mContext, list, LogUtilsClassTAGBean.class); } - /** - * 扫描应用内所有类的 TAG 并添加到过滤映射表 - * 1. 通过 DexFile 读取 APK 中所有类; - * 2. 过滤指定包名前缀(cc.winboll.studio)的类; - * 3. 反射获取类中 public static final String TAG 字段的值; - * 4. 将 TAG 加入映射表,默认禁用(false)。 - */ - private static void scanAllClassTags() { + static void addClassTAGList() { try { - // 应用 APK 路径(通过上下文获取) - String apkPath = sContext.getPackageCodePath(); - d(TAG, String.format("APK 路径:%s", apkPath)); + // 包名前缀(过滤当前应用的类) + String packageNamePrefix = "cc.winboll.studio"; + List classNames = new ArrayList<>(); + String apkPath = _mContext.getPackageCodePath(); + LogUtils.d(TAG, String.format("apkPath : %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); + 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); } } - // 打印扫描统计(调试用) - d(TAG, String.format("APK 总类数:%d,目标包下类数:%d", totalClassCount, targetClassNames.size())); + LogUtils.d(TAG, String.format("countTemp : %d\nClassNames size : %d", countTemp, classNames.size())); - // 反射获取每个类的 TAG 字段(Java 7 增强 for 循环) - for (String className : targetClassNames) { + 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 + // 过滤静态、公共、String类型的 TAG 字段 + if (Modifier.isStatic(field.getModifiers()) + && Modifier.isPublic(field.getModifiers()) + && field.getType() == String.class && "TAG".equals(field.getName())) { - // 获取 TAG 字段的值(静态字段,传入 null 即可) String tagValue = (String) field.get(null); - // 添加到映射表,默认禁用 - sTagEnableMap.put(tagValue, false); + 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()); } } } catch (IOException e) { - // 捕获 APK 读取异常 - d(TAG, e, Thread.currentThread().getStackTrace()); + LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); } } - /** - * 设置单个 TAG 的启用状态 - * @param tag TAG 名称 - * @param isEnable 是否启用(true:输出该 TAG 的日志;false:不输出) - */ - public static void setTagEnable(String tag, boolean isEnable) { - // 遍历映射表,更新目标 TAG 的状态(Java 7 迭代器遍历) - Iterator> iterator = sTagEnableMap.entrySet().iterator(); + 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())) { @@ -243,395 +182,551 @@ public class LogUtils { break; } } - // 保存配置到文件(持久化) - saveTagEnableSettings(); - d(TAG, String.format("TAG 配置更新:%s", sTagEnableMap.toString())); + 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 迭代器遍历) - Iterator> iterator = sTagEnableMap.entrySet().iterator(); + public static void setALlTAGListEnable(boolean isEnable) { + Iterator> iterator = mapTAGList.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry entry = iterator.next(); entry.setValue(isEnable); } - // 保存配置到文件(持久化) - saveTagEnableSettings(); - d(TAG, String.format("所有 TAG 配置更新:%s", sTagEnableMap.toString())); + 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; - } - // TAG 未配置或未启用:不输出 - if (sTagEnableMap.get(tag) == null || !sTagEnableMap.get(tag)) { + } + // TAG 未配置或禁用时,不打印日志 + 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) { + // 当前日志级别 >= 目标级别时,允许打印(级别顺序:Off < Error < Warn < Info < Debug < Verbose) + return LogUtils._mLogUtilsBean.getLogLevel().ordinal() >= logLevel.ordinal(); } - /** - * 获取日志缓存文件夹路径(外部可通过此方法获取日志存储目录) - * @return 日志缓存文件夹 - */ + // + // 获取应用日志文件夹 + // public static File getLogCacheDir() { - return sLogCacheDir; + return _mfLogCacheDir; } + // ================================= 补全所有日志重载方法(Error 级别) ================================= /** - * 输出 Error 级别日志 - * @param tag TAG 名称 - * @param message 日志内容 + * Error 级别日志(仅消息) + * @param szTAG 标签 + * @param szMessage 日志消息 */ - 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()); - } - } - - /** - * 输出 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); - } - } - - /** - * 输出 Warn 级别日志(带异常信息和调用栈) - * 包含日志内容、异常详情、调用位置,便于警告场景下的问题定位 - * @param tag TAG 名称 - * @param message 日志内容 - * @param e 异常对象(存储异常信息和调用栈) - */ - public static void w(String tag, String message, Exception e) { - if (isLoggable(tag, LOG_LEVEL.Warn)) { - StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); - StringBuilder sb = new StringBuilder(message); - // 拼接异常信息(类型 + 消息) - sb.append(" \nException: ") - .append(e.getClass().getSimpleName()) - .append(" : ") - .append(e.getMessage() != null ? e.getMessage() : "无异常消息"); - // 拼接调用栈信息(stackTrace[2] 为实际调用处) - sb.append(" \nAt ") - .append(stackTrace[2].getMethodName()) - .append(" (") - .append(stackTrace[2].getFileName()) - .append(":") - .append(stackTrace[2].getLineNumber()) - .append(")"); - saveLog(tag, LOG_LEVEL.Warn, sb.toString()); - } - } - - /** - * 输出 Info 级别日志 - * @param tag TAG 名称 - * @param message 日志内容 - */ - public static void i(String tag, String message) { - if (isLoggable(tag, LOG_LEVEL.Info)) { - saveLog(tag, LOG_LEVEL.Info, message); - } - } - - /** - * 输出 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); - } - } - - /** - * 输出 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()); - } - } - - /** - * 输出 Debug 级别日志(带异常信息和调用栈) - * 包含异常类型、异常信息、调用位置,便于异常定位 - * @param tag TAG 名称 + * Error 级别日志(消息 + 异常) + * @param szTAG 标签 + * @param szMessage 日志消息 * @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()); + public static void e(String szTAG, String szMessage, Exception e) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Error)) { + StringBuilder sb = new StringBuilder(szMessage); + sb.append("\n【异常信息】: ").append(getExceptionInfo(e)); + saveLog(szTAG, LogUtils.LOG_LEVEL.Error, sb.toString()); } } /** - * 输出 Debug 级别日志(带日志内容+异常对象,简化调用) - * 无需手动传入调用栈,内部自动获取,适配常见调试场景 - * @param tag TAG 名称 - * @param message 日志内容 - * @param e 异常对象(存储异常信息和调用栈) + * Error 级别日志(仅异常) + * @param szTAG 标签 + * @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()); + public static void e(String szTAG, Exception e) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Error)) { + String message = "【异常信息】: " + getExceptionInfo(e); + saveLog(szTAG, LogUtils.LOG_LEVEL.Error, message); } } /** - * 输出 Verbose 级别日志(最详细级别) - * @param tag TAG 名称 - * @param message 日志内容 + * Error 级别日志(消息 + 异常 + 堆栈) + * @param szTAG 标签 + * @param szMessage 日志消息 + * @param e 异常对象 + * @param listStackTrace 堆栈信息 */ - public static void v(String tag, String message) { - if (isLoggable(tag, LOG_LEVEL.Verbose)) { - saveLog(tag, LOG_LEVEL.Verbose, message); + public static void e(String szTAG, String szMessage, Exception e, StackTraceElement[] listStackTrace) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Error)) { + StringBuilder sb = new StringBuilder(szMessage); + sb.append("\n【异常信息】: ").append(getExceptionInfo(e)); + sb.append("\n【堆栈信息】: ").append(getStackTraceInfo(listStackTrace)); + saveLog(szTAG, LogUtils.LOG_LEVEL.Error, sb.toString()); + } + } + + // ================================= 补全所有日志重载方法(Warn 级别) ================================= + /** + * Warn 级别日志(仅消息) + * @param szTAG 标签 + * @param szMessage 日志消息 + */ + public static void w(String szTAG, String szMessage) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Warn)) { + saveLog(szTAG, LogUtils.LOG_LEVEL.Warn, szMessage); } } /** - * 核心日志保存方法(将日志写入文件) - * 日志格式:[级别] [时间戳] [TAG] - * 日志内容 - * @param tag TAG 名称 + * Warn 级别日志(消息 + 异常) + * @param szTAG 标签 + * @param szMessage 日志消息 + * @param e 异常对象 + */ + public static void w(String szTAG, String szMessage, Exception e) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Warn)) { + StringBuilder sb = new StringBuilder(szMessage); + sb.append("\n【异常信息】: ").append(getExceptionInfo(e)); + saveLog(szTAG, LogUtils.LOG_LEVEL.Warn, sb.toString()); + } + } + + /** + * Warn 级别日志(仅异常) + * @param szTAG 标签 + * @param e 异常对象 + */ + public static void w(String szTAG, Exception e) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Warn)) { + String message = "【异常信息】: " + getExceptionInfo(e); + saveLog(szTAG, LogUtils.LOG_LEVEL.Warn, message); + } + } + + // ================================= 补全所有日志重载方法(Info 级别) ================================= + /** + * Info 级别日志(仅消息) + * @param szTAG 标签 + * @param szMessage 日志消息 + */ + public static void i(String szTAG, String szMessage) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Info)) { + saveLog(szTAG, LogUtils.LOG_LEVEL.Info, szMessage); + } + } + + /** + * Info 级别日志(消息 + 数据对象) + * @param szTAG 标签 + * @param szMessage 日志消息 + * @param obj 数据对象(自动转为字符串) + */ + public static void i(String szTAG, String szMessage, Object obj) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Info)) { + String objStr = obj == null ? "null" : obj.toString(); + StringBuilder sb = new StringBuilder(szMessage); + sb.append("\n【数据对象】: ").append(objStr); + saveLog(szTAG, LogUtils.LOG_LEVEL.Info, sb.toString()); + } + } + + // ================================= 补全所有日志重载方法(Debug 级别) ================================= + /** + * Debug 级别日志(仅消息) + * @param szTAG 标签 + * @param szMessage 日志消息 + */ + public static void d(String szTAG, String szMessage) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) { + saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, szMessage); + } + } + + /** + * Debug 级别日志(消息 + 堆栈) + * @param szTAG 标签 + * @param szMessage 日志消息 + * @param listStackTrace 堆栈信息 + */ + public static void d(String szTAG, String szMessage, StackTraceElement[] listStackTrace) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) { + StringBuilder sbMessage = new StringBuilder(szMessage); + sbMessage.append("\n【调用堆栈】: ").append(getStackTraceInfo(listStackTrace)); + saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sbMessage.toString()); + } + } + + /** + * Debug 级别日志(消息 + 异常) + * @param szTAG 标签 + * @param szMessage 日志消息 + * @param e 异常对象 + */ + public static void d(String szTAG, String szMessage, Exception e) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) { + StringBuilder sb = new StringBuilder(szMessage); + sb.append("\n【异常信息】: ").append(getExceptionInfo(e)); + saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString()); + } + } + + /** + * Debug 级别日志(异常 + 堆栈) + * @param szTAG 标签 + * @param e 异常对象 + * @param listStackTrace 堆栈信息 + */ + public static void d(String szTAG, Exception e, StackTraceElement[] listStackTrace) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) { + StringBuilder sbMessage = new StringBuilder(); + sbMessage.append("【异常信息】: ").append(getExceptionInfo(e)); + sbMessage.append("\n【调用堆栈】: ").append(getStackTraceInfo(listStackTrace)); + saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sbMessage.toString()); + } + } + + /** + * Debug 级别日志(消息 + 数据对象) + * @param szTAG 标签 + * @param szMessage 日志消息 + * @param obj 数据对象(自动转为字符串) + */ + public static void d(String szTAG, String szMessage, Object obj) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) { + String objStr = obj == null ? "null" : obj.toString(); + StringBuilder sb = new StringBuilder(szMessage); + sb.append("\n【数据对象】: ").append(objStr); + saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString()); + } + } + + /** + * Debug 级别日志(仅数据对象) + * @param szTAG 标签 + * @param obj 数据对象(自动转为字符串) + */ + public static void d(String szTAG, Object obj) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) { + String objStr = obj == null ? "null" : obj.toString(); + String message = "【数据对象】: " + objStr; + saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, message); + } + } + + // ================================= 补全所有日志重载方法(Verbose 级别) ================================= + /** + * Verbose 级别日志(仅消息) + * @param szTAG 标签 + * @param szMessage 日志消息 + */ + public static void v(String szTAG, String szMessage) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Verbose)) { + saveLog(szTAG, LogUtils.LOG_LEVEL.Verbose, szMessage); + } + } + + /** + * Verbose 级别日志(消息 + 数据对象) + * @param szTAG 标签 + * @param szMessage 日志消息 + * @param obj 数据对象(自动转为字符串) + */ + public static void v(String szTAG, String szMessage, Object obj) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Verbose)) { + String objStr = obj == null ? "null" : obj.toString(); + StringBuilder sb = new StringBuilder(szMessage); + sb.append("\n【数据对象】: ").append(objStr); + saveLog(szTAG, LogUtils.LOG_LEVEL.Verbose, sb.toString()); + } + } + + /** + * Verbose 级别日志(仅数据对象) + * @param szTAG 标签 + * @param obj 数据对象(自动转为字符串) + */ + public static void v(String szTAG, Object obj) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Verbose)) { + String objStr = obj == null ? "null" : obj.toString(); + String message = "【数据对象】: " + objStr; + saveLog(szTAG, LogUtils.LOG_LEVEL.Verbose, message); + } + } + + // ================================= 新增:通用日志工具方法(补充调试能力) ================================= + /** + * 打印当前线程信息(Debug 级别) + * @param szTAG 标签 + * @param szMessage 日志消息 + */ + public static void printThreadInfo(String szTAG, String szMessage) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug)) { + Thread currentThread = Thread.currentThread(); + StringBuilder sb = new StringBuilder(szMessage); + sb.append("\n【线程信息】: ") + .append("线程名=").append(currentThread.getName()) + .append(", 线程ID=").append(currentThread.getId()) + .append(", 线程状态=").append(currentThread.getState().name()); + saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString()); + } + } + + /** + * 打印 Map 数据(Debug 级别,格式化输出,便于查看) + * @param szTAG 标签 + * @param szMessage 日志消息 + * @param map 要打印的 Map 数据 + */ + public static void printMap(String szTAG, String szMessage, Map map) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug) && map != null) { + StringBuilder sb = new StringBuilder(szMessage); + sb.append("\n【Map 数据】(size=").append(map.size()).append("):"); + for (Map.Entry entry : map.entrySet()) { + String keyStr = entry.getKey() == null ? "null" : entry.getKey().toString(); + String valueStr = entry.getValue() == null ? "null" : entry.getValue().toString(); + sb.append("\n ").append(keyStr).append(" = ").append(valueStr); + } + saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString()); + } + } + + /** + * 打印 List 数据(Debug 级别,格式化输出) + * @param szTAG 标签 + * @param szMessage 日志消息 + * @param list 要打印的 List 数据 + */ + public static void printList(String szTAG, String szMessage, List list) { + if (isLoggable(szTAG, LogUtils.LOG_LEVEL.Debug) && list != null) { + StringBuilder sb = new StringBuilder(szMessage); + sb.append("\n【List 数据】(size=").append(list.size()).append("):"); + for (int i = 0; i < list.size(); i++) { + T item = list.get(i); + String itemStr = item == null ? "null" : item.toString(); + sb.append("\n 索引").append(i).append(" = ").append(itemStr); + } + saveLog(szTAG, LogUtils.LOG_LEVEL.Debug, sb.toString()); + } + } + + // ================================= 私有工具方法(异常/堆栈信息格式化) ================================= + /** + * 格式化异常信息(提取异常类型、消息、简化堆栈) + * @param e 异常对象 + * @return 格式化后的异常字符串 + */ + private static String getExceptionInfo(Exception e) { + if (e == null) { + return "异常对象为null"; + } + StringBuilder sb = new StringBuilder(); + // 异常类型 + 异常消息 + sb.append(e.getClass().getSimpleName()).append(" : ").append(e.getMessage() == null ? "无异常消息" : e.getMessage()); + // 简化堆栈(取前5行,避免日志过长) + StackTraceElement[] stackTrace = e.getStackTrace(); + if (stackTrace != null && stackTrace.length > 0) { + sb.append("\n 简化堆栈(前5行):"); + int limit = Math.min(stackTrace.length, 5); + for (int i = 0; i < limit; i++) { + sb.append("\n ").append(stackTrace[i].toString()); + } + } + return sb.toString(); + } + + /** + * 格式化堆栈信息(提取关键调用链路) + * @param stackTrace 堆栈数组 + * @return 格式化后的堆栈字符串 + */ + private static String getStackTraceInfo(StackTraceElement[] stackTrace) { + if (stackTrace == null || stackTrace.length == 0) { + return "堆栈信息为空"; + } + StringBuilder sb = new StringBuilder(); + // 过滤 LogUtils 内部调用,取真实业务调用链路(前8行) + int count = 0; + for (StackTraceElement element : stackTrace) { + // 跳过 LogUtils 自身的堆栈(避免冗余) + if (element.getClassName().contains("cc.winboll.studio.libappbase.LogUtils")) { + continue; + } + sb.append("\n ").append(element.getClassName()).append(".") + .append(element.getMethodName()).append("(") + .append(element.getFileName()).append(":").append(element.getLineNumber()).append(")"); + count++; + if (count >= 8) { // 限制堆栈长度,避免日志过大 + break; + } + } + return sb.toString(); + } + + // ================================= 原有核心方法(保留并优化) ================================= + /** + * 日志文件保存函数(优化:增加异常捕获完整性,避免流泄漏) + * @param szTAG 标签 * @param logLevel 日志级别 - * @param message 日志内容 + * @param szMessage 日志消息 */ - 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) { + BufferedWriter out = null; 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); + // 确保日志文件存在(创建父目录 + 文件) + if (!_mfLogCatchFile.exists()) { + File parentDir = _mfLogCatchFile.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs(); + } + _mfLogCatchFile.createNewFile(); + } + // 追加写入日志(UTF-8编码,避免中文乱码) + out = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(_mfLogCatchFile, true), "UTF-8")); + String logLine = "[" + logLevel + "] " + + mSimpleDateFormat.format(System.currentTimeMillis()) + + " [" + szTAG + "]\n" + + szMessage + "\n\n"; // 增加空行,区分不同日志 + out.write(logLine); + out.flush(); // 强制刷新,确保日志及时写入 } catch (IOException e) { - // 日志写入失败时,输出内部调试日志 - d(TAG, "日志写入失败:" + (e.getMessage() != null ? e.getMessage() : "未知错误")); + // 日志写入失败时,打印系统日志(避免递归调用) + android.util.Log.e(TAG, "日志写入失败: " + e.getMessage()); } finally { - // 关闭流,避免资源泄漏(Java 7 手动关闭,不使用 try-with-resources) - if (writer != null) { + // 关闭流,避免资源泄漏(Java 7 手动关闭,无 try-with-resources) + if (out != null) { try { - writer.close(); + out.close(); } catch (IOException e) { - e.printStackTrace(); + android.util.Log.e(TAG, "流关闭失败: " + e.getMessage()); } } } } /** - * 加载历史日志(读取日志文件所有内容) - * @return 历史日志字符串(空字符串表示文件不存在或读取失败) + * 历史日志加载函数(优化:增加流关闭,避免内存泄漏) + * @return 日志内容字符串(空串表示无日志) */ public static String loadLog() { - // 日志文件不存在,返回空 - if (sLogFile == null || !sLogFile.exists()) { - return ""; + if (_mfLogCatchFile == null || !_mfLogCatchFile.exists()) { + return "日志文件不存在"; } - - StringBuilder logContent = new StringBuilder(); - BufferedReader reader = null; + StringBuffer sb = new StringBuffer(); + BufferedReader in = 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"); + 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) { - // 读取失败时,输出内部调试日志 - d(TAG, "日志读取失败:" + (e.getMessage() != null ? e.getMessage() : "未知错误")); + sb.append("日志加载失败: ").append(e.getMessage()); + LogUtils.e(TAG, "日志加载异常", e); } finally { // 关闭流,避免资源泄漏 - if (reader != null) { + if (in != null) { try { - reader.close(); + in.close(); } catch (IOException e) { - e.printStackTrace(); + LogUtils.e(TAG, "流关闭异常", e); } } } - return logContent.toString(); + return sb.toString(); } /** - * 清理历史日志(清空日志文件内容) + * 清理日志函数(优化:支持清空日志,避免文件过大) */ public static void cleanLog() { - if (sLogFile == null || !sLogFile.exists()) { + if (_mfLogCatchFile == null) { + LogUtils.d(TAG, "日志文件未初始化,无需清理"); return; } - try { - // 写入空字符串到文件,实现清空(Java 7 手动处理流) - BufferedWriter writer = new BufferedWriter( - new OutputStreamWriter( - new FileOutputStream(sLogFile), - "UTF-8" - ) - ); - writer.write(""); - writer.close(); + // 清空文件内容(覆盖写入空字符串) + BufferedWriter out = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(_mfLogCatchFile, false), "UTF-8")); + out.write(""); + out.flush(); + out.close(); + LogUtils.d(TAG, "日志已清空,文件路径: " + _mfLogCatchFile.getPath()); } catch (IOException e) { - // 清空失败时,输出内部调试日志(带调用栈) - d(TAG, e, Thread.currentThread().getStackTrace()); + LogUtils.e(TAG, "日志清空失败", e); } } /** - * 辅助方法:创建文件夹(不存在则创建) - * @param dir 目标文件夹 + * 检查日志文件大小(新增:避免日志文件过大占用内存) + * @param maxSizeMB 最大允许大小(MB) + * @return true:文件超过限制;false:文件大小正常 */ - private static void createDirIfNotExists(File dir) { - if (dir != null && !dir.exists()) { - dir.mkdirs(); + public static boolean checkLogFileSize(int maxSizeMB) { + if (_mfLogCatchFile == null || !_mfLogCatchFile.exists()) { + return false; } + // 转换为字节(1MB = 1024*1024 字节) + long maxSizeByte = maxSizeMB * 1024 * 1024; + long fileSize = _mfLogCatchFile.length(); + LogUtils.d(TAG, String.format("日志文件大小: %.2f MB(限制: %d MB)", + fileSize / (1024.0 * 1024), maxSizeMB)); + return fileSize > maxSizeByte; + } + + /** + * 初始化检查(新增:快速判断 LogUtils 是否初始化完成) + * @return true:已初始化;false:未初始化 + */ + public static boolean isInited() { + return _IsInited; + } + + /** + * 显示短提示(新增:日志+Toast联动,调试时快速提示) + * @param context 上下文 + * @param message 提示内容 + */ + public static void showShortToast(final Context context, final String message) { + if (context == null || message == null) { + return; + } + // 主线程显示Toast,避免子线程崩溃 + if (Thread.currentThread().getId() == android.os.Process.myTid()) { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + } else { + ((android.app.Activity) context).runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + } + }); + } + // 同时写入日志 + LogUtils.d(TAG, "Toast提示: " + message); } } 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 af50d0f..345489f 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,6 +26,9 @@ 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; @@ -28,49 +36,27 @@ import java.util.Comparator; import java.util.List; import java.util.Map; -/** - * @Author ZhanGSKen - * @Date 2024/08/12 14:36:18 - * @Describe 日志可视化自定义 View(继承 RelativeLayout) - * 核心功能:日志展示、日志级别筛选、TAG 过滤(启用/禁用)、TAG 搜索定位、日志清理/复制、视图交互控制 - * 依赖 LogUtils 进行日志读写,通过 LogViewThread 监听日志文件变化并自动刷新 - */ public class LogView extends RelativeLayout { - /** 当前 View 的日志 TAG(用于调试输出) */ public static final String TAG = "LogView"; - /** 日志处理中标志(避免并发刷新,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 HorizontalListView mTagHorizontalListView; + 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); @@ -91,307 +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); - mTagHorizontalListView = rootView.findViewById(R.id.tags_listview); - } - - /** - * 设置 TAG 搜索输入框监听(文本变化时触发 TAG 定位) - */ - private void setupTagSearchListener() { - mTagSearchEt.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - String searchText = s.toString().trim(); - LogUtils.d(TAG, "TAG 搜索内容:" + searchText); - if (!searchText.isEmpty()) { - // 搜索文本非空,定位匹配的 TAG - scrollToTargetTag(searchText); - } else { - // 搜索文本为空,重置滚动位置 - HorizontalScrollView parentHs = findViewById(R.id.viewlogHorizontalScrollView1); - parentHs.smoothScrollTo(0, 0); - mTagHorizontalListView.resetScrollToStart(); - } - } - - @Override - public void afterTextChanged(Editable s) {} - }); - } - - /** - * 设置功能按钮监听(清理日志、复制日志) - */ - private void setupFunctionButtonListeners(View rootView) { - // 清理日志按钮(点击清空所有历史日志) - rootView.findViewById(R.id.viewlogButtonClean).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.cleanLog(); - LogUtils.d(TAG, "日志已清理"); - } - }); - - // 复制日志按钮(点击复制所有日志到剪贴板) - rootView.findViewById(R.id.viewlogButtonCopy).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); - // 将日志内容复制到剪贴板(标签为应用包名) - clipboard.setPrimaryClip(ClipData.newPlainText(mContext.getPackageName(), LogUtils.loadLog())); - LogUtils.d(TAG, "日志已复制到剪贴板"); - } - }); - } - - /** - * 设置文本选择开关监听(控制日志文本是否可选中复制) - */ - private void setupTextSelectableListener() { - mTextSelectableCb.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mTextSelectableCb.isChecked()) { - // 允许文本选择:子视图优先获取焦点 - setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); - } else { - // 禁止文本选择:阻止子视图获取焦点 - setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); - } - } - }); - } - - /** - * 初始化日志级别下拉框(Spinner) - * 1. 绑定 LogUtils.LOG_LEVEL 枚举数据; - * 2. 设置默认选中当前全局日志级别; - * 3. 监听级别变化,更新 LogUtils 全局配置。 - */ - private void initLogLevelSpinner() { - // 从资源文件加载日志级别数组(R.array.enum_loglevel_array 与 LOG_LEVEL 枚举对应) - mLogLevelAdapter = ArrayAdapter.createFromResource( - mContext, R.array.enum_loglevel_array, android.R.layout.simple_spinner_item); - // 设置下拉列表样式 - mLogLevelAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - mLogLevelSpinner.setAdapter(mLogLevelAdapter); - - // 监听下拉框选择变化 - mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - // 根据选择的位置设置全局日志级别(position 与 LOG_LEVEL 枚举索引对应) - LogUtils.setLogLevel(LogUtils.LOG_LEVEL.values()[position]); - } - - @Override - public void onNothingSelected(AdapterView parent) {} - }); - - // 设置默认选中当前日志级别 - int defaultLevelIndex = LogUtils.getLogLevel().ordinal(); - if (defaultLevelIndex >= 0) { - mLogLevelSpinner.setSelection(defaultLevelIndex); - } - } - - /** - * 初始化 TAG 水平列表 - * 1. 加载 LogUtils 中的所有 TAG 及其启用状态; - * 2. 初始化 TAG 列表适配器; - * 3. 设置全选 TAG 开关监听。 - */ - private void initTagListView() { - // 获取 LogUtils 中的 TAG 启用状态映射表 - Map tagEnableMap = LogUtils.getTagEnableMap(); - // 判断是否所有 TAG 都已启用(初始化全选开关状态) - boolean isAllTagEnabled = isAllTagsEnabled(tagEnableMap); - mSelectAllTagCb.setChecked(isAllTagEnabled); - - // 初始化 TAG 水平列表(设置垂直偏移,绑定适配器) - mTagHorizontalListView.setVerticalOffset(10); - mTagListAdapter = new TAGListAdapter(mContext, tagEnableMap); - mTagHorizontalListView.setAdapter(mTagListAdapter); - mTagListAdapter.notifyDataSetChanged(); // 刷新列表数据 - - // 全选 TAG 开关监听(点击时启用/禁用所有 TAG) - mSelectAllTagCb.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - boolean isSelectAll = mSelectAllTagCb.isChecked(); - LogUtils.setAllTagsEnable(isSelectAll); // 批量更新所有 TAG 状态 - mTagListAdapter.reload(); // 重新加载 TAG 数据并刷新视图 - } - }); - } - - /** - * 判断是否所有 TAG 都已启用 - * @param tagEnableMap TAG 启用状态映射表 - * @return true:所有 TAG 均启用;false:存在未启用的 TAG - */ - private boolean isAllTagsEnabled(Map tagEnableMap) { - for (Map.Entry entry : tagEnableMap.entrySet()) { - if (!entry.getValue()) { - return false; - } - } - return true; - } - - /** - * 更新日志视图(由 LogViewThread 触发,通知有新日志) - * 避免并发刷新:正在处理时标记新日志,处理完成后再次刷新 - */ public void updateLogView() { - if (mLogViewHandler.isHandling()) { - // 正在处理日志刷新,标记有新日志待处理 + 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 (mTagListAdapter == null || 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 = mTagListAdapter.getItemList(); - mTagHorizontalListView.post(new Runnable() { - @Override - public void run() { - int targetPosition = -1; - // 遍历 TAG 列表,查找前缀匹配的 TAG(忽略大小写) - for (int i = 0; i < tagItemList.size(); i++) { - String tag = tagItemList.get(i).getTag(); - if (tag != null && tag.toLowerCase().startsWith(prefix.toLowerCase())) { - targetPosition = i; - break; - } - } + final List itemList = mTAGListAdapter.getItemList(); - if (targetPosition != -1) { - final int targetPositionFinal = targetPosition; - // 延迟滚动(确保布局完成,避免滚动失效) - mTagHorizontalListView.postDelayed(new Runnable() { - @Override - public void run() { - LogUtils.d(TAG, "定位到 TAG 位置:" + targetPositionFinal); - mTagHorizontalListView.scrollToItem(targetPositionFinal); - } - }, 100); - } else { - LogUtils.d(TAG, "未找到匹配前缀的 TAG:" + prefix); - } - } - }); + 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); @@ -414,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) { @@ -463,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 { @@ -481,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/ToastUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java index f4586ff..a69fc37 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java @@ -121,6 +121,18 @@ public class ToastUtils { LogUtils.d(TAG, "ToastUtils 初始化完成,上下文已设置"); } + // ===================================== 新增:isInited() 方法 ===================================== + /** + * 判断 ToastUtils 是否已初始化(供外部调用,如 CrashHandleNotifyUtils 中的复制提示) + * @return true:已初始化(可正常显示吐司);false:未初始化/已释放(无法正常显示) + */ + public static boolean isInited() { + ToastUtils instance = getInstance(); + // 双重校验:1. 未释放 2. 上下文已设置(确保初始化完成) + return !instance.isReleased && instance.mContext != null; + } + // ===================================== 新增结束 ===================================== + /** * 外部接口:显示短时长吐司 * @param message 吐司内容 @@ -243,7 +255,6 @@ public class ToastUtils { instance.mWorkerThread.join(1000); } catch (InterruptedException e) { LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - //LogUtils.e(TAG, "线程退出异常", e); Thread.currentThread().interrupt(); } instance.mWorkerThread = null; diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java new file mode 100644 index 0000000..00e3842 --- /dev/null +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/CrashHandleNotifyUtils.java @@ -0,0 +1,264 @@ +package cc.winboll.studio.libappbase.utils; + +import android.app.Application; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.text.TextUtils; + +import cc.winboll.studio.libappbase.CrashHandler; +import cc.winboll.studio.libappbase.GlobalCrashActivity; +import cc.winboll.studio.libappbase.LogUtils; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/29 21:12 + * @Describe 应用崩溃处理通知实用工具集(类库兼容版) + * 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志 + * 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用 + */ +public class CrashHandleNotifyUtils { + + public static final String TAG = "CrashHandleNotifyUtils"; + + /** 通知渠道ID(Android 8.0+ 必须) */ + private static final String CRASH_NOTIFY_CHANNEL_ID = "crash_notify_channel"; + /** 通知渠道名称(用户可见) */ + private static final String CRASH_NOTIFY_CHANNEL_NAME = "应用崩溃通知"; + /** 通知ID(唯一) */ + public static final int CRASH_NOTIFY_ID = 0x001; + /** Android 12 对应 API 版本号(31) */ + private static final int API_LEVEL_ANDROID_12 = 31; + /** PendingIntent.FLAG_IMMUTABLE 常量值(API 31+) */ + private static final int FLAG_IMMUTABLE = 0x00000040; + + /** 通知内容最大行数(控制在3行,超出部分省略) */ + private static final int NOTIFICATION_MAX_LINES = 3; + + + /** + * 处理未捕获异常(核心方法,类库入口) + * 改进点:新增宿主包名参数,移除类库对固定包名的依赖 + * @param hostApp 宿主应用的 Application 实例(用于获取宿主上下文) + * @param hostPackageName 宿主应用的包名(关键:用于绑定意图、匹配 Activity) + * @param errorLog 崩溃日志(从宿主 CrashHandler 传递过来) + */ + public static void handleUncaughtException(Application hostApp, String hostPackageName, String errorLog) { + // 1. 校验核心参数(类库场景必须严格校验,避免空指针) + if (hostApp == null || TextUtils.isEmpty(hostPackageName) || TextUtils.isEmpty(errorLog)) { + LogUtils.e(TAG, "发送崩溃通知失败:参数为空(hostApp=" + hostApp + ", hostPackageName=" + hostPackageName + ", errorLog=" + errorLog + ")"); + return; + } + + // 2. 获取宿主应用名称(使用宿主上下文,避免类库包名混淆) + String hostAppName = getHostAppName(hostApp, hostPackageName); + + // 3. 发送崩溃通知到宿主通知栏(点击跳转宿主的 GlobalCrashActivity) + sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog); + } + + /** + * 重载方法:兼容原有调用逻辑(避免宿主集成时改动过大) + * 从 Intent 中提取崩溃日志和宿主包名,适配原有 CrashHandler 调用方式 + * @param hostApp 宿主应用的 Application 实例 + * @param intent 存储崩溃信息的意图(extra 中携带崩溃日志) + */ + public static void handleUncaughtException(Application hostApp, Intent intent) { + // 从意图中提取宿主包名(优先使用意图中携带的包名,无则用宿主 Application 包名) + String hostPackageName = intent.getStringExtra("EXTRA_HOST_PACKAGE_NAME"); + if (TextUtils.isEmpty(hostPackageName)) { + hostPackageName = hostApp.getPackageName(); + LogUtils.w(TAG, "意图中未携带宿主包名,使用 Application 包名:" + hostPackageName); + } + + // 从意图中提取崩溃日志(与 CrashHandler.EXTRA_CRASH_INFO 保持一致) + String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG); + + // 调用核心方法处理 + handleUncaughtException(hostApp, hostPackageName, errorLog); + } + + /** + * 获取宿主应用名称(类库场景适配:使用宿主包名获取,避免类库包名干扰) + * @param hostContext 宿主应用的上下文(Application 实例) + * @param hostPackageName 宿主应用的包名 + * @return 宿主应用名称(读取失败返回 "未知应用") + */ + private static String getHostAppName(Context hostContext, String hostPackageName) { + try { + // 用宿主包名获取宿主应用信息,确保获取的是宿主的应用名称(类库关键改进) + return hostContext.getPackageManager().getApplicationLabel( + hostContext.getPackageManager().getApplicationInfo(hostPackageName, 0) + ).toString(); + } catch (Exception e) { + LogUtils.e(TAG, "获取宿主应用名称失败(包名:" + hostPackageName + ")", e); + return "未知应用"; + } + } + + /** + * 发送崩溃通知到宿主系统通知栏(类库兼容版) + * 改进点:全程使用宿主上下文和宿主包名,避免类库包名依赖 + * @param hostContext 宿主应用的上下文(Application 实例) + * @param hostPackageName 宿主应用的包名 + * @param hostAppName 宿主应用的名称(用于通知标题) + * @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity) + */ + private static void sendCrashNotification(Context hostContext, String hostPackageName, String hostAppName, String errorLog) { + // 1. 获取宿主的通知管理器(使用宿主上下文,确保通知归属宿主应用) + NotificationManager notificationManager = (NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) { + LogUtils.e(TAG, "获取宿主 NotificationManager 失败(包名:" + hostPackageName + ")"); + return; + } + + // 2. 适配 Android 8.0+(API 26+):创建宿主的通知渠道(归属宿主,避免类库渠道冲突) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createCrashNotifyChannel(hostContext, notificationManager); + } + + // 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity) + PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog); + if (jumpIntent == null) { + LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")"); + return; + } + + // 4. 构建通知实例(使用宿主上下文,确保通知资源归属宿主) + Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent); + + // 5. 发送通知(归属宿主应用,避免类库与宿主通知混淆) + notificationManager.notify(CRASH_NOTIFY_ID, notification); + LogUtils.d(TAG, "崩溃通知发送成功(宿主包名:" + hostPackageName + ",标题:" + hostAppName + ",日志长度:" + errorLog.length() + "字符)"); + } + + /** + * 创建宿主应用的崩溃通知渠道(类库场景:渠道归属宿主,避免类库与宿主渠道冲突) + * @param hostContext 宿主应用的上下文 + * @param notificationManager 宿主的通知管理器 + */ + private static void createCrashNotifyChannel(Context hostContext, NotificationManager notificationManager) { + // 仅 Android 8.0+ 执行(避免低版本报错) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // 构建通知渠道(归属宿主应用,描述明确类库用途) + android.app.NotificationChannel channel = new android.app.NotificationChannel( + CRASH_NOTIFY_CHANNEL_ID, + CRASH_NOTIFY_CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT + ); + channel.setDescription("应用崩溃通知(由 WinBoLL Studio 类库提供,点击查看详情)"); + // 注册渠道到宿主的通知管理器,确保渠道归属宿主 + notificationManager.createNotificationChannel(channel); + LogUtils.d(TAG, "宿主崩溃通知渠道创建成功(宿主包名:" + hostContext.getPackageName() + ",渠道ID:" + CRASH_NOTIFY_CHANNEL_ID + ")"); + } + } + + /** + * 核心改进:构建跳转宿主 GlobalCrashActivity 的意图(类库关键) + * 1. 绑定宿主包名,确保类库能正确启动宿主的 Activity; + * 2. 传递崩溃日志,与宿主 GlobalCrashActivity 日志接收逻辑匹配; + * 3. 使用宿主上下文,避免类库上下文导致的适配问题。 + * @param hostContext 宿主应用的上下文 + * @param hostPackageName 宿主应用的包名 + * @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity) + * @return 跳转崩溃详情页的 PendingIntent + */ + private static PendingIntent getGlobalCrashPendingIntent(Context hostContext, String hostPackageName, String errorLog) { + try { + // 1. 构建跳转宿主 GlobalCrashActivity 的显式意图(类库场景必须显式指定宿主包名) + Intent crashIntent = new Intent(hostContext, GlobalCrashActivity.class); + // 关键:绑定宿主包名,确保意图能正确匹配宿主的 Activity(避免类库包名干扰) + crashIntent.setPackage(hostPackageName); + // 传递崩溃日志(键:EXTRA_CRASH_INFO,与宿主 GlobalCrashActivity 完全匹配) + crashIntent.putExtra(CrashHandler.EXTRA_CRASH_LOG, errorLog); + // 设置意图标志:确保在宿主应用中正常启动,避免重复创建和任务栈混乱 + crashIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + // 2. 构建 PendingIntent(使用宿主上下文,适配高版本) + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) { + flags |= FLAG_IMMUTABLE; + } + + return PendingIntent.getActivity( + hostContext, + CRASH_NOTIFY_ID, // 用通知ID作为请求码,确保唯一(避免意图复用) + crashIntent, + flags + ); + } catch (Exception e) { + LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")", e); + return null; + } + } + + /** + * 构建通知实例(类库兼容版) + * 改进点:使用宿主上下文加载资源,确保通知样式适配宿主应用 + * @param hostContext 宿主应用的上下文 + * @param hostAppName 宿主应用的名称(通知标题) + * @param errorLog 崩溃日志(通知内容) + * @param jumpIntent 通知点击跳转意图(跳转宿主的 GlobalCrashActivity) + * @return 构建完成的 Notification 对象 + */ + private static Notification buildNotification(Context hostContext, String hostAppName, String errorLog, PendingIntent jumpIntent) { + // 兼容 Android 8.0+:指定宿主的通知渠道ID + Notification.Builder builder = new Notification.Builder(hostContext); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID); + } + + // 核心:用BigTextStyle控制“默认3行省略,下拉显示完整”(使用宿主上下文构建) + Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle(); + bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容"); + bigTextStyle.bigText(errorLog); + bigTextStyle.setBigContentTitle(hostAppName + " 崩溃"); // 标题明确标识宿主和崩溃状态 + builder.setStyle(bigTextStyle); + + // 配置通知核心参数(全程使用宿主上下文,确保资源归属宿主) + builder + // 关键:使用宿主应用的小图标(避免类库图标显示异常) + .setSmallIcon(hostContext.getApplicationInfo().icon) + .setContentTitle(hostAppName + " 崩溃") + .setContentText(getShortContent(errorLog)) // 3行内缩略文本 + .setContentIntent(jumpIntent) // 点击跳转宿主的 GlobalCrashActivity + .setAutoCancel(true) // 点击后自动关闭 + .setWhen(System.currentTimeMillis()) + .setPriority(Notification.PRIORITY_DEFAULT); + + // 适配 Android 4.1+:确保在宿主应用中正常显示 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return builder.build(); + } else { + return builder.getNotification(); + } + } + + /** + * 辅助方法:截取日志文本,确保显示在3行内(通用逻辑,无包名依赖) + * @param content 完整崩溃日志 + * @return 3行内的缩略文本 + */ + private static String getShortContent(String content) { + if (content == null || content.isEmpty()) { + return "无崩溃日志"; + } + int maxLength = 80; // 估算3行字符数(可根据需求调整) + return content.length() <= maxLength ? content : content.substring(0, maxLength) + "..."; + } + + /** + * 释放资源(类库场景:空实现,避免宿主调用时报错,预留扩展) + * @param hostContext 宿主应用的上下文(显式传入,避免类库上下文依赖) + */ + public static void release(Context hostContext) { + LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(宿主包名:" + (hostContext != null ? hostContext.getPackageName() : "未知") + ")"); + } +} 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 0000000..4e41357 --- /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/res/drawable/ic_content_copy.xml b/libappbase/src/main/res/drawable/ic_content_copy.xml new file mode 100644 index 0000000..0a8394f --- /dev/null +++ b/libappbase/src/main/res/drawable/ic_content_copy.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/libappbase/src/main/res/layout/view_log.xml b/libappbase/src/main/res/layout/view_log.xml index 77c1b56..cb03dba 100644 --- a/libappbase/src/main/res/layout/view_log.xml +++ b/libappbase/src/main/res/layout/view_log.xml @@ -99,7 +99,7 @@ android:layout_weight="1.0" android:id="@+id/viewlogHorizontalScrollView1"> - diff --git a/libappbase/src/main/res/values/colors.xml b/libappbase/src/main/res/values/colors.xml index 87d3836..3526edd 100644 --- a/libappbase/src/main/res/values/colors.xml +++ b/libappbase/src/main/res/values/colors.xml @@ -4,4 +4,5 @@ #FF005C12 #FF8DFFA2 #FFFFFB8D +