۱. مقدمه
انیمیشنها راهی عالی برای بهبود تجربه کاربری برنامه شما، انتقال اطلاعات مهم به کاربر و جذابتر و لذتبخشتر کردن استفاده از برنامه شما هستند.
مروری بر چارچوب انیمیشن فلاتر
فلاتر با بازسازی بخشی از درخت ویجت در هر فریم، جلوههای انیمیشن را نمایش میدهد. این فریمورک جلوههای انیمیشن از پیش ساخته شده و سایر 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 برمیگردانند.
آنچه خواهید ساخت
در این آزمایشگاه کد، شما یک بازی مسابقه چند گزینهای خواهید ساخت که دارای جلوهها و تکنیکهای انیمیشنی متنوعی است.

خواهید دید که چگونه میتوان...
- ساخت ویجتی که اندازه و رنگ خود را متحرک میکند
- ساخت افکت ورق زدن کارت سه بعدی
- از جلوههای انیمیشنی از پیش ساخته شدهی فانتزی از بستهی انیمیشنها استفاده کنید
- پشتیبانی از ژست حرکتی پیشبینیکننده برای بازگشت به عقب که در آخرین نسخه اندروید موجود است، اضافه شد.
آنچه یاد خواهید گرفت
در این آزمایشگاه کد یاد خواهید گرفت:
- نحوه استفاده از جلوههای انیمیشن ضمنی برای دستیابی به انیمیشنهای زیبا بدون نیاز به کد زیاد.
- نحوه استفاده از جلوههای انیمیشنی صریح برای پیکربندی جلوههای خودتان با استفاده از ویجتهای انیمیشنی از پیش ساخته شده مانند
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نمایش داده میشود، ذخیره میکند.

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

ویجت جدول امتیازات بدون انیمیشن را ایجاد کنید
یک فایل جدید به 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 انجام میشود.

استفاده از 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.
},
),
);
}
}
حالا، برنامه را دوباره اجرا کنید تا انیمیشن جدید را ببینید.

توجه داشته باشید که مقدار end ColorTween ما بر اساس مقدار پارامتر isActive تغییر میکند. دلیل این امر آن است که TweenAnimationBuilder هر زمان که مقدار Tween.end تغییر کند، انیمیشن خود را دوباره اجرا میکند. وقتی این اتفاق میافتد، انیمیشن جدید از مقدار انیمیشن فعلی به مقدار پایانی جدید اجرا میشود، که به شما امکان میدهد رنگ را در هر زمان (حتی در حین اجرای انیمیشن) تغییر دهید و یک جلوه انیمیشن روان با مقادیر صحیح بینابینی نمایش دهید.
اعمال منحنی
هر دوی این جلوههای انیمیشن با سرعت ثابتی اجرا میشوند، اما انیمیشنها اغلب وقتی سرعتشان کم یا زیاد میشود، از نظر بصری جالبتر و آموزندهتر هستند.
یک Curve یک تابع easing اعمال میکند که نرخ تغییر یک پارامتر را در طول زمان تعریف میکند. فلاتر (Flutter) مجموعهای از منحنیهای easing از پیش ساخته شده در کلاس Curves ، مانند easeIn یا easeOut را ارائه میدهد.


این نمودارها (که در صفحه مستندات 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 یک اثر فنری اغراقآمیز ایجاد میکند که با حرکت فنر شروع میشود و تا انتها به تعادل میرسد.

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

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


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

۵. از جلوههای انیمیشن صریح استفاده کنید
مانند انیمیشنهای ضمنی، انیمیشنهای صریح نیز جلوههای انیمیشن از پیش ساخته شده هستند، اما به جای گرفتن یک مقدار هدف، یک شیء 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 به یک سوال جدید تغییر میکند، آن را در مرکز فضای موجود در حین اجرای انیمیشن قرار میدهد، اما وقتی انیمیشن متوقف میشود، ویجت به بالای صفحه میرود. این باعث یک انیمیشن نامنظم میشود زیرا موقعیت نهایی کارت سوال با موقعیت آن در حین اجرای انیمیشن مطابقت ندارد.

برای رفع این مشکل، 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، باید این مراحل را دنبال کنید:
- ایجاد یک
StatefulWidget - از Mixin
SingleTickerProviderStateMixinدر کلاسStateخود برای ارائه یکTickerبهAnimationControllerخود استفاده کنید. -
AnimationControllerدر متد چرخه حیاتinitStateمقداردهی اولیه کنید و شیءStateفعلی را به پارامترvsync(TickerProvider) ارائه دهید. - مطمئن شوید که ویجت شما هر زمان که
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 برگردانده میشوند.

ممکن است متوجه شده باشید که این کلاس بسیار شبیه یک جلوه انیمیشن صریح است. در واقع، اغلب ایده خوبی است که کلاس 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 را امتحان کنید.

۷. از انتقالهای ناوبری سفارشی استفاده کنید
تاکنون، نحوه سفارشیسازی جلوهها در یک صفحه نمایش واحد را دیدهایم، اما روش دیگر استفاده از انیمیشنها، استفاده از آنها برای انتقال بین صفحات است. در این بخش، یاد خواهید گرفت که چگونه جلوههای انیمیشن را با استفاده از جلوههای انیمیشن داخلی و جلوههای انیمیشن از پیش ساخته شده که توسط بسته رسمی انیمیشنها در 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'),
),
انیمیشن پیشبینانهی بازگشت را سفارشی کنید

