codelab MDC-103では、マテリアルコンポーネント(MDC)の色、標高、タイポグラフィ、および形状をカスタマイズして、アプリのスタイルを設定しました。
マテリアルデザインシステムのコンポーネントは、事前定義された一連のタスクを実行し、ボタンなどの特定の特性を備えています。
ただし、ボタンはユーザーがアクションを実行するための単なる方法ではなく、ユーザーにインタラクティブであり、タッチまたはクリックすると何かが発生することをユーザーに知らせる形状、サイズ、および色の視覚的表現でもあります。
マテリアルデザインのガイドラインでは、設計者の観点からコンポーネントについて説明しています。 これらは、プラットフォーム間で利用可能な幅広い基本機能と、各コンポーネントを構成する解剖学的要素について説明しています。 たとえば、背景には、バックレイヤーとそのコンテンツ、フロントレイヤーとそのコンテンツ、モーションルール、および表示オプションが含まれます。 これらの各コンポーネントは、各アプリのニーズ、ユースケース、およびコンテンツに合わせてカスタマイズできます。 これらの部分は、ほとんどの場合、プラットフォームのSDKの従来のビュー、コントロール、および機能です。
マテリアルデザインのガイドラインでは多くのコンポーネントに名前が付けられていますが、それらのすべてが再利用可能なコードの適切な候補であるとは限らないため、候補が見つからない場合もあるかもしれません。
これらのエクスペリエンスを自分で作成して、すべて従来のコードを使用して、アプリのカスタマイズされたスタイルを実現できます。
Contents
What you’ll build
このコードラボでは、ShrineアプリのUIを「backdrop」と呼ばれる2レベルのプレゼンテーションに変更します。 backdropには、非対称グリッドに表示される製品をフィルタリングするために使用される選択可能なカテゴリを一覧表示するメニューが含まれています。 このコードラボでは、次のFlutterコンポーネントを使用します。
Shape
Motion
Flutterウィジェット(以前のコードラボで使用したもの)
2. Set up your Flutter environment
工事中🏗
backdropは、他のすべてのコンテンツとコンポーネントの背後に表示されます。 これは、バックレイヤー(アクションとフィルターを表示する)とフロントレイヤー(コンテンツを表示する)の2つのレイヤーで構成されています。 背景を使用して、ナビゲーションやコンテンツフィルターなどのインタラクティブな情報やアクションを表示できます。
(1)Remove the home app bar
HomePageウィジェットは、フロントレイヤーのコンテンツになります。 現在、アプリバーがあります。 アプリバーをバックレイヤーに移動すると、ホームページにはAsymmetricViewのみが含まれます。
home.dartで、build()関数を変更してAsymmetricViewを返すだけにします。
ここまでのコード
//home.dart 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) @override Widget build(BuildContext context) { return AsymmetricView( products: ProductsRepository.loadProducts(Category.all)); } }
(2)Add the Backdrop widget
frontLayerとbackLayerを含むBackdropというウィジェットを作成します。
backLayerには、リストをフィルタリングするためのカテゴリ(currentCategory)を選択できるメニューが含まれています。 メニューの選択を維持したいので、Backdropをステートフルウィジェットにします。
backdrop.dartという名前の新しいファイルを/ libに追加します。
import 'package:flutter/material.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,
Key? key,
}) : super(key: key);
@override
_BackdropState createState() => _BackdropState();
}
// TODO: Add _FrontLayer class (104)
// TODO: Add _BackdropTitle class (104)
// TODO: Add _BackdropState class (104)
Notice we mark certain properties required
. This is a best practice for properties in the constructor that have no default value and cannot be null
and therefore should not be forgotten.
いくつかのプロパティをrequiredキーワードを付けていることに注目してください。これは「デフォルト値が無く、nullになることがない、したがって忘れてはならない」プロパティに対するベストプラクティスです。
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)
Under the Backdrop class definition, add _BackdropState class:
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が使用していたのと同じように、AppBarを備えたScaffoldウィジェットを返します。 しかし、ScaffoldウィジェットのbodyはStackウィジェットです。 Stackの子ウィジェットは重なり合うことが可能です。 各子のサイズと場所は、Stackの親を基準にして指定されます。
次に、BackdropインスタンスをShrineAppに追加します。
app.dartで、backdrop.dartとmodel /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ウィジェットに設定します。
BackdropウィジェットはfrontLayerプロパティにHomePage()コンストラクタを設定しています。
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'),
),
再生ボタンを押すと、ホームページが表示され、アプリバーも表示されます。
backLayerは、frontLayerホームページの後ろの新しいレイヤーにピンク色の領域を表示します。
Flutter Inspectorを使用して、スタックのホームページの背後に実際にコンテナーがあることを確認できます。 これに似ているはずです:
これで、2つのレイヤーのデザインとコンテンツを調整できます。
5. Add a shape
このステップでは、フロントレイヤーのスタイルを設定して、左上隅にカットを追加します。
マテリアルデザインでは、このタイプのカスタマイズをshapeと呼びます。
材料表面は任意の形状を持つことができます。 形状は表面に強調とスタイルを追加し、ブランディングを表現するために使用できます。
通常の長方形の形状は、曲線または角度の付いたコーナーとエッジ、および任意の数の側面でカスタマイズできます。 それらは対称的にもできますし不規則にもできます。
(1)Add a shape to the front layer
角度の付いたShrineのロゴは、Shrineアプリのシェイプストーリーに影響を与えました。 シェイプストーリーは、アプリ全体に適用されるシェイプの一般的な使用法です。 たとえば、ロゴの形状は、形状が適用されているログインページ要素にエコーされます。 このステップでは、左上隅に角度を付けたカットを使用してフロントレイヤーのスタイルを設定します。
ロゴであるダイヤモンドの形状が、このShrineアプリの他のページの要素(ボタンなど)のデザインにも影響を与える、そうしてアプリ全体としてデザインの統一感を演出する、みたいなことかと思います。よくこれだけわかりにくい説明ができるものだなあ、という感じです笑
backdrop.dartに、新しいクラス_FrontLayerを追加します。
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),
],
);
}
Shrineの最前面にカスタムShapeを施しました。
表面のelevation(Materialウィジェットのelevationプロパティ)により、ユーザーは前面の白いレイヤーのすぐ後ろに何かがあることがわかります。 ユーザーがbackdropのバックレイヤーを表示できるように、モーションを追加しましょう。
6. Add motion
モーションは、アプリに命を吹き込む方法です。
それは大きくて劇的、微妙で最小限、またはその間のどこでもかまいません。
使用するモーションのタイプは状況に適している必要があることに注意してください。
繰り返される定期的なアクションに適用されるモーションは、アクションがユーザーの気を散らしたり、定期的に時間がかかりすぎたりしないように、小さくて微妙なものにする必要があります。
ユーザーが初めてアプリを開いたときなど、より目を引く適切な状況があります。一部のアニメーションは、アプリの使用方法についてユーザーを教育するのに役立ちます。
backdrop.dartの上部で、クラスまたは関数のスコープ外に、アニメーションに必要な速度を表す定数を追加します。
const double _kFlingVelocity = 2.0;
AnimationControllerウィジェットを_BackdropStateに追加し、initState()関数でインスタンス化し、状態のdispose()関数で破棄します。
ウィジェットのライフサイクル
initState()メソッドは、ウィジェットがレンダーツリーの一部になる前に一度だけ呼び出されます。 dispose()メソッドも、ウィジェットがツリーから完全に削除されたときに1回だけ呼び出されます。
AnimationControllerはアニメーションを調整し、アニメーションを再生、反転、停止するためのAPIを提供します。 今、私たちはそれを動かす関数が必要です。
フロントレイヤーの可視性を決定および変更する関数を追加します。
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のメニュー項目を除外します。
// 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は、ビルダーコールバックがサイズの制約を提供する特別なウィジェットです。
LayoutBuilder
ウィジェットツリーは、葉に向かって移動することによってレイアウトを実行します。
制約はツリーに渡されますが、サイズは通常、リーフが制約に基づいてサイズを返すまで計算されません。
葉がその親のサイズを知る必要がある場合、それはまだ計算されていないため、それはできませんでした。
LayoutBuilderは、ウィジェットがそれ自体をレイアウトするために親ウィジェットのサイズを認識している必要がある場合に使用されます
(そして、親のサイズは子に依存しません。)
LayoutBuilderは、ウィジェットを返す関数を取ります。
build()関数で、アプリバーの先頭のメニューアイコンをIconButtonに変え、ボタンがタップされたときにフロントレイヤーの表示を切り替えるために使用します。
// TODO: Replace leading menu icon with IconButton (104)
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),
メニューは、テキストアイテムがタッチされたときにリスナーに通知するタップ可能なテキストアイテムのリストです。 このステップでは、カテゴリフィルタリングメニューを追加します。
メニューをフロントレイヤーに追加し、インタラクティブボタンをバックレイヤーに追加します。
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(オプション)+ Enterを押します。
3.「StatefulWidgetに変換」を選択します。
app.dartで、_ShrineAppStateに、選択したカテゴリのための変数を追加します。タップされた時に呼び出されるコールバックも追加します。
class _ShrineAppState extends State<ShrineApp> {
Category _currentCategory = Category.all;
void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}
次に、バックレイヤーをCategoryMenuPageに変更します。
app.dartの
_ShrineAppStateクラス
のbuild()メソッドで、バックレイヤーフィールドをCategoryMenuPageに変更し、currentCategoryフィールドを変更してインスタンス変数を取得します。
//app.dart(buildメソッドのみ) @override Widget build(BuildContext context) { return MaterialApp( color:Colors.lightGreenAccent, title: 'Shrine', //↓(2)Add the Backdrop widgetで追加。 home: Backdrop( // ↓(1)Add the menuで変更。 currentCategory: _currentCategory, // TODO: Pass _currentCategory for frontLayer (104) frontLayer: HomePage(), //↓(1)Add the menuで変更。エラーが出るのでファイルを読み込む。 backLayer: CategoryMenuPage( currentCategory: _currentCategory, onCategoryTap: _onCategoryTap, ), //backLayer: Container(), frontTitle: Text('SHRINE'), backTitle: Text('MENU'), ), // TODO: Make currentCategory field take _currentCategory (104) // TODO: Pass _currentCategory for frontLayer (104) // TODO: Change backLayer field value to CategoryMenuPage (104) initialRoute: '/login', onGenerateRoute: _getRoute, theme: _kShrineTheme, ); }
backLayer: CategoryMenuPage(
の部分でエラー(赤い波下線)が出ます。さっき作成した
lib/category_menu_page.dart
をインポートしていないからです。ですのでインポートします。
CategoryMenuPageの部分にカーソルを持ってきて
オプション+エンターキー
を押してください。ドロップダウンメニューが出て、エラーを直す選択肢を表示してくれます。
「Import liblary ‘category_menu_page.dart’」
を選択してください。自動的にapp.dart先頭部分に
import 'category_menu_page.dart';
が追加されて、赤い波下線が消えました。
工事中🏗
参考
https://codelabs.developers.google.com/codelabs/mdc-104-flutter#0