refactor: 优化 QuTipDialog + 删除 ResultFlashView

QuTipDialog 优化:
- 图标区域放大 85→110dp,图标字号 38→50sp
- 加入淡入+缩放动画(OvershootInterpolator 回弹效果)
- 默认倒计时 3→2 秒
- 倒计时文字默认隐藏(showCountdown 参数控制)
- 简化调用:show(status, title) 即可,2秒后自动消失

NFC 打卡加回 QuTipDialog:
- 成功:绿色图标 + "打卡成功" + 2秒消失
- 失败:红色图标 + "打卡失败" + 错误信息

删除 ResultFlashView(方案不合适,统一用优化后的 QuTipDialog)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-05-06 15:40:07 +09:30
parent 1ef6824c5e
commit 3ab6a94676
9 changed files with 94 additions and 224 deletions

View File

@@ -17,7 +17,6 @@ import com.xiaoqu.watch.service.manager.NfcTaskManager
import com.xiaoqu.watch.service.manager.NotificationManager import com.xiaoqu.watch.service.manager.NotificationManager
import com.xiaoqu.watch.service.manager.SystemStateMonitor import com.xiaoqu.watch.service.manager.SystemStateMonitor
import com.xiaoqu.watch.service.manager.UpdateManager import com.xiaoqu.watch.service.manager.UpdateManager
import com.xiaoqu.watch.ui.widget.ResultFlashView
import com.xiaoqu.watch.ui.widget.UpdateDialogView import com.xiaoqu.watch.ui.widget.UpdateDialogView
import com.xiaoqu.watch.ui.widget.NotificationBannerView import com.xiaoqu.watch.ui.widget.NotificationBannerView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -51,8 +50,6 @@ class MainActivity : AppCompatActivity() {
/** NFC 任务打卡管理器 */ /** NFC 任务打卡管理器 */
@Inject lateinit var nfcTaskManager: NfcTaskManager @Inject lateinit var nfcTaskManager: NfcTaskManager
@Inject lateinit var userPrefs: UserPrefs @Inject lateinit var userPrefs: UserPrefs
/** 操作结果指示器Apple Watch 风格) */
lateinit var resultFlash: ResultFlashView
/** OTA 更新弹窗 */ /** OTA 更新弹窗 */
lateinit var updateDialog: UpdateDialogView lateinit var updateDialog: UpdateDialogView
lateinit var notificationBanner: NotificationBannerView lateinit var notificationBanner: NotificationBannerView
@@ -88,9 +85,6 @@ class MainActivity : AppCompatActivity() {
// 初始化通知横幅 // 初始化通知横幅
notificationBanner = binding.notificationBanner notificationBanner = binding.notificationBanner
// 初始化结果指示器
resultFlash = binding.resultFlash
// 初始化 OTA 更新弹窗 // 初始化 OTA 更新弹窗
updateDialog = binding.updateDialog updateDialog = binding.updateDialog
setupUpdateDialog() setupUpdateDialog()
@@ -211,10 +205,8 @@ class MainActivity : AppCompatActivity() {
Timber.d("返回键: 触发主动打卡") Timber.d("返回键: 触发主动打卡")
nfcTaskManager.startActivePunch { success, message -> nfcTaskManager.startActivePunch { success, message ->
if (success) { if (message.isNotEmpty()) {
resultFlash.showSuccess() android.widget.Toast.makeText(this@MainActivity, message, android.widget.Toast.LENGTH_SHORT).show()
} else if (message.isNotEmpty()) {
resultFlash.showFailure()
} }
} }
} }

View File

@@ -222,12 +222,20 @@ class TaskDetailFragment : BaseFragment<FragmentTaskDetailBinding>() {
btn.setBackgroundResource(R.drawable.bg_foot_btn_grey) btn.setBackgroundResource(R.drawable.bg_foot_btn_grey)
nfcTaskManager.startTaskPunch(taskId) { success, message -> nfcTaskManager.startTaskPunch(taskId) { success, message ->
val resultFlash = (activity as? com.xiaoqu.watch.app.MainActivity)?.resultFlash
if (success) { if (success) {
resultFlash?.showSuccess { findNavController().popBackStack() } tipDialog.show(
status = QuTipDialog.Status.SUCCESS,
title = "打卡成功",
step = 1,
onBack = { findNavController().popBackStack() }
)
} else { } else {
if (message != "超时") { if (message != "超时") {
resultFlash?.showFailure() tipDialog.show(
status = QuTipDialog.Status.ERROR,
title = "打卡失败",
desc = message
)
} }
// 恢复按钮(可重试) // 恢复按钮(可重试)
btn.text = "开启打卡" btn.text = "开启打卡"

View File

@@ -451,12 +451,20 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
btn.setBackgroundResource(R.drawable.bg_foot_btn_grey) btn.setBackgroundResource(R.drawable.bg_foot_btn_grey)
nfcTaskManager.startTaskPunch(taskId) { success, message -> nfcTaskManager.startTaskPunch(taskId) { success, message ->
val resultFlash = (activity as? com.xiaoqu.watch.app.MainActivity)?.resultFlash
if (success) { if (success) {
resultFlash?.showSuccess { fetchCurrentDetail() } tipDialog.show(
status = QuTipDialog.Status.SUCCESS,
title = "打卡成功"
)
// 2 秒后(弹窗消失后)刷新任务详情
btn.postDelayed({ fetchCurrentDetail() }, 2100)
} else { } else {
if (message != "超时") { if (message != "超时") {
resultFlash?.showFailure() tipDialog.show(
status = QuTipDialog.Status.ERROR,
title = "打卡失败",
desc = message
)
} }
// 恢复按钮(可重试) // 恢复按钮(可重试)
btn.text = "开启打卡" btn.text = "开启打卡"

View File

@@ -1,30 +1,35 @@
package com.xiaoqu.watch.ui.widget package com.xiaoqu.watch.ui.widget
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.graphics.Typeface import android.graphics.Typeface
import android.os.CountDownTimer import android.os.CountDownTimer
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.OvershootInterpolator
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.TextView import android.widget.TextView
import com.xiaoqu.watch.R import com.xiaoqu.watch.R
/** /**
* 提示弹窗(对应旧版 qu-tip.vue * 提示弹窗(优化版
* 显示状态图标(成功/警告/错误)+ 标题 + 可选描述 + 倒计时自动关闭/返回 *
* 显示状态图标(成功/警告/错误)+ 标题 + 可选描述
* 带淡入+缩放动画,自动倒计时关闭
* *
* 使用方式: * 使用方式:
* ``` * ```
* val tip = QuTipDialog(binding.dialogContainer) * // 简洁模式1.5 秒后自动消失
* tip.show( * tip.show(status = SUCCESS, title = "打卡成功")
* status = QuTipDialog.Status.SUCCESS, *
* title = "打卡成功", * // 带返回1.5 秒后自动消失并执行回调
* desc = "已记录考勤", * tip.show(status = SUCCESS, title = "打卡成功",
* back = true, * step = 1, onBack = { popBackStack() })
* step = 1, // 0=只关闭1=返回上一页 *
* countdown = 3, * // 带描述 + 倒计时文字
* onBack = { findMainNavController().popBackStack() } * tip.show(status = ERROR, title = "打卡失败", desc = "网络异常",
* ) * showCountdown = true, countdown = 3)
* ``` * ```
*/ */
class QuTipDialog( class QuTipDialog(
@@ -38,12 +43,13 @@ class QuTipDialog(
/** /**
* 显示提示弹窗 * 显示提示弹窗
* @param status 状态类型(成功/警告/错误/定位) * @param status 状态类型
* @param title 标题文字 * @param title 标题文字
* @param desc 描述文字(可选) * @param desc 描述文字(可选,不传则不显示
* @param back 是否显示倒计时返回按钮 * @param back 是否自动倒计时关闭(默认 true
* @param step 倒计时结束后行为0=只关闭弹窗>0=触发 onBack 回调 * @param step 倒计时结束后行为0=只关闭,>0=触发 onBack 回调
* @param countdown 倒计时秒数(默认 3 秒) * @param countdown 倒计时秒数(默认 2 秒)
* @param showCountdown 是否显示倒计时文字(默认 false更简洁
* @param onBack 返回回调step > 0 时触发) * @param onBack 返回回调step > 0 时触发)
*/ */
fun show( fun show(
@@ -51,8 +57,9 @@ class QuTipDialog(
title: String, title: String,
desc: String? = null, desc: String? = null,
back: Boolean = true, back: Boolean = true,
step: Int = 1, step: Int = 0,
countdown: Int = 3, countdown: Int = 2,
showCountdown: Boolean = false,
onBack: (() -> Unit)? = null onBack: (() -> Unit)? = null
) { ) {
// 先移除旧弹窗 // 先移除旧弹窗
@@ -103,29 +110,33 @@ class QuTipDialog(
descView.visibility = View.VISIBLE descView.visibility = View.VISIBLE
} }
// 倒计时返回按钮 // 倒计时
val backBtn = view.findViewById<TextView>(R.id.tipBackBtn) val backBtn = view.findViewById<TextView>(R.id.tipBackBtn)
if (back) { if (back) {
if (showCountdown) {
backBtn.visibility = View.VISIBLE backBtn.visibility = View.VISIBLE
backBtn.typeface = typeface backBtn.typeface = typeface
}
// 启动倒计时 // 启动倒计时
timer = object : CountDownTimer(countdown * 1000L, 1000L) { timer = object : CountDownTimer(countdown * 1000L, 1000L) {
override fun onTick(millisUntilFinished: Long) { override fun onTick(millisUntilFinished: Long) {
if (showCountdown) {
val seconds = (millisUntilFinished / 1000) + 1 val seconds = (millisUntilFinished / 1000) + 1
backBtn.text = "${IconFont.BACK} ${seconds}s" backBtn.text = "${IconFont.BACK} ${seconds}s"
} }
}
override fun onFinish() { override fun onFinish() {
dismiss() dismiss()
// step > 0 时触发返回回调
if (step > 0) { if (step > 0) {
onBack?.invoke() onBack?.invoke()
} }
} }
}.start() }.start()
// 点击立即返回(不等倒计时) // 点击立即关闭(不等倒计时)
if (showCountdown) {
backBtn.setOnClickListener { backBtn.setOnClickListener {
dismiss() dismiss()
if (step > 0) { if (step > 0) {
@@ -133,10 +144,28 @@ class QuTipDialog(
} }
} }
} }
}
// 显示弹窗 // 显示弹窗
container.addView(view) container.addView(view)
container.visibility = View.VISIBLE container.visibility = View.VISIBLE
// 淡入 + 缩放动画
val content = view.findViewById<View>(R.id.tipContent)
content.alpha = 0f
content.scaleX = 0.8f
content.scaleY = 0.8f
val fadeIn = ObjectAnimator.ofFloat(content, "alpha", 0f, 1f)
val scaleX = ObjectAnimator.ofFloat(content, "scaleX", 0.8f, 1f)
val scaleY = ObjectAnimator.ofFloat(content, "scaleY", 0.8f, 1f)
AnimatorSet().apply {
playTogether(fadeIn, scaleX, scaleY)
duration = 250
interpolator = OvershootInterpolator(1.2f)
start()
}
} }
/** 关闭弹窗 */ /** 关闭弹窗 */

View File

@@ -1,120 +0,0 @@
package com.xiaoqu.watch.ui.widget
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.animation.OvershootInterpolator
import android.widget.FrameLayout
import android.widget.TextView
import com.xiaoqu.watch.R
/**
* 操作结果闪烁指示器Apple Watch 风格)
*
* 全屏深色遮罩 + 中心大圆圈图标,属性动画弹出后自动淡出。
* 用于所有操作的成功/失败反馈,替代 QuTipDialog。
*
* 使用方式:
* - 添加到 activity_main.xml 最顶层
* - 调用 showSuccess() 或 showFailure()
* - 1.5 秒后自动消失,执行 onDismiss 回调
*/
class ResultFlashView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
companion object {
/** 弹出动画时长(毫秒) */
private const val ANIM_IN_DURATION = 300L
/** 停留时长(毫秒) */
private const val STAY_DURATION = 1000L
/** 淡出动画时长(毫秒) */
private const val ANIM_OUT_DURATION = 300L
/** 成功绿色 */
private const val COLOR_SUCCESS = 0xFF4CAF50.toInt()
/** 失败红色 */
private const val COLOR_FAILURE = 0xFFF44336.toInt()
}
private val iconContainer: View
private val circleRing: View
private val tvIcon: TextView
/** 消失后回调 */
var onDismiss: (() -> Unit)? = null
init {
LayoutInflater.from(context).inflate(R.layout.view_result_flash, this, true)
iconContainer = findViewById(R.id.iconContainer)
circleRing = findViewById(R.id.circleRing)
tvIcon = findViewById(R.id.tvIcon)
}
/** 显示成功指示器(绿色 ✓) */
fun showSuccess(onDone: (() -> Unit)? = null) {
onDismiss = onDone
setColor(COLOR_SUCCESS)
tvIcon.text = ""
playAnimation()
}
/** 显示失败指示器(红色 ✗) */
fun showFailure(onDone: (() -> Unit)? = null) {
onDismiss = onDone
setColor(COLOR_FAILURE)
tvIcon.text = ""
playAnimation()
}
/** 设置圆圈颜色 */
private fun setColor(color: Int) {
val drawable = circleRing.background as? GradientDrawable
drawable?.setStroke(6, color)
tvIcon.setTextColor(color)
}
/** 执行弹出 → 停留 → 淡出动画 */
private fun playAnimation() {
// 重置状态
visibility = View.VISIBLE
alpha = 1f
iconContainer.scaleX = 0.3f
iconContainer.scaleY = 0.3f
iconContainer.alpha = 0f
// 弹出动画scale + alphaOvershootInterpolator 有回弹效果)
val scaleX = ObjectAnimator.ofFloat(iconContainer, "scaleX", 0.3f, 1f)
val scaleY = ObjectAnimator.ofFloat(iconContainer, "scaleY", 0.3f, 1f)
val alphaIn = ObjectAnimator.ofFloat(iconContainer, "alpha", 0f, 1f)
val animIn = AnimatorSet().apply {
playTogether(scaleX, scaleY, alphaIn)
duration = ANIM_IN_DURATION
interpolator = OvershootInterpolator(1.5f)
}
// 淡出动画
val fadeOut = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f).apply {
duration = ANIM_OUT_DURATION
startDelay = ANIM_IN_DURATION + STAY_DURATION
}
// 播放
animIn.start()
fadeOut.start()
// 动画结束后隐藏 + 回调
postDelayed({
visibility = View.GONE
alpha = 1f
onDismiss?.invoke()
onDismiss = null
}, ANIM_IN_DURATION + STAY_DURATION + ANIM_OUT_DURATION)
}
}

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 结果指示器圆形边框(默认绿色,代码中动态设置颜色) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke android:width="4dp" android:color="#4CAF50" />
</shape>

View File

@@ -33,14 +33,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" /> android:visibility="gone" />
<!-- Layer 4: 操作结果指示器Apple Watch 风格,弹出+淡出,默认隐藏) --> <!-- Layer 4: OTA 更新弹窗(最顶层,全屏覆盖,默认隐藏) -->
<com.xiaoqu.watch.ui.widget.ResultFlashView
android:id="@+id/resultFlash"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<!-- Layer 5: OTA 更新弹窗(最顶层,全屏覆盖,默认隐藏) -->
<com.xiaoqu.watch.ui.widget.UpdateDialogView <com.xiaoqu.watch.ui.widget.UpdateDialogView
android:id="@+id/updateDialog" android:id="@+id/updateDialog"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- QuTipDialog反馈提示弹窗按原型图V3样式 <!-- QuTipDialog反馈提示弹窗优化版
圆形图标背景 + 标题 + 描述 + 倒计时 --> 圆形图标 + 标题,简洁清晰 -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/background"> android:background="@color/background">
<LinearLayout <LinearLayout
android:id="@+id/tipContent"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
@@ -14,11 +15,11 @@
android:paddingStart="21dp" android:paddingStart="21dp"
android:paddingEnd="21dp"> android:paddingEnd="21dp">
<!-- 圆形图标背景(85dp --> <!-- 圆形图标背景(放大 85→110dp -->
<FrameLayout <FrameLayout
android:layout_width="85dp" android:layout_width="110dp"
android:layout_height="85dp" android:layout_height="110dp"
android:layout_marginBottom="19dp"> android:layout_marginBottom="16dp">
<View <View
android:id="@+id/tipIconBg" android:id="@+id/tipIconBg"
@@ -30,7 +31,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:textSize="38sp" /> android:textSize="50sp" />
</FrameLayout> </FrameLayout>
@@ -43,7 +44,7 @@
android:textSize="28sp" android:textSize="28sp"
android:textStyle="bold" /> android:textStyle="bold" />
<!-- 描述20sp --> <!-- 描述20sp,可选 -->
<TextView <TextView
android:id="@+id/tipDesc" android:id="@+id/tipDesc"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -55,7 +56,7 @@
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:visibility="gone" /> android:visibility="gone" />
<!-- 倒计时 --> <!-- 倒计时(隐藏,只在需要时显示) -->
<TextView <TextView
android:id="@+id/tipBackBtn" android:id="@+id/tipBackBtn"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 操作结果闪烁指示器(类 Apple Watch 风格)
全屏深色遮罩 + 中心大图标,属性动画弹出/淡出 -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#CC000000">
<!-- 中心图标容器(圆形背景 + 图标文字) -->
<FrameLayout
android:id="@+id/iconContainer"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center">
<!-- 圆形边框 -->
<View
android:id="@+id/circleRing"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_result_circle" />
<!-- ✓ 或 ✗ 图标 -->
<TextView
android:id="@+id/tvIcon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="48sp"
android:textStyle="bold" />
</FrameLayout>
</FrameLayout>