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
|
@Singleton
|
||||||
class FiseVibrationController @Inject constructor(
|
class FiseVibrationController @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val configManager: VibrationConfigManager
|
private val configManager: VibrationConfigManager,
|
||||||
|
private val edgeTtsManager: com.xiaoqu.watch.service.manager.EdgeTtsManager
|
||||||
) : VibrationController {
|
) : VibrationController {
|
||||||
|
|
||||||
/** 系统振动器 */
|
/** 系统振动器 */
|
||||||
@@ -98,9 +99,11 @@ class FiseVibrationController @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 音频:系统级开关 + 用户级开关 + 有音频文件
|
// 音频:系统级开关 + 用户级开关 + 有音频文件 + TTS 未在播放
|
||||||
if (voiceOk) {
|
if (voiceOk && !edgeTtsManager.isPlaying) {
|
||||||
playAudio(pattern.audioResId, configManager.voiceVolume)
|
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.ApiResult
|
||||||
import com.xiaoqu.watch.network.api.TaskApi
|
import com.xiaoqu.watch.network.api.TaskApi
|
||||||
import com.xiaoqu.watch.network.safeApiCall
|
import com.xiaoqu.watch.network.safeApiCall
|
||||||
|
import com.xiaoqu.watch.service.manager.EdgeTtsManager
|
||||||
import com.xiaoqu.watch.service.manager.NfcTaskManager
|
import com.xiaoqu.watch.service.manager.NfcTaskManager
|
||||||
import com.xiaoqu.watch.ui.common.BaseFragment
|
import com.xiaoqu.watch.ui.common.BaseFragment
|
||||||
import com.xiaoqu.watch.ui.widget.QuTipDialog
|
import com.xiaoqu.watch.ui.widget.QuTipDialog
|
||||||
@@ -30,6 +31,7 @@ class TaskDetailFragment : BaseFragment<FragmentTaskDetailBinding>() {
|
|||||||
|
|
||||||
@Inject lateinit var taskApi: TaskApi
|
@Inject lateinit var taskApi: TaskApi
|
||||||
@Inject lateinit var nfcTaskManager: NfcTaskManager
|
@Inject lateinit var nfcTaskManager: NfcTaskManager
|
||||||
|
@Inject lateinit var edgeTtsManager: EdgeTtsManager
|
||||||
|
|
||||||
/** 当前任务数据 */
|
/** 当前任务数据 */
|
||||||
private var taskDetail: TaskDetail? = null
|
private var taskDetail: TaskDetail? = null
|
||||||
@@ -53,6 +55,9 @@ class TaskDetailFragment : BaseFragment<FragmentTaskDetailBinding>() {
|
|||||||
findNavController().popBackStack()
|
findNavController().popBackStack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TTS 播放按钮
|
||||||
|
binding.btnTts.setOnClickListener { toggleTts() }
|
||||||
|
|
||||||
// 从导航参数获取任务 ID
|
// 从导航参数获取任务 ID
|
||||||
val taskId = arguments?.getLong("taskId", 0) ?: 0
|
val taskId = arguments?.getLong("taskId", 0) ?: 0
|
||||||
if (taskId > 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>
|
</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
|
<TextView
|
||||||
android:id="@+id/btnAction"
|
android:id="@+id/btnAction"
|
||||||
|
|||||||
Reference in New Issue
Block a user