1. Wprowadzenie
Flutter to opracowany przez Google zestaw narzędzi interfejsu do tworzenia pięknych, natywnie skompilowanych aplikacji na urządzenia mobilne, komputery i komputery przy użyciu jednej bazy kodu. Flutter współpracuje z istniejącym kodem, jest używany przez deweloperów i organizacje na całym świecie oraz jest dostępny bezpłatnie i o otwartym kodzie źródłowym.
Dzięki temu ćwiczeniu w programowaniu ulepszysz aplikację muzyczną Flutter, zmieniając ją z nudnej na piękną. W tym celu wykorzystano narzędzia i interfejsy API wprowadzone w Material 3.
Czego się nauczysz
- Jak napisać użyteczną i atrakcyjną aplikację Flutter na różnych platformach.
- Jak zaprojektować tekst w aplikacji, aby spełniał oczekiwania użytkowników.
- Jak wybrać odpowiednie kolory, dostosować widżety, utworzyć własny motyw oraz szybko i łatwo wdrożyć tryb ciemny.
- Jak tworzyć aplikacje adaptacyjne działające na różnych platformach
- Jak tworzyć aplikacje, które będą dobrze wyglądać na każdym ekranie
- Jak dodać ruch do aplikacji Flutter, dzięki czemu będzie się wyróżniała.
Wymagania wstępne:
W tym ćwiczeniu w programowaniu zakładamy, że masz już doświadczenie w korzystaniu z platformy Flutter. Jeśli nie, warto najpierw poznać podstawy. Przydatne będą te linki:
- Obejrzyj prezentację platformy widżetów Flutter
- Wykonaj ćwiczenia w programie Tworzenie pierwszej aplikacji Flutter (część 1).
Co utworzysz
Dzięki nim dowiesz się, jak utworzyć ekran główny aplikacji o nazwie MyArtist, czyli odtwarzacza muzyki, dzięki któremu fani mogą na bieżąco śledzić informacje o ulubionych wykonawcach. Omawiamy w nim, jak zmienić wygląd aplikacji, aby wyglądała atrakcyjnie na różnych platformach.
Po ukończeniu tego ćwiczenia z programowania zobaczysz, jak działa aplikacja:
Czego chcesz się dowiedzieć z tego ćwiczenia z programowania?
2. Konfigurowanie środowiska programistycznego Flutter
Aby ukończyć ten moduł, potrzebujesz 2 oprogramowania: pakietu SDK Flutter i edytora.
Ćwiczenie z programowania możesz uruchomić na dowolnym z tych urządzeń:
- Fizyczne urządzenie z Androidem lub iOS podłączone do komputera i ustawione w trybie programisty.
- Symulator iOS (wymaga zainstalowania narzędzi Xcode).
- Emulator Androida (wymaga skonfigurowania Android Studio).
- Przeglądarka (do debugowania wymagany jest Chrome).
- Aplikacja komputerowa w systemie Windows, Linux lub macOS Programowanie należy tworzyć na platformie, na której zamierzasz wdrożyć usługę. Jeśli więc chcesz opracować aplikację komputerową dla systemu Windows, musisz to zrobić w tym systemie, aby uzyskać dostęp do odpowiedniego łańcucha kompilacji. Istnieją wymagania związane z konkretnymi systemami operacyjnymi, które zostały szczegółowo omówione na stronie docs.flutter.dev/desktop.
3. Pobierz aplikację startową w Codelabs
Sklonuj je z GitHuba
Aby skopiować to ćwiczenia z programowania z GitHuba, uruchom te polecenia:
git clone https://github.com/flutter/codelabs.git cd codelabs/boring_to_beautiful/step_01/
Aby upewnić się, że wszystko działa, uruchom aplikację Flutter jako aplikację komputerową, jak pokazano poniżej. Możesz też otworzyć ten projekt w swoim IDE i użyć jego narzędzi do uruchomienia aplikacji.
Gotowe! Kod startowy ekranu głównego kanału MyArtist powinien być uruchomiony. Powinien wyświetlić się ekran główny usługi MyArtist. Na komputerach wygląda dobrze, ale na komórce jest... Kiepska sprawa. Po pierwsze, nie uwzględnia czołówki. Nie martw się, rozwiążesz ten problem.
Poznaj kod
Następnie zapoznaj się z kodem.
Otwórz plik lib/src/features/home/view/home_screen.dart
, który zawiera te elementy:
lib/src/features/home/view/home_screen.dart
import 'package:adaptive_components/adaptive_components.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 '../../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 conditional mobile layout
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2), // 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(2), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2), // 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(2), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
// Add spacer between tables
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(2), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Ten plik importuje material.dart
i implementuje widżet stanowy za pomocą 2 klas:
- Instrukcja
import
udostępnia komponenty materiałowe. - Klasa
HomeScreen
reprezentuje całą wyświetlaną stronę. - Metoda
build()
klasy_HomeScreenState
tworzy element główny drzewa widżetów, co ma wpływ na sposób tworzenia wszystkich widżetów w interfejsie.
4. Korzystaj z typografii
Tekst jest wszędzie. Tekst to przydatny sposób komunikacji z użytkownikiem. Czy Twoja aplikacja jest przyjazna i zabawna, czy może bezpieczna i profesjonalna? Twoja ulubiona aplikacja bankowa z jakiegoś powodu nie korzysta z Comic Sans. Sposób przedstawiania tekstu wpływa na pierwsze wrażenie użytkownika o aplikacji. Oto kilka sposobów na bardziej przemyślane korzystanie z tekstu.
Pokazuj zamiast opisywać
Gdy tylko jest to możliwe, należy pokazywać zamiast „powiedz”. Na przykład NavigationRail
w aplikacji startowej zawiera karty każdej głównej trasy, ale ikony wiodące są identyczne:
Nie jest to pomocne, ponieważ użytkownik nadal musi przeczytać tekst na każdej karcie. Zacznij od dodania wskazówek wizualnych, aby użytkownik mógł szybko spojrzeć na główne ikony i znaleźć odpowiednią kartę. Pomaga to również w lokalizacji i ułatwieniach dostępu.
W lib/src/shared/router.dart
dodaj różne wiodące ikony dla każdego miejsca docelowego nawigacji (domu, playlisty i osób):
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 poprawnie, poszukaj literówek. W razie potrzeby możesz ponownie skorzystać z kodu, do którego prowadzą podane niżej linki.
Przemyślany dobór czcionek
Czcionki nadają aplikacji charakter, dlatego dobór odpowiedniej czcionki ma kluczowe znaczenie. Oto kilka kwestii, które należy wziąć pod uwagę podczas wybierania czcionki:
- Sans-szeryfowa lub szeryfowa: czcionki szeryfowe zawierają dekoracyjne kreski. na końcu listów i są postrzegane jako bardziej formalne. Czcionki bezszeryfowe nie mają dodatkowych kreski i są postrzegane jako bardziej nieformalne. Wielka bezszeryfowa wielka litera T i wielka litera T
- Czcionki z wielkością liter: pisanie pisanymi wielkimi literami jest odpowiednie, aby zwrócić uwagę na niewielką ilość tekstu (na przykład nagłówki), ale jeśli zostanie nadwyższone, może zostać postrzegany jako krzyki i skłaniające użytkownika do całkowitego ignorowania go.
- Jak nazwy własne lub jak w zdaniu: dodając tytuły lub etykiety, staraj się używać wielkich liter. Użyj jakości tytułu, gdzie pierwsze litery każdego wyrazu będą pisane wielką literą („To jest tytuł pisany wielkimi literami”). Jak w zdaniu, w którym wielkie litery są używane tylko w rzeczownikach własnych i pierwszym słowie w tekście („Jak w zdaniu Jak w przypadku tytułów”), jest bardziej swobodny i swobodny.
- Kering (odstępy między poszczególnymi literami), długość wiersza (szerokość całego tekstu na ekranie) i wysokość wiersza (wysokość każdego wiersza tekstu): zbyt duże lub małe odstępy między literami utrudniają czytanie aplikacji. Na przykład podczas czytania dużego, nierozdzielonego bloku tekstu łatwo zgubić się w miejscu.
Mając to na uwadze, przejdź do Google Fonts i wybierz czcionkę bezszeryfową, np. Montserrat, ponieważ aplikacja muzyczna ma być zabawna i zabawna.
Z wiersza poleceń pobierz pakiet google_fonts
. Spowoduje to też zaktualizowanie pliku pubspec przez dodanie czcionek jako zależności 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" "http://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/>
<!-- Make sure these lines are present from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- To here. -->
<key>com.apple.security.network.server</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.
Montserrat TextTheme:
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
Załaduj ponownie gorąco , aby aktywować zmiany. Aby załadować ponownie z góry, użyj przycisku w środowisku IDE lub w wierszu poleceń wpisz r
.
Nowe ikony NavigationRail
powinny pojawić się razem z tekstem zapisanym czcionką Montserrat.
Masz problemy?
Jeśli aplikacja nie działa poprawnie, poszukaj literówek. W razie potrzeby możesz ponownie skorzystać z kodu, do którego prowadzą podane niżej linki.
5. Ustaw motyw
Motywy umożliwiają ustrukturyzowany projekt i jednolitość aplikacji przez określenie systemu kolorów i stylów tekstu. Motywy umożliwiają szybką implementację interfejsu użytkownika bez konieczności zajmowania się drobnymi szczegółami, takimi jak określenie dokładnego koloru każdego widżetu.
Programiści korzystający z platformy Flutter zazwyczaj tworzą komponenty o niestandardowej tematyce na 2 sposoby:
- Twórz indywidualne widżety niestandardowe, każdy z własnym motywem.
- Utwórz motywy o zakresie na potrzeby widżetów domyślnych.
Ten przykład korzysta z dostawcy motywów znajdującego się w lokalizacji lib/src/shared/providers/theme.dart
, aby utworzyć widżety i kolory o spójnej tematyce 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.value, settings.value.sourceColor.value));
}
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),
);
CardTheme cardTheme() {
return CardTheme(
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,
);
}
TabBarTheme tabBarTheme(ColorScheme colors) {
return TabBarTheme(
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 _colors = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme,
colorScheme: _colors,
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(_colors),
bottomAppBarTheme: bottomAppBarTheme(_colors),
bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
navigationRailTheme: navigationRailTheme(_colors),
tabBarTheme: tabBarTheme(_colors),
drawerTheme: drawerTheme(_colors),
scaffoldBackgroundColor: _colors.background,
useMaterial3: true,
);
}
ThemeData dark([Color? targetColor]) {
final _colors = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme,
colorScheme: _colors,
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(_colors),
bottomAppBarTheme: bottomAppBarTheme(_colors),
bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
navigationRailTheme: navigationRailTheme(_colors),
tabBarTheme: tabBarTheme(_colors),
drawerTheme: drawerTheme(_colors),
scaffoldBackgroundColor: _colors.background,
useMaterial3: true,
);
}
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));
}
// Custom Colors
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 instancję i przekaż ją do obiektu motywu o zakresie w MaterialApp
znajdującym się w lokalizacji lib/src/shared/app.dart
. Zostanie ona odziedziczona 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,
);
},
),
)),
),
);
}
}
Po skonfigurowaniu motywu wybierz kolory aplikacji.
Dobór odpowiedniego zestawu kolorów nie zawsze jest łatwy. Kolor podstawowy może Ci się przydać, ale prawdopodobnie chcesz, by aplikacja miała więcej niż 1 kolor. Jakiego koloru ma być tekst? Tytuł? Treść? Linki? A co z kolorem tła? Material Theme Builder to narzędzie internetowe (dostępne w Material 3), które pozwala wybrać zestaw uzupełniających się kolorów do aplikacji.
Aby wybrać kolor źródłowy aplikacji, otwórz Material Theme Builder i zapoznaj się z różnymi kolorami interfejsu użytkownika. Ważne jest, aby wybrać kolor, który pasuje do estetyki marki i Twoich osobistych preferencji.
Po utworzeniu motywu kliknij prawym przyciskiem myszy dymek Podstawowy – otworzy się okno z wartością szesnastkową koloru podstawowego. Skopiuj tę wartość. (W tym oknie możesz też ustawić kolor).
Przekaż wartość szesnastkową koloru podstawowego do dostawcy motywu. Na przykład szesnastkowy kod koloru #00cbe6
jest określony jako Color(0xff00cbe6)
. Element ThemeProvider
generuje ThemeData
zawierający zestaw uzupełniających się kolorów, które zostały wyświetlone na podglądzie w narzędziu Material Theme Builder:
final settings = ValueNotifier(ThemeSettings(
sourceColor: Color(0xff00cbe6), // Replace this color
themeMode: ThemeMode.system,
));
Uruchom ponownie aplikację na gorąco. Po ustawieniu koloru głównego aplikacja staje się bardziej wyrazista. Wszystkie nowe kolory są dostępne dzięki odwołaniu się do motywu w kontekście i złapaniu ColorScheme
:
final colors = Theme.of(context).colorScheme;
Aby użyć konkretnego koloru, uzyskaj dostęp do roli dotyczącej koloru w colorScheme
. Otwórz lib/src/shared/views/outlined_card.dart
i dodaj ramkę OutlinedCard
:
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(
child: widget.child,
// Add from here...
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
),
// ... To here.
),
);
}
}
Material 3 wprowadza zróżnicowane role kolorów, które się uzupełniają i można ich używać w interfejsie do dodawania nowych warstw ekspresji. Te nowe role dotyczące 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 projektowe obsługują zarówno jasny, jak i ciemny motyw:
Tych ról kolorystycznych można użyć, aby nadać znaczenie różnym częściom interfejsu. Nawet jeśli komponent nie jest widoczny, nadal może korzystać z dynamicznego koloru.
Użytkownik może ustawić jasność aplikacji w ustawieniach systemowych urządzenia. Gdy w lib/src/shared/app.dart
na urządzeniu jest ustawiony tryb ciemny, przywrócisz MaterialApp
ciemny motyw i tryb motywu.
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 Twoja aplikacja nie działa prawidłowo, skorzystaj z poniższego linku, aby rozwiązać ten problem.
6. Dodaj projekt adaptacyjny
Flutter umożliwia tworzenie aplikacji, które działają niemal wszędzie, ale nie oznacza to, że wszystkie działają tak samo wszędzie. Użytkownicy oczekują różnych zachowań i funkcji na różnych platformach.
Material oferuje pakiety, które ułatwiają pracę z układami adaptacyjnymi. Te pakiety znajdziesz na GitHubie.
Podczas tworzenia wieloplatformowej aplikacji adaptacyjnej pamiętaj o tych różnicach między platformami:
- Metoda wprowadzania tekstu: mysz, przycisk dotykowy lub pad do gier
- rozmiar czcionki, orientacja urządzenia i odległość wyświetlania;
- Rozmiar i format ekranu: telefon, tablet, urządzenie składane, komputer, internet
Plik lib/src/shared/views/adaptive_navigation.dart
zawiera klasę nawigacji, w której możesz podać listę miejsc docelowych i treści do renderowania. Ponieważ używasz tego układu na wielu ekranach, każdy element podrzędny ma swój wspólny układ podstawowy. Szyny nawigacyjne sprawdzają się w przypadku komputerów i dużych ekranów, ale warto dostosować układ do urządzeń mobilnych, wyświetlając dolny pasek nawigacyjny na urządzeniach mobilnych.
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 super.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) {
// Tablet Layout
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. Aby wyświetlić na telefonie wersję aplikacji na komputer, trzeba było użyć kombinacji mrugania i powiększania, żeby zobaczyć wszystko. Chcesz, aby wygląd aplikacji zmieniał się w zależności od ekranu, na którym się wyświetla. Dzięki projektowaniu responsywnemu Twoja aplikacja będzie dobrze wyglądać na ekranach o różnych rozmiarach.
Aby Twoja aplikacja była elastyczna, wprowadź kilka adaptacyjnych punktów przerwania (nie należy ich mylić z punktami przerwania debugowania). Te punkty przerwania określają rozmiary ekranu, na których aplikacja powinna zmieniać układ.
Na mniejszych ekranach nie można wyświetlić tak dużych ekranów, nawet pomniejszając zawartość. Aby aplikacja nie wyglądała jak aplikacja komputerowa, która została skrócona, utwórz osobny układ dla urządzeń mobilnych, który rozdziela zawartość za pomocą kart. Dzięki temu aplikacja będzie bardziej natywna na urządzeniach mobilnych.
Podczas projektowania zoptymalizowanych układów pod kątem różnych miejsc docelowych warto zacząć od poniższych metod rozszerzeń (zdefiniowanych w projekcie MyArtist w lib/src/shared/extensions.dart
).
lib/src/shared/extensions.dart
extension BreakpointUtils on BoxConstraints {
bool get isTablet => maxWidth > 730;
bool get isDesktop => maxWidth > 1200;
bool get isMobile => !isTablet && !isDesktop;
}
Za tablet uznajemy ekran większy niż 730 pikseli (w najdłuższym kierunku), ale mniejszy niż 1200 pikseli. Wszystkie elementy większe niż 1200 pikseli są uznawane za komputery. Jeśli urządzenie nie jest tabletem ani komputerem, jest uznawane za urządzenie mobilne. Więcej informacji o adaptacyjnych punktach przerwania znajdziesz na stronie material.io. Rozważ użycie pakietu adaptive_breakpoints.
Elastyczny układ ekranu głównego wykorzystuje elementy AdaptiveContainer
i AdaptiveColumn
oparte na 12-kolumnowej siatce wykorzystującej pakiety adaptive_components i adaptive_breakpoints do wdrożenia elastycznego układu siatki w stylu Material Design.
return LayoutBuilder(
builder: (context, constraints) {
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 40,
),
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(
horizontal: 15,
vertical: 20,
),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
Układ adaptacyjny wymaga 2 układów: jednego dla urządzeń mobilnych i układu elastycznego dla większych ekranów. LayoutBuilder
obecnie zwraca tylko 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:adaptive_components/adaptive_components.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 '../../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.symmetric(
horizontal: 20,
vertical: 40,
),
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(
horizontal: 15,
vertical: 20,
),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Masz problemy?
Jeśli Twoja aplikacja nie działa prawidłowo, skorzystaj z poniższego linku, aby rozwiązać ten problem.
Użyj odstępu
Odstęp jest ważnym narzędziem wizualnym w przypadku aplikacji, który tworzy odstęp między sekcjami.
Lepiej mieć za dużo białego obszaru niż za mało. Zwiększając odstęp, lepiej jest zmniejszyć rozmiar czcionki lub elementów wizualnych, aby lepiej zmieścić się w przestrzeni.
Brak białego obszaru może być poważnym problemem dla osób z wadami wzroku. Zbyt duża ilość białego obszaru może brakować spójności i sprawić, że interfejs będzie wyglądać słabo. Oto przykładowe zrzuty ekranu:
Dodaj do niego odstęp, aby zwolnić miejsce. Następnie musisz dokładniej dostosować układ, aby dostosować odstępy.
Otaczaj widżet obiektem Padding
, aby dodać do niego odstęp. Zwiększ wszystkie wartości dopełnienia, które są obecnie w polu lib/src/features/home/view/home_screen.dart
, do 35:
lib/src/features/home/view/home_screen.dart
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,
),
),
],
),
),
// Add spacer between tables
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ę na gorąco. Powinien wyglądać tak samo jak wcześniej, ale z większą liczbą odstępów między widżetami. Dodatkowe dopełnienie wygląda lepiej, ale baner z najciekawszymi momentami u góry nadal jest za blisko krawędzi.
W lib/src/features/home/view/home_highlight.dart
zmień dopełnienie banera na 35:
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(35), // 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: () => launch('https://docs.flutter.dev'),
),
),
),
],
);
}
}
Ponownie załaduj aplikację na gorąco. Dwie playlisty na dole nie są rozdzielone, więc wyglądają, jakby należały do tej samej tabeli. To nie jest problem. Następnym razem rozwiążesz ten problem.
Dodaj odstępy między playlistami, wstawiając widżet rozmiaru do elementu 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
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,
),
),
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,
),
),
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
],
),
),
],
),
),
Ponownie załaduj aplikację na gorąco. Aplikacja powinna wyglądać tak:
Teraz masz sporo miejsca na zawartość ekranu głównego, ale wszystko wygląda zbyt osobno i nie ma spójności między sekcjami.
Dotychczas ustawiliśmy całe dopełnienie (poziome i pionowe) widżetów na ekranie głównym na 35 za pomocą parametru EdgeInsets.all(35)
, ale możesz też ustawić dopełnienie każdej krawędzi oddzielnie. Dostosuj dopełnienie, aby lepiej pasowało do miejsca.
EdgeInsets.LTRB()
ustawia kolejno lewo, górną, prawą i dolną częśćEdgeInsets.symmetric()
ustawia dopełnienie w pionie (góra i dół) jako równoważne, a dopełnienie poziome (lewa i prawa) – równoważne.EdgeInsets.only()
ustawia tylko określone krawędzie.
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(
horizontal: 15,
vertical: 10,
), // Modify this line
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(left: 8, bottom: 8), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
W usłudze lib/src/features/home/view/home_highlight.dart
ustaw dopełnienie lewe i prawe banera na 35, a dopełnienie u góry i u dołu 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 this 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: () => launch('https://docs.flutter.dev'),
),
),
),
],
);
}
}
Ponownie załaduj aplikację na gorąco. Układ i odstępy wyglądają znacznie lepiej. Na koniec dodaj trochę ruchu i animacji.
Masz problemy?
Jeśli Twoja aplikacja nie działa prawidłowo, skorzystaj z poniższego linku, aby rozwiązać ten problem.
7. Dodaj ruch i animację
Ruch i animacja to świetne sposoby na przedstawienie ruchu i energii oraz przekazywania opinii, gdy użytkownik korzysta z aplikacji.
Przełączanie się między ekranami
ThemeProvider
definiuje element PageTransitionsTheme
z animacjami przejścia ekranu dla platform mobilnych (iOS, Android). Użytkownicy komputerów otrzymują informacje zwrotne po kliknięciu myszki lub trackpada, więc animacja przejścia między stronami nie jest potrzebna.
Flutter udostępnia animacje przejścia ekranu, które możesz skonfigurować w przypadku aplikacji na podstawie platformy docelowej (patrz 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(),
},
);
Na pasku PageTransitionsTheme
wybierz jasny i ciemny motyw w lib/src/shared/providers/theme.dart
lib/src/shared/providers/theme.dart
ThemeData light([Color? targetColor]) {
final _colors = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: ColorScheme.fromSeed(
seedColor: source(targetColor),
brightness: Brightness.light,
),
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(),
tabBarTheme: tabBarTheme(_colors),
scaffoldBackgroundColor: _colors.background,
);
}
ThemeData dark([Color? targetColor]) {
final _colors = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: ColorScheme.fromSeed(
seedColor: source(targetColor),
brightness: Brightness.dark,
),
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(),
tabBarTheme: tabBarTheme(_colors),
scaffoldBackgroundColor: _colors.background,
);
}
Bez animacji na iOS
Z animacją w iOS
Masz problemy?
Jeśli Twoja aplikacja nie działa prawidłowo, skorzystaj z poniższego linku, aby rozwiązać ten problem.
Dodaj stany po najechaniu kursorem
Jednym ze sposobów na dodanie ruchu do aplikacji komputerowej jest stosowanie stanów po najechaniu, w którym widżet zmienia swój stan (np. kolor, kształt lub zawartość) po najechaniu na niego kursorem myszy.
Domyślnie klasa _OutlinedCardState
(używana w przypadku kafelków playlisty „Ostatnio odtwarzane”) zwraca wartość MouseRegion
– dzięki czemu strzałka kursora po najechaniu kursorem zmienia się w wskaźnik. Możesz jednak dodać więcej wizualnych wskazówek.
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.withOpacity(
_hovered ? 0.12 : 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,
),
),
);
}
}
Załaduj ponownie aplikację na gorąco, a następnie najedź kursorem na jeden z ostatnio odtwarzanych kafelków playlisty.
Element OutlinedCard
zmienia przezroczystość i zaokrągla rogi.
Na koniec dodaj do animacji 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 narzędziu lib/src/features/playlists/view/playlist_songs.dart
umieść w widżecie Center
(zawierającym numer utworu) etykietę HoverableSongPlayButton
:
lib/src/features/playlists/view/playlist_songs.dart
HoverableSongPlayButton( // Add this line
hoverMode: HoverMode.overlay, // Add this line
song: playlist.songs[index], // Add this line
child: Center( // Modify this line
child: Text(
(index + 1).toString(),
textAlign: TextAlign.center,
),
),
), // Add this line
Ponownie załaduj aplikację i najedź kursorem na numer utworu na playliście Najpopularniejsze utwory lub Nowości.
Liczba zmienia się w przycisk odtwórz, którego kliknięcie powoduje odtworzenie utworu.
Ostateczny kod projektu znajdziesz na GitHub.
8. Gratulacje!
To już koniec ćwiczenia z programowania. Wiesz już, że istnieje wiele drobnych zmian, które można zintegrować z aplikacją, aby uczynić ją ładniejszą, bardziej dostępną, bardziej zlokalizowaną i dostosowaną do wielu platform. Te techniki to m.in.:
- Typografia: tekst to nie tylko narzędzie do komunikacji. Wykorzystaj sposób prezentowania tekstu, aby uzyskać pozytywny wpływ na wrażeniach i postrzegania aplikacji.
- Tematyka: stwórz system projektowania, z którego można korzystać niezawodnie bez konieczności podejmowania decyzji dotyczących projektu każdego widżetu.
- Adaptalność: weź pod uwagę urządzenie i platformę, na których użytkownik korzysta z Twojej aplikacji, oraz jej możliwości. Weź pod uwagę rozmiar ekranu i sposób wyświetlania aplikacji.
- Ruch i animacja: dodanie ruchu do aplikacji zwiększa satysfakcję użytkowników, a praktycznie pozwala im przekazać informacje zwrotne.
Wystarczy kilka drobnych poprawek, aby aplikacja stała się nudna lub atrakcyjna:
Przed
Po
Dalsze kroki
Mamy nadzieję, że dowiedzieliście się więcej o tworzeniu pięknych aplikacji w ramach Flutter.
Jeśli zastosujesz którąś z wymienionych tutaj porad lub wskazówek (lub chcesz podzielić się własną wskazówką), chętnie poznamy Twoją opinię. Skontaktuj się z nami na Twitterze: @rodydavis i @khanhnwin.
Przydatne mogą okazać się również poniższe materiały.
Motywy
- Material Theme Builder (narzędzie)
Zasoby adaptacyjne i elastyczne:
- Dekodowanie Flutter w przypadku metod adaptacyjnych i elastycznych (film)
- Adaptacyjne układy (film z programu Boring Flutter Development Show)
- Tworzenie aplikacji elastycznych i adaptacyjnych (flutter.dev)
- Komponenty adaptacyjnych materiałów na potrzeby Flutter (biblioteka w GitHubie)
- 5 rzeczy, które możesz zrobić, aby przygotować aplikację na duże ekrany (film z Google I/O 2021)
Ogólne zasoby dotyczące projektowania:
- Drobnostki: jak zostać mitycznym projektantem i programistą (film z Flutter Engage)
- Material Design 3 na urządzenia składane (material.io)
Nawiąż kontakt ze społecznością Flutter.
Twórz piękne rzeczy ze świata aplikacji.