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 395360c..cfe8736 100644 --- a/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt +++ b/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt @@ -17,7 +17,6 @@ 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 @@ -51,8 +50,6 @@ 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 @@ -88,9 +85,6 @@ class MainActivity : AppCompatActivity() { // 初始化通知横幅 notificationBanner = binding.notificationBanner - // 初始化结果指示器 - resultFlash = binding.resultFlash - // 初始化 OTA 更新弹窗 updateDialog = binding.updateDialog setupUpdateDialog() @@ -211,10 +205,8 @@ class MainActivity : AppCompatActivity() { Timber.d("返回键: 触发主动打卡") nfcTaskManager.startActivePunch { success, message -> - if (success) { - resultFlash.showSuccess() - } else if (message.isNotEmpty()) { - resultFlash.showFailure() + if (message.isNotEmpty()) { + android.widget.Toast.makeText(this@MainActivity, message, android.widget.Toast.LENGTH_SHORT).show() } } } 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 99bed0b..fa3ed70 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,12 +222,20 @@ 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) { - resultFlash?.showSuccess { findNavController().popBackStack() } + tipDialog.show( + status = QuTipDialog.Status.SUCCESS, + title = "打卡成功", + step = 1, + onBack = { findNavController().popBackStack() } + ) } else { if (message != "超时") { - resultFlash?.showFailure() + tipDialog.show( + status = QuTipDialog.Status.ERROR, + title = "打卡失败", + desc = message + ) } // 恢复按钮(可重试) btn.text = "开启打卡" 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 ca3df16..937b2ab 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,12 +451,20 @@ 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) { - resultFlash?.showSuccess { fetchCurrentDetail() } + tipDialog.show( + status = QuTipDialog.Status.SUCCESS, + title = "打卡成功" + ) + // 2 秒后(弹窗消失后)刷新任务详情 + btn.postDelayed({ fetchCurrentDetail() }, 2100) } else { if (message != "超时") { - resultFlash?.showFailure() + tipDialog.show( + status = QuTipDialog.Status.ERROR, + title = "打卡失败", + desc = message + ) } // 恢复按钮(可重试) btn.text = "开启打卡" diff --git a/app/src/main/java/com/xiaoqu/watch/ui/widget/QuTipDialog.kt b/app/src/main/java/com/xiaoqu/watch/ui/widget/QuTipDialog.kt index 940d4fd..dd84f0f 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/widget/QuTipDialog.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/widget/QuTipDialog.kt @@ -1,30 +1,35 @@ package com.xiaoqu.watch.ui.widget +import android.animation.AnimatorSet +import android.animation.ObjectAnimator import android.graphics.Typeface import android.os.CountDownTimer import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.animation.OvershootInterpolator import android.widget.FrameLayout import android.widget.TextView import com.xiaoqu.watch.R /** - * 提示弹窗(对应旧版 qu-tip.vue) - * 显示状态图标(成功/警告/错误)+ 标题 + 可选描述 + 倒计时自动关闭/返回 + * 提示弹窗(优化版) + * + * 显示状态图标(成功/警告/错误)+ 标题 + 可选描述 + * 带淡入+缩放动画,自动倒计时关闭 * * 使用方式: * ``` - * val tip = QuTipDialog(binding.dialogContainer) - * tip.show( - * status = QuTipDialog.Status.SUCCESS, - * title = "打卡成功", - * desc = "已记录考勤", - * back = true, - * step = 1, // 0=只关闭,1=返回上一页 - * countdown = 3, - * onBack = { findMainNavController().popBackStack() } - * ) + * // 简洁模式:1.5 秒后自动消失 + * tip.show(status = SUCCESS, title = "打卡成功") + * + * // 带返回:1.5 秒后自动消失并执行回调 + * tip.show(status = SUCCESS, title = "打卡成功", + * step = 1, onBack = { popBackStack() }) + * + * // 带描述 + 倒计时文字 + * tip.show(status = ERROR, title = "打卡失败", desc = "网络异常", + * showCountdown = true, countdown = 3) * ``` */ class QuTipDialog( @@ -38,12 +43,13 @@ class QuTipDialog( /** * 显示提示弹窗 - * @param status 状态类型(成功/警告/错误/定位) + * @param status 状态类型 * @param title 标题文字 - * @param desc 描述文字(可选) - * @param back 是否显示倒计时返回按钮 - * @param step 倒计时结束后行为:0=只关闭弹窗,>0=触发 onBack 回调 - * @param countdown 倒计时秒数(默认 3 秒) + * @param desc 描述文字(可选,不传则不显示) + * @param back 是否自动倒计时关闭(默认 true) + * @param step 倒计时结束后行为:0=只关闭,>0=触发 onBack 回调 + * @param countdown 倒计时秒数(默认 2 秒) + * @param showCountdown 是否显示倒计时文字(默认 false,更简洁) * @param onBack 返回回调(step > 0 时触发) */ fun show( @@ -51,8 +57,9 @@ class QuTipDialog( title: String, desc: String? = null, back: Boolean = true, - step: Int = 1, - countdown: Int = 3, + step: Int = 0, + countdown: Int = 2, + showCountdown: Boolean = false, onBack: (() -> Unit)? = null ) { // 先移除旧弹窗 @@ -103,33 +110,38 @@ class QuTipDialog( descView.visibility = View.VISIBLE } - // 倒计时返回按钮 + // 倒计时 val backBtn = view.findViewById(R.id.tipBackBtn) if (back) { - backBtn.visibility = View.VISIBLE - backBtn.typeface = typeface + if (showCountdown) { + backBtn.visibility = View.VISIBLE + backBtn.typeface = typeface + } // 启动倒计时 timer = object : CountDownTimer(countdown * 1000L, 1000L) { override fun onTick(millisUntilFinished: Long) { - val seconds = (millisUntilFinished / 1000) + 1 - backBtn.text = "${IconFont.BACK} ${seconds}s" + if (showCountdown) { + val seconds = (millisUntilFinished / 1000) + 1 + backBtn.text = "${IconFont.BACK} ${seconds}s" + } } override fun onFinish() { dismiss() - // step > 0 时触发返回回调 if (step > 0) { onBack?.invoke() } } }.start() - // 点击立即返回(不等倒计时) - backBtn.setOnClickListener { - dismiss() - if (step > 0) { - onBack?.invoke() + // 点击立即关闭(不等倒计时) + if (showCountdown) { + backBtn.setOnClickListener { + dismiss() + if (step > 0) { + onBack?.invoke() + } } } } @@ -137,6 +149,23 @@ class QuTipDialog( // 显示弹窗 container.addView(view) container.visibility = View.VISIBLE + + // 淡入 + 缩放动画 + val content = view.findViewById(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() + } } /** 关闭弹窗 */ 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 deleted file mode 100644 index 36b8e88..0000000 --- a/app/src/main/java/com/xiaoqu/watch/ui/widget/ResultFlashView.kt +++ /dev/null @@ -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 + 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 deleted file mode 100644 index 6b91fe2..0000000 --- a/app/src/main/res/drawable/bg_result_circle.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d8176c6..17806ef 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -33,14 +33,7 @@ android:layout_height="match_parent" android:visibility="gone" /> - - - - + - + - + + android:layout_width="110dp" + android:layout_height="110dp" + android:layout_marginBottom="16dp"> + android:textSize="50sp" /> @@ -43,7 +44,7 @@ android:textSize="28sp" android:textStyle="bold" /> - + - + - - - - - - - - - - - - - - -