1. ก่อนเริ่มต้น
Flutter คือความสามารถที่ยอดเยี่ยมในการช่วยให้นักพัฒนาซอฟต์แวร์สร้างอินเทอร์เฟซผู้ใช้ใหม่ได้อย่างรวดเร็วซ้ำๆ โดยอาศัยการโหลดซ้ำและ UI แบบประกาศสถานะ แต่ก็มีบางครั้งที่คุณจำเป็นต้องเพิ่มการโต้ตอบเพิ่มเติมลงในอินเทอร์เฟซ การแตะเหล่านี้สามารถทำได้ง่าย เพียงแค่ทำให้ปุ่มเคลื่อนไหวเมื่อวางเมาส์เหนือปุ่ม หรือมีความตื่นตาตื่นใจเหมือนเป็นแถบเฉดสีที่บิดเบี้ยวอินเทอร์เฟซผู้ใช้ด้วยพลังของ GPU
ใน Codelab นี้ คุณจะได้สร้างแอป Flutter ที่ใช้พลังของภาพเคลื่อนไหว เครื่องมือให้เฉดสี และฟิลด์อนุภาคเพื่อสร้างอินเทอร์เฟซผู้ใช้ที่จะกระตุ้นให้มีภาพยนตร์และรายการทีวีแนวนิยายวิทยาศาสตร์ที่พวกเราทุกคนชอบดูตอนไม่ได้เขียนโค้ด
สิ่งที่คุณจะสร้าง
คุณจะได้สร้างหน้าเมนูเริ่มต้นสำหรับเกมธีมไซไฟหลังวันสิ้นโลก มีชื่อที่มีตัวใส่เฉดสี Fragment ที่แสดงตัวอย่างข้อความเพื่อทำให้เป็นภาพเคลื่อนไหว, เมนูระดับความยากที่เปลี่ยนธีมสีของหน้าด้วยการแสดงภาพเคลื่อนไหวจำนวนมาก และลูกแก้วที่เคลื่อนไหวได้ซึ่งแต่งด้วย Fragment Frame หากแค่นั้นยังไม่พอ ที่ส่วนท้ายของ Codelab จะใส่เอฟเฟกต์อนุภาคเล็กๆ เพื่อเพิ่มการเคลื่อนไหวและความน่าสนใจให้กับหน้าเว็บ
ภาพหน้าจอต่อไปนี้แสดงแอปที่คุณจะสร้างบนระบบปฏิบัติการเดสก์ท็อป 3 ระบบที่รองรับ ได้แก่ Windows, Linux และ macOS เพื่อความสมบูรณ์ ระบบจะจัดเตรียมเวอร์ชันของเว็บเบราว์เซอร์ (รองรับด้วย) ให้ด้วย ภาพเคลื่อนไหวและเครื่องมือให้เฉดสี Fragment ทุกที่
ข้อกำหนดเบื้องต้น
- ความรู้พื้นฐานเกี่ยวกับการพัฒนา Flutter ด้วย Dart ตามที่อธิบายไว้ใน Codelab ของแอป Flutter แรกของคุณ
สิ่งที่คุณจะได้เรียนรู้
- วิธีใช้
flutter_animate
เพื่อสร้างภาพเคลื่อนไหวที่สื่ออารมณ์ชัดเจน - วิธีใช้การรองรับเครื่องมือใส่เฉดสีส่วนย่อยของ Flutter ในเดสก์ท็อปและเว็บ
- วิธีเพิ่มภาพเคลื่อนไหวของอนุภาคลงในแอปด้วย
particle_field
สิ่งที่ต้องมี
- Flutter SDK
- การตั้งค่า VS Code สำหรับ Flutter และ Dart
- การตั้งค่าการรองรับเดสก์ท็อปสำหรับ Flutter สำหรับ Windows, Linux หรือ macOS
- การตั้งค่าการสนับสนุนผ่านเว็บสำหรับ Flutter
2. เริ่มต้นใช้งาน
ดาวน์โหลดโค้ดเริ่มต้น
- ไปที่ที่เก็บ GitHub นี้
- คลิกโค้ด > ดาวน์โหลดรหัสไปรษณีย์เพื่อดาวน์โหลดรหัสทั้งหมดสำหรับ Codelab นี้
- แตกไฟล์ ZIP ที่ดาวน์โหลดเพื่อแตกโฟลเดอร์รูท
codelabs-main
คุณต้องใช้เพียงไดเรกทอรีย่อยnext-gen-ui/
ซึ่งมีโฟลเดอร์step_01
ถึงstep_06
ซึ่งมีซอร์สโค้ดที่คุณสร้างขึ้นสำหรับแต่ละขั้นตอนใน Codelab นี้
ดาวน์โหลดทรัพยากร Dependency ของโปรเจ็กต์
- ใน VS Code ให้คลิก File > เปิดโฟลเดอร์ > Codelabs-main > UI รุ่นถัดไป > Step_01 เพื่อเปิดโปรเจ็กต์เริ่มต้น
- หากคุณเห็นกล่องโต้ตอบ VS Code ที่แจ้งให้ดาวน์โหลดแพ็กเกจที่จำเป็นสำหรับแอปเริ่มต้น ให้คลิกรับแพ็กเกจ
- หากไม่เห็นกล่องโต้ตอบ "VS Code" ที่แจ้งให้ดาวน์โหลดแพ็กเกจที่จำเป็นสำหรับแอปเริ่มต้น ให้เปิดเทอร์มินัลแล้วไปที่โฟลเดอร์
step_01
และเรียกใช้คำสั่งflutter pub get
เรียกใช้แอปเริ่มต้น
- ใน VS Code ให้เลือกระบบปฏิบัติการเดสก์ท็อปที่กำลังใช้อยู่หรือ Chrome หากต้องการทดสอบแอปในเว็บเบราว์เซอร์
ต่อไปนี้คือสิ่งที่คุณจะเห็นเมื่อใช้ macOS เป็นเป้าหมายในการทำให้ใช้งานได้
ต่อไปนี้คือสิ่งที่คุณจะเห็นเมื่อใช้ Chrome เป็นเป้าหมายในการทำให้ใช้งานได้
- เปิดไฟล์
lib/main.dart
แล้วคลิก เริ่มแก้ไขข้อบกพร่อง แอปจะเปิดขึ้นในระบบปฏิบัติการบนเดสก์ท็อปหรือในเบราว์เซอร์ Chrome
สำรวจแอปเริ่มต้น
ในแอปเริ่มต้น ให้สังเกตสิ่งต่อไปนี้
- UI พร้อมให้คุณสร้างแล้ว
- ไดเรกทอรี
assets
มีเนื้อหาศิลปะและเครื่องมือปรับแสงเงา Fragment 2 รายการที่คุณจะใช้ - ไฟล์
pubspec.yaml
แสดงรายการเนื้อหาและคอลเล็กชันแพ็กเกจผู้เผยแพร่โฆษณาที่จะใช้อยู่แล้ว - ไดเรกทอรี
lib
มีไฟล์main.dart
ที่บังคับใช้ ไฟล์assets.dart
ที่แสดงเส้นทางของเนื้อหาศิลปะและตัวปรับแสงเงา Fragment และไฟล์styles.dart
ที่แสดง TextStyles และ Colors ที่จะใช้ - ไดเรกทอรี
lib
ยังมีไดเรกทอรีcommon
ซึ่งมียูทิลิตีที่มีประโยชน์จำนวนหนึ่งที่คุณจะใช้ใน Codelab นี้ และไดเรกทอรีorb_shader
ซึ่งมีWidget
ที่จะใช้เพื่อแสดงลูกโลกที่มีตัวปรับแสงเงา Vertex
นี่คือสิ่งที่คุณจะเห็นเมื่อเริ่มแอป
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),
],
),
),
);
}
}
วิดเจ็ตนี้มีฉากที่มีเนื้อหากองซ้อนกันเป็นเลเยอร์ เลเยอร์พื้นหลัง เลเยอร์กลาง และเลเยอร์พื้นหน้าจะแสดงด้วยกลุ่มภาพ 2 หรือ 3 ภาพ รูปภาพเหล่านี้จะสว่างด้วยสีต่างๆ เพื่อจับภาพการเคลื่อนที่ของแสงผ่านฉาก
- เพิ่มเนื้อหาต่อไปนี้ในไฟล์
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
);
}
}
ซึ่งจะแทนที่ UI ของแอปด้วยฉากขาวดำที่เนื้อหาศิลปะสร้างขึ้น ต่อไป ให้คุณลงสีแต่ละเลเยอร์
เพิ่มยูทิลิตีการระบายสีรูปภาพ
เพิ่มยูทิลิตีการระบายสีรูปภาพโดยการเพิ่มเนื้อหาต่อไปนี้ลงในไฟล์ 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. เพิ่ม UI
ในขั้นตอนนี้ คุณจะวางอินเทอร์เฟซผู้ใช้บนฉากที่สร้างขึ้นในขั้นตอนก่อนหน้า ซึ่งรวมถึงชื่อ ปุ่มตัวเลือกความยาก และปุ่มเริ่มต้นที่สำคัญทั้งหมด
เพิ่มชื่อ
- สร้างไฟล์
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,
);
}
}
การเรียกใช้โค้ดนี้จะแสดงชื่อ ซึ่งเป็นจุดเริ่มต้นของอินเทอร์เฟซผู้ใช้
เพิ่มปุ่มระดับความยาก
- อัปเดต
title_screen_ui.dart
ด้วยการเพิ่มการนำเข้าใหม่สำหรับแพ็กเกจfocusable_control_builder
:
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.
],
),
);
}
}
- เพิ่มวิดเจ็ต 2 รายการต่อไปนี้เพื่อใช้ปุ่มระดับความยาก:
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,
);
}
}
ต่อไปนี้เป็น UI ในการตั้งค่าความยาก 2 แบบที่แตกต่างกัน โปรดสังเกตว่าสีระดับความยากที่ใช้เป็นมาสก์ให้กับรูปภาพโทนสีเทาจะสร้างเอฟเฟกต์ที่สมจริงและสะท้อนแสงได้
เพิ่มปุ่มเริ่มต้น
- อัปเดตไฟล์
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 เคลื่อนไหว หนึ่งในวิธีการคือการใช้ flutter_animate
ภาพเคลื่อนไหวที่ขับเคลื่อนโดยแพ็กเกจนี้จะเล่นซ้ำโดยอัตโนมัติทุกครั้งที่คุณ Hot Load แอปเพื่อเร่งการพัฒนาซอฟต์แวร์ให้เร็วขึ้น
- แก้ไขโค้ดใน
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.
);
}
}
- กดโหลดซ้ำ เพื่อดูชื่อหนังสือที่ค่อยๆ ปรากฏขึ้น
ค่อยๆ จางปุ่มความยาก
- เพิ่มภาพเคลื่อนไหวให้กับลักษณะที่ปรากฏเริ่มต้นของปุ่มความยากด้วยการแก้ไขวิดเจ็ต
_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
ดังนี้
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. เพิ่มตัวปรับแสงเงา Fragment
ในขั้นตอนนี้คุณจะเพิ่ม Fragment Shape ลงในแอป ขั้นแรก ให้ใช้เฉดสีเพื่อแก้ไขชื่อ ให้อารมณ์แบบดิสโทเปียมากขึ้น จากนั้นให้คุณเพิ่มตัวปรับแสงเงาที่ 2 เพื่อสร้างลูกโลกที่ทำหน้าที่เป็นจุดโฟกัสตรงกลางของหน้า
การทําให้ชื่อบิดเบี้ยวด้วยตัวปรับแสงเงา Fragment
การเปลี่ยนแปลงนี้ทำให้คุณได้แนะนำแพ็กเกจ 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.
}
}
คุณควรจะเห็นชื่อที่ผิดเพี้ยนไป อย่างที่คิดไว้ในอนาคตแบบดิสโทเปีย
เพิ่มลูกโลก
จากนั้นเพิ่มลูกโลกที่กึ่งกลางของหน้าต่าง คุณต้องเพิ่ม Callback 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
),
),
),
],
),
);
}
}
ตอนนี้คุณได้แก้ไขปุ่มเริ่มต้นด้วย Callback แล้ว คุณจะต้องทำการแก้ไขจำนวนมากกับไฟล์ 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 {
- เพิ่ม
ParticleOverlay
ลงใน UI โดยแก้ไขเมธอดbuild
ของ_TitleScreenState
ดังนี้
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 ทำงานบนเว็บ จะมีเครื่องมือแสดงผล 2 แบบที่ใช้ได้ ได้แก่ เครื่องมือ CanvasKit ซึ่งใช้เป็นค่าเริ่มต้นในเบราว์เซอร์คลาสบนเดสก์ท็อป และเครื่องมือแสดงผล HTML DOM ซึ่งระบบจะใช้ในอุปกรณ์เคลื่อนที่โดยค่าเริ่มต้น ปัญหาคือตัวแสดงผล HTML DOM ไม่รองรับตัวปรับแสงเงาส่วนย่อย
วิธีแก้ไขคือให้สร้างบิลด์สำหรับเว็บโดยใช้ตัวแสดงผล CanvasKit เท่านั้น ในการดำเนินการนี้ ให้เพิ่มแฟล็กไปยังคำสั่งบิลด์ดังนี้
$ 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
นี่คือความทุ่มเททั้งหมดของคุณ ซึ่งแสดงในเบราว์เซอร์ Chrome ในครั้งนี้
8. ขอแสดงความยินดี
คุณได้สร้างหน้าจอแนะนำเกมที่มีฟีเจอร์เต็มรูปแบบพร้อมภาพเคลื่อนไหว ตัวใส่เฉดสีส่วนย่อย และภาพเคลื่อนไหวอนุภาค ตอนนี้คุณจะใช้เทคนิคเหล่านี้ในทุกแพลตฟอร์มที่ Flutter รองรับได้แล้ว
ดูข้อมูลเพิ่มเติม
- ดูแพ็กเกจ
flutter_animate
- ดูเอกสารประกอบการรองรับ Flutter สำหรับ Fragment Shaders
- The Book of Shaders โดย Patricio Gonzalez Vivo และ Jen Lowe
- Shader toy สนามเด็กเล่นเฉดสีสำหรับทำงานร่วมกัน
- simple_shader เป็นโปรเจ็กต์ตัวอย่างสำหรับ Fragment ของ Flutter อย่างง่าย