Kodokon kodokon.com

แอนิเมชัน: implicit และ explicit เบื้องหลังการทำงาน

ทำความเข้าใจกลไกภายในของแอนิเมชันแบบ implicit และควบคุม AnimationController ของคุณเองอย่างละเอียด

10 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

AnimatedContainer อยู่ในตระกูล แอนิเมชันแบบ implicit: คุณเปลี่ยนค่าเป้าหมาย แล้วเฟรมเวิร์กจะทำแอนิเมชันการเปลี่ยนผ่านให้ เบื้องหลังนั้น ImplicitlyAnimatedWidget ทุกตัวมี AnimationController ซ่อนอยู่ในสเตตของมัน เมื่อมีการตั้งค่าใหม่เข้ามา didUpdateWidget จะไล่ดูคุณสมบัติต่าง ๆ ผ่าน forEachTween และ ตั้งเป้าหมายใหม่ (re-target) ให้ tween แต่ละตัว: ขอบเขต begin จะกลายเป็นค่าที่ถูกแทรก (interpolate) อยู่ในปัจจุบัน ส่วนขอบเขต 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 เพียงครั้งเดียว

แบบ implicit ครอบคลุมคุณสมบัติที่เฟรมเวิร์กคาดการณ์ไว้ ส่วน TweenAnimationBuilder ขยายกระบวนการนี้ให้ใช้กับค่าใด ๆ ที่แทรกค่าได้ ทันทีที่คุณต้องการ ควบคุม แอนิเมชัน ไม่ว่าจะหยุดชั่วคราว ย้อนกลับ ทำซ้ำ หรือตอบสนองต่อสถานะ ให้เปลี่ยนไปใช้ AnimationController แบบ explicit พารามิเตอร์ vsync ต้องการ TickerProvider: mixin จะสร้าง Ticker ซึ่งเป็นคอลแบ็กที่ SchedulerBinding เรียกในทุกเฟรม รายละเอียดเล็ก ๆ ที่น้อยคนรู้: TickerMode จะ ปิดเสียง (mute) ticker ของ subtree ที่ถูกซ่อนอยู่ (เช่น อยู่ใต้เส้นทางที่ถูกบัง) เวลายังคงเดินต่อไป แต่ไม่มีคอลแบ็กใดทำงานอีกแล้ว ไม่มีการเปลือง 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: ขาเข้าและขาออกต่างก็มีจังหวะของตัวเอง

คอนโทรลเลอร์ก็เป็นเพียง Animation<double> ที่วิ่งจาก 0 ถึง 1: จงประกอบมันขึ้นมาแทนที่จะใช้ค่าดิบของมันตรง ๆ controller.drive(CurveTween(...)) สร้างแอนิเมชันที่แตกยอดออกมา โดยไม่มีสเตตเพิ่ม ในการนำค่าไปใช้กับ tree ให้เลือกใช้วิดเจ็ตตระกูล Transition (FadeTransition, RotationTransition) พวกมันฟังแอนิเมชันโดยตรงที่ระดับ render object โดยไม่ต้องสร้างวิดเจ็ตใหม่แม้แต่ตัวเดียวต่อเฟรม เมื่อการแปลงเป็นแบบใด ๆ ก็ตาม AnimatedBuilder จัดการงานนี้ได้ ขอเพียงคุณส่ง subtree ที่อยู่นิ่งผ่าน child: มันจะถูกสร้าง เพียงครั้งเดียว แล้วถูกฉีดกลับเข้าไปใน builder ในทุกเฟรม นั่นคือการปรับให้เหมาะสมที่ให้ผลตอบแทนสูงที่สุดสำหรับแอนิเมชันแบบ explicit

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. พารามิเตอร์ vsync ของ AnimationController มีไว้เพื่ออะไรกันแน่?
    • มันให้ Ticker ที่ซิงก์กับเฟรม ซึ่ง TickerMode สามารถปิดเสียงได้เมื่อ subtree ถูกซ่อน
    • มันซิงก์คอนโทรลเลอร์เข้ากับแอนิเมชันอื่น ๆ ของแอป
    • มันจำกัดแอนิเมชันไว้ที่ 30 เฟรมต่อวินาที
    • มันเปิดใช้การเร่งความเร็วด้วยฮาร์ดแวร์ GPU
  3. ทำไมจึงส่ง subtree ที่อยู่นิ่งผ่านพารามิเตอร์ child ของ AnimatedBuilder?
    • มันถูกสร้างครั้งเดียวแล้วฉีดกลับเข้าไปใน builder แทนที่จะถูกสร้างใหม่ในทุกเฟรม
    • มันเป็นข้อบังคับ: builder ไม่สามารถคืนวิดเจ็ตใหม่ได้
    • มันปล่อยคืนคอนโทรลเลอร์ให้โดยอัตโนมัติเมื่อจบ