انیمیشن در فلاتر

۱. مقدمه

انیمیشن‌ها راهی عالی برای بهبود تجربه کاربری برنامه شما، انتقال اطلاعات مهم به کاربر و جذاب‌تر و لذت‌بخش‌تر کردن استفاده از برنامه شما هستند.

مروری بر چارچوب انیمیشن فلاتر

فلاتر با بازسازی بخشی از درخت ویجت در هر فریم، جلوه‌های انیمیشن را نمایش می‌دهد. این فریم‌ورک جلوه‌های انیمیشن از پیش ساخته شده و سایر APIها را برای آسان‌تر کردن ایجاد و ترکیب انیمیشن‌ها فراهم می‌کند.

  • انیمیشن‌های ضمنی، جلوه‌های انیمیشن از پیش ساخته شده‌ای هستند که کل انیمیشن را به طور خودکار اجرا می‌کنند. وقتی مقدار هدف انیمیشن تغییر می‌کند، انیمیشن را از مقدار فعلی تا مقدار هدف اجرا می‌کند و هر مقدار بین آنها را نمایش می‌دهد تا ویجت به طور روان حرکت کند. نمونه‌هایی از انیمیشن‌های ضمنی شامل AnimatedSize ، AnimatedScale و AnimatedPositioned است.
  • انیمیشن‌های صریح (Explicit animations) نیز جلوه‌های انیمیشن از پیش ساخته شده‌ای هستند، اما برای کار کردن به یک شیء Animation نیاز دارند. نمونه‌هایی از آنها عبارتند از SizeTransition ، ScaleTransition یا PositionedTransition .
  • Animation کلاسی است که یک انیمیشن در حال اجرا یا متوقف شده را نشان می‌دهد و از یک مقدار که نشان‌دهنده مقدار هدفی است که انیمیشن به سمت آن اجرا می‌شود و status که نشان‌دهنده مقدار فعلی است که انیمیشن در هر زمان معین روی صفحه نمایش می‌دهد، تشکیل شده است. این کلاس زیرکلاس Listenable است و هنگام تغییر وضعیت در حین اجرای انیمیشن، به شنوندگان خود اطلاع می‌دهد.
  • AnimationController روشی برای ایجاد یک انیمیشن و کنترل وضعیت آن است. متدهای آن مانند forward() ، reset() ، stop() و repeat() می‌توانند برای کنترل انیمیشن بدون نیاز به تعریف جلوه انیمیشنی که نمایش داده می‌شود، مانند مقیاس، اندازه یا موقعیت، استفاده شوند.
  • از Tweenها برای درون‌یابی مقادیر بین یک مقدار ابتدایی و انتهایی استفاده می‌شود و می‌تواند هر نوعی مانند double، Offset یا Color را نشان دهد.
  • منحنی‌ها برای تنظیم نرخ تغییر یک پارامتر در طول زمان استفاده می‌شوند. وقتی یک انیمیشن اجرا می‌شود، معمولاً یک منحنی کاهش سرعت اعمال می‌شود تا نرخ تغییر در ابتدا یا انتهای انیمیشن سریع‌تر یا کندتر شود. منحنی‌ها یک مقدار ورودی بین 0.0 تا 1.0 می‌گیرند و یک مقدار خروجی بین 0.0 تا 1.0 برمی‌گردانند.

آنچه خواهید ساخت

در این آزمایشگاه کد، شما یک بازی مسابقه چند گزینه‌ای خواهید ساخت که دارای جلوه‌ها و تکنیک‌های انیمیشنی متنوعی است.

3026390ad413769c.gif

خواهید دید که چگونه می‌توان...

  • ساخت ویجتی که اندازه و رنگ خود را متحرک می‌کند
  • ساخت افکت ورق زدن کارت سه بعدی
  • از جلوه‌های انیمیشنی از پیش ساخته شده‌ی فانتزی از بسته‌ی انیمیشن‌ها استفاده کنید
  • پشتیبانی از ژست حرکتی پیش‌بینی‌کننده برای بازگشت به عقب که در آخرین نسخه اندروید موجود است، اضافه شد.

آنچه یاد خواهید گرفت

در این آزمایشگاه کد یاد خواهید گرفت:

  • نحوه استفاده از جلوه‌های انیمیشن ضمنی برای دستیابی به انیمیشن‌های زیبا بدون نیاز به کد زیاد.
  • نحوه استفاده از جلوه‌های انیمیشنی صریح برای پیکربندی جلوه‌های خودتان با استفاده از ویجت‌های انیمیشنی از پیش ساخته شده مانند AnimatedSwitcher یا AnimationController .
  • نحوه استفاده از AnimationController برای تعریف ویجت خودتان که جلوه سه بعدی را نمایش می‌دهد.
  • نحوه استفاده از بسته animations برای نمایش جلوه‌های انیمیشنی زیبا با حداقل تنظیمات.

آنچه نیاز دارید

  • کیت توسعه نرم‌افزار فلاتر
  • یک IDE مانند VSCode یا Android Studio / IntelliJ

۲. محیط توسعه فلاتر خود را تنظیم کنید

برای تکمیل این آزمایشگاه به دو نرم‌افزار نیاز دارید - SDK فلاتر و یک ویرایشگر .

شما می‌توانید codelab را با استفاده از هر یک از این دستگاه‌ها اجرا کنید:

  • یک دستگاه اندروید ( که برای پیاده‌سازی پیش‌بینی در مرحله ۷ توصیه می‌شود ) یا iOS فیزیکی که به رایانه شما متصل شده و در حالت توسعه‌دهنده (Developer mode) تنظیم شده باشد.
  • شبیه‌ساز iOS (نیاز به نصب ابزارهای Xcode دارد).
  • شبیه‌ساز اندروید (نیاز به راه‌اندازی در اندروید استودیو دارد).
  • یک مرورگر (برای اشکال‌زدایی، کروم مورد نیاز است).
  • یک کامپیوتر رومیزی ویندوز ، لینوکس یا macOS . شما باید روی پلتفرمی که قصد دارید آن را مستقر کنید، توسعه دهید. بنابراین، اگر می‌خواهید یک برنامه دسکتاپ ویندوزی توسعه دهید، باید روی ویندوز توسعه دهید تا به زنجیره ساخت مناسب دسترسی داشته باشید. الزامات خاص سیستم عامل وجود دارد که به تفصیل در docs.flutter.dev/desktop پوشش داده شده است.

نصب خود را تأیید کنید

برای تأیید اینکه SDK فلاتر شما به درستی پیکربندی شده است و حداقل یکی از پلتفرم‌های هدف فوق را نصب کرده‌اید، از ابزار Flutter Doctor استفاده کنید:

$ flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.2, on macOS 14.6.1 23G93 darwin-arm64, locale
    en)
[✓] Android toolchain - develop for Android devices
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio
[✓] IntelliJ IDEA Ultimate Edition
[✓] VS Code
[✓] Connected device (4 available)
[✓] Network resources

• No issues found!

۳. برنامه‌ی آغازین را اجرا کنید

برنامه شروع کننده را دانلود کنید

git برای کپی کردن برنامه‌ی شروع از مخزن flutter/samples در گیت‌هاب استفاده کنید.

git clone https://github.com/flutter/codelabs.git
cd codelabs/animations/step_01/

روش دیگر این است که می‌توانید کد منبع را به صورت یک فایل زیپ دانلود کنید .

برنامه را اجرا کنید

برای اجرای برنامه، از دستور flutter run استفاده کنید و یک دستگاه هدف مانند android ، ios یا chrome را مشخص کنید. برای مشاهده لیست کامل پلتفرم‌های پشتیبانی شده، به صفحه پلتفرم‌های پشتیبانی شده مراجعه کنید.

flutter run -d android

همچنین می‌توانید برنامه را با استفاده از IDE مورد نظر خود اجرا و اشکال‌زدایی کنید. برای اطلاعات بیشتر به مستندات رسمی Flutter مراجعه کنید.

کد را بررسی کنید

برنامه‌ی آغازین یک بازی مسابقه‌ی چندگزینه‌ای است که از دو صفحه نمایش با الگوی طراحی model-view-view-model یا MVVM تشکیل شده است. QuestionScreen (View) از کلاس QuizViewModel (View-Model) برای پرسیدن سوالات چندگزینه‌ای از کاربر از کلاس QuestionBank (Model) استفاده می‌کند.

  • home_screen.dart - صفحه‌ای را با دکمه‌ی «بازی جدید» نمایش می‌دهد
  • main.dart - MaterialApp برای استفاده از متریال ۳ و نمایش صفحه اصلی پیکربندی می‌کند.
  • model.dart - کلاس‌های اصلی مورد استفاده در سراسر برنامه را تعریف می‌کند.
  • question_screen.dart - رابط کاربری بازی مسابقه را نمایش می‌دهد
  • view_model.dart - وضعیت و منطق بازی مسابقه را که توسط QuestionScreen نمایش داده می‌شود، ذخیره می‌کند.

fbb1e1f7b6c91e21.png

این برنامه هنوز از هیچ جلوه انیمیشنی پشتیبانی نمی‌کند، به جز گذار نمای پیش‌فرض که توسط کلاس Navigator فلاتر هنگام فشار دادن دکمه New Game توسط کاربر نمایش داده می‌شود.

۴. از جلوه‌های انیمیشن ضمنی استفاده کنید

انیمیشن‌های ضمنی در بسیاری از موقعیت‌ها انتخاب بسیار خوبی هستند، زیرا به هیچ پیکربندی خاصی نیاز ندارند. در این بخش، ویجت StatusBar را به‌روزرسانی خواهید کرد تا یک تابلوی امتیازات متحرک نمایش دهد. برای یافتن جلوه‌های رایج انیمیشن ضمنی، مستندات API مربوط به ImplicitlyAnimatedWidget را مرور کنید.

206dd8d9c1fae95.gif

ویجت جدول امتیازات بدون انیمیشن را ایجاد کنید

یک فایل جدید به lib/scoreboard.dart با کد زیر ایجاد کنید:

lib/scoreboard.dart

import 'package:flutter/material.dart';

class Scoreboard extends StatelessWidget {
  final int score;
  final int totalQuestions;

  const Scoreboard({
    super.key,
    required this.score,
    required this.totalQuestions,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          for (var i = 0; i < totalQuestions; i++)
            Icon(
              Icons.star,
              size: 50,
              color: score < i + 1
                  ? Colors.grey.shade400
                  : Colors.yellow.shade700,
            ),
        ],
      ),
    );
  }
}

سپس ویجت Scoreboard را در فرزندان ویجت StatusBar اضافه کنید و آن را جایگزین ویجت‌های Text کنید که قبلاً نمره و تعداد کل سوالات را نشان می‌دادند. ویرایشگر شما باید به طور خودکار import "scoreboard.dart" در بالای فایل اضافه کند.

lib/question_screen.dart

class StatusBar extends StatelessWidget {
  final QuizViewModel viewModel;

  const StatusBar({required this.viewModel, super.key});

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      child: Padding(
        padding: EdgeInsets.all(8.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Scoreboard(                                        // NEW
              score: viewModel.score,                          // NEW
              totalQuestions: viewModel.totalQuestions,        // NEW
            ),
          ],
        ),
      ),
    );
  }
}

این ویجت برای هر سوال یک آیکون ستاره نمایش می‌دهد. وقتی به یک سوال به درستی پاسخ داده می‌شود، ستاره دیگری فوراً و بدون هیچ انیمیشنی روشن می‌شود. در مراحل بعدی، با متحرک‌سازی اندازه و رنگ ستاره، به کاربر اطلاع می‌دهید که امتیازش تغییر کرده است.

از یک جلوه انیمیشن ضمنی استفاده کنید

یک ویجت جدید به نام AnimatedStar ایجاد کنید که از یک ویجت AnimatedScale برای تغییر مقدار scale از 0.5 به 1.0 هنگام فعال شدن ستاره استفاده می‌کند:

lib/scoreboard.dart

import 'package:flutter/material.dart';

class Scoreboard extends StatelessWidget {
  final int score;
  final int totalQuestions;

  const Scoreboard({
    super.key,
    required this.score,
    required this.totalQuestions,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          for (var i = 0; i < totalQuestions; i++)
            AnimatedStar(isActive: score > i),                 // Edit this line.
        ],
      ),
    );
  }
}

