From b68c967d82292379b6df44c1bb81ad48abde80ae Mon Sep 17 00:00:00 2001 From: zxj <275873859@qq.com> Date: Wed, 16 Jul 2025 11:36:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E9=A1=B6=E9=83=A8?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 + .../sw/platecabinet/activity/BaseActivity.kt | 27 +++- .../activity/LoginByFaceActivity.kt | 5 + .../activity/LoginByPwdActivity.kt | 5 + .../sw/platecabinet/activity/MainActivity.kt | 5 + .../sw/platecabinet/utils/DateTimeUtils.kt | 50 +++++++ .../com/sw/platecabinet/utils/GsonUtils.kt | 123 ++++++++++++++++++ .../java/com/sw/platecabinet/utils/SPUtil.kt | 111 ++++++++++++++++ .../main/res/layout/activity_login_by_pwd.xml | 4 +- .../main/res/layout/activity_login_face.xml | 4 +- app/src/main/res/layout/activity_main.xml | 4 +- gradle/libs.versions.toml | 6 + 12 files changed, 344 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/sw/platecabinet/utils/DateTimeUtils.kt create mode 100644 app/src/main/java/com/sw/platecabinet/utils/GsonUtils.kt create mode 100644 app/src/main/java/com/sw/platecabinet/utils/SPUtil.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e77a224..2f903fd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -78,4 +78,8 @@ dependencies { implementation(libs.gson) // 日志打印 implementation(libs.timber) + // 协程 + implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt index 6d54a83..b8fd174 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/BaseActivity.kt @@ -6,6 +6,12 @@ import android.view.WindowManager import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.viewbinding.ViewBinding +import com.sw.inbound.utils.DateTimeUtils +import com.sw.platecabinet.databinding.ItemTitleTimeBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch /** * activity 基类 @@ -14,7 +20,9 @@ abstract class BaseActivity : AppCompatActivity() { protected lateinit var binding: VB private set + private var headerBinding: ItemTitleTimeBinding? = null protected lateinit var context: Context + private var timeJob: Job? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -25,16 +33,33 @@ abstract class BaseActivity : AppCompatActivity() { ) context = this binding = inflateViewBinding() + headerBinding = inflateTitleBinding() setContentView(binding.root) + updateTime() initialize() } + fun updateTime() { + headerBinding?.let { + val scope = CoroutineScope(Dispatchers.Main) + timeJob = scope.launch { + DateTimeUtils.realTimeChineseDateFlow() + .collect { (date, time) -> + headerBinding!!.tvLeftDate.text = date + headerBinding!!.tvRightTime.text = time + } + } + } + } + protected abstract fun inflateViewBinding(): VB + protected abstract fun inflateTitleBinding(): ItemTitleTimeBinding? + protected abstract fun initialize() - override fun onDestroy() { + timeJob?.cancel() super.onDestroy() } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt index 1467240..4f3d806 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt @@ -10,6 +10,7 @@ import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat import com.google.common.util.concurrent.ListenableFuture import com.sw.platecabinet.databinding.ActivityLoginFaceBinding +import com.sw.platecabinet.databinding.ItemTitleTimeBinding import timber.log.Timber /** @@ -22,6 +23,10 @@ class LoginByFaceActivity : BaseActivity() { return ActivityLoginFaceBinding.inflate(layoutInflater) } + override fun inflateTitleBinding(): ItemTitleTimeBinding { + return binding.includeHeader + } + override fun initialize() { initView() startCamera() diff --git a/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt index 011ef3b..b7aff2f 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/LoginByPwdActivity.kt @@ -2,12 +2,17 @@ package com.sw.platecabinet.activity import android.content.Intent import com.sw.platecabinet.databinding.ActivityLoginByPwdBinding +import com.sw.platecabinet.databinding.ItemTitleTimeBinding class LoginByPwdActivity : BaseActivity() { override fun inflateViewBinding(): ActivityLoginByPwdBinding { return ActivityLoginByPwdBinding.inflate(layoutInflater) } + override fun inflateTitleBinding(): ItemTitleTimeBinding { + return binding.includeHeader + } + override fun initialize() { binding.tvFaceRec.setOnClickListener { val intent = Intent(this, LoginByFaceActivity::class.java) diff --git a/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt index 794c2eb..e60becd 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/MainActivity.kt @@ -1,6 +1,7 @@ package com.sw.platecabinet.activity import com.sw.platecabinet.databinding.ActivityMainBinding +import com.sw.platecabinet.databinding.ItemTitleTimeBinding class MainActivity : BaseActivity() { @@ -8,6 +9,10 @@ class MainActivity : BaseActivity() { return ActivityMainBinding.inflate(layoutInflater) } + override fun inflateTitleBinding(): ItemTitleTimeBinding { + return binding.includeHeader + } + override fun initialize() { } diff --git a/app/src/main/java/com/sw/platecabinet/utils/DateTimeUtils.kt b/app/src/main/java/com/sw/platecabinet/utils/DateTimeUtils.kt new file mode 100644 index 0000000..5244f5f --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/utils/DateTimeUtils.kt @@ -0,0 +1,50 @@ +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) + } + + // 使用线程安全的日期格式化(避免 SimpleDateFormat 的线程安全问题) + private val dateFormat by lazy { + SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA) + } + private val timeFormat by lazy { + SimpleDateFormat("HH:mm:ss", Locale.CHINA) + } + + fun getChineseDateTimePair(date: Date = Date()): Pair { + return dateFormat.format(date) to timeFormat.format(date) + } + + /** + * 实时时间流(每秒更新) + * @param intervalMillis 更新间隔(默认1秒) + */ + fun realTimeChineseDateFlow(intervalMillis: Long = 1000) = flow { + while (true) { + emit(getChineseDateTimePair()) + delay(intervalMillis) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/utils/GsonUtils.kt b/app/src/main/java/com/sw/platecabinet/utils/GsonUtils.kt new file mode 100644 index 0000000..07dc7b1 --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/utils/GsonUtils.kt @@ -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 fromJson(json: String?, clazz: Class): 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 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 fromJsonList(json: String?, clazz: Class): List? { + if (json.isNullOrEmpty()) { + return null + } + return try { + val type = TypeToken.getParameterized(List::class.java, clazz).type + defaultGson.fromJson>(json, type) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + /** + * 将 JSON 字符串转换为 Map 对象 + * @param json JSON 字符串 + * @param keyClazz Map 的 key 类型 + * @param valueClazz Map 的 value 类型 + * @return 转换后的 Map 对象 + */ + fun fromJsonMap( + json: String?, + keyClazz: Class, + valueClazz: Class + ): Map? { + if (json.isNullOrEmpty()) { + return null + } + return try { + val type = TypeToken.getParameterized(Map::class.java, keyClazz, valueClazz).type + defaultGson.fromJson>(json, type) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + /** + * 将对象转换为另一种类型的对象 + * @param obj 源对象 + * @param clazz 目标类型 + * @return 转换后的对象 + */ + fun convert(obj: Any?, clazz: Class): T? { + if (obj == null) { + return null + } + return fromJson(toJson(obj), clazz) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/utils/SPUtil.kt b/app/src/main/java/com/sw/platecabinet/utils/SPUtil.kt new file mode 100644 index 0000000..bf13ea7 --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/utils/SPUtil.kt @@ -0,0 +1,111 @@ +package com.sw.inbound.utils + +import android.content.Context +import androidx.core.content.edit +import com.sw.plate.App +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 = App.getContext(), + 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) } + else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}") + } + notifyDataChanged(key) + } + + @Suppress("UNCHECKED_CAST") + fun 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) 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 { + 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 = emptySet()) = SpProperty(key, default) + + inner class SpProperty(private val key: String, private val defaultValue: T) : + ReadWriteProperty { + + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login_by_pwd.xml b/app/src/main/res/layout/activity_login_by_pwd.xml index 0b2b90e..b836271 100644 --- a/app/src/main/res/layout/activity_login_by_pwd.xml +++ b/app/src/main/res/layout/activity_login_by_pwd.xml @@ -5,7 +5,9 @@ android:background="@mipmap/bg" android:orientation="vertical"> - + - + - + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1fee65b..58c4a55 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,9 @@ junit = "4.13.2" junitVersion = "1.1.5" espressoCore = "3.5.1" appcompat = "1.6.1" +kotlinxCoroutinesAndroid = "1.6.4" +lifecycleViewmodelKtx = "2.8.3" +lifecycleRuntimeKtx = "2.8.3" material = "1.10.0" activity = "1.8.0" constraintlayout = "2.1.4" @@ -20,10 +23,13 @@ hiltAndroid = "2.56.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }