MDC-104 Flutter: マテリアルの高度なコンポーネント(Flutter)

logo_components_color_2x_web_96dp.png

マテリアル コンポーネント(MDC)は、デベロッパーがマテリアル デザインを実装する際に役立ちます。Google のエンジニアと UX デザイナーのチームが作成した MDC には、美しく機能的な UI コンポーネントが多数含まれており、Android、iOS、ウェブ、Flutter.material.io/develop で利用できます。

Codelab MDC-103 では、アプリのスタイルを設定するマテリアル コンポーネント(MDC)のカラー、高度、タイポグラフィ、シェイプをカスタマイズしました。

マテリアル デザイン システムのコンポーネントは、事前定義されたタスクのセットを実行します。また、ボタンなど、特定の特性を持っています。ボタンは単にユーザーがアクションを実行するための手段であるだけでなく、形状、サイズ、色を備えた視覚的表現でもあり、UI が操作可能であることと、タップまたはクリックすると何かが起こることをユーザーに伝えます。

マテリアル デザイン ガイドラインは、デザイナーの観点から見たコンポーネントの記述です。複数のプラットフォームで利用できるさまざまな基本機能と、各コンポーネントを構成する微細な要素について説明しています。たとえば、背景(バックドロップ)には、バックレイヤとそのコンテンツ、フロントレイヤとそのコンテンツ、モーション ルール、表示オプションが含まれます。それぞれのコンポーネントは、アプリのニーズ、ユースケース、コンテンツに合わせてカスタマイズできます。そうしたコンポーネントの大半は、プラットフォームの SDK に含まれる従来のビュー、コントロール、関数に対応しています。

マテリアル デザイン ガイドラインには多くのコンポーネントが記載されていますが、一部のコンポーネントは再利用可能なコードでの利用に適しておらず、そのため MDC に含まれていません。従来のコードのみを使用して、そのようなエクスペリエンスを独自に作成し、アプリ用にカスタマイズしたスタイルを実現できます。

作成するアプリの概要

この Codelab では、Shrine アプリの UI を、2 つのレベルからなる「背景」というプレゼンテーションに変更します。背景には、選択可能なカテゴリを一覧表示するメニューが含まれています。カテゴリは、非対称グリッドに表示される商品をフィルタするために使用されます。この Codelab では、以下の Flutter コンポーネントを使用します。

  • シェイプ
  • モーション
  • Flutter ウィジェット(前の Codelab で使用したもの)

Android

iOS

この Codelab の MDC-Flutter コンポーネント

  • シェイプ

Flutter 開発の経験についてお答えください。

初心者 中級者 上級者

準備

Flutter でモバイルアプリを開発するには、以下の作業が必要です。

  1. Flutter SDK をダウンロードしてインストールします。
  2. Flutter SDK で PATH を更新します。
  3. Flutter プラグインと Dart プラグインを備えた Android Studio をインストールするか、お好みのエディタをインストールします。
  4. Android Emulator または iOS シミュレータ(Xcode をインストールした Mac が必要)をインストールするか、物理デバイスを使用します。

Flutter のインストールについて詳しくは、スタートガイド: インストールをご覧ください。エディタのセットアップについては、スタートガイド: エディタのセットアップをご覧ください。Android Emulator をインストールする場合は、最新のシステム イメージを搭載した Pixel 3 スマートフォンなどのデフォルトのオプションをご利用になれます。VM アクセラレーションを有効にすることをおすすめしますが、必須ではありません。上記の 4 つの手順を完了したら、Codelab を再開できます。この Codelab を完了するには、1 つのプラットフォーム(Android または iOS)に Flutter をインストールするだけで十分です。

Flutter SDK が正しい状態にあることを確認する

この Codelab を先に進める前に、SDK が正しい状態にあることを確認します。Flutter SDK を以前にインストールしている場合は、flutter upgrade を使用して SDK が最新の状態であることを確認します。

 flutter upgrade

flutter upgrade を実行すると flutter doctor. が自動的に実行されます。これが最新の Flutter インストールで、アップグレードが不要だった場合は、手動で flutter doctor を実行します。セットアップを完了するために依存関係をインストールする必要があるかどうかが通知されます。自分に関係がないチェックマークは無視してかまいません(たとえば、iOS 向けの開発を意図していない場合の Xcode など)。

 flutter doctor

よくある質問

MDC-103 から続行する場合

MDC-103 が完了済みであれば、コードはこの Codelab を進められる状態になっています。「背景メニューを追加する」ステップに進んでください。

ゼロから始める

スターター アプリをダウンロード

スターター アプリは material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series ディレクトリにあります。

...または、GitHub からクローンを作成する

GitHub からこの Codelab のクローンを作成するには、次のコマンドを実行します。

git clone https://github.com/material-components/material-components-flutter-codelabs.git
cd material-components-flutter-codelabs/mdc_100_series
git checkout 104-starter_and_103-complete

プロジェクトをセットアップする

これ以降の手順は、Android Studio(IntelliJ)の使用を前提としています。

プロジェクトを開く

1. Android Studio を開きます。

2. ウェルカム画面が表示されたら、[Open an existing Android Studio project] をクリックします。

3. material-components-flutter-codelabs/mdc_100_series ディレクトリに移動して [Open] をクリックします。プロジェクトが開きます。プロジェクトの作成をいったん終了するまで、Dart Analysis に表示されるエラーは無視してかまいません。

4. プロンプトが表示された場合は、次の操作を行います。

  • プラットフォームとプラグインのアップデートをすべてインストールするか、FlutterRunConfigurationType をインストールします。
  • Dart または Flutter SDK が設定されていない場合は、Flutter プラグインの Flutter SDK パスを設定します。
  • Android フレームワークを設定します。
  • [Get dependencies] または [Run ‘flutter packages get'] をクリックします。

Android Studio を再起動します。

スターター アプリを実行する

以下の手順は、Android Emulator または Android デバイスでのテストを前提としていますが、Xcode がインストールされている場合は、iOS シミュレータまたは iOS デバイスでもテストできます。

1. デバイスまたはエミュレータを選択します。Android Emulator がまだ起動していない場合は、[Tools] -> [Android] -> [AVD Manager] を選択し、仮想デバイスを作成してエミュレータを起動します。AVD がすでに存在する場合は、次のステップに示すように、Android Studio のデバイス セレクタから直接エミュレータを起動できます(iOS シミュレータの場合は、まだ起動していなければ、開発マシンで [Flutter Device Selection] -> [Open iOS Simulator] を選択してシミュレータを起動します)。

2. Flutter アプリを起動します。

  • エディタ画面の上部にある [Flutter Device Selection] プルダウン メニューでデバイスを探して選択します(たとえば、<version> 向けにビルドされた Android SDK または iPhone SE など)。
  • 再生アイコン()をクリックします。

正常に完了すると、シミュレータまたはエミュレータに、前の Codelab で作成した Shrine のログインページが表示されます。

Android

iOS

背景は、他のすべてのコンテンツとコンポーネントの背後に表示されます。背景は、バックレイヤ(アクションとフィルタを表示)とフロントレイヤ(コンテンツを表示)の 2 つのレイヤで構成されます。背景を使用して、ナビゲーションやコンテンツ フィルタなどのインタラクティブな情報とアクションを表示できます。

ホーム アプリバーを削除する

HomePage ウィジェットは、フロントレイヤのコンテンツになります。現在、このウィジェットにはアプリバーがあります。アプリバーをバックレイヤに移動し、HomePage に AsymmetricView のみが含まれるようにします。

home.dart で、build() 関数を変更して AsymmetricView のみを返すようにします。

// TODO: Return an AsymmetricView (104)
return  AsymmetricView(products: ProductsRepository.loadProducts(Category.all));

Backdrop ウィジェットを追加する

frontLayerbackLayer を含む Backdrop という名前のウィジェットを作成します。

backLayer には、カテゴリを選択してリストをフィルタできるメニュー(currentCategory)が含まれています。メニュー選択機能を維持するため、Backdrop をステートフル ウィジェットにします。

backdrop.dart という名前の新しいファイルを /lib に追加します。

import 'package:flutter/material.dart';
import 'package:meta/meta.dart';

import 'model/product.dart';

// TODO: Add velocity constant (104)

class Backdrop extends StatefulWidget {
  final Category currentCategory;
  final Widget frontLayer;
  final Widget backLayer;
  final Widget frontTitle;
  final Widget backTitle;

  const Backdrop({
    @required this.currentCategory,
    @required this.frontLayer,
    @required this.backLayer,
    @required this.frontTitle,
    @required this.backTitle,
  })  : assert(currentCategory != null),
        assert(frontLayer != null),
        assert(backLayer != null),
        assert(frontTitle != null),
        assert(backTitle != null);

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

// TODO: Add _FrontLayer class (104)
// TODO: Add _BackdropTitle class (104)
// TODO: Add _BackdropState class (104)

プロパティに @required マークを付けるために、meta パッケージをインポートします。これは、デフォルト値がなく null にできないゆえに保持すする必要があるプロパティがコンストラクタに含まれている場合のおすすめの方法です。また、これらのフィールドに渡された値が実際には null でないことを確認する assert がコンストラクタの後に存在することに注目してください。

Backdrop クラスの定義の下に、_BackdropState クラスを追加します。

// TODO: Add _BackdropState class (104)
class _BackdropState extends State<Backdrop>
    with SingleTickerProviderStateMixin {
  final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');

  // TODO: Add AnimationController widget (104)

  // TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
  Widget _buildStack() {
    return Stack(
    key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        widget.backLayer,
        widget.frontLayer,
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    var appBar = AppBar(
      brightness: Brightness.light,
      elevation: 0.0,
      titleSpacing: 0.0,
      // TODO: Replace leading menu icon with IconButton (104)
      // TODO: Remove leading property (104)
      // TODO: Create title with _BackdropTitle parameter (104)
      leading: Icon(Icons.menu),
      title: Text('SHRINE'),
      actions: <Widget>[
        // TODO: Add shortcut to login screen from trailing icons (104)
        IconButton(
          icon: Icon(
            Icons.search,
            semanticLabel: 'search',
          ),
          onPressed: () {
          // TODO: Add open login (104)
          },
        ),
        IconButton(
          icon: Icon(
            Icons.tune,
            semanticLabel: 'filter',
          ),
          onPressed: () {
          // TODO: Add open login (104)
          },
        ),
      ],
    );
    return Scaffold(
      appBar: appBar,
      // TODO: Return a LayoutBuilder widget (104)
      body: _buildStack(),
    );
  }
}

build() 関数は、以前の HomePage と同じく、アプリバーを含む Scaffold を返します。ただし、Scaffold の本文はスタックです。スタックの子はオーバーラップできます。それぞれの子のサイズと場所は、スタックの親を基準にして指定されます。

次に、ShrineApp に Backdrop インスタンスを追加します。

app.dart で、backdrop.dartmodel/product.dart をインポートします。

import 'backdrop.dart'; // New code
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart'; // New code
import 'supplemental/cut_corners_border.dart';

app.dart, で、ShrineApp の build() 関数を変更します。home: を Backdrop に変更し、その frontLayer として HomePage を指定します。

      // TODO: Change home: to a Backdrop with a HomePage frontLayer (104)
      home: Backdrop(
        // TODO: Make currentCategory field take _currentCategory (104)
        currentCategory: Category.all,
        // TODO: Pass _currentCategory for frontLayer (104)
        frontLayer: HomePage(),
        // TODO: Change backLayer field value to CategoryMenuPage (104)
        backLayer: Container(color: kShrinePink100),
        frontTitle: Text('SHRINE'),
        backTitle: Text('MENU'),
      ),

再生ボタンをクリックすると、ホームページが表示され、アプリバーも表示されるはずです。

Android

iOS

backLayer により、frontLayer のホームページの背後にある新しいレイヤにピンクの領域が表示されます。

Flutter Inspector を使用すると、実際には HomePage の背後に Stack の Container があることを確認できます。画面では次のように表示されます。

ad988a22875b5e82.png

以上で、2 つのレイヤのデザインとコンテンツを調整できるようになりました。

このステップでは、左上隅をカットしたスタイルをフロントレイヤに設定します。

マテリアル デザインでは、このようなカスタマイズが可能なコンポーネントをシェイプと呼びます。マテリアルのサーフェスは任意のシェイプを持つことができます。シェイプによってサーフェスに強調とスタイルを加え、ブランドを表現できます。通常の四角形のシェイプは、カーブまたは角度の付いた隅と縁、および任意の数の辺を使用してカスタマイズできます。また、対称または非対称にできます。

フロントレイヤにシェイプを追加する

角度が付いた Shrine のロゴは、Shrine アプリのシェイプ ストーリーに影響を与えました。シェイプ ストーリーとは、アプリ全体に適用されるシェイプの一般的な使用方法です。たとえば、ロゴのシェイプは、シェイプが適用されているログインページの各要素に反映されます。このステップでは、左上隅を角度を付けてカットしたスタイルをフロントレイヤに設定します。

backdrop.dart で、新しいクラス _FrontLayer を追加します。

// TODO: Add _FrontLayer class (104)
class _FrontLayer extends StatelessWidget {
  // TODO: Add on-tap callback (104)
  const _FrontLayer({
    Key key,
    this.child,
  }) : super(key: key);

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Material(
      elevation: 16.0,
      shape: BeveledRectangleBorder(
        borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          // TODO: Add a GestureDetector (104)
          Expanded(
            child: child,
          ),
        ],
      ),
    );
  }
}

次に、_BackdropState の _buildStack() 関数内で、フロントレイヤを _FrontLayer でラップします。

  Widget _buildStack() {
    // TODO: Create a RelativeRectTween Animation (104)

    return Stack(
    key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        widget.backLayer,
        // TODO: Add a PositionedTransition (104)
        // TODO: Wrap front layer in _FrontLayer (104)
          _FrontLayer(child: widget.frontLayer),
      ],
    );
  }

