Informacje o tym ćwiczeniu (w Codelabs)
1. Wprowadzenie
Flutter to pakiet narzędzi Google do tworzenia interfejsu użytkownika, który umożliwia tworzenie atrakcyjnych aplikacji kompilowanych natywnie na urządzenia mobilne, komputery i internet na podstawie jednej bazy kodu. Flutter współpracuje z dotychczasowym kodem, jest używany przez deweloperów i organizacje na całym świecie, a do tego jest bezpłatny i oparty na otwartym kodzie źródłowym.
W tym laboratorium programistycznym ulepszysz aplikację muzyczną Flutter, zmieniając ją z nudnej na piękną. W tym celu to ćwiczenie wykorzystuje narzędzia i interfejsy API omówione w Materiale 3.
Czego się nauczysz
- Jak napisać aplikację Flutter, która jest użyteczna i ładna na różnych platformach.
- Jak zaprojektować tekst w aplikacji, aby zapewnić lepsze wrażenia użytkownikom.
- Jak wybierać odpowiednie kolory, dostosowywać widżety, tworzyć własne motywy i szybko wdrażać tryb ciemny.
- Jak tworzyć aplikacje adaptacyjne na wiele platform.
- Jak tworzyć aplikacje, które dobrze wyglądają na każdym ekranie
- Jak dodać do aplikacji Flutter animacje, aby wyróżnić ją na tle innych.
Wymagania wstępne
To ćwiczenie z programowania zakłada, że masz już pewne doświadczenie w używaniu Fluttera. Jeśli nie, warto najpierw zapoznać się z podstawami. Te linki mogą być przydatne:
- Zapoznaj się z artykułem Tworzenie interfejsów użytkownika za pomocą Fluttera.
- Wypróbuj ćwiczenie z programowania Twoja pierwsza aplikacja Flutter
Co utworzysz
To ćwiczenie z programowania poprowadzi Cię przez proces tworzenia ekranu głównego aplikacji MyArtist
, czyli odtwarzacza muzyki, w której fani mogą śledzić ulubionych wykonawców. W tym artykule znajdziesz informacje o tym, jak zmodyfikować projekt aplikacji, aby wyglądała ona estetycznie na wszystkich platformach.
Te filmy pokazują, jak działa aplikacja po zakończeniu tego ćwiczenia:
Czego chcesz się nauczyć z tego Codelab?
2. Konfigurowanie środowiska programistycznego Flutter
Do wykonania tego laboratorium potrzebne są 2 programy: Flutter SDK i edytor.
Możesz uruchomić laboratorium programistyczne na dowolnym z tych urządzeń:
- fizyczne urządzenie Android lub iOS połączone z komputerem i ustawione w trybie programisty.
- Symulator iOS (wymaga zainstalowania narzędzi Xcode).
- Emulator Androida (wymaga skonfigurowania w Android Studio).
- przeglądarka (do debugowania wymagana jest przeglądarka Chrome);
- jako aplikacja na komputer z systemem Windows, Linux lub macOS; Musisz tworzyć aplikację na platformie, na której planujesz ją wdrożyć. Jeśli więc chcesz tworzyć aplikacje na komputery z systemem Windows, musisz korzystać z Windowsa, aby uzyskać dostęp do odpowiedniego łańcucha kompilacji. Istnieją wymagania dotyczące poszczególnych systemów operacyjnych, które omówiono szczegółowo na stronie docs.flutter.dev/desktop.
3. Pobierz aplikację startową z ćwiczeniami z programowania
Sklonuj je z GitHuba.
Aby skopiować ten projekt z GitHuba, uruchom te polecenia:
git clone https://github.com/flutter/codelabs.git cd codelabs/boring_to_beautiful/step_01/
Aby mieć pewność, że wszystko działa prawidłowo, uruchom aplikację Flutter jako aplikację komputerową, jak pokazano poniżej. Możesz też otworzyć ten projekt w swojej IDE i uruchomić aplikację za pomocą narzędzi dostępnych w tym środowisku.
flutter run
Gotowe! Kod inicjujący ekranu głównego MyArtist powinien być uruchomiony. Powinien pojawić się ekran główny MyArtist. Na komputerze wygląda dobrze, ale na urządzeniu mobilnym... Niezbyt dobrze. Po pierwsze, nie uwzględnia wycięcia. Nie martw się, to się da naprawić.
Omówienie kodu
Następnie zapoznaj się z kodem.
Otwórz plik lib/src/features/home/view/home_screen.dart
, który zawiera:
lib/src/features/home/view/home_screen.dart
import 'package:flutter/material.dart';
import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../../utils/adaptive_components.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final PlaylistsProvider playlistProvider = PlaylistsProvider();
final List<Playlist> playlists = playlistProvider.playlists;
final Playlist topSongs = playlistProvider.topSongs;
final Playlist newReleases = playlistProvider.newReleases;
final ArtistsProvider artistsProvider = ArtistsProvider();
final List<Artist> artists = artistsProvider.artists;
return LayoutBuilder(
builder: (context, constraints) {
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(playlists: playlists),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Ten plik importuje material.dart
i wdraża widżet stanowy za pomocą 2 klas:
- Instrukcja
import
udostępnia komponenty Material Design. - Klasa
HomeScreen
reprezentuje całą wyświetlaną stronę. - Metoda
build()
klasy_HomeScreenState
tworzy element główny drzewa widżetów, który wpływa na sposób tworzenia wszystkich widżetów w interfejsie użytkownika.
4. Korzystanie z typografii
Tekst jest wszędzie. Tekst to przydatny sposób na komunikowanie się z użytkownikiem. Czy Twoja aplikacja ma być przyjazna i zabawna, czy może wiarygodna i profesjonalna? Jest powód, dla którego Twoja ulubiona aplikacja bankowa nie używa czcionki Comic Sans. Sposób prezentacji tekstu wpływa na pierwsze wrażenie użytkownika na temat aplikacji. Oto kilka sposobów na bardziej przemyślane używanie tekstu.
Pokazuj zamiast opisywać
W miarę możliwości używaj „pokaż”, a nie „powiedz”. Na przykład NavigationRail
w aplikacji startowej ma karty dla każdej głównej trasy, ale ikony na kartach są identyczne:
To nie jest pomocne, ponieważ użytkownik nadal musi przeczytać tekst na każdej karcie. Zacznij od dodania wskazówek wizualnych, aby użytkownik mógł szybko znaleźć odpowiednią ikonę. Pomaga to też w lokalizacji i ułatwieniu dostępu.
W lib/src/shared/router.dart
dodaj charakterystyczne ikony dla każdego miejsca docelowego nawigacji (Ekran główny, Playlista i Osoby):
lib/src/shared/router.dart
const List<NavigationDestination> destinations = [
NavigationDestination(
label: 'Home',
icon: Icon(Icons.home), // Modify this line
route: '/',
),
NavigationDestination(
label: 'Playlists',
icon: Icon(Icons.playlist_add_check), // Modify this line
route: '/playlists',
),
NavigationDestination(
label: 'Artists',
icon: Icon(Icons.people), // Modify this line
route: '/artists',
),
];
Masz problemy?
Jeśli aplikacja nie działa prawidłowo, sprawdź, czy nie ma w niej literówek. W razie potrzeby użyj kodu z tych linków, aby wrócić na właściwą drogę.
Rozważnie dobieraj czcionki
Czcionki nadają aplikacji charakter, dlatego wybór odpowiedniej czcionki jest kluczowy. Wybierając czcionkę, weź pod uwagę te kwestie:
- Czcionki szeryfowe lub bezszeryfowe: czcionki szeryfowe mają ozdobne kreski lub „ogonki” na końcu liter i są postrzegane jako bardziej formalne. Czcionki bezszeryfowe nie mają dekoracyjnych kresek i są postrzegane jako bardziej nieformalne.
- Czcionki tylko z wielkich liter: używanie czcionek tylko z wielkich liter jest odpowiednie w przypadku chęci zwrócenia uwagi na niewielkie ilości tekstu (np. nagłówki), ale nadmierne ich stosowanie może być odbierane jako krzyczenie, przez co użytkownik może je całkowicie zignorować.
- Jak nazwy własne lub jak w zdaniu: podczas dodawania tytułów lub etykiet zastanów się nad tym, jak używasz wielkich liter: jak nazwy własne, gdzie pierwsza litera każdego słowa jest wielką literą („To jest tytuł własny”) jest bardziej formalna. Jak w zdaniu, czyli tylko nazwy własne i pierwsze słowo w tekście są pisane wielką literą („To jest tytuł pisany jak w zdaniu”), jest bardziej potoczny i nieformalny.
- Kerning (odstęp między literami), długość linii (szerokość całego tekstu na ekranie) i wysokość linii (wysokość każdej linii tekstu): zbyt duże lub zbyt małe wartości tych parametrów powodują, że aplikacja jest mniej czytelna. Podczas czytania dużego, nieprzerwanego bloku tekstu trudno jest na przykład zachować miejsce, w którym się zatrzymaliśmy.
Pamiętając o tym, wejdź na stronę Czcionki Google i wybierz czcionkę bezszeryfową, taką jak Montserrat, ponieważ aplikacja muzyczna ma być zabawna i ciekawa.
W wierszu poleceń pobierz pakiet google_fonts
. Spowoduje to też zaktualizowanie pliku pubspec.yaml
, aby dodać czcionki jako zależność aplikacji.
flutter pub add google_fonts
macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- Make sure the following two lines are present -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
W lib/src/shared/extensions.dart
zaimportuj nowy pakiet:
lib/src/shared/extensions.dart
import 'package:google_fonts/google_fonts.dart'; // Add this line.
Ustaw Montserrat TextTheme:
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
Aby zastosować zmiany, wykonaj . (użyj przycisku w IDE lub wpisz
r
w wierszu poleceń, aby wykonać szybkie przeładowanie):
Powinny się wyświetlić nowe ikony NavigationRail
oraz tekst w czcionce Montserrat.
Masz problemy?
Jeśli aplikacja nie działa prawidłowo, sprawdź, czy nie ma w niej literówek. W razie potrzeby użyj kodu z tych linków, aby wrócić na właściwą drogę.
5. Ustawianie motywu
Motywy pomagają w zapewnianiu uporządkowanego projektu i jednolitości aplikacji przez określenie zestawu kolorów i stylów tekstu. Dzięki temu możesz szybko wdrożyć interfejs użytkownika bez konieczności zwracania uwagi na drobne szczegóły, takie jak dokładny kolor każdego widżetu.
Deweloperzy Fluttera zwykle tworzą komponenty w niestandardowym stylu na jeden z 2 sposobów:
- Twórz poszczególne niestandardowe widżety z własnym motywem.
- Tworzenie ograniczonych motywów dla domyślnych widżetów.
W tym przykładzie dostawca motywów znajdujący się w lib/src/shared/providers/theme.dart
służy do tworzenia spójnych motywów widżetów i kolorów w całej aplikacji:
lib/src/shared/providers/theme.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
class NoAnimationPageTransitionsBuilder extends PageTransitionsBuilder {
const NoAnimationPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
}
}
class ThemeSettingChange extends Notification {
ThemeSettingChange({required this.settings});
final ThemeSettings settings;
}
class ThemeProvider extends InheritedWidget {
const ThemeProvider({
super.key,
required this.settings,
required this.lightDynamic,
required this.darkDynamic,
required super.child,
});
final ValueNotifier<ThemeSettings> settings;
final ColorScheme? lightDynamic;
final ColorScheme? darkDynamic;
final pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
},
);
Color custom(CustomColor custom) {
if (custom.blend) {
return blend(custom.color);
} else {
return custom.color;
}
}
Color blend(Color targetColor) {
return Color(
Blend.harmonize(
targetColor.toARGB32(),
settings.value.sourceColor.toARGB32(),
),
);
}
Color source(Color? target) {
Color source = settings.value.sourceColor;
if (target != null) {
source = blend(target);
}
return source;
}
ColorScheme colors(Brightness brightness, Color? targetColor) {
final dynamicPrimary = brightness == Brightness.light
? lightDynamic?.primary
: darkDynamic?.primary;
return ColorScheme.fromSeed(
seedColor: dynamicPrimary ?? source(targetColor),
brightness: brightness,
);
}
ShapeBorder get shapeMedium =>
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8));
CardThemeData cardTheme() {
return CardThemeData(
elevation: 0,
shape: shapeMedium,
clipBehavior: Clip.antiAlias,
);
}
ListTileThemeData listTileTheme(ColorScheme colors) {
return ListTileThemeData(
shape: shapeMedium,
selectedColor: colors.secondary,
);
}
AppBarTheme appBarTheme(ColorScheme colors) {
return AppBarTheme(
elevation: 0,
backgroundColor: colors.surface,
foregroundColor: colors.onSurface,
);
}
TabBarThemeData tabBarTheme(ColorScheme colors) {
return TabBarThemeData(
labelColor: colors.secondary,
unselectedLabelColor: colors.onSurfaceVariant,
indicator: BoxDecoration(
border: Border(bottom: BorderSide(color: colors.secondary, width: 2)),
),
);
}
BottomAppBarTheme bottomAppBarTheme(ColorScheme colors) {
return BottomAppBarTheme(color: colors.surface, elevation: 0);
}
BottomNavigationBarThemeData bottomNavigationBarTheme(ColorScheme colors) {
return BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: colors.surfaceContainerHighest,
selectedItemColor: colors.onSurface,
unselectedItemColor: colors.onSurfaceVariant,
elevation: 0,
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
);
}
NavigationRailThemeData navigationRailTheme(ColorScheme colors) {
return const NavigationRailThemeData();
}
DrawerThemeData drawerTheme(ColorScheme colors) {
return DrawerThemeData(backgroundColor: colors.surface);
}
ThemeData light([Color? targetColor]) {
final colorScheme = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
colorScheme: colorScheme,
appBarTheme: appBarTheme(colorScheme),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(colorScheme),
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
navigationRailTheme: navigationRailTheme(colorScheme),
tabBarTheme: tabBarTheme(colorScheme),
drawerTheme: drawerTheme(colorScheme),
scaffoldBackgroundColor: colorScheme.surface,
);
}
ThemeData dark([Color? targetColor]) {
final colorScheme = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
colorScheme: colorScheme,
appBarTheme: appBarTheme(colorScheme),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(colorScheme),
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
navigationRailTheme: navigationRailTheme(colorScheme),
tabBarTheme: tabBarTheme(colorScheme),
drawerTheme: drawerTheme(colorScheme),
scaffoldBackgroundColor: colorScheme.surface,
);
}
ThemeMode themeMode() {
return settings.value.themeMode;
}
ThemeData theme(BuildContext context, [Color? targetColor]) {
final brightness = MediaQuery.of(context).platformBrightness;
return brightness == Brightness.light
? light(targetColor)
: dark(targetColor);
}
static ThemeProvider of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!;
}
@override
bool updateShouldNotify(covariant ThemeProvider oldWidget) {
return oldWidget.settings != settings;
}
}
class ThemeSettings {
ThemeSettings({required this.sourceColor, required this.themeMode});
final Color sourceColor;
final ThemeMode themeMode;
}
Color randomColor() {
return Color(Random().nextInt(0xFFFFFFFF));
}
const linkColor = CustomColor(name: 'Link Color', color: Color(0xFF00B0FF));
class CustomColor {
const CustomColor({
required this.name,
required this.color,
this.blend = true,
});
final String name;
final Color color;
final bool blend;
Color value(ThemeProvider provider) {
return provider.custom(this);
}
}
Aby użyć dostawcy, utwórz jego instancję i przenieś ją do obiektu tematu ograniczonego w funkcji MaterialApp
, która znajduje się w pliku lib/src/shared/app.dart
. Będzie ona dziedziczona przez wszystkie zagnieżdżone obiekty Theme
:
lib/src/shared/app.dart
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'playback/bloc/bloc.dart';
import 'providers/theme.dart';
import 'router.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final settings = ValueNotifier(ThemeSettings(
sourceColor: Colors.pink,
themeMode: ThemeMode.system,
));
@override
Widget build(BuildContext context) {
return BlocProvider<PlaybackBloc>(
create: (context) => PlaybackBloc(),
child: DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) => ThemeProvider(
lightDynamic: lightDynamic,
darkDynamic: darkDynamic,
settings: settings,
child: NotificationListener<ThemeSettingChange>(
onNotification: (notification) {
settings.value = notification.settings;
return true;
},
child: ValueListenableBuilder<ThemeSettings>(
valueListenable: settings,
builder: (context, value, _) {
final theme = ThemeProvider.of(context); // Add this line
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: theme.light(settings.value.sourceColor), // Add this line
routeInformationParser: appRouter.routeInformationParser,
routerDelegate: appRouter.routerDelegate,
);
},
),
)),
),
);
}
}
Gdy motyw jest już skonfigurowany, wybierz kolory aplikacji.
Wybór odpowiedniego zestawu kolorów może być trudny. Możesz mieć pomysł na kolor podstawowy, ale prawdopodobnie chcesz, aby Twoja aplikacja miała więcej niż jeden kolor. Jakiego koloru ma być tekst? Tytuł? Treść? Linki? A co z kolorem tła? Kreator motywu Material to narzędzie internetowe (wprowadzone w Material 3), które pomaga wybrać zestaw uzupełniających się kolorów dla aplikacji.
Aby wybrać kolor źródłowy aplikacji, otwórz Kreator motywu Material i wypróbuj różne kolory interfejsu. Wybierz kolor, który pasuje do estetyki marki lub Twoich osobistych preferencji.
Po utworzeniu motywu kliknij prawym przyciskiem myszy okrąg koloru Główny. Otworzy się okno zawierające szesnastkowy kod koloru podstawowego. Skopiuj tę wartość. (możesz też ustawić kolor w tym oknie).
Przekaż wartość szesnastkową głównego koloru dostawcy motywu. Na przykład szesnastkowy kod koloru #00cbe6
jest określony jako Color(0xff00cbe6)
. ThemeProvider
generuje ThemeData
, który zawiera zestaw kolorów uzupełniających, które zostały wyświetlone w Material Theme Builder:
final settings = ValueNotifier(ThemeSettings(
sourceColor: Color(0xff00cbe6), // Replace this color
themeMode: ThemeMode.system,
));
Uruchom aplikację ponownie. Gdy kolor podstawowy jest już ustawiony, aplikacja staje się bardziej wyrazista. Aby uzyskać dostęp do wszystkich nowych kolorów, odwołuj się do motywu w kontekście i pobierz ColorScheme
:
final colors = Theme.of(context).colorScheme;
Aby użyć określonego koloru, uzyskaj dostęp do rolę koloru w colorScheme
. Otwórz lib/src/shared/views/outlined_card.dart
i nadaj elementowi OutlinedCard
obramowanie:
lib/src/shared/views/outlined_card.dart
class _OutlinedCardState extends State<OutlinedCard> {
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: widget.clickable
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: Container(
// Add from here...
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
),
// ... To here.
child: widget.child,
),
);
}
}
Material 3 wprowadza subtelne role kolorów, które się wzajemnie uzupełniają i można ich używać w całym interfejsie, aby dodać nowe warstwy wyrazu. Te nowe role kolorów obejmują:
Primary
,OnPrimary
,PrimaryContainer
,OnPrimaryContainer
Secondary
,OnSecondary
,SecondaryContainer
,OnSecondaryContainer
Tertiary
,OnTertiary
,TertiaryContainer
,OnTertiaryContainer
Error
,OnError
,ErrorContainer
,OnErrorContainer
Background
,OnBackground
Surface
,OnSurface
,SurfaceVariant
,OnSurfaceVariant
Shadow
,Outline
,InversePrimary
Dodatkowo nowe tokeny projektów obsługują zarówno motyw jasny, jak i ciemny:
Te role kolorów można wykorzystać do nadawania znaczenia i podkreślania różnych części interfejsu. Nawet jeśli komponent nie jest widoczny, może korzystać z dynamicznych kolorów.
Użytkownik może ustawić jasność aplikacji w ustawieniach systemu urządzenia. W lib/src/shared/app.dart
, gdy urządzenie jest w trybie ciemnym, przywróć ciemny motyw i tryb MaterialApp
.
lib/src/shared/app.dart
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: theme.light(settings.value.sourceColor),
darkTheme: theme.dark(settings.value.sourceColor), // Add this line
themeMode: theme.themeMode(), // Add this line
routeInformationParser: appRouter.routeInformationParser,
routerDelegate: appRouter.routerDelegate,
);
Aby włączyć tryb ciemny, kliknij ikonę księżyca w prawym górnym rogu.
Masz problemy?
Jeśli aplikacja nie działa prawidłowo, skorzystaj z kodu pod tym linkiem, aby ją naprawić.
6. Dodawanie projektu adaptacyjnego
Dzięki Flutter możesz tworzyć aplikacje, które działają prawie wszędzie, ale nie oznacza to, że każda aplikacja działa wszędzie tak samo. Użytkownicy oczekują różnych zachowań i funkcji na różnych platformach.
Material Design udostępnia pakiety, które ułatwiają pracę z dopasowywanymi układami. Te pakiety Fluttera znajdziesz na GitHubie.
Podczas tworzenia aplikacji dostosowywalnej na wiele platform pamiętaj o tych różnicach:
- Metoda wprowadzania: mysz, dotyk lub gamepad.
- Rozmiar czcionki, orientacja urządzenia i odległość od ekranu
- Rozmiar i format ekranu: telefon, tablet, urządzenie składane, komputer, przeglądarka
Plik lib/src/shared/views/adaptive_navigation.dart
zawiera klasę nawigacji, w której możesz podać listę miejsc docelowych i treści do wyrenderowania w tekstowej części. Ponieważ używasz tego układu na wielu ekranach, do każdego ekranu podrzędnego należy przekazać wspólny podstawowy układ. Paski nawigacyjne sprawdzają się na komputerach i dużych ekranach, ale na urządzeniach mobilnych lepiej jest wyświetlać pasek nawigacyjny u dołu ekranu.
lib/src/shared/views/adaptive_navigation.dart
import 'package:flutter/material.dart';
class AdaptiveNavigation extends StatelessWidget {
const AdaptiveNavigation({
super.key,
required this.destinations,
required this.selectedIndex,
required this.onDestinationSelected,
required this.child,
});
final List<NavigationDestination> destinations;
final int selectedIndex;
final void Function(int index) onDestinationSelected;
final Widget child;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, dimens) {
if (dimens.maxWidth >= 600) { // Add this line
return Scaffold(
body: Row(
children: [
NavigationRail(
extended: dimens.maxWidth >= 800,
minExtendedWidth: 180,
destinations: destinations
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
),
)
.toList(),
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
Expanded(child: child),
],
),
);
} // Add this line
// Mobile Layout
// Add from here...
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
destinations: destinations,
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
);
// ... To here.
},
);
}
}
Nie wszystkie ekrany mają ten sam rozmiar. Jeśli spróbujesz wyświetlić wersję aplikacji na komputer na telefonie, będziesz musiał mrużyć oczy i powiększać ekran, aby zobaczyć wszystko. Chcesz, aby wygląd aplikacji zmieniał się w zależności od ekranu, na którym jest wyświetlana. Dzięki elastycznemu projektowaniu masz pewność, że Twoja aplikacja będzie dobrze wyglądać na ekranach o różnych rozmiarach.
Aby aplikacja była responsywna, wprowadź kilka punktów przełamania dostosowujących się do zmian (nie myl ich z punktami przełamania do debugowania). Te punkty graniczne określają rozmiary ekranu, przy których aplikacja powinna zmienić układ.
Mniejsze ekrany nie mogą wyświetlać tak dużo treści jak większe ekrany bez ich zmniejszania. Aby aplikacja nie wyglądała jak aplikacja na komputer, która została pomniejszona, utwórz osobny układ na urządzenia mobilne, który wykorzystuje karty do podziału treści. Dzięki temu aplikacja będzie bardziej natywnych na urządzeniach mobilnych.
Podane niżej metody rozszerzania (zdefiniowane w projekcie MyArtist
w pliku lib/src/shared/extensions.dart
) stanowią dobry punkt wyjścia przy projektowaniu optymalnych układów pod kątem różnych grup docelowych.
lib/src/shared/extensions.dart
extension BreakpointUtils on BoxConstraints {
bool get isTablet => maxWidth > 730;
bool get isDesktop => maxWidth > 1200;
bool get isMobile => !isTablet && !isDesktop;
}
Ekran o rozmiary większych niż 730 pikseli (w najdłuższym kierunku), ale mniejszych niż 1200 pikseli jest uznawany za tablet. Wszystko, co przekracza 1200 pikseli, jest uważane za komputer. Jeśli urządzenie nie jest ani tabletem, ani komputerem stacjonarnym, jest uznawane za urządzenie mobilne. Więcej informacji o punktach kontrolnych w materiałach na stronie material.io.
Responsywny układ ekranu głównego używa AdaptiveContainer
i AdaptiveColumn
na podstawie siatki 12 kolumn.
Układ adaptacyjny wymaga 2 układów: jednego dla urządzeń mobilnych i elastycznego dla większych ekranów. W tym momencie LayoutBuilder
zwraca układ na komputery. W lib/src/features/home/view/home_screen.dart
utwórz układ mobilny jako TabBar
i TabBarView
z 4 kartami.
lib/src/features/home/view/home_screen.dart
import 'package:flutter/material.dart';
import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../../utils/adaptive_components.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final PlaylistsProvider playlistProvider = PlaylistsProvider();
final List<Playlist> playlists = playlistProvider.playlists;
final Playlist topSongs = playlistProvider.topSongs;
final Playlist newReleases = playlistProvider.newReleases;
final ArtistsProvider artistsProvider = ArtistsProvider();
final List<Artist> artists = artistsProvider.artists;
return LayoutBuilder(
builder: (context, constraints) {
// Add from here...
if (constraints.isMobile) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('Good morning'),
actions: const [BrightnessToggle()],
bottom: const TabBar(
isScrollable: true,
tabs: [
Tab(text: 'Home'),
Tab(text: 'Recently Played'),
Tab(text: 'New Releases'),
Tab(text: 'Top Songs'),
],
),
),
body: LayoutBuilder(
builder: (context, constraints) => TabBarView(
children: [
SingleChildScrollView(
child: Column(
children: [
const HomeHighlight(),
HomeArtists(
artists: artists,
constraints: constraints,
),
],
),
),
HomeRecent(playlists: playlists, axis: Axis.vertical),
PlaylistSongs(playlist: topSongs, constraints: constraints),
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
],
),
),
),
);
}
// ... To here.
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(playlists: playlists),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Masz problemy?
Jeśli aplikacja nie działa prawidłowo, skorzystaj z kodu pod tym linkiem, aby ją naprawić.
7. Używanie spacji
Przestrzeń jest ważnym narzędziem wizualnym w aplikacji, ponieważ pozwala na uporządkowanie treści w poszczególnych sekcjach.
Lepiej jest mieć za dużo pustej przestrzeni niż za mało. Lepiej jest dodać więcej pustej przestrzeni niż zmniejszać rozmiar czcionki lub elementów wizualnych, aby zmieścić więcej treści.
Brak odstępów może być utrudnieniem dla osób mających problemy ze wzrokiem. Zbyt dużo pustego miejsca może powodować brak spójności i sprawić, że interfejs będzie wyglądał niechlujnie. Przykłady takich zrzutów ekranu:
Następnie dodaj puste miejsce na ekranie głównym, aby uzyskać więcej miejsca. Następnie możesz dostosować układ, aby doprecyzować odstępy.
Owiń widżet obiektem Padding
, aby dodać wokół niego białą przestrzeń. Zwiększ wszystkie wartości wypełniania w pliku lib/src/features/home/view/home_screen.dart
do 35:
lib/src/features/home/view/home_screen.dart
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(playlists: playlists),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
Ponownie załaduj aplikację. Powinna wyglądać tak samo jak wcześniej, ale z większymi odstępami między widżetami. Dodatkowe wypełnienie wygląda lepiej, ale banner z wyróżnieniem u góry jest nadal zbyt blisko krawędzi.
W pliku lib/src/features/home/view/home_highlight.dart
zmień wypełnienie banera na 15:
lib/src/features/home/view/home_highlight.dart
class HomeHighlight extends StatelessWidget {
const HomeHighlight({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(15), // Modify this line
child: Clickable(
child: SizedBox(
height: 275,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/news/concert.jpeg',
fit: BoxFit.cover,
),
),
),
onTap: () => launchUrl(Uri.parse('https://docs.flutter.dev')),
),
),
),
],
);
}
}
Ponownie wczytaj aplikację. Dwie playlisty u dołu nie mają między sobą żadnych pustych miejsc, więc wyglądają tak, jakby należały do tej samej tabeli. Nie jest to jednak Twój przypadek, więc w następnym kroku zajmiesz się tym problemem.
Dodaj pustą przestrzeń między playlistami, wstawiając widżet rozmiaru do Row
, który je zawiera. W lib/src/features/home/view/home_screen.dart
dodaj SizedBox
o szerokości 35:
lib/src/features/home/view/home_screen.dart
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 35), // Add this line
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
Ponownie wczytaj aplikację. Aplikacja powinna wyglądać tak:
Teraz jest wystarczająco dużo miejsca na treści na ekranie głównym, ale wszystko wygląda zbyt od siebie oddzielone i nie ma spójności między sekcjami.
Jak dotąd ustawiliśmy wszystkie odstępy (zarówno poziome, jak i pionowy) dla widżetów na ekranie głównym na 35 z użyciem EdgeInsets.all(35)
, ale możesz też ustawić odstępy dla poszczególnych krawędzi niezależnie od siebie. Dostosuj odstęp, aby lepiej dopasować obraz do miejsca.
EdgeInsets.LTRB()
ustawia lewą, górną, prawą i dolną krawędź osobnoEdgeInsets.symmetric()
ustawia dopełnienie w orientacji pionowej (góra i dół) na równe, a w orientacji poziomej (lewo i prawo) na równe.EdgeInsets.only()
ustawia tylko określone krawędzie.
lib/src/features/home/view/home_screen.dart
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 25, 20, 10), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric( // Modify from here...
horizontal: 15,
vertical: 10,
), // To here.
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(playlists: playlists),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only( // Modify from here...
left: 8,
bottom: 8,
), // To here.
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25), // Modify this line
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only( // Modify from here...
left: 8,
bottom: 8,
), // To here.
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
W lib/src/features/home/view/home_highlight.dart
ustaw dopełnienie po lewej i po prawej stronie banera na 35, a po górze i na dole na 5:
lib/src/features/home/view/home_highlight.dart
class HomeHighlight extends StatelessWidget {
const HomeHighlight({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
// Modify the following line
padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 5),
child: Clickable(
child: SizedBox(
height: 275,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/news/concert.jpeg',
fit: BoxFit.cover,
),
),
),
onTap: () => launchUrl(Uri.parse('https://docs.flutter.dev')),
),
),
),
],
);
}
}
Załaduj ponownie aplikację. Układ i odstępy będą wyglądać znacznie lepiej. Na koniec dodaj ruch i animację.
Masz problemy?
Jeśli aplikacja nie działa prawidłowo, skorzystaj z kodu pod tym linkiem, aby ją naprawić.
8. Dodawanie ruchu i animacji
Ruch i animacja to świetne sposoby na wprowadzenie ruchu i energii oraz zapewnienie informacji zwrotnych, gdy użytkownik wchodzi w interakcję z aplikacją.
Animacja między ekranami
Element ThemeProvider
definiuje PageTransitionsTheme
z animacjami przejścia między ekranami na platformach mobilnych (iOS, Android). Użytkownicy komputerów już otrzymują informacje zwrotne po kliknięciu myszką lub trackpadem, więc animacja przejścia między stronami nie jest potrzebna.
Flutter udostępnia animacje przejścia między ekranami, które możesz skonfigurować w aplikacji na podstawie platformy docelowej, jak widać na lib/src/shared/providers/theme.dart
:
lib/src/shared/providers/theme.dart
final pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
},
);
Przekaż PageTransitionsTheme
do motywu jasnego i ciemnego w lib/src/shared/providers/theme.dart
lib/src/shared/providers/theme.dart
ThemeData light([Color? targetColor]) {
final colorScheme = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: colorScheme,
appBarTheme: appBarTheme(colorScheme),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(colorScheme),
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
navigationRailTheme: navigationRailTheme(colorScheme),
tabBarTheme: tabBarTheme(colorScheme),
drawerTheme: drawerTheme(colorScheme),
scaffoldBackgroundColor: colorScheme.surface,
);
}
ThemeData dark([Color? targetColor]) {
final colorScheme = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: colorScheme,
appBarTheme: appBarTheme(colorScheme),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(colorScheme),
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
navigationRailTheme: navigationRailTheme(colorScheme),
tabBarTheme: tabBarTheme(colorScheme),
drawerTheme: drawerTheme(colorScheme),
scaffoldBackgroundColor: colorScheme.surface,
);
}
Bez animacji na iOS
Z animacją na iOS
Masz problemy?
Jeśli aplikacja nie działa prawidłowo, skorzystaj z kodu pod tym linkiem, aby ją naprawić.
9. Dodawanie stanów najechania
Jednym ze sposobów na dodanie ruchu do aplikacji na komputery jest stan najechania kursorem, w którym widżet zmienia stan (np. kolor, kształt lub zawartość), gdy użytkownik najedzie na niego kursorem.
Domyślnie klasa _OutlinedCardState
(używana w przypadku kafelków playlisty „Ostatnio odtwarzane”) zwraca wartość MouseRegion
, która zmienia strzałkę kursora na wskaźnik po najechaniu kursorem na element, ale możesz dodać więcej informacji wizualnych.
Otwórz plik lib/src/shared/views/outlined_card.dart
i zastąp jego zawartość poniższą implementacją, aby wprowadzić stan _hovered
.
lib/src/shared/views/outlined_card.dart
import 'package:flutter/material.dart';
class OutlinedCard extends StatefulWidget {
const OutlinedCard({super.key, required this.child, this.clickable = true});
final Widget child;
final bool clickable;
@override
State<OutlinedCard> createState() => _OutlinedCardState();
}
class _OutlinedCardState extends State<OutlinedCard> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.circular(_hovered ? 20 : 8);
const animationCurve = Curves.easeInOut;
return MouseRegion(
onEnter: (_) {
if (!widget.clickable) return;
setState(() {
_hovered = true;
});
},
onExit: (_) {
if (!widget.clickable) return;
setState(() {
_hovered = false;
});
},
cursor: widget.clickable
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: AnimatedContainer(
duration: kThemeAnimationDuration,
curve: animationCurve,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
borderRadius: borderRadius,
),
foregroundDecoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.onSurface.withAlpha(_hovered ? 30 : 0),
borderRadius: borderRadius,
),
child: TweenAnimationBuilder<BorderRadius>(
duration: kThemeAnimationDuration,
curve: animationCurve,
tween: Tween(begin: BorderRadius.zero, end: borderRadius),
builder: (context, borderRadius, child) => ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: borderRadius,
child: child,
),
child: widget.child,
),
),
);
}
}
Ponownie załaduj aplikację, a następnie najedź kursorem na jeden z kafelków ostatnio odtwarzanych playlist.
OutlinedCard
zmienia przezroczystość i zaokrągla rogi.
Na koniec użyj animacji, aby zmienić numer utworu na playliście w przycisk odtwarzania za pomocą widżetu HoverableSongPlayButton
zdefiniowanego w lib/src/shared/views/hoverable_song_play_button.dart
. W lib/src/features/playlists/view/playlist_songs.dart
nawiń widżet Center
(zawierający numer utworu) za pomocą elementu HoverableSongPlayButton
:
lib/src/features/playlists/view/playlist_songs.dart
rowBuilder: (context, index) => DataRow.byIndex(
index: index,
cells: [
DataCell(
HoverableSongPlayButton( // Modify from here...
hoverMode: HoverMode.overlay,
song: playlist.songs[index],
child: Center(
child: Text(
(index + 1).toString(),
textAlign: TextAlign.center,
),
),
), // To here.
),
DataCell(
Row(
children: [
Padding(
padding: const EdgeInsets.all(2),
child: ClippedImage(playlist.songs[index].image.image),
),
const SizedBox(width: 10),
Expanded(child: Text(playlist.songs[index].title)),
],
),
),
DataCell(Text(playlist.songs[index].length.toHumanizedString())),
],
),
Wczytaj ponownie aplikację, a następnie najedź kursorem na numer utworu na liście Najpopularniejsze dziś utwory lub na liście Nowości.
Liczba zmienia się w przycisk odtwarzania, który po kliknięciu odtwarza utwór.
Zobacz kod końcowy projektu na GitHubie.
10. Gratulacje!
To ćwiczenie zostało ukończone. Wiesz już, że w aplikacji można wprowadzić wiele drobnych zmian, które sprawią, że będzie ona ładniejsza, bardziej dostępna, łatwiejsza do zlokalizowania i bardziej odpowiednia dla wielu platform. Oto niektóre z tych technik:
- Typografia: tekst to coś więcej niż tylko narzędzie do komunikacji. Zadbaj o sposób wyświetlania tekstu, aby pozytywnie wpłynąć na wrażenia użytkowników i ich postrzeganie aplikacji.
- Motyw: utwórz system projektowania, którego możesz używać bez konieczności podejmowania decyzji dotyczących projektu każdego widżetu.
- Adaptacyjność: weź pod uwagę urządzenie i platformę, na której użytkownik uruchamia aplikację, oraz ich możliwości. Weź pod uwagę rozmiar ekranu i sposób wyświetlania aplikacji.
- Ruch i animacja: dodanie ruchu do aplikacji zwiększa jej dynamikę i wrażenia użytkownika, a co ważniejsze, zapewnia mu informacje zwrotne.
Kilka drobnych poprawek może sprawić, że Twoja aplikacja stanie się piękna:
Przed
Po
Dalsze kroki
Mamy nadzieję, że udało Ci się dowiedzieć więcej o tworzeniu atrakcyjnych aplikacji w Flutterze.
Jeśli zastosujesz któryś z podanych tu wskazówek lub porad (lub masz własne, którymi chcesz się podzielić), daj nam znać. Skontaktuj się z nami na Twitterze: @rodydavis i @khanhnwin.
Mogą Ci się też przydać te materiały.
Motywy
- Kreator motywów Material (narzędzie)
Zasoby adaptacyjne i responsywne:
- Dekodowanie Fluttera na potrzeby wersji adaptacyjnej i responsywnej (film)
- Układy adaptacyjne (film z The Boring Flutter Development Show)
- Tworzenie elastycznych i adaptacyjnych aplikacji (flutter.dev)
- Adaptacyjne komponenty Material Design dla Fluttera (biblioteka na GitHubie)
- 5 sposobów na przygotowanie aplikacji na duże ekrany (wideo z Google I/O 2021)
Ogólne zasoby projektowe:
- The little things: Becoming the mythical designer-developer (film z Flutter Engage)
- Material Design 3 na urządzenia składane (material.io)
Nawiązuj kontakt ze społecznością Fluttera.
Zacznij tworzyć piękne aplikacje.