コンサルティング事業 2021.01.04

Flutter × UI #01 ~ agexコーポレートサイトをトレースしてみた (ヘッダー編) ~

これは、Flutterの楽しさを分かち合いたい1人のバックエンドエンジニアが、Flutterを広めるために行った、FlutterでのUI作成についての小さな物語です。

FlutterでUIを作成する楽しさを感じながら、ゆるくながく続けるつもりです。

今回のおはなし
今回は、agexコーポレートサイトの「ヘッダー」をFlutterでトレースした際のおはなしです。

分析してみる
トレース対象となる「ヘッダー」の主要素である「各メニューアイテム」(以降、「アイテム」と呼ぶことにします)の分析を行います。

上記のヘッダーを分析してみると、ざっくりと下記の要素の組み合わせでアイテムが作られていることが分かります。
・ マウスが各アイテム内に移動すると、アニメーション開始
・ 各アイテムの下部にラインが表示される
・ ラインのアニメーションは中央 → 両端に広がるような動き
・ アニメーションが進むに連れ、ラインの透明度が下がる(色が濃くなる)
・ アニメーション開始時のライン横幅は、文字列幅の半分ぐらい
・ アニメーション完了時のライン横幅は、文字列幅に一致
・ マウスが各アイテム外に移動すると、アニメーションが逆再生される

実装してみる
上記の分析内容を1つずつFlutterで実装していきます。理論上、この分析内容を1つずつ実装していくことで、ヘッダーのアイテムを再現することができるはずです。記念すべき第1回、上手く再現できるでしょうか。。。

マウスが各アイテム内に移動すると、アニメーション開始
これは、「MouseRegion」というWidgetで実現することができます。
このWidgetは、
・「onEnter」
・「onExit」

というコールバック関数をもっており(デフォルトでは何も登録されていない)、名前の通り、それぞれに対し、
・「onEnter」:マウスがWidget上に移動
・「onExit」:マウスがWidget外に移動

をトリガに呼び出される関数を登録することができます。
ですので、「マウスホバーでアニメーション開始」を実現するには
・「onEnter」:マウスがWidget上に移動
    → アニメーション開始!

となるようコールバック関数を登録すると良いということになります。

class HeaderMenuItem extends StatefulWidget {
  const HeaderMenuItem({Key key}) : super(key: key);

  @override
  _HeaderMenuItemState createState() => _HeaderMenuItemState();
}

class _HeaderMenuItemState extends State {
  // 初期透明度
  double _opacity = 0;
  // 初期ライン横幅
  double _lineRatio = 0.5;

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (details) => _onEnterHandler(details, context),
      child: AnimatedContainer(
        width: 100 * _lineRatio, // ラインの横幅。100は一時的に設定
        height: 2, // ラインの高さ(太さ)
        color: Colors.black.withOpacity(_opacity),
        curve: Curves.linear,
        duration: Duration(milliseconds: 200),
      ),
    );
  }

  // onEnterに登録するコールバック関数
  void _onEnterHandler(PointerEvent details, BuildContext context) {
    // ライン横幅を最大に
    _lineRatio = 1;
    // 透明度を最大に
    _opacity = 1;
    // 画面の更新
    setState(() {
    });
  }
}

これで、マウスホバーでライン幅が広くなるアニメーションが作成できました!
ついでに、アニメーションの初期値・終了値も設定してしまっています。

[ここまでの成果]
マウスが各アイテム内に移動すると、アニメーション開始
・ 各アイテムの下部にラインが表示される
・ ラインのアニメーションは中央 → 両端に広がるような動き
アニメーションが進むに連れ、ラインの透明度が下がる(色が濃くなる)
アニメーション開始時のライン横幅は、文字列幅の半分ぐらい
・ アニメーション完了時のライン横幅は、文字列幅に一致
・ マウスが各アイテム外に移動すると、アニメーションが逆再生される

おや、もう半分以上終わっている、、良い滑り出しです!