再読み込みを行います。

Android

iOS

これで、Shrine のプライマリ サーフェスにカスタムのシェイプが設定されました。サーフェスの高度により、白いフロントレイヤのすぐ後ろに何かがあることがユーザーにわかります。ユーザーが背景のバックレイヤを見ることができるように、モーションを追加しましょう。

モーションは、アプリに命を吹き込む手段です。モーションは、大きくて目立つものでも、小さくて微妙なものでも、その中間のどんなものでもかまいません。ただし、使用するモーションが状況に適したものになるように留意する必要があります。反復される定期的なアクションに適用するモーションは、ユーザーの注意を逸らさないように、また、動作に時間がかかりすぎないように、小さくて微妙なものにする必要があります。一方、ユーザーが初めてアプリを開いたときのように、ユーザーの目を惹きつけることが適切な場合もあります。ある種のアニメーションは、アプリの使い方をユーザーに教えるのに役立ちます。

メニューボタンに開示モーションを追加する

backdrop.dart の先頭で、クラスまたは関数のスコープの外部に、アニメーションに要求される速度を表す定数を追加します。

// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;

_BackdropState に AnimationController ウィジェットを追加し、initState() 関数でインスタンス化して、状態の dispose() 関数で廃棄します。

  // TODO: Add AnimationController widget (104)
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 300),
      value: 1.0,
      vsync: this,
    );
  }

  // TODO: Add override for didUpdateWidget (104)

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  // TODO: Add functions to get and change front layer visibility (104)

