feat: 任务详情页语音播报功能 (REQ-20260508-0006)

- 详情页右侧悬浮播放按钮(36dp半透明圆形)
- 播报内容:任务名+地点+时间+积分(>0)+备注,空字段跳过
- 播放中按钮高亮+图标切换,再点停止
- 离开页面自动停止播放
- TTS播放期间抑制提示音,避免叠加

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-05-08 12:57:51 +09:30
parent f01b1e3311
commit cf4458526b
7 changed files with 141 additions and 3 deletions

View File

@@ -24,7 +24,8 @@ import javax.inject.Singleton
@Singleton
class FiseVibrationController @Inject constructor(
@ApplicationContext private val context: Context,
private val configManager: VibrationConfigManager
private val configManager: VibrationConfigManager,
private val edgeTtsManager: com.xiaoqu.watch.service.manager.EdgeTtsManager
) : VibrationController {
/** 系统振动器 */
@@ -98,9 +99,11 @@ class FiseVibrationController @Inject constructor(
}
}
// 音频:系统级开关 + 用户级开关 + 有音频文件
if (voiceOk) {
// 音频:系统级开关 + 用户级开关 + 有音频文件 + TTS 未在播放
if (voiceOk && !edgeTtsManager.isPlaying) {
playAudio(pattern.audioResId, configManager.voiceVolume)
} else if (voiceOk && edgeTtsManager.isPlaying) {
Timber.d("振动: TTS 播放中,跳过提示音")
}
}

View File

@@ -13,6 +13,7 @@ import com.xiaoqu.watch.databinding.FragmentTaskDetailBinding
import com.xiaoqu.watch.network.ApiResult
import com.xiaoqu.watch.network.api.TaskApi
import com.xiaoqu.watch.network.safeApiCall
import com.xiaoqu.watch.service.manager.EdgeTtsManager
import com.xiaoqu.watch.service.manager.NfcTaskManager
import com.xiaoqu.watch.ui.common.BaseFragment
import com.xiaoqu.watch.ui.widget.QuTipDialog
@@ -30,6 +31,7 @@ class TaskDetailFragment : BaseFragment<FragmentTaskDetailBinding>() {
@Inject lateinit var taskApi: TaskApi
@Inject lateinit var nfcTaskManager: NfcTaskManager
@Inject lateinit var edgeTtsManager: EdgeTtsManager
/** 当前任务数据 */
private var taskDetail: TaskDetail? = null
@@ -53,6 +55,9 @@ class TaskDetailFragment : BaseFragment<FragmentTaskDetailBinding>() {
findNavController().popBackStack()
}
// TTS 播放按钮
binding.btnTts.setOnClickListener { toggleTts() }
// 从导航参数获取任务 ID
val taskId = arguments?.getLong("taskId", 0) ?: 0
if (taskId > 0) {
@@ -284,4 +289,69 @@ class TaskDetailFragment : BaseFragment<FragmentTaskDetailBinding>() {
}
}
}
// ===== TTS 语音播报 =====
/** 切换播放/停止 */
private fun toggleTts() {
if (edgeTtsManager.isPlaying) {
stopTts()
} else {
startTts()
}
}
/** 开始播报当前任务内容 */
private fun startTts() {
val detail = taskDetail ?: return
// 拼接播报文本:有值的字段才读,空字段跳过
val parts = mutableListOf<String>()
if (detail.name.isNotBlank()) parts.add("任务:${detail.displayName}")
if (detail.positionText.isNotBlank()) parts.add("地点:${detail.positionText}")
if (detail.sendTime.isNotBlank()) parts.add("时间:${detail.sendTime}")
if (detail.point > 0) parts.add("积分:${detail.pointText}")
if (detail.content.isNotBlank()) parts.add("备注:${detail.content}")
if (parts.isEmpty()) return
val text = parts.joinToString("")
Timber.d("TTS 播报: $text")
// 更新按钮状态为播放中
updateTtsButton(playing = true)
// 播放完成回调:恢复按钮
edgeTtsManager.onComplete = {
activity?.runOnUiThread { updateTtsButton(playing = false) }
}
edgeTtsManager.speak(text) { errorMsg ->
Timber.w("TTS 播报失败: $errorMsg")
updateTtsButton(playing = false)
}
}
/** 停止播报 */
private fun stopTts() {
edgeTtsManager.stop()
updateTtsButton(playing = false)
}
/** 更新 TTS 按钮的 UI 状态 */
private fun updateTtsButton(playing: Boolean) {
binding.btnTts.setImageResource(
if (playing) R.drawable.ic_speaker_stop else R.drawable.ic_speaker
)
binding.btnTts.setBackgroundResource(
if (playing) R.drawable.bg_tts_button_active else R.drawable.bg_tts_button
)
}
override fun onDestroyView() {
// 离开页面自动停止播放
edgeTtsManager.stop()
edgeTtsManager.onComplete = null
super.onDestroyView()
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- TTS 悬浮按钮背景:半透明圆形 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#4DFFFFFF" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- TTS 悬浮按钮背景(播放中):主题色半透明圆形 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#803B9EFF" />
</shape>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 喇叭图标语音播报按钮24×24dp -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- 喇叭主体 -->
<path
android:fillColor="@color/text_primary"
android:pathData="M3,9v6h4l5,5V4L7,9H3z" />
<!-- 声波 -->
<path
android:fillColor="@color/text_primary"
android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02z" />
<path
android:fillColor="@color/text_primary"
android:pathData="M14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z" />
</vector>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 停止图标语音播报停止状态24×24dp -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- 喇叭主体 -->
<path
android:fillColor="@color/text_primary"
android:pathData="M3,9v6h4l5,5V4L7,9H3z" />
<!-- 叉号(静音标记) -->
<path
android:fillColor="@color/text_primary"
android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v2.21l2.45,2.45c0.03,-0.2 0.05,-0.41 0.05,-0.63z" />
<path
android:fillColor="@color/text_primary"
android:pathData="M19,12c0,0.94 -0.2,1.82 -0.54,2.64l1.51,1.51C20.63,14.91 21,13.5 21,12c0,-4.28 -2.99,-7.86 -7,-8.77v2.06c2.89,0.86 5,3.54 5,6.71z" />
<path
android:fillColor="@color/text_primary"
android:pathData="M4.27,3L3,4.27 7.73,9H3v6h4l5,5v-6.73l4.25,4.25c-0.67,0.52 -1.42,0.93 -2.25,1.18v2.06c1.38,-0.31 2.63,-0.95 3.69,-1.81L19.73,21 21,19.73l-9,-9L4.27,3z" />
</vector>

View File

@@ -101,6 +101,18 @@
</ScrollView>
<!-- TTS 语音播报悬浮按钮右侧垂直居中36dp 圆形,触摸区 44dp -->
<ImageView
android:id="@+id/btnTts"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical|end"
android:layout_marginEnd="8dp"
android:padding="6dp"
android:src="@drawable/ic_speaker"
android:background="@drawable/bg_tts_button"
android:contentDescription="语音播报" />
<!-- 底部固定操作按钮 -->
<TextView
android:id="@+id/btnAction"