1. مقدمه
انیمیشن ها روشی عالی برای بهبود تجربه کاربری برنامه شما، انتقال اطلاعات مهم به کاربر، و ساخت اپلیکیشن شما جذاب تر و لذت بخش تر برای استفاده هستند.
مروری بر چارچوب انیمیشن فلاتر
Flutter با ساخت مجدد بخشی از درخت ویجت در هر فریم، جلوه های انیمیشن را نمایش می دهد. این افکت های انیمیشن از پیش ساخته شده و سایر API ها را برای ایجاد و ساخت انیمیشن ها آسان تر می کند.
- انیمیشن های ضمنی افکت های انیمیشن از پیش ساخته شده ای هستند که کل انیمیشن را به صورت خودکار اجرا می کنند. هنگامی که مقدار هدف انیمیشن تغییر می کند، انیمیشن را از مقدار فعلی به مقدار هدف اجرا می کند و هر مقدار را در بین آن نمایش می دهد تا ویجت به آرامی متحرک شود. نمونه هایی از انیمیشن های ضمنی عبارتند از
AnimatedSize
،AnimatedScale
وAnimatedPositioned
. - انیمیشنهای واضح نیز افکتهای انیمیشن از پیش ساخته شدهاند، اما برای کار کردن به یک شی
Animation
نیاز دارند. به عنوان مثال می توان بهSizeTransition
،ScaleTransition
یاPositionedTransition
اشاره کرد. - انیمیشن کلاسی است که یک انیمیشن در حال اجرا یا متوقف شده را نشان می دهد و از مقداری تشکیل شده است که نشان دهنده مقدار هدفی است که انیمیشن روی آن اجرا می شود و وضعیت که نشان دهنده مقدار فعلی است که انیمیشن در هر زمان معین روی صفحه نمایش می دهد. این یک زیر کلاس از
Listenable
است و به شنوندگان خود هنگام تغییر وضعیت در حین اجرا شدن انیمیشن، اطلاع می دهد. - AnimationController راهی برای ایجاد انیمیشن و کنترل وضعیت آن است. روش های آن مانند
forward()
،reset()
،stop()
وrepeat()
را می توان برای کنترل انیمیشن بدون نیاز به تعریف افکت انیمیشنی که نمایش داده می شود، مانند مقیاس، اندازه یا موقعیت استفاده کرد. - Tweens برای درون یابی مقادیر بین یک مقدار آغاز و پایان استفاده می شود و می تواند هر نوع را نشان دهد، مانند دو،
Offset
یاColor
. - منحنی ها برای تنظیم نرخ تغییر یک پارامتر در طول زمان استفاده می شوند. هنگامی که یک انیمیشن اجرا می شود، معمول است که یک منحنی کاهش را اعمال کنید تا سرعت تغییر در ابتدا یا انتهای انیمیشن سریعتر یا کندتر شود. منحنی ها یک مقدار ورودی بین 0.0 و 1.0 می گیرند و یک مقدار خروجی بین 0.0 و 1.0 برمی گردند.
چیزی که خواهی ساخت
در این کد لبه، شما قصد دارید یک بازی مسابقه چند گزینه ای بسازید که دارای افکت ها و تکنیک های مختلف انیمیشن است.
خواهید دید که چگونه ...
- ویجتی بسازید که اندازه و رنگ آن را متحرک کند
- یک جلوه برگردان کارت سه بعدی بسازید
- از جلوه های انیمیشن فانتزی از پیش ساخته شده از بسته انیمیشن استفاده کنید
- پشتیبانی پیشبینی حرکت برگشتی را که در آخرین نسخه اندروید موجود است اضافه کنید
چیزی که یاد خواهید گرفت
در این کد لبه یاد خواهید گرفت:
- نحوه استفاده از افکتهای متحرک ضمنی برای دستیابی به انیمیشنهای عالی بدون نیاز به کد زیاد.
- نحوه استفاده از جلوه های متحرک صریح برای پیکربندی جلوه های خود با استفاده از ویجت های متحرک از پیش ساخته شده مانند
AnimatedSwitcher
یاAnimationController
. - نحوه استفاده از
AnimationController
برای تعریف ویجت خود که یک افکت سه بعدی را نمایش می دهد. - نحوه استفاده از بسته
animations
برای نمایش جلوه های انیمیشن فانتزی با حداقل تنظیمات.
آنچه شما نیاز دارید
- فلاتر SDK
- یک IDE، مانند VSCode یا Android Studio / IntelliJ
2. محیط توسعه Flutter خود را تنظیم کنید
برای تکمیل این آزمایشگاه به دو نرم افزار نیاز دارید - Flutter SDK و یک ویرایشگر .
شما می توانید کدلب را با استفاده از هر یک از این دستگاه ها اجرا کنید:
- یک Android فیزیکی ( توصیه شده برای اجرای پیشبینی در مرحله 7 ) یا دستگاه iOS که به رایانه شما متصل شده و روی حالت برنامهنویس تنظیم شده است.
- شبیه ساز iOS (نیاز به نصب ابزار Xcode دارد).
- شبیه ساز اندروید (نیاز به نصب در Android Studio دارد).
- یک مرورگر (Chrome برای اشکال زدایی لازم است).
- یک رایانه رومیزی Windows ، Linux ، یا macOS . شما باید روی پلتفرمی که قصد استقرار در آن را دارید توسعه دهید. بنابراین، اگر می خواهید یک برنامه دسکتاپ ویندوز توسعه دهید، باید در ویندوز توسعه دهید تا به زنجیره ساخت مناسب دسترسی داشته باشید. الزامات خاص سیستم عامل وجود دارد که به طور مفصل در docs.flutter.dev/desktop پوشش داده شده است.
نصب خود را تأیید کنید
برای تأیید اینکه Flutter 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!
3. برنامه استارتر را اجرا کنید
برنامه شروع را دانلود کنید
از git
برای کلون کردن برنامه شروع از مخزن flutter/samples در GitHub استفاده کنید.
$ git clone https://github.com/flutter/codelabs.git $ cd codelabs/animations/step_01/
همچنین، میتوانید کد منبع را بهعنوان یک فایل zip. دانلود کنید .
برنامه را اجرا کنید
برای اجرای برنامه، از دستور flutter run
استفاده کنید و دستگاه مورد نظر مانند android
، ios
یا chrome
را مشخص کنید. برای لیست کامل پلتفرم های پشتیبانی شده، صفحه پلتفرم های پشتیبانی شده را ببینید.
$ flutter run -d android
همچنین می توانید برنامه را با استفاده از IDE انتخابی خود اجرا و اشکال زدایی کنید. برای اطلاعات بیشتر به مستندات رسمی فلاتر مراجعه کنید.
کد را بگردید
برنامه شروع یک بازی مسابقه چند گزینه ای است که از دو صفحه تشکیل شده است که از الگوی طراحی model-view-view-model یا MVVM پیروی می کنند. QuestionScreen
(View) از کلاس QuizViewModel
(View-Model) برای پرسیدن سوالات چند گزینه ای از کلاس QuestionBank
(Model) از کاربر استفاده می کند.
- home_screen.dart - صفحه ای را با دکمه بازی جدید نمایش می دهد
- main.dart -
MaterialApp
را برای استفاده از Material 3 و نمایش صفحه اصلی پیکربندی می کند - model.dart - کلاس های اصلی مورد استفاده در برنامه را تعریف می کند
- question_screen.dart - رابط کاربری بازی مسابقه را نشان می دهد
- view_model.dart - وضعیت و منطق بازی مسابقه را ذخیره می کند که توسط
QuestionScreen
نمایش داده می شود.
این برنامه هنوز هیچ افکت متحرکی را پشتیبانی نمی کند، به جز تغییر نمای پیش فرض که توسط کلاس Flutter's Navigator
نمایش داده می شود زمانی که کاربر دکمه بازی جدید را فشار می دهد.
4. از جلوه های انیمیشن ضمنی استفاده کنید
انیمیشن های ضمنی در بسیاری از موقعیت ها یک انتخاب عالی هستند، زیرا نیازی به پیکربندی خاصی ندارند. در این بخش، ویجت 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( // NEW
isActive: score > i, // NEW
) // NEW
],
),
);
}
}
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<Color>
مورد نیاز برای ساخت افکت انیمیشن ما را برآورده می کند.
ویجت 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, // Modify from here...
);
}, // To here.
),
);
}
}
اکنون، برنامه را دوباره بارگیری کنید تا انیمیشن جدید را ببینید.
توجه داشته باشید که مقدار end
ColorTween
ما بر اساس مقدار پارامتر isActive
تغییر می کند. این به این دلیل است که TweenAnimationBuilder
هر زمان که مقدار Tween.end
تغییر کند انیمیشن خود را دوباره اجرا می کند. هنگامی که این اتفاق میافتد، انیمیشن جدید از مقدار فعلی انیمیشن به مقدار پایانی جدید اجرا میشود، که به شما امکان میدهد رنگ را در هر زمان (حتی زمانی که انیمیشن در حال اجرا است) تغییر دهید و یک افکت انیمیشن صاف با مقادیر صحیح بین نمایش دهید. .
منحنی را اعمال کنید
هر دوی این افکتهای متحرک با سرعت ثابتی اجرا میشوند، اما انیمیشنها اغلب از نظر بصری جالبتر و آموزندهتر هستند که سرعت آنها افزایش یا کاهش یابد.
یک Curve
یک تابع کاهش را اعمال می کند، که نرخ تغییر یک پارامتر را در طول زمان تعریف می کند. فلاتر با مجموعهای از منحنیهای از پیش ساخته شده در کلاس Curves
، مانند easeIn
یا easeOut
عرضه میشود.
این نمودارها (در صفحه مستندات Curves
API موجود است) سرنخی از نحوه کار منحنی ها ارائه می دهد. منحنی ها یک مقدار ورودی بین 0.0 و 1.0 (نمایش داده شده در محور x) را به یک مقدار خروجی بین 0.0 و 1.0 (نمایش داده شده در محور 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 در VSCode یا با انتخاب دکمه Open Flutter DevTools در پنجره Debug tool در IntelliJ / Android Studio، Widget Inspector را باز کنید.
هنگامی که بازرس ویجت باز شد، روی دکمه Slow animations در نوار ابزار کلیک کنید.
5. از جلوه های انیمیشن صریح استفاده کنید
مانند انیمیشن های ضمنی، انیمیشن های صریح افکت های انیمیشن از پیش ساخته شده اند، اما به جای گرفتن یک مقدار هدف، یک شی 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
آن را لغو کنید. سازنده انتقال، ویجت فرزند را که به AnimatedSwitcher
ارسال شده و یک شی Animation
ارائه میکند. این یک فرصت عالی برای استفاده از یک انیمیشن واضح است.
برای این نرم افزار کد، اولین انیمیشن صریح که استفاده خواهیم کرد SlideTransition
است، که یک Animation<Offset>
را می گیرد که افست شروع و پایان را تعریف می کند که ویجت های ورودی و خروجی بین آنها حرکت می کنند.
توئین ها یک تابع کمکی animate()
دارند که هر Animation
با استفاده از tween به Animation
دیگری تبدیل می کند. این بدان معناست که یک Tween<Offset>
می تواند برای تبدیل Animation<double>
ارائه شده توسط AnimatedSwitcher
به Animation<Offset>
برای ارائه به ویجت 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<double>
که از 0.0 به 1.0 متغیر است، به T ween<Offset>
که از 0.1- به 0.0 در x تغییر می کند، استفاده می کند. -محور
از طرف دیگر، کلاس Animation یک تابع drive()
دارد که هر Tween
(یا Animatable
) را می گیرد و آن را به یک Animation
جدید تبدیل می کند. این اجازه می دهد تا توئین ها "زنجیره شوند" و کد حاصل را مختصرتر می کند:
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 به یک سؤال جدید تغییر می کند، آن را در مرکز فضای موجود در حالی که انیمیشن در حال اجرا است قرار می دهد، اما وقتی انیمیشن متوقف می شود، ویجت به بالای صفحه می چسبد. این باعث ایجاد انیمیشن janky می شود زیرا موقعیت نهایی کارت سؤال با موقعیت زمانی که انیمیشن در حال اجرا است مطابقت ندارد.
برای رفع این مشکل، AnimatedSwitcher یک پارامتر layoutBuilder نیز دارد که می توان از آن برای تعریف layout استفاده کرد. از این تابع برای پیکربندی layout builder برای تراز کردن کارت با بالای صفحه استفاده کنید:
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.topCenter
به جای Alignment.center
استفاده می کند.
خلاصه
- انیمیشنهای واضح افکتهای انیمیشنی هستند که یک شی Animation را میگیرند (بر خلاف ImplicitlyAnimatedWidgets که مقدار و مدت زمان هدف را میگیرند)
- کلاس Animation یک انیمیشن در حال اجرا را نشان می دهد، اما افکت خاصی را تعریف نمی کند.
- از Tween().animate یا Animation.drive() برای اعمال Tweens و Curves (با استفاده از CurveTween) روی یک انیمیشن استفاده کنید.
- از پارامتر layoutBuilder AnimatedSwitcher برای تنظیم نحوه چیدمان فرزندان خود استفاده کنید.
6. کنترل وضعیت یک انیمیشن
تاکنون هر انیمیشن به صورت خودکار توسط فریمورک اجرا شده است. انیمیشنهای ضمنی بهطور خودکار اجرا میشوند، و افکتهای انیمیشن صریح به یک انیمیشن نیاز دارند تا به درستی کار کنند. در این بخش، یاد خواهید گرفت که چگونه با استفاده از AnimationController، اشیاء انیمیشن خود را ایجاد کنید و از TweenSequence برای ترکیب Tweens با یکدیگر استفاده کنید.
با استفاده از AnimationController یک انیمیشن را اجرا کنید
برای ایجاد یک انیمیشن با استفاده از AnimationController، باید این مراحل را دنبال کنید:
- یک StatefulWidget ایجاد کنید
- از میکسین 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
برای ویجت کارت ارائه کرده اید:
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 را مستقیماً برای پیاده سازی نسخه خود گسترش دهید. متأسفانه، از آنجایی که این کلاس باید ویجت قبلی را در وضعیت خود ذخیره کند، باید از 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
را به روش ساخت 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
، یک انیمیشن جدید ایجاد کنید که تاخیر را با استفاده از TweenSequence
اعمال می کند. توجه داشته باشید که این از هیچ ابزاری از کتابخانه 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>([ // NEW
if (widget.delayAmount > 0) // NEW
TweenSequenceItem( // NEW
tween: ConstantTween<double>(0.0), // NEW
weight: widget.delayAmount, // NEW
), // NEW
TweenSequenceItem( // NEW
tween: Tween(begin: 0.0, end: 1.0), // NEW
weight: 1.0, // NEW
), // NEW
]).animate(_animationController); // NEW
}
در نهایت انیمیشن AnimationController را با انیمیشن تاخیری جدید در متد ساخت جایگزین کنید.
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
آزمایش کنید.
7. از انتقال ناوبری سفارشی استفاده کنید
تا کنون، نحوه سفارشی سازی افکت ها را روی یک صفحه نمایش دیده ایم، اما راه دیگر برای استفاده از انیمیشن ها استفاده از آنها برای انتقال بین صفحه نمایش ها است. در این بخش، نحوه اعمال افکت های انیمیشن را بر روی تغییر صفحه نمایش با استفاده از جلوه های انیمیشن داخلی و جلوه های انیمیشن از پیش ساخته شده فانتزی ارائه شده توسط بسته رسمی انیمیشن ها در pub.dev خواهید آموخت.
یک انتقال ناوبری را متحرک کنید
کلاس PageRouteBuilder
Route
است که به شما امکان می دهد انیمیشن انتقال را سفارشی کنید. این امکان را به شما می دهد تا فراخوانی transitionBuilder
خود را لغو کنید، که دو شیء انیمیشن را ارائه می دهد، که نشان دهنده انیمیشن ورودی و خروجی است که توسط Navigator اجرا می شود.
برای سفارشی کردن انیمیشن انتقال، MaterialPageRoute
با یک PageRouteBuilder
جایگزین کنید و زمانی که کاربر از HomeScreen
به QuestionScreen
حرکت می کند، انیمیشن انتقال را سفارشی کنید. از FadeTransition (یک ویجت متحرک صریحا) استفاده کنید تا صفحه جدید در بالای صفحه قبلی محو شود.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // NEW
pageBuilder: (context, animation, secondaryAnimation) { // NEW
return QuestionScreen(); // NEW
}, // NEW
transitionsBuilder: // NEW
(context, animation, secondaryAnimation, child) { // NEW
return FadeTransition( // NEW
opacity: animation, // NEW
child: child, // NEW
); // NEW
}, // NEW
), // NEW
);
},
child: Text('New Game'),
),
بسته انیمیشن ها جلوه های انیمیشن فانتزی از پیش ساخته شده مانند FadeThroughTransition را ارائه می دهد. بسته انیمیشن ها را وارد کنید و 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( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
},
),
);
},
child: Text('New Game'),
),
سفارشی کردن انیمیشن پیشگویانه
پیشبینیکننده یک ویژگی جدید اندروید است که به کاربر این امکان را میدهد تا قبل از پیمایش، پشت مسیر یا برنامه فعلی را نگاه کند تا ببیند پشت آن چه چیزی وجود دارد. انیمیشن زیرچشمی توسط مکان انگشت کاربر در حالی که روی صفحه به عقب کشیده می شود هدایت می شود.
Flutter با فعال کردن این ویژگی در سطح سیستم، زمانی که Flutter هیچ مسیری برای نمایش در پشته ناوبری خود ندارد، یا به عبارت دیگر، زمانی که پشتی از برنامه خارج میشود، از بازگشت پیشبینی سیستم پشتیبانی میکند. این انیمیشن توسط سیستم مدیریت می شود نه توسط خود Flutter.
Flutter همچنین هنگام پیمایش بین مسیرها در یک برنامه Flutter از برگشت پیش بینی کننده پشتیبانی می کند. یک PageTransitionsBuilder ویژه به نام PredictiveBackPageTransitionsBuilder
به ژستهای برگشتی پیشبینیکننده سیستم گوش میدهد و تغییر صفحه خود را با پیشرفت حرکت انجام میدهد.
پیشبینیکننده فقط در Android U و بالاتر پشتیبانی میشود، اما Flutter بهخوبی به رفتار اشارهای برگشت و ZoomPageTransitionBuilder برمیگردد. برای اطلاعات بیشتر، از جمله بخشی در مورد نحوه تنظیم آن در برنامه خود، به پست وبلاگ ما مراجعه کنید.
در پیکربندی ThemeData برای برنامه خود، PageTransitionsTheme را برای استفاده از PredictiveBack در Android و جلوه انتقال محو از بسته انیمیشن ها در سایر پلتفرم ها پیکربندی کنید:
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),
useMaterial3: true,
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(builder: (context) { // NEW
return const QuestionScreen(); // NEW
}), // NEW
);
},
child: Text('New Game'),
),
برای تغییر سوال فعلی از FadeThroughTransition استفاده کنید
ویجت AnimatedSwitcher تنها یک انیمیشن در پاسخ به تماس سازنده خود ارائه می دهد. برای رفع این مشکل، بسته 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( // NEW
layoutBuilder: (entries) { // NEW
return Stack( // NEW
alignment: Alignment.topCenter, // NEW
children: entries, // NEW
); // NEW
}, // NEW
transitionBuilder: (child, animation, secondaryAnimation) { // NEW
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
}, // NEW
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 اضافه کنید و یک callback ذخیره کنید که برای نمایش صفحه "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، کارت را با یک ویجت OpenContainer از بسته انیمیشنها جایگزین کنید، و دو فیلد جدید برای viewModel و باز تماس کانتینر اضافه کنید:
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
),
);
}
}
8. تبریک می گویم
تبریک میگوییم، شما با موفقیت افکتهای انیمیشن را به برنامه Flutter اضافه کردید و در مورد اجزای اصلی سیستم انیمیشن Flutter یاد گرفتید. به طور خاص، شما یاد گرفتید:
- نحوه استفاده از ویجت ImplicitlyAnimated
- نحوه استفاده از ویجت Explicitly Animated
- نحوه اعمال Curves و Tweens در یک انیمیشن
- نحوه استفاده از ویجت های انتقال از پیش ساخته شده مانند AnimatedSwitcher یا PageRouteBuilder
- نحوه استفاده از جلوه های انیمیشن فانتزی از پیش ساخته شده از بسته
animations
، مانند FadeThroughTransition و OpenContainer - نحوه سفارشی کردن انیمیشن انتقال پیشفرض، از جمله افزودن پشتیبانی از Predictive Back در اندروید.
بعدش چی؟
برخی از این کدها را بررسی کنید:
- ساخت یک طرح بندی برنامه پاسخگو متحرک با Material 3
- ساختن ترانزیشن های زیبا با حرکت مواد برای فلاتر
- برنامه Flutter خود را از خسته کننده به زیبا تبدیل کنید
یا برنامه نمونه انیمیشن را دانلود کنید که تکنیک های مختلف انیمیشن را به نمایش می گذارد
در ادامه مطلب
می توانید منابع انیمیشن های بیشتری را در flutter.dev بیابید:
- مقدمه ای بر انیمیشن ها
- آموزش انیمیشن (آموزش)
- انیمیشن های ضمنی (آموزش)
- متحرک کردن خواص ظرف (کتاب آشپزی)
- محو کردن یک ویجت در داخل و خارج (کتاب آشپزی)
- انیمیشن های قهرمان
- متحرک سازی انتقال مسیر صفحه (کتاب آشپزی)
- متحرک سازی یک ویجت با استفاده از شبیه سازی فیزیک (کتاب آشپزی)
- انیمیشن های مبهم
- ویجت های انیمیشن و حرکت (کاتالوگ ویجت)
یا این مقالات را در Medium بررسی کنید:
- انیمیشن شیرجه عمیق
- انیمیشن های ضمنی سفارشی در Flutter
- مدیریت انیمیشن با Flutter و Flux / Redux
- چگونه انتخاب کنید کدام ویجت انیمیشن Flutter برای شما مناسب است؟
- انیمیشن های جهت دار با انیمیشن های واضح داخلی
- اصول اولیه انیمیشن فلوتر با انیمیشن های ضمنی
- چه زمانی باید از AnimatedBuilder یا AnimatedWidget استفاده کنم؟