Kodokon kodokon.com

アニメーション:暗黙的と明示的、その内部の仕組み

暗黙的アニメーションの内部の仕組みを理解し、自分のAnimationControllerを細かく操作しましょう。

10 分 · 3 問

このレッスンを Kodokon で開く

AnimatedContainer暗黙的アニメーションの仲間です。目標値を変えると、フレームワークが遷移をアニメーションしてくれます。内部では、すべてのImplicitlyAnimatedWidgetが、状態の中に隠されたAnimationControllerを持っています。新しい構成が届くと、didUpdateWidgetforEachTweenを通じてプロパティをたどり、それぞれのtweenを再ターゲットします。beginの端が現在の補間値になり、endの端が新しい目標になって、それからコントローラーがゼロから再スタートします。価値ある帰結があります。アニメーションの途中で目標を変えても視覚的な飛びが起きません。アニメーションはちょうど止まっていた場所から再開します。

DART
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,
          ),
        ),
      ),
    );
  }
}
サイズ、色、半径が一つのsetStateからアニメーションする。

暗黙的アニメーションはフレームワークが予期するプロパティをカバーし、TweenAnimationBuilderはその処理を任意の補間可能な値へと一般化します。アニメーションを操作する必要が出てきたらすぐに、つまり一時停止、逆再生、繰り返し、状態への反応が必要になったら、明示的なAnimationControllerに切り替えましょう。vsyncパラメーターはTickerProviderを期待します。ミックスインがTickerを作り、これはSchedulerBindingによってフレームごとに呼び出されるコールバックです。あまり知られていない機微があります。TickerModeは隠れたサブツリー(たとえば覆われたルートの下)のtickerをミュートします。時間は流れ続けますが、もうコールバックは実行されません。見えないアニメーションに無駄なCPUはゼロです。

DART
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,
      ),
    );
  }
}
reverseDuration:行きと帰りがそれぞれ独自のテンポを持つ。

コントローラーは0から1へと進むただのAnimation<double>です。生のまま消費するのではなく、合成しましょう。controller.drive(CurveTween(...))は、余分な状態を持たない派生アニメーションを生み出します。値をツリーに適用するには、Transitionウィジェット(FadeTransitionRotationTransition)を選びましょう。これらはフレームごとにウィジェットを一つも再構築することなく、レンダーオブジェクトのレベルで直接アニメーションを購読します。変換が任意のものであるときは、AnimatedBuilderが仕事をこなします。ただし静的なサブツリーをchildを通じて渡すことが条件です。それは一度だけ構築され、それからフレームごとにbuilderへと再注入されます。それが明示的アニメーションにおける最も費用対効果の高い最適化です。

DART
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,
        );
      },
    );
  }
}
childとして渡されたアイコンは一度だけ構築される。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. 前のアニメーションが途中まで進んでいるときに、AnimatedContainerが新しい目標の色を受け取ります。どうなりますか?
    • tweenが再ターゲットされます。現在の値から新しい目標に向かって、視覚的な飛びなく再スタートします
    • 実行中のアニメーションが先に終わってから、新しいものが始まる
    • ウィジェットが即座に新しい色へ飛ぶ
  2. AnimationControllerのvsyncパラメーターは、正確には何のためにありますか?
    • フレームに同期したTickerを提供し、サブツリーが隠れているときTickerModeがそれをミュートできる
    • コントローラーをアプリのほかのアニメーションと同期させる
    • アニメーションを毎秒30フレームに制限する
    • GPUのハードウェアアクセラレーションを有効にする
  3. なぜ静的なサブツリーをAnimatedBuilderのchildパラメーターを通じて渡すのですか?
    • フレームごとに再構築されるのではなく、一度構築されてからbuilderに再注入される
    • 必須です。builderは新しいウィジェットを返せない
    • 最後にコントローラーを自動的に解放する