Compare commits
27 Commits
appbase-v1
...
appbase-v1
| Author | SHA1 | Date | |
|---|---|---|---|
| 8348075df1 | |||
| 8c335cb96d | |||
| 460df3fc92 | |||
| f83e62b05d | |||
| 656e58ec5c | |||
| 778a1bc98e | |||
| 34a30d4635 | |||
| 536e6eef60 | |||
| 0963025bbd | |||
| bb94f87597 | |||
| 669a6eab0c | |||
| a0d65d9f78 | |||
| f5f9d7c46e | |||
| 7afd1a8c20 | |||
| c00f25dbab | |||
| 4bf74875ab | |||
| 03c0afb655 | |||
| 73c654e6ce | |||
| d505277f15 | |||
| 0180b3225c | |||
| 00142a0b05 | |||
| 382f8c3412 | |||
| edf9d07e58 | |||
| 34a6322f3d | |||
| 270d2afe0c | |||
| 2aaacc88c3 | |||
| 7f030cafda |
17
README.md
@@ -5,10 +5,11 @@
|
||||
## ☁ ☁ ☁ WinBoLL APP ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ WinBoLL Studio Android 应用开源项目。☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ WinBoLL 网站地址 https://www.winboll.cc/ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ WinBoLL 源码地址 <https://gitea.winboll.cc/Studio/APPBase> ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/appbase.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
|
||||
# ☁ ☁ ☁ WinBoLL 源码地址 <https://gitea.winboll.cc/Studio/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/winboll.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 在 jitpack.io 托管的 APPBase 类库源码<https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 在 jitpack.io 托管的 AES 类库源码<https://github.com/ZhanGSKen/AES.git> ☁ ☁ ☁ ☁
|
||||
## WinBoLL 提问
|
||||
同样是 /sdcard 目录,在开发 Android 应用时,
|
||||
能否实现手机编译与电脑编译的源码同步。
|
||||
@@ -154,3 +155,11 @@ $ bash gradlew assembleBetaDebug
|
||||
$ bash gradlew assembleStageDebug
|
||||
|
||||
### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
||||
|
||||
# 应用版本号命名方式
|
||||
## statge 渠道
|
||||
V<应用开发环境编号><应用功能变更号><应用调试阶段号>
|
||||
如:APPBase_15.7.0
|
||||
## beta 渠道
|
||||
V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)>
|
||||
如:APPBase_15.9.6-beta8_5413
|
||||
|
||||
@@ -33,7 +33,7 @@ android {
|
||||
// versionName 更新后需要手动设置
|
||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.12"
|
||||
versionName "15.14"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sat Dec 06 14:24:33 HKT 2025
|
||||
stageCount=2
|
||||
#Mon Dec 15 17:43:37 HKT 2025
|
||||
stageCount=1
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.12
|
||||
publishVersion=15.12.1
|
||||
baseVersion=15.14
|
||||
publishVersion=15.14.0
|
||||
buildCount=0
|
||||
baseBetaVersion=15.12.2
|
||||
baseBetaVersion=15.14.1
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
|
||||
<application
|
||||
tools:replace="android:icon,android:roundIcon"
|
||||
android:icon="@drawable/ic_winboll_beta"
|
||||
android:roundIcon="@drawable/ic_winboll_beta">
|
||||
tools:replace="android:icon"
|
||||
android:icon="@drawable/ic_winboll_beta">
|
||||
|
||||
<!-- Put flavor specific code here -->
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cc.winboll.studio.appbase">
|
||||
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:icon="@drawable/ic_winboll"
|
||||
@@ -11,7 +11,7 @@
|
||||
android:resizeableActivity="true"
|
||||
android:process=":App">
|
||||
|
||||
<activity
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"
|
||||
@@ -29,13 +29,15 @@
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
|
||||
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -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<zhangsken@qq.com>
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/item_home"
|
||||
android:title="WinBoLL Home"
|
||||
android:icon="@drawable/ic_winboll"/>
|
||||
android:title="Home"
|
||||
android:icon="@drawable/ic_winboll"
|
||||
android:showAsAction="always"/>
|
||||
</menu>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="MyAPPBaseTheme" parent="APPBaseTheme">
|
||||
<item name="attrColorPrimary">@color/colorPrimary</item>
|
||||
<item name="themeGlobalCrashActivity">@style/MyGlobalCrashActivityTheme</item>
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sat Dec 06 14:24:33 HKT 2025
|
||||
stageCount=2
|
||||
#Mon Dec 15 17:43:37 HKT 2025
|
||||
stageCount=1
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.12
|
||||
publishVersion=15.12.1
|
||||
baseVersion=15.14
|
||||
publishVersion=15.14.0
|
||||
buildCount=0
|
||||
baseBetaVersion=15.12.2
|
||||
baseBetaVersion=15.14.1
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package="cc.winboll.studio.libappbase">
|
||||
|
||||
<application>
|
||||
|
||||
<activity
|
||||
android:name=".CrashHandler$CrashActivity"
|
||||
android:label="CrashActivity"
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.widget.HorizontalScrollView;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -52,7 +53,7 @@ public final class CrashHandler {
|
||||
public static final String TITTLE = "CrashReport";
|
||||
|
||||
/** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */
|
||||
public static final String EXTRA_CRASH_INFO = "crashInfo";
|
||||
public static final String EXTRA_CRASH_LOG = "crashInfo";
|
||||
|
||||
/** SharedPreferences 存储键(用于记录崩溃状态) */
|
||||
final static String PREFS = CrashHandler.class.getName() + "PREFS";
|
||||
@@ -169,12 +170,12 @@ public final class CrashHandler {
|
||||
LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK");
|
||||
// 保险丝正常:启动自定义样式的崩溃报告页面(GlobalCrashActivity)
|
||||
intent.setClass(app, GlobalCrashActivity.class);
|
||||
intent.putExtra(EXTRA_CRASH_INFO, errorLog); // 传递崩溃日志
|
||||
intent.putExtra(EXTRA_CRASH_LOG, errorLog); // 传递崩溃日志
|
||||
} else {
|
||||
LogUtils.d(TAG, "gotoCrashActiviy: else");
|
||||
// 保险丝熔断:启动基础版崩溃页面(CrashActivity,避免复杂页面再次崩溃)
|
||||
intent.setClass(app, CrashActivity.class);
|
||||
intent.putExtra(EXTRA_CRASH_INFO, errorLog);
|
||||
intent.putExtra(EXTRA_CRASH_LOG, errorLog);
|
||||
}
|
||||
|
||||
// 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面)
|
||||
@@ -185,10 +186,17 @@ public final class CrashHandler {
|
||||
);
|
||||
|
||||
try {
|
||||
// 启动崩溃页面,终止当前进程(确保完全重启)
|
||||
app.startActivity(intent);
|
||||
if (GlobalApplication.isDebugging()&&AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
|
||||
// 如果是 debug 版,启动崩溃页面窗口
|
||||
app.startActivity(intent);
|
||||
} else {
|
||||
// 如果是 release 版,就只发送一个通知
|
||||
CrashHandleNotifyUtils.handleUncaughtException(app, intent);
|
||||
}
|
||||
// 终止当前进程(确保完全重启)
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
|
||||
} catch (ActivityNotFoundException e) {
|
||||
// 未找到崩溃页面(如未在 Manifest 注册),交给系统默认处理器
|
||||
e.printStackTrace();
|
||||
@@ -428,7 +436,7 @@ public final class CrashHandler {
|
||||
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
|
||||
|
||||
// 获取传递的崩溃日志
|
||||
mLog = getIntent().getStringExtra(EXTRA_CRASH_INFO);
|
||||
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
|
||||
// 设置系统默认主题(避免自定义主题冲突)
|
||||
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
|
||||
|
||||
|
||||
@@ -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 终止,单例实例已释放");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public final class GlobalCrashActivity extends Activity implements MenuItem.OnMe
|
||||
.postResumeCrashSafetyWireHandler(getApplicationContext());
|
||||
|
||||
// 从 Intent 中获取崩溃日志数据(EXTRA_CRASH_INFO 为 CrashHandler 定义的常量键)
|
||||
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_INFO);
|
||||
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||
|
||||
// 设置当前 Activity 的布局文件(展示崩溃报告的 UI 结构)
|
||||
setContentView(R.layout.activity_globalcrash);
|
||||
|
||||
@@ -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&豆包大模型<zhangsken@qq.com>
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -19,8 +24,11 @@ import android.widget.EditText;
|
||||
import android.widget.HorizontalScrollView;
|
||||
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 cc.winboll.studio.libappbase.widget.LogTagSpinner;
|
||||
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<zhangsken@qq.com>
|
||||
* @Date 2024/08/12 14:36:18
|
||||
* @Describe 日志可视化自定义 View(继承 RelativeLayout)
|
||||
* 核心功能:日志展示、日志级别筛选、TAG 过滤(启用/禁用)、TAG 搜索定位、日志清理/复制、视图交互控制
|
||||
* 依赖 LogUtils 进行日志读写,通过 LogViewThread 监听日志文件变化并自动刷新
|
||||
*/
|
||||
public class LogView extends RelativeLayout {
|
||||
|
||||
/** 当前 View 的日志 TAG(用于调试输出) */
|
||||
public static final String TAG = "LogView";
|
||||
|
||||
/** 日志处理中标志(避免并发刷新,volatile 保证多线程可见性) */
|
||||
private volatile boolean mIsHandling;
|
||||
/** 新日志添加标志(标记有未处理的新日志,volatile 保证多线程可见性) */
|
||||
private volatile boolean mIsAddNewLog;
|
||||
public volatile boolean mIsHandling;
|
||||
public volatile boolean mIsAddNewLog;
|
||||
|
||||
/** 上下文对象(用于布局加载、系统服务获取) */
|
||||
private Context mContext;
|
||||
/** 日志滚动视图(包裹日志文本,支持垂直滚动) */
|
||||
private ScrollView mLogScrollView;
|
||||
/** 日志文本展示控件(显示所有日志内容) */
|
||||
private TextView mLogTextView;
|
||||
/** TAG 搜索输入框(用于搜索并定位目标 TAG) */
|
||||
private EditText mTagSearchEt;
|
||||
/** 文本选择开关(控制是否允许选中日志文本) */
|
||||
private CheckBox mTextSelectableCb;
|
||||
/** 全选 TAG 开关(控制所有 TAG 的启用/禁用) */
|
||||
private CheckBox mSelectAllTagCb;
|
||||
/** TAG 列表适配器(绑定 TAG 数据与视图,处理勾选状态) */
|
||||
private TAGListAdapter mTagListAdapter;
|
||||
/** 日志监听线程(监听日志文件变化,触发视图刷新) */
|
||||
private LogViewThread mLogViewThread;
|
||||
/** 日志视图 Handler(主线程更新 UI,避免跨线程操作) */
|
||||
private LogViewHandler mLogViewHandler;
|
||||
/** 日志级别选择下拉框(用于切换全局日志输出级别) */
|
||||
private Spinner mLogLevelSpinner;
|
||||
/** 日志级别适配器(绑定 LogUtils.LOG_LEVEL 枚举与 Spinner) */
|
||||
private ArrayAdapter<CharSequence> mLogLevelAdapter;
|
||||
/** TAG 水平列表视图(横向展示所有 TAG,支持滚动) */
|
||||
private HorizontalListView mTagHorizontalListView;
|
||||
Context mContext;
|
||||
ScrollView mScrollView;
|
||||
TextView mTextView;
|
||||
EditText metTagSearch;
|
||||
CheckBox mSelectableCheckBox;
|
||||
CheckBox mSelectAllTAGCheckBox;
|
||||
TAGListAdapter mTAGListAdapter;
|
||||
LogViewThread mLogViewThread;
|
||||
LogViewHandler mLogViewHandler;
|
||||
LogTagSpinner mLogLevelSpinner;
|
||||
ArrayAdapter<CharSequence> mLogLevelSpinnerAdapter;
|
||||
// 标签列表
|
||||
HorizontalListView mListViewTags;
|
||||
|
||||
// ====================== 构造方法(初始化视图) ======================
|
||||
public LogView(Context context) {
|
||||
super(context);
|
||||
initView(context);
|
||||
@@ -91,307 +77,274 @@ 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.setTextColor(mContext.getResources().getColor(R.color.white));
|
||||
metTagSearch.addTextChangedListener(new TextWatcher() {
|
||||
|
||||
// 设置 TAG 搜索输入框监听(实时搜索并定位 TAG)
|
||||
setupTagSearchListener();
|
||||
// 设置功能按钮监听(清理日志、复制日志)
|
||||
setupFunctionButtonListeners(rootView);
|
||||
// 设置文本选择开关监听(控制日志文本是否可选中)
|
||||
setupTextSelectableListener();
|
||||
// 初始化日志级别下拉框(绑定级别数据,设置默认值)
|
||||
initLogLevelSpinner();
|
||||
// 初始化 TAG 列表(加载所有 TAG,设置全选状态)
|
||||
initTagListView();
|
||||
// 设置默认交互模式(默认禁止子视图获取焦点,避免误触)
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int p, int p1, int p2) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
LogUtils.d(TAG, s.toString());
|
||||
if (s.length() > 0) {
|
||||
scrollToTag(s.toString());
|
||||
} else {
|
||||
HorizontalScrollView hsRoot = findViewById(R.id.viewlogHorizontalScrollView1);
|
||||
hsRoot.smoothScrollTo(0, 0);
|
||||
mListViewTags.resetScrollToStart();
|
||||
}
|
||||
// mListViewTags.postDelayed(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// mListViewTags.scrollToItem(5);
|
||||
// }
|
||||
// }, 100);
|
||||
}
|
||||
// 其他方法留空或按需实现
|
||||
});
|
||||
|
||||
|
||||
(findViewById(cc.winboll.studio.libappbase.R.id.viewlogButtonClean)).setOnClickListener(new View.OnClickListener(){
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.cleanLog();
|
||||
LogUtils.d(TAG, "Log is cleaned.");
|
||||
}
|
||||
});
|
||||
(findViewById(cc.winboll.studio.libappbase.R.id.viewlogButtonCopy)).setOnClickListener(new View.OnClickListener(){
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
||||
ClipboardManager cm = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
cm.setPrimaryClip(ClipData.newPlainText(mContext.getPackageName(), LogUtils.loadLog()));
|
||||
LogUtils.d(TAG, "Log is copied.");
|
||||
}
|
||||
});
|
||||
mSelectableCheckBox = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBoxSelectable);
|
||||
mSelectableCheckBox.setOnClickListener(new View.OnClickListener(){
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mSelectableCheckBox.isChecked()) {
|
||||
setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
|
||||
} else {
|
||||
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 设置日志级别列表
|
||||
// ArrayList<String> adapterItems = new ArrayList<>();
|
||||
// for (LogUtils.LOG_LEVEL e : LogUtils.LOG_LEVEL.values()) {
|
||||
// adapterItems.add(e.name());
|
||||
// }
|
||||
String[] mLogLevelSpinnerData = new String[LogUtils.LOG_LEVEL.values().length];
|
||||
for (int i = 0; i < LogUtils.LOG_LEVEL.values().length; i++) {
|
||||
mLogLevelSpinnerData[i] = LogUtils.LOG_LEVEL.values()[i].name();
|
||||
}
|
||||
mLogLevelSpinner.setLogTagData(mLogLevelSpinnerData);
|
||||
// 假设你有一个字符串数组作为选项列表
|
||||
//String[] options = {"Option 1", "Option 2", "Option 3"};
|
||||
// 创建一个ArrayAdapter来绑定数据到spinner
|
||||
// mLogLevelSpinnerAdapter = ArrayAdapter.createFromResource(
|
||||
// context, cc.winboll.studio.libappbase.R.array.enum_loglevel_array, android.R.layout.simple_spinner_item);
|
||||
// mLogLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
|
||||
// // 设置适配器并将它应用到spinner上
|
||||
// mLogLevelSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // 设置下拉视图样式
|
||||
// mLogLevelSpinner.setAdapter(mLogLevelSpinnerAdapter);
|
||||
// 为Spinner添加监听器
|
||||
mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
//String selectedOption = mLogLevelSpinnerAdapter.getItem(position);
|
||||
// 处理选中的选项...
|
||||
LogUtils.setLogLevel(LogUtils.LOG_LEVEL.values()[position]);
|
||||
}
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
// 如果没有选择,则执行此操作...
|
||||
}
|
||||
});
|
||||
// 获取默认值的索引
|
||||
int defaultValueIndex = LogUtils.getLogLevel().ordinal();
|
||||
|
||||
if (defaultValueIndex != -1) {
|
||||
// 如果找到了默认值,设置默认选项
|
||||
mLogLevelSpinner.setSelection(defaultValueIndex);
|
||||
}
|
||||
|
||||
// 加载标签列表
|
||||
Map<String, Boolean> mapTAGList = LogUtils.getMapTAGList();
|
||||
boolean isAllSelect = true;
|
||||
for (Map.Entry<String, Boolean> entry : mapTAGList.entrySet()) {
|
||||
if (entry.getValue() == false) {
|
||||
isAllSelect = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CheckBox cbALLTAG = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBox1);
|
||||
cbALLTAG.setChecked(isAllSelect);
|
||||
|
||||
// 加载标签表
|
||||
mListViewTags = findViewById(cc.winboll.studio.libappbase.R.id.tags_listview);
|
||||
mListViewTags.setVerticalOffset(10);
|
||||
mTAGListAdapter = new TAGListAdapter(mContext, mapTAGList);
|
||||
mListViewTags.setAdapter(mTAGListAdapter);
|
||||
|
||||
// 可以添加点击监听器来处理勾选框状态变化后的逻辑,比如获取当前勾选情况等
|
||||
mTAGListAdapter.notifyDataSetChanged();
|
||||
|
||||
mSelectAllTAGCheckBox = findViewById(cc.winboll.studio.libappbase.R.id.viewlogCheckBox1);
|
||||
ViewGroup.LayoutParams layoutParams2 = mSelectAllTAGCheckBox.getLayoutParams();
|
||||
if (layoutParams2 != null) {
|
||||
layoutParams2.width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
layoutParams2.height = 75;
|
||||
}
|
||||
mSelectAllTAGCheckBox.setLayoutParams(layoutParams2);
|
||||
//mSelectAllTAGCheckBox.setPadding(0,0,0,0);
|
||||
mSelectAllTAGCheckBox.setTextColor(mContext.getResources().getColor(R.color.white));
|
||||
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()));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
mLogLevelSpinner.updateTextSize(R.dimen.log_spinner_text_size);
|
||||
|
||||
// 设置滚动时不聚焦日志
|
||||
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定布局控件(通过 ID 查找并初始化所有子组件)
|
||||
* @param rootView 根布局视图
|
||||
*/
|
||||
private void bindViews(View rootView) {
|
||||
mLogScrollView = rootView.findViewById(R.id.viewlogScrollViewLog);
|
||||
mLogTextView = rootView.findViewById(R.id.viewlogTextViewLog);
|
||||
mTagSearchEt = rootView.findViewById(R.id.tagsearch_et);
|
||||
mLogLevelSpinner = rootView.findViewById(R.id.viewlogSpinner1);
|
||||
mTextSelectableCb = rootView.findViewById(R.id.viewlogCheckBoxSelectable);
|
||||
mSelectAllTagCb = rootView.findViewById(R.id.viewlogCheckBox1);
|
||||
mTagHorizontalListView = rootView.findViewById(R.id.tags_listview);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 TAG 搜索输入框监听(文本变化时触发 TAG 定位)
|
||||
*/
|
||||
private void setupTagSearchListener() {
|
||||
mTagSearchEt.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
String searchText = s.toString().trim();
|
||||
LogUtils.d(TAG, "TAG 搜索内容:" + searchText);
|
||||
if (!searchText.isEmpty()) {
|
||||
// 搜索文本非空,定位匹配的 TAG
|
||||
scrollToTargetTag(searchText);
|
||||
} else {
|
||||
// 搜索文本为空,重置滚动位置
|
||||
HorizontalScrollView parentHs = findViewById(R.id.viewlogHorizontalScrollView1);
|
||||
parentHs.smoothScrollTo(0, 0);
|
||||
mTagHorizontalListView.resetScrollToStart();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置功能按钮监听(清理日志、复制日志)
|
||||
*/
|
||||
private void setupFunctionButtonListeners(View rootView) {
|
||||
// 清理日志按钮(点击清空所有历史日志)
|
||||
rootView.findViewById(R.id.viewlogButtonClean).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.cleanLog();
|
||||
LogUtils.d(TAG, "日志已清理");
|
||||
}
|
||||
});
|
||||
|
||||
// 复制日志按钮(点击复制所有日志到剪贴板)
|
||||
rootView.findViewById(R.id.viewlogButtonCopy).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
// 将日志内容复制到剪贴板(标签为应用包名)
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(mContext.getPackageName(), LogUtils.loadLog()));
|
||||
LogUtils.d(TAG, "日志已复制到剪贴板");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文本选择开关监听(控制日志文本是否可选中复制)
|
||||
*/
|
||||
private void setupTextSelectableListener() {
|
||||
mTextSelectableCb.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mTextSelectableCb.isChecked()) {
|
||||
// 允许文本选择:子视图优先获取焦点
|
||||
setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
|
||||
} else {
|
||||
// 禁止文本选择:阻止子视图获取焦点
|
||||
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化日志级别下拉框(Spinner)
|
||||
* 1. 绑定 LogUtils.LOG_LEVEL 枚举数据;
|
||||
* 2. 设置默认选中当前全局日志级别;
|
||||
* 3. 监听级别变化,更新 LogUtils 全局配置。
|
||||
*/
|
||||
private void initLogLevelSpinner() {
|
||||
// 从资源文件加载日志级别数组(R.array.enum_loglevel_array 与 LOG_LEVEL 枚举对应)
|
||||
mLogLevelAdapter = ArrayAdapter.createFromResource(
|
||||
mContext, R.array.enum_loglevel_array, android.R.layout.simple_spinner_item);
|
||||
// 设置下拉列表样式
|
||||
mLogLevelAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
mLogLevelSpinner.setAdapter(mLogLevelAdapter);
|
||||
|
||||
// 监听下拉框选择变化
|
||||
mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
// 根据选择的位置设置全局日志级别(position 与 LOG_LEVEL 枚举索引对应)
|
||||
LogUtils.setLogLevel(LogUtils.LOG_LEVEL.values()[position]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {}
|
||||
});
|
||||
|
||||
// 设置默认选中当前日志级别
|
||||
int defaultLevelIndex = LogUtils.getLogLevel().ordinal();
|
||||
if (defaultLevelIndex >= 0) {
|
||||
mLogLevelSpinner.setSelection(defaultLevelIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 TAG 水平列表
|
||||
* 1. 加载 LogUtils 中的所有 TAG 及其启用状态;
|
||||
* 2. 初始化 TAG 列表适配器;
|
||||
* 3. 设置全选 TAG 开关监听。
|
||||
*/
|
||||
private void initTagListView() {
|
||||
// 获取 LogUtils 中的 TAG 启用状态映射表
|
||||
Map<String, Boolean> tagEnableMap = LogUtils.getTagEnableMap();
|
||||
// 判断是否所有 TAG 都已启用(初始化全选开关状态)
|
||||
boolean isAllTagEnabled = isAllTagsEnabled(tagEnableMap);
|
||||
mSelectAllTagCb.setChecked(isAllTagEnabled);
|
||||
|
||||
// 初始化 TAG 水平列表(设置垂直偏移,绑定适配器)
|
||||
mTagHorizontalListView.setVerticalOffset(10);
|
||||
mTagListAdapter = new TAGListAdapter(mContext, tagEnableMap);
|
||||
mTagHorizontalListView.setAdapter(mTagListAdapter);
|
||||
mTagListAdapter.notifyDataSetChanged(); // 刷新列表数据
|
||||
|
||||
// 全选 TAG 开关监听(点击时启用/禁用所有 TAG)
|
||||
mSelectAllTagCb.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
boolean isSelectAll = mSelectAllTagCb.isChecked();
|
||||
LogUtils.setAllTagsEnable(isSelectAll); // 批量更新所有 TAG 状态
|
||||
mTagListAdapter.reload(); // 重新加载 TAG 数据并刷新视图
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否所有 TAG 都已启用
|
||||
* @param tagEnableMap TAG 启用状态映射表
|
||||
* @return true:所有 TAG 均启用;false:存在未启用的 TAG
|
||||
*/
|
||||
private boolean isAllTagsEnabled(Map<String, Boolean> tagEnableMap) {
|
||||
for (Map.Entry<String, Boolean> entry : tagEnableMap.entrySet()) {
|
||||
if (!entry.getValue()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新日志视图(由 LogViewThread 触发,通知有新日志)
|
||||
* 避免并发刷新:正在处理时标记新日志,处理完成后再次刷新
|
||||
*/
|
||||
public void updateLogView() {
|
||||
if (mLogViewHandler.isHandling()) {
|
||||
// 正在处理日志刷新,标记有新日志待处理
|
||||
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<TAGItemModel> tagItemList = mTagListAdapter.getItemList();
|
||||
mTagHorizontalListView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int targetPosition = -1;
|
||||
// 遍历 TAG 列表,查找前缀匹配的 TAG(忽略大小写)
|
||||
for (int i = 0; i < tagItemList.size(); i++) {
|
||||
String tag = tagItemList.get(i).getTag();
|
||||
if (tag != null && tag.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
targetPosition = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final List<TAGItemModel> 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 +367,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 +408,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 +427,116 @@ public class LogView extends RelativeLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写 hashCode 方法(基于 TAG 名称生成哈希值)
|
||||
* @return 哈希值(空 TAG 返回 0)
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return tag == null ? 0 : tag.hashCode();
|
||||
return tag == null ? 0 : tag.hashCode(); // 手动处理空值
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 内部类:TAG 列表适配器(绑定数据与视图) ======================
|
||||
/**
|
||||
* TAG 水平列表适配器(继承 BaseAdapter)
|
||||
* 负责 TAG 数据与列表项视图的绑定,处理勾选状态变化
|
||||
*/
|
||||
private class TAGListAdapter extends BaseAdapter {
|
||||
/** 上下文对象(用于加载列表项布局) */
|
||||
|
||||
public class TAGListAdapter extends BaseAdapter {
|
||||
|
||||
private Context context;
|
||||
/** 原始 TAG 启用状态映射表(来自 LogUtils) */
|
||||
private Map<String, Boolean> originTagMap;
|
||||
/** TAG 列表项数据(转换为 TAGItemModel 列表,便于排序和绑定) */
|
||||
private List<TAGItemModel> tagItemList;
|
||||
private Map<String, Boolean> mapOrigin;
|
||||
private List<TAGItemModel> itemList;
|
||||
|
||||
/**
|
||||
* 构造方法(初始化数据并加载到列表)
|
||||
* @param context 上下文
|
||||
* @param tagMap TAG 启用状态映射表
|
||||
*/
|
||||
public TAGListAdapter(Context context, Map<String, Boolean> tagMap) {
|
||||
public TAGListAdapter(Context context, Map<String, Boolean> map) {
|
||||
this.context = context;
|
||||
this.originTagMap = tagMap;
|
||||
loadTagData(originTagMap); // 加载并转换数据
|
||||
mapOrigin = map;
|
||||
loadMap(mapOrigin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 TAG 列表项数据(供外部定位 TAG 使用)
|
||||
* @return TAGItemModel 列表
|
||||
*/
|
||||
public List<TAGItemModel> getItemList() {
|
||||
return tagItemList;
|
||||
return itemList;
|
||||
}
|
||||
|
||||
// ====================== BaseAdapter 抽象方法实现 ======================
|
||||
@Override
|
||||
public int getCount() {
|
||||
return tagItemList == null ? 0 : tagItemList.size();
|
||||
return itemList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return tagItemList.get(position);
|
||||
public Object getItem(int p) {
|
||||
return itemList.get(p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
public long getItemId(int p) {
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 TAG 数据(将 Map 转换为 List 并排序)
|
||||
* @param tagMap TAG 启用状态映射表
|
||||
*/
|
||||
private void loadTagData(Map<String, Boolean> tagMap) {
|
||||
tagItemList = new ArrayList<>();
|
||||
// 遍历 Map,转换为 TAGItemModel 并添加到列表
|
||||
for (Map.Entry<String, Boolean> entry : tagMap.entrySet()) {
|
||||
tagItemList.add(new TAGItemModel(entry.getKey(), entry.getValue()));
|
||||
void loadMap(Map<String, Boolean> map) {
|
||||
itemList = new ArrayList<TAGItemModel>();
|
||||
for (Map.Entry<String, Boolean> entry : map.entrySet()) {
|
||||
itemList.add(new TAGItemModel(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
// 按 TAG 名称升序排序(中文排序兼容)
|
||||
Collections.sort(tagItemList, new TagAscComparator(true));
|
||||
// 添加排序功能,按照tag进行升序排序
|
||||
Collections.sort(itemList, new SortMapEntryByKeyString(true));
|
||||
//Collections.sort(itemList, new SortMapEntryByKeyString(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载 TAG 数据(用于全选/反选后刷新列表)
|
||||
*/
|
||||
public void reload() {
|
||||
loadTagData(originTagMap); // 重新加载数据
|
||||
notifyDataSetChanged(); // 通知视图刷新
|
||||
loadMap(mapOrigin);
|
||||
super.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建/复用列表项视图(优化性能,避免重复 inflate)
|
||||
* @param position 列表项位置
|
||||
* @param convertView 复用视图(可为 null)
|
||||
* @param parent 父容器
|
||||
* @return 列表项视图
|
||||
*/
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
ViewHolder holder;
|
||||
// 复用视图(减少布局加载开销)
|
||||
if (convertView == null) {
|
||||
// 加载列表项布局(R.layout.item_logtag 为 TAG 项自定义布局)
|
||||
convertView = LayoutInflater.from(context).inflate(R.layout.item_logtag, parent, false);
|
||||
holder = new ViewHolder();
|
||||
// 绑定列表项控件(TAG 文本和勾选框)
|
||||
holder.tagTv = convertView.findViewById(R.id.viewlogtagTextView1);
|
||||
holder.tagCb = convertView.findViewById(R.id.viewlogtagCheckBox1);
|
||||
convertView.setTag(holder); // 保存 ViewHolder 到视图
|
||||
holder.tvText = convertView.findViewById(R.id.viewlogtagTextView1);
|
||||
holder.cbChecked = convertView.findViewById(R.id.viewlogtagCheckBox1);
|
||||
convertView.setTag(holder);
|
||||
} else {
|
||||
holder = (ViewHolder) convertView.getTag(); // 复用 ViewHolder
|
||||
holder = (ViewHolder) convertView.getTag();
|
||||
}
|
||||
|
||||
// 绑定数据到视图
|
||||
final TAGItemModel item = tagItemList.get(position);
|
||||
holder.tagTv.setText(item.getTag()); // 设置 TAG 名称
|
||||
holder.tagCb.setChecked(item.isChecked()); // 设置勾选状态
|
||||
final TAGItemModel item = itemList.get(position);
|
||||
holder.tvText.setText(item.getTag());
|
||||
ViewGroup.LayoutParams layoutParams = holder.tvText.getLayoutParams();
|
||||
if (layoutParams != null) {
|
||||
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
layoutParams.height = 75;
|
||||
}
|
||||
holder.tvText.setLayoutParams(layoutParams);
|
||||
holder.tvText.setPadding(0,0,0,0);
|
||||
holder.tvText.setTextColor(mContext.getResources().getColor(R.color.white));
|
||||
holder.cbChecked.setChecked(item.isChecked());
|
||||
holder.cbChecked.setLayoutParams(layoutParams);
|
||||
holder.cbChecked.setPadding(0,0,0,0);
|
||||
holder.cbChecked.setOnClickListener(new View.OnClickListener(){
|
||||
|
||||
// 勾选框点击监听(更新 TAG 启用状态)
|
||||
holder.tagCb.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
boolean isChecked = ((CheckBox) v).isChecked();
|
||||
// 调用 LogUtils 更新该 TAG 的启用状态
|
||||
LogUtils.setTagEnable(item.getTag(), isChecked);
|
||||
// 同步更新本地模型状态(避免刷新后状态不一致)
|
||||
item.setChecked(isChecked);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.setTAGListEnable(item.getTag(), ((CheckBox)v).isChecked());
|
||||
}
|
||||
});
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表项 ViewHolder(缓存控件,提升列表滑动性能)
|
||||
*/
|
||||
private class ViewHolder {
|
||||
TextView tagTv; // TAG 名称文本控件
|
||||
CheckBox tagCb; // TAG 启用状态勾选框
|
||||
public class ViewHolder {
|
||||
TextView tvText;
|
||||
CheckBox cbChecked;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 内部类:TAG 排序比较器(中文兼容) ======================
|
||||
/**
|
||||
* TAG 名称排序比较器(实现 Comparator)
|
||||
* 支持中文排序(基于系统默认中文 Locale),可选择升序/降序
|
||||
*/
|
||||
private class TagAscComparator implements Comparator<TAGItemModel> {
|
||||
/** 排序方向(true:升序;false:降序) */
|
||||
private boolean isAsc;
|
||||
/** 中文排序器(兼容中文汉字排序) */
|
||||
private Collator chineseCollator = Collator.getInstance(java.util.Locale.CHINA);
|
||||
|
||||
public TagAscComparator(boolean isAsc) {
|
||||
this.isAsc = isAsc;
|
||||
class SortMapEntryByKeyString implements Comparator<TAGItemModel> {
|
||||
private boolean mIsDesc = true;
|
||||
// isDesc 是否降序排列
|
||||
public SortMapEntryByKeyString(boolean isDesc) {
|
||||
mIsDesc = isDesc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个 TAGItemModel(按 TAG 名称排序)
|
||||
* @param o1 第一个比较对象
|
||||
* @param o2 第二个比较对象
|
||||
* @return 比较结果(正数:o1 在 o2 后;负数:o1 在 o2 前;0:相等)
|
||||
*/
|
||||
Collator cmp = Collator.getInstance(java.util.Locale.CHINA);
|
||||
@Override
|
||||
public int compare(TAGItemModel o1, TAGItemModel o2) {
|
||||
String tag1 = o1.getTag();
|
||||
String tag2 = o2.getTag();
|
||||
// 处理空值(空 TAG 排在最前)
|
||||
if (tag1 == null) return -1;
|
||||
if (tag2 == null) return 1;
|
||||
|
||||
// 根据排序方向返回比较结果
|
||||
if (isAsc) {
|
||||
return chineseCollator.compare(tag1, tag2); // 升序
|
||||
if (mIsDesc) {
|
||||
return o1.getTag().compareTo(o2.getTag());
|
||||
} else {
|
||||
return chineseCollator.compare(tag2, tag1); // 降序
|
||||
return o2.getTag().compareTo(o1.getTag());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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&豆包大模型<zhangsken@qq.com>
|
||||
* @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() : "未知") + ")");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
package cc.winboll.studio.libappbase.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.R;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/15 13:51
|
||||
* @Describe 纯原生 LogTag 专属 Spinner(无 androidx 依赖)
|
||||
* 核心特性:1. 继承原生 Spinner,适配全 Android 版本;2. dimen 统一配置所有尺寸;3. 文字大小支持 dp 单位;4. 简化外部初始化
|
||||
*/
|
||||
public class LogTagSpinner extends Spinner {
|
||||
public static final String TAG = "LogTagSpinner";
|
||||
|
||||
Context mContext;
|
||||
// 尺寸缓存(dimen 解析后转 px,避免重复计算)
|
||||
private int mSpinnerWidth; // 控件自身框度(px)
|
||||
private int mSpinnerHeight; // 控件自身高度(px)
|
||||
private int mItemWidth; // 下拉项单个高度(px)
|
||||
private int mItemHeight; // 下拉项单个高度(px)
|
||||
private float mTextSizePx; // 文字大小(px,dp 转译后)
|
||||
private int mTextPadding; // 文字左右内边距(px)
|
||||
|
||||
// 内置适配器(外部无需关心内部实现)
|
||||
private ArrayAdapter<String> mLogTagAdapter;
|
||||
|
||||
|
||||
// -------------------------- 构造方法(原生 Spinner 必重写 3 个)--------------------------
|
||||
public LogTagSpinner(Context context) {
|
||||
super(context);
|
||||
initCoreLogic(context);
|
||||
}
|
||||
|
||||
public LogTagSpinner(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initCoreLogic(context);
|
||||
}
|
||||
|
||||
public LogTagSpinner(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initCoreLogic(context);
|
||||
}
|
||||
|
||||
// -------------------------- 核心初始化(解析资源 + 配置样式 + 初始化适配器)--------------------------
|
||||
private void initCoreLogic(Context context) {
|
||||
this.mContext = context;
|
||||
// 1. 读取 dimen 资源(dp 转 px,核心适配)
|
||||
parseDimenResources();
|
||||
// 2. 设置 Spinner 自身基础样式(高度、背景)
|
||||
configSelfStyle();
|
||||
// 3. 初始化适配器(统一选中项+下拉项样式)
|
||||
initCustomAdapter();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 步骤 1:解析 dimen 资源,所有 dp 单位转为 px(跨设备视觉一致)
|
||||
*/
|
||||
private void parseDimenResources() {
|
||||
Context context = this.mContext;
|
||||
// 控件自身宽度(dp → px)
|
||||
mSpinnerWidth = context.getResources().getDimensionPixelSize(R.dimen.log_spinner_width);
|
||||
// 控件自身高度(dp → px)
|
||||
mSpinnerHeight = context.getResources().getDimensionPixelSize(R.dimen.log_spinner_height);
|
||||
// 下拉项宽度(dp → px)
|
||||
mItemWidth = context.getResources().getDimensionPixelSize(R.dimen.log_spinner_item_width);
|
||||
// 下拉项高度(dp → px)
|
||||
mItemHeight = context.getResources().getDimensionPixelSize(R.dimen.log_spinner_item_height);
|
||||
// 文字大小(dp → px,核心:用 getDimensionPixelSize 确保 dp 精准转译)
|
||||
mTextSizePx = context.getResources().getDimensionPixelSize(R.dimen.log_spinner_text_size);
|
||||
// 文字内边距(dp → px)
|
||||
mTextPadding = context.getResources().getDimensionPixelSize(R.dimen.log_spinner_text_padding);
|
||||
|
||||
LogUtils.d("LogTagSpinner", "dimen 解析完成:高度=" + mSpinnerHeight + "px,文字大小=" + mTextSizePx + "px");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 步骤 2:配置 Spinner 自身样式(覆盖布局属性,统一控制)
|
||||
*/
|
||||
private void configSelfStyle() {
|
||||
// 动态设置控件高度(布局中 layout_height 设 wrap_content 即可,这里统一控制)
|
||||
ViewGroup.LayoutParams layoutParams = getLayoutParams();
|
||||
if (layoutParams != null) {
|
||||
layoutParams.width = mSpinnerWidth;
|
||||
layoutParams.height = mSpinnerHeight;
|
||||
} else {
|
||||
// 代码创建控件时,手动初始化布局参数
|
||||
layoutParams = new ViewGroup.LayoutParams(
|
||||
mSpinnerWidth,
|
||||
mSpinnerHeight
|
||||
);
|
||||
}
|
||||
setLayoutParams(layoutParams);
|
||||
|
||||
// 统一背景色(外部可通过 setBackground 手动覆盖)
|
||||
setBackgroundColor(this.mContext.getColor(R.color.btn_gray_normal));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 步骤 3:初始化自定义适配器,统一选中项+下拉项样式
|
||||
*/
|
||||
private void initCustomAdapter() {
|
||||
// 用原生系统布局(避免自定义布局,减少依赖),后续重写样式
|
||||
mLogTagAdapter = new ArrayAdapter<String>(this.mContext, android.R.layout.simple_spinner_item) {
|
||||
// 重写:控制「已选中项」的样式(文字大小、高度、内边距)
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
TextView itemTv = (TextView) super.getView(position, convertView, parent);
|
||||
setItemUniformStyle(itemTv);
|
||||
return itemTv;
|
||||
}
|
||||
|
||||
// 重写:控制「下拉列表项」的样式(必须重写,否则下拉项样式不生效)
|
||||
@Override
|
||||
public View getDropDownView(int position, View convertView, ViewGroup parent) {
|
||||
TextView itemTv = (TextView) super.getDropDownView(position, convertView, parent);
|
||||
setItemUniformStyle(itemTv);
|
||||
return itemTv;
|
||||
}
|
||||
};
|
||||
|
||||
// 绑定下拉项布局(原生系统布局,确保低版本兼容)
|
||||
mLogTagAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
// 设置适配器到 Spinner
|
||||
setAdapter(mLogTagAdapter);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通用方法:统一设置列表项(选中项/下拉项)的样式
|
||||
*/
|
||||
private void setItemUniformStyle(TextView itemTv) {
|
||||
if (itemTv == null) return;
|
||||
|
||||
// 1. 文字大小(核心:按 px 赋值,dp 转译后无二次换算,精准适配)
|
||||
itemTv.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSizePx);
|
||||
// 2. 列表项高度(固定高度,避免文字多少导致高度不一致)
|
||||
itemTv.setWidth(mItemWidth);
|
||||
itemTv.setHeight(mItemHeight);
|
||||
// 3. 内边距(左右留白,优化排版,避免文字贴边)
|
||||
itemTv.setPadding(mTextPadding, 0, mTextPadding, 0);
|
||||
// 4. 文字对齐(垂直居中+靠左,符合常规 UI 设计)
|
||||
//itemTv.setGravity(View.GRAVITY_CENTER_VERTICAL | View.GRAVITY_START);
|
||||
// 5. 文字颜色(统一深色,可改为项目颜色资源)
|
||||
itemTv.setTextColor(this.mContext.getColor(R.color.white));
|
||||
itemTv.setBackgroundColor(this.mContext.getColor(R.color.btn_gray_normal));
|
||||
// 6. 文字溢出处理(最多 2 行,超出省略,避免长标签换行过多)
|
||||
itemTv.setSingleLine(false);
|
||||
itemTv.setMaxLines(2);
|
||||
itemTv.setEllipsize(TextUtils.TruncateAt.END);
|
||||
}
|
||||
|
||||
|
||||
// -------------------------- 外部调用 API(极简用法,无需关心内部逻辑)--------------------------
|
||||
/**
|
||||
* 填充日志标签数据(外部核心调用,一行代码搞定)
|
||||
* @param logTagArray 日志标签数组(如:{"TAG_MAIN", "TAG_NET", "TAG_DB"})
|
||||
*/
|
||||
public void setLogTagData(String[] logTagArray) {
|
||||
if (mLogTagAdapter == null || logTagArray == null || logTagArray.length == 0) {
|
||||
LogUtils.w("LogTagSpinner", "填充数据失败:适配器为空或数据无效");
|
||||
return;
|
||||
}
|
||||
// 清空旧数据,添加新数据,刷新适配器
|
||||
mLogTagAdapter.clear();
|
||||
mLogTagAdapter.addAll(logTagArray);
|
||||
mLogTagAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认选中的标签(索引从 0 开始)
|
||||
* @param defaultIndex 默认选中索引(需在 setLogTagData 之后调用)
|
||||
*/
|
||||
public void setDefaultSelectedTag(int defaultIndex) {
|
||||
if (mLogTagAdapter == null) return;
|
||||
// 索引合法性校验,避免数组越界
|
||||
if (defaultIndex >= 0 && defaultIndex < mLogTagAdapter.getCount()) {
|
||||
setSelection(defaultIndex);
|
||||
LogUtils.d("LogTagSpinner", "默认选中标签:" + mLogTagAdapter.getItem(defaultIndex));
|
||||
} else {
|
||||
LogUtils.w("LogTagSpinner", "默认选中索引无效:" + defaultIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中的日志标签(外部业务逻辑调用)
|
||||
* @return 当前选中的标签文字(无选中时返回空字符串)
|
||||
*/
|
||||
public String getCurrentSelectedTag() {
|
||||
Object selectedItem = getSelectedItem();
|
||||
return selectedItem != null ? selectedItem.toString() : "";
|
||||
}
|
||||
|
||||
|
||||
// -------------------------- 优化扩展(可选,提升稳定性)--------------------------
|
||||
/**
|
||||
* 视图附着到窗口时,确保默认有数据(避免空指针)
|
||||
*/
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
if (mLogTagAdapter != null && mLogTagAdapter.getCount() == 0) {
|
||||
setLogTagData(new String[]{"默认标签"});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部扩展:动态修改文字大小(dp 单位)
|
||||
* @param textSizeDp 目标文字大小(dp)
|
||||
*/
|
||||
public void updateTextSize(int textSizeDp) {
|
||||
mTextSizePx = this.mContext.getResources().getDimensionPixelSize(textSizeDp);
|
||||
// 刷新所有列表项样式
|
||||
if (mLogTagAdapter != null) {
|
||||
mLogTagAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部扩展:动态修改文字颜色
|
||||
* @param textColorRes 文字颜色资源 ID(如 R.color.red)
|
||||
*/
|
||||
public void updateTextColor(int textColorRes) {
|
||||
int textColor = this.mContext.getResources().getColor(textColorRes);
|
||||
// 刷新选中项颜色
|
||||
TextView selectedTv = (TextView) getSelectedView();
|
||||
if (selectedTv != null) {
|
||||
selectedTv.setTextColor(textColor);
|
||||
}
|
||||
// 刷新下拉项颜色
|
||||
if (mLogTagAdapter != null) {
|
||||
mLogTagAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
libappbase/src/main/res/drawable/btn_gray_bg.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 按钮状态选择器:按优先级匹配(按压 > 禁用 > 默认) -->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- 状态 1:按压时(手指按住)→ 深灰色 -->
|
||||
<item android:color="@color/btn_gray_pressed" android:state_pressed="true"/>
|
||||
|
||||
<!-- 状态 2:禁用时(setEnabled(false))→ 浅灰色 -->
|
||||
<item android:color="@color/btn_gray_disabled" android:state_enabled="false"/>
|
||||
|
||||
<!-- 状态 3:默认态(正常可点击)→ 常规灰色 -->
|
||||
<item android:color="@color/btn_gray_normal"/>
|
||||
|
||||
</selector>
|
||||
@@ -6,6 +6,6 @@
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#ff000000"
|
||||
android:pathData="M22,6C22,4.9 21.1,4 20,4H4C2.9,4 2,4.9 2,6V18C2,19.1 2.9,20 4,20H20C21.1,20 22,19.1 22,18V6M20,6L12,11L4,6H20M20,18H4V8L12,13L20,8V18Z"/>
|
||||
android:pathData="M19,21H8V7H19M19,5H8A2,2 0,0 0,6 7V21A2,2 0,0 0,8 23H19A2,2 0,0 0,21 21V7A2,2 0,0 0,19 5M16,1H4A2,2 0,0 0,2 3V17H4V3H16V1Z"/>
|
||||
|
||||
</vector>
|
||||
</vector>
|
||||
@@ -9,13 +9,15 @@
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/button_height"
|
||||
android:layout_height="@dimen/log_button_height"
|
||||
android:textSize="@dimen/log_text_size"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:id="@+id/viewlogtagTextView1"/>
|
||||
|
||||
<CheckBox
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/button_height"
|
||||
android:layout_height="@dimen/log_button_height"
|
||||
android:textSize="@dimen/log_text_size"
|
||||
android:id="@+id/viewlogtagCheckBox1"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -7,58 +7,64 @@
|
||||
android:background="#FF000000">
|
||||
|
||||
<RelativeLayout
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_height="34dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@drawable/bg_toolbar_log"
|
||||
android:id="@+id/viewlogRelativeLayoutToolbar">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:layout_width="@dimen/log_button_width"
|
||||
android:layout_height="@dimen/log_button_height"
|
||||
android:textSize="@dimen/log_text_size"
|
||||
android:text="Clean"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:backgroundTint="@drawable/btn_gray_bg"
|
||||
android:layout_centerVertical="true"
|
||||
android:id="@+id/viewlogButtonClean"
|
||||
android:layout_marginLeft="5dp"/>
|
||||
|
||||
<TextView
|
||||
android:background="#FF000000"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:layout_height="20dp"
|
||||
android:textSize="@dimen/log_text_size"
|
||||
android:padding="@dimen/log_text_padding"
|
||||
android:text="LV:"
|
||||
android:layout_toRightOf="@+id/viewlogButtonClean"
|
||||
android:layout_centerVertical="true"
|
||||
android:id="@+id/viewlogTextView1"
|
||||
android:textColor="#FFFFFFFF"/>
|
||||
android:background="@color/btn_gray_normal"
|
||||
android:textColor="@color/black"/>
|
||||
|
||||
<Spinner
|
||||
android:background="#FFFFFFFF"
|
||||
<cc.winboll.studio.libappbase.widget.LogTagSpinner
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toRightOf="@+id/viewlogTextView1"
|
||||
android:layout_centerVertical="true"
|
||||
android:id="@+id/viewlogSpinner1"/>
|
||||
android:id="@+id/viewlogSpinner1"
|
||||
android:padding="@dimen/log_spinner_text_padding"/>
|
||||
|
||||
<CheckBox
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:layout_width="@dimen/log_checkbox_width"
|
||||
android:layout_height="@dimen/log_checkbox_height"
|
||||
android:textSize="@dimen/log_text_size"
|
||||
android:layout_toLeftOf="@+id/viewlogButtonCopy"
|
||||
android:layout_centerVertical="true"
|
||||
android:text="Selectable"
|
||||
android:background="#FFFFFFFF"
|
||||
android:paddingRight="10dp"
|
||||
android:textSize="16dp"
|
||||
android:id="@+id/viewlogCheckBoxSelectable"/>
|
||||
android:background="@color/btn_gray_normal"
|
||||
android:id="@+id/viewlogCheckBoxSelectable"
|
||||
android:padding="@dimen/log_text_padding"
|
||||
android:textColor="@color/white"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:layout_width="@dimen/log_button_width"
|
||||
android:layout_height="@dimen/log_button_height"
|
||||
android:textSize="@dimen/log_text_size"
|
||||
android:textColor="@color/white"
|
||||
android:backgroundTint="@drawable/btn_gray_bg"
|
||||
android:text="Copy"
|
||||
android:layout_alignParentRight="true"
|
||||
android:textSize="14dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:id="@+id/viewlogButtonCopy"
|
||||
android:layout_marginRight="5dp"/>
|
||||
@@ -68,7 +74,7 @@
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_height="@dimen/log_button_height"
|
||||
android:layout_below="@+id/viewlogRelativeLayoutToolbar"
|
||||
android:id="@+id/viewlogLinearLayout1"
|
||||
android:gravity="center_vertical"
|
||||
@@ -76,8 +82,10 @@
|
||||
|
||||
<CheckBox
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/button_height"
|
||||
android:layout_height="@dimen/log_button_height"
|
||||
android:textSize="@dimen/log_text_size"
|
||||
android:text="ALL"
|
||||
android:padding="2dp"
|
||||
android:id="@+id/viewlogCheckBox1"
|
||||
android:background="@drawable/bg_border_round"
|
||||
android:layout_marginLeft="5dp"
|
||||
@@ -86,7 +94,8 @@
|
||||
<EditText
|
||||
android:layout_width="50dp"
|
||||
android:ems="10"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="@dimen/log_button_height"
|
||||
android:textSize="@dimen/log_text_size"
|
||||
android:singleLine="true"
|
||||
android:id="@+id/tagsearch_et"/>
|
||||
|
||||
@@ -95,11 +104,11 @@
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_border"
|
||||
android:scrollbars="none"
|
||||
android:padding="5dp"
|
||||
android:padding="2dp"
|
||||
android:layout_weight="1.0"
|
||||
android:id="@+id/viewlogHorizontalScrollView1">
|
||||
|
||||
<cc.winboll.studio.libappbase.HorizontalListView
|
||||
<cc.winboll.studio.libappbase.views.HorizontalListView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/tags_listview"/>
|
||||
@@ -125,6 +134,7 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:textSize="@dimen/log_text_size"
|
||||
android:text="Text"
|
||||
android:textColor="#FF00FF00"
|
||||
android:textIsSelectable="true"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<attr name="attrColorPrimary" format="color" />
|
||||
|
||||
<attr name="themeGlobalCrashActivity" format="reference"/>
|
||||
|
||||
|
||||
@@ -4,4 +4,71 @@
|
||||
<color name="colorPrimaryDark">#FF005C12</color>
|
||||
<color name="colorAccent">#FF8DFFA2</color>
|
||||
<color name="colorText">#FFFFFB8D</color>
|
||||
<color name="colorTextBackgound">#FF000000</color>
|
||||
|
||||
<!-- ============== 基础黑白(必含,适配文字/背景) ============== -->
|
||||
<color name="white">#FFFFFF</color> <!-- 纯白色(文字/背景) -->
|
||||
<color name="black">#000000</color> <!-- 近黑色(重要文字) -->
|
||||
|
||||
<!-- ============== 基础色系(按钮/强调色常用) ============== -->
|
||||
<!-- 蓝色系(常用:确认/链接/主题色) -->
|
||||
<color name="blue_light">#4A90E2</color> <!-- 浅蓝(次要按钮) -->
|
||||
<color name="blue_normal">#2196F3</color> <!-- 标准蓝(主题/确认按钮) -->
|
||||
<color name="blue_dark">#1976D2</color> <!-- 深蓝(按压态/重要强调) -->
|
||||
<!-- 绿色系(常用:成功/完成/安全提示) -->
|
||||
<color name="green_light">#66BB6A</color> <!-- 浅绿(次要成功态) -->
|
||||
<color name="green_normal">#4CAF50</color> <!-- 标准绿(成功按钮/提示) -->
|
||||
<color name="green_dark">#388E3C</color> <!-- 深绿(按压态/重要成功) -->
|
||||
<!-- 红色系(常用:错误/警告/删除按钮) -->
|
||||
<color name="red_light">#EF5350</color> <!-- 浅红(次要错误提示) -->
|
||||
<color name="red_normal">#F44336</color> <!-- 标准红(删除/错误按钮) -->
|
||||
<color name="red_dark">#D32F2F</color> <!-- 深红(按压态/重要错误) -->
|
||||
<!-- 黄色系(常用:警告/提醒/高亮) -->
|
||||
<color name="yellow_light">#FFF59D</color> <!-- 浅黄(次要提醒) -->
|
||||
<color name="yellow_normal">#FFC107</color> <!-- 标准黄(警告提示/高亮) -->
|
||||
<color name="yellow_dark">#FFA000</color> <!-- 深黄(重要警告) -->
|
||||
<!-- 橙色系(常用:提醒/进度/活力色) -->
|
||||
<color name="orange_normal">#FF9800</color> <!-- 标准橙(提醒按钮/进度) -->
|
||||
<!-- 紫色系(常用:特殊强调/个性按钮) -->
|
||||
<color name="purple_normal">#9C27B0</color> <!-- 标准紫(特殊功能按钮) -->
|
||||
|
||||
<!-- ============== 透明色(遮罩/背景叠加) ============== -->
|
||||
<color name="transparent">#00000000</color> <!-- 全透明 -->
|
||||
<color name="black_transparent_50">#80000000</color> <!-- 50%透明黑(遮罩) -->
|
||||
|
||||
|
||||
|
||||
<!-- 1. 不透明灰色(常用深浅梯度,直接用) -->
|
||||
<color name="gray_100">#F5F5F5</color> <!-- 极浅灰(接近白色,背景用) -->
|
||||
<color name="gray_200">#EEEEEE</color> <!-- 浅灰(卡片/分割线背景) -->
|
||||
<color name="gray_300">#E0E0E0</color> <!-- 中浅灰(边框/次要背景) -->
|
||||
<color name="gray_400">#BDBDBD</color> <!-- 中灰(次要文字/图标) -->
|
||||
<color name="gray_500">#9E9E9E</color> <!-- 标准中灰(常用辅助文字) -->
|
||||
<color name="gray_600">#757575</color> <!-- 中深灰(常规辅助文字) -->
|
||||
<color name="gray_700">#616161</color> <!-- 深灰(重要辅助文字) -->
|
||||
<color name="gray_800">#424242</color> <!-- 极深灰(接近黑色,标题副文本) -->
|
||||
<color name="gray_900">#212121</color> <!-- 近黑色(特殊场景用) -->
|
||||
|
||||
<!-- 2. 半透明灰色(带透明度,遮罩/蒙层用) -->
|
||||
<color name="gray_transparent_30">#4D9E9E9E</color> <!-- 30%透明中灰(A=4D) -->
|
||||
<color name="gray_transparent_50">#809E9E9E</color> <!-- 50%透明中灰(A=80) -->
|
||||
<color name="gray_transparent_70">#B39E9E9E</color> <!-- 70%透明中灰(A=B3) -->
|
||||
|
||||
<color name="gray_light">#EEE</color> <!-- 等价 #EEEEEE(浅灰) -->
|
||||
<color name="gray_mid">#999</color> <!-- 等价 #999999(中灰) -->
|
||||
<color name="gray_dark">#666</color> <!-- 等价 #666666(深灰) -->
|
||||
<color name="gray_black">#333</color> <!-- 等价 #333333(极深灰) -->
|
||||
|
||||
<!-- 50% 透明中灰(弹窗遮罩常用) -->
|
||||
<color name="mask_gray">#809E9E9E</color>
|
||||
<!-- 30% 透明深灰(背景叠加) -->
|
||||
<color name="bg_overlay_gray">#4D424242</color>
|
||||
|
||||
<!-- 1. 常规灰色(按钮默认态,常用中灰) -->
|
||||
<color name="btn_gray_normal">#9E9E9E</color>
|
||||
<!-- 2. 按压深色(按钮点击态,加深一级,提升交互感) -->
|
||||
<color name="btn_gray_pressed">#757575</color>
|
||||
<!-- 3. 禁用灰色(按钮不可点击态,浅灰) -->
|
||||
<color name="btn_gray_disabled">#E0E0E0</color>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- 定义一个名为text_size_normal的尺寸,值为16sp -->
|
||||
<dimen name="text_size_normal">16sp</dimen>
|
||||
<!-- 定义一个名为margin_small的尺寸,值为8dp -->
|
||||
<dimen name="margin_small">8dp</dimen>
|
||||
<!-- 定义一个名为image_width的尺寸,值为200dp -->
|
||||
<dimen name="button_height">24dp</dimen>
|
||||
<dimen name="log_text_size">12dp</dimen>
|
||||
<dimen name="log_text_padding">2dp</dimen>
|
||||
|
||||
<dimen name="log_button_width">65dp</dimen>
|
||||
<dimen name="log_button_height">34dp</dimen>
|
||||
|
||||
<dimen name="log_checkbox_width">100dp</dimen>
|
||||
<dimen name="log_checkbox_height">20dp</dimen>
|
||||
|
||||
<dimen name="log_spinner_width">60dp</dimen>
|
||||
<dimen name="log_spinner_height">16dp</dimen>
|
||||
<dimen name="log_spinner_item_width">@dimen/log_spinner_width</dimen>
|
||||
<dimen name="log_spinner_item_height">@dimen/log_spinner_height</dimen>
|
||||
<dimen name="log_spinner_text_size">@dimen/log_text_size</dimen>
|
||||
<dimen name="log_spinner_text_padding">@dimen/log_text_padding</dimen>
|
||||
|
||||
</resources>
|
||||
|
||||
34
winboll/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# WinBoLL
|
||||
|
||||
#### 介绍
|
||||
WinBoLL 网站浏览器。
|
||||
|
||||
#### 软件架构
|
||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
|
||||
|
||||
|
||||
#### Gradle 编译说明
|
||||
调试版编译命令 :gradle assembleBetaDebug
|
||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh winboll
|
||||
|
||||
#### 使用说明
|
||||
|
||||
#### 参与贡献
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 新建 Feat_xxx 分支
|
||||
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
|
||||
4. 新建 Pull Request
|
||||
|
||||
|
||||
#### 特技
|
||||
|
||||
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||
|
||||
#### 参考文档
|
||||
@@ -81,8 +81,13 @@ dependencies {
|
||||
api 'com.github.bumptech.glide:glide:4.9.0'
|
||||
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
|
||||
api 'cc.winboll.studio:libaes:15.11.8'
|
||||
api 'cc.winboll.studio:libappbase:15.11.6'
|
||||
// WinBoLL库 nexus.winboll.cc 地址
|
||||
//api 'cc.winboll.studio:libaes:15.12.0'
|
||||
//api 'cc.winboll.studio:libappbase:15.12.2'
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
api 'com.github.ZhanGSKen:AES:aes-v15.12.1'
|
||||
api 'com.github.ZhanGSKen:APPBase:appbase-v15.12.2'
|
||||
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Fri Dec 05 12:58:53 GMT 2025
|
||||
stageCount=6
|
||||
#Sun Dec 07 04:17:43 GMT 2025
|
||||
stageCount=8
|
||||
libraryProject=
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.5
|
||||
buildCount=20
|
||||
baseBetaVersion=15.11.6
|
||||
publishVersion=15.11.7
|
||||
buildCount=1
|
||||
baseBetaVersion=15.11.8
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher_stage"
|
||||
android:roundIcon="@drawable/ic_launcher_stage"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:roundIcon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/MyAppTheme"
|
||||
android:resizeableActivity="true"
|
||||
@@ -37,7 +37,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher_stage"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="true">
|
||||
|
||||
<intent-filter>
|
||||
@@ -280,4 +280,4 @@
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -59,6 +59,7 @@ public class App extends GlobalApplication {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
//setIsDebugging(false);
|
||||
|
||||
WinBoLLActivityManager.init(this);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package cc.winboll.studio.winboll;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@@ -9,37 +10,38 @@ import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.models.APPInfo;
|
||||
import cc.winboll.studio.libaes.models.DrawerMenuBean;
|
||||
import cc.winboll.studio.libaes.unittests.TestAButtonFragment;
|
||||
import cc.winboll.studio.libaes.unittests.TestViewPageFragment;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
import cc.winboll.studio.winboll.activities.AboutActivity;
|
||||
import cc.winboll.studio.winboll.activities.SettingsActivity;
|
||||
import cc.winboll.studio.winboll.fragments.BrowserFragment;
|
||||
import java.util.ArrayList;
|
||||
import android.content.Intent;
|
||||
import cc.winboll.studio.libaes.activitys.AboutActivity;
|
||||
|
||||
public class MainActivity extends DrawerFragmentActivity implements IWinBoLLActivity {
|
||||
public class MainActivity extends DrawerFragmentActivity {
|
||||
|
||||
|
||||
public static final String TAG = "MainActivity";
|
||||
|
||||
BrowserFragment mBrowserFragment;
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
// ------------------- 新增:Handler 消息定义(接收URL历史更新消息) -------------------
|
||||
// 消息标识:URL加载历史更新(刷新抽屉菜单的历史列表)
|
||||
public static final int MSG_URLLOADHISTORY_UPDATE = 1002;
|
||||
// 自定义Handler(接收应用内消息,如BrowserFragment发送的历史更新消息)
|
||||
private static Handler _mMainHandler;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// ------------------- 新增:初始化MainActivity的Handler(关键) -------------------
|
||||
initMainHandler();
|
||||
|
||||
if (mBrowserFragment == null) {
|
||||
mBrowserFragment = new BrowserFragment();
|
||||
addFragment(mBrowserFragment);
|
||||
@@ -47,28 +49,75 @@ public class MainActivity extends DrawerFragmentActivity implements IWinBoLLActi
|
||||
showFragment(mBrowserFragment);
|
||||
}
|
||||
|
||||
public static void sendMessage(Message msg) {
|
||||
_mMainHandler.sendMessage(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Handler(接收MSG_URLLOADHISTORY_UPDATE消息,刷新抽屉历史菜单)
|
||||
*/
|
||||
private void initMainHandler() {
|
||||
// 清理旧数据
|
||||
if (_mMainHandler != null) {
|
||||
_mMainHandler.removeCallbacksAndMessages(null);
|
||||
_mMainHandler = null;
|
||||
}
|
||||
|
||||
// Java 7 匿名内部类实现Handler(主线程创建,安全更新UI/抽屉菜单)
|
||||
_mMainHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
switch (msg.what) {
|
||||
case MSG_URLLOADHISTORY_UPDATE:
|
||||
// 处理URL历史更新消息:刷新抽屉菜单的历史列表
|
||||
LogUtils.d(TAG, "收到URL历史更新消息,刷新抽屉菜单");
|
||||
refreshUrlHistoryDrawerMenu();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initDrawerMenuItemList(ArrayList<DrawerMenuBean> listDrawerMenu) {
|
||||
super.initDrawerMenuItemList(listDrawerMenu);
|
||||
LogUtils.d(TAG, "initDrawerMenuItemList");
|
||||
//listDrawerMenu.clear();
|
||||
// 添加抽屉菜单项
|
||||
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, TestAButtonFragment.TAG));
|
||||
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, TestViewPageFragment.TAG));
|
||||
//LogUtils.d(TAG, "initDrawerMenuItemList");
|
||||
// 加载URL历史菜单(初始化时加载)
|
||||
refreshUrlHistoryDrawerMenu();
|
||||
notifyDrawerMenuDataChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reinitDrawerMenuItemList(ArrayList<DrawerMenuBean> listDrawerMenu) {
|
||||
super.reinitDrawerMenuItemList(listDrawerMenu);
|
||||
LogUtils.d(TAG, "reinitDrawerMenuItemList");
|
||||
//listDrawerMenu.clear();
|
||||
// 添加抽屉菜单项
|
||||
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, TestAButtonFragment.TAG));
|
||||
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, TestViewPageFragment.TAG));
|
||||
//LogUtils.d(TAG, "reinitDrawerMenuItemList");
|
||||
// 重新加载URL历史菜单(菜单重置时加载)
|
||||
refreshUrlHistoryDrawerMenu();
|
||||
notifyDrawerMenuDataChanged();
|
||||
}
|
||||
|
||||
void loadUrlLoadHistotyMenu(ArrayList<DrawerMenuBean> listDrawerMenu) {
|
||||
listDrawerMenu.clear();
|
||||
if (BrowserFragment._mUrlLoadHistory != null) {
|
||||
for (String url : BrowserFragment._mUrlLoadHistory) {
|
||||
listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, url));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------- 新增:刷新URL历史抽屉菜单(提取独立方法,复用) -------------------
|
||||
private void refreshUrlHistoryDrawerMenu() {
|
||||
// 获取抽屉菜单列表,重新加载历史数据并刷新
|
||||
ArrayList<DrawerMenuBean> drawerMenuList = super.malDrawerMenuItem; // 假设父类提供获取菜单列表的方法
|
||||
if (drawerMenuList != null) {
|
||||
loadUrlLoadHistotyMenu(drawerMenuList); // 重新加载更新后的历史数据
|
||||
notifyDrawerMenuDataChanged(); // 通知抽屉菜单刷新UI
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DrawerFragmentActivity.ActivityType initActivityType() {
|
||||
return DrawerFragmentActivity.ActivityType.Main;
|
||||
@@ -77,50 +126,75 @@ public class MainActivity extends DrawerFragmentActivity implements IWinBoLLActi
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
// if (App.isDebugging()) {
|
||||
// getMenuInflater().inflate(cc.winboll.studio.libapputils.R.menu.toolbar_studio_debug, menu);
|
||||
// }
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
super.onItemClick(parent, view, position, id);
|
||||
switch (position) {
|
||||
case 0 : {
|
||||
if (mBrowserFragment == null) {
|
||||
mBrowserFragment = new BrowserFragment();
|
||||
addFragment(mBrowserFragment);
|
||||
}
|
||||
showFragment(mBrowserFragment);
|
||||
break;
|
||||
}
|
||||
if (mBrowserFragment != null && mBrowserFragment.getBrowserHandler() != null) {
|
||||
Message msg = Message.obtain();
|
||||
msg.what = BrowserFragment.MSG_HISTORY_POSITION;
|
||||
msg.obj = position;
|
||||
mBrowserFragment.getBrowserHandler().sendMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int nItemId = item.getItemId();
|
||||
if(item.getItemId() == R.id.item_home) {
|
||||
// 关键:获取BrowserFragment的Handler
|
||||
if (nItemId == R.id.item_home) {
|
||||
// 发送MSG_HOMEPAGE消息给BrowserFragment
|
||||
if (mBrowserFragment != null && mBrowserFragment.getBrowserHandler() != null) {
|
||||
// 创建消息(Java 7 显式创建Message)
|
||||
Message msg = Message.obtain();
|
||||
msg.what = BrowserFragment.MSG_HOMEPAGE; // 指定消息标识
|
||||
// 发送消息(可携带数据,如msg.obj = "额外参数";)
|
||||
msg.what = BrowserFragment.MSG_HOMEPAGE;
|
||||
mBrowserFragment.getBrowserHandler().sendMessage(msg);
|
||||
}
|
||||
}if (item.getItemId() == R.id.item_settings) {
|
||||
} else if (nItemId == R.id.item_settings) {
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), SettingsActivity.class);
|
||||
} else if (item.getItemId() == R.id.item_log) {
|
||||
WinBoLLActivityManager.getInstance().startLogActivity(getApplicationContext());
|
||||
} else if (nItemId == R.id.item_about) {
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), AboutActivity.class);
|
||||
return true;
|
||||
}
|
||||
} else if (nItemId == cc.winboll.studio.libaes.R.id.item_about) {
|
||||
Intent intent = new Intent(getApplicationContext(), AboutActivity.class);
|
||||
APPInfo appInfo = genDefaultAPPInfo();
|
||||
intent.putExtra(AboutActivity.EXTRA_APPINFO, appInfo);
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), intent, AboutActivity.class);
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
APPInfo genDefaultAPPInfo() {
|
||||
String szBranchName = "winboll";
|
||||
APPInfo appInfo = new APPInfo();
|
||||
appInfo.setAppName("WinBoLL");
|
||||
appInfo.setAppIcon(cc.winboll.studio.libaes.R.drawable.ic_winboll);
|
||||
appInfo.setAppDescription("WinBoLL 网站浏览器。");
|
||||
appInfo.setAppGitName("WinBoLL");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(szBranchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=WinBoLL");
|
||||
appInfo.setAppAPKName("WinBoLL");
|
||||
appInfo.setAppAPKFolderName("WinBoLL");
|
||||
return appInfo;
|
||||
}
|
||||
|
||||
// ------------------- 新增:对外提供Handler(供其他组件发送消息,如BrowserFragment) -------------------
|
||||
public Handler getMainHandler() {
|
||||
return _mMainHandler;
|
||||
}
|
||||
|
||||
// ------------------- 新增:生命周期管理(防止Handler内存泄漏) -------------------
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// 清除Handler所有消息和回调,避免内存泄漏
|
||||
if (_mMainHandler != null) {
|
||||
_mMainHandler.removeCallbacksAndMessages(null);
|
||||
_mMainHandler = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package cc.winboll.studio.winboll.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/29 13:30
|
||||
* @Describe 应用介绍窗口
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.winboll.BuildConfig;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.models.APPInfo;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libaes.views.AboutView;
|
||||
|
||||
public class AboutActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "AboutActivity";
|
||||
|
||||
Context mContext;
|
||||
Toolbar mToolbar;
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mContext = this;
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(TAG);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
AboutView aboutView = CreateAboutView();
|
||||
// 在 Activity 的 onCreate 或其他生命周期方法中调用
|
||||
// LinearLayout layout = new LinearLayout(this);
|
||||
// layout.setOrientation(LinearLayout.VERTICAL);
|
||||
// // 创建布局参数(宽度和高度)
|
||||
// ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||
// ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
// ViewGroup.LayoutParams.MATCH_PARENT
|
||||
// );
|
||||
// addContentView(aboutView, params);
|
||||
|
||||
LinearLayout layout = findViewById(R.id.aboutviewroot_ll);
|
||||
// 创建布局参数(宽度和高度)
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
layout.addView(aboutView, params);
|
||||
|
||||
WinBoLLActivityManager.getInstance().add(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
WinBoLLActivityManager.getInstance().registeRemove(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public AboutView CreateAboutView() {
|
||||
String szBranchName = "winboll";
|
||||
APPInfo appInfo = new APPInfo();
|
||||
appInfo.setAppName("WinBoLL");
|
||||
appInfo.setAppIcon(cc.winboll.studio.libaes.R.drawable.ic_winboll);
|
||||
appInfo.setAppDescription("WinBoLL Description");
|
||||
appInfo.setAppGitName("APPBase");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(szBranchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(szBranchName);
|
||||
appInfo.setAppHomePage("https://discuz.winboll.cc/forum.php?mod=viewthread&tid=3&extra=page%3D1");
|
||||
appInfo.setAppAPKName("WinBoLL");
|
||||
appInfo.setAppAPKFolderName("WinBoLL");
|
||||
//appInfo.setIsAddDebugTools(false);
|
||||
//appInfo.setIsAddDebugTools(BuildConfig.DEBUG);
|
||||
return new AboutView(mContext, appInfo);
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,11 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import cc.winboll.studio.winboll.MainActivity;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
import cc.winboll.studio.winboll.views.WinBoLLView;
|
||||
import java.util.ArrayList;
|
||||
import android.app.Activity;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
@@ -35,10 +38,13 @@ public class BrowserFragment extends Fragment implements View.OnClickListener, W
|
||||
private Button mBtnBack;
|
||||
private ProgressBar mProgressBar;
|
||||
private WinBoLLView mWinBoLLView;
|
||||
public static ArrayList<String> _mUrlLoadHistory = new ArrayList<String>();
|
||||
|
||||
// ------------------- 新增:Handler 消息定义(应用内通信) -------------------
|
||||
// 消息标识:跳转首页(winboll.cc)
|
||||
public static final int MSG_HOMEPAGE = 1001;
|
||||
// 跳转到历史记录位置
|
||||
public static final int MSG_HISTORY_POSITION = 1002;
|
||||
// 自定义Handler(接收应用内其他页面发送的消息)
|
||||
private Handler mBrowserHandler;
|
||||
|
||||
@@ -52,6 +58,9 @@ public class BrowserFragment extends Fragment implements View.OnClickListener, W
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
// 加载布局(Java 7 显式强转,无菱形语法)
|
||||
View view = inflater.inflate(R.layout.fragment_browser, container, false);
|
||||
// 清理旧历史记录
|
||||
_mUrlLoadHistory.clear();
|
||||
|
||||
// 初始化控件
|
||||
initViews(view);
|
||||
// 绑定事件
|
||||
@@ -129,7 +138,16 @@ public class BrowserFragment extends Fragment implements View.OnClickListener, W
|
||||
mEtUrl.setText(homeUrl);
|
||||
showToast("已跳转至首页");
|
||||
break;
|
||||
// 可扩展:添加其他消息标识(如MSG_OPEN_URL、MSG_REFRESH等)
|
||||
case MSG_HISTORY_POSITION:
|
||||
int position = (int)msg.obj;
|
||||
if(-1 < position && position < _mUrlLoadHistory.size()) {
|
||||
// 处理“跳转首页”消息:加载winboll.cc
|
||||
String historyUrl = _mUrlLoadHistory.get(position);
|
||||
mWinBoLLView.loadUrlSafe(historyUrl);
|
||||
mEtUrl.setText(historyUrl);
|
||||
//showToast("已跳转至" + historyUrl);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -221,6 +239,7 @@ public class BrowserFragment extends Fragment implements View.OnClickListener, W
|
||||
// 页面加载完成:更新输入框URL
|
||||
if (mEtUrl != null && url != null) {
|
||||
mEtUrl.setText(url);
|
||||
addUrlToHistory(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,5 +292,28 @@ public class BrowserFragment extends Fragment implements View.OnClickListener, W
|
||||
mBrowserHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 在 BrowserFragment 中添加以下代码(Java 7 语法)
|
||||
/**
|
||||
* 发送URL历史更新消息给MainActivity(当历史列表变化时调用)
|
||||
*/
|
||||
private void sendUrlHistoryUpdateMsg() {
|
||||
Message msg = Message.obtain();
|
||||
msg.what = MainActivity.MSG_URLLOADHISTORY_UPDATE;
|
||||
MainActivity.sendMessage(msg);
|
||||
}
|
||||
|
||||
// 调用时机示例(在BrowserFragment加载URL并更新历史列表后调用)
|
||||
// 假设BrowserFragment中有添加URL到历史的方法:
|
||||
private void addUrlToHistory(String url) {
|
||||
if (_mUrlLoadHistory == null) {
|
||||
_mUrlLoadHistory = new ArrayList<String>();
|
||||
}
|
||||
if (!_mUrlLoadHistory.contains(url)) {
|
||||
_mUrlLoadHistory.add(0, url);
|
||||
// 关键:添加历史后发送更新消息,通知MainActivity刷新抽屉菜单
|
||||
sendUrlHistoryUpdateMsg();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#ff000000"
|
||||
android:pathData="M4,1C2.89,1 2,1.89 2,3V7C2,8.11 2.89,9 4,9H1V11H13V9H10C11.11,9 12,8.11 12,7V3C12,1.89 11.11,1 10,1H4M4,3H10V7H4V3M3,12V14H5V12H3M14,13C12.89,13 12,13.89 12,15V19C12,20.11 12.89,21 14,21H11V23H23V21H20C21.11,21 22,20.11 22,19V15C22,13.89 21.11,13 20,13H14M3,15V17H5V15H3M14,15H20V19H14V15M3,18V20H5V18H3M6,18V20H8V18H6M9,18V20H11V18H9Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#ff000000"
|
||||
android:pathData="M4,1C2.89,1 2,1.89 2,3V7C2,8.11 2.89,9 4,9H1V11H13V9H10C11.11,9 12,8.11 12,7V3C12,1.89 11.11,1 10,1H4M4,3H10V7H4V3M14,13C12.89,13 12,13.89 12,15V19C12,20.11 12.89,21 14,21H11V23H23V21H20C21.11,21 22,20.11 22,19V15C22,13.89 21.11,13 20,13H14M3.88,13.46L2.46,14.88L4.59,17L2.46,19.12L3.88,20.54L6,18.41L8.12,20.54L9.54,19.12L7.41,17L9.54,14.88L8.12,13.46L6,15.59L3.88,13.46M14,15H20V19H14V15Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#ff000000"
|
||||
android:pathData="M24,7H22V13H24V7M24,15H22V17H24V15M20,6C20,4.9 19.1,4 18,4H2C0.9,4 0,4.9 0,6V18C0,19.1 0.9,20 2,20H18C19.1,20 20,19.1 20,18V6M18,6L10,11L2,6H18M18,18H2V8L10,13L18,8V18Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="1565dp"
|
||||
android:height="1565dp"
|
||||
android:viewportWidth="1565"
|
||||
android:viewportHeight="1565">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="4.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="10"
|
||||
android:pathData="M793.8 222.7C782.5 239.2 736.3 298.6 685.6 361.9 647 410.2 633.3 428.9 633.6 432.7 633.7 433.4 633.8 434.6 633.9 435.3 634.1 437.8 647.9 440.7 663.8 441.5 706.7 443.8 717 445.3 724.9 450.6 733.7 456.4 735.9 467.4 736 504.2 736 504.2 736 521.9 736 521.9 736 521.9 730.8 522.4 730.8 522.4 727.9 522.8 691.8 525.3 650.5 528 609.3 530.8 571.9 533.3 567.5 533.6 531.8 536 503.4 540.5 487.4 546 475 550.3 442.2 566.5 428.2 575.2 392.1 597.6 360.1 629.2 338 664.3 317 697.7 304.6 729.6 292.5 781 287.8 801 283.9 805.6 267.2 811 255.1 814.9 248.7 818.3 243.4 823.5 239.7 827.2 239.2 828.4 237.7 836.5 236.7 841.5 235.5 850 235 855.5 234.3 863.9 232.6 911.1 232.6 924.5 232.6 934.6 234.2 959.8 235 963 237.4 972 242.3 975.8 258.7 981.7 271.7 986.3 277.4 989.5 280.4 993.9 283.1 997.9 286.5 1008.7 287.4 1016.5 289.3 1031.7 293.3 1050.2 297.7 1064 306.4 1091.9 317.6 1107.7 330 1109.5 332.4 1109.9 334.7 1110.3 335 1110.5 335.3 1110.6 339.1 1111.1 343.5 1111.4 347.9 1111.8 353.8 1112.3 356.5 1112.5 370 1113.7 407.8 1115.8 430 1116.5 439.6 1116.8 453.4 1117.3 460.5 1117.5 467.7 1117.8 482.3 1118.2 493 1118.5 503.7 1118.8 520.8 1119.2 531 1119.5 587.8 1121.1 683 1122 794.5 1122 918.8 1122 996.1 1121 1075 1118.5 1083.5 1118.2 1098.6 1117.8 1108.5 1117.4 1118.4 1117.1 1129.4 1116.7 1133 1116.5 1136.6 1116.3 1144.7 1115.8 1151 1115.5 1183.3 1114 1207.5 1110.9 1221.5 1106.3 1235.3 1101.9 1244.8 1094.1 1249.9 1083.2 1253.5 1075.4 1253.7 1074.4 1259 1044 1266.4 1001.7 1269.1 993.5 1276.9 989.2 1278.9 988.1 1284.7 986.1 1289.9 984.6 1305.8 980.1 1313.3 975.5 1314.4 969.7 1316.2 959.5 1317.1 881.4 1315.7 859.5 1314 834.8 1313.1 831.4 1307 824.7 1301.2 818.3 1295 814.8 1283 811 1278 809.5 1272.4 807 1270.5 805.6 1264 800.6 1259.5 790.1 1252.5 763 1243.5 728.4 1235.9 706.8 1224.6 683.5 1202.3 637.7 1167.5 602.5 1111.8 569.1 1087.6 554.7 1054.4 542.7 1028.1 539 1024.1 538.4 1020.5 537.8 1020.1 537.6 1019.8 537.4 1016.4 536.9 1012.7 536.5 1009 536.2 1005.1 535.7 1004.2 535.5 999.7 534.7 988.4 533.6 973.5 532.5 969.7 532.2 964.5 531.8 962 531.5 959.5 531.2 940.4 530.1 919.5 529 851.7 525.4 836.1 523.7 829.5 519.4 821.2 514 825.2 495.6 837.6 481.8 843.8 474.9 844.2 474.3 856.8 448 862.6 436 868.9 425.2 879.9 408.7 888.2 396.2 895 385.5 895 385.1 895 384.6 889.7 383.3 883.3 382 851.1 375.9 812.7 367.8 806.9 365.9 792.4 361.2 790.7 358.7 790.2 342.5 789.7 326.8 792.4 303.4 800 258.5 802.1 246.1 803.3 220.9 801.9 219.6 801.7 219.4 800.5 218.9 799.3 218.6 797.4 218 796.5 218.7 793.8 222.7Z"/>
|
||||
<path
|
||||
android:fillColor="#FF62686C"
|
||||
android:strokeColor="#FF62686C"
|
||||
android:strokeWidth="4.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="10"
|
||||
android:pathData="M570.4 704.1C553.5 706.1 539.7 713 532.9 722.7 523.5 736.3 519.9 764.2 519.6 826 519.5 859.4 519.7 860.6 530.1 872.9 543.4 888.6 560.4 900 574.1 902.1 586.7 904 601.5 898.9 615.7 887.5 632.5 874.1 633.5 868.9 632.7 793.6 632.2 742.5 631.6 736 627 725.2 620.5 710 596.2 700.9 570.4 704.1Z"/>
|
||||
<path
|
||||
android:fillColor="#FF62686C"
|
||||
android:strokeColor="#FF62686C"
|
||||
android:strokeWidth="4.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="10"
|
||||
android:pathData="M949.2 705.5C935.1 708.7 927 716.8 922.6 732.4 918.4 746.7 917.8 757.3 917.3 816.5 917.1 847.8 917.3 874 917.7 874.6 919.2 876.7 945.3 892.3 955.5 897.1 973.4 905.5 983.5 905.4 978.9 896.8 977.1 893.4 979.1 892.6 985.9 893.9 993.6 895.4 997.8 894.3 1005.2 889.1 1010.9 885 1020.2 873.6 1023.8 866.5 1029.6 854.9 1031.1 841.6 1031.1 800.5 1031.1 750.1 1028.6 737.6 1014.9 720.4 1009.4 713.5 1004.6 709.8 996.9 706.8 992.2 704.9 989.1 704.6 973.5 704.4 960.6 704.1 953.7 704.5 949.2 705.5Z"/>
|
||||
<path
|
||||
android:fillColor="#FF62686C"
|
||||
android:strokeColor="#FF62686C"
|
||||
android:strokeWidth="4.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="10"
|
||||
android:pathData="M717 988.7C671.1 990.4 640.9 994.2 635.6 998.8 631.5 1002.5 636.3 1017.5 643.6 1024 650.9 1030.3 661.7 1032.5 694 1034 721.9 1035.3 829.3 1035.3 857 1034 891.4 1032.4 901.4 1029.9 908.9 1021.3 914.5 1015 917.9 1003.4 915.2 999.4 910.4 992.1 856 987.9 772 988.2 745.9 988.3 721.1 988.5 717 988.7Z"/>
|
||||
</vector>
|
||||
@@ -9,5 +9,5 @@
|
||||
android:top="0dp"
|
||||
android:right="0dp"
|
||||
android:bottom="0dp"
|
||||
android:drawable="@drawable/ic_iw"/>
|
||||
android:drawable="@drawable/ic_launcher"/>
|
||||
</layer-list>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:clickable="true"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp">
|
||||
<item android:drawable="@drawable/ic_launcher_background"/>
|
||||
<item
|
||||
android:left="0dp"
|
||||
android:top="0dp"
|
||||
android:right="0dp"
|
||||
android:bottom="0dp"
|
||||
android:drawable="@drawable/ic_launcher_foreground_disable"/>
|
||||
</layer-list>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M16.61,15.15C16.15,15.15 15.77,14.78 15.77,14.32S16.15,13.5 16.61,13.5H16.61C17.07,13.5 17.45,13.86 17.45,14.32C17.45,14.78 17.07,15.15 16.61,15.15M7.41,15.15C6.95,15.15 6.57,14.78 6.57,14.32C6.57,13.86 6.95,13.5 7.41,13.5H7.41C7.87,13.5 8.24,13.86 8.24,14.32C8.24,14.78 7.87,15.15 7.41,15.15M16.91,10.14L18.58,7.26C18.67,7.09 18.61,6.88 18.45,6.79C18.28,6.69 18.07,6.75 18,6.92L16.29,9.83C14.95,9.22 13.5,8.9 12,8.91C10.47,8.91 9,9.24 7.73,9.82L6.04,6.91C5.95,6.74 5.74,6.68 5.57,6.78C5.4,6.87 5.35,7.08 5.44,7.25L7.1,10.13C4.25,11.69 2.29,14.58 2,18H22C21.72,14.59 19.77,11.7 16.91,10.14H16.91Z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#FF808080"
|
||||
android:pathData="M16.61,15.15C16.15,15.15 15.77,14.78 15.77,14.32S16.15,13.5 16.61,13.5H16.61C17.07,13.5 17.45,13.86 17.45,14.32C17.45,14.78 17.07,15.15 16.61,15.15M7.41,15.15C6.95,15.15 6.57,14.78 6.57,14.32C6.57,13.86 6.95,13.5 7.41,13.5H7.41C7.87,13.5 8.24,13.86 8.24,14.32C8.24,14.78 7.87,15.15 7.41,15.15M16.91,10.14L18.58,7.26C18.67,7.09 18.61,6.88 18.45,6.79C18.28,6.69 18.07,6.75 18,6.92L16.29,9.83C14.95,9.22 13.5,8.9 12,8.91C10.47,8.91 9,9.24 7.73,9.82L6.04,6.91C5.95,6.74 5.74,6.68 5.57,6.78C5.4,6.87 5.35,7.08 5.44,7.25L7.1,10.13C4.25,11.69 2.29,14.58 2,18H22C21.72,14.59 19.77,11.7 16.91,10.14H16.91Z"/>
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/aboutviewroot_ll">
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
android:id="@+id/toolbar_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_iw"
|
||||
android:src="@drawable/ic_launcher"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="6dp"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |