feat: 任务详情页语音播报功能 (REQ-20260508-0006)
- 详情页右侧悬浮播放按钮(36dp半透明圆形) - 播报内容:任务名+地点+时间+积分(>0)+备注,空字段跳过 - 播放中按钮高亮+图标切换,再点停止 - 离开页面自动停止播放 - TTS播放期间抑制提示音,避免叠加 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 播放中,跳过提示音")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
6
app/src/main/res/drawable/bg_tts_button.xml
Normal file
6
app/src/main/res/drawable/bg_tts_button.xml
Normal 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>
|
||||
6
app/src/main/res/drawable/bg_tts_button_active.xml
Normal file
6
app/src/main/res/drawable/bg_tts_button_active.xml
Normal 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>
|
||||
19
app/src/main/res/drawable/ic_speaker.xml
Normal file
19
app/src/main/res/drawable/ic_speaker.xml
Normal 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>
|
||||
22
app/src/main/res/drawable/ic_speaker_stop.xml
Normal file
22
app/src/main/res/drawable/ic_speaker_stop.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user