การสร้าง UI รุ่นใหม่ใน Flutter

1. ก่อนเริ่มต้น

Flutter คือความสามารถที่ยอดเยี่ยมในการช่วยให้นักพัฒนาซอฟต์แวร์สร้างอินเทอร์เฟซผู้ใช้ใหม่ได้อย่างรวดเร็วซ้ำๆ โดยอาศัยการโหลดซ้ำและ UI แบบประกาศสถานะ แต่ก็มีบางครั้งที่คุณจำเป็นต้องเพิ่มการโต้ตอบเพิ่มเติมลงในอินเทอร์เฟซ การแตะเหล่านี้สามารถทำได้ง่าย เพียงแค่ทำให้ปุ่มเคลื่อนไหวเมื่อวางเมาส์เหนือปุ่ม หรือมีความตื่นตาตื่นใจเหมือนเป็นแถบเฉดสีที่บิดเบี้ยวอินเทอร์เฟซผู้ใช้ด้วยพลังของ GPU

ใน Codelab นี้ คุณจะได้สร้างแอป Flutter ที่ใช้พลังของภาพเคลื่อนไหว เครื่องมือให้เฉดสี และฟิลด์อนุภาคเพื่อสร้างอินเทอร์เฟซผู้ใช้ที่จะกระตุ้นให้มีภาพยนตร์และรายการทีวีแนวนิยายวิทยาศาสตร์ที่พวกเราทุกคนชอบดูตอนไม่ได้เขียนโค้ด

สิ่งที่คุณจะสร้าง

คุณจะได้สร้างหน้าเมนูเริ่มต้นสำหรับเกมธีมไซไฟหลังวันสิ้นโลก มีชื่อที่มีตัวใส่เฉดสี Fragment ที่แสดงตัวอย่างข้อความเพื่อทำให้เป็นภาพเคลื่อนไหว, เมนูระดับความยากที่เปลี่ยนธีมสีของหน้าด้วยการแสดงภาพเคลื่อนไหวจำนวนมาก และลูกแก้วที่เคลื่อนไหวได้ซึ่งแต่งด้วย Fragment Frame หากแค่นั้นยังไม่พอ ที่ส่วนท้ายของ Codelab จะใส่เอฟเฟกต์อนุภาคเล็กๆ เพื่อเพิ่มการเคลื่อนไหวและความน่าสนใจให้กับหน้าเว็บ

ภาพหน้าจอต่อไปนี้แสดงแอปที่คุณจะสร้างบนระบบปฏิบัติการเดสก์ท็อป 3 ระบบที่รองรับ ได้แก่ Windows, Linux และ macOS เพื่อความสมบูรณ์ ระบบจะจัดเตรียมเวอร์ชันของเว็บเบราว์เซอร์ (รองรับด้วย) ให้ด้วย ภาพเคลื่อนไหวและเครื่องมือให้เฉดสี Fragment ทุกที่

แอปที่เสร็จสิ้นซึ่งทำงานใน Windows

แอปที่ทำงานในเบราว์เซอร์ Chrome เสร็จแล้ว

แอปที่เสร็จสิ้นซึ่งทำงานบน Linux

แอปที่เสร็จสิ้นซึ่งทำงานใน macOS

ข้อกำหนดเบื้องต้น

  • ความรู้พื้นฐานเกี่ยวกับการพัฒนา Flutter ด้วย Dart ตามที่อธิบายไว้ใน Codelab ของแอป Flutter แรกของคุณ

สิ่งที่คุณจะได้เรียนรู้

สิ่งที่ต้องมี

2. เริ่มต้นใช้งาน

ดาวน์โหลดโค้ดเริ่มต้น

  1. ไปที่ที่เก็บ GitHub นี้
  2. คลิกโค้ด > ดาวน์โหลดรหัสไปรษณีย์เพื่อดาวน์โหลดรหัสทั้งหมดสำหรับ Codelab นี้
  3. แตกไฟล์ ZIP ที่ดาวน์โหลดเพื่อแตกโฟลเดอร์รูท codelabs-main คุณต้องใช้เพียงไดเรกทอรีย่อย next-gen-ui/ ซึ่งมีโฟลเดอร์ step_01 ถึง step_06 ซึ่งมีซอร์สโค้ดที่คุณสร้างขึ้นสำหรับแต่ละขั้นตอนใน Codelab นี้

