添加了顶部时间更新

This commit is contained in:
zxj
2025-07-16 11:36:56 +08:00
parent 229591dcb3
commit b68c967d82
12 changed files with 344 additions and 4 deletions
+4
View File
@@ -78,4 +78,8 @@ dependencies {
implementation(libs.gson) implementation(libs.gson)
// 日志打印 // 日志打印
implementation(libs.timber) implementation(libs.timber)
// 协程
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
} }
@@ -6,6 +6,12 @@ import android.view.WindowManager
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding 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 基类 * activity 基类
@@ -14,7 +20,9 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected lateinit var binding: VB protected lateinit var binding: VB
private set private set
private var headerBinding: ItemTitleTimeBinding? = null
protected lateinit var context: Context protected lateinit var context: Context
private var timeJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -25,16 +33,33 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
) )
context = this context = this
binding = inflateViewBinding() binding = inflateViewBinding()
headerBinding = inflateTitleBinding()
setContentView(binding.root) setContentView(binding.root)
updateTime()
initialize() 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 inflateViewBinding(): VB
protected abstract fun inflateTitleBinding(): ItemTitleTimeBinding?
protected abstract fun initialize() protected abstract fun initialize()
override fun onDestroy() { override fun onDestroy() {
timeJob?.cancel()
super.onDestroy() super.onDestroy()
} }
} }
@@ -10,6 +10,7 @@ import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.sw.platecabinet.databinding.ActivityLoginFaceBinding import com.sw.platecabinet.databinding.ActivityLoginFaceBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import timber.log.Timber import timber.log.Timber
/** /**
@@ -22,6 +23,10 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
return ActivityLoginFaceBinding.inflate(layoutInflater) return ActivityLoginFaceBinding.inflate(layoutInflater)
} }
override fun inflateTitleBinding(): ItemTitleTimeBinding {
return binding.includeHeader
}
override fun initialize() { override fun initialize() {
initView() initView()
startCamera() startCamera()
@@ -2,12 +2,17 @@ package com.sw.platecabinet.activity
import android.content.Intent import android.content.Intent
import com.sw.platecabinet.databinding.ActivityLoginByPwdBinding import com.sw.platecabinet.databinding.ActivityLoginByPwdBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() { class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
override fun inflateViewBinding(): ActivityLoginByPwdBinding { override fun inflateViewBinding(): ActivityLoginByPwdBinding {
return ActivityLoginByPwdBinding.inflate(layoutInflater) return ActivityLoginByPwdBinding.inflate(layoutInflater)
} }
override fun inflateTitleBinding(): ItemTitleTimeBinding {
return binding.includeHeader
}
override fun initialize() { override fun initialize() {
binding.tvFaceRec.setOnClickListener { binding.tvFaceRec.setOnClickListener {
val intent = Intent(this, LoginByFaceActivity::class.java) val intent = Intent(this, LoginByFaceActivity::class.java)
@@ -1,6 +1,7 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import com.sw.platecabinet.databinding.ActivityMainBinding import com.sw.platecabinet.databinding.ActivityMainBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
class MainActivity : BaseActivity<ActivityMainBinding>() { class MainActivity : BaseActivity<ActivityMainBinding>() {
@@ -8,6 +9,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
return ActivityMainBinding.inflate(layoutInflater) return ActivityMainBinding.inflate(layoutInflater)
} }
override fun inflateTitleBinding(): ItemTitleTimeBinding {
return binding.includeHeader
}
override fun initialize() { override fun initialize() {
} }
@@ -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<String, String> {
return dateFormat.format(date) to timeFormat.format(date)
}
/**
* 实时时间流(每秒更新)
* @param intervalMillis 更新间隔(默认1秒)
*/
fun realTimeChineseDateFlow(intervalMillis: Long = 1000) = flow {
while (true) {
emit(getChineseDateTimePair())
delay(intervalMillis)
}
}
}
@@ -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,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<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)
}
}
}
@@ -5,7 +5,9 @@
android:background="@mipmap/bg" android:background="@mipmap/bg"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/item_title_time" /> <include
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -8,7 +8,9 @@
android:orientation="vertical" android:orientation="vertical"
tools:context=".activity.LoginByFaceActivity"> tools:context=".activity.LoginByFaceActivity">
<include layout="@layout/item_title_time" /> <include
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
+3 -1
View File
@@ -6,5 +6,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".activity.MainActivity"> tools:context=".activity.MainActivity">
<include
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
+6
View File
@@ -6,6 +6,9 @@ junit = "4.13.2"
junitVersion = "1.1.5" junitVersion = "1.1.5"
espressoCore = "3.5.1" espressoCore = "3.5.1"
appcompat = "1.6.1" appcompat = "1.6.1"
kotlinxCoroutinesAndroid = "1.6.4"
lifecycleViewmodelKtx = "2.8.3"
lifecycleRuntimeKtx = "2.8.3"
material = "1.10.0" material = "1.10.0"
activity = "1.8.0" activity = "1.8.0"
constraintlayout = "2.1.4" constraintlayout = "2.1.4"
@@ -20,10 +23,13 @@ hiltAndroid = "2.56.2"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 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" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } 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-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } 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" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }