This commit is contained in:
zxj
2025-07-08 15:40:39 +08:00
parent d2698ed60f
commit 4501b6d5fc
132 changed files with 8998 additions and 0 deletions
@@ -0,0 +1,50 @@
package com.sw.inbound.utils
import android.content.Context
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
/**
* Context 工具类
*/
object ContextUtils {
// ========== Composable 内获取 ==========
/**
* 获取当前 Composable 的 Activity Context
* 只能在 @Composable 函数中调用
*/
@Composable
fun getActivityContext(): Context {
return LocalContext.current
}
/**
* 获取当前 Composable 的 View
*/
@Composable
fun getLocalView(): View {
return LocalView.current
}
// ========== 非 Composable 环境获取 ==========
/**
* 通过静态 Application 引用获取
* 需要在 Application 类中初始化
*/
private var _applicationContext: Context? = null
fun initAppContext(context: Context) {
_applicationContext = context.applicationContext
}
fun getAppContext(): Context {
return _applicationContext ?: throw IllegalStateException(
"Application context not initialized. Call initAppContext() first."
)
}
}
@@ -0,0 +1,237 @@
package com.sw.inbound.utils
import android.app.ActivityManager
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Process
import com.sw.inbound.MainActivity
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.system.exitProcess
/**
* 崩溃处理
*/
class CrashHandler private constructor(private val context: Context) :
Thread.UncaughtExceptionHandler {
companion object {
private const val TAG = "CrashHandler"
private const val CRASH_REPORTS_DIR = "crash_reports"
private const val LOG_LINES = 500 // 收集最近500行日志
@Volatile
private var instance: CrashHandler? = null
fun init(context: Context) {
if (instance == null) {
synchronized(CrashHandler::class.java) {
if (instance == null) {
instance = CrashHandler(context.applicationContext)
}
}
}
}
fun getCrashReportFiles(context: Context): Array<File> {
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
return if (crashDir.exists() && crashDir.isDirectory) {
crashDir.listFiles { _, name -> name.endsWith(".log") } ?: emptyArray()
} else {
emptyArray()
}
}
fun clearCrashReports(context: Context) {
getCrashReportFiles(context).forEach { it.delete() }
}
}
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
init {
Thread.setDefaultUncaughtExceptionHandler(this)
}
override fun uncaughtException(thread: Thread, ex: Throwable) {
handleException(thread, ex)
// 如果系统提供了默认的异常处理器,则交给系统去结束程序
// 否则自己结束程序
defaultHandler?.uncaughtException(thread, ex) ?: run {
Process.killProcess(Process.myPid())
exitProcess(1)
}
}
/**
* 自动重启app
*/
private fun restartApp() {
// 延迟1秒后重启应用
Handler(Looper.getMainLooper()).postDelayed({
val intent = Intent(context, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent)
Process.killProcess(Process.myPid())
exitProcess(1)
}, 1000)
}
private fun handleException(thread: Thread, ex: Throwable) {
// 收集设备信息和异常信息
val crashInfo = collectCrashInfo(thread, ex)
// 保存日志文件
saveCrashInfoToFile(crashInfo)
// 这里可以添加其他处理逻辑,比如上传到服务器等
}
private fun collectCrashInfo(thread: Thread, ex: Throwable): String {
return buildString {
// 收集设备信息
collectDeviceInfo(this)
// 收集应用日志
append("\n\n").append(collectLogs())
// 收集线程和异常信息
append("\n\n========== Thread & Exception Info ==========\n")
append("Thread: ${thread.name}\n")
append("Stack Trace:\n")
val sw = StringWriter()
val pw = PrintWriter(sw)
ex.printStackTrace(pw)
var cause: Throwable? = ex.cause
while (cause != null) {
cause.printStackTrace(pw)
cause = cause.cause
}
pw.close()
append(sw.toString())
}
}
private fun collectDeviceInfo(sb: StringBuilder) {
sb.append("========== Device Info ==========\n")
try {
// 应用信息
val pm = context.packageManager
val pi = pm.getPackageInfo(context.packageName, 0)
sb.append("App Version: ${pi.versionName}_${pi.versionCode}\n")
// Android 版本信息
sb.append("OS Version: ${Build.VERSION.RELEASE}_${Build.VERSION.SDK_INT}\n")
// 设备信息
sb.append("Vendor: ${Build.MANUFACTURER}\n")
sb.append("Model: ${Build.MODEL}\n")
sb.append("CPU ABI: ${Build.SUPPORTED_ABIS[0]}\n")
// 其他信息
sb.append("Locale: ${Locale.getDefault()}\n")
sb.append(
"Current Time: ${
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.getDefault()
).format(Date())
}\n"
)
// 内存信息
val memoryInfo = ActivityManager.MemoryInfo()
val activityManager =
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
activityManager.getMemoryInfo(memoryInfo)
sb.append("Available Memory: ${memoryInfo.availMem / (1024 * 1024)}MB\n")
sb.append("Total Memory: ${memoryInfo.totalMem / (1024 * 1024)}MB\n")
sb.append("Low Memory: ${memoryInfo.lowMemory}\n")
} catch (e: Exception) {
Timber.e(e, "Error while collecting device info")
sb.append("Error while collecting device info: ${e.message}\n")
}
}
private fun collectLogs(): String {
return buildString {
append("========== Application Logs ==========\n")
try {
val process = Runtime.getRuntime().exec("logcat -d -v threadtime")
val reader = process.inputStream.bufferedReader()
val logLines = reader.readLines()
val start = maxOf(0, logLines.size - LOG_LINES)
logLines.subList(start, logLines.size).forEach {
append(it).append("\n")
}
} catch (e: IOException) {
Timber.e(e, "Error collecting logs")
append("Error collecting logs: ${e.message}\n")
}
}
}
private fun saveCrashInfoToFile(crashInfo: String) {
try {
val time = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()).format(Date())
val fileName = "crash_$time.log"
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
if (!crashDir.exists() && !crashDir.mkdirs()) {
Timber.tag(TAG).e("Failed to create crash report directory")
return
}
val crashFile = File(crashDir, fileName)
FileOutputStream(crashFile).use { it.write(crashInfo.toByteArray()) }
Timber.tag(TAG).d("Crash info saved to: ${crashFile.absolutePath}")
} catch (e: Exception) {
Timber.tag(TAG).e(e, "Error saving crash info to file")
}
}
/**
* 清理旧的崩溃日志
*/
fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
if (!crashDir.exists() || !crashDir.isDirectory) return
val now = System.currentTimeMillis()
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
crashDir.listFiles()?.forEach { file ->
if (file.lastModified() < now - maxAgeMillis) {
file.delete()
}
}
}
}
@@ -0,0 +1,37 @@
package com.sw.inbound.utils
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* 时间格式化工具类
*/
object DateTimeUtils {
/**
* 获取完整中文日期格式(示例:2025年6月11日 星期三)
*/
fun getChineseDateString(date: Date = Date()): String {
return SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA).format(date)
}
/**
* 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30
*/
fun getChineseDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy年M月d日 EEEE HH:mm:ss", Locale.CHINA).format(date)
}
/**
* 实时时间流(每秒更新)
*/
fun realTimeChineseDateFlow() = flow {
while (true) {
emit(getChineseDateString())
delay(1000)
}
}
}
@@ -0,0 +1,90 @@
package com.sw.inbound.utils
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import java.io.File
object FileUtils {
/**
* 通过Uri删除文件
* @param context 上下文
* @param uri 文件Uri
* @return Boolean 是否删除成功
*/
fun deleteFileWithUri(context: Context, uri: Uri): Boolean {
return when {
// 1. 处理 content:// 类型的Uri (MediaStore)
uri.scheme.equals("content", ignoreCase = true) -> {
deleteContentUriFile(context, uri)
}
// 2. 处理 file:// 类型的Uri
uri.scheme.equals("file", ignoreCase = true) -> {
deleteFileUriFile(uri)
}
// 3. 其他情况尝试直接解析路径
else -> {
deleteFileFromPath(uri.path ?: return false)
}
}
}
// 删除Content Uri文件
private fun deleteContentUriFile(context: Context, uri: Uri): Boolean {
return try {
context.contentResolver.delete(uri, null, null) > 0
} catch (e: SecurityException) {
// Android 10+需要特殊处理
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
deleteMediaStoreFile(context, uri)
} else {
false
}
} catch (e: Exception) {
false
}
}
// Android 10+删除MediaStore文件
@RequiresApi(Build.VERSION_CODES.Q)
private fun deleteMediaStoreFile(context: Context, uri: Uri): Boolean {
val contentResolver = context.contentResolver
val projection = arrayOf(MediaStore.MediaColumns._ID)
return try {
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id =
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val contentUri = ContentUris.withAppendedId(uri, id)
contentResolver.delete(contentUri, null, null) > 0
} else {
false
}
} ?: false
} catch (e: Exception) {
false
}
}
// 删除File Uri文件
private fun deleteFileUriFile(uri: Uri): Boolean {
return try {
File(uri.path ?: return false).delete()
} catch (e: Exception) {
false
}
}
// 直接通过路径删除文件
private fun deleteFileFromPath(path: String): Boolean {
return try {
File(path).delete()
} catch (e: Exception) {
false
}
}
}
@@ -0,0 +1,123 @@
package com.sw.inbound.utils
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
object GsonUtils {
// 默认的 Gson 实例
private val defaultGson: Gson by lazy {
GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss") // 设置日期格式
// .disableHtmlEscaping() // 禁止转义HTML标签
.create()
}
/**
* 获取默认配置的 Gson 实例
*/
fun getGson(): Gson = defaultGson
/**
* 将对象转换为 JSON 字符串
* @param obj 要转换的对象
* @return JSON 字符串
*/
fun toJson(obj: Any?): String {
return if (obj == null) "" else defaultGson.toJson(obj)
}
/**
* 将 JSON 字符串转换为对象
* @param json JSON 字符串
* @param clazz 目标类
* @return 转换后的对象
*/
fun <T> fromJson(json: String?, clazz: Class<T>): T? {
if (json.isNullOrEmpty()) {
return null
}
return try {
defaultGson.fromJson(json, clazz)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为对象 (支持泛型)
* @param json JSON 字符串
* @param type 类型令牌,用于获取泛型类型
* @return 转换后的对象
*/
fun <T> fromJson(json: String?, type: Type): T? {
if (json.isNullOrEmpty()) {
return null
}
return try {
defaultGson.fromJson(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为 List 对象
* @param json JSON 字符串
* @param clazz List 中的元素类型
* @return 转换后的 List 对象
*/
fun <T> fromJsonList(json: String?, clazz: Class<T>): List<T>? {
if (json.isNullOrEmpty()) {
return null
}
return try {
val type = TypeToken.getParameterized(List::class.java, clazz).type
defaultGson.fromJson<List<T>>(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为 Map 对象
* @param json JSON 字符串
* @param keyClazz Map 的 key 类型
* @param valueClazz Map 的 value 类型
* @return 转换后的 Map 对象
*/
fun <K, V> fromJsonMap(
json: String?,
keyClazz: Class<K>,
valueClazz: Class<V>
): Map<K, V>? {
if (json.isNullOrEmpty()) {
return null
}
return try {
val type = TypeToken.getParameterized(Map::class.java, keyClazz, valueClazz).type
defaultGson.fromJson<Map<K, V>>(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将对象转换为另一种类型的对象
* @param obj 源对象
* @param clazz 目标类型
* @return 转换后的对象
*/
fun <T> convert(obj: Any?, clazz: Class<T>): T? {
if (obj == null) {
return null
}
return fromJson(toJson(obj), clazz)
}
}
@@ -0,0 +1,61 @@
package com.sw.inbound.utils
import android.content.Context
import android.net.Uri
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import timber.log.Timber
import java.io.File
/**
* file:///data/user/0/com.sw.inbound/cache/IMG_17515262353556856678814444882273.jpg
*/
object ImageUtils {
// 从Uri获取File
private fun getFileFromUri(context: Context, uri: Uri): File? {
return when (uri.scheme) {
"file" -> File(uri.path ?: return null)
"content" -> {
try {
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val cacheDir = context.cacheDir
val file = File.createTempFile(
"upload_${System.currentTimeMillis()}",
".jpg",
cacheDir
)
file.outputStream().use { output ->
inputStream.copyTo(output)
}
file
} catch (e: Exception) {
null
}
}
else -> null
}
}
fun genRequestPart(context: Context, imageUri: Uri): MultipartBody.Part? {
Timber.d("genRequestPart imageUri = $imageUri")
// 1. 从Uri获取文件
val file = getFileFromUri(context, imageUri)
if (file == null) {
Timber.e("getFileFromUri file is null")
return null
}
// 2. 创建请求体
val requestFile = RequestBody.create(
"application/octet-stream".toMediaTypeOrNull(),
file
)
val imagePart = MultipartBody.Part.createFormData(
"file",
file.name,
requestFile
)
return imagePart
}
}
@@ -0,0 +1,259 @@
package com.sw.inbound.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* Jetpack Compose 交互工具集
* 包含快速点击过滤、防抖、节流、双击检测、长按检测等功能
*/
object InteractionUtils {
// ======================== 点击过滤 ========================
/**
* 快速点击过滤器
* @param minInterval 最小点击间隔时间(毫秒),默认500ms
*/
class ClickFilter(private val minInterval: Long = 500L) {
private var lastClickTime: Long = 0
/**
* 处理点击事件
* @return Boolean 是否允许此次点击(true=允许,false=拦截)
*/
fun processClick(): Boolean {
val currentTime = System.currentTimeMillis()
return if (currentTime - lastClickTime > minInterval) {
lastClickTime = currentTime
true
} else {
false
}
}
/**
* 处理点击事件(带回调)
*/
fun processClick(block: () -> Unit) {
if (processClick()) {
block()
}
}
}
/**
* 记住点击过滤器
*/
@Composable
fun rememberClickFilter(minInterval: Long = 500L): ClickFilter {
return remember { ClickFilter(minInterval) }
}
// ======================== 防抖处理 ========================
/**
* 防抖处理器
*/
class Debouncer(
private val delayMillis: Long = 300L,
private val coroutineScope: CoroutineScope
) {
private var debounceJob: Job? = null
/**
* 执行防抖操作
*/
fun <T> debounce(value: T, action: (T) -> Unit) {
debounceJob?.cancel()
debounceJob = coroutineScope.launch {
delay(delayMillis)
action(value)
}
}
}
/**
* 记住防抖处理器
*/
@Composable
fun rememberDebouncer(
delayMillis: Long = 300L,
coroutineScope: CoroutineScope = rememberCoroutineScope()
): Debouncer {
return remember { Debouncer(delayMillis, coroutineScope) }
}
// ======================== 节流处理 ========================
/**
* 节流处理器
*/
class Throttler(private val timeoutMs: Long = 300L) {
private var lastRunTime: Long = 0
/**
* 执行节流操作
*/
fun throttle(block: () -> Unit) {
val now = System.currentTimeMillis()
if (now - lastRunTime > timeoutMs) {
lastRunTime = now
block()
}
}
}
/**
* 记住节流处理器
*/
@Composable
fun rememberThrottler(timeoutMs: Long = 300L): Throttler {
return remember { Throttler(timeoutMs) }
}
// ======================== 双击检测 ========================
/**
* 双击检测器
*/
class DoubleClickDetector(
private val timeout: Long = 300L,
private val onSingleClick: () -> Unit = {},
private val onDoubleClick: () -> Unit
) {
private var clickCount by mutableStateOf(0)
private var lastClickTime by mutableStateOf(0L)
/**
* 处理点击事件
*/
fun processClick(coroutineScope: CoroutineScope) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime < timeout) {
clickCount++
if (clickCount == 2) {
onDoubleClick()
clickCount = 0
}
} else {
clickCount = 1
coroutineScope.launch {
delay(timeout)
if (clickCount == 1) {
onSingleClick()
}
clickCount = 0
}
}
lastClickTime = currentTime
}
}
/**
* 记住双击检测器
*/
@Composable
fun rememberDoubleClickDetector(
timeout: Long = 300L,
onSingleClick: () -> Unit = {},
onDoubleClick: () -> Unit
): () -> Unit {
val detector = remember { DoubleClickDetector(timeout, onSingleClick, onDoubleClick) }
val scope = rememberCoroutineScope()
return {
detector.processClick(scope)
}
}
// ======================== 长按检测 ========================
/**
* 长按检测器
*/
class LongPressDetector(
private val delay: Long = 1000L,
private val onLongPress: () -> Unit,
private val onClick: () -> Unit = {}
) {
private var pressJob: Job? = null
/**
* 处理按压事件
*/
fun handlePress(coroutineScope: CoroutineScope) {
pressJob = coroutineScope.launch {
delay(delay)
onLongPress()
}
}
/**
* 处理释放事件
*/
fun handleRelease() {
pressJob?.cancel()
pressJob = null
onClick()
}
}
/**
* 记住长按检测器
*/
@Composable
fun rememberLongPressDetector(
delay: Long = 1000L,
onLongPress: () -> Unit,
onClick: () -> Unit = {}
): Pair<() -> Unit, () -> Unit> {
val detector = remember { LongPressDetector(delay, onLongPress, onClick) }
val scope = rememberCoroutineScope()
return Pair(
first = { detector.handlePress(scope) },
second = { detector.handleRelease() }
)
}
// ======================== 组合工具 ========================
/**
* 带状态的按钮控制器
*/
class StatefulButtonController {
var isLoading by mutableStateOf(false)
private val clickFilter = ClickFilter()
/**
* 处理按钮点击
*/
suspend fun handleClick(block: suspend () -> Unit) {
if (clickFilter.processClick()) {
isLoading = true
try {
block()
} finally {
isLoading = false
}
}
}
}
/**
* 记住带状态的按钮控制器
*/
@Composable
fun rememberStatefulButtonController(): StatefulButtonController {
return remember { StatefulButtonController() }
}
}
@@ -0,0 +1,110 @@
package com.sw.inbound.utils
import android.content.Context
import androidx.core.content.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class SPUtil private constructor(context: Context, private val spName: String) {
companion object {
@Volatile
private var instance: SPUtil? = null
fun getInstance(
context: Context = ContextUtils.getAppContext(),
spName: String = "default_sp"
): SPUtil {
return instance ?: synchronized(this) {
instance ?: SPUtil(context.applicationContext, spName).also { instance = it }
}
}
}
private val sharedPreferences by lazy {
context.getSharedPreferences(spName, Context.MODE_PRIVATE)
}
// 基础存储方法
fun put(key: String, value: Any?) {
when (value) {
null -> remove(key) // 存入null视为删除
is String -> sharedPreferences.edit { putString(key, value) }
is Int -> sharedPreferences.edit { putInt(key, value) }
is Long -> sharedPreferences.edit { putLong(key, value) }
is Float -> sharedPreferences.edit { putFloat(key, value) }
is Boolean -> sharedPreferences.edit { putBoolean(key, value) }
is Set<*> -> sharedPreferences.edit { putStringSet(key, value as Set<String>) }
else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}")
}
notifyDataChanged(key)
}
@Suppress("UNCHECKED_CAST")
fun <T> get(key: String, defaultValue: T? = null): T? {
return when (defaultValue) {
is String -> sharedPreferences.getString(key, defaultValue) as T
is Int -> sharedPreferences.getInt(key, defaultValue) as T
is Long -> sharedPreferences.getLong(key, defaultValue) as T
is Float -> sharedPreferences.getFloat(key, defaultValue) as T
is Boolean -> sharedPreferences.getBoolean(key, defaultValue) as T
is Set<*> -> sharedPreferences.getStringSet(key, defaultValue as Set<String>) as T
null -> when {
sharedPreferences.contains(key) -> get(key, "") as? T // 尝试作为String获取
else -> null
}
else -> throw IllegalArgumentException("Unsupported type: ${defaultValue.javaClass.name}")
}
}
fun remove(key: String) {
if (sharedPreferences.contains(key)) {
sharedPreferences.edit { remove(key) }
notifyDataChanged(key)
}
}
fun clear() {
sharedPreferences.edit { clear() }
notifyDataChanged(null)
}
fun contains(key: String): Boolean {
return sharedPreferences.contains(key)
}
// 监听变化
private val dataChangeFlow = MutableStateFlow(0)
private fun notifyDataChanged(key: String?) {
dataChangeFlow.value++
}
fun observeKey(key: String): Flow<Any?> {
return dataChangeFlow.map { get(key) }
}
// 属性委托支持
fun int(key: String, default: Int = 0) = SpProperty(key, default)
fun long(key: String, default: Long = 0L) = SpProperty(key, default)
fun float(key: String, default: Float = 0f) = SpProperty(key, default)
fun boolean(key: String, default: Boolean = false) = SpProperty(key, default)
fun string(key: String, default: String = "") = SpProperty(key, default)
fun stringSet(key: String, default: Set<String> = emptySet()) = SpProperty(key, default)
inner class SpProperty<T>(private val key: String, private val defaultValue: T) :
ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return get(key, defaultValue) ?: defaultValue
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
put(key, value)
}
}
}
@@ -0,0 +1,105 @@
package com.sw.inbound.utils
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
open class TextFieldState(
initialText: String = "",
initialSelection: TextRange = TextRange(initialText.length)
) {
private var _value by mutableStateOf(TextFieldValue(initialText, initialSelection))
open var value: TextFieldValue
get() = _value
set(newValue) {
_value = newValue
}
open fun updateFromString(text: String) {
_value = TextFieldValue(text, TextRange(text.length))
}
val text: String get() = _value.text
}
class AmountFieldState(initialAmount: String = "") : TextFieldState(initialAmount) {
// 获取原始数字字符串(不含格式字符)
fun getRawAmount(): String = value.text.filter { it.isDigit() }
// 获取Double类型的金额值
fun getAmountValue(): Double = value.text
.filter { it.isDigit() || it == '.' }
.toDoubleOrNull() ?: 0.0
// 重写value的setter以实现金额格式化
override var value: TextFieldValue
get() = super.value
set(newValue) {
super.value = formatAmountValue(newValue)
}
// 从外部更新金额(如从数据库加载)
override fun updateFromString(amount: String) {
super.updateFromString(formatAmount(amount))
}
private fun formatAmountValue(input: TextFieldValue): TextFieldValue {
val filtered = input.text.filter { it.isDigit() }
val formatted = formatAmount(filtered)
// 计算新光标位置
val newCursorPos = calculateNewCursorPosition(
originalText = input.text,
originalSelection = input.selection,
filteredText = filtered,
formattedText = formatted
)
return TextFieldValue(
text = formatted,
selection = TextRange(newCursorPos)
)
}
private fun formatAmount(amount: String): String {
val filtered = amount.filter { it.isDigit() }
return when {
filtered.isEmpty() -> "0.00"
filtered.length <= 2 -> "0.${filtered.padStart(2, '0')}"
else -> "${filtered.dropLast(2)}.${filtered.takeLast(2)}"
}
}
private fun calculateNewCursorPosition(
originalText: String,
originalSelection: TextRange,
filteredText: String,
formattedText: String
): Int {
// 如果在末尾添加,保持光标在末尾
if (originalSelection.start >= originalText.length) {
return formattedText.length
}
// 计算原始文本中光标前的数字个数
val digitsBeforeCursor = originalText
.substring(0, originalSelection.start)
.count { it.isDigit() }
// 在格式化文本中找到对应位置
var digitCount = 0
formattedText.forEachIndexed { index, char ->
if (char.isDigit()) {
digitCount++
if (digitCount > digitsBeforeCursor) {
return index
}
}
}
return formattedText.length
}
}
@@ -0,0 +1,124 @@
package com.sw.inbound.utils
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.coroutines.CoroutineContext
/**
* 多功能线程工具类
* 结合协程、Handler和线程池实现线程切换
*/
object ThreadUtils : CoroutineScope {
// 主线程Handler
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
// 后台线程池(IO密集型任务)
private val ioThreadPool: ExecutorService by lazy {
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)
}
// CPU密集型线程池
private val cpuThreadPool: ExecutorService by lazy {
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
}
// 协程Job管理
private val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
// ========== Handler相关方法 ==========
/**
* 在主线程执行任务
* @param delayMillis 延迟时间(毫秒)
*/
fun runOnUiThread(delayMillis: Long = 0, block: () -> Unit) {
if (delayMillis > 0) {
mainHandler.postDelayed(block, delayMillis)
} else {
if (isOnMainThread()) {
block()
} else {
mainHandler.post(block)
}
}
}
/**
* 移除主线程任务
*/
fun removeUiThreadTask(block: () -> Unit) {
mainHandler.removeCallbacks(block)
}
// ========== 线程池相关方法 ==========
/**
* 在IO线程执行任务
*/
fun runOnIoThread(block: () -> Unit) {
ioThreadPool.execute(block)
}
/**
* 在CPU计算线程执行任务
*/
fun runOnCpuThread(block: () -> Unit) {
cpuThreadPool.execute(block)
}
// ========== 协程相关方法 ==========
/**
* 启动协程(默认在主线程)
*/
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return launch(coroutineContext, block = block)
}
/**
* 在IO线程启动协程
*/
fun launchOnIo(block: suspend CoroutineScope.() -> Unit): Job {
return launch(Dispatchers.IO, block = block)
}
/**
* 切换到主线程(协程环境)
*/
suspend fun <T> switchToMain(block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.Main, block)
}
/**
* 切换到IO线程(协程环境)
*/
suspend fun <T> switchToIo(block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.IO, block)
}
/**
* 是否在主线程
*/
fun isOnMainThread(): Boolean {
return Looper.myLooper() == Looper.getMainLooper()
}
/**
* 释放资源
*/
fun release() {
job.cancel()
ioThreadPool.shutdown()
cpuThreadPool.shutdown()
}
}
@@ -0,0 +1,56 @@
package com.sw.inbound.utils
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
object ToastUtils {
private var show by mutableStateOf(false)
private var message by mutableStateOf("")
fun showToast(msg: String) {
message = msg
show = true
// Toast.makeText(ContextUtils.getAppContext(), msg, Toast.LENGTH_LONG).show()
}
@Composable
fun ToastComposable() {
if (show) {
LaunchedEffect(Unit) {
delay(2000) // 自动2秒后消失
show = false
}
Box(
modifier = Modifier
// .fillMaxWidth()
.fillMaxSize()
.padding(bottom = 56.dp),
contentAlignment = Alignment.BottomCenter
) {
Text(
text = message,
modifier = Modifier
.background(Color.Black.copy(alpha = 0.7f), RoundedCornerShape(8.dp))
.padding(horizontal = 24.dp, vertical = 12.dp),
color = Color.White,
fontSize = 24.sp
)
}
}
}
}