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(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
یک تابع کاهش را اعمال می کند، که نرخ تغییر یک پارامتر را در طول زمان تعریف می کند. Flutter با مجموعهای از منحنیهای از پیش ساخته شده در کلاس 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
می توان برای تبدیل Animation
استفاده کرد 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
که از 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
کهvalue
وduration
هدف را میگیرند) - کلاس
Animation
یک انیمیشن در حال اجرا را نشان می دهد، اما افکت خاصی را تعریف نمی کند. -
Tween().animate
یاAnimation.drive()
برای اعمالTweens
وCurves
(با استفاده ازCurveTween
) روی یک انیمیشن استفاده کنید. - از پارامتر
layoutBuilder
AnimatedSwitcher
برای تنظیم نحوه چیدمان فرزندان خود استفاده کنید.
6. کنترل وضعیت یک انیمیشن
تاکنون هر انیمیشن به صورت خودکار توسط فریمورک اجرا شده است. انیمیشنهای ضمنی بهطور خودکار اجرا میشوند، و افکتهای انیمیشن صریح به یک Animation
نیاز دارند تا به درستی کار کنند. در این بخش، یاد خواهید گرفت که چگونه با استفاده از AnimationController
، اشیاء Animation
خود را ایجاد کنید و از TweenSequence
برای ترکیب Tween
با یکدیگر استفاده کنید.
با استفاده از 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
برای ویجت 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
به روش ساخت 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
اعمال می کند. توجه داشته باشید که این از هیچ ابزاری از کتابخانه 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
آزمایش کنید.
7. از انتقال ناوبری سفارشی استفاده کنید
تا کنون، نحوه سفارشی سازی افکت ها را روی یک صفحه نمایش دیده ایم، اما راه دیگر برای استفاده از انیمیشن ها استفاده از آنها برای انتقال بین صفحه نمایش ها است. در این بخش، نحوه اعمال افکتهای انیمیشن را بر روی تغییر صفحهنمایش با استفاده از جلوههای انیمیشن داخلی و جلوههای انیمیشن فانتزی از پیش ساخته شده ارائهشده توسط بسته رسمی انیمیشنها در 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'),
),
بسته انیمیشن ها جلوه های انیمیشن فانتزی از پیش ساخته مانند 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( // Add from here...
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
); // To here.
},
),
);
},
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),
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
اضافه کنید و یک 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
، Card
با یک ویجت OpenContainer
از بسته animations
جایگزین کنید، و دو فیلد جدید برای 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 یاد گرفتید. به طور خاص، شما یاد گرفتید:
- نحوه استفاده از ویجت
ImplicitlyAnimatedWidget
- نحوه استفاده از ویجت
ExplicitlyAnimatedWidget
- نحوه اعمال
Curves
وTweens
در یک انیمیشن - نحوه استفاده از ویجت های انتقال از پیش ساخته شده مانند
AnimatedSwitcher
یاPageRouteBuilder
- نحوه استفاده از جلوه های انیمیشن فانتزی از پیش ساخته شده از بسته
animations
، مانندFadeThroughTransition
وOpenContainer
- نحوه سفارشی کردن انیمیشن انتقال پیشفرض، از جمله افزودن پشتیبانی از Predictive Back در اندروید.
بعدش چی؟
برخی از این کدها را بررسی کنید:
- ساخت یک طرح بندی برنامه پاسخگو متحرک با Material 3
- ساختن ترانزیشن های زیبا با حرکت مواد برای فلاتر
- برنامه Flutter خود را از خسته کننده به زیبا تبدیل کنید
یا برنامه نمونه انیمیشن را دانلود کنید که تکنیک های مختلف انیمیشن را به نمایش می گذارد.
در ادامه مطلب
می توانید منابع انیمیشن های بیشتری را در flutter.dev بیابید:
- مقدمه ای بر انیمیشن ها
- آموزش انیمیشن (آموزش)
- انیمیشن های ضمنی (آموزش)
- متحرک کردن خواص ظرف (کتاب آشپزی)
- محو کردن یک ویجت در داخل و خارج (کتاب آشپزی)
- انیمیشن های قهرمان
- متحرک سازی انتقال مسیر صفحه (کتاب آشپزی)
- متحرک سازی یک ویجت با استفاده از شبیه سازی فیزیک (کتاب آشپزی)
- انیمیشن های مبهم
- ویجت های انیمیشن و حرکت (کاتالوگ ویجت)
یا این مقالات را در Medium بررسی کنید:
- انیمیشن شیرجه عمیق
- انیمیشن های ضمنی سفارشی در Flutter
- مدیریت انیمیشن با Flutter و Flux / Redux
- چگونه انتخاب کنید کدام ویجت انیمیشن Flutter برای شما مناسب است؟
- انیمیشن های جهت دار با انیمیشن های واضح داخلی
- اصول اولیه انیمیشن فلوتر با انیمیشن های ضمنی
- چه زمانی باید از AnimatedBuilder یا AnimatedWidget استفاده کنم؟