قابلیت پیشبینی بازگشت، یک ویژگی جدید اندروید است که به کاربر اجازه میدهد قبل از پیمایش، نگاهی به پشت مسیر فعلی یا برنامه بیندازد تا ببیند پشت آن چیست. انیمیشن نگاه کردن با توجه به محل انگشت کاربر هنگام کشیدن انگشت به عقب در صفحه نمایش، هدایت میشود.
فلاتر با فعال کردن این ویژگی در سطح سیستم، زمانی که فلاتر هیچ مسیری برای نمایش در پشته ناوبری خود ندارد، یا به عبارت دیگر، زمانی که یک بک از برنامه خارج میشود، از قابلیت پیشبینی سیستم پشتیبانی میکند. این انیمیشن توسط سیستم مدیریت میشود و نه توسط خود فلاتر.
فلاتر همچنین از پیشبینی بازگشت هنگام پیمایش بین مسیرها در یک برنامه فلاتر پشتیبانی میکند. یک 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 استفاده کنید

ویجت 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
),
);
}
}

۸. تبریک
تبریک میگوییم، شما با موفقیت جلوههای انیمیشن را به یک برنامه Flutter اضافه کردید و با اجزای اصلی سیستم انیمیشن Flutter آشنا شدید. به طور خاص، موارد زیر را آموختید:
- نحوه استفاده از
ImplicitlyAnimatedWidget - نحوه استفاده از
ExplicitlyAnimatedWidget - نحوه اعمال
CurvesوTweensبه یک انیمیشن - نحوه استفاده از ویجتهای انتقال از پیش ساخته شده مانند
AnimatedSwitcherیاPageRouteBuilder - نحوه استفاده از جلوههای انیمیشنی از پیش ساخته شده از بسته
animations، مانندFadeThroughTransitionوOpenContainer - نحوه سفارشیسازی انیمیشن انتقال پیشفرض، از جمله افزودن پشتیبانی از Predictive Back در اندروید.

بعدش چی؟
به برخی از این آزمایشگاههای کد نگاهی بیندازید:
- ساخت یک طرحبندی اپلیکیشن واکنشگرای متحرک با Material 3
- ساخت ترنزیشنهای زیبا با حرکت متریال برای فلاتر
- برنامه Flutter خود را از کسل کننده به زیبا تبدیل کنید
یا برنامه نمونه انیمیشنها را دانلود کنید که تکنیکهای مختلف انیمیشن را نشان میدهد.
مطالعه بیشتر
میتوانید منابع انیمیشن بیشتری را در flutter.dev پیدا کنید:
- مقدمهای بر انیمیشنها
- آموزش انیمیشن (آموزش)
- انیمیشنهای ضمنی (آموزش)
- متحرکسازی ویژگیهای یک ظرف (کتاب آشپزی)
- محو کردن و محو کردن ویجت (کتاب آشپزی)
- انیمیشنهای قهرمان
- متحرکسازی گذار مسیر صفحه (کتاب آشپزی)
- متحرکسازی یک ویجت با استفاده از شبیهسازی فیزیک (کتاب آشپزی)
- انیمیشنهای پلکانی
- ابزارکهای انیمیشن و حرکت (کاتالوگ ابزارک)
یا این مقالات را در Medium ببینید:
- انیمیشن غواصی عمیق
- انیمیشنهای ضمنی سفارشی در فلاتر
- مدیریت انیمیشن با Flutter و Flux/Redux
- چگونه ویجت انیمیشن فلاتر مناسب خود را انتخاب کنیم؟
- انیمیشنهای جهتدار با انیمیشنهای صریح داخلی
- اصول اولیه انیمیشن فلاتر با انیمیشنهای ضمنی
- چه زمانی باید از AnimatedBuilder یا AnimatedWidget استفاده کنم؟