ดาวน์โหลดทรัพยากร Dependency ของโปรเจ็กต์

  1. ใน VS Code ให้คลิก File > เปิดโฟลเดอร์ > Codelabs-main > UI รุ่นถัดไป > Step_01 เพื่อเปิดโปรเจ็กต์เริ่มต้น
  2. หากคุณเห็นกล่องโต้ตอบ VS Code ที่แจ้งให้ดาวน์โหลดแพ็กเกจที่จำเป็นสำหรับแอปเริ่มต้น ให้คลิกรับแพ็กเกจ

กล่องโต้ตอบ VS Code ที่แจ้งให้คุณดาวน์โหลดแพ็กเกจที่จำเป็นสำหรับแอปเริ่มต้น

  1. หากไม่เห็นกล่องโต้ตอบ "VS Code" ที่แจ้งให้ดาวน์โหลดแพ็กเกจที่จำเป็นสำหรับแอปเริ่มต้น ให้เปิดเทอร์มินัลแล้วไปที่โฟลเดอร์ step_01 และเรียกใช้คำสั่ง flutter pub get

เรียกใช้แอปเริ่มต้น

  1. ใน VS Code ให้เลือกระบบปฏิบัติการเดสก์ท็อปที่กำลังใช้อยู่หรือ Chrome หากต้องการทดสอบแอปในเว็บเบราว์เซอร์

ต่อไปนี้คือสิ่งที่คุณจะเห็นเมื่อใช้ macOS เป็นเป้าหมายในการทำให้ใช้งานได้

การตกแต่งแถบสถานะ VSCode ที่แสดงเป้าหมาย Flutter คือ macOS (ดาร์วิน)

ต่อไปนี้คือสิ่งที่คุณจะเห็นเมื่อใช้ Chrome เป็นเป้าหมายในการทำให้ใช้งานได้

การตกแต่งแถบสถานะ VSCode ที่แสดงเป้าหมาย Flutter คือ Chrome (web-javascript)

  1. เปิดไฟล์ lib/main.dart แล้วคลิก ปุ่มเล่นจาก VSCode เริ่มแก้ไขข้อบกพร่อง แอปจะเปิดขึ้นในระบบปฏิบัติการบนเดสก์ท็อปหรือในเบราว์เซอร์ Chrome

สำรวจแอปเริ่มต้น

ในแอปเริ่มต้น ให้สังเกตสิ่งต่อไปนี้

  • UI พร้อมให้คุณสร้างแล้ว
  • ไดเรกทอรี assets มีเนื้อหาศิลปะและเครื่องมือปรับแสงเงา Fragment 2 รายการที่คุณจะใช้
  • ไฟล์ pubspec.yaml แสดงรายการเนื้อหาและคอลเล็กชันแพ็กเกจผู้เผยแพร่โฆษณาที่จะใช้อยู่แล้ว
  • ไดเรกทอรี lib มีไฟล์ main.dart ที่บังคับใช้ ไฟล์ assets.dart ที่แสดงเส้นทางของเนื้อหาศิลปะและตัวปรับแสงเงา Fragment และไฟล์ styles.dart ที่แสดง TextStyles และ Colors ที่จะใช้
  • ไดเรกทอรี lib ยังมีไดเรกทอรี common ซึ่งมียูทิลิตีที่มีประโยชน์จำนวนหนึ่งที่คุณจะใช้ใน Codelab นี้ และไดเรกทอรี orb_shader ซึ่งมี Widget ที่จะใช้เพื่อแสดงลูกโลกที่มีตัวปรับแสงเงา Vertex

นี่คือสิ่งที่คุณจะเห็นเมื่อเริ่มแอป

แอป Codelab ที่ทำงานด้วยชื่อ "แทรก UI รุ่นถัดไปที่นี่..."

3. ทาสีฉาก