AnimationController は、Animation を調整するとともに、アニメーションの再生、巻き戻し、停止を行う API を提供します。ここで、アニメーションを動かす関数が必要です。

フロントレイヤの表示設定の決定と変更を行う関数を追加します。

  // TODO: Add functions to get and change front layer visibility (104)
  bool get _frontLayerVisible {
    final AnimationStatus status = _controller.status;
    return status == AnimationStatus.completed ||
        status == AnimationStatus.forward;
  }

  void _toggleBackdropLayerVisibility() {
    _controller.fling(
        velocity: _frontLayerVisible ? -_kFlingVelocity : _kFlingVelocity);
  }

ExcludeSemantics ウィジェットで backLayer をラップします。このウィジェットは、バックレイヤが表示されないときに、セマンティクス ツリーから backLayer のメニュー項目を除外します。

    return Stack(
      key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        ExcludeSemantics(
          child: widget.backLayer,
          excluding: _frontLayerVisible,
        ),
      ...

_buildStack() 関数を変更して、BuildContext と BoxConstraints を受け取るようにします。また、RelativeRectTween Animation を受け取る PositionedTransition を追加します。

  // TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
  Widget _buildStack(BuildContext context, BoxConstraints constraints) {
    const double layerTitleHeight = 48.0;
    final Size layerSize = constraints.biggest;
    final double layerTop = layerSize.height - layerTitleHeight;

    // TODO: Create a RelativeRectTween Animation (104)
    Animation<RelativeRect> layerAnimation = RelativeRectTween(
      begin: RelativeRect.fromLTRB(
          0.0, layerTop, 0.0, layerTop - layerSize.height),
      end: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
    ).animate(_controller.view);

    return Stack(
      key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        ExcludeSemantics(
          child: widget.backLayer,
          excluding: _frontLayerVisible,
        ),
        // TODO: Add a PositionedTransition (104)
        PositionedTransition(
          rect: layerAnimation,
          child: _FrontLayer(
            // TODO: Implement onTap property on _BackdropState (104)
            child: widget.frontLayer,
          ),
        ),
      ],
    );
  }

