暗黙的アニメーションの内部の仕組みを理解し、自分のAnimationControllerを細かく操作しましょう。
このレッスンを Kodokon で開くAnimatedContainerは暗黙的アニメーションの仲間です。目標値を変えると、フレームワークが遷移をアニメーションしてくれます。内部では、すべてのImplicitlyAnimatedWidgetが、状態の中に隠されたAnimationControllerを持っています。新しい構成が届くと、didUpdateWidgetがforEachTweenを通じてプロパティをたどり、それぞれのtweenを再ターゲットします。beginの端が現在の補間値になり、endの端が新しい目標になって、それからコントローラーがゼロから再スタートします。価値ある帰結があります。アニメーションの途中で目標を変えても視覚的な飛びが起きません。アニメーションはちょうど止まっていた場所から再開します。
import 'package:flutter/material.dart';
class SelectableCard extends StatefulWidget {
const SelectableCard({super.key});
@override
State<SelectableCard> createState() =>
_SelectableCardState();
}
class _SelectableCardState extends State<SelectableCard> {
bool _selected = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () =>
setState(() => _selected = !_selected),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
width: _selected ? 220 : 160,
height: 96,
decoration: BoxDecoration(
color: _selected
? Colors.indigo
: Colors.indigo.shade200,
borderRadius: BorderRadius.circular(
_selected ? 28 : 12,
),
),
),
);
}
}暗黙的アニメーションはフレームワークが予期するプロパティをカバーし、TweenAnimationBuilderはその処理を任意の補間可能な値へと一般化します。アニメーションを操作する必要が出てきたらすぐに、つまり一時停止、逆再生、繰り返し、状態への反応が必要になったら、明示的なAnimationControllerに切り替えましょう。vsyncパラメーターはTickerProviderを期待します。ミックスインがTickerを作り、これはSchedulerBindingによってフレームごとに呼び出されるコールバックです。あまり知られていない機微があります。TickerModeは隠れたサブツリー(たとえば覆われたルートの下)のtickerをミュートします。時間は流れ続けますが、もうコールバックは実行されません。見えないアニメーションに無駄なCPUはゼロです。
import 'package:flutter/material.dart';
class RevealPanel extends StatefulWidget {
const RevealPanel({super.key, required this.child});
final Widget child;
@override
State<RevealPanel> createState() => _RevealPanelState();
}
class _RevealPanelState extends State<RevealPanel>
with SingleTickerProviderStateMixin {
late final AnimationController _controller =
AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
reverseDuration: const Duration(milliseconds: 200),
);
late final Animation<double> _opacity = _controller
.drive(CurveTween(curve: Curves.easeOutCubic));
void toggle() {
switch (_controller.status) {
case AnimationStatus.forward:
case AnimationStatus.completed:
_controller.reverse();
case AnimationStatus.reverse:
case AnimationStatus.dismissed:
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: toggle,
child: FadeTransition(
opacity: _opacity,
child: widget.child,
),
);
}
}コントローラーは0から1へと進むただのAnimation<double>です。生のまま消費するのではなく、合成しましょう。controller.drive(CurveTween(...))は、余分な状態を持たない派生アニメーションを生み出します。値をツリーに適用するには、Transitionウィジェット(FadeTransition、RotationTransition)を選びましょう。これらはフレームごとにウィジェットを一つも再構築することなく、レンダーオブジェクトのレベルで直接アニメーションを購読します。変換が任意のものであるときは、AnimatedBuilderが仕事をこなします。ただし静的なサブツリーをchildを通じて渡すことが条件です。それは一度だけ構築され、それからフレームごとにbuilderへと再注入されます。それが明示的アニメーションにおける最も費用対効果の高い最適化です。
import 'dart:math' as math;
import 'package:flutter/material.dart';
class Orbit extends StatefulWidget {
const Orbit({super.key});
@override
State<Orbit> createState() => _OrbitState();
}
class _OrbitState extends State<Orbit>
with SingleTickerProviderStateMixin {
late final AnimationController _controller =
AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
)..repeat();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: const Icon(Icons.satellite_alt, size: 32),
builder: (context, child) {
final angle = _controller.value * 2 * math.pi;
return Transform.translate(
offset: Offset(
math.cos(angle) * 80,
math.sin(angle) * 80,
),
child: child,
);
},
);
}
}