FlutterでメッセージアプリのトークっぽいUIを作る【フォーム編】


今回はメッセージアプリのフォームと送信ボタンを作ってみます
前回の【リスト編】は[こちら](https://qiita.com/guevara-net/items/1008922e9ee492e909b6)です。

#フォームを作る
フォームには[TextFormField](https://api.flutter.dev/flutter/material/TextFormField-class.html)というウィジェットを使います
言葉の通りテキストのフォームを配置することができます。
プロパティを設定することでフォームをカスタマイズすることができます。

##改行してもフォーム内で見える様にしたい
メッセージを入力するフォームなので、textareaの様な改行を入れられるフォームにします。
`keyboardType`は入力方法を制御するためのプロパティです。今回は複数行のテキストを入力するので文字通り `TextInputType.multiline`と設定します。数値入力やEmailアドレス入力などがあり、その方法にあったキーボード入力が可能です。
`maxLines`と`minLines`はテキスト入力フォームの高さを設定できます。フォームが空の場合は1行分の高さにし、最大5行分の高さにします。`minLines`を設定しないと空の場合でも`maxLines`の高さになってしまうので注意。

```dart
new TextFormField (
  keyboardType: TextInputType.multiline, //複数行のテキスト入力
  maxLines: 5,
  minLines: 1,
)
```

##プレースホルダーを表示したい
`decoration`プロパティに`InputDecoration`というクラスを使うと、フォームに様々なラベルやアイコンなどを表示することができます。今回はプレースホルダーを付けたいので`hintText`を使用します。

```dart
new TextFormField(
  decoration: const InputDecoration(
    hintText: 'メッセージを入力してください',
  )
)
```

#送信ボタンを作る
`IconButton`というウィジェットを使って送信ボタンを作ります。

```dart
IconButton(
  icon: Icon(Icons.send),
  color: Colors.white,
)
```

##送信ボタンのスタイル
このままだと味気ないので丸いボタンにし、押下時のエフェクトも付けてみます。
`Ink`でスプラッシュエフェクトをつけることができます。プロパティに`ShapeDecoration`を設定すると丸ボタンにすることができます。ついでに色も変えました。親要素に`Center`を置くことでボタンを中央揃えにすることがきます。

```dart
Center(
  child: Ink(
    decoration: const ShapeDecoration(
      color: Colors.green,
      shape: CircleBorder(),
    ),
    child: IconButton(
      icon: Icon(Icons.send),
      color: Colors.white,
    ),
  ),
)
```

#フォームの位置画面下部にしたい
よくあるトークの入力フォームは画面下部に固定されています。スクロールしても表示される様に今回は`Stack`を使います。

##[Stack](https://api.flutter.dev/flutter/widgets/Stack-class.html)とは
Stackはウィジェットを重ね合わせて配置できるウィジェットです。
前回作ったリストのウィジェットとフォームを重ねて表示するようにします。

```dart
Stack(
  alignment: Alignment.bottomCenter,
  children : <Widget> [
    //~メッセージのリストのウィジェット~,
    //~入力フォームと送信ボタンのウィジェット~
  ]
)
```

#フォームや送信ボタンをタップした時の挙動をつける
タップした際に最新のメッセージが見れるようにしたいので、自動スクロールするようにします。タップした際はキーボードが展開されるため、その高さ分を考慮する必要があります。`_scrollController.position.maxScrollExtent`は一番下までスクロールし、`MediaQuery.of(context).viewInsets.bottom`でキーボード分の高さを取得できます。
`ScrollController`の`animateTo`でスクロールさせることができます。キーボードが表示されるタイミングの関係でonTap等ですぐスクロールさせようとしてもうまくキーボードの高さが取れない時があるため、`Timer`で少し、スクロールさせるタイミングをずらしています。本当はキーボードが表示されたあとといったイベントを取りたいのですが見当たりませんでした。。。

```dart
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が完成しました。
![c8323e29a706520dbe474cf14de94c73.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541992/f6446aa1-a1c9-e43b-9835-9c1c7f5a1c75.gif)


作ったコードがこちら。解説した以外のものも入ってます。
ツリー構造なので作り込むと階層深くなるのでなるべく分離できるように作るとテストも楽になると思います。

```dart:talkMessageListPage.dart
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は[こちら](https://github.com/taka-guevara/FlutterMessangerApp)
随時更新しているので今回解説した状態から変わる可能性もありますのでご了承ください。