ในขั้นตอนนี้ คุณจะได้วางเนื้อหาภาพพื้นหลังทั้งหมดบนหน้าจอเป็นเลเยอร์ ให้คาดว่าภาพจะมีลักษณะเป็นโมโนโครมแปลกๆ ในตอนแรก แต่คุณจะเพิ่มสีลงในฉากในตอนท้ายของขั้นตอนนี้

เพิ่มเนื้อหาลงในฉาก

  1. สร้างไดเรกทอรี 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 ภาพ รูปภาพเหล่านี้จะสว่างด้วยสีต่างๆ เพื่อจับภาพการเคลื่อนที่ของแสงผ่านฉาก

  1. เพิ่มเนื้อหาต่อไปนี้ในไฟล์ 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 ของแอปด้วยฉากขาวดำที่เนื้อหาศิลปะสร้างขึ้น ต่อไป ให้คุณลงสีแต่ละเลเยอร์

แอป Codelab ที่ทำงานโดยใช้เพียงเนื้อหาศิลปะเบื้องหลัง ตอนกลาง และเบื้องหน้า โดยแสดงเป็นโมโนโครม

เพิ่มยูทิลิตีการระบายสีรูปภาพ

เพิ่มยูทิลิตีการระบายสีรูปภาพโดยการเพิ่มเนื้อหาต่อไปนี้ลงในไฟล์ 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,
    );
  }
}

ขอเชิญพบกับแอปอีกครั้ง โดยคราวนี้ชิ้นงานศิลปะจะย้อมสีเขียว

แอป Codelab ที่ทำงานพร้อมกับเนื้อหาศิลปะที่ย้อมสีเขียว

4. เพิ่ม UI

ในขั้นตอนนี้ คุณจะวางอินเทอร์เฟซผู้ใช้บนฉากที่สร้างขึ้นในขั้นตอนก่อนหน้า ซึ่งรวมถึงชื่อ ปุ่มตัวเลือกความยาก และปุ่มเริ่มต้นที่สำคัญทั้งหมด

เพิ่มชื่อ

  1. สร้างไฟล์ 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),
      ],
    );
  }
}

วิดเจ็ตนี้มีชื่อและปุ่มทั้งหมดที่ประกอบขึ้นเป็นอินเทอร์เฟซผู้ใช้สำหรับแอปนี้

  1. อัปเดตไฟล์ 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,
    );
  }
}

การเรียกใช้โค้ดนี้จะแสดงชื่อ ซึ่งเป็นจุดเริ่มต้นของอินเทอร์เฟซผู้ใช้

แอป Codelab ทำงานโดยใช้ชื่อว่า "Outpost [57] ในสิ่งที่ไม่รู้จัก"

เพิ่มปุ่มระดับความยาก

  1. อัปเดต 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';
  1. เพิ่มข้อมูลต่อไปนี้ลงในวิดเจ็ต 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.
        ],
      ),
    );
  }
}
  1. เพิ่มวิดเจ็ต 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),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}
  1. แปลงวิดเจ็ต 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 แบบที่แตกต่างกัน โปรดสังเกตว่าสีระดับความยากที่ใช้เป็นมาสก์ให้กับรูปภาพโทนสีเทาจะสร้างเอฟเฟกต์ที่สมจริงและสะท้อนแสงได้

แอป Codelab ที่เลือกระดับความยากปกติ ซึ่งแสดงชิ้นงานรูปภาพเป็นสีม่วงและน้ำเงินอมเขียว

แอป Codelab ที่เลือกความยากระดับฮาร์ดคอร์ แสดงเนื้อหารูปภาพเป็นสีส้มแดงร้อน

เพิ่มปุ่มเริ่มต้น

  1. อัปเดตไฟล์ 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.
        ],
      ),
    );
  }
}
  1. เพิ่มวิดเจ็ตต่อไปนี้เพื่อใช้ปุ่มเริ่มต้น

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)),
                  ],
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

และนี่คือแอปที่กำลังทำงานโดยมีปุ่มต่างๆ มากมาย

แอป Codelab ที่เลือกระดับความยากตามปกติ แสดงชื่อ ปุ่มความยาก และปุ่มเริ่มต้น

5. เพิ่มภาพเคลื่อนไหว

