概要
Flutter ver.1.17 に合わせて公開された、新しいアニメーション Material motion for Flutter の Container tranform の実装方法をご紹介します。ここでは Flutter Packages から提供されているanimationsを利用して、シンプルな構成で Container transform の実装方法をご紹介いたします。
Container transform とは
Material motion の Container transform は以下のようなアニメーションです。
*画像は animations(pub.dev)を参照しています。
Flutter で Material motion を実装するには、animations パッケージの OpenContianer ウィジェットを利用します。
OpenContainer ウィジェットについて
Container transform を実装するためのウィジェットが OpenContainer です。 下記が最小構成の実装になります。
OpenContainer(
//- 開いた時のウィジェットを設定
openBuilder: (context, VoidCallback openContainer) {
return DetailPage(),
},
//- 通常時のウィジェットを設定
closedBuilder: (context, VoidCallback openContainer) {
return ListTile(
title: Text('Title'),
onTap: openContainer,
);
}
),
下記が OpenContainer のオプション一覧です
//- 閉じている時のウィジェット。
Widget closedBuilder
//- 開いた時のウィジェット。
Widget openBuilder
//- 閉じている時の背景色。デフォルトは白(Colors.white)
Color closedColor
//- 開いている時の背景色。デフォルトは白(Colors.white)
Color openColor
//- 閉じている時のエレベーション。デフォルトは1.0
double closedElevation
//- 開いている時のエレベーション。デフォルトは4.0
double openElevation
//- 閉じている時のウィジェットのシェイプ。
//- デフォルトはRoundedRectangleBorderでradiusが4.0
ShapeBorder closedShape
//- 開いている時のウィジェットのシェイプ。
//- デフォルトはRoundedRectangleBorder()
ShapeBorder openShape
//- (必須)閉じている時(通常時)のウィジェット
OpenContainerBuilder closedBuilder:(BuildContext context, VoidCallback action)
//- (必須)開いた時のウィジェット
OpenContainerBuilder opendBuilder:(BuildContext context, VoidCallback action)
//- 閉じている時のウィジェットをタップして開けるようにするかどうか。 デフォルトはtrue。
//- ただし、closeBuilderで指定したaction(onTap等)でのみ開くようにするにはfalseにする。
bool tappable
//- アニメーション時間。デフォルトは300ms
Duration transitionDuration
//- アニメーションタイプ。fade か fadeThrough。デフォルトはfade
ContainerTransitionType transitionType
サンプル
以下 Container transform の実装サンプルをご紹介します。
ファイル
lib ディレクトリ以下 4 つのファイルで構成されています。
lib/
├── detail_page.dart
├── gridview_demo.dart
├── listview_demo.dart
└── main.dart
- main.dart
ListView と GridView の各デモ画面に移動することを目的としています。 - detail_ page.dart
アニメーション後の詳細ページです。 - listview_ demo.dart
ListView で Container transform を実装したデモページです。 - gridview_ demo.dart
GridView で Container transform を実装したデモページです。
コード全体
import 'package:flutter/material.dart';
import 'listview_demo.dart';
import 'gridview_demo.dart';
void main() {
runApp(
MaterialApp(
home: Menu(),
routes: {
ListViewDemo.routeName: (context) => ListViewDemo(),
GridViewDemo.routeName: (context) => GridViewDemo(),
},
),
);
}
class Menu extends StatelessWidget {
const Menu({
Key key,
}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Container Transition Demo'),
),
body: ListView(
children: <Widget>[
ListTile(
title: Text('ListView Demo'),
trailing: Icon(Icons.arrow_right),
onTap: () => Navigator.pushNamed(context, ListViewDemo.routeName),
),
ListTile(
title: Text('GridView Demo'),
trailing: Icon(Icons.arrow_right),
onTap: () => Navigator.pushNamed(context, GridViewDemo.routeName),
)
],
),
);
}
}
ListView での Container transform 実装例です。
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'detail_page.dart';
class ListViewDemo extends StatefulWidget {
static const String routeName = 'ListViewDemo';
_ListViewDemoState createState() => _ListViewDemoState();
}
class _ListViewDemoState extends State<ListViewDemo> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ListView Demo'),
),
body: ListView(
children: <Widget>[
...List<Widget>.generate(
20,
(index) => OpenContainer(
tappable: false,
openBuilder: (context, VoidCallback openContainer) => DetailPage(
index: index,
),
closedBuilder: (context, VoidCallback openContainer) => _ListItem(
index: index,
openContainer: openContainer,
),
),
),
],
),
);
}
}
class _ListItem extends StatelessWidget {
final int index;
final VoidCallback openContainer;
const _ListItem({
Key key,
this.index,
this.openContainer,
}) : assert(index != null),
assert(openContainer != null),
super(key: key);
Widget build(BuildContext context) {
return ListTile(
leading: Icon(
Icons.image,
size: 40.0,
),
onTap: openContainer,
title: Text('List item ${index + 1}'),
subtitle: const Text('Secondary text'),
);
}
}
GridView での Container transform 実装例です。
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'detail_page.dart';
class GridViewDemo extends StatefulWidget {
static const String routeName = 'GridViewDemo';
_GridViewDemoState createState() => _GridViewDemoState();
}
class _GridViewDemoState extends State<GridViewDemo> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GridView Transition Demo'),
),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 10 / 9,
children: <Widget>[
...List<Widget>.generate(
10,
(index) => OpenContainer(
tappable: false,
openBuilder: (context, VoidCallback openContainer) =>
DetailPage(
index: index,
),
closedBuilder: (context, VoidCallback openContainer) =>
_GridItem(
index: index,
openContainer: openContainer,
),
),
),
],
),
),
);
}
}
class _GridItem extends StatelessWidget {
final VoidCallback openContainer;
final int index;
const _GridItem({
Key key,
this.index,
this.openContainer,
}) : assert(index != null),
assert(openContainer != null),
super(key: key);
Widget build(BuildContext context) {
return InkWell(
onTap: openContainer,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
color: Colors.black12,
width: double.infinity,
height: 120.0,
child: Icon(Icons.image),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Grid Item ${index + 1}'),
),
],
),
);
}
}
アニメーション後の詳細ページです
import 'package:flutter/material.dart';
class DetailPage extends StatelessWidget {
final int index;
const DetailPage({
Key key,
this.index,
}) : assert(index != null),
super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Detail Page ${index + 1}'),
),
body: ListView(
children: <Widget>[
Container(
color: Colors.black12,
height: 250,
child: Icon(
Icons.image,
size: 40.0,
),
),
Padding(
padding: EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Title ${index + 1}',
style: Theme.of(context).textTheme.headline1.copyWith(
color: Colors.black54,
fontSize: 30.0,
),
),
const SizedBox(height: 10),
Text(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
style: Theme.of(context).textTheme.bodyText1.copyWith(
color: Colors.black54,
height: 1.5,
fontSize: 16.0,
),
),
],
),
),
],
),
);
}
}
あとがき
Container transform は、AppStore 等でよくみるアニメーションですが、animations パッケージを利用することで簡単に同じようなアニメーションを実装することができました。
animations には Shared axis を実装するためのウィジェットも同梱されており、こちらも汎用性のあるアニメーションを実装することができます。
Flutter も 1.17 になったことで iOS ではメタルを利用した処理が優先してされるようになり、リッチな表現がもっと増えてくるのではないでしょうか。