点击一个叫voice的按键开始录音,录音结果输入到editTextMessage里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package com.example.filmguide

import android.Manifest
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.speech.RecognizerIntent
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.filmguide.databinding.ActivityAiactivityBinding
import com.example.filmguide.logic.model.ChatMessage
import com.example.filmguide.ui.ChatAdapter
import java.util.*

class AIActivity : AppCompatActivity() {
private lateinit var binding: ActivityAiactivityBinding
private lateinit var adapter: ChatAdapter
private val messages = mutableListOf<ChatMessage>()

companion object {
private const val REQUEST_RECORD_AUDIO = 100
private const val REQ_SPEECH = 101
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAiactivityBinding.inflate(layoutInflater)
setContentView(binding.root)

adapter = ChatAdapter(messages)
binding.recyclerViewMessages.layoutManager = LinearLayoutManager(this)
binding.recyclerViewMessages.adapter = adapter

binding.buttonSend.setOnClickListener {
sendMessage()
}

// 点击“语音”图标时,先检查权限再启动语音识别
binding.voice.setOnClickListener {
ensureAudioPermission()
}
}

// 检查录音权限
private fun ensureAudioPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.RECORD_AUDIO),
REQUEST_RECORD_AUDIO
)
} else {
startSpeechInput()
}
}

override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_RECORD_AUDIO &&
grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
startSpeechInput()
} else {
Toast.makeText(this, "需要录音权限才能识别语音", Toast.LENGTH_SHORT).show()
}
}

// 启动系统语音识别
private fun startSpeechInput() {
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
)
putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault())
putExtra(RecognizerIntent.EXTRA_PROMPT, "请说话…")
}
try {
startActivityForResult(intent, REQ_SPEECH)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, "设备不支持语音识别", Toast.LENGTH_SHORT).show()
}
}

// 接收语音识别结果,填入 editText
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQ_SPEECH && resultCode == Activity.RESULT_OK) {
val results = data
?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
val text = results?.firstOrNull().orEmpty()
// 直接填充到输入框
binding.editTextMessage.setText(text)
}
}

private fun sendMessage() {
val text = binding.editTextMessage.text.toString().trim()
if (text.isNotEmpty()) {
messages.add(ChatMessage(text, true))
adapter.notifyItemInserted(messages.lastIndex)
binding.recyclerViewMessages.scrollToPosition(messages.lastIndex)
binding.editTextMessage.text?.clear()

// 模拟回复
Handler(Looper.getMainLooper()).postDelayed({
messages.add(ChatMessage("收到:$text", false))
adapter.notifyItemInserted(messages.lastIndex)
binding.recyclerViewMessages.scrollToPosition(messages.lastIndex)
}, 800)
}
}
}

录音功能做出来了,但是录音界面太丑了,自定义背景方法如下

系统自带的 RecognizerIntent 界面几乎无法定制,如果要好看又与 App 风格一致,推荐自己写一个录音弹窗/浮层,用底层的 SpeechRecognizer 做识别,再在界面上展示自己的动画、波形、提示文字和按钮。下面给你一个思路:


1. 布局文件:自定义一个录音对话框 (res/layout/dialog_record.xml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="24dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_record_dialog"
android:gravity="center">

<ImageView
android:id="@+id/ivMic"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@drawable/ic_mic"
android:tint="@color/colorAccent" />

<ProgressBar
android:id="@+id/volumeBar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_marginTop="12dp"
android:progress="0"
android:max="100"/>

<TextView
android:id="@+id/tvHint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="请说话…"
android:textSize="14sp"
android:textColor="@color/black"
android:layout_marginTop="12dp"/>
</LinearLayout>
  • bg_record_dialog.xml 可以是一个圆角+半透明背景的 shape。
  • ic_mic 可以是自己设计的动效图标。

2. 弹窗代码:用 DialogFragment 管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class RecordDialogFragment(
private val onResult: (String) -> Unit
) : DialogFragment(), RecognitionListener {

private lateinit var recognizer: SpeechRecognizer
private lateinit var ivMic: ImageView
private lateinit var volumeBar: ProgressBar
private lateinit var tvHint: TextView

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val view = LayoutInflater.from(context).inflate(R.layout.dialog_record, null)
ivMic = view.findViewById(R.id.ivMic)
volumeBar = view.findViewById(R.id.volumeBar)
tvHint = view.findViewById(R.id.tvHint)

recognizer = SpeechRecognizer.createSpeechRecognizer(requireContext()).apply {
setRecognitionListener(this@RecordDialogFragment)
}

val dialog = AlertDialog.Builder(requireContext())
.setView(view)
.setCancelable(false)
.create()

dialog.setOnShowListener {
startListening()
}
return dialog
}

private fun startListening() {
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
putExtra(RecognizerIntent.EXTRA_LANGUAGE, "zh-CN")
putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
}
recognizer.startListening(intent)
}

override fun onReadyForSpeech(params: Bundle?) {
tvHint.text = "请开始说话"
}

override fun onBeginningOfSpeech() {
ivMic.setImageResource(R.drawable.ic_mic_active) // 切换高亮图标
}

override fun onRmsChanged(rmsdB: Float) {
// rmsdB 大致在 0–10 之间,映射到 0–100
val level = ((rmsdB.coerceIn(0f, 10f) / 10f) * 100).toInt()
volumeBar.progress = level
}

override fun onEndOfSpeech() {
tvHint.text = "识别中…"
}

override fun onError(error: Int) {
tvHint.text = "识别失败,请重试"
Handler(Looper.getMainLooper()).postDelayed({ dismiss() }, 1000)
}

override fun onResults(results: Bundle?) {
val list = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
val text = list?.firstOrNull().orEmpty()
onResult(text)
dismiss()
}

override fun onPartialResults(partialResults: Bundle?) {
// 可选:显示中间识别结果
}

override fun onBufferReceived(buffer: ByteArray?) {}
override fun onEvent(eventType: Int, params: Bundle?) {}
override fun onDestroy() {
super.onDestroy()
recognizer.destroy()
}
}

3. 在 Activity/Fragment 中调用

1
2
3
4
5
6
binding.voice.setOnClickListener {
RecordDialogFragment { recognizedText ->
// 识别结果回调,填入 EditText
binding.editTextMessage.setText(recognizedText)
}.show(supportFragmentManager, "record")
}

优化点

  • 美化动效:可以用 Lottie 播放一段话筒动画,或者用帧动画切换不同图标。
  • 边说边显示:在 onPartialResults 里实时更新 editText 或者 hint 文本,感知更加即时。
  • 超时/取消:Dialog 上加个“取消”按钮,或者识别超时自动结束并提示。
  • 主题一致:背景、文字、图标都按项目主色调,保持 UI 风格统一。

这样你就有了一个完全自定义、可随意美化的“录音识别”界面,既好看又灵活。