マウスが各アイテム外に移動すると、アニメーションが逆再生される。
もうお気づきかと思いますが、「MouseRegion」の「onExit」に「アニメーションを逆再生するための処理」を入れることで実現することができます。

@override
Widget build(BuildContext context) {
  return MouseRegion(
    onEnter: (details) => _onEnterHandler(details, context),
    onExit: (details) => _onExitHandler(details, context),
    child: AnimatedContainer(
      width: 100 * _lineRatio, // ラインの横幅。100は一時的に設定
      height: 2, // ラインの高さ(太さ)
      color: Colors.black.withOpacity(_opacity),
      curve: Curves.linear,
      duration: Duration(milliseconds: 200),
    ),
  );
}



// onExitに登録するコールバック関数
void _onExitHandler(PointerEvent details, BuildContext context) {
  // ライン横幅を初期値に
  _lineRatio = 0.5;
  // 透明度を初期値に
  _opacity = 0;
  // 画面の更新
  setState(() {
  });
}

[ここまでの成果]
マウスが各アイテム内に移動すると、アニメーション開始
・ 各アイテムの下部にラインが表示される
・ ラインのアニメーションは中央 → 両端に広がるような動き
アニメーションが進むに連れ、ラインの透明度が下がる(色が濃くなる)
アニメーション開始時のライン横幅は、文字列幅の半分ぐらい
・ アニメーション完了時のライン横幅は、文字列幅に一致
マウスが各アイテム外に移動すると、アニメーションが逆再生される

なんとなんと、あっという間に残り2項目となりました。

各アイテムの下部にラインが表示される。
今までの作業で、「マウスがアイテム内に移動するとアニメーションが開始し、アイテム外に移動するとアニメーションが逆再生される」ラインを作成できたので、そのラインを「各アイテムの下部」に表示するよう実装していきます。

これは、「Column」というWidgetで実現することができます。
このWidgetでは、複数の要素を縦に並べることができるので、
・ ヘッダーのメニュー名
・ 作成したアニメーション付きライン

を縦に並べ、ヘッダーの「各アイテムの下部にラインが表示される」機能を実現することができます。

class HeaderMenuItem extends StatefulWidget {
  final itemName; // 各アイテム名
  final link; // ヘッダーのリンク先URL
  final double fontSize = 16; // ヘッダーアイテム名のフォントサイズ

  const HeaderMenuItem({Key key, this.itemName, this.link}) : super(key: key);

  @override
  _HeaderMenuItemState createState() => _HeaderMenuItemState();
}

class _HeaderMenuItemState extends State {



  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (details) => _onEnterHandler(details, context),
      onExit: (details) => _onExitHandler(details, context),
      child: Column(
        children: [
          Expanded(
            child: Container(
              alignment: Alignment.center,
                child: Text(
                  widget.itemName,
                  style: GoogleFonts.robotoCondensed(
                  fontSize: widget.fontSize,
                  wordSpacing: 0,
                  letterSpacing: 5,
                ),
              ),
            ),
          ),
          AnimatedContainer(
            width: 100 * _lineRatio, // ラインの横幅。100は一時的に設定
            height: 2, // ラインの高さ(太さ)
            color: Colors.black.withOpacity(_opacity),
            curve: Curves.linear,
            duration: Duration(milliseconds: 200),
          ),
        ],
      ),
    );
  }



}

Columnの水平方向に対するアライメントを設定する「crossAxisAlignment」の初期値は中央(CrossAxisAlignment.center)なので、上記の実装で「各アイテムの下部にラインが表示される」だけでなく、「ラインのアニメーションは中央 → 両端に広がるような動き」も実現されています。

[ここまでの成果]
マウスが各アイテム内に移動すると、アニメーション開始
各アイテムの下部にラインが表示される
ラインのアニメーションは中央 → 両端に広がるような動き
アニメーションが進むに連れ、ラインの透明度が下がる(色が濃くなる)
アニメーション開始時のライン横幅は、文字列幅の半分ぐらい
・ アニメーション完了時のライン横幅は、文字列幅に一致
マウスが各アイテム外に移動すると、アニメーションが逆再生される

残すはあと1項目です!あと一息、一気に仕上げていきます!

アニメーション完了時のライン横幅は、文字列幅に一致
下記「100」で一時的に指定している値を、文字列幅に合わせる必要があります。

AnimatedContainer(
  width: 100 * _lineRatio, // ラインの横幅。100は一時的に設定
  height: 2, // ラインの高さ(太さ)
  color: Colors.black.withOpacity(_opacity),
  curve: Curves.linear,
  duration: Duration(milliseconds: 200),
),

そのために、「TextPainter」を利用して文字列の横幅を取得するメソッドを用意します。

double lineWidth(String text) {
  var textPainter = TextPainter(
    text: TextSpan(
      text: text,
      style: GoogleFonts.robotoCondensed(
        fontSize: widget.fontSize,
        wordSpacing: 0,
        letterSpacing: 5,
      ),
    ),
    textDirection: TextDirection.ltr,
  )..layout();

  return textPainter.width;
}

上記のメソッドを利用し、AnimatedContainerのwidthを下記の通り変更します。

AnimatedContainer(
  width: lineWidth(widget.itemName) * _lineRatio, // ラインの横幅
  height: 2, // ラインの高さ(太さ)
  color: Colors.black.withOpacity(_opacity),
  curve: Curves.linear,
  duration: Duration(milliseconds: 200),
),

[ここまでの成果]
マウスが各アイテム内に移動すると、アニメーション開始
各アイテムの下部にラインが表示される
ラインのアニメーションは中央 → 両端に広がるような動き
アニメーションが進むに連れ、ラインの透明度が下がる(色が濃くなる)
アニメーション開始時のライン横幅は、文字列幅の半分ぐらい
アニメーション完了時のライン横幅は、文字列幅に一致
マウスが各アイテム外に移動すると、アニメーションが逆再生される

これで、分析した内容をすべて実装することができました!

あとは、作成したアイテムを横方向に配列してあげることで、「ヘッダー」を再現することができます。

import ‘package:flutter/material.dart’;
import ‘header_menu_item.dart’;

class Header extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints.expand(
        // ヘッダーサイズ
        width: 1240,
        height: 80,
      ),
      child: Row(
        children: [
          // ロゴも入れてあげる
          Padding(
            padding: const EdgeInsets.only(right: 30),
            child: Image.asset(
              ‘lib/assets/images/logo.png’,
            ),
          ),
          const HeaderMenuItem(
            itemName: ‘BUSINESS’,
          ),
          const HeaderMenuItem(
            itemName: ‘RECRUIT’,
          ),
          const HeaderMenuItem(
            itemName: ‘BLOG’,
          ),
          const HeaderMenuItem(
            itemName: ‘COMPANY’,
          ),
          const HeaderMenuItem(
            itemName: ‘CONTACT’,
          ),
          const Spacer(),
        ],
      ),
    );
  }
}

これで出来上がったのが、、、こちら!

↓↓↓↓↓↓↓↓↓↓↓↓

(SNSアイコンの実装漏れはご愛嬌)

どうでしょうか?なかなかの再現度ではないでしょうか??
とりあえず第1回、ある程度形になってホッとしました。。

次回からは、~ agexコーポレートサイトをトレースしてみた(スライダー編) ~ を予定しています。
そうです、あのトップページ上部にある、一番実装が難しそうな、あれです。。。

今日のまとめ
Flutterは楽しい。バックエンドエンジニアでも楽しい。

参考
https://stackoverflow.com/questions/52659759/how-can-i-get-the-size-of-the-text-widget-in-flutter

お知らせ

agexアドベントカレンダーやってます!(1月ですが)
他にもいろいろな記事が投稿されていますので、ぜひご覧ください!

agexアドベントカレンダー 2021 January(コンサルティング事業部)

この記事をシェアする
事業について詳しく知る

コンサルティング事業について詳しく知りたい方はこちら

RECOMMEND

おすすめ記事