diff --git a/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt index 2a9d722..5f69c84 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt @@ -172,9 +172,12 @@ class HomeFragment : BaseFragment() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { punchViewModel.uiState.collect { state -> - // 更新面板按钮 + // 更新面板按钮和状态文字 punchPanel.updateButtons(state) + // 更新 NFC 倒计时 + punchPanel.updateNfcCountdown(state.nfcCountdown) + // 处理打卡结果(一次性事件) state.punchResult?.let { result -> punchViewModel.consumePunchResult() diff --git a/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchPanelView.kt b/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchPanelView.kt index de47b3b..13eb79e 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchPanelView.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchPanelView.kt @@ -1,6 +1,5 @@ package com.xiaoqu.watch.ui.punch -import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater @@ -17,13 +16,13 @@ import com.xiaoqu.watch.util.DateUtil * * 交互方式: * - 调用 show() 展开面板 - * - 点击遮罩区域收回 + * - 点击遮罩 / 底部指示条收回 * - 打卡完成后自动收回 * * 按钮显示规则(来自源码分析,已评审修正): * - onPunchState=0 → "上班打卡" - * - onPunchState=1, offPunchState=0 → "下班打卡" - * - onPunchState=1, offPunchState=1 → "撤销打卡" + "下班打卡" + * - onPunchState=1, offPunchState=0 → "下班打卡" + 状态文字"已上班 HH:MM" + * - onPunchState=1, offPunchState=1 → "撤销" + "下班打卡" + 状态文字"已下班 HH:MM" */ class PunchPanelView @JvmOverloads constructor( context: Context, @@ -35,10 +34,12 @@ class PunchPanelView @JvmOverloads constructor( private val overlay: View private val panelContent: LinearLayout private val tvPunchTime: TextView + private val tvPunchStatus: TextView private val tvNfcHint: TextView private val btnPunchIn: TextView private val btnPunchOut: TextView private val btnRevoke: TextView + private val dismissBar: View /** 面板是否正在显示 */ var isShowing = false @@ -48,27 +49,27 @@ class PunchPanelView @JvmOverloads constructor( var onPunchInClick: (() -> Unit)? = null var onPunchOutClick: (() -> Unit)? = null var onRevokeClick: (() -> Unit)? = null - /** 面板关闭回调(用于通知 HomeFragment 恢复 ViewPager2 滑动) */ + /** 面板关闭回调 */ var onDismiss: (() -> Unit)? = null init { - // 加载布局 LayoutInflater.from(context).inflate(R.layout.view_punch_panel, this, true) - - // 默认隐藏 visibility = GONE // 绑定 View overlay = findViewById(R.id.overlay) panelContent = findViewById(R.id.panelContent) tvPunchTime = findViewById(R.id.tvPunchTime) + tvPunchStatus = findViewById(R.id.tvPunchStatus) tvNfcHint = findViewById(R.id.tvNfcHint) btnPunchIn = findViewById(R.id.btnPunchIn) btnPunchOut = findViewById(R.id.btnPunchOut) btnRevoke = findViewById(R.id.btnRevoke) + dismissBar = findViewById(R.id.dismissBar) - // 点击遮罩收回面板 + // 点击遮罩 / 指示条 收回面板 overlay.setOnClickListener { dismiss() } + dismissBar.setOnClickListener { dismiss() } // 按钮点击 btnPunchIn.setOnClickListener { onPunchInClick?.invoke() } @@ -80,11 +81,8 @@ class PunchPanelView @JvmOverloads constructor( fun show() { if (isShowing) return isShowing = true - - // 更新时间 updateClock() - // 显示并播放动画 visibility = VISIBLE panelContent.translationY = -panelContent.height.toFloat().coerceAtLeast(300f) panelContent.animate() @@ -94,10 +92,7 @@ class PunchPanelView @JvmOverloads constructor( .start() overlay.alpha = 0f - overlay.animate() - .alpha(1f) - .setDuration(200) - .start() + overlay.animate().alpha(1f).setDuration(200).start() } /** 收回面板 */ @@ -105,7 +100,6 @@ class PunchPanelView @JvmOverloads constructor( if (!isShowing) return isShowing = false - // 收回动画 panelContent.animate() .translationY(-panelContent.height.toFloat().coerceAtLeast(300f)) .setDuration(200) @@ -113,49 +107,61 @@ class PunchPanelView @JvmOverloads constructor( .withEndAction { visibility = GONE tvNfcHint.visibility = GONE + tvPunchStatus.visibility = GONE onDismiss?.invoke() } .start() - overlay.animate() - .alpha(0f) - .setDuration(200) - .start() + overlay.animate().alpha(0f).setDuration(200).start() } /** - * 更新按钮显示状态 + * 更新面板显示状态 * @param state 考勤状态 */ fun updateButtons(state: PunchUiState) { val onState = state.onPunchState val offState = state.offPunchState - // 重置所有按钮 + // 重置按钮 btnPunchIn.visibility = GONE btnPunchOut.visibility = GONE btnRevoke.visibility = GONE + // 重置按钮权重(单按钮时 weight=1,双按钮时撤销0.4+下班0.6) when { - // 未上班打卡 → 显示"上班打卡" + // 未上班打卡 → "上班打卡" onState == 0 -> { btnPunchIn.visibility = VISIBLE + setWeight(btnPunchIn, 1f) + // 无状态文字 + tvPunchStatus.visibility = GONE } - // 已上班,已下班 → 显示"撤销打卡" + "下班打卡" + // 已上班,已下班 → "撤销" + "下班打卡" onState == 1 && offState == 1 -> { btnRevoke.visibility = VISIBLE btnPunchOut.visibility = VISIBLE + setWeight(btnRevoke, 0.4f) + setWeight(btnPunchOut, 0.6f) + // 状态:已下班 HH:MM + tvPunchStatus.visibility = VISIBLE + tvPunchStatus.text = "已下班 ${state.actualOffTime ?: ""}" + tvPunchStatus.setTextColor(context.getColor(R.color.text_secondary)) } - // 已上班,未下班 → 仅"下班打卡" + // 已上班,未下班 → "下班打卡" onState == 1 && offState == 0 -> { btnPunchOut.visibility = VISIBLE + setWeight(btnPunchOut, 1f) + // 状态:已上班 HH:MM + tvPunchStatus.visibility = VISIBLE + tvPunchStatus.text = "已上班 ${state.actualOnTime ?: ""}" + tvPunchStatus.setTextColor(context.getColor(R.color.success)) } } - // NFC 扫描中 → 显示提示 + // NFC 扫描中 if (state.isNfcScanning) { tvNfcHint.visibility = VISIBLE - // 扫描中禁用按钮 btnPunchIn.isEnabled = false btnPunchOut.isEnabled = false btnRevoke.isEnabled = false @@ -167,8 +173,28 @@ class PunchPanelView @JvmOverloads constructor( } } - /** 更新时钟显示 */ + /** + * 更新 NFC 扫描倒计时 + * @param secondsLeft 剩余秒数,-1 表示不显示 + */ + fun updateNfcCountdown(secondsLeft: Int) { + if (secondsLeft > 0) { + tvNfcHint.text = "请贴近信标(${secondsLeft}s)" + tvNfcHint.visibility = VISIBLE + } else { + tvNfcHint.visibility = GONE + } + } + + /** 更新时钟 */ fun updateClock() { tvPunchTime.text = DateUtil.formatTimeShort() } + + /** 设置 View 的 layout_weight */ + private fun setWeight(view: View, weight: Float) { + val lp = view.layoutParams as LinearLayout.LayoutParams + lp.weight = weight + view.layoutParams = lp + } } diff --git a/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchViewModel.kt b/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchViewModel.kt index bebca64..b3a5e53 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchViewModel.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchViewModel.kt @@ -107,12 +107,16 @@ class PunchViewModel @Inject constructor( handleNfcResult(nfcId, punchType) } - // 3. 超时自动关闭 + // 3. 倒计时 + ��时自动关闭 nfcTimeoutJob = viewModelScope.launch { - delay(nfcTimeoutMs) + val totalSeconds = (nfcTimeoutMs / 1000).toInt() + for (i in totalSeconds downTo 1) { + _uiState.update { it.copy(nfcCountdown = i) } + delay(1000) + } Timber.d("考勤: NFC超时自动关闭") closeNfc() - _uiState.update { it.copy(isNfcScanning = false, scanningPunchType = -1) } + _uiState.update { it.copy(isNfcScanning = false, scanningPunchType = -1, nfcCountdown = 0) } } } @@ -130,6 +134,8 @@ class PunchViewModel @Inject constructor( // 关闭 NFC 硬件(不播关闭音效,成功/失败音效由 API 结果决定) nfcController.stopScan() nfcController.close() + // 清除倒计时 + _uiState.update { it.copy(nfcCountdown = 0) } // 调用打卡 API viewModelScope.launch { @@ -266,6 +272,7 @@ data class PunchUiState( val actualOffTime: String? = null, val isNfcScanning: Boolean = false, val scanningPunchType: Int = -1, + val nfcCountdown: Int = 0, val punchResult: PunchResult? = null, val errorMessage: String? = null ) diff --git a/app/src/main/res/drawable/bg_btn_pill_blue.xml b/app/src/main/res/drawable/bg_btn_pill_blue.xml new file mode 100644 index 0000000..832d683 --- /dev/null +++ b/app/src/main/res/drawable/bg_btn_pill_blue.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_btn_pill_gray.xml b/app/src/main/res/drawable/bg_btn_pill_gray.xml new file mode 100644 index 0000000..e0cf86f --- /dev/null +++ b/app/src/main/res/drawable/bg_btn_pill_gray.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_dismiss_bar.xml b/app/src/main/res/drawable/bg_dismiss_bar.xml new file mode 100644 index 0000000..95b37c1 --- /dev/null +++ b/app/src/main/res/drawable/bg_dismiss_bar.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/layout/view_punch_panel.xml b/app/src/main/res/layout/view_punch_panel.xml index 4fdcba2..dca6429 100644 --- a/app/src/main/res/layout/view_punch_panel.xml +++ b/app/src/main/res/layout/view_punch_panel.xml @@ -25,7 +25,7 @@ android:paddingStart="28dp" android:paddingTop="27dp" android:paddingEnd="28dp" - android:paddingBottom="20dp"> + android:paddingBottom="8dp"> - + + + + @@ -57,48 +67,56 @@ android:gravity="center" android:orientation="horizontal"> - + - + + + + - - - + + +