ในขั้นตอนนี้ คุณจะทำให้อินเทอร์เฟซผู้ใช้และการเปลี่ยนสีสำหรับเนื้อหาศิลปะเคลื่อนไหว

ตั้งชื่อให้จางลง

ในขั้นตอนนี้ คุณจะใช้วิธีมากมายในการทำให้แอป Flutter เคลื่อนไหว หนึ่งในวิธีการคือการใช้ flutter_animate ภาพเคลื่อนไหวที่ขับเคลื่อนโดยแพ็กเกจนี้จะเล่นซ้ำโดยอัตโนมัติทุกครั้งที่คุณ Hot Load แอปเพื่อเร่งการพัฒนาซอฟต์แวร์ให้เร็วขึ้น

  1. แก้ไขโค้ดใน 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(),
    );
  }
}
  1. หากต้องการใช้ประโยชน์จากแพ็กเกจ 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 {
  1. เพิ่มภาพเคลื่อนไหวลงในชื่อโดยแก้ไขวิดเจ็ต _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.
    );
  }
}
  1. กดโหลดซ้ำ เพื่อดูชื่อหนังสือที่ค่อยๆ ปรากฏขึ้น

ค่อยๆ จางปุ่มความยาก

  1. เพิ่มภาพเคลื่อนไหวให้กับลักษณะที่ปรากฏเริ่มต้นของปุ่มความยากด้วยการแก้ไขวิดเจ็ต _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),
      ],
    );
  }
}
  1. กดโหลดซ้ำเพื่อดูปุ่มความยากปรากฏขึ้นตามลำดับพร้อมสไลด์เล็กๆ ขึ้นเป็นโบนัส

ปุ่มเริ่มค่อยๆ จางลง

  1. เพิ่มภาพเคลื่อนไหวลงในปุ่มเริ่มต้นโดยแก้ไขคลาสสถานะ _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.
    );
  }
}
  1. กดโหลดซ้ำเพื่อดูปุ่มความยากปรากฏขึ้นตามลำดับพร้อมสไลด์เล็กๆ ขึ้นเป็นโบนัส

สร้างภาพเคลื่อนไหวของเอฟเฟกต์การวางเมาส์เหนือความยาก

เพิ่มภาพเคลื่อนไหวลงในปุ่มระดับความยาก สถานะโฮเวอร์โดยแก้ไขคลาสสถานะ _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 เมื่อวางเมาส์เหนือปุ่มที่ยังไม่ได้เลือกไว้

ทำให้การเปลี่ยนสีเคลื่อนไหว

  1. การเปลี่ยนสีพื้นหลังเป็นไปทันทีและรุนแรง การแสดงภาพเคลื่อนไหวในโทนสีต่างๆ จะให้ผลดีกว่า เพิ่ม 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 {
  1. เพิ่มวิดเจ็ต _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!);
          },
        );
      },
    );
  }
}
  1. ใช้วิดเจ็ตที่คุณเพิ่งสร้างเพื่อทำให้สีของรูปภาพที่สว่างเคลื่อนไหวโดยอัปเดตเมธอด 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

  1. แก้ไขโค้ดใน 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(),
    );
  }
}
  1. ในการใช้ประโยชน์จากแพ็กเกจ 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 {
  1. ทำให้ชื่อบิดเบี้ยวด้วยตัวปรับแสงเงาโดยการแก้ไขวิดเจ็ต _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 ที่ปุ่มเริ่มต้น

  1. ใน 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

  1. แก้ไขการนำเข้าดังนี้

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 {
  1. แก้ไข _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);
            },
          ),
        ),
      ),
    );
  }
}
  1. แก้ไข _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. เพิ่มภาพเคลื่อนไหวของอนุภาค

ในขั้นตอนนี้ คุณจะเพิ่มภาพเคลื่อนไหวของอนุภาคเพื่อสร้างการเคลื่อนไหวที่กะพริบเล็กน้อยให้กับแอป

เพิ่มอนุภาคในทุกที่

  1. สร้างไฟล์ 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,
          );
        }
      },
    );
  }
}
  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 {
  1. เพิ่ม 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 รองรับได้แล้ว

ดูข้อมูลเพิ่มเติม