1. Hinweis
Flutter eignet sich hervorragend dafür, mit der Kombination aus Hot Refresh und deklarativer Benutzeroberfläche schnell und iterativ neue Benutzeroberflächen zu erstellen. Es kann jedoch vorkommen, dass Sie einer Schnittstelle zusätzliche Interaktivität hinzufügen müssen. Dabei kann es sich um eine einfache Animation wie die Animation einer Schaltfläche beim Bewegen des Mauszeigers oder um eine dramatische Animation wie ein Shader handeln, bei der die Benutzeroberfläche unter Verwendung der GPU-Leistung verzerrt wird.
In diesem Codelab entwickeln Sie eine Flutter-App, die mithilfe von Animationen, Shadern und Partikelfeldern eine Benutzeroberfläche erschafft, die an die Science-Fiction-Filme und -TV-Sendungen erinnert, die wir uns alle gerne ohne Programmieren ansehen.
Aufgaben
Du erstellst die erste Menüseite für ein postapokalyptisches Sci-Fi-Spiel. Es gibt einen Titel mit einem Fragment-Shader, der den Text stichproben soll, um ihn visuell zu animieren, ein Schwierigkeitsmenü, das das Farbthema der Seite mit zahlreichen Animationen ändert, und eine animierte Kugel, die mit einem zweiten Fragment-Shader dargestellt wird. Wenn das nicht ausreicht, können Sie am Ende des Codelabs einen subtilen Partikeleffekt hinzufügen, um die Seite zu bewegen und Interesse zu wecken.
Die folgenden Screenshots zeigen die App, die Sie auf den drei unterstützten Desktop-Betriebssystemen erstellen: Windows, Linux und macOS. Der Vollständigkeit halber wird eine Webbrowserversion bereitgestellt, die ebenfalls unterstützt wird. Animationen und Fragment-Shader überall!
Vorbereitung
- Grundkenntnisse der Flutter-Entwicklung mit Dart aus dem Codelab Deine erste Flutter-App
Lerninhalte
- Mit
flutter_animate
ausdrucksstarke Animationen erstellen - Unterstützung von Fragment-Shadern von Flutter auf Computern und im Web verwenden
- So fügen Sie Ihrer App mit
particle_field
Partikelanimationen hinzu
Voraussetzungen
- Das Flutter SDK
- VS Code-Einrichtung für Flutter und Dart
- Desktopunterstützung für Flutter unter Windows, Linux oder macOS einrichten
- Websupport für Flutter einrichten
2. Erste Schritte
Starter-Code herunterladen
- Rufen Sie dieses GitHub-Repository auf.
- Klicken Sie auf Code > Lade eine ZIP-Datei herunter, um den gesamten Code für dieses Codelab herunterzuladen.
- Entpacke die heruntergeladene ZIP-Datei, um den Stammordner
codelabs-main
zu entpacken. Sie benötigen nur das Unterverzeichnisnext-gen-ui/
, das die Ordnerstep_01
undstep_06
enthält. Diese Ordner enthalten den Quellcode, auf dem Sie für jeden Schritt in diesem Codelab aufbauen.
Projektabhängigkeiten herunterladen
- Klicken Sie in VS Code auf File > Ordner öffnen > codelabs-main > Benutzeroberfläche der nächsten Generation > Schritt 01, um das Startprojekt zu öffnen.
- Wenn ein VS Code-Dialogfeld angezeigt wird, in dem Sie aufgefordert werden, die erforderlichen Pakete für die Start-App herunterzuladen, klicken Sie auf Getpackages (Pakete abrufen).
- Wenn kein VS Code-Dialogfeld angezeigt wird, in dem Sie aufgefordert werden, die erforderlichen Pakete für die Start-App herunterzuladen, öffnen Sie Ihr Terminal, gehen Sie zum Ordner
step_01
und führen Sie den Befehlflutter pub get
aus.
Start-App ausführen
- Wählen Sie in VS Code entweder das ausgeführte Desktop-Betriebssystem aus oder Chrome, wenn Sie Ihre Anwendung in einem Webbrowser testen möchten.
Wenn Sie macOS als Bereitstellungsziel verwenden, sehen Sie beispielsweise Folgendes:
Wenn Sie Chrome als Bereitstellungsziel verwenden, wird Folgendes angezeigt:
- Öffnen Sie die Datei
lib/main.dart
und klicken Sie auf Fehlerbehebung starten. Die App wird auf Ihrem Desktop-Betriebssystem oder in einem Chrome-Browser gestartet.
Die Starter-App entdecken
Beachten Sie in der Start-App Folgendes:
- Die Benutzeroberfläche kann jetzt erstellt werden.
- Das Verzeichnis
assets
enthält die Art-Assets und zwei Fragment-Shader, die Sie verwenden werden. - In der Datei
pubspec.yaml
sind bereits die Assets und eine Sammlung von Pub-Paketen aufgeführt, die du verwenden wirst. - Das Verzeichnis
lib
enthält die obligatorischemain.dart
-Datei, eineassets.dart
-Datei, in der der Pfad der Art-Assets und Fragment-Shader angegeben ist, sowie einestyles.dart
-Datei, in der die von Ihnen verwendeten TextStyles und Colors aufgeführt sind. - Das Verzeichnis
lib
enthält außerdem eincommon
-Verzeichnis mit einigen nützlichen Dienstprogrammen, die du in diesem Codelab verwenden wirst, und das Verzeichnisorb_shader
, das einenWidget
enthält, der zum Anzeigen der Kugel mit einem Vertex-Shader verwendet wird.
Nach dem Starten der App wird Folgendes angezeigt:
3. Malen Sie die Szene
In diesem Schritt platzieren Sie alle Assets für Hintergrundgrafiken in Ebenen auf dem Bildschirm. Erwartungsgemäß wird er anfangs seltsam monochrom erscheinen, aber Sie fügen der Szene am Ende dieses Schritts Farben hinzu.
Assets zur Szene hinzufügen
- Erstellen Sie in Ihrem Verzeichnis
lib
ein Verzeichnistitle_screen
und fügen Sie dann die Dateititle_screen.dart
hinzu. Fügen Sie der Datei den folgenden Inhalt hinzu.
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),
],
),
),
);
}
}
Dieses Widget enthält die Szene mit in Ebenen gestapelten Assets. Die Hintergrund-, Mittel- und Vordergrundebene werden jeweils durch eine Gruppe von zwei oder drei Bildern dargestellt. Diese Bilder werden mit verschiedenen Farben beleuchtet, um die Bewegung des Lichts durch die Szene einzufangen.
- Fügen Sie in die Datei
main.dart
den folgenden Inhalt ein:
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
);
}
}
Dadurch wird die Benutzeroberfläche der App durch die monochrome Szene ersetzt, die durch die Art-Assets erzeugt wird. Als Nächstes färben Sie jede Ebene ein.
Dienstprogramm zum Ausmalen von Bildern hinzufügen
Fügen Sie ein Dienstprogramm zum Ausmalen von Bildern hinzu, indem Sie der Datei title_screen.dart
den folgenden Inhalt hinzufügen:
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.
Dieses _LitImage
-Dienstprogramm-Widget verfärbt jedes der Art-Assets neu, je nachdem, ob sie Licht ausstrahlen oder empfangen. Möglicherweise wird eine Linter-Warnung ausgelöst, da du dieses neue Widget noch nicht verwendest.
Farbe bemalen
Bringen Sie in Farbe, indem Sie die Datei title_screen.dart
so ändern:
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,
);
}
}
Hier ist noch einmal die App, diesmal mit grün gefärbten Art-Assets.
4. Benutzeroberfläche hinzufügen
In diesem Schritt platzieren Sie eine Benutzeroberfläche über der Szene, die Sie im vorherigen Schritt erstellt haben. Dazu gehören der Titel, die Schaltflächen zur Auswahl des Schwierigkeitsgrads und die wichtige Schaltfläche Start.
Titel hinzufügen
- Erstellen Sie eine
title_screen_ui.dart
-Datei im Verzeichnislib/title_screen
und fügen Sie der Datei den folgenden Inhalt hinzu:
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),
],
);
}
}
Dieses Widget enthält den Titel und alle Schaltflächen, aus denen die Benutzeroberfläche dieser App besteht.
- Aktualisieren Sie die Datei
lib/title_screen/title_screen.dart
so:
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,
);
}
}
Wenn Sie diesen Code ausführen, wird der Titel angezeigt, der den Anfang der Benutzeroberfläche darstellt.
Schaltflächen für den Schwierigkeitsgrad hinzufügen
- Aktualisieren Sie
title_screen_ui.dart
, indem Sie einen neuen Import für das Paketfocusable_control_builder
hinzufügen:
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';
- Fügen Sie dem
TitleScreenUi
-Widget Folgendes hinzu:
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.
],
),
);
}
}
- Fügen Sie die folgenden beiden Widgets hinzu, um die Schaltflächen für den Schwierigkeitsgrad zu implementieren:
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),
),
],
),
),
);
},
);
}
}
- Konvertieren Sie das
TitleScreen
-Widget von zustandslos in zustandsorientiert und fügen Sie einen Zustand hinzu, um das Farbschema je nach Schwierigkeitsgrad ändern zu können:
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,
);
}
}
Hier ist die Benutzeroberfläche mit zwei Schwierigkeitsgraden. Beachten Sie, dass die Schwierigkeitsgrade, die als Masken auf Graustufenbildern angewendet werden, einen realistischen, reflektierenden Effekt erzeugen!
Startschaltfläche hinzufügen
- Aktualisieren Sie die Datei
title_screen_ui.dart
. Fügen Sie demTitleScreenUi
-Widget Folgendes hinzu:
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.
],
),
);
}
}
- Fügen Sie das folgende Widget hinzu, um die Startschaltfläche zu implementieren:
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)),
],
),
),
],
),
);
},
);
}
}
Und hier ist die App mit einer vollständigen Sammlung von Schaltflächen.
5. Animation hinzufügen
In diesem Schritt animieren Sie die Benutzeroberfläche und die Farbübergänge für die Art-Assets.
Titel einblenden
In diesem Schritt verwenden Sie mehrere Ansätze, um eine Flutter-App zu animieren. Eine der möglichen Methoden ist die Verwendung von flutter_animate
. Von diesem Paket unterstützte Animationen können bei jedem Hot-Update Ihrer App automatisch wiedergegeben werden, um Entwicklungsiterationen zu beschleunigen.
- Ändern Sie den Code in
lib/main.dart
so:
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(),
);
}
}
- Um das
flutter_animate
-Paket nutzen zu können, müssen Sie es importieren. Fügen Sie den Import inlib/title_screen/title_screen_ui.dart
so hinzu:
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 {
- Fügen Sie dem Titel eine Animation hinzu, indem Sie das
_TitleText
-Widget so bearbeiten:
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.
);
}
}
- Klicken Sie auf Aktualisieren, um zu sehen, wie der Titel eingeblendet wird.
Schaltflächen für den Schwierigkeitsgrad ein- und ausblenden
- Füge dem anfänglichen Aussehen der Schaltflächen für den Schwierigkeitsgrad eine Animation hinzu, indem du das
_DifficultyBtns
-Widget so bearbeitest:
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),
],
);
}
}
- Drücke auf Aktualisieren, um die Schaltflächen für den Schwierigkeitsgrad und einen kleinen Schieberegler nach oben zu sehen.
Startschaltfläche einblenden
- Fügen Sie der Startschaltfläche eine Animation hinzu, indem Sie die Zustandsklasse
_StartBtnState
so bearbeiten:
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.
);
}
}
- Drücke auf Aktualisieren, um die Schaltflächen für den Schwierigkeitsgrad und einen kleinen Schieberegler nach oben zu sehen.
Schwierigkeitsgrad des Hover-Effekts animieren
Animationen zu den Schaltflächen für den Schwierigkeitsgrad hinzufügen Dazu bearbeiten Sie die Zustandsklasse _DifficultyBtn
so:
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),
),
],
),
),
);
},
);
}
}
Die Schaltflächen für den Schwierigkeitsgrad zeigen jetzt den BoxDecoration
an, wenn der Mauszeiger auf eine Schaltfläche bewegt wird, die nicht ausgewählt wurde.
Farbänderung animieren
- Die Hintergrundfarbe ändert sich sofort und scharf. Es ist besser, die ausgeleuchteten Bilder zwischen den Farbschemas zu animieren.
flutter_animate
zulib/title_screen/title_screen.dart
hinzufügen:
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 {
- Fügen Sie ein
_AnimatedColors
-Widget inlib/title_screen/title_screen.dart
hinzu:
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!);
},
);
},
);
}
}
- Verwenden Sie das soeben erstellte Widget, um die Farben der beleuchteten Bilder zu animieren. Aktualisieren Sie dazu die Methode
build
in_TitleScreenState
so:
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.
),
);
}
}
Mit dieser letzten Bearbeitung haben Sie jedem Element auf dem Bildschirm Animationen hinzugefügt und es sieht viel besser aus!
6. Fragment-Shader hinzufügen
In diesem Schritt fügen Sie der App Fragment-Shader hinzu. Zuerst modifizieren Sie den Titel mit einem Shader, um ihm ein dystopisches Flair zu verleihen. Anschließend fügen Sie einen zweiten Shader hinzu, um einen Kugel zu erstellen, der als zentraler Mittelpunkt der Seite dient.
Titel mit einem Fragment-Shader verzerrt
Mit dieser Änderung führen Sie das Paket provider
ein, mit dem die kompilierten Shader in der Widget-Struktur übergeben werden können. Wenn Sie wissen möchten, wie die Shader geladen werden, sehen Sie sich die Implementierung in lib/assets.dart
an.
- Ändern Sie den Code in
lib/main.dart
so:
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(),
);
}
}
- Wenn Sie das Paket
provider
und die instep_01
enthaltenen Shader-Dienstprogramme nutzen möchten, müssen Sie diese importieren. Fügen Sie inlib/title_screen/title_screen_ui.dart
neue Importe hinzu:
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 {
- Verzerre den Titel mit dem Shader, indem du das
_TitleText
-Widget so bearbeitest:
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.
}
}
Der Titel sollte verzerrt sein, wie Sie es in einer dystopischen Zukunft erwarten würden.
Kugel hinzufügen
Fügen Sie nun die Kugel in der Mitte des Fensters hinzu. Du musst der Startschaltfläche einen onPressed
-Callback hinzufügen.
- Ändern Sie
TitleScreenUi
inlib/title_screen/title_screen_ui.dart
so:
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
),
),
),
],
),
);
}
}
Nachdem du die Startschaltfläche mit einem Callback geändert hast, musst du umfangreiche Änderungen an der Datei lib/title_screen/title_screen.dart
vornehmen.
- Ändern Sie die Importe so:
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 {
- Passen Sie
_TitleScreenState
an Folgendes an. Nahezu jeder Teil des Kurses wird in irgendeiner Weise modifiziert.
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);
},
),
),
),
);
}
}
- Ändern Sie
_LitImage
so:
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.
}
}
Dies ist das Ergebnis dieser Ergänzung.
7. Partikelanimationen hinzufügen
In diesem Schritt fügen Sie Partikelanimationen hinzu, um der App eine subtile pulsierende Bewegung zu verleihen.
Partikel überall hinzufügen
- Erstellen Sie eine neue
lib/title_screen/particle_overlay.dart
-Datei und fügen Sie den folgenden Code hinzu:
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,
);
}
},
);
}
}
- Ändern Sie die Importe für
lib/title_screen/title_screen.dart
so:
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 {
- Fügen Sie der Benutzeroberfläche das
ParticleOverlay
hinzu. Ändern Sie dazu die Methodebuild
von_TitleScreenState
so:
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);
},
),
),
),
);
}
Das Endergebnis umfasst Animationen, Fragment-Shader und Partikeleffekte – auf mehreren Plattformen!
Partikel überall hinzufügen – sogar im Web
Es gibt ein kleines Problem mit dem Code in seiner jetzigen Form. Wenn Flutter im Web ausgeführt wird, stehen zwei alternative Rendering-Engines zur Verfügung: die CanvasKit-Engine, die standardmäßig in Browsern von Desktop-Klassen verwendet wird, und einen HTML-DOM-Renderer, der standardmäßig für Mobilgeräte verwendet wird. Das Problem ist, dass der HTML-DOM-Renderer keine Fragment-Shader unterstützt.
Die Lösung besteht darin, nur mit dem CanvasKit-Renderer für das Web zu erstellen. Dazu fügen Sie dem Build-Befehl wie folgt ein Flag hinzu:
$ 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
Hier sehen Sie Ihre harte Arbeit – diesmal im Chrome-Browser.
8. Glückwunsch
Sie haben einen Spielstartbildschirm mit allen Funktionen und Animationen, Fragment-Shadern und Partikelanimationen erstellt. Sie können diese Techniken jetzt auf allen Plattformen verwenden, die von Flutter unterstützt werden.
Weitere Informationen
flutter_animate
-Paket ansehen- Dokumentation zur Flutter-Unterstützung für Fragment Shaders lesen
- The Book of Shaders von Patricio Gonzalez Vivo und Jen Lowe
- Shader Toy, ein gemeinsam genutzter Shader-Spielplatz
- simple_shader, ein einfaches Beispielprojekt für Flutter-Fragment-Shader