From 8cfa83d025df24b94885e291b8cb549fc3cabaa0 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Fri, 28 Nov 2025 21:31:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=A8=A1=E5=9E=8B=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appbase/build.properties | 4 +- libappbase/build.properties | 4 +- .../studio/libappbase/GlobalApplication.java | 2 + .../studio/libappbase/HorizontalListView.java | 240 ------------------ .../winboll/studio/libappbase/LogUtils.java | 91 ++++--- .../cc/winboll/studio/libappbase/LogView.java | 74 +++--- .../libappbase/TagsHorizontalListView.java | 240 ++++++++++++++++++ .../studio/libappbase/model/TagModel.java | 41 +++ .../studio/libappbase/views/TagItemView.java | 107 ++++++++ libappbase/src/main/res/layout/view_log.xml | 11 +- 10 files changed, 501 insertions(+), 313 deletions(-) delete mode 100644 libappbase/src/main/java/cc/winboll/studio/libappbase/HorizontalListView.java create mode 100644 libappbase/src/main/java/cc/winboll/studio/libappbase/TagsHorizontalListView.java create 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/TagItemView.java diff --git a/appbase/build.properties b/appbase/build.properties index ca3c7825..b555e90f 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Fri Nov 21 11:41:04 HKT 2025 +#Fri Nov 28 13:30:32 GMT 2025 stageCount=2 libraryProject=libappbase baseVersion=15.11 publishVersion=15.11.1 -buildCount=0 +buildCount=9 baseBetaVersion=15.11.2 diff --git a/libappbase/build.properties b/libappbase/build.properties index ca3c7825..b555e90f 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Fri Nov 21 11:41:04 HKT 2025 +#Fri Nov 28 13:30:32 GMT 2025 stageCount=2 libraryProject=libappbase baseVersion=15.11 publishVersion=15.11.1 -buildCount=0 +buildCount=9 baseBetaVersion=15.11.2 diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java index 2c734afb..9fb80352 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java @@ -85,6 +85,7 @@ public class GlobalApplication extends Application { super.onCreate(); // 初始化单例实例(确保在所有初始化操作前完成) sInstance = this; + // 初始化基础组件(日志、崩溃处理、Toast) initCoreComponents(); @@ -169,6 +170,7 @@ public class GlobalApplication extends Application { // 释放单例引用(可选,避免内存泄漏风险) sInstance = null; LogUtils.d(TAG, "GlobalApplication 终止,单例实例已释放"); + } } diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/HorizontalListView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/HorizontalListView.java deleted file mode 100644 index 1a48fb61..00000000 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/HorizontalListView.java +++ /dev/null @@ -1,240 +0,0 @@ -package cc.winboll.studio.libappbase; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ListView; -import android.widget.Scroller; - -/** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/11/11 20:26 - * @Describe 水平滚动 ListView 控件 - * 继承自 ListView,重写布局和测量逻辑,实现子项水平排列和滚动,替代默认垂直布局 - */ -public class HorizontalListView extends ListView { - /** 日志标签,用于当前控件的日志输出标识 */ - public static final String TAG = "HorizontalListView"; - - /** 子项垂直偏移量(用于调整子项在垂直方向的位置,默认 0) */ - private int mVerticalOffset = 0; - /** 平滑滚动控制器(用于实现水平方向的平滑滚动动画) */ - private Scroller mScroller; - /** 所有子项总宽度(包含内边距),用于计算滚动范围 */ - private int mTotalWidth; - - /** - * 构造方法:仅上下文 - * @param context 上下文(Activity/Fragment) - */ - public HorizontalListView(Context context) { - super(context); - init(); - } - - /** - * 构造方法:上下文 + 自定义属性 - * @param context 上下文 - * @param attrs 自定义属性集合(如布局文件中设置的属性) - */ - public HorizontalListView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - /** - * 构造方法:上下文 + 自定义属性 + 样式属性 - * @param context 上下文 - * @param attrs 自定义属性集合 - * @param defStyle 样式属性(如系统默认样式) - */ - public HorizontalListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(); - } - - /** - * 初始化控件配置 - * 初始化滚动控制器,设置滚动条显示状态 - */ - private void init() { - // 初始化平滑滚动器(上下文为当前控件所在上下文) - mScroller = new Scroller(getContext()); - // 启用水平滚动条(默认显示) - setHorizontalScrollBarEnabled(true); - // 禁用垂直滚动条(水平列表无需垂直滚动) - setVerticalScrollBarEnabled(false); - } - - /** - * 设置子项垂直偏移量 - * 用于整体调整所有子项在垂直方向的位置(如居中、偏移) - * @param verticalOffset 垂直偏移像素值(正数向下偏移,负数向上偏移) - */ - public void setVerticalOffset(int verticalOffset) { - this.mVerticalOffset = verticalOffset; - } - - /** - * 重写布局方法:实现子项水平排列 - * 遍历所有子项,按水平方向依次布局(左对齐,叠加排列) - * @param changed 布局是否发生变化(true:首次布局或尺寸变化;false:重绘) - * @param l 控件左边界坐标 - * @param t 控件上边界坐标 - * @param r 控件右边界坐标 - * @param b 控件下边界坐标 - */ - @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(); - mTotalWidth = left; // 初始化总宽度为左内边距 - - // 遍历子项,水平排列 - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); // 获取当前子项 - int childWidth = child.getMeasuredWidth(); // 子项测量宽度 - int childHeight = child.getMeasuredHeight(); // 子项测量高度 - - // 布局子项:水平方向从 left 开始,垂直方向偏移 mVerticalOffset - child.layout( - left, // 子项左边界 - mVerticalOffset, // 子项上边界(带垂直偏移) - left + childWidth, // 子项右边界(左 + 宽度) - mVerticalOffset + childHeight // 子项下边界(偏移 + 高度) - ); - - left += childWidth; // 更新下一个子项的起始左坐标 - } - - // 计算总宽度(所有子项宽度 + 左右内边距) - mTotalWidth = left + getPaddingRight(); - } - - /** - * 重写测量方法:设置控件测量规则 - * 水平方向:允许无限宽度(适应所有子项总宽度);垂直方向:自适应内容高度 - * @param widthMeasureSpec 父控件传递的宽度测量规格 - * @param heightMeasureSpec 父控件传递的高度测量规格 - */ - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - // 重写宽度测量规则:最大值(Integer.MAX_VALUE >> 2 避免溢出),自适应内容 - int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); - // 重写高度测量规则:同上,自适应子项高度 - int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); - - // 执行父类测量逻辑(使用重写后的测量规格) - super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec); - } - - /** - * 重写滚动计算方法:实现平滑滚动 - * 配合 Scroller 实现水平方向的平滑滚动动画(需在滚动时调用 invalidate() 触发) - */ - @Override - public void computeScroll() { - // 判断滚动是否正在进行(Scroller 计算当前滚动位置) - if (mScroller.computeScrollOffset()) { - // 滚动到当前计算的位置(x 轴水平滚动,y 轴固定 0) - scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); - // 触发重绘,持续更新滚动状态 - postInvalidate(); - } - } - - /** - * 平滑滚动到指定坐标 - * 基于 Scroller 实现水平方向的平滑滚动(300ms 动画时长) - * @param x 目标 x 轴坐标(水平滚动位置) - * @param y 目标 y 轴坐标(固定 0,无需垂直滚动) - */ - public void smoothScrollTo(int x, int y) { - // 计算滚动距离(目标坐标 - 当前滚动坐标) - int dx = x - getScrollX(); - int dy = y - getScrollY(); - - // 启动平滑滚动:起始坐标(当前滚动位置)、滚动距离、动画时长(300ms) - mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 300); - // 触发重绘,启动滚动动画 - invalidate(); - } - - /** - * 计算水平滚动总范围(用于滚动条显示比例) - * @return 滚动总宽度(所有子项总宽度 + 内边距) - */ - @Override - public int computeHorizontalScrollRange() { - return mTotalWidth; - } - - /** - * 计算当前水平滚动偏移量(用于滚动条位置) - * @return 当前 x 轴滚动坐标 - */ - @Override - public int computeHorizontalScrollOffset() { - return getScrollX(); - } - - /** - * 计算水平滚动可视范围(用于滚动条大小) - * @return 控件可见宽度(当前显示区域宽度) - */ - @Override - public int computeHorizontalScrollExtent() { - return getWidth(); - } - - /** - * 滚动到指定位置的子项(水平方向) - * 定位目标子项,计算滚动坐标,执行平滑滚动 - * @param position 子项索引(从 0 开始,仅当前可见子项有效) - */ - public void scrollToItem(int position) { - // 校验索引有效性(避免数组越界) - if (position < 0 || position >= getChildCount()) { - LogUtils.d(TAG, "无效的子项索引: " + position); - return; - } - - View targetView = getChildAt(position); // 获取目标子项 - int targetLeft = targetView.getLeft(); // 目标子项左边界坐标 - // 计算目标滚动坐标(子项左边界 - 控件左内边距,确保子项左对齐显示) - int scrollX = targetLeft - getPaddingLeft(); - - // 修正滚动范围(避免超出总宽度或小于 0) - int maxScrollX = mTotalWidth - getWidth(); // 最大滚动坐标(总宽度 - 控件宽度) - scrollX = Math.max(0, Math.min(scrollX, maxScrollX)); - - // 强制重新布局和绘制(确保子项位置正确) - requestLayout(); - invalidateViews(); - // 平滑滚动到目标坐标 - smoothScrollTo(scrollX, 0); - - // 打印滚动日志(调试用) - LogUtils.d(TAG, String.format( - "滚动到子项索引: %d, 目标滚动X: %d, 总滚动范围: %d", - position, scrollX, computeHorizontalScrollRange() - )); - } - - /** - * 重置滚动到起始位置(最左侧) - * 强制重新布局后,平滑滚动到 x=0 坐标 - */ - public void resetScrollToStart() { - // 强制重新布局和绘制(确保滚动位置准确) - requestLayout(); - invalidateViews(); - // 平滑滚动到最左侧(x=0,y=0) - smoothScrollTo(0, 0); - } -} - 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 ca0f29df..572cf94e 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogUtils.java @@ -1,6 +1,7 @@ package cc.winboll.studio.libappbase; import android.content.Context; +import cc.winboll.studio.libappbase.model.TagModel; import dalvik.system.DexFile; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -15,7 +16,6 @@ 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; @@ -57,7 +57,7 @@ public class LogUtils { /** 日志配置实体类(封装日志级别等配置) */ private static LogUtilsBean sLogConfigBean; /** TAG 过滤映射表(key:TAG 名称;value:是否启用该 TAG 的日志输出) */ - public static Map sTagEnableMap = new HashMap(); + public static ArrayList sTagEnableTagModelList = new ArrayList(); /** * 初始化日志工具(默认日志级别:Off,不输出日志) @@ -115,15 +115,15 @@ public class LogUtils { sIsInited = true; // 打印初始化日志(调试用) - d(TAG, String.format("TAG 过滤映射表初始化完成:%s", sTagEnableMap.toString())); + d(TAG, String.format("TAG 过滤映射表初始化完成:%s", sTagEnableTagModelList.toString())); } /** * 获取 TAG 过滤映射表(外部可通过此方法获取所有 TAG 及其启用状态) * @return TAG 名称与启用状态的映射 */ - public static Map getTagEnableMap() { - return sTagEnableMap; + public static ArrayList getTagEnableTagModelList() { + return sTagEnableTagModelList; } /** @@ -140,9 +140,15 @@ public class LogUtils { String tag = tagSetting.getTag(); boolean isEnable = tagSetting.getEnable(); // 仅更新已存在的 TAG(避免无效配置) - if (sTagEnableMap.containsKey(tag)) { - sTagEnableMap.put(tag, isEnable); - } + for (TagModel tagModel : sTagEnableTagModelList) { + if (tagModel.equals(tag)) { + tagModel.setChecked(isEnable); + } + } +// if (sTagEnableTagModelList.containsKey(tag)) { +// sTagEnableTagModelList.put(tag, isEnable); +// } + } } @@ -153,11 +159,16 @@ public class LogUtils { 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())); - } + 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); } @@ -210,7 +221,8 @@ public class LogUtils { // 获取 TAG 字段的值(静态字段,传入 null 即可) String tagValue = (String) field.get(null); // 添加到映射表,默认禁用 - sTagEnableMap.put(tagValue, false); + sTagEnableTagModelList.add(new TagModel(false, tagValue)); +// sTagEnableTagModelList.put(tagValue, false); } } } catch (NoClassDefFoundError e) { @@ -235,17 +247,23 @@ public class LogUtils { */ public static void setTagEnable(String tag, boolean isEnable) { // 遍历映射表,更新目标 TAG 的状态(Java 7 迭代器遍历) - Iterator> iterator = sTagEnableMap.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - if (tag.equals(entry.getKey())) { - entry.setValue(isEnable); - break; - } - } + 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", sTagEnableMap.toString())); + d(TAG, String.format("TAG 配置更新:%s", sTagEnableTagModelList.toString())); } /** @@ -254,14 +272,17 @@ public class LogUtils { */ public static void setAllTagsEnable(boolean isEnable) { // 遍历映射表,批量更新所有 TAG 的状态(Java 7 迭代器遍历) - Iterator> iterator = sTagEnableMap.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - entry.setValue(isEnable); - } + 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", sTagEnableMap.toString())); + d(TAG, String.format("所有 TAG 配置更新:%s", sTagEnableTagModelList.toString())); } /** @@ -295,10 +316,20 @@ public class LogUtils { if (!sIsInited) { 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 (sTagEnableMap.get(tag) == null || !sTagEnableMap.get(tag)) { + if (!isTagExist) { return false; } + // 日志级别未达到:不输出 if (!isLevelMatched(logLevel)) { return false; 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 af50d0fa..ab171dca 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/LogView.java @@ -27,6 +27,7 @@ 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 @@ -58,7 +59,7 @@ public class LogView extends RelativeLayout { /** 全选 TAG 开关(控制所有 TAG 的启用/禁用) */ private CheckBox mSelectAllTagCb; /** TAG 列表适配器(绑定 TAG 数据与视图,处理勾选状态) */ - private TAGListAdapter mTagListAdapter; + //private TAGListAdapter mTagListAdapter; /** 日志监听线程(监听日志文件变化,触发视图刷新) */ private LogViewThread mLogViewThread; /** 日志视图 Handler(主线程更新 UI,避免跨线程操作) */ @@ -68,7 +69,7 @@ public class LogView extends RelativeLayout { /** 日志级别适配器(绑定 LogUtils.LOG_LEVEL 枚举与 Spinner) */ private ArrayAdapter mLogLevelAdapter; /** TAG 水平列表视图(横向展示所有 TAG,支持滚动) */ - private HorizontalListView mTagHorizontalListView; + private TagsHorizontalListView mTagsHorizontalListView; // ====================== 构造方法(初始化视图) ====================== public LogView(Context context) { @@ -162,7 +163,7 @@ public class LogView extends RelativeLayout { 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); + mTagsHorizontalListView = rootView.findViewById(R.id.tags_listview); } /** @@ -177,15 +178,17 @@ public class LogView extends RelativeLayout { 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(); - } + // 定位匹配的 TAG + scrollToTargetTag(searchText); +// if (!searchText.isEmpty()) { +// // 搜索文本非空,定位匹配的 TAG +// scrollToTargetTag(searchText); +// } else { +// // 搜索文本为空,重置滚动位置 +// HorizontalScrollView parentHs = findViewById(R.id.viewlogHorizontalScrollView1); +// parentHs.smoothScrollTo(0, 0); +// mTagsHorizontalListView.resetScrollToStart(); +// } } @Override @@ -277,16 +280,16 @@ public class LogView extends RelativeLayout { */ private void initTagListView() { // 获取 LogUtils 中的 TAG 启用状态映射表 - Map tagEnableMap = LogUtils.getTagEnableMap(); + ArrayList tagEnableTagModel = LogUtils.getTagEnableTagModelList(); // 判断是否所有 TAG 都已启用(初始化全选开关状态) - boolean isAllTagEnabled = isAllTagsEnabled(tagEnableMap); + boolean isAllTagEnabled = isAllTagsEnabled(tagEnableTagModel); mSelectAllTagCb.setChecked(isAllTagEnabled); // 初始化 TAG 水平列表(设置垂直偏移,绑定适配器) - mTagHorizontalListView.setVerticalOffset(10); - mTagListAdapter = new TAGListAdapter(mContext, tagEnableMap); - mTagHorizontalListView.setAdapter(mTagListAdapter); - mTagListAdapter.notifyDataSetChanged(); // 刷新列表数据 + //mTagHorizontalListView.setVerticalOffset(10); + //mTagListAdapter = new TAGListAdapter(mContext, tagEnableMap); + mTagsHorizontalListView.setTagList(tagEnableTagModel); + //mTagListAdapter.notifyDataSetChanged(); // 刷新列表数据 // 全选 TAG 开关监听(点击时启用/禁用所有 TAG) mSelectAllTagCb.setOnClickListener(new View.OnClickListener() { @@ -294,7 +297,9 @@ public class LogView extends RelativeLayout { public void onClick(View v) { boolean isSelectAll = mSelectAllTagCb.isChecked(); LogUtils.setAllTagsEnable(isSelectAll); // 批量更新所有 TAG 状态 - mTagListAdapter.reload(); // 重新加载 TAG 数据并刷新视图 + + LogUtils.d(TAG, "mTagListAdapter.reload() not yet."); + //mTagListAdapter.reload(); // 重新加载 TAG 数据并刷新视图 } }); } @@ -304,12 +309,17 @@ public class LogView extends RelativeLayout { * @param tagEnableMap TAG 启用状态映射表 * @return true:所有 TAG 均启用;false:存在未启用的 TAG */ - private boolean isAllTagsEnabled(Map tagEnableMap) { - for (Map.Entry entry : tagEnableMap.entrySet()) { - if (!entry.getValue()) { - return false; - } - } + 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; } @@ -344,19 +354,19 @@ public class LogView extends RelativeLayout { * @param prefix 搜索文本(TAG 前缀) */ private void scrollToTargetTag(final String prefix) { - if (mTagListAdapter == null || prefix == null || prefix.isEmpty()) { + if (prefix == null || prefix.isEmpty()) { LogUtils.d(TAG, "TAG 搜索参数为空,无法定位"); return; } - final List tagItemList = mTagListAdapter.getItemList(); - mTagHorizontalListView.post(new Runnable() { + 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).getTag(); + String tag = tagItemList.get(i).getTagName(); if (tag != null && tag.toLowerCase().startsWith(prefix.toLowerCase())) { targetPosition = i; break; @@ -366,11 +376,13 @@ public class LogView extends RelativeLayout { if (targetPosition != -1) { final int targetPositionFinal = targetPosition; // 延迟滚动(确保布局完成,避免滚动失效) - mTagHorizontalListView.postDelayed(new Runnable() { + mTagsHorizontalListView.postDelayed(new Runnable() { @Override public void run() { LogUtils.d(TAG, "定位到 TAG 位置:" + targetPositionFinal); - mTagHorizontalListView.scrollToItem(targetPositionFinal); + + LogUtils.d(TAG, "mTagsHorizontalListView.scrollToItem(targetPositionFinal); not yet."); + //mTagsHorizontalListView.scrollToItem(targetPositionFinal); } }, 100); } else { diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/TagsHorizontalListView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/TagsHorizontalListView.java new file mode 100644 index 00000000..78f30d1b --- /dev/null +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/TagsHorizontalListView.java @@ -0,0 +1,240 @@ +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 new file mode 100644 index 00000000..28927f86 --- /dev/null +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/model/TagModel.java @@ -0,0 +1,41 @@ +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/TagItemView.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/views/TagItemView.java new file mode 100644 index 00000000..1094ad87 --- /dev/null +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/views/TagItemView.java @@ -0,0 +1,107 @@ +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 77c1b560..48291ede 100644 --- a/libappbase/src/main/res/layout/view_log.xml +++ b/libappbase/src/main/res/layout/view_log.xml @@ -91,15 +91,10 @@ android:id="@+id/tagsearch_et"/> + android:layout_width="wrap_content" + android:layout_height="wrap_content"> -