目次
- 作成するアニメーション
- ベースプログラム
- コーディング手順
- 1. アニメーションを作る準備
- 2. 波の要素を1つ追加
- 3. WaveClipperを実装
- 4. 波をもう一つ追加
- 最終的なプログラム
- 応用
作成するアニメーション
ベースプログラム
これが今回のベースとなるプログラムです。
一番下の_WaveViewState
にアニメーションの実装をしていきます。
main.dart
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// システムバー・ナビゲーションバーの色
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
systemNavigationBarColor: Colors.white,
systemNavigationBarDividerColor: Colors.grey,
systemNavigationBarIconBrightness: Brightness.dark,
));
return MaterialApp(
debugShowCheckedModeBanner: false, // DEBUGバナー削除
title: 'Wave Animation',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: WaveView(),
);
}
}
class WaveView extends StatefulWidget {
@override
_WaveViewState createState() => _WaveViewState();
}
class _WaveViewState extends State<WaveView> {
// ...
}
コーディング手順
1. アニメーションを作る準備
// 1
class _WaveViewState extends State<WaveView> with SingleTickerProviderStateMixin {
// 2
AnimationController waveController; // AnimationControllerの宣言
static const darkBlue = Color(0xFF264bc5); // 波の色
// 3
@override
void initState() {
waveController = AnimationController(
duration: const Duration(seconds: 3), // アニメーションの間隔を3秒に設定
vsync: this, // おきまり
)..repeat(); // リピート設定
super.initState();
}
// 4
@override
void dispose() {
waveController.dispose(); // AnimationControllerは明示的にdisposeする。
super.dispose();
}
// 5
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: waveController, // waveControllerを設定
builder: (context, child) => Stack(
children: <Widget>[
Text(waveController.value.toString()), // テスト用
// 本当の要素はこのあと追加します。
],
),
),
);
}
}
- AnimationControllerを1つだけ扱うので
SingleTickerProviderStateMixin
を適用 -
waveController
というAnimationController
を宣言 - アニメーションの間隔・リピートを設定
-
AnimationController
の破棄 -
AnimatedBuilder
にwaveController
を設定
ここまでできたら一度コンパイルして動かしてみましょう。
waveController.value
が3秒間隔で0~1まで変化していることがわかるはずです。
2. 波の要素を1つ追加
build関数のみ
class _WaveViewState extends State<WaveView> with SingleTickerProviderStateMixin {
// ...
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: waveController, // waveControllerを設定
builder: (context, child) => Stack(
children: <Widget>[
// ↓ 追加部分
// 1
ClipPath(
// 3
child: Container(color: darkBlue),
// 2
clipper: WaveClipper(context, waveController.value, 0),
),
// ↑ 追加部分
],
),
),
);
}
}
- クリッピング(切り抜き)用のウイジェット
ClipPath
を追加 - 切り抜き対象(child)にContainerを設定(波の色を指定)
- 切り抜き方法として
WaveClipper
を設定
WaveClipper
が未定義であるため,このままでは動きません。
次のステップでWaveClipper
を実装します。
3. WaveClipper
を実装
WaveClipper
は_WaveViewState
の外に実装します。
class WaveClipper extends CustomClipper<Path> {
// 1
WaveClipper(this.context, this.waveControllerValue, this.offset) {
final width = MediaQuery.of(context).size.width; // 画面の横幅
final height = MediaQuery.of(context).size.height; // 画面の高さ
// coordinateListに波の座標を追加
for (var i = 0; i <= width / 3; i++) {
final step = (i / width) - waveControllerValue;
coordinateList.add(
Offset(
i.toDouble() * 3, // X座標
math.sin(step * 2 * math.pi - offset) * 45 + height * 0.5, // Y座標
),
);
}
}
final BuildContext context;
final double waveControllerValue; // waveController.valueの値
final double offset; // 波のずれ
final List<Offset> coordinateList = []; // 波の座標のリスト
// 2
@override
Path getClip(Size size) {
final path = Path()
// addPolygon: coordinateListに入っている座標を直線で結ぶ。
// false -> 最後に始点に戻らない
..addPolygon(coordinateList, false)
..lineTo(size.width, size.height) // 画面右下へ
..lineTo(0, size.height) // 画面左下へ
..close(); // 始点に戻る
return path;
}
// 3
// 再クリップするタイミング -> animationValueが更新されていたとき
@override
bool shouldReclip(WaveClipper oldClipper) =>
waveControllerValue != oldClipper.waveControllerValue;
}
- コンストラクタを定義,
coordinateList
の初期化を行います。 - 切り抜き方法を定義, 引数の
size
は画面のサイズです。 - 再クリップの判定方法を定義, 返り値がtrueなら再クリップされます。
これで動くようになりました。リロードしてみましょう。波が一つ追加されているはずです。
4. 波をもう一つ追加
build関数のみ
class _WaveViewState extends State<WaveView> with SingleTickerProviderStateMixin {
// ...
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: waveController, // waveControllerを設定
builder: (context, child) => Stack(
children: <Widget>[
// 1つ目の波
ClipPath(
child: Container(color: darkBlue),
clipper: WaveClipper(context, waveController.value, 0),
),
// 2つ目の波
ClipPath(
child: Container(color: darkBlue.withOpacity(0.6)),
clipper: WaveClipper(context, waveController.value, 0.5),
),
],
),
),
);
}
最終的なプログラム(_WaveViewState
とWaveClipper
のみ)
main.dart
class _WaveViewState extends State<WaveView>
with SingleTickerProviderStateMixin {
AnimationController waveController; // AnimationControllerの宣言
static const darkBlue = Color(0xFF264bc5); // 波の色
@override
void initState() {
waveController = AnimationController(
duration: const Duration(seconds: 3), // アニメーションの間隔を3秒に設定
vsync: this, // おきまり
)..repeat(); // リピート設定
super.initState();
}
@override
void dispose() {
waveController.dispose(); // AnimationControllerは明示的にdisposeする。
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: waveController, // waveControllerを設定
builder: (context, child) => Stack(
children: <Widget>[
// 1つ目の波
ClipPath(
child: Container(color: darkBlue),
clipper: WaveClipper(context, waveController.value, 0),
),
// 2つ目の波
ClipPath(
child: Container(color: darkBlue.withOpacity(0.6)),
clipper: WaveClipper(context, waveController.value, 0.5),
),
],
),
),
);
}
}
class WaveClipper extends CustomClipper<Path> {
WaveClipper(this.context, this.waveControllerValue, this.offset) {
final width = MediaQuery.of(context).size.width; // 画面の横幅
final height = MediaQuery.of(context).size.height; // 画面の高さ
// coordinateListに波の座標を追加
for (var i = 0; i <= width / 3; i++) {
final step = (i / width) - waveControllerValue;
coordinateList.add(
Offset(
i.toDouble() * 3, // X座標
math.sin(step * 2 * math.pi - offset) * 45 + height * 0.5, // Y座標
),
);
}
}
final BuildContext context;
final double waveControllerValue; // waveController.valueの値
final double offset; // 波のずれ
final List<Offset> coordinateList = []; // 波の座標のリスト
@override
Path getClip(Size size) {
final path = Path()
// addPolygon: coordinateListに入っている座標を直線で結ぶ。
// false -> 最後に始点に戻らない
..addPolygon(coordinateList, false)
..lineTo(size.width, size.height) // 画面右下へ
..lineTo(0, size.height) // 画面左下へ
..close(); // 始点に戻る
return path;
}
// 再クリップするタイミング -> animationValueが更新されていたとき
@override
bool shouldReclip(WaveClipper oldClipper) =>
waveControllerValue != oldClipper.waveControllerValue;
}
応用
燃料タンクっぽいやつ#Flutter pic.twitter.com/CNraqtW56O
— シン (@derodero24) May 11, 2020