1. قبل از شروع
Flutter در این امکان عالی است که توسعه دهندگان را قادر می سازد تا به سرعت رابط های کاربری جدید را با استفاده از ترکیب بارگذاری مجدد داغ و رابط کاربری اعلامی ایجاد کنند. با این حال، زمانی فرا می رسد که باید تعامل بیشتری را به یک رابط اضافه کنید. این لمسها میتوانند به سادگی متحرک کردن یک دکمه روی شناور یا به اندازه یک سایهزن که رابط کاربری را با استفاده از قدرت GPU تغییر میدهد، جذاب باشد.
در این کد لبه، شما یک برنامه Flutter خواهید ساخت که از قدرت انیمیشنها، سایهزنها و فیلدهای ذرهای برای ایجاد رابط کاربری استفاده میکند که فیلمهای علمی تخیلی و برنامههای تلویزیونی را تداعی میکند که همه ما دوست داریم زمانی که برنامهنویسی نمیکنیم تماشا کنیم.
چیزی که خواهی ساخت
شما صفحه منوی اولیه را برای یک بازی علمی تخیلی پس از آخرالزمان خواهید ساخت. عنوانی با سایهزن قطعهای وجود دارد که متن را برای متحرکسازی بصری نمونهبرداری میکند، یک منوی دشواری که تم رنگ صفحه را با انیمیشنهای فراوان تغییر میدهد، و یک گوی متحرک که با سایهزن قطعه دوم نقاشی شده است. اگر این کافی نیست، در انتهای صفحه کد، یک افکت ذره ظریف برای ایجاد حرکت و جذابیت به صفحه اضافه میکنید.
اسکرین شات های زیر برنامه ای را که روی سه سیستم عامل دسکتاپ پشتیبانی شده می سازید نشان می دهد: Windows، Linux، و macOS. برای تکمیل، یک نسخه مرورگر وب (همچنین پشتیبانی می شود) ارائه شده است. انیمیشن ها و سایه زن های قطعه در همه جا!
پیش نیازها
- دانش اولیه توسعه Flutter با دارت، همانطور که در اولین کد برنامه Flutter شما پوشش داده شده است
چیزی که یاد خواهید گرفت
- نحوه استفاده از
flutter_animate
برای ساخت انیمیشن های رسا - نحوه استفاده از پشتیبانی فلاتر برای شیدرهای قطعه در دسکتاپ و وب
- چگونه با
particle_field
انیمیشن های ذرات را به برنامه خود اضافه کنید
آنچه شما نیاز دارید
- فلاتر SDK
- تنظیم کد VS برای فلاتر و دارت
- راه اندازی پشتیبانی دسکتاپ برای Flutter برای Windows، Linux، یا macOS
- راه اندازی پشتیبانی وب برای Flutter
2. شروع کنید
کد شروع را دانلود کنید
- به این مخزن GitHub بروید.
- روی Code > Download zip کلیک کنید تا همه کدهای این کد لبه را دانلود کنید.
- فایل فشرده دانلود شده را از حالت فشرده خارج کنید تا بسته بندی پوشه
codelabs-main
root باز شود. شما فقط به زیرشاخهnext-gen-ui/
نیاز دارید که شامل پوشه هایstep_01
تاstep_06
است که حاوی کد منبعی است که برای هر مرحله در این Codelab ایجاد می کنید.
وابستگی های پروژه را دانلود کنید
- در VS Code، روی File > Open folder > codelabs-main > next-gen-uis > step_01 کلیک کنید تا پروژه شروع کننده باز شود.
- اگر یک گفتگوی VS Code میبینید که از شما میخواهد بستههای مورد نیاز برنامه شروع را دانلود کنید، روی دریافت بستهها کلیک کنید.
- اگر گفتگوی VS Code را نمیبینید که از شما میخواهد بستههای مورد نیاز را برای برنامه استارت بارگیری کنید، ترمینال خود را باز کنید و سپس به پوشه
step_01
بروید و دستورflutter pub get
اجرا کنید.
برنامه استارتر را اجرا کنید
- در VS Code، یا سیستم عامل دسکتاپ مورد استفاده خود را انتخاب کنید یا اگر می خواهید برنامه خود را در یک مرورگر وب آزمایش کنید، Chrome را انتخاب کنید.
برای مثال، در اینجا چیزی است که هنگام استفاده از macOS به عنوان هدف استقرار خود مشاهده می کنید:
در اینجا چیزی است که هنگام استفاده از Chrome به عنوان هدف استقرار خود مشاهده می کنید:
- فایل
lib/main.dart
را باز کرده و کلیک کنید اشکال زدایی را شروع کنید . برنامه روی سیستم عامل دسکتاپ شما یا در مرورگر کروم راه اندازی می شود.
برنامه شروع را کاوش کنید
در برنامه شروع به موارد زیر توجه کنید:
- UI برای ساخت شما آماده است.
- فهرست
assets
دارای دارایی های هنری و دو قطعه سایه زن است که شما استفاده خواهید کرد. - فایل
pubspec.yaml
از قبل دارایی ها و مجموعه ای از بسته های میخانه ای را که از آنها استفاده خواهید کرد فهرست می کند. - دایرکتوری
lib
حاوی فایل اجباریmain.dart
، یک فایلassets.dart
است که مسیر دارایی های هنری و سایه بان های قطعه را فهرست می کند، و یک فایلstyles.dart
که TextStyles و رنگ هایی را که استفاده می کنید فهرست می کند. - دایرکتوری
lib
همچنین شامل یک دایرکتوریcommon
است که تعدادی ابزار مفید را در خود جای داده است که در این کد لبه استفاده خواهید کرد و دایرکتوریorb_shader
که حاوی یکWidget
است که برای نمایش گوی با سایه زن رأس استفاده می شود.
در اینجا چیزی است که پس از شروع برنامه مشاهده خواهید کرد.
3. صحنه را نقاشی کنید
در این مرحله تمام دارایی های هنری پس زمینه را به صورت لایه ای روی صفحه قرار می دهید. انتظار داشته باشید که در ابتدا به طور عجیبی تک رنگ به نظر برسد، اما در پایان این مرحله رنگ ها را به صحنه اضافه می کنید.
دارایی ها را به صحنه اضافه کنید
- یک پوشه
title_screen
در فهرستlib
خود ایجاد کنید و سپس یک فایلtitle_screen.dart
اضافه کنید. محتوای زیر را به فایل اضافه کنید:
lib/title_screen/title_screen.dart
import 'package:flutter/material.dart';
import '../assets.dart';
class TitleScreen extends StatelessWidget {
const TitleScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [
/// Bg-Base
Image.asset(AssetPaths.titleBgBase),
/// Bg-Receive
Image.asset(AssetPaths.titleBgReceive),
/// Mg-Base
Image.asset(AssetPaths.titleMgBase),
/// Mg-Receive
Image.asset(AssetPaths.titleMgReceive),
/// Mg-Emit
Image.asset(AssetPaths.titleMgEmit),
/// Fg-Rocks
Image.asset(AssetPaths.titleFgBase),
/// Fg-Receive
Image.asset(AssetPaths.titleFgReceive),
/// Fg-Emit
Image.asset(AssetPaths.titleFgEmit),
],
),
),
);
}
}
این ویجت شامل صحنه با داراییهایی است که در لایهها روی هم چیده شدهاند. لایه های پس زمینه، میانی و پیش زمینه هر کدام با گروهی از دو یا سه تصویر نشان داده می شوند. این تصاویر با رنگ های مختلف روشن می شوند تا نحوه حرکت نور در صحنه را به تصویر بکشند.
- در فایل
main.dart
محتوای زیر را اضافه کنید:
lib/main.dart
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:window_size/window_size.dart';
// Remove 'styles.dart' import
import 'title_screen/title_screen.dart'; // Add this import
void main() {
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
WidgetsFlutterBinding.ensureInitialized();
setWindowMinSize(const Size(800, 500));
}
runApp(const NextGenApp());
}
class NextGenApp extends StatelessWidget {
const NextGenApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
themeMode: ThemeMode.dark,
darkTheme: ThemeData(brightness: Brightness.dark),
home: const TitleScreen(), // Replace with this widget
);
}
}
با این کار صحنه تک رنگی که داراییهای هنری ایجاد میکنند، رابط کاربری برنامه را جایگزین میکند. بعد، هر لایه را رنگ می کنید.
یک ابزار رنگ آمیزی تصویر اضافه کنید
با افزودن محتوای زیر به فایل title_screen.dart
یک ابزار رنگ آمیزی تصویر اضافه کنید:
lib/title_screen/title_screen.dart
import 'package:flutter/material.dart';
import '../assets.dart';
class TitleScreen extends StatelessWidget {
const TitleScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [
/// Bg-Base
Image.asset(AssetPaths.titleBgBase),
/// Bg-Receive
Image.asset(AssetPaths.titleBgReceive),
/// Mg-Base
Image.asset(AssetPaths.titleMgBase),
/// Mg-Receive
Image.asset(AssetPaths.titleMgReceive),
/// Mg-Emit
Image.asset(AssetPaths.titleMgEmit),
/// Fg-Rocks
Image.asset(AssetPaths.titleFgBase),
/// Fg-Receive
Image.asset(AssetPaths.titleFgReceive),
/// Fg-Emit
Image.asset(AssetPaths.titleFgEmit),
],
),
),
);
}
}
class _LitImage extends StatelessWidget { // Add from here...
const _LitImage({
required this.color,
required this.imgSrc,
required this.lightAmt,
});
final Color color;
final String imgSrc;
final double lightAmt;
@override
Widget build(BuildContext context) {
final hsl = HSLColor.fromColor(color);
return Image.asset(
imgSrc,
color: hsl.withLightness(hsl.lightness * lightAmt).toColor(),
colorBlendMode: BlendMode.modulate,
);
}
} // to here.
این ویجت ابزار _LitImage
هر یک از دارایی های هنری را بسته به اینکه نور منتشر می کنند یا دریافت می کنند، دوباره رنگ می کند. از آنجایی که شما هنوز از این ویجت جدید استفاده نمیکنید، ممکن است یک هشدار لینتر را ایجاد کند.
رنگ آمیزی کنید
با تغییر فایل title_screen.dart
به صورت زیر رنگ آمیزی کنید:
lib/title_screen/title_screen.dart
import 'package:flutter/material.dart';
import '../assets.dart';
import '../styles.dart'; // Add this import
class TitleScreen extends StatelessWidget {
const TitleScreen({super.key});
final _finalReceiveLightAmt = 0.7; // Add this attribute
final _finalEmitLightAmt = 0.5; // And this attribute
@override
Widget build(BuildContext context) {
final orbColor = AppColors.orbColors[0]; // Add this final variable
final emitColor = AppColors.emitColors[0]; // And this one
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [
/// Bg-Base
Image.asset(AssetPaths.titleBgBase),
/// Bg-Receive
_LitImage( // Modify from here...
color: orbColor,
imgSrc: AssetPaths.titleBgReceive,
lightAmt: _finalReceiveLightAmt,
), // to here.
/// Mg-Base
_LitImage( // Modify from here...
imgSrc: AssetPaths.titleMgBase,
color: orbColor,
lightAmt: _finalReceiveLightAmt,
), // to here.
/// Mg-Receive
_LitImage( // Modify from here...
imgSrc: AssetPaths.titleMgReceive,
color: orbColor,
lightAmt: _finalReceiveLightAmt,
), // to here.
/// Mg-Emit
_LitImage( // Modify from here...
imgSrc: AssetPaths.titleMgEmit,
color: emitColor,
lightAmt: _finalEmitLightAmt,
), // to here.
/// Fg-Rocks
Image.asset(AssetPaths.titleFgBase),
/// Fg-Receive
_LitImage( // Modify from here...
imgSrc: AssetPaths.titleFgReceive,
color: orbColor,
lightAmt: _finalReceiveLightAmt,
), // to here.
/// Fg-Emit
_LitImage( // Modify from here...
imgSrc: AssetPaths.titleFgEmit,
color: emitColor,
lightAmt: _finalEmitLightAmt,
), // to here.
],
),
),
);
}
}
class _LitImage extends StatelessWidget {
const _LitImage({
required this.color,
required this.imgSrc,
required this.lightAmt,
});
final Color color;
final String imgSrc;
final double lightAmt;
@override
Widget build(BuildContext context) {
final hsl = HSLColor.fromColor(color);
return Image.asset(
imgSrc,
color: hsl.withLightness(hsl.lightness * lightAmt).toColor(),
colorBlendMode: BlendMode.modulate,
);
}
}
در اینجا دوباره برنامه، این بار با دارایی های هنری سبز رنگ شده است.
4. یک رابط کاربری اضافه کنید
در این مرحله یک رابط کاربری را روی صحنه ایجاد شده در مرحله قبل قرار می دهید. این شامل عنوان، دکمههای انتخاب سختی، و دکمه شروع بسیار مهم است.
عنوان اضافه کنید
- یک فایل
title_screen_ui.dart
در دایرکتوریlib/title_screen
ایجاد کنید و محتوای زیر را به فایل اضافه کنید:
lib/title_screen/title_screen_ui.dart
import 'package:extra_alignments/extra_alignments.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import '../assets.dart';
import '../common/ui_scaler.dart';
import '../styles.dart';
class TitleScreenUi extends StatelessWidget {
const TitleScreenUi({
super.key,
});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 40, horizontal: 50),
child: Stack(
children: [
/// Title Text
TopLeft(
child: UiScaler(
alignment: Alignment.topLeft,
child: _TitleText(),
),
),
],
),
);
}
}
class _TitleText extends StatelessWidget {
const _TitleText();
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Gap(20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Transform.translate(
offset: Offset(-(TextStyles.h1.letterSpacing! * .5), 0),
child: Text('OUTPOST', style: TextStyles.h1),
),
Image.asset(AssetPaths.titleSelectedLeft, height: 65),
Text('57', style: TextStyles.h2),
Image.asset(AssetPaths.titleSelectedRight, height: 65),
],
),
Text('INTO THE UNKNOWN', style: TextStyles.h3),
],
);
}
}
این ویجت شامل عنوان و تمام دکمههایی است که رابط کاربری این برنامه را تشکیل میدهند.
- فایل
lib/title_screen/title_screen.dart
را به صورت زیر به روز کنید:
lib/title_screen/title_screen.dart
import 'package:flutter/material.dart';
import '../assets.dart';
import '../styles.dart';
import 'title_screen_ui.dart'; // Add this import
class TitleScreen extends StatelessWidget {
const TitleScreen({super.key});
final _finalReceiveLightAmt = 0.7;
final _finalEmitLightAmt = 0.5;
@override
Widget build(BuildContext context) {
final orbColor = AppColors.orbColors[0];
final emitColor = AppColors.emitColors[0];
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [
/// Bg-Base
Image.asset(AssetPaths.titleBgBase),
/// Bg-Receive
_LitImage(
color: orbColor,
imgSrc: AssetPaths.titleBgReceive,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Base
_LitImage(
imgSrc: AssetPaths.titleMgBase,
color: orbColor,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Receive
_LitImage(
imgSrc: AssetPaths.titleMgReceive,
color: orbColor,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Emit
_LitImage(
imgSrc: AssetPaths.titleMgEmit,
color: emitColor,
lightAmt: _finalEmitLightAmt,
),
/// Fg-Rocks
Image.asset(AssetPaths.titleFgBase),
/// Fg-Receive
_LitImage(
imgSrc: AssetPaths.titleFgReceive,
color: orbColor,
lightAmt: _finalReceiveLightAmt,
),
/// Fg-Emit
_LitImage(
imgSrc: AssetPaths.titleFgEmit,
color: emitColor,
lightAmt: _finalEmitLightAmt,
),
/// UI
const Positioned.fill( // Add from here...
child: TitleScreenUi(),
), // to here.
],
),
),
);
}
}
class _LitImage extends StatelessWidget {
const _LitImage({
required this.color,
required this.imgSrc,
required this.lightAmt,
});
final Color color;
final String imgSrc;
final double lightAmt;
@override
Widget build(BuildContext context) {
final hsl = HSLColor.fromColor(color);
return Image.asset(
imgSrc,
color: hsl.withLightness(hsl.lightness * lightAmt).toColor(),
colorBlendMode: BlendMode.modulate,
);
}
}
با اجرای این کد عنوانی که ابتدای رابط کاربری است آشکار می شود.
دکمه های سختی را اضافه کنید
- با افزودن یک واردات جدید برای بسته
focusable_control_builder
title_screen_ui.dart
را بهروزرسانی کنید:
lib/title_screen/title_screen_ui.dart
import 'package:extra_alignments/extra_alignments.dart';
import 'package:flutter/material.dart';
import 'package:focusable_control_builder/focusable_control_builder.dart'; // Add import
import 'package:gap/gap.dart';
import '../assets.dart';
import '../common/ui_scaler.dart';
import '../styles.dart';
- به ویجت
TitleScreenUi
موارد زیر را اضافه کنید:
lib/title_screen/title_screen_ui.dart
class TitleScreenUi extends StatelessWidget {
const TitleScreenUi({
super.key,
required this.difficulty, // Edit from here...
required this.onDifficultyPressed,
required this.onDifficultyFocused,
});
final int difficulty;
final void Function(int difficulty) onDifficultyPressed;
final void Function(int? difficulty) onDifficultyFocused; // to here.
@override
Widget build(BuildContext context) {
return Padding( // Move this const...
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50), // to here.
child: Stack(
children: [
/// Title Text
const TopLeft( // Add a const here, as well
child: UiScaler(
alignment: Alignment.topLeft,
child: _TitleText(),
),
),
/// Difficulty Btns
BottomLeft( // Add from here...
child: UiScaler(
alignment: Alignment.bottomLeft,
child: _DifficultyBtns(
difficulty: difficulty,
onDifficultyPressed: onDifficultyPressed,
onDifficultyFocused: onDifficultyFocused,
),
),
), // to here.
],
),
);
}
}
- دو ویجت زیر را برای پیاده سازی دکمه های دشواری اضافه کنید:
lib/title_screen/title_screen_ui.dart
class _DifficultyBtns extends StatelessWidget {
const _DifficultyBtns({
required this.difficulty,
required this.onDifficultyPressed,
required this.onDifficultyFocused,
});
final int difficulty;
final void Function(int difficulty) onDifficultyPressed;
final void Function(int? difficulty) onDifficultyFocused;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_DifficultyBtn(
label: 'Casual',
selected: difficulty == 0,
onPressed: () => onDifficultyPressed(0),
onHover: (over) => onDifficultyFocused(over ? 0 : null),
),
_DifficultyBtn(
label: 'Normal',
selected: difficulty == 1,
onPressed: () => onDifficultyPressed(1),
onHover: (over) => onDifficultyFocused(over ? 1 : null),
),
_DifficultyBtn(
label: 'Hardcore',
selected: difficulty == 2,
onPressed: () => onDifficultyPressed(2),
onHover: (over) => onDifficultyFocused(over ? 2 : null),
),
const Gap(20),
],
);
}
}
class _DifficultyBtn extends StatelessWidget {
const _DifficultyBtn({
required this.selected,
required this.onPressed,
required this.onHover,
required this.label,
});
final String label;
final bool selected;
final VoidCallback onPressed;
final void Function(bool hasFocus) onHover;
@override
Widget build(BuildContext context) {
return FocusableControlBuilder(
onPressed: onPressed,
onHoverChanged: (_, state) => onHover.call(state.isHovered),
builder: (_, state) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 250,
height: 60,
child: Stack(
children: [
/// Bg with fill and outline
Container(
decoration: BoxDecoration(
color: const Color(0xFF00D1FF).withOpacity(.1),
border: Border.all(color: Colors.white, width: 5),
),
),
if (state.isHovered || state.isFocused) ...[
Container(
decoration: BoxDecoration(
color: const Color(0xFF00D1FF).withOpacity(.1),
),
),
],
/// cross-hairs (selected state)
if (selected) ...[
CenterLeft(
child: Image.asset(AssetPaths.titleSelectedLeft),
),
CenterRight(
child: Image.asset(AssetPaths.titleSelectedRight),
),
],
/// Label
Center(
child: Text(label.toUpperCase(), style: TextStyles.btn),
),
],
),
),
);
},
);
}
}
- ویجت
TitleScreen
را از حالت بدون حالت به حالت حالت تبدیل کنید و برای فعال کردن تغییر طرح رنگ بر اساس سختی، حالت را اضافه کنید:
lib/title_screen/title_screen.dart
import 'package:flutter/material.dart';
import '../assets.dart';
import '../styles.dart';
import 'title_screen_ui.dart';
class TitleScreen extends StatefulWidget {
const TitleScreen({super.key});
@override
State<TitleScreen> createState() => _TitleScreenState();
}
class _TitleScreenState extends State<TitleScreen> {
Color get _emitColor =>
AppColors.emitColors[_difficultyOverride ?? _difficulty];
Color get _orbColor =>
AppColors.orbColors[_difficultyOverride ?? _difficulty];
/// Currently selected difficulty
int _difficulty = 0;
/// Currently focused difficulty (if any)
int? _difficultyOverride;
void _handleDifficultyPressed(int value) {
setState(() => _difficulty = value);
}
void _handleDifficultyFocused(int? value) {
setState(() => _difficultyOverride = value);
}
final _finalReceiveLightAmt = 0.7;
final _finalEmitLightAmt = 0.5;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [
/// Bg-Base
Image.asset(AssetPaths.titleBgBase),
/// Bg-Receive
_LitImage(
color: _orbColor,
imgSrc: AssetPaths.titleBgReceive,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Base
_LitImage(
imgSrc: AssetPaths.titleMgBase,
color: _orbColor,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Receive
_LitImage(
imgSrc: AssetPaths.titleMgReceive,
color: _orbColor,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Emit
_LitImage(
imgSrc: AssetPaths.titleMgEmit,
color: _emitColor,
lightAmt: _finalEmitLightAmt,
),
/// Fg-Rocks
Image.asset(AssetPaths.titleFgBase),
/// Fg-Receive
_LitImage(
imgSrc: AssetPaths.titleFgReceive,
color: _orbColor,
lightAmt: _finalReceiveLightAmt,
),
/// Fg-Emit
_LitImage(
imgSrc: AssetPaths.titleFgEmit,
color: _emitColor,
lightAmt: _finalEmitLightAmt,
),
/// UI
Positioned.fill(
child: TitleScreenUi(
difficulty: _difficulty,
onDifficultyFocused: _handleDifficultyFocused,
onDifficultyPressed: _handleDifficultyPressed,
),
),
],
),
),
);
}
}
class _LitImage extends StatelessWidget {
const _LitImage({
required this.color,
required this.imgSrc,
required this.lightAmt,
});
final Color color;
final String imgSrc;
final double lightAmt;
@override
Widget build(BuildContext context) {
final hsl = HSLColor.fromColor(color);
return Image.asset(
imgSrc,
color: hsl.withLightness(hsl.lightness * lightAmt).toColor(),
colorBlendMode: BlendMode.modulate,
);
}
}
در اینجا رابط کاربری با دو تنظیمات دشواری متفاوت است. توجه داشته باشید که رنگهای دشواری که بهعنوان ماسک روی تصاویر در مقیاس خاکستری اعمال میشوند، جلوهای واقعی و انعکاسی ایجاد میکنند!
دکمه شروع را اضافه کنید
- فایل
title_screen_ui.dart
را به روز کنید. به ویجتTitleScreenUi
موارد زیر را اضافه کنید:
lib/title_screen/title_screen_ui.dart
class TitleScreenUi extends StatelessWidget {
const TitleScreenUi({
super.key,
required this.difficulty,
required this.onDifficultyPressed,
required this.onDifficultyFocused,
});
final int difficulty;
final void Function(int difficulty) onDifficultyPressed;
final void Function(int? difficulty) onDifficultyFocused;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
child: Stack(
children: [
/// Title Text
const TopLeft(
child: UiScaler(
alignment: Alignment.topLeft,
child: _TitleText(),
),
),
/// Difficulty Btns
BottomLeft(
child: UiScaler(
alignment: Alignment.bottomLeft,
child: _DifficultyBtns(
difficulty: difficulty,
onDifficultyPressed: onDifficultyPressed,
onDifficultyFocused: onDifficultyFocused,
),
),
),
/// StartBtn
BottomRight( // Add from here...
child: UiScaler(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(bottom: 20, right: 40),
child: _StartBtn(onPressed: () {}),
),
),
), // to here.
],
),
);
}
}
- ویجت زیر را برای اجرای دکمه شروع اضافه کنید:
lib/title_screen/title_screen_ui.dart
class _StartBtn extends StatefulWidget {
const _StartBtn({required this.onPressed});
final VoidCallback onPressed;
@override
State<_StartBtn> createState() => _StartBtnState();
}
class _StartBtnState extends State<_StartBtn> {
AnimationController? _btnAnim;
bool _wasHovered = false;
@override
Widget build(BuildContext context) {
return FocusableControlBuilder(
cursor: SystemMouseCursors.click,
onPressed: widget.onPressed,
builder: (_, state) {
if ((state.isHovered || state.isFocused) &&
!_wasHovered &&
_btnAnim?.status != AnimationStatus.forward) {
_btnAnim?.forward(from: 0);
}
_wasHovered = (state.isHovered || state.isFocused);
return SizedBox(
width: 520,
height: 100,
child: Stack(
children: [
Positioned.fill(child: Image.asset(AssetPaths.titleStartBtn)),
if (state.isHovered || state.isFocused) ...[
Positioned.fill(
child: Image.asset(AssetPaths.titleStartBtnHover)),
],
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('START MISSION',
style: TextStyles.btn
.copyWith(fontSize: 24, letterSpacing: 18)),
],
),
),
],
),
);
},
);
}
}
و در اینجا برنامه با مجموعه ای کامل از دکمه ها اجرا می شود.
5. انیمیشن اضافه کنید
در این مرحله رابط کاربری و انتقال رنگ را برای دارایی های هنری متحرک می کنید.
محو شدن در عنوان
در این مرحله، شما از چندین رویکرد برای متحرک سازی اپلیکیشن فلاتر استفاده می کنید. یکی از رویکردها استفاده از flutter_animate
است. انیمیشنهای ارائهشده توسط این بسته میتوانند بهطور خودکار هر زمان که برنامهتان را بارگیری مجدد کنید، بهمنظور سرعت بخشیدن به تکرارهای توسعه، دوباره پخش شوند.
- کد
lib/main.dart
را به صورت زیر تغییر دهید:
lib/main.dart
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; // Add this import
import 'package:window_size/window_size.dart';
import 'title_screen/title_screen.dart';
void main() {
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
WidgetsFlutterBinding.ensureInitialized();
setWindowMinSize(const Size(800, 500));
}
Animate.restartOnHotReload = true; // Add this line
runApp(const NextGenApp());
}
class NextGenApp extends StatelessWidget {
const NextGenApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
themeMode: ThemeMode.dark,
darkTheme: ThemeData(brightness: Brightness.dark),
home: const TitleScreen(),
);
}
}
- برای استفاده از بسته
flutter_animate
، باید آن را وارد کنید. وارد کردن را درlib/title_screen/title_screen_ui.dart
به صورت زیر اضافه کنید:
lib/title_screen/title_screen_ui.dart
import 'package:extra_alignments/extra_alignments.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; // Add this import
import 'package:focusable_control_builder/focusable_control_builder.dart';
import 'package:gap/gap.dart';
import '../assets.dart';
import '../common/ui_scaler.dart';
import '../styles.dart';
class TitleScreenUi extends StatelessWidget {
- با ویرایش ویجت
_TitleText
، به صورت زیر انیمیشن را به عنوان اضافه کنید:
lib/title_screen/title_screen_ui.dart
class _TitleText extends StatelessWidget {
const _TitleText();
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Gap(20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Transform.translate(
offset: Offset(-(TextStyles.h1.letterSpacing! * .5), 0),
child: Text('OUTPOST', style: TextStyles.h1),
),
Image.asset(AssetPaths.titleSelectedLeft, height: 65),
Text('57', style: TextStyles.h2),
Image.asset(AssetPaths.titleSelectedRight, height: 65),
], // Edit from here...
).animate().fadeIn(delay: .8.seconds, duration: .7.seconds),
Text('INTO THE UNKNOWN', style: TextStyles.h3)
.animate()
.fadeIn(delay: 1.seconds, duration: .7.seconds),
], // to here.
);
}
}
- برای مشاهده محو شدن عنوان ، Reload را فشار دهید.
محو شدن در دکمه های دشواری
- با ویرایش ویجت
_DifficultyBtns
به شکل زیر، انیمیشن را به ظاهر اولیه دکمه های سختی اضافه کنید:
lib/title_screen/title_screen_ui.dart
class _DifficultyBtns extends StatelessWidget {
const _DifficultyBtns({
required this.difficulty,
required this.onDifficultyPressed,
required this.onDifficultyFocused,
});
final int difficulty;
final void Function(int difficulty) onDifficultyPressed;
final void Function(int? difficulty) onDifficultyFocused;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_DifficultyBtn(
label: 'Casual',
selected: difficulty == 0,
onPressed: () => onDifficultyPressed(0),
onHover: (over) => onDifficultyFocused(over ? 0 : null),
) // Add from here...
.animate()
.fadeIn(delay: 1.3.seconds, duration: .35.seconds)
.slide(begin: const Offset(0, .2)), // to here
_DifficultyBtn(
label: 'Normal',
selected: difficulty == 1,
onPressed: () => onDifficultyPressed(1),
onHover: (over) => onDifficultyFocused(over ? 1 : null),
) // Add from here...
.animate()
.fadeIn(delay: 1.5.seconds, duration: .35.seconds)
.slide(begin: const Offset(0, .2)), // to here
_DifficultyBtn(
label: 'Hardcore',
selected: difficulty == 2,
onPressed: () => onDifficultyPressed(2),
onHover: (over) => onDifficultyFocused(over ? 2 : null),
) // Add from here...
.animate()
.fadeIn(delay: 1.7.seconds, duration: .35.seconds)
.slide(begin: const Offset(0, .2)), // to here
const Gap(20),
],
);
}
}
- بارگذاری مجدد را فشار دهید تا دکمه های سختی را به ترتیب با یک اسلاید ظریف به سمت بالا به عنوان یک جایزه ببینید.
محو شدن در دکمه شروع
- با ویرایش کلاس حالت
_StartBtnState
، به شکل زیر انیمیشن را به دکمه start اضافه کنید:
lib/title_screen/title_screen_ui.dart
class _StartBtnState extends State<_StartBtn> {
AnimationController? _btnAnim;
bool _wasHovered = false;
@override
Widget build(BuildContext context) {
return FocusableControlBuilder(
cursor: SystemMouseCursors.click,
onPressed: widget.onPressed,
builder: (_, state) {
if ((state.isHovered || state.isFocused) &&
!_wasHovered &&
_btnAnim?.status != AnimationStatus.forward) {
_btnAnim?.forward(from: 0);
}
_wasHovered = (state.isHovered || state.isFocused);
return SizedBox(
width: 520,
height: 100,
child: Stack(
children: [
Positioned.fill(child: Image.asset(AssetPaths.titleStartBtn)),
if (state.isHovered || state.isFocused) ...[
Positioned.fill(
child: Image.asset(AssetPaths.titleStartBtnHover)),
],
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('START MISSION',
style: TextStyles.btn
.copyWith(fontSize: 24, letterSpacing: 18)),
],
),
),
],
) // Edit from here...
.animate(autoPlay: false, onInit: (c) => _btnAnim = c)
.shimmer(duration: .7.seconds, color: Colors.black),
)
.animate()
.fadeIn(delay: 2.3.seconds)
.slide(begin: const Offset(0, .2));
}, // to here.
);
}
}
- بارگذاری مجدد را فشار دهید تا دکمه های سختی را به ترتیب با یک اسلاید ظریف به سمت بالا به عنوان یک جایزه ببینید.
اثر شناور دشواری را متحرک کنید
با ویرایش کلاس حالت _DifficultyBtn
، انیمیشن را به حالت شناور دکمه های دشواری اضافه کنید:
lib/title_screen/title_screen_ui.dart
class _DifficultyBtn extends StatelessWidget {
const _DifficultyBtn({
required this.selected,
required this.onPressed,
required this.onHover,
required this.label,
});
final String label;
final bool selected;
final VoidCallback onPressed;
final void Function(bool hasFocus) onHover;
@override
Widget build(BuildContext context) {
return FocusableControlBuilder(
onPressed: onPressed,
onHoverChanged: (_, state) => onHover.call(state.isHovered),
builder: (_, state) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 250,
height: 60,
child: Stack(
children: [
/// Bg with fill and outline
AnimatedOpacity( // Edit from here
opacity: (!selected && (state.isHovered || state.isFocused))
? 1
: 0,
duration: .3.seconds,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF00D1FF).withOpacity(.1),
border: Border.all(color: Colors.white, width: 5),
),
),
), // to here.
if (state.isHovered || state.isFocused) ...[
Container(
decoration: BoxDecoration(
color: const Color(0xFF00D1FF).withOpacity(.1),
),
),
],
/// cross-hairs (selected state)
if (selected) ...[
CenterLeft(
child: Image.asset(AssetPaths.titleSelectedLeft),
),
CenterRight(
child: Image.asset(AssetPaths.titleSelectedRight),
),
],
/// Label
Center(
child: Text(label.toUpperCase(), style: TextStyles.btn),
),
],
),
),
);
},
);
}
}
دکمههای سختی اکنون BoxDecoration
زمانی نشان میدهند که ماوس روی دکمهای قرار میگیرد که انتخاب نشده است.
تغییر رنگ را متحرک کنید
- تغییر رنگ پس زمینه آنی و خشن است. بهتر است تصاویر روشن را بین طرح های رنگی متحرک کنید.
flutter_animate
بهlib/title_screen/title_screen.dart
اضافه کنید:
lib/title_screen/title_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; // Add this import
import '../assets.dart';
import '../styles.dart';
import 'title_screen_ui.dart';
class TitleScreen extends StatefulWidget {
- یک ویجت
_AnimatedColors
را درlib/title_screen/title_screen.dart
اضافه کنید:
lib/title_screen/title_screen.dart
class _AnimatedColors extends StatelessWidget {
const _AnimatedColors({
required this.emitColor,
required this.orbColor,
required this.builder,
});
final Color emitColor;
final Color orbColor;
final Widget Function(BuildContext context, Color orbColor, Color emitColor)
builder;
@override
Widget build(BuildContext context) {
final duration = .5.seconds;
return TweenAnimationBuilder(
tween: ColorTween(begin: emitColor, end: emitColor),
duration: duration,
builder: (_, emitColor, __) {
return TweenAnimationBuilder(
tween: ColorTween(begin: orbColor, end: orbColor),
duration: duration,
builder: (context, orbColor, __) {
return builder(context, orbColor!, emitColor!);
},
);
},
);
}
}
- با بهروزرسانی روش
build
در_TitleScreenState
، از ویجتی که ایجاد کردهاید برای متحرک کردن رنگهای تصاویر روشن استفاده کنید.
lib/title_screen/title_screen.dart
class _TitleScreenState extends State<TitleScreen> {
Color get _emitColor =>
AppColors.emitColors[_difficultyOverride ?? _difficulty];
Color get _orbColor =>
AppColors.orbColors[_difficultyOverride ?? _difficulty];
/// Currently selected difficulty
int _difficulty = 0;
/// Currently focused difficulty (if any)
int? _difficultyOverride;
void _handleDifficultyPressed(int value) {
setState(() => _difficulty = value);
}
void _handleDifficultyFocused(int? value) {
setState(() => _difficultyOverride = value);
}
final _finalReceiveLightAmt = 0.7;
final _finalEmitLightAmt = 0.5;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: _AnimatedColors( // Edit from here...
orbColor: _orbColor,
emitColor: _emitColor,
builder: (_, orbColor, emitColor) {
return Stack(
children: [
/// Bg-Base
Image.asset(AssetPaths.titleBgBase),
/// Bg-Receive
_LitImage(
color: orbColor,
imgSrc: AssetPaths.titleBgReceive,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Base
_LitImage(
imgSrc: AssetPaths.titleMgBase,
color: orbColor,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Receive
_LitImage(
imgSrc: AssetPaths.titleMgReceive,
color: orbColor,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Emit
_LitImage(
imgSrc: AssetPaths.titleMgEmit,
color: emitColor,
lightAmt: _finalEmitLightAmt,
),
/// Fg-Rocks
Image.asset(AssetPaths.titleFgBase),
/// Fg-Receive
_LitImage(
imgSrc: AssetPaths.titleFgReceive,
color: orbColor,
lightAmt: _finalReceiveLightAmt,
),
/// Fg-Emit
_LitImage(
imgSrc: AssetPaths.titleFgEmit,
color: emitColor,
lightAmt: _finalEmitLightAmt,
),
/// UI
Positioned.fill(
child: TitleScreenUi(
difficulty: _difficulty,
onDifficultyFocused: _handleDifficultyFocused,
onDifficultyPressed: _handleDifficultyPressed,
),
),
],
).animate().fadeIn(duration: 1.seconds, delay: .3.seconds);
},
), // to here.
),
);
}
}
با این ویرایش نهایی، انیمیشن هایی را به هر عنصر روی صفحه اضافه کردید، و بسیار بهتر به نظر می رسد!
6. سایه زن های قطعه را اضافه کنید
در این مرحله Shader های قطعه را به برنامه اضافه می کنید. ابتدا از یک سایهزن برای تغییر عنوان استفاده میکنید تا حس دیستوپیایی بیشتری به آن ببخشید. سپس، یک سایه زن دوم را اضافه می کنید تا یک گوی ایجاد کنید که به عنوان نقطه کانونی مرکزی صفحه عمل می کند.
تحریف عنوان با شیدر قطعه
با این تغییر بسته provider
را معرفی می کنید که امکان عبور سایه بان های کامپایل شده را به درخت ویجت می دهد. اگر به نحوه بارگذاری سایهزنها علاقه دارید، اجرای آن را در lib/assets.dart
ببینید.
- کد
lib/main.dart
را به صورت زیر تغییر دهید:
lib/main.dart
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:provider/provider.dart'; // Add this import
import 'package:window_size/window_size.dart';
import 'assets.dart'; // Add this import
import 'title_screen/title_screen.dart';
void main() {
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
WidgetsFlutterBinding.ensureInitialized();
setWindowMinSize(const Size(800, 500));
}
Animate.restartOnHotReload = true;
runApp( // Edit from here...
FutureProvider<FragmentPrograms?>(
create: (context) => loadFragmentPrograms(),
initialData: null,
child: const NextGenApp(),
),
); // to here.
}
class NextGenApp extends StatelessWidget {
const NextGenApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
themeMode: ThemeMode.dark,
darkTheme: ThemeData(brightness: Brightness.dark),
home: const TitleScreen(),
);
}
}
- برای استفاده از بسته
provider
و ابزارهای سایه زن موجود درstep_01
، باید آنها را وارد کنید. واردهای جدید را درlib/title_screen/title_screen_ui.dart
به شرح زیر اضافه کنید:
lib/title_screen/title_screen_ui.dart
import 'package:extra_alignments/extra_alignments.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:focusable_control_builder/focusable_control_builder.dart';
import 'package:gap/gap.dart';
import 'package:provider/provider.dart'; // Add this import
import '../assets.dart';
import '../common/shader_effect.dart'; // And this import
import '../common/ticking_builder.dart'; // And this import
import '../common/ui_scaler.dart';
import '../styles.dart';
class TitleScreenUi extends StatelessWidget {
- با ویرایش ویجت
_TitleText
به صورت زیر عنوان را با سایه زن تحریف کنید:
lib/title_screen/title_screen_ui.dart
class _TitleText extends StatelessWidget {
const _TitleText();
@override
Widget build(BuildContext context) {
Widget content = Column( // Modify this line
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Gap(20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Transform.translate(
offset: Offset(-(TextStyles.h1.letterSpacing! * .5), 0),
child: Text('OUTPOST', style: TextStyles.h1),
),
Image.asset(AssetPaths.titleSelectedLeft, height: 65),
Text('57', style: TextStyles.h2),
Image.asset(AssetPaths.titleSelectedRight, height: 65),
],
).animate().fadeIn(delay: .8.seconds, duration: .7.seconds),
Text('INTO THE UNKNOWN', style: TextStyles.h3)
.animate()
.fadeIn(delay: 1.seconds, duration: .7.seconds),
],
);
return Consumer<FragmentPrograms?>( // Add from here...
builder: (context, fragmentPrograms, _) {
if (fragmentPrograms == null) return content;
return TickingBuilder(
builder: (context, time) {
return AnimatedSampler(
(image, size, canvas) {
const double overdrawPx = 30;
final shader = fragmentPrograms.ui.fragmentShader();
shader
..setFloat(0, size.width)
..setFloat(1, size.height)
..setFloat(2, time)
..setImageSampler(0, image);
Rect rect = Rect.fromLTWH(-overdrawPx, -overdrawPx,
size.width + overdrawPx, size.height + overdrawPx);
canvas.drawRect(rect, Paint()..shader = shader);
},
child: content,
);
},
);
},
); // to here.
}
}
شما باید عنوان را تحریف کنید - همانطور که ممکن است در آینده ای دیستوپیایی انتظار داشته باشید.
گوی را اضافه کنید
حالا گوی وسط پنجره را اضافه کنید. شما باید یک پاسخ به تماس onPressed
را به دکمه شروع اضافه کنید.
- در
lib/title_screen/title_screen_ui.dart
،TitleScreenUi
به صورت زیر تغییر دهید:
lib/title_screen/title_screen_ui.dart
class TitleScreenUi extends StatelessWidget {
const TitleScreenUi({
super.key,
required this.difficulty,
required this.onDifficultyPressed,
required this.onDifficultyFocused,
required this.onStartPressed, // Add this argument
});
final int difficulty;
final void Function(int difficulty) onDifficultyPressed;
final void Function(int? difficulty) onDifficultyFocused;
final VoidCallback onStartPressed; // Add this attribute
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
child: Stack(
children: [
/// Title Text
const TopLeft(
child: UiScaler(
alignment: Alignment.topLeft,
child: _TitleText(),
),
),
/// Difficulty Btns
BottomLeft(
child: UiScaler(
alignment: Alignment.bottomLeft,
child: _DifficultyBtns(
difficulty: difficulty,
onDifficultyPressed: onDifficultyPressed,
onDifficultyFocused: onDifficultyFocused,
),
),
),
/// StartBtn
BottomRight(
child: UiScaler(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(bottom: 20, right: 40),
child: _StartBtn(onPressed: onStartPressed), // Edit this line
),
),
),
],
),
);
}
}
اکنون که دکمه شروع را با یک تماس برگشتی تغییر داده اید، باید تغییرات گسترده ای در فایل lib/title_screen/title_screen.dart
ایجاد کنید.
- واردات را به شرح زیر تغییر دهید:
lib/title_screen/title_screen.dart
import 'dart:math'; // Add this import
import 'dart:ui'; // And this import
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // Add this import
import 'package:flutter_animate/flutter_animate.dart';
import '../assets.dart';
import '../orb_shader/orb_shader_config.dart'; // And this import
import '../orb_shader/orb_shader_widget.dart'; // And this import too
import '../styles.dart';
import 'title_screen_ui.dart';
class TitleScreen extends StatefulWidget {
-
_TitleScreenState
را برای مطابقت با موارد زیر تغییر دهید. تقریباً هر قسمت از کلاس به نوعی اصلاح شده است.
lib/title_screen/title_screen.dart
class _TitleScreenState extends State<TitleScreen>
with SingleTickerProviderStateMixin {
final _orbKey = GlobalKey<OrbShaderWidgetState>();
/// Editable Settings
/// 0-1, receive lighting strength
final _minReceiveLightAmt = .35;
final _maxReceiveLightAmt = .7;
/// 0-1, emit lighting strength
final _minEmitLightAmt = .5;
final _maxEmitLightAmt = 1;
/// Internal
var _mousePos = Offset.zero;
Color get _emitColor =>
AppColors.emitColors[_difficultyOverride ?? _difficulty];
Color get _orbColor =>
AppColors.orbColors[_difficultyOverride ?? _difficulty];
/// Currently selected difficulty
int _difficulty = 0;
/// Currently focused difficulty (if any)
int? _difficultyOverride;
double _orbEnergy = 0;
double _minOrbEnergy = 0;
double get _finalReceiveLightAmt {
final light =
lerpDouble(_minReceiveLightAmt, _maxReceiveLightAmt, _orbEnergy) ?? 0;
return light + _pulseEffect.value * .05 * _orbEnergy;
}
double get _finalEmitLightAmt {
return lerpDouble(_minEmitLightAmt, _maxEmitLightAmt, _orbEnergy) ?? 0;
}
late final _pulseEffect = AnimationController(
vsync: this,
duration: _getRndPulseDuration(),
lowerBound: -1,
upperBound: 1,
);
Duration _getRndPulseDuration() => 100.ms + 200.ms * Random().nextDouble();
double _getMinEnergyForDifficulty(int difficulty) => switch (difficulty) {
1 => 0.3,
2 => 0.6,
_ => 0,
};
@override
void initState() {
super.initState();
_pulseEffect.forward();
_pulseEffect.addListener(_handlePulseEffectUpdate);
}
void _handlePulseEffectUpdate() {
if (_pulseEffect.status == AnimationStatus.completed) {
_pulseEffect.reverse();
_pulseEffect.duration = _getRndPulseDuration();
} else if (_pulseEffect.status == AnimationStatus.dismissed) {
_pulseEffect.duration = _getRndPulseDuration();
_pulseEffect.forward();
}
}
void _handleDifficultyPressed(int value) {
setState(() => _difficulty = value);
_bumpMinEnergy();
}
Future<void> _bumpMinEnergy([double amount = 0.1]) async {
setState(() {
_minOrbEnergy = _getMinEnergyForDifficulty(_difficulty) + amount;
});
await Future<void>.delayed(.2.seconds);
setState(() {
_minOrbEnergy = _getMinEnergyForDifficulty(_difficulty);
});
}
void _handleStartPressed() => _bumpMinEnergy(0.3);
void _handleDifficultyFocused(int? value) {
setState(() {
_difficultyOverride = value;
if (value == null) {
_minOrbEnergy = _getMinEnergyForDifficulty(_difficulty);
} else {
_minOrbEnergy = _getMinEnergyForDifficulty(value);
}
});
}
/// Update mouse position so the orbWidget can use it, doing it here prevents
/// btns from blocking the mouse-move events in the widget itself.
void _handleMouseMove(PointerHoverEvent e) {
setState(() {
_mousePos = e.localPosition;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: MouseRegion(
onHover: _handleMouseMove,
child: _AnimatedColors(
orbColor: _orbColor,
emitColor: _emitColor,
builder: (_, orbColor, emitColor) {
return Stack(
children: [
/// Bg-Base
Image.asset(AssetPaths.titleBgBase),
/// Bg-Receive
_LitImage(
color: orbColor,
imgSrc: AssetPaths.titleBgReceive,
pulseEffect: _pulseEffect,
lightAmt: _finalReceiveLightAmt,
),
/// Orb
Positioned.fill(
child: Stack(
children: [
// Orb
OrbShaderWidget(
key: _orbKey,
mousePos: _mousePos,
minEnergy: _minOrbEnergy,
config: OrbShaderConfig(
ambientLightColor: orbColor,
materialColor: orbColor,
lightColor: orbColor,
),
onUpdate: (energy) => setState(() {
_orbEnergy = energy;
}),
),
],
),
),
/// Mg-Base
_LitImage(
imgSrc: AssetPaths.titleMgBase,
color: orbColor,
pulseEffect: _pulseEffect,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Receive
_LitImage(
imgSrc: AssetPaths.titleMgReceive,
color: orbColor,
pulseEffect: _pulseEffect,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Emit
_LitImage(
imgSrc: AssetPaths.titleMgEmit,
color: emitColor,
pulseEffect: _pulseEffect,
lightAmt: _finalEmitLightAmt,
),
/// Fg-Rocks
Image.asset(AssetPaths.titleFgBase),
/// Fg-Receive
_LitImage(
imgSrc: AssetPaths.titleFgReceive,
color: orbColor,
pulseEffect: _pulseEffect,
lightAmt: _finalReceiveLightAmt,
),
/// Fg-Emit
_LitImage(
imgSrc: AssetPaths.titleFgEmit,
color: emitColor,
pulseEffect: _pulseEffect,
lightAmt: _finalEmitLightAmt,
),
/// UI
Positioned.fill(
child: TitleScreenUi(
difficulty: _difficulty,
onDifficultyFocused: _handleDifficultyFocused,
onDifficultyPressed: _handleDifficultyPressed,
onStartPressed: _handleStartPressed,
),
),
],
).animate().fadeIn(duration: 1.seconds, delay: .3.seconds);
},
),
),
),
);
}
}
-
_LitImage
به صورت زیر تغییر دهید:
lib/title_screen/title_screen.dart
class _LitImage extends StatelessWidget {
const _LitImage({
required this.color,
required this.imgSrc,
required this.pulseEffect, // Add this parameter
required this.lightAmt,
});
final Color color;
final String imgSrc;
final AnimationController pulseEffect; // Add this attribute
final double lightAmt;
@override
Widget build(BuildContext context) {
final hsl = HSLColor.fromColor(color);
return ListenableBuilder( // Edit from here...
listenable: pulseEffect,
builder: (context, child) {
return Image.asset(
imgSrc,
color: hsl.withLightness(hsl.lightness * lightAmt).toColor(),
colorBlendMode: BlendMode.modulate,
);
},
); // to here.
}
}
این نتیجه این اضافه است.
7. انیمیشن های ذرات را اضافه کنید
در این مرحله، انیمیشن های ذرات را اضافه می کنید تا یک حرکت ضربانی ظریف به برنامه ایجاد کنید.
ذرات را در همه جا اضافه کنید
- یک فایل
lib/title_screen/particle_overlay.dart
جدید ایجاد کنید و سپس کد زیر را اضافه کنید:
lib/title_screen/particle_overlay.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:particle_field/particle_field.dart';
import 'package:rnd/rnd.dart';
class ParticleOverlay extends StatelessWidget {
const ParticleOverlay({super.key, required this.color, required this.energy});
final Color color;
final double energy;
@override
Widget build(BuildContext context) {
return ParticleField(
spriteSheet: SpriteSheet(
image: const AssetImage('assets/images/particle-wave.png'),
),
// blend the image's alpha with the specified color:
blendMode: BlendMode.dstIn,
// this runs every tick:
onTick: (controller, _, size) {
List<Particle> particles = controller.particles;
// add a new particle with random angle, distance & velocity:
double a = rnd(pi * 2);
double dist = rnd(1, 4) * 35 + 150 * energy;
double vel = rnd(1, 2) * (1 + energy * 1.8);
particles.add(Particle(
// how many ticks this particle will live:
lifespan: rnd(1, 2) * 20 + energy * 15,
// starting distance from center:
x: cos(a) * dist,
y: sin(a) * dist,
// starting velocity:
vx: cos(a) * vel,
vy: sin(a) * vel,
// other starting values:
rotation: a,
scale: rnd(1, 2) * 0.6 + energy * 0.5,
));
// update all of the particles:
for (int i = particles.length - 1; i >= 0; i--) {
Particle p = particles[i];
if (p.lifespan <= 0) {
// particle is expired, remove it:
particles.removeAt(i);
continue;
}
p.update(
scale: p.scale * 1.025,
vx: p.vx * 1.025,
vy: p.vy * 1.025,
color: color.withOpacity(p.lifespan * 0.001 + 0.01),
lifespan: p.lifespan - 1,
);
}
},
);
}
}
- واردات
lib/title_screen/title_screen.dart
به صورت زیر تغییر دهید:
lib/title_screen/title_screen.dart
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import '../assets.dart';
import '../orb_shader/orb_shader_config.dart';
import '../orb_shader/orb_shader_widget.dart';
import '../styles.dart';
import 'particle_overlay.dart'; // Add this import
import 'title_screen_ui.dart';
class TitleScreen extends StatefulWidget {
- با تغییر روش
build
_TitleScreenState
، به شرح زیر،ParticleOverlay
را به رابط کاربری اضافه کنید:
lib/title_screen/title_screen.dart
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: MouseRegion(
onHover: _handleMouseMove,
child: _AnimatedColors(
orbColor: _orbColor,
emitColor: _emitColor,
builder: (_, orbColor, emitColor) {
return Stack(
children: [
/// Bg-Base
Image.asset(AssetPaths.titleBgBase),
/// Bg-Receive
_LitImage(
color: orbColor,
imgSrc: AssetPaths.titleBgReceive,
pulseEffect: _pulseEffect,
lightAmt: _finalReceiveLightAmt,
),
/// Orb
Positioned.fill(
child: Stack(
children: [
// Orb
OrbShaderWidget(
key: _orbKey,
mousePos: _mousePos,
minEnergy: _minOrbEnergy,
config: OrbShaderConfig(
ambientLightColor: orbColor,
materialColor: orbColor,
lightColor: orbColor,
),
onUpdate: (energy) => setState(() {
_orbEnergy = energy;
}),
),
],
),
),
/// Mg-Base
_LitImage(
imgSrc: AssetPaths.titleMgBase,
color: orbColor,
pulseEffect: _pulseEffect,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Receive
_LitImage(
imgSrc: AssetPaths.titleMgReceive,
color: orbColor,
pulseEffect: _pulseEffect,
lightAmt: _finalReceiveLightAmt,
),
/// Mg-Emit
_LitImage(
imgSrc: AssetPaths.titleMgEmit,
color: emitColor,
pulseEffect: _pulseEffect,
lightAmt: _finalEmitLightAmt,
),
/// Particle Field
Positioned.fill( // Add from here...
child: IgnorePointer(
child: ParticleOverlay(
color: orbColor,
energy: _orbEnergy,
),
),
), // to here.
/// Fg-Rocks
Image.asset(AssetPaths.titleFgBase),
/// Fg-Receive
_LitImage(
imgSrc: AssetPaths.titleFgReceive,
color: orbColor,
pulseEffect: _pulseEffect,
lightAmt: _finalReceiveLightAmt,
),
/// Fg-Emit
_LitImage(
imgSrc: AssetPaths.titleFgEmit,
color: emitColor,
pulseEffect: _pulseEffect,
lightAmt: _finalEmitLightAmt,
),
/// UI
Positioned.fill(
child: TitleScreenUi(
difficulty: _difficulty,
onDifficultyFocused: _handleDifficultyFocused,
onDifficultyPressed: _handleDifficultyPressed,
onStartPressed: _handleStartPressed,
),
),
],
).animate().fadeIn(duration: 1.seconds, delay: .3.seconds);
},
),
),
),
);
}
نتیجه نهایی شامل انیمیشنها، سایهزنهای قطعه، و جلوههای ذرهای بر روی چندین پلتفرم است!
ذرات را در همه جا اضافه کنید - حتی وب
یک مشکل جزئی با کد به شکل فعلی وجود دارد. هنگامی که Flutter در وب اجرا می شود، دو موتور رندر جایگزین وجود دارد که می توان از آنها استفاده کرد: موتور CanvasKit که به طور پیش فرض در مرورگرهای کلاس دسکتاپ استفاده می شود و یک رندر HTML DOM که به طور پیش فرض برای دستگاه های تلفن همراه استفاده می شود. مشکل این است که رندر HTML DOM از shader های قطعه پشتیبانی نمی کند.
راه حل این است که فقط با استفاده از رندر CanvasKit برای وب بسازید. برای این کار یک پرچم به دستور build به صورت زیر اضافه کنید:
$ flutter build web --web-renderer canvaskit Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 7692 bytes (99.5% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app. Font asset "CupertinoIcons.ttf" was tree-shaken, reducing it from 257628 to 1172 bytes (99.5% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app. Compiling lib/main.dart for the Web... 15.6s ✓ Built build/web
در اینجا تمام کارهای سخت شما است که این بار در مرورگر کروم نشان داده شده است.
8. تبریک می گویم
شما یک صفحه معرفی بازی با امکانات کامل با انیمیشن ها، سایه زن های قطعه، و انیمیشن های ذرات ساخته اید! اکنون می توانید از این تکنیک ها در تمام پلتفرم هایی که Flutter پشتیبانی می کند استفاده کنید.
بیشتر بدانید
- بسته
flutter_animate
را بررسی کنید - پشتیبانی Flutter برای مستندات Fragment Shaders را مرور کنید
- کتاب سایه ها اثر پاتریسیو گونزالس ویو و جن لو
- اسباب بازی سایه زن ، زمین بازی سایه زن مشترک
- simple_shader ، یک پروژه نمونه شیدرهای فرگمنت فلاتر ساده