今回はメッセージアプリのフォームと送信ボタンを作ってみます
前回の【リスト編】はこちらです。
#フォームを作る
フォームにはTextFormFieldというウィジェットを使います
言葉の通りテキストのフォームを配置することができます。
プロパティを設定することでフォームをカスタマイズすることができます。
##改行してもフォーム内で見える様にしたい
メッセージを入力するフォームなので、textareaの様な改行を入れられるフォームにします。
keyboardType
は入力方法を制御するためのプロパティです。今回は複数行のテキストを入力するので文字通り TextInputType.multiline
と設定します。数値入力やEmailアドレス入力などがあり、その方法にあったキーボード入力が可能です。
maxLines
とminLines
はテキスト入力フォームの高さを設定できます。フォームが空の場合は1行分の高さにし、最大5行分の高さにします。minLines
を設定しないと空の場合でもmaxLines
の高さになってしまうので注意。
new TextFormField (
keyboardType: TextInputType.multiline, //複数行のテキスト入力
maxLines: 5,
minLines: 1,
)
##プレースホルダーを表示したい
decoration
プロパティにInputDecoration
というクラスを使うと、フォームに様々なラベルやアイコンなどを表示することができます。今回はプレースホルダーを付けたいのでhintText
を使用します。
new TextFormField(
decoration: const InputDecoration(
hintText: 'メッセージを入力してください',
)
)
#送信ボタンを作る
IconButton
というウィジェットを使って送信ボタンを作ります。
IconButton(
icon: Icon(Icons.send),
color: Colors.white,
)
##送信ボタンのスタイル
このままだと味気ないので丸いボタンにし、押下時のエフェクトも付けてみます。
Ink
でスプラッシュエフェクトをつけることができます。プロパティにShapeDecoration
を設定すると丸ボタンにすることができます。ついでに色も変えました。親要素にCenter
を置くことでボタンを中央揃えにすることがきます。
Center(
child: Ink(
decoration: const ShapeDecoration(
color: Colors.green,
shape: CircleBorder(),
),
child: IconButton(
icon: Icon(Icons.send),
color: Colors.white,
),
),
)
#フォームの位置画面下部にしたい
よくあるトークの入力フォームは画面下部に固定されています。スクロールしても表示される様に今回はStack
を使います。
##Stackとは
Stackはウィジェットを重ね合わせて配置できるウィジェットです。
前回作ったリストのウィジェットとフォームを重ねて表示するようにします。
Stack(
alignment: Alignment.bottomCenter,
children : <Widget> [
//~メッセージのリストのウィジェット~,
//~入力フォームと送信ボタンのウィジェット~
]
)
#フォームや送信ボタンをタップした時の挙動をつける
タップした際に最新のメッセージが見れるようにしたいので、自動スクロールするようにします。タップした際はキーボードが展開されるため、その高さ分を考慮する必要があります。_scrollController.position.maxScrollExtent
は一番下までスクロールし、MediaQuery.of(context).viewInsets.bottom
でキーボード分の高さを取得できます。
ScrollController
のanimateTo
でスクロールさせることができます。キーボードが表示されるタイミングの関係でonTap等ですぐスクロールさせようとしてもうまくキーボードの高さが取れない時があるため、Timer
で少し、スクロールさせるタイミングをずらしています。本当はキーボードが表示されたあとといったイベントを取りたいのですが見当たりませんでした。。。
ScrollController _scrollController = new ScrollController();
void _scrollToBottom(){
_scrollController.animateTo(
_scrollController.position.maxScrollExtent + MediaQuery.of(context).viewInsets.bottom,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 300),
);
}
// onTap時に以下を流す
Timer(
Duration(milliseconds: 200),
_scrollToBottom,
);
##完成
メッセージアプリのトークっぽいUIが完成しました。
作ったコードがこちら。解説した以外のものも入ってます。
ツリー構造なので作り込むと階層深くなるのでなるべく分離できるように作るとテストも楽になると思います。
import 'package:flutter/material.dart';
import 'package:messanger/model/chatMessageModel.dart';
import 'dart:async';
class TalkMessageListPage extends StatefulWidget {
const TalkMessageListPage({Key key, this.messageList}) : super(key: key);
final List<ChatMessageModel> messageList;
@override
_TalkMessageListPageState createState() => _TalkMessageListPageState();
}
class _TalkMessageListPageState extends State<TalkMessageListPage> {
final messageTextInputCtl = new TextEditingController();
final _formKey = GlobalKey<FormState>();
ScrollController _scrollController = new ScrollController();
void _addMessage(String message) {
setState(() {
widget.messageList.add(
ChatMessageModel(
avatarUrl: "https://randomuser.me/api/portraits/men/49.jpg",
name: "自分",
datetime: "20:34",
message: message,
isMine: true,
)
);
});
}
void _scrollToBottom(){
_scrollController.animateTo(
_scrollController.position.maxScrollExtent + MediaQuery.of(context).viewInsets.bottom,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 300),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("メッセージ"),
),
body: Stack(
alignment: Alignment.bottomCenter,
children : <Widget> [
GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: ListView(
controller: _scrollController,
padding: const EdgeInsets.only(top: 10.0, right: 5.0, bottom: 50.0, left: 5.0),
children: [
for (int index = 0; index < widget.messageList.length; index++)
Card(
margin: widget.messageList[index].isMine
? EdgeInsets.only(top: 5.0, left: 90.0, bottom: 5.0, right: 8.0)
: EdgeInsets.only(top: 5.0, left: 8.0, bottom: 5.0, right: 90.0),
child:ListTile(
title:Text(widget.messageList[index].message),
subtitle: Row(
mainAxisAlignment: widget.messageList[index].isMine
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: <Widget>[
CircleAvatar(
backgroundImage: NetworkImage(widget.messageList[index].avatarUrl),
radius: 10.0,
),
Text(widget.messageList[index].name + widget.messageList[index].datetime),
]
),
),
),
],
)
),
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
new Container(
color: Colors.green[100],
child: Column(
children: <Widget>[
new Form(
key: _formKey,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
new Flexible(
child: new TextFormField(
controller: messageTextInputCtl,
keyboardType: TextInputType.multiline,
maxLines: 5,
minLines: 1,
decoration: const InputDecoration(
hintText: 'メッセージを入力してください',
),
onTap: (){
// タイマーを入れてキーボード分スクロールする様に
Timer(
Duration(milliseconds: 200),
_scrollToBottom,
);
},
)
),
Material(
color: Colors.green[100],
child: Center(
child: Ink(
decoration: const ShapeDecoration(
color: Colors.green,
shape: CircleBorder(),
),
child: IconButton(
icon: Icon(Icons.send),
color: Colors.white,
onPressed: () {
_addMessage(messageTextInputCtl.text);
FocusScope.of(context).unfocus();
messageTextInputCtl.clear();
Timer(
Duration(milliseconds: 200),
_scrollToBottom,
);
},
),
),
),
)
]
)
),
]
)
),
],
)
]
),
);
}
}
Githubはこちら
随時更新しているので今回解説した状態から変わる可能性もありますのでご了承ください。