From 7f6e9cf039a04b9fdf7422622bf1d832a935415e Mon Sep 17 00:00:00 2001 From: dongliang Date: Wed, 6 May 2026 14:25:57 +0930 Subject: [PATCH] =?UTF-8?q?feat:=20ResultFlashView=20=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E6=8C=87=E7=A4=BA=E5=99=A8=EF=BC=88Apple=20W?= =?UTF-8?q?atch=20=E9=A3=8E=E6=A0=BC=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 全屏深色遮罩 + 大圆圈图标,属性动画弹出+淡出,1.5秒自动消失。 - 成功:绿色圆圈 + ✓,OvershootInterpolator 回弹效果 - 失败:红色圆圈 + ✗ - 替代 QuTipDialog 用于 NFC 打卡和按钮操作的结果反馈 - 所有操作统一视觉风格 应用到: - TaskDetailFragment NFC 打卡 - TaskListFragment NFC 打卡 - MainActivity 返回键主动打卡 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../java/com/xiaoqu/watch/app/MainActivity.kt | 12 +- .../watch/ui/task/TaskDetailFragment.kt | 11 +- .../xiaoqu/watch/ui/task/TaskListFragment.kt | 11 +- .../xiaoqu/watch/ui/widget/ResultFlashView.kt | 120 ++++++++++++++++++ .../main/res/drawable/bg_result_circle.xml | 6 + app/src/main/res/layout/activity_main.xml | 9 +- app/src/main/res/layout/view_result_flash.xml | 36 ++++++ 7 files changed, 192 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/xiaoqu/watch/ui/widget/ResultFlashView.kt create mode 100644 app/src/main/res/drawable/bg_result_circle.xml create mode 100644 app/src/main/res/layout/view_result_flash.xml diff --git a/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt b/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt index cfe8736..395360c 100644 --- a/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt +++ b/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt @@ -17,6 +17,7 @@ import com.xiaoqu.watch.service.manager.NfcTaskManager import com.xiaoqu.watch.service.manager.NotificationManager import com.xiaoqu.watch.service.manager.SystemStateMonitor 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.NotificationBannerView import dagger.hilt.android.AndroidEntryPoint @@ -50,6 +51,8 @@ class MainActivity : AppCompatActivity() { /** NFC 任务打卡管理器 */ @Inject lateinit var nfcTaskManager: NfcTaskManager @Inject lateinit var userPrefs: UserPrefs + /** 操作结果指示器(Apple Watch 风格) */ + lateinit var resultFlash: ResultFlashView /** OTA 更新弹窗 */ lateinit var updateDialog: UpdateDialogView lateinit var notificationBanner: NotificationBannerView @@ -85,6 +88,9 @@ class MainActivity : AppCompatActivity() { // 初始化通知横幅 notificationBanner = binding.notificationBanner + // 初始化结果指示器 + resultFlash = binding.resultFlash + // 初始化 OTA 更新弹窗 updateDialog = binding.updateDialog setupUpdateDialog() @@ -205,8 +211,10 @@ class MainActivity : AppCompatActivity() { Timber.d("返回键: 触发主动打卡") nfcTaskManager.startActivePunch { success, message -> - if (message.isNotEmpty()) { - android.widget.Toast.makeText(this@MainActivity, message, android.widget.Toast.LENGTH_SHORT).show() + if (success) { + resultFlash.showSuccess() + } else if (message.isNotEmpty()) { + resultFlash.showFailure() } } } diff --git a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt index 6ee3498..99bed0b 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt @@ -222,13 +222,14 @@ class TaskDetailFragment : BaseFragment() { btn.setBackgroundResource(R.drawable.bg_foot_btn_grey) nfcTaskManager.startTaskPunch(taskId) { success, message -> + val resultFlash = (activity as? com.xiaoqu.watch.app.MainActivity)?.resultFlash if (success) { - // 音效已由 NfcTaskManager 播放,按钮短暂显示"打卡成功"后返回 - btn.text = "打卡成功" - btn.setBackgroundResource(R.drawable.bg_foot_btn_green) - btn.postDelayed({ findNavController().popBackStack() }, 1000) + resultFlash?.showSuccess { findNavController().popBackStack() } } else { - // 失败/超时 → 恢复按钮(可重试) + if (message != "超时") { + resultFlash?.showFailure() + } + // 恢复按钮(可重试) btn.text = "开启打卡" btn.isEnabled = true btn.setBackgroundResource(R.drawable.bg_foot_btn_orange) diff --git a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt index aaf5d10..ca3df16 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt @@ -451,13 +451,14 @@ class TaskListFragment : BaseFragment() { btn.setBackgroundResource(R.drawable.bg_foot_btn_grey) nfcTaskManager.startTaskPunch(taskId) { success, message -> + val resultFlash = (activity as? com.xiaoqu.watch.app.MainActivity)?.resultFlash if (success) { - // 音效已由 NfcTaskManager 播放,按钮短暂显示"打卡成功"后刷新 - btn.text = "打卡成功" - btn.setBackgroundResource(R.drawable.bg_foot_btn_green) - btn.postDelayed({ fetchCurrentDetail() }, 1000) + resultFlash?.showSuccess { fetchCurrentDetail() } } else { - // 失败/超时 → 恢复按钮(可重试) + if (message != "超时") { + resultFlash?.showFailure() + } + // 恢复按钮(可重试) btn.text = "开启打卡" btn.isEnabled = true btn.setBackgroundResource(R.drawable.bg_foot_btn_orange) diff --git a/app/src/main/java/com/xiaoqu/watch/ui/widget/ResultFlashView.kt b/app/src/main/java/com/xiaoqu/watch/ui/widget/ResultFlashView.kt new file mode 100644 index 0000000..36b8e88 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/widget/ResultFlashView.kt @@ -0,0 +1,120 @@ +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 + alpha,OvershootInterpolator 有回弹效果) + 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) + } +} diff --git a/app/src/main/res/drawable/bg_result_circle.xml b/app/src/main/res/drawable/bg_result_circle.xml new file mode 100644 index 0000000..6b91fe2 --- /dev/null +++ b/app/src/main/res/drawable/bg_result_circle.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 17806ef..d8176c6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -33,7 +33,14 @@ android:layout_height="match_parent" android:visibility="gone" /> - + + + + + + + + + + + + + + + + + + +