feat: ResultFlashView 操作结果指示器(Apple Watch 风格)
全屏深色遮罩 + 大圆圈图标,属性动画弹出+淡出,1.5秒自动消失。 - 成功:绿色圆圈 + ✓,OvershootInterpolator 回弹效果 - 失败:红色圆圈 + ✗ - 替代 QuTipDialog 用于 NFC 打卡和按钮操作的结果反馈 - 所有操作统一视觉风格 应用到: - TaskDetailFragment NFC 打卡 - TaskListFragment NFC 打卡 - MainActivity 返回键主动打卡 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ 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
|
||||||
@@ -50,6 +51,8 @@ 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
|
||||||
@@ -85,6 +88,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// 初始化通知横幅
|
// 初始化通知横幅
|
||||||
notificationBanner = binding.notificationBanner
|
notificationBanner = binding.notificationBanner
|
||||||
|
|
||||||
|
// 初始化结果指示器
|
||||||
|
resultFlash = binding.resultFlash
|
||||||
|
|
||||||
// 初始化 OTA 更新弹窗
|
// 初始化 OTA 更新弹窗
|
||||||
updateDialog = binding.updateDialog
|
updateDialog = binding.updateDialog
|
||||||
setupUpdateDialog()
|
setupUpdateDialog()
|
||||||
@@ -205,8 +211,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
Timber.d("返回键: 触发主动打卡")
|
Timber.d("返回键: 触发主动打卡")
|
||||||
nfcTaskManager.startActivePunch { success, message ->
|
nfcTaskManager.startActivePunch { success, message ->
|
||||||
if (message.isNotEmpty()) {
|
if (success) {
|
||||||
android.widget.Toast.makeText(this@MainActivity, message, android.widget.Toast.LENGTH_SHORT).show()
|
resultFlash.showSuccess()
|
||||||
|
} else if (message.isNotEmpty()) {
|
||||||
|
resultFlash.showFailure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,13 +222,14 @@ 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) {
|
||||||
// 音效已由 NfcTaskManager 播放,按钮短暂显示"打卡成功"后返回
|
resultFlash?.showSuccess { findNavController().popBackStack() }
|
||||||
btn.text = "打卡成功"
|
|
||||||
btn.setBackgroundResource(R.drawable.bg_foot_btn_green)
|
|
||||||
btn.postDelayed({ findNavController().popBackStack() }, 1000)
|
|
||||||
} else {
|
} else {
|
||||||
// 失败/超时 → 恢复按钮(可重试)
|
if (message != "超时") {
|
||||||
|
resultFlash?.showFailure()
|
||||||
|
}
|
||||||
|
// 恢复按钮(可重试)
|
||||||
btn.text = "开启打卡"
|
btn.text = "开启打卡"
|
||||||
btn.isEnabled = true
|
btn.isEnabled = true
|
||||||
btn.setBackgroundResource(R.drawable.bg_foot_btn_orange)
|
btn.setBackgroundResource(R.drawable.bg_foot_btn_orange)
|
||||||
|
|||||||
@@ -451,13 +451,14 @@ 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) {
|
||||||
// 音效已由 NfcTaskManager 播放,按钮短暂显示"打卡成功"后刷新
|
resultFlash?.showSuccess { fetchCurrentDetail() }
|
||||||
btn.text = "打卡成功"
|
|
||||||
btn.setBackgroundResource(R.drawable.bg_foot_btn_green)
|
|
||||||
btn.postDelayed({ fetchCurrentDetail() }, 1000)
|
|
||||||
} else {
|
} else {
|
||||||
// 失败/超时 → 恢复按钮(可重试)
|
if (message != "超时") {
|
||||||
|
resultFlash?.showFailure()
|
||||||
|
}
|
||||||
|
// 恢复按钮(可重试)
|
||||||
btn.text = "开启打卡"
|
btn.text = "开启打卡"
|
||||||
btn.isEnabled = true
|
btn.isEnabled = true
|
||||||
btn.setBackgroundResource(R.drawable.bg_foot_btn_orange)
|
btn.setBackgroundResource(R.drawable.bg_foot_btn_orange)
|
||||||
|
|||||||
120
app/src/main/java/com/xiaoqu/watch/ui/widget/ResultFlashView.kt
Normal file
120
app/src/main/java/com/xiaoqu/watch/ui/widget/ResultFlashView.kt
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
app/src/main/res/drawable/bg_result_circle.xml
Normal file
6
app/src/main/res/drawable/bg_result_circle.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?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>
|
||||||
@@ -33,7 +33,14 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
<!-- Layer 4: OTA 更新弹窗(最顶层,全屏覆盖,默认隐藏) -->
|
<!-- Layer 4: 操作结果指示器(Apple Watch 风格,弹出+淡出,默认隐藏) -->
|
||||||
|
<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"
|
||||||
|
|||||||
36
app/src/main/res/layout/view_result_flash.xml
Normal file
36
app/src/main/res/layout/view_result_flash.xml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?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"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<!-- 中心图标容器(圆形背景 + 图标文字) -->
|
||||||
|
<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>
|
||||||
Reference in New Issue
Block a user