class AnimatedStar extends StatelessWidget {                   // Add from here...
  final bool isActive;
  final Duration _duration = const Duration(milliseconds: 1000);
  final Color _deactivatedColor = Colors.grey.shade400;
  final Color _activatedColor = Colors.yellow.shade700;

  AnimatedStar({super.key, required this.isActive});

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      scale: isActive ? 1.0 : 0.5,
      duration: _duration,
      child: Icon(
        Icons.star,
        size: 50,
        color: isActive ? _activatedColor : _deactivatedColor,
      ),
    );
  }
}                                                              // To here.

حالا، وقتی کاربر به درستی به یک سوال پاسخ می‌دهد، ویجت AnimatedStar اندازه آن را با استفاده از یک انیمیشن ضمنی به‌روزرسانی می‌کند. color Icon در اینجا متحرک نیست، فقط scale آن متحرک می‌شود که توسط ویجت AnimatedScale انجام می‌شود.

84aec4776e70b870.gif

استفاده از Tween برای درون‌یابی بین دو مقدار

توجه داشته باشید که رنگ ویجت AnimatedStar بلافاصله پس از تغییر فیلد isActive به true تغییر می‌کند.

برای دستیابی به یک جلوه رنگی متحرک، می‌توانید از ویجت AnimatedContainer (که زیرکلاس دیگری از ImplicitlyAnimatedWidget است) استفاده کنید، زیرا می‌تواند به طور خودکار تمام ویژگی‌های خود، از جمله رنگ را متحرک کند. متأسفانه، ویجت ما باید یک آیکون را نمایش دهد، نه یک کانتینر.

همچنین می‌توانید AnimatedIcon امتحان کنید که جلوه‌های انتقال بین شکل‌های آیکون‌ها را پیاده‌سازی می‌کند. اما پیاده‌سازی پیش‌فرضی از آیکون ستاره در کلاس AnimatedIcons وجود ندارد.

در عوض، ما از زیرکلاس دیگری از ImplicitlyAnimatedWidget به نام TweenAnimationBuilder استفاده خواهیم کرد که یک Tween به عنوان پارامتر می‌گیرد. tween کلاسی است که دو مقدار ( begin و end ) را می‌گیرد و مقادیر بین آنها را محاسبه می‌کند تا یک انیمیشن بتواند آنها را نمایش دهد. در این مثال، ما از ColorTween استفاده می‌کنیم که Tween برآورده می‌کند. Tween رابط مورد نیاز برای ساخت جلوه انیمیشن ما.

ویجت Icon را انتخاب کنید و از قابلیت «Wrap with Builder» در IDE خود استفاده کنید، نام آن را به TweenAnimationBuilder تغییر دهید. سپس مدت زمان و ColorTween وارد کنید.

lib/scoreboard.dart

class AnimatedStar extends StatelessWidget {
  final bool isActive;
  final Duration _duration = const Duration(milliseconds: 1000);
  final Color _deactivatedColor = Colors.grey.shade400;
  final Color _activatedColor = Colors.yellow.shade700;

  AnimatedStar({super.key, required this.isActive});

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      scale: isActive ? 1.0 : 0.5,
      duration: _duration,
      child: TweenAnimationBuilder(                            // Add from here...
        duration: _duration,
        tween: ColorTween(
          begin: _deactivatedColor,
          end: isActive ? _activatedColor : _deactivatedColor,
        ),
        builder: (context, value, child) {                     // To here.
          return Icon(Icons.star, size: 50, color: value);     // And modify this line.
        },
      ),
    );
  }
}

حالا، برنامه را دوباره اجرا کنید تا انیمیشن جدید را ببینید.

8b0911f4af299a60.gif

توجه داشته باشید که مقدار end ColorTween ما بر اساس مقدار پارامتر isActive تغییر می‌کند. دلیل این امر آن است که TweenAnimationBuilder هر زمان که مقدار Tween.end تغییر کند، انیمیشن خود را دوباره اجرا می‌کند. وقتی این اتفاق می‌افتد، انیمیشن جدید از مقدار انیمیشن فعلی به مقدار پایانی جدید اجرا می‌شود، که به شما امکان می‌دهد رنگ را در هر زمان (حتی در حین اجرای انیمیشن) تغییر دهید و یک جلوه انیمیشن روان با مقادیر صحیح بینابینی نمایش دهید.

اعمال منحنی

هر دوی این جلوه‌های انیمیشن با سرعت ثابتی اجرا می‌شوند، اما انیمیشن‌ها اغلب وقتی سرعتشان کم یا زیاد می‌شود، از نظر بصری جالب‌تر و آموزنده‌تر هستند.

یک Curve یک تابع easing اعمال می‌کند که نرخ تغییر یک پارامتر را در طول زمان تعریف می‌کند. فلاتر (Flutter) مجموعه‌ای از منحنی‌های easing از پیش ساخته شده در کلاس Curves ، مانند easeIn یا easeOut را ارائه می‌دهد.

5dabe68d1210b8a1.gif

3a9e7490c594279a.gif

این نمودارها (که در صفحه مستندات API مربوط به Curves موجود است) سرنخی از نحوه کار منحنی‌ها ارائه می‌دهند. منحنی‌ها یک مقدار ورودی بین ۰.۰ تا ۱.۰ (نمایش داده شده روی محور x) را به یک مقدار خروجی بین ۰.۰ تا ۱.۰ (نمایش داده شده روی محور y) تبدیل می‌کنند. این نمودارها همچنین پیش‌نمایشی از جلوه‌های انیمیشن مختلف هنگام استفاده از منحنی کاهشی را نشان می‌دهند.

یک فیلد جدید در AnimatedStar با نام _curve ایجاد کنید و آن را به عنوان پارامتر به ویجت‌های AnimatedScale و TweenAnimationBuilder ارسال کنید.

lib/scoreboard.dart

class AnimatedStar extends StatelessWidget {
  final bool isActive;
  final Duration _duration = const Duration(milliseconds: 1000);
  final Color _deactivatedColor = Colors.grey.shade400;
  final Color _activatedColor = Colors.yellow.shade700;
  final Curve _curve = Curves.elasticOut;                       // NEW

  AnimatedStar({super.key, required this.isActive});

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      scale: isActive ? 1.0 : 0.5,
      curve: _curve,                                           // NEW
      duration: _duration,
      child: TweenAnimationBuilder(
        curve: _curve,                                         // NEW
        duration: _duration,
        tween: ColorTween(
          begin: _deactivatedColor,
          end: isActive ? _activatedColor : _deactivatedColor,
        ),
        builder: (context, value, child) {
          return Icon(Icons.star, size: 50, color: value);
        },
      ),
    );
  }
}

در این مثال، منحنی elasticOut یک اثر فنری اغراق‌آمیز ایجاد می‌کند که با حرکت فنر شروع می‌شود و تا انتها به تعادل می‌رسد.

8f84142bff312373.gif

برای مشاهده‌ی اعمال این منحنی روی AnimatedSize و TweenAnimationBuilder ، برنامه را مجدداً بارگذاری کنید.

206dd8d9c1fae95.gif

استفاده از DevTools برای فعال کردن انیمیشن‌های کند

برای اشکال‌زدایی هرگونه جلوه انیمیشنی، Flutter DevTools راهی برای کاهش سرعت همه انیمیشن‌ها در برنامه شما فراهم می‌کند تا بتوانید انیمیشن را واضح‌تر ببینید.

برای باز کردن DevTools، مطمئن شوید که برنامه در حالت اشکال‌زدایی (debug mode) اجرا می‌شود و Widget Inspector را با انتخاب آن در نوار ابزار Debug در VSCode یا با انتخاب دکمه Open Flutter DevTools در پنجره ابزار اشکال‌زدایی در IntelliJ/Android Studio باز کنید.

3ce33dc01d096b14.png

363ae0fbcd0c2395.png

پس از باز شدن پنجره‌ی ابزارک‌ها (widget inspector) ، روی دکمه‌ی انیمیشن‌های آهسته (Slow animations) در نوار ابزار کلیک کنید.

adea0a16d01127ad.png

۵. از جلوه‌های انیمیشن صریح استفاده کنید

مانند انیمیشن‌های ضمنی، انیمیشن‌های صریح نیز جلوه‌های انیمیشن از پیش ساخته شده هستند، اما به جای گرفتن یک مقدار هدف، یک شیء Animation را به عنوان پارامتر می‌گیرند. این امر آنها را در موقعیت‌هایی که انیمیشن از قبل توسط یک انتقال ناوبری، AnimatedSwitcher یا AnimationController تعریف شده است، مفید می‌کند.

از یک جلوه انیمیشن صریح استفاده کنید

برای شروع کار با یک جلوه انیمیشن صریح، ویجت Card را با یک AnimatedSwitcher پوشش دهید.

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({required this.question, super.key});

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(                                 // NEW
      duration: const Duration(milliseconds: 300),           // NEW
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),                                                     // NEW
    );
  }
}

AnimatedSwitcher به طور پیش‌فرض از یک افکت محوشدگی متقاطع استفاده می‌کند، اما می‌توانید با استفاده از پارامتر transitionBuilder این مورد را لغو کنید. transition builder ویجت فرزندی را که به AnimatedSwitcher ارسال شده است و یک شیء Animation را ارائه می‌دهد. این یک فرصت عالی برای استفاده از یک انیمیشن صریح است.

برای این کد، اولین انیمیشن صریحی که استفاده خواهیم کرد SlideTransition است که یک Animation<Offset> می‌گیرد که فاصله شروع و پایان را که ویجت‌های ورودی و خروجی بین آنها حرکت می‌کنند، تعریف می‌کند.

Tweenها یک تابع کمکی animate() دارند که هر Animation با اعمال tween به Animation دیگری تبدیل می‌کند. این بدان معناست که یک Tween قابل استفاده برای تبدیل Animation ارائه شده توسط AnimatedSwitcher به یک Animation ، برای ارائه به ویجت SlideTransition .

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({required this.question, super.key});

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(
      transitionBuilder: (child, animation) {               // Add from here...
        final curveAnimation = CurveTween(
          curve: Curves.easeInCubic,
        ).animate(animation);
        final offsetAnimation = Tween<Offset>(
          begin: Offset(-0.1, 0.0),
          end: Offset.zero,
        ).animate(curveAnimation);
        return SlideTransition(position: offsetAnimation, child: child);
      },                                                    // To here.
      duration: const Duration(milliseconds: 300),
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),
    );
  }
}

توجه داشته باشید که این از Tween.animate برای اعمال یک Curve به Animation و سپس تبدیل آن از Tween استفاده می‌کند. Tween که از 0.0 تا 1.0، تا یک Tween متغیر است Tween که روی محور x از -0.1 به 0.0 تغییر می‌کند.

از طرف دیگر، کلاس Animation یک تابع drive() دارد که هر Tween (یا Animatable ) را می‌گیرد و آن را به یک Animation جدید تبدیل می‌کند. این به tween ها اجازه می‌دهد تا "زنجیره‌ای" شوند و کد حاصل را مختصرتر کنند:

lib/question_screen.dart

transitionBuilder: (child, animation) {
  var offsetAnimation = animation
      .drive(CurveTween(curve: Curves.easeInCubic))
      .drive(Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero));
  return SlideTransition(position: offsetAnimation, child: child);
},

مزیت دیگر استفاده از انیمیشن‌های صریح این است که می‌توان آن‌ها را با هم ترکیب کرد. یک انیمیشن صریح دیگر، FadeTransition اضافه کنید که از همان انیمیشن منحنی با قرار دادن ویجت SlideTransition استفاده می‌کند.

lib/question_screen.dart

return AnimatedSwitcher(
  transitionBuilder: (child, animation) {
    final curveAnimation = CurveTween(
      curve: Curves.easeInCubic,
    ).animate(animation);
    final offsetAnimation = Tween<Offset>(
      begin: Offset(-0.1, 0.0),
      end: Offset.zero,
    ).animate(curveAnimation);
    final fadeInAnimation = curveAnimation;                            // NEW
    return FadeTransition(                                             // NEW
      opacity: fadeInAnimation,                                        // NEW
      child: SlideTransition(position: offsetAnimation, child: child), // NEW
    );                                                                 // NEW
  },

سفارشی‌سازی layoutBuilder

ممکن است متوجه یک مشکل کوچک در AnimationSwitcher شوید. وقتی یک QuestionCard به یک سوال جدید تغییر می‌کند، آن را در مرکز فضای موجود در حین اجرای انیمیشن قرار می‌دهد، اما وقتی انیمیشن متوقف می‌شود، ویجت به بالای صفحه می‌رود. این باعث یک انیمیشن نامنظم می‌شود زیرا موقعیت نهایی کارت سوال با موقعیت آن در حین اجرای انیمیشن مطابقت ندارد.

d77de181bdde58f7.gif

برای رفع این مشکل، AnimatedSwitcher یک پارامتر layoutBuilder نیز دارد که می‌تواند برای تعریف طرح‌بندی استفاده شود. از این تابع برای پیکربندی سازنده طرح‌بندی جهت تراز کردن کارت در بالای صفحه استفاده کنید:

lib/question_screen.dart

@override
Widget build(BuildContext context) {
  return AnimatedSwitcher(
    layoutBuilder: (currentChild, previousChildren) {
      return Stack(
        alignment: Alignment.topCenter,
        children: <Widget>[
          ...previousChildren,
          if (currentChild != null) currentChild,
        ],
      );
    },

این کد یک نسخه اصلاح‌شده از defaultLayoutBuilder از کلاس AnimatedSwitcher است، اما به جای Alignment.center از Alignment.topCenter استفاده می‌کند.

خلاصه

  • انیمیشن‌های صریح، جلوه‌های انیمیشنی هستند که یک شیء Animation می‌گیرند (برخلاف ImplicitlyAnimatedWidgets که یک value هدف و duration می‌گیرند).
  • کلاس Animation یک انیمیشن در حال اجرا را نشان می‌دهد، اما یک افکت خاص را تعریف نمی‌کند.
  • برای اعمال Tweens و Curves (با استفاده از CurveTween ) به یک انیمیشن، Tween().animate یا Animation.drive() استفاده کنید.
  • از پارامتر layoutBuilder مربوط به AnimatedSwitcher برای تنظیم نحوه‌ی چیدمان فرزندانش استفاده کنید.

۶. کنترل وضعیت یک انیمیشن

تاکنون، هر انیمیشن به طور خودکار توسط چارچوب اجرا شده است. انیمیشن‌های ضمنی به طور خودکار اجرا می‌شوند و جلوه‌های انیمیشن صریح برای عملکرد صحیح به یک Animation نیاز دارند. در این بخش، یاد خواهید گرفت که چگونه اشیاء Animation خود را با استفاده از AnimationController ایجاد کنید و از TweenSequence برای ترکیب Tween ها با یکدیگر استفاده کنید.

اجرای یک انیمیشن با استفاده از AnimationController

برای ایجاد انیمیشن با استفاده از AnimationController، باید این مراحل را دنبال کنید:

  1. ایجاد یک StatefulWidget
  2. از Mixin SingleTickerProviderStateMixin در کلاس State خود برای ارائه یک Ticker به AnimationController خود استفاده کنید.
  3. AnimationController در متد چرخه حیات initState مقداردهی اولیه کنید و شیء State فعلی را به پارامتر vsync ( TickerProvider ) ارائه دهید.
  4. مطمئن شوید که ویجت شما هر زمان که AnimationController به شنوندگان خود اطلاع می‌دهد، چه با استفاده از AnimatedBuilder و چه با فراخوانی listen() و setState به صورت دستی، دوباره ساخته می‌شود.

یک فایل جدید به flip_effect.dart ایجاد کنید و کد زیر را در آن کپی و پیست کنید:

lib/flip_effect.dart

import 'dart:math' as math;

import 'package:flutter/widgets.dart';

class CardFlipEffect extends StatefulWidget {
  final Widget child;
  final Duration duration;

  const CardFlipEffect({
    super.key,
    required this.child,
    required this.duration,
  });

  @override
  State<CardFlipEffect> createState() => _CardFlipEffectState();
}

class _CardFlipEffectState extends State<CardFlipEffect>
    with SingleTickerProviderStateMixin {
  late final AnimationController _animationController;
  Widget? _previousChild;

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(
      vsync: this,
      duration: widget.duration,
    );

    _animationController.addListener(() {
      if (_animationController.value == 1) {
        _animationController.reset();
      }
    });
  }

  @override
  void didUpdateWidget(covariant CardFlipEffect oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (widget.child.key != oldWidget.child.key) {
      _handleChildChanged(widget.child, oldWidget.child);
    }
  }

  void _handleChildChanged(Widget newChild, Widget previousChild) {
    _previousChild = previousChild;
    _animationController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animationController,
      builder: (context, child) {
        return Transform(
          alignment: Alignment.center,
          transform: Matrix4.identity()
            ..rotateX(_animationController.value * math.pi),
          child: _animationController.isAnimating
              ? _animationController.value < 0.5
                    ? _previousChild
                    : Transform.flip(flipY: true, child: child)
              : child,
        );
      },
      child: widget.child,
    );
  }
}

این کلاس یک AnimationController تنظیم می‌کند و هر زمان که فریم‌ورک didUpdateWidget را فراخوانی کند تا به آن اطلاع دهد که پیکربندی ویجت تغییر کرده است و ممکن است یک ویجت فرزند جدید وجود داشته باشد، انیمیشن را دوباره اجرا می‌کند.

AnimatedBuilder تضمین می‌کند که درخت ویجت هر زمان که AnimationController به شنوندگان خود اطلاع می‌دهد، دوباره ساخته شود و ویجت Transform برای اعمال یک جلوه چرخش سه‌بعدی برای شبیه‌سازی برگرداندن یک کارت استفاده می‌شود.

برای استفاده از این ویجت، هر کارت پاسخ را با یک ویجت CardFlipEffect بپوشانید. حتماً یک key برای ویجت Card ارائه دهید:

lib/question_screen.dart

@override
Widget build(BuildContext context) {
  return GridView.count(
    shrinkWrap: true,
    crossAxisCount: 2,
    childAspectRatio: 5 / 2,
    children: List.generate(answers.length, (index) {
      var color = Theme.of(context).colorScheme.primaryContainer;
      if (correctAnswer == index) {
        color = Theme.of(context).colorScheme.tertiaryContainer;
      }
      return CardFlipEffect(                                    // NEW
        duration: const Duration(milliseconds: 300),            // NEW
        child: Card.filled(                                     // NEW
          key: ValueKey(answers[index]),                        // NEW
          color: color,
          elevation: 2,
          margin: EdgeInsets.all(8),
          clipBehavior: Clip.hardEdge,
          child: InkWell(
            onTap: () => onTapped(index),
            child: Padding(
              padding: EdgeInsets.all(16.0),
              child: Center(
                child: Text(
                  answers.length > index ? answers[index] : '',
                  style: Theme.of(context).textTheme.titleMedium,
                  overflow: TextOverflow.clip,
                ),
              ),
            ),
          ),
        ),                                                      // NEW
      );
    }),
  );
}

اکنون برنامه را مجدداً بارگذاری کنید تا ببینید که کارت‌های پاسخ با استفاده از ویجت CardFlipEffect برگردانده می‌شوند.

5455def725b866f6.gif

ممکن است متوجه شده باشید که این کلاس بسیار شبیه یک جلوه انیمیشن صریح است. در واقع، اغلب ایده خوبی است که کلاس AnimatedWidget را مستقیماً برای پیاده‌سازی نسخه خودتان گسترش دهید. متأسفانه، از آنجایی که این کلاس باید ویجت قبلی را در State خود ذخیره کند، باید از یک StatefulWidget استفاده کند. برای کسب اطلاعات بیشتر در مورد ایجاد جلوه‌های انیمیشن صریح خودتان، به مستندات API مربوط به AnimatedWidget مراجعه کنید.

با استفاده از TweenSequence یک تأخیر اضافه کنید

در این بخش، یک تأخیر به ویجت CardFlipEffect اضافه خواهید کرد تا هر کارت یکی‌یکی ورق بخورد. برای شروع، یک فیلد جدید به نام delayAmount اضافه کنید.

lib/flip_effect.dart

class CardFlipEffect extends StatefulWidget {
  final Widget child;
  final Duration duration;
  final double delayAmount;                      // NEW

  const CardFlipEffect({
    super.key,
    required this.child,
    required this.duration,
    required this.delayAmount,                   // NEW
  });

  @override
  State<CardFlipEffect> createState() => _CardFlipEffectState();
}

سپس delayAmount را به متد build مربوط به AnswerCards اضافه کنید.

lib/question_screen.dart

@override
Widget build(BuildContext context) {
  return GridView.count(
    shrinkWrap: true,
    crossAxisCount: 2,
    childAspectRatio: 5 / 2,
    children: List.generate(answers.length, (index) {
      var color = Theme.of(context).colorScheme.primaryContainer;
      if (correctAnswer == index) {
        color = Theme.of(context).colorScheme.tertiaryContainer;
      }
      return CardFlipEffect(
        delayAmount: index.toDouble() / 2,                     // NEW
        duration: const Duration(milliseconds: 300),
        child: Card.filled(
          key: ValueKey(answers[index]),

سپس در _CardFlipEffectState ، یک Animation جدید ایجاد کنید که با استفاده از TweenSequence تأخیر را اعمال کند. توجه داشته باشید که این Animation از هیچ ابزاری از کتابخانه dart:async ، مانند Future.delayed استفاده نمی‌کند . دلیل این امر این است که تأخیر بخشی از انیمیشن است و چیزی نیست که ویجت هنگام استفاده از AnimationController به صراحت کنترل کند. این امر باعث می‌شود که هنگام فعال کردن انیمیشن‌های کند در DevTools، اشکال‌زدایی جلوه انیمیشن آسان‌تر شود، زیرا از همان TickerProvider استفاده می‌کند.

برای استفاده از TweenSequence ، دو TweenSequenceItem ایجاد کنید، یکی شامل یک ConstantTween که انیمیشن را برای مدت زمان نسبی در 0 نگه می‌دارد و یک Tween معمولی که از 0.0 تا 1.0 متغیر است.

lib/flip_effect.dart

class _CardFlipEffectState extends State<CardFlipEffect>
    with SingleTickerProviderStateMixin {
  late final AnimationController _animationController;
  Widget? _previousChild;
  late final Animation<double> _animationWithDelay;            // NEW

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(
      vsync: this,
      duration: widget.duration * (widget.delayAmount + 1),
    );

    _animationController.addListener(() {
      if (_animationController.value == 1) {
        _animationController.reset();
      }
    });

    _animationWithDelay = TweenSequence<double>([              // Add from here...
      if (widget.delayAmount > 0)
        TweenSequenceItem(
          tween: ConstantTween<double>(0.0),
          weight: widget.delayAmount,
        ),
      TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1.0),
    ]).animate(_animationController);                          // To here.
  }

در نهایت، انیمیشن AnimationController را با انیمیشن جدید با تأخیر در متد build جایگزین کنید.

lib/flip_effect.dart

@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _animationWithDelay,                            // Modify this line
    builder: (context, child) {
      return Transform(
        alignment: Alignment.center,
        transform: Matrix4.identity()
          ..rotateX(_animationWithDelay.value * math.pi),      // And this line
        child: _animationController.isAnimating
            ? _animationWithDelay.value < 0.5                  // And this one.
                  ? _previousChild
                  : Transform.flip(flipY: true, child: child)
            : child,
      );
    },
    child: widget.child,
  );
}

حالا برنامه را دوباره اجرا کنید و ببینید که کارت‌ها یکی یکی ورق می‌خورند. برای چالش بیشتر ، تغییر پرسپکتیو جلوه سه‌بعدی ارائه شده توسط ویجت Transform را امتحان کنید.

28b5291de9b3f55f.gif

۷. از انتقال‌های ناوبری سفارشی استفاده کنید

تاکنون، نحوه سفارشی‌سازی جلوه‌ها در یک صفحه نمایش واحد را دیده‌ایم، اما روش دیگر استفاده از انیمیشن‌ها، استفاده از آنها برای انتقال بین صفحات است. در این بخش، یاد خواهید گرفت که چگونه جلوه‌های انیمیشن را با استفاده از جلوه‌های انیمیشن داخلی و جلوه‌های انیمیشن از پیش ساخته شده که توسط بسته رسمی انیمیشن‌ها در pub.dev ارائه شده است، به انتقال‌های صفحه نمایش اعمال کنید.

متحرک‌سازی یک گذار ناوبری

کلاس PageRouteBuilder یک Route است که به شما امکان می‌دهد انیمیشن انتقال را سفارشی کنید. این کلاس به شما امکان می‌دهد فراخوانی transitionBuilder آن را که دو شیء Animation ارائه می‌دهد، نادیده بگیرید که نشان‌دهنده انیمیشن ورودی و خروجی است که توسط Navigator اجرا می‌شود.

برای سفارشی‌سازی انیمیشن انتقال، MaterialPageRoute را با PageRouteBuilder جایگزین کنید و برای سفارشی‌سازی انیمیشن انتقال هنگام حرکت کاربر از HomeScreen به QuestionScreen ، از FadeTransition (یک ویجت با انیمیشن صریح) استفاده کنید تا صفحه جدید روی صفحه قبلی محو شود.

lib/home_screen.dart

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      PageRouteBuilder(                                         // Add from here...
        pageBuilder: (context, animation, secondaryAnimation) {
          return const QuestionScreen();
        },
        transitionsBuilder:
            (context, animation, secondaryAnimation, child) {
              return FadeTransition(
                opacity: animation,
                child: child,
              );
            },
      ),                                                        // To here.
    );
  },
  child: Text('New Game'),
),

پکیج animations افکت‌های انیمیشنی از پیش ساخته شده‌ی جذابی مانند FadeThroughTransition را ارائه می‌دهد. پکیج animations را وارد کنید و FadeTransition با ویجت FadeThroughTransition جایگزین کنید:

lib/home_screen.dart

import 'package;animations/animations.dart';

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      PageRouteBuilder(
        pageBuilder: (context, animation, secondaryAnimation) {
          return const QuestionScreen();
        },
        transitionsBuilder:
            (context, animation, secondaryAnimation, child) {
              return FadeThroughTransition(                     // Add from here...
                animation: animation,
                secondaryAnimation: secondaryAnimation,
                child: child,
              );                                                // To here.
            },
      ),
    );
  },
  child: Text('New Game'),
),

انیمیشن پیش‌بینانه‌ی بازگشت را سفارشی کنید

1c0558ffa3b76439.gif

قابلیت پیش‌بینی بازگشت، یک ویژگی جدید اندروید است که به کاربر اجازه می‌دهد قبل از پیمایش، نگاهی به پشت مسیر فعلی یا برنامه بیندازد تا ببیند پشت آن چیست. انیمیشن نگاه کردن با توجه به محل انگشت کاربر هنگام کشیدن انگشت به عقب در صفحه نمایش، هدایت می‌شود.

فلاتر با فعال کردن این ویژگی در سطح سیستم، زمانی که فلاتر هیچ مسیری برای نمایش در پشته ناوبری خود ندارد، یا به عبارت دیگر، زمانی که یک بک از برنامه خارج می‌شود، از قابلیت پیش‌بینی سیستم پشتیبانی می‌کند. این انیمیشن توسط سیستم مدیریت می‌شود و نه توسط خود فلاتر.

فلاتر همچنین از پیش‌بینی بازگشت هنگام پیمایش بین مسیرها در یک برنامه فلاتر پشتیبانی می‌کند. یک PageTransitionsBuilder ویژه به نام PredictiveBackPageTransitionsBuilder به حرکات پیش‌بینی بازگشت سیستم گوش می‌دهد و انتقال صفحه خود را با پیشرفت حرکت هدایت می‌کند.

قابلیت پیش‌بینی حرکت برگشت فقط در اندروید U و بالاتر پشتیبانی می‌شود، اما فلاتر به طرز زیبایی به رفتار اصلی حرکت برگشت و ZoomPageTransitionBuilder برمی‌گردد. برای اطلاعات بیشتر، از جمله بخشی در مورد نحوه تنظیم آن در برنامه خود، به پست وبلاگ ما مراجعه کنید.

در پیکربندی ThemeData برای برنامه خود، PageTransitionsTheme طوری پیکربندی کنید که در اندروید از PredictiveBack و در پلتفرم‌های دیگر از افکت محو شدن تدریجی (fade-through transition) از پکیج animations استفاده کند:

lib/main.dart

import 'package:animations/animations.dart';                                 // NEW
import 'package:flutter/material.dart';

import 'home_screen.dart';

void main() {
  runApp(MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        pageTransitionsTheme: PageTransitionsTheme(
          builders: {
            TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),  // NEW
            TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(),         // NEW
            TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(),       // NEW
            TargetPlatform.windows: FadeThroughPageTransitionsBuilder(),     // NEW
            TargetPlatform.linux: FadeThroughPageTransitionsBuilder(),       // NEW
          },
        ),
      ),
      home: HomeScreen(),
    );
  }
}

حالا می‌توانید فراخوانی Navigator.push() را به MaterialPageRoute تغییر دهید.

lib/home_screen.dart

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      MaterialPageRoute(                                        // Add from here...
        builder: (context) {
          return const QuestionScreen();
        },
      ),                                                        // To here.
    );
  },
  child: Text('New Game'),
),

استفاده از FadeThroughTransition برای تغییر سوال فعلی

ویجت AnimatedSwitcher فقط یک Animation در تابع فراخوانی سازنده خود ارائه می‌دهد. برای رفع این مشکل، پکیج animations یک PageTransitionSwitcher ارائه می‌دهد.

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({required this.question, super.key});

  @override
  Widget build(BuildContext context) {
    return PageTransitionSwitcher(                              // Add from here...
      layoutBuilder: (entries) {
        return Stack(alignment: Alignment.topCenter, children: entries);
      },
      transitionBuilder: (child, animation, secondaryAnimation) {
        return FadeThroughTransition(
          animation: animation,
          secondaryAnimation: secondaryAnimation,
          child: child,
        );
      },                                                        // To here.
      duration: const Duration(milliseconds: 300),
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),
    );
  }
}

از OpenContainer استفاده کنید

77358e5776eb104c.png

ویجت OpenContainer از بسته animations یک جلوه انیمیشنی تبدیل کانتینر ارائه می‌دهد که برای ایجاد ارتباط بصری بین دو ویجت گسترش می‌یابد.

ویجت برگردانده شده توسط closedBuilder در ابتدا نمایش داده می‌شود و هنگامی که به کانتینر ضربه زده می‌شود یا فراخوانی تابع openContainer انجام می‌شود، به ویجت برگردانده شده توسط openBuilder گسترش می‌یابد.

برای اتصال تابع فراخوانی openContainer به view-model، یک پاس جدید به viewModel در ویجت QuestionCard اضافه کنید و یک تابع فراخوانی ذخیره کنید که برای نمایش صفحه "Game Over" استفاده خواهد شد:

lib/question_screen.dart

class QuestionScreen extends StatefulWidget {
  const QuestionScreen({super.key});

  @override
  State<QuestionScreen> createState() => _QuestionScreenState();
}

class _QuestionScreenState extends State<QuestionScreen> {
  late final QuizViewModel viewModel = QuizViewModel(
    onGameOver: _handleGameOver,
  );
  VoidCallback? _showGameOverScreen;                                    // NEW

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: viewModel,
      builder: (context, child) {
        return Scaffold(
          appBar: AppBar(
            actions: [
              TextButton(
                onPressed:
                    viewModel.hasNextQuestion && viewModel.didAnswerQuestion
                    ? () {
                        viewModel.getNextQuestion();
                      }
                    : null,
                child: const Text('Next'),
              ),
            ],
          ),
          body: Center(
            child: Column(
              children: [
                QuestionCard(                                           // NEW
                  onChangeOpenContainer: _handleChangeOpenContainer,    // NEW
                  question: viewModel.currentQuestion?.question,        // NEW
                  viewModel: viewModel,                                 // NEW
                ),                                                      // NEW
                Spacer(),
                AnswerCards(
                  onTapped: (index) {
                    viewModel.checkAnswer(index);
                  },
                  answers: viewModel.currentQuestion?.possibleAnswers ?? [],
                  correctAnswer: viewModel.didAnswerQuestion
                      ? viewModel.currentQuestion?.correctAnswer
                      : null,
                ),
                StatusBar(viewModel: viewModel),
              ],
            ),
          ),
        );
      },
    );
  }

  void _handleChangeOpenContainer(VoidCallback openContainer) {        // NEW
    _showGameOverScreen = openContainer;                               // NEW
  }                                                                    // NEW

  void _handleGameOver() {                                             // NEW
    if (_showGameOverScreen != null) {                                 // NEW
      _showGameOverScreen!();                                          // NEW
    }                                                                  // NEW
  }                                                                    // NEW
}

یک ویجت جدید به GameOverScreen اضافه کنید:

lib/question_screen.dart

class GameOverScreen extends StatelessWidget {
  final QuizViewModel viewModel;
  const GameOverScreen({required this.viewModel, super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(automaticallyImplyLeading: false),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Scoreboard(
              score: viewModel.score,
              totalQuestions: viewModel.totalQuestions,
            ),
            Text('You Win!', style: Theme.of(context).textTheme.displayLarge),
            Text(
              'Score: ${viewModel.score} / ${viewModel.totalQuestions}',
              style: Theme.of(context).textTheme.displaySmall,
            ),
            ElevatedButton(
              child: Text('OK'),
              onPressed: () {
                Navigator.popUntil(context, (route) => route.isFirst);
              },
            ),
          ],
        ),
      ),
    );
  }
}

در ویجت QuestionCard ، Card با یک ویجت OpenContainer از بسته animations جایگزین کنید و دو فیلد جدید برای viewModel و فراخوانی open container اضافه کنید:

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({
    required this.onChangeOpenContainer,
    required this.question,
    required this.viewModel,
    super.key,
  });

  final ValueChanged<VoidCallback> onChangeOpenContainer;
  final QuizViewModel viewModel;

  static const _backgroundColor = Color(0xfff2f3fa);

  @override
  Widget build(BuildContext context) {
    return PageTransitionSwitcher(
      duration: const Duration(milliseconds: 200),
      transitionBuilder: (child, animation, secondaryAnimation) {
        return FadeThroughTransition(
          animation: animation,
          secondaryAnimation: secondaryAnimation,
          child: child,
        );
      },
      child: OpenContainer(                                         // NEW
        key: ValueKey(question),                                    // NEW
        tappable: false,                                            // NEW
        closedColor: _backgroundColor,                              // NEW
        closedShape: const RoundedRectangleBorder(                  // NEW
          borderRadius: BorderRadius.all(Radius.circular(12.0)),    // NEW
        ),                                                          // NEW
        closedElevation: 4,                                         // NEW
        closedBuilder: (context, openContainer) {                   // NEW
          onChangeOpenContainer(openContainer);                     // NEW
          return ColoredBox(                                        // NEW
            color: _backgroundColor,                                // NEW
            child: Padding(                                         // NEW
              padding: const EdgeInsets.all(16.0),                  // NEW
              child: Text(
                question ?? '',
                style: Theme.of(context).textTheme.displaySmall,
              ),
            ),
          );
        },
        openBuilder: (context, closeContainer) {                    // NEW
          return GameOverScreen(viewModel: viewModel);              // NEW
        },                                                          // NEW
      ),
    );
  }
}

4120f9395857d218.gif

۸. تبریک

تبریک می‌گوییم، شما با موفقیت جلوه‌های انیمیشن را به یک برنامه Flutter اضافه کردید و با اجزای اصلی سیستم انیمیشن Flutter آشنا شدید. به طور خاص، موارد زیر را آموختید:

  • نحوه استفاده از ImplicitlyAnimatedWidget
  • نحوه استفاده از ExplicitlyAnimatedWidget
  • نحوه اعمال Curves و Tweens به یک انیمیشن
  • نحوه استفاده از ویجت‌های انتقال از پیش ساخته شده مانند AnimatedSwitcher یا PageRouteBuilder
  • نحوه استفاده از جلوه‌های انیمیشنی از پیش ساخته شده از بسته animations ، مانند FadeThroughTransition و OpenContainer
  • نحوه سفارشی‌سازی انیمیشن انتقال پیش‌فرض، از جمله افزودن پشتیبانی از Predictive Back در اندروید.

3026390ad413769c.gif

بعدش چی؟

به برخی از این آزمایشگاه‌های کد نگاهی بیندازید:

یا برنامه نمونه انیمیشن‌ها را دانلود کنید که تکنیک‌های مختلف انیمیشن را نشان می‌دهد.

مطالعه بیشتر

می‌توانید منابع انیمیشن بیشتری را در flutter.dev پیدا کنید:

یا این مقالات را در Medium ببینید:

اسناد مرجع