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:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,12 +222,20 @@ class TaskDetailFragment : BaseFragment<FragmentTaskDetailBinding>() {
|
||||
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 = "开启打卡"
|
||||
|
||||
@@ -451,12 +451,20 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
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 = "开启打卡"
|
||||
|
||||
@@ -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<TextView>(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<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()
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭弹窗 */
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user