7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

今回はメッセージアプリのフォームと送信ボタンを作ってみます
前回の【リスト編】はこちらです。

#フォームを作る
フォームにはTextFormFieldというウィジェットを使います
言葉の通りテキストのフォームを配置することができます。
プロパティを設定することでフォームをカスタマイズすることができます。

##改行してもフォーム内で見える様にしたい
メッセージを入力するフォームなので、textareaの様な改行を入れられるフォームにします。
keyboardTypeは入力方法を制御するためのプロパティです。今回は複数行のテキストを入力するので文字通り TextInputType.multilineと設定します。数値入力やEmailアドレス入力などがあり、その方法にあったキーボード入力が可能です。
maxLinesminLinesはテキスト入力フォームの高さを設定できます。フォームが空の場合は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でキーボード分の高さを取得できます。
ScrollControlleranimateToでスクロールさせることができます。キーボードが表示されるタイミングの関係で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が完成しました。
c8323e29a706520dbe474cf14de94c73.gif

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

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はこちら
随時更新しているので今回解説した状態から変わる可能性もありますのでご了承ください。

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?