最後に、Scaffold の本文の _buildStack 関数を呼び出す代わりに、_buildStack をビルダーとして使用する LayoutBuilder ウィジェットを返します。

    return Scaffold(
      appBar: appBar,
      // TODO: Return a LayoutBuilder widget (104)
      body: LayoutBuilder(builder: _buildStack),
    );

LayoutBuilder を使用してフロントレイヤ / バックレイヤのスタックの作成をレイアウト時まで遅らせることにより、背景全体の実際的な高さを組み込めるようになりました。LayoutBuilder は、ビルダー コールバックがサイズ制約を提供する特殊なウィジェットです。

build() 関数で、アプリバーの先頭のメニュー アイコンを IconButton に変更し、それを使用して、ボタンがタップされたときにフロントレイヤの表示設定を切り替えます。

      // TODO: Replace leading menu icon with IconButton (104)
      leading: IconButton(
        icon: Icon(Icons.menu),
        onPressed: _toggleBackdropLayerVisibility,
      ),

再読み込みを行い、シミュレータのメニューボタンをタップします。

Android

iOS

フロントレイヤにアニメーションが設定され、下にスライドできるようになりました。しかし、スライドして下を見ると、赤いエラーとオーバーフロー エラーが表示されます。これは、AsymmetricView がスクイーズされてこのアニメーションの分だけ小さくなり、それによって Column のスペースが狭くなるためです。最終的には、Column は与えられたスペースに自身をレイアウトできなくなり、その結果エラーが発生します。Column を ListView に置き換えると、アニメーションの際に列のサイズが維持されます。

商品の列を ListView でラップする

supplemental/product_columns.dart で、OneProductCardColumn 内の Column を ListView に置き換えます。

class OneProductCardColumn extends StatelessWidget {
  OneProductCardColumn({this.product});

  final Product product;

  @override
  Widget build(BuildContext context) {
    // TODO: Replace Column with a ListView (104)
    return ListView(
      physics: const ClampingScrollPhysics(),
      reverse: true,
      children: <Widget>[
        SizedBox(
          height: 40.0,
        ),
        ProductCard(
          product: product,
        ),
      ],
    );
  }
}

Column には MainAxisAlignment.end が含まれています。最下部からレイアウトを開始するには、reverse: true のマークを付けます。変更を補正するため、子の順序が逆になります。

再読み込みを行い、メニューボタンをタップします。

Android

iOS

OneProductCardColumn のグレーのオーバーフロー警告が表示されなくなりました。他のエラーも修正しましょう。

supplemental/product_columns.dart で、imageAspectRatio の計算方法を変更し、TwoProductCardColumn 内の Column を ListView に置き換えます。

      // TODO: Change imageAspectRatio calculation (104)
      double imageAspectRatio = heightOfImages >= 0.0
          ? constraints.biggest.width / heightOfImages
          : 49.0 / 33.0;
      // TODO: Replace Column with a ListView (104)
      return ListView(
        physics: const ClampingScrollPhysics(),
        children: <Widget>[
          Padding(
            padding: EdgeInsetsDirectional.only(start: 28.0),
            child: top != null
                ? ProductCard(
                    imageAspectRatio: imageAspectRatio,
                    product: top,
                  )
                : SizedBox(
                    height: heightOfCards,
                  ),
          ),
          SizedBox(height: spacerHeight),
          Padding(
            padding: EdgeInsetsDirectional.only(end: 28.0),
            child: ProductCard(
              imageAspectRatio: imageAspectRatio,
              product: bottom,
            ),
          ),
        ],
      );

imageAspectRatio もより安全になりました。

再読み込みを行います。次に、メニューボタンをタップします。

Android

iOS

オーバーフローがなくなりました。

メニューはタップ可能なテキスト項目のリストで、テキスト項目がタップされるとリスナーに通知されます。このステップでは、カテゴリをフィルタするメニューを追加します。

メニューを追加する

メニューをフロントレイヤに追加し、インタラクティブなボタンをバックレイヤに追加します。

lib/category_menu_page.dart という名前の新しいファイルを作成します。

import 'package:flutter/material.dart';
import 'package:meta/meta.dart';

import 'colors.dart';
import 'model/product.dart';

class CategoryMenuPage extends StatelessWidget {
  final Category currentCategory;
  final ValueChanged<Category> onCategoryTap;
  final List<Category> _categories = Category.values;

  const CategoryMenuPage({
    Key key,
    @required this.currentCategory,
    @required this.onCategoryTap,
  })  : assert(currentCategory != null),
        assert(onCategoryTap != null);

  Widget _buildCategory(Category category, BuildContext context) {
    final categoryString =
        category.toString().replaceAll('Category.', '').toUpperCase();
    final ThemeData theme = Theme.of(context);

    return GestureDetector(
      onTap: () => onCategoryTap(category),
      child: category == currentCategory
        ? Column(
          children: <Widget>[
            SizedBox(height: 16.0),
            Text(
              categoryString,
              style: theme.textTheme.bodyText1,
              textAlign: TextAlign.center,
            ),
            SizedBox(height: 14.0),
            Container(
              width: 70.0,
              height: 2.0,
              color: kShrinePink400,
            ),
          ],
        )
      : Padding(
        padding: EdgeInsets.symmetric(vertical: 16.0),
        child: Text(
          categoryString,
          style: theme.textTheme.bodyText1.copyWith(
              color: kShrineBrown900.withAlpha(153)
            ),
          textAlign: TextAlign.center,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        padding: EdgeInsets.only(top: 40.0),
        color: kShrinePink100,
        child: ListView(
          children: _categories
            .map((Category c) => _buildCategory(c, context))
            .toList()),
      ),
    );
  }
}

これは、子がカテゴリ名である Column をラップする GestureDetector です。選択されたカテゴリを示すために、アンダースコアが使用されています。

app.dart で、ShrineApp ウィジェットをステートレスからステートフルに変換します。

  1. ShrineApp. をハイライト表示します。
  2. alt(option)+Enter キーを押します。
  3. [Convert to StatefulWidget] を選択します。
  4. ShrineAppState クラスをプライベート クラス(_ShrineAppState)に変更します。IDE のメインメニューからこれを行うには、[Refactor] > [Rename] を選択します。または、コード内からこれを行うには、クラス名 ShrineAppState をハイライト表示し、右クリックして [Refactor] > [Rename] を選択します。プライベート クラスにするため、「_ShrineAppState」と入力します。

app.dart で、選択された Category の _ShrineAppState に対する変数と、タップされたときのコールバックを追加します。

// TODO: Convert ShrineApp to stateful widget (104)
class _ShrineAppState extends State<ShrineApp> {
  Category _currentCategory = Category.all;

  void _onCategoryTap(Category category) {
    setState(() {
      _currentCategory = category;
    });
  }

次に、バックレイヤを CategoryMenuPage に変更します。

app.dart で、CategoryMenuPage をインポートします。

import 'backdrop.dart';
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'category_menu_page.dart';
import 'model/product.dart';
import 'supplemental/cut_corners_border.dart';

build() 関数で、backlayer フィールドを CategoryMenuPage に変更し、currentCategory フィールドをインスタンス変数を受け取るように変更します。

      home: Backdrop(
        // TODO: Make currentCategory field take _currentCategory (104)
        currentCategory: _currentCategory,
        // TODO: Pass _currentCategory for frontLayer (104)
        frontLayer: HomePage(),
        // TODO: Change backLayer field value to CategoryMenuPage (104)
        backLayer: CategoryMenuPage(
          currentCategory: _currentCategory,
          onCategoryTap: _onCategoryTap,
        ),
        frontTitle: Text('SHRINE'),
        backTitle: Text('MENU'),
      ),

再読み込みを行い、メニューボタンをタップします。

Android

iOS

メニュー オプションをタップしても、今はまだ何も起こりません。この問題を修正しましょう。

home.dart で、Category の変数を追加して AsymmetricView に渡します。

import 'package:flutter/material.dart';

import 'model/products_repository.dart';
import 'model/product.dart';
import 'supplemental/asymmetric_view.dart';

class HomePage extends StatelessWidget {
  // TODO: Add a variable for Category (104)
  final Category category;

  const HomePage({this.category: Category.all});

  @override
  Widget build(BuildContext context) {
    // TODO: Pass Category variable to AsymmetricView (104)
    return AsymmetricView(products: ProductsRepository.loadProducts(category));
  }
}

app.dart で、frontLayer_currentCategory を渡します。

        // TODO: Pass _currentCategory for frontLayer (104)
        frontLayer: HomePage(category: _currentCategory),

再読み込みを行います。シミュレータのメニューボタンをタップして、カテゴリを選択します。

Android

iOS

メニュー アイコンをタップすると、商品が表示されます。商品がフィルタされています。

メニューの選択後にフロントレイヤを閉じる

backdrop.dart で、_BackdropState に didUpdateWidget() 関数のオーバーライドを追加します。

  // TODO: Add override for didUpdateWidget() (104)
  @override
  void didUpdateWidget(Backdrop old) {
    super.didUpdateWidget(old);

    if (widget.currentCategory != old.currentCategory) {
      _toggleBackdropLayerVisibility();
    } else if (!_frontLayerVisible) {
      _controller.fling(velocity: _kFlingVelocity);
    }
  }

ホットリロードを行い、メニュー アイコンをタップしてカテゴリを選択します。メニューが自動的に閉じ、選択した商品カテゴリが表示されます。次に、フロントレイヤにもこの機能を追加します。

フロントレイヤを切り替える

backdrop.dart で、背景レイヤに on-tap コールバックを追加します。

class _FrontLayer extends StatelessWidget {
  // TODO: Add on-tap callback (104)
  const _FrontLayer({
    Key key,
    this.onTap, // New code
    this.child,
  }) : super(key: key);

  final VoidCallback onTap; // New code
  final Widget child;

次に、_FrontLayer の子である Column の子に GestureDetector を追加します。

      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          // TODO: Add a GestureDetector (104)
          GestureDetector(
            behavior: HitTestBehavior.opaque,
            onTap: onTap,
            child: Container(
              height: 40.0,
              alignment: AlignmentDirectional.centerStart,
            ),
          ),
          Expanded(
            child: child,
          ),
        ],
      ),

さらに、_buildStack() 関数内の _BackdropState に新しい onTap プロパティを実装します。

          PositionedTransition(
            rect: layerAnimation,
            child: _FrontLayer(
              // TODO: Implement onTap property on _BackdropState (104)
              onTap: _toggleBackdropLayerVisibility,
              child: widget.frontLayer,
            ),
          ),

再読み込みを行い、フロントレイヤの上部をタップします。フロントレイヤの上部をタップするたびに、レイヤが開いては閉じます。

ブランドの図像も親しみやすいアイコンに拡張されています。開示アイコンをカスタマイズしてタイトルと結合し、ユニークなブランドをデザインしましょう。

メニューボタンのアイコンを変更する

Android

iOS

backdrop.dart で、新しいクラス _BackdropTitle を作成します。

// TODO: Add _BackdropTitle class (104)
class _BackdropTitle extends AnimatedWidget {
  final Function onPress;
  final Widget frontTitle;
  final Widget backTitle;

  const _BackdropTitle({
    Key key,
    Listenable listenable,
    this.onPress,
    @required this.frontTitle,
    @required this.backTitle,
  })  : assert(frontTitle != null),
        assert(backTitle != null),
        super(key: key, listenable: listenable);

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = this.listenable;

    return DefaultTextStyle(
      style: Theme.of(context).primaryTextTheme.headline6,
      softWrap: false,
      overflow: TextOverflow.ellipsis,
      child: Row(children: <Widget>[
        // branded icon
        SizedBox(
          width: 72.0,
          child: IconButton(
            padding: EdgeInsets.only(right: 8.0),
            onPressed: this.onPress,
            icon: Stack(children: <Widget>[
              Opacity(
                opacity: animation.value,
                child: ImageIcon(AssetImage('assets/slanted_menu.png')),
              ),
              FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset.zero,
                  end: Offset(1.0, 0.0),
                ).evaluate(animation),
                child: ImageIcon(AssetImage('assets/diamond.png')),
              )]),
          ),
        ),
        // Here, we do a custom cross fade between backTitle and frontTitle.
        // This makes a smooth animation between the two texts.
        Stack(
          children: <Widget>[
            Opacity(
              opacity: CurvedAnimation(
                parent: ReverseAnimation(animation),
                curve: Interval(0.5, 1.0),
              ).value,
              child: FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset.zero,
                  end: Offset(0.5, 0.0),
                ).evaluate(animation),
                child: backTitle,
              ),
            ),
            Opacity(
              opacity: CurvedAnimation(
                parent: animation,
                curve: Interval(0.5, 1.0),
              ).value,
              child: FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset(-0.25, 0.0),
                  end: Offset.zero,
                ).evaluate(animation),
                child: frontTitle,
              ),
            ),
          ],
        )
      ]),
    );
  }
}

_BackdropTitle は、AppBar ウィジェットの title パラメータ用の書式なし Text ウィジェットに代わるカスタム ウィジェットです。このウィジェットには、アニメーション メニュー アイコンと、フロント タイトルとバックタイトル間のアニメーション遷移が含まれています。アニメーション メニュー アイコンは、新しいアセットを使用します。新しい slanted_menu.png への参照を pubspec.yaml に追加する必要があります。

assets:
    - assets/diamond.png
    # TODO: Add slanted menu asset (104)
    - assets/slanted_menu.png
    - packages/shrine_images/0-0.jpg

AppBar ビルダー内の leading プロパティを削除します。これは、カスタム ブランド アイコンを元の leading ウィジェットの位置にレンダリングするためです。ブランド アイコンのアニメーション listenableonPress ハンドラが _BackdropTitle に渡されます。また、背景のタイトル内にレンダリングできるように、frontTitlebackTitle も渡されます。AppBartitle パラメータは次のようになります。

// TODO: Create title with _BackdropTitle parameter (104)
title: _BackdropTitle(
  listenable: _controller.view,
  onPress: _toggleBackdropLayerVisibility,
  frontTitle: widget.frontTitle,
  backTitle: widget.backTitle,
),

ブランド アイコンは _BackdropTitle. で作成されます。これには、アニメーション アイコンの Stack(斜度が付いたメニューと、クリックできるように IconButton でラップされたダイヤモンド)が含まれています。さらに、IconButton は、水平方向のアイコン モーションのスペースを空けるために、SizedBox でラップされています。

Flutter の「すべてがウィジェット」方式のアーキテクチャでは、まったく新しいカスタム AppBar ウィジェットを作成しなくても、デフォルトの AppBar のレイアウトを変更できます。title パラメータは元々は Text ウィジェットでしたが、より複雑な _BackdropTitle に置き換えることができます。また、_BackdropTitle にはカスタム アイコンが含まれているため、leading プロパティの代わりに使用できます。つまり、このプロパティを削除できます。このシンプルなウィジェットの置換は、他のパラメータ(アクション アイコンなど)の変更なしで実現され、他のパラメータは独立して引き続き機能します。

ログイン画面に戻るショートカットを追加する

backdrop.dart, で、アプリバーの末尾の 2 つのアイコンからログイン画面に戻るショートカットを追加します。アイコンのセマンティック ラベルは、新しい目的を示すものに変更します。

        // TODO: Add shortcut to login screen from trailing icons (104)
        IconButton(
          icon: Icon(
            Icons.search,
            semanticLabel: 'login', // New code
          ),
          onPressed: () {
            // TODO: Add open login (104)
            Navigator.push(
              context,
              MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
            );
          },
        ),
        IconButton(
          icon: Icon(
            Icons.tune,
            semanticLabel: 'login', // New code
          ),
          onPressed: () {
            // TODO: Add open login (104)
            Navigator.push(
              context,
              MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
            );
          },
        ),

再読み込みを試みると、エラーが表示されます。login.dart をインポートして、エラーを修正します。

import 'login.dart';

アプリの再読み込みを行います。検索ボタンまたは調整ボタンをタップすると、ログイン画面に戻ります。

4 つの Codelab を実践することにより、マテリアル コンポーネントを使用して、ブランドの個性とスタイルを表現する独自の洗練されたユーザー エクスペリエンスを構築する方法を学びました。

次のステップ

この Codelab MDC-104 で一連の Codelab は完了です。Flutter ウィジェット カタログにアクセスすると、もっと多くの MDC-Flutter のコンポーネントを探究できます。

難度の高い目標に挑戦する場合は、ブランド アイコンを AnimatedIcon に置き換えてみてください。これは、背景が表示されているときに 2 つのアイコンの間でアニメーションが動作するアイコンです。

作業用バックエンドのためにアプリを Firebase に接続する方法については、Flutter 用の Firebase Codelab を参照してください。

この Codelab を完了するためにそれなりの時間と労力を必要とした

非常にそう思う そう思う どちらとも言えない そう思わない まったくそう思わない

今後もマテリアル コンポーネントを使用したい

非常にそう思う そう思う どちらとも言えない そう思わない まったくそう思わない