イケてるFlutterのUIからいい感じの実装を紹介する

# はじめに

昨日は@unsoluble_sugarさんの[FlutterでFirebase Authenticationを使ったGoogleアカウントログインを実装してみた](https://qiita.com/unsoluble_sugar/items/95b16c01b456be19f9ac)という記事でした。
同じgoogle系のサービスだけあって、設定などが簡単で使いやすい印象をうけました。

はじめまして。
flutterはほぼ初心者ですが、アドベントカレンダーはいい機会だと思い書いてみることにしました。
テーマは、タイトルの通り『イケてるFlutterのUIからいい感じの実装を紹介する』です。
イカしたUIはあるものの、それの実装を探す作業が割と面倒臭い経験があったため、少しでも同じような方の参考となれば幸いです。

# 参考にしたページ
[Flutter-UI-Kit - github](https://github.com/iampawan/Flutter-UI-Kit)
[flutter-ui-nice - github](https://github.com/nb312/flutter-ui-nice)

## イケてるカード
まずは、以下のような画面の中の１つのカードから。
[元コードリンク](https://github.com/iampawan/Flutter-UI-Kit/blob/master/lib/ui/page/home_page.dart)
<img width="368" alt="スクリーンショット 2019-12-06 22.47.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190157/0a09b733-da94-0247-fb09-3afd81a43782.png">

ポイントとしては、

- `Stack`を使ってWidgetの上下関係を構築している
- `Padding`を使って画像の上に文字を出している
- `Positioned`Widgetの使い方

あたりが参考になりました。


```dart
// 3
  Widget menuImage(Menu menu) => Image.asset(
        menu.image,
        fit: BoxFit.cover,
      );
```

```dart
  Widget menuIOS(Menu menu, BuildContext context) {
    return Container(
      height: deviceSize.height / 2,
      decoration: ShapeDecoration(
        shape:
            RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
      ),
// 1
      child: Card(
        clipBehavior: Clip.antiAlias,
        elevation: 3.0,
        margin: EdgeInsets.all(16.0),
        child: Stack(
          fit: StackFit.expand,
          children: <Widget>[
// 3
            menuImage(menu),
// 2
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Text(
                menu.title,
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 28.0,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
// 4
            Positioned(
              bottom: 0.0,
              left: 0.0,
              right: 0.0,
              height: 60.0,
              child: Container(
                width: double.infinity,
                color: menu.menuColor,
                child: iosCardBottom(menu, context),
              ),
            )
          ],
        ),
      ),
    );
  }
```

```dart
// 4
  Widget iosCardBottom(Menu menu, BuildContext context) => Padding(
        padding: const EdgeInsets.all(12.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            Container(
              width: 40.0,
              decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(10.0),
                  border: Border.all(width: 3.0, color: Colors.white),
                  image: DecorationImage(
                      fit: BoxFit.cover,
                      image: AssetImage(
                        menu.image,
                      ))),
            ),
            SizedBox(
              width: 20.0,
            ),
            Text(
              menu.title,
              textAlign: TextAlign.start,
              style: TextStyle(color: Colors.white),
            ),
            SizedBox(
              width: 20.0,
            ),
            FittedBox(
              child: CupertinoButton(
                onPressed: () => _showModalBottomSheet(context, menu),
                borderRadius: BorderRadius.circular(50.0),
                child: Text(
                  "Go",
                  textAlign: TextAlign.left,
                  style: TextStyle(color: CupertinoColors.activeBlue),
                ),
                color: Colors.white,
              ),
            )
          ],
        ),
      );
```

## イカしたボトムメニュー
次は以下の画像の、ボトムのメニューです。
[元コードリンク](https://github.com/iampawan/Flutter-UI-Kit/blob/master/lib/ui/page/shopping/product_detail_page.dart)

<img width="371" alt="スクリーンショット 2019-12-06 23.06.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190157/05238095-d81b-80d3-9f56-214f55327999.png">

参考になったポイントは以下です

- `FloatingActionButton`がめり込むボトムメニューの実装方法
- `LinearGradient`を使った、グラデーションのイケてる表示
- `SizedBox` Widgetの効果的な使い方

```dart
// 1
  Widget myBottomBar() => BottomAppBar(
        clipBehavior: Clip.antiAlias,
        shape: CircularNotchedRectangle(),
        child: Ink(
          height: 50.0,
          decoration: new BoxDecoration(
              gradient: new LinearGradient(colors: UIData.kitGradients)),
// 3
          child: new Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              SizedBox(
                height: double.infinity,
                child: new InkWell(
                  radius: 10.0,
                  splashColor: Colors.yellow,
                  onTap: () {},
                  child: Center(
                    child: new Text(
                      "ADD TO WISHLIST",
                      style: new TextStyle(
                          fontSize: 12.0,
                          fontWeight: FontWeight.bold,
                          color: Colors.white),
                    ),
                  ),
                ),
              ),
              new SizedBox(
                width: 20.0,
              ),
              SizedBox(
                height: double.infinity,
                child: new InkWell(
                  onTap: () {},
                  radius: 10.0,
                  splashColor: Colors.yellow,
                  child: Center(
                    child: new Text(
                      "ORDER PAGE",
                      style: new TextStyle(
                          fontSize: 12.0,
                          fontWeight: FontWeight.bold,
                          color: Colors.white),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      );
```

呼び出し部分

```dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // (中略)
      body: bodyData,
// 2
      floatingActionButton: showFAB
          ? CustomFloat(
              builder: centerDocked
                  ? Text("5", style: TextStyle(color: Colors.white, fontSize: 10.0))
                  : null,
              icon: floatingIcon,  // Icons.add_shopping_cart を引数に渡されている
              qrCallback: () {},
            )
          : null,
      floatingActionButtonLocation: centerDocked
          ? FloatingActionButtonLocation.centerDocked
          : FloatingActionButtonLocation.endFloat,
      bottomNavigationBar: showBottomNav ? myBottomBar() : null,
    );
  }
```


## イカしたTweetコンポーネント
いつかTwitterを作る時のために。
[元コードリンク](https://github.com/iampawan/Flutter-UI-Kit/blob/master/lib/ui/page/timeline/timeline_two_page.dart)
<img width="368" alt="スクリーンショット 2019-12-06 22.37.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190157/1d0ee9b1-fa23-7e33-3341-e939f28c6e09.png">
(見にくいですが、コメント、RTなどの欄が６です）

参考になったポイントは以下です

- `Column`と`Row`の入れ子構造の実装
- `Column`への配置の大きさの目安

```dart
  Widget bodyList(List<Post> posts) => ListView.builder(
        controller: scrollController,
        itemCount: posts.length,
        itemBuilder: (context, i) {
          Post post = posts[i];
          return Card(
            color: Colors.grey.shade900,
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.start,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
// 1
                  CircleAvatar(
                      radius: 25.0,
                      backgroundImage: NetworkImage(
                        post.personImage,
                      )),
// 2
                  rightColumn(post),
                ],
              ),
            ),
          );
        },
      );
```

```dart
  Widget rightColumn(Post post) => Expanded(
        child: Padding(
          padding: const EdgeInsets.only(left: 16.0, right: 4.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
// 3
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: RichText(
                  maxLines: 1,
                  text: TextSpan(children: [
                    TextSpan(
                      text: "${post.personName}  ",
                    ),
                    TextSpan(
                        text: "@${post.address} · ",
                        style: TextStyle(color: Colors.grey)),
                    TextSpan(
                        text: "${post.postTime}",
                        style: TextStyle(color: Colors.grey))
                  ]),
                ),
              ),
// 4
              Padding(  
                padding: const EdgeInsets.all(8.0),
                child: Text(
                  post.message,
                  style: TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.normal,
                      fontFamily: UIData.quickFont),
                ),
              ),
// 4,5間の空白
              SizedBox(
                height: 10.0,
              ),
// 5
              post.messageImage != null
                  ? Material(
                      borderRadius: BorderRadius.circular(8.0),
                      child: Image.network(
                        post.messageImage,
                        gaplessPlayback: true,
                        fit: BoxFit.cover,
                      ),
                    )
                  : Container(),
              SizedBox(
                height: 20.0,
              ),
// 6
              actionRow(post),
            ],
          ),
        ),
      );
```

```dart
// 6
  Widget actionRow(Post post) => Padding(
        padding: const EdgeInsets.only(right: 50.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            Icon(
              FontAwesomeIcons.comment,
              size: 15.0,
              color: Colors.grey,
            ),
            Icon(
              FontAwesomeIcons.retweet,
              size: 15.0,
              color: Colors.grey,
            ),
            Icon(
              FontAwesomeIcons.heart,
              size: 15.0,
              color: Colors.grey,
            ),
            Icon(
              FontAwesomeIcons.share,
              size: 15.0,
              color: Colors.grey,
            )
          ],
        ),
      );
```

## イカしたStack
[元コードリンク](https://github.com/nb312/flutter-ui-nice/blob/master/lib/page/navigation/navigation1/coordinator.dart)
![transition.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190157/ad050625-b225-68d9-7330-1b62d538b7a6.gif)

参考になったのは以下です

- `Stack`を使って複数のWidgetを同時に出す方法

```dart
  @override
  Widget build(BuildContext context) => Material(
		child: BackgroundCommon(
			child: Stack(
				children: <Widget>[
					Positioned(
						bottom: 100.0,
						right: 50.0,
						child: MenuButtons(
							onChatPressed: _onChatPressed,
							onFeedPressed: _onFeedPressed,
							onHomePressed: _onHomePressed,
							onProfilePressed: _onProfilePressed,
							onSettingsPressed: _onSettingsPressed,
						),
					),

					AnimatedBuilder(
							animation: _controller,
							builder: (context, widget) => Transform(
								alignment: Alignment.centerLeft,
								transform: Matrix4
										.translationValues(_animator.translateLeft.value, 0.0, 0.0)
										..scale(_animator.scaleDown.value),
								child: HomePage(() => _openMenu()),
							),
					),
				],
			),
		),
	);

	Future _openMenu() async {
		try {
			await _controller.forward().orCancel;
		} on TickerCanceled {
			print("Animation Failed");
		}
	}
```

```dart
class HomePage extends StatelessWidget {
	HomePage(this.onMenuPressed);
	final VoidCallback onMenuPressed;

	@override
  Widget build(BuildContext context) {
    return Container(
			decoration: BoxDecoration(
				borderRadius: BorderRadius.circular(10.0),
				gradient: LinearGradient(
					begin: Alignment.topLeft,
					end: Alignment.bottomRight,
					colors: [YELLOW, GREEN, BLUE],
				),
				boxShadow: <BoxShadow>[
					BoxShadow(
						color: Colors.black26,
						offset: Offset(2.0, 1.0),
						blurRadius: 10.0,
					)
				],
			),
			child: Center(
				child: RaisedButton(onPressed: onMenuPressed, child: Text("Open Menu"),),
			),
		);
  }
}
```

# さいごに
`flutter/sample`もそうですが、UIだけならばサンプルの実装のWidgetの組み合わせだけで綺麗な画面を作ることができそうです。


(2019-12-10) P.S. 必要ない供養ネタは別で投稿し直しました。