Flutter iOS 音声メモアプリ開発(6) 音声認識用TextField
Flutterの音声認識(speech_to_text)については、何度も利用してきました。今までは、音声認識結果をText Widgetに表示していましたが、これをTextFieldに変更したいと思います。これは、認識結果が少し間違っていた場合など、直ぐに修正できるようにするためです。
合わせて、再利用が可能なように、クラス化してみたいと思います。
事前準備
まずは音声認識用のパッケージ(speech_to_text)のインストールをします。
また、iOSの場合には、info.plistファイルの修正も忘れない様にします。
音声認識の事前準備は、以下の記事を参考にしてください。
TextFieldのイベント処理追加
TextFieldは入力値の値が変わったとき、onChangeイベントを使って入力値を取得することができます。ただし、これはユーザがキーボードから入力した場合にのみ発生するイベントであって、音声認識結果でTextFieldの値を変更してもonChangeイベントは発生しません。
そこで、TextFieldにcontrollerで設定したTextEditingController
オブジェクトをListenし、変更を検知する必要があります。
以下は、TextEditingController
を使用して、音声入力された時のイベントを取得する方法の例です。
final TextEditingController _controller = TextEditingController();
@override
void initState() {
super.initState();
_controller.addListener(_onTextChanged);
}
void _onTextChanged() {
String text = _controller.text;
// テキストが変更されたら、ここで行いたい処理を行う
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
);
}
これで、音声認識結果を_controller.textに代入したときにでも、_onTextChanged()関数が呼び出されます。
マイクボタンの追加
TextFiledにマイクボタンを追加して、ボタンのトリガーで、音声認識のON/OFFを制御します。
@override
Widget build(BuildContext context) {
return Column(children: [
CupertinoTextField(
placeholder: 'マイクアイコンを押して話しかけてください...',
controller: _controller,
suffix: GestureDetector(
onTap: () {
if (_isListening) {
_stopListening();
} else {
_controller.text = '';
_startListening();
}
},
child: Icon(
_isListening ? CupertinoIcons.paperplane : CupertinoIcons.mic),
),
),
]);
}
suffixによって、TextFiledの後にWidgetを付加します。今回はIconを追加しています。
_isListeningが音声認識中か否かを持つフラグです。Iconをタップした時、認識中であれば音声認識をストップし、認識中でなければ、TextFiledの値を初期化して認識を開始します。
また、認識中か否かで、"マイク"と"紙飛行機"のどちらのIconを表示するかを変えています。
コールバック関数
音声認識の結果を呼び出し元に渡すためには、コールバック関数を定義します。
一般的には、Widget間で値を引き渡す方法は、以下の様になります。
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
String _text = '';
@override
Widget build(BuildContext context) {
return ChildWidget(
callback: (value) {
setState(() {
_text = value;
});
},
);
}
}
class ChildWidget extends StatelessWidget {
final Function(String) callback;
const ChildWidget({Key key, this.callback}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextField(
onChanged: (value) {
callback(value);
},
);
}
}
この例では、ChildWidgetのonChangedイベント発生時にコールバック関数(callback(value))を読んでいます。しかし、先述の通り、キーボードからの入力でなければonChangedは発生しません。今回の場合、Listenerの中でコールバック関数を呼ぶこととなります。
@override
void initState() {
super.initState();
//リスナーの定義と、コールバック
_controller.addListener(() {
String text = _controller.text;
widget.onTextChanged(text);
});
}
サンプルコード
音声入力用TextFieldの全体のコードは、以下の様になります。
import 'package:flutter/cupertino.dart';
import 'package:speech_to_text/speech_to_text.dart' as stt;
class VoiceTextField extends StatefulWidget {
final void Function(String) onTextChanged; // コールバック関数の定義
const VoiceTextField({super.key, required this.onTextChanged}); // requiredを追加
@override
_VoiceTextFieldState createState() => _VoiceTextFieldState();
}
class _VoiceTextFieldState extends State<VoiceTextField> {
final _speech = stt.SpeechToText();
bool _isListening = false;
final TextEditingController _controller = TextEditingController();
String text = '';
@override
void initState() {
super.initState();
//リスナーの定義と、コールバック
_controller.addListener(() {
String text = _controller.text;
widget.onTextChanged(text);
});
}
//音声認識スタート
void _startListening() async {
if (!_isListening) {
bool available = await _speech.initialize();
if (available) {
setState(() => _isListening = true);
_speech.listen(onResult: (result) {
setState(() {
_controller.text = result.recognizedWords;
});
});
} else {
setState(() {
_isListening = false;
});
}
}
}
//音声認識ストップ
void _stopListening() {
if (_isListening) {
setState(() => _isListening = false);
_speech.stop();
}
}
@override
Widget build(BuildContext context) {
return Column(children: [
CupertinoTextField(
placeholder: 'マイクアイコンを押して話しかけてください...',
controller: _controller,
suffix: GestureDetector(
onTap: () {
if (_isListening) {
_stopListening();
} else {
_controller.text = '';
_startListening();
}
},
child: Icon(
_isListening ? CupertinoIcons.paperplane : CupertinoIcons.mic),
),
),
]);
}
@override
void dispose() {
_speech.stop();
super.dispose();
}
}
このクラスの使用例(main .dart)は以下の様になります。
import 'package:flutter/material.dart';
import 'voice_textfield.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Voice textField',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String voiceInput = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
VoiceTextField(onTextChanged: (text) { // 音声認識用TextField
setState(() {
voiceInput = text;
});
}),
const SizedBox(height: 20),
Text(
'認識結果:$voiceInput',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
),
);
}
}
実行結果は、以下の様な画面になります。
これで、VoiceTextFieldの値が変わるたびに、MyHomePageのvoiceInput変数が更新されることとなります。