1. Введение
Flutter — это набор инструментов пользовательского интерфейса Google для создания красивых, скомпилированных в собственном коде приложений для мобильных устройств, Интернета и настольных компьютеров из единой базы кода. Flutter работает с существующим кодом, используется разработчиками и организациями по всему миру, он бесплатен и имеет открытый исходный код.
В этой кодовой лаборатории вы улучшите музыкальное приложение Flutter, превратив его из скучного в красивое. Для этого в этой лаборатории кода используются инструменты и API, представленные в Материале 3 .
Что вы узнаете
- Как написать приложение Flutter, которое будет удобным и красивым на разных платформах.
- Как разработать текст в своем приложении, чтобы он повышал удобство использования.
- Как выбрать правильные цвета, настроить виджеты, создать собственную тему и быстро и легко реализовать темный режим.
- Как создавать кроссплатформенные адаптивные приложения.
- Как создавать приложения, которые хорошо выглядят на любом экране.
- Как добавить движение в ваше приложение Flutter, чтобы оно стало по-настоящему ярким.
Предпосылки:
В этой кодовой лаборатории предполагается, что у вас есть некоторый опыт работы с Flutter. Если нет, возможно, вам стоит сначала изучить основы. Следующие ссылки полезны:
- Ознакомьтесь с фреймворком виджетов Flutter
- Попробуйте написать свое первое приложение Flutter, часть 1, кодовая лаборатория
Что ты построишь
Эта лаборатория кода поможет вам создать главный экран для приложения MyArtist — музыкального проигрывателя, с помощью которого фанаты могут быть в курсе событий своих любимых исполнителей. В нем обсуждается, как можно изменить дизайн своего приложения, чтобы оно выглядело красиво на разных платформах.
В следующих видеороликах показано, как приложение работает после завершения этой лаборатории кода:
Что бы вы хотели узнать из этой кодовой лаборатории?
2. Настройте среду разработки Flutter.
Для выполнения этой лабораторной работы вам понадобятся два программного обеспечения — Flutter SDK и редактор .
Вы можете запустить кодовую лабораторию, используя любое из этих устройств:
- Физическое устройство Android или iOS , подключенное к вашему компьютеру и переведенное в режим разработчика.
- Симулятор iOS (требуется установка инструментов Xcode).
- Эмулятор Android (требуется установка в Android Studio).
- Браузер (для отладки необходим Chrome).
- В качестве настольного приложения для Windows , Linux или macOS . Вы должны разрабатывать на платформе, на которой планируете развернуть. Итак, если вы хотите разработать настольное приложение для Windows, вам необходимо разработать его в Windows, чтобы получить доступ к соответствующей цепочке сборки. Существуют требования, специфичные для операционной системы, которые подробно описаны на docs.flutter.dev/desktop .
3. Загрузите стартовое приложение Codelab.
Клонируйте его с GitHub
Чтобы клонировать эту кодовую лабораторию из GitHub, выполните следующие команды:
git clone https://github.com/flutter/codelabs.git cd codelabs/boring_to_beautiful/step_01/
Чтобы убедиться, что все работает, запустите приложение Flutter как настольное приложение, как показано ниже. Альтернативно откройте этот проект в своей IDE и используйте его инструменты для запуска приложения.
Успех! Стартовый код для главного экрана MyArtist должен быть запущен. Вы должны увидеть главный экран MyArtist. На настольном компьютере он выглядит нормально, но на мобильных устройствах... Не очень хорошо. Во-первых, это не соответствует отметке. Не волнуйся, ты это исправишь!
Ознакомьтесь с кодом
Далее ознакомьтесь с кодом.
Откройте lib/src/features/home/view/home_screen.dart
, который содержит следующее:
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Этот файл импортирует material.dart
и реализует виджет с сохранением состояния, используя два класса:
- Оператор
import
делает доступными материальные компоненты. - Класс
HomeScreen
представляет всю отображаемую страницу. - Метод
build()
класса_HomeScreenState
создает корень дерева виджетов, который влияет на то, как создаются все виджеты в пользовательском интерфейсе.
4. Воспользуйтесь преимуществами типографики
Текст повсюду. Текст — полезный способ общения с пользователем. Ваше приложение должно быть дружелюбным и веселым или, возможно, заслуживающим доверия и профессиональным? Есть причина, по которой ваше любимое банковское приложение не использует Comic Sans. То, как представлен текст, формирует первое впечатление пользователя о вашем приложении. Вот несколько способов более продуманного использования текста.
Покажи, а не рассказывай
Везде, где это возможно, «покажите», а не «расскажите». Например, NavigationRail
в стартовом приложении имеет вкладки для каждого основного маршрута, но ведущие значки идентичны:
Это бесполезно, поскольку пользователю все равно придется читать текст каждой вкладки. Начните с добавления визуальных подсказок, чтобы пользователь мог быстро взглянуть на ведущие значки и найти нужную вкладку. Это также помогает с локализацией и доступностью.
В lib/src/shared/router.dart
добавьте отдельные ведущие значки для каждого пункта назначения навигации (дома, списка воспроизведения и людей):
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',
),
];
Проблемы?
Если ваше приложение работает неправильно, поищите опечатки. При необходимости используйте код по следующим ссылкам, чтобы вернуться в нужное русло.
Выбирайте шрифты вдумчиво
Шрифты определяют индивидуальность вашего приложения, поэтому выбор правильного шрифта имеет решающее значение. При выборе шрифта следует учитывать несколько факторов:
- Без засечек или с засечками : шрифты с засечками имеют декоративные штрихи или «хвостики» на концах букв и воспринимаются как более формальные. Шрифты без засечек не имеют декоративных штрихов и обычно воспринимаются как более неформальные. Заглавная буква Т без засечек и заглавная буква Т.
- Шрифты с заглавными буквами . Использование заглавных букв подходит для привлечения внимания к небольшим объемам текста (например, к заголовкам), но при чрезмерном использовании это может быть воспринято как крик, заставляющий пользователя полностью его игнорировать.
- Регистр заголовка или регистр предложения . При добавлении заголовков или меток учитывайте, как вы используете заглавные буквы: регистр заголовка , в котором первая буква каждого слова пишется с заглавной буквы («Это заголовок регистра заголовка»), является более формальным. Падеж предложения , в котором с заглавной буквы пишутся только имена собственные и первое слово в тексте («Это заголовок падежа предложения»), является более разговорным и неформальным.
- Кернинг (интервал между буквами), длина строки (ширина всего текста на экране) и высота строки (высота каждой строки текста) : слишком много или слишком мало любого из этих элементов делает ваше приложение менее читабельным. Например, легко потерять место, читая большой непрерывный блок текста.
Имея это в виду, зайдите в Google Fonts и выберите шрифт без засечек, например Montserrat , поскольку музыкальное приложение должно быть игривым и веселым.
Из командной строки извлеките пакет google_fonts
. При этом также обновляется файл pubspec , в который добавляются шрифты в качестве зависимости приложения.
$ 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>
В lib/src/shared/extensions.dart
импортируйте новый пакет:
lib/src/shared/extensions.dart
import 'package:google_fonts/google_fonts.dart'; // Add this line.
Установите TextTheme:
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
Горячая перезагрузка чтобы активировать изменения. (Используйте кнопку в вашей IDE или в командной строке введите r
для горячей перезагрузки.):
Вы должны увидеть новые значки NavigationRail
вместе с текстом, отображаемым шрифтом Montserrat.
Проблемы?
Если ваше приложение работает неправильно, поищите опечатки. При необходимости используйте код по следующим ссылкам, чтобы вернуться в нужное русло.
5. Установите тему
Темы помогают придать приложению структурированный дизайн и единообразие, определяя заданную систему цветов и стилей текста. Темы позволяют быстро реализовать пользовательский интерфейс, не зацикливаясь на мелких деталях, таких как указание точного цвета для каждого отдельного виджета.
Разработчики Flutter обычно создают компоненты с индивидуальной темой одним из двух способов:
- Создавайте отдельные пользовательские виджеты, каждый со своей темой.
- Создавайте темы с ограниченной областью действия для виджетов по умолчанию.
В этом примере используется поставщик темы , расположенный в lib/src/shared/providers/theme.dart
для создания виджетов и цветов с единой темой во всем приложении:
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);
}
}
Чтобы использовать поставщика, создайте экземпляр и передайте его объекту темы с заданной областью в MaterialApp
, расположенном в lib/src/shared/app.dart
. Он будет унаследован любыми вложенными объектами 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,
);
},
),
)),
),
);
}
}
Теперь, когда тема настроена, выберите цвета для приложения.
Выбрать правильный набор цветов не всегда легко. У вас может быть представление об основном цвете, но, скорее всего, вы хотите, чтобы в вашем приложении было более одного цвета. Какого цвета должен быть текст? Заголовок? Содержание? Ссылки? А как насчет цвета фона? Material Theme Builder — это веб-инструмент (представленный в Материале 3), который помогает вам выбрать набор дополнительных цветов для вашего приложения.
Чтобы выбрать исходный цвет для приложения, откройте конструктор тем материалов и изучите различные цвета для пользовательского интерфейса. Важно выбрать цвет, который соответствует эстетике бренда и/или вашим личным предпочтениям.
После создания темы щелкните правой кнопкой мыши пузырь основного цвета — откроется диалоговое окно, содержащее шестнадцатеричное значение основного цвета. Скопируйте это значение. (Вы также можете установить цвет с помощью этого диалогового окна.)
Передайте шестнадцатеричное значение основного цвета поставщику темы. Например, шестнадцатеричный цвет #00cbe6
указывается как Color(0xff00cbe6)
. ThemeProvider
генерирует ThemeData
, содержащий набор дополнительных цветов, которые вы предварительно просмотрели в Material Theme Builder:
final settings = ValueNotifier(ThemeSettings(
sourceColor: Color(0xff00cbe6), // Replace this color
themeMode: ThemeMode.system,
));
Горячий перезапуск приложения. При наличии основного цвета приложение становится более выразительным. Получите доступ ко всем новым цветам, ссылаясь на тему в контексте и получая ColorScheme
:
final colors = Theme.of(context).colorScheme;
Чтобы использовать определенный цвет, получите доступ к роли цвета в colorScheme
. Перейдите в lib/src/shared/views/outlined_card.dart
и задайте 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 представлены тонкие цветовые роли, которые дополняют друг друга и могут использоваться в пользовательском интерфейсе для добавления новых уровней выражения. Эти новые цветовые роли включают в себя:
-
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
Кроме того, новые токены дизайна поддерживают как светлую, так и темную темы:
Эти цветовые роли можно использовать для присвоения значения и акцента различным частям пользовательского интерфейса. Даже если компонент не заметен, он все равно может использовать преимущества динамического цвета.
Пользователь может установить яркость приложения в системных настройках устройства. В lib/src/shared/app.dart
, когда устройство переведено в темный режим, верните темную тему и режим темы в 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,
);
Нажмите значок луны в правом верхнем углу, чтобы включить темный режим.
Проблемы?
Если ваше приложение работает неправильно, используйте код по следующей ссылке, чтобы вернуться в нужное русло.
6. Добавьте адаптивный дизайн
С помощью Flutter вы можете создавать приложения, которые работают практически где угодно, но это не значит, что каждое приложение должно вести себя везде одинаково. Пользователи привыкли ожидать разного поведения и функций от разных платформ.
Material предлагает пакеты, упрощающие работу с адаптивными макетами — эти пакеты Flutter можно найти на GitHub .
При создании кроссплатформенного адаптивного приложения учитывайте следующие различия платформ:
- Способ ввода : мышь, сенсорный экран или геймпад.
- Размер шрифта, ориентация устройства и расстояние просмотра
- Размер и форм-фактор экрана : телефон, планшет, складной, настольный компьютер, Интернет.
Файл lib/src/shared/views/adaptive_navigation.dart
содержит класс навигации, в котором вы можете предоставить список пунктов назначения и контент для визуализации тела. Поскольку вы используете этот макет на нескольких экранах, существует общий базовый макет, который можно передать каждому дочернему элементу. Навигационные направляющие хороши для настольных компьютеров и больших экранов, но сделайте макет удобным для мобильных устройств, показывая вместо этого нижнюю панель навигации на мобильных устройствах.
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.
},
);
}
}
Не все экраны одинакового размера. Если бы вы попытались отобразить настольную версию своего приложения на своем телефоне, вам пришлось бы щуриться и масштабировать изображение, чтобы увидеть все. Вы хотите, чтобы ваше приложение меняло внешний вид в зависимости от экрана, на котором оно отображается. Благодаря адаптивному дизайну ваше приложение будет отлично выглядеть на экранах любого размера.
Чтобы сделать ваше приложение отзывчивым, добавьте несколько адаптивных точек останова (не путать с точками останова отладки). Эти точки останова определяют размеры экрана, при которых ваше приложение должно изменить свой макет.
Меньшие экраны не могут отображать столько же, сколько большие экраны, без сжатия содержимого. Чтобы приложение не выглядело как уменьшенное настольное приложение, создайте отдельный макет для мобильных устройств, в котором для разделения содержимого используются вкладки. Это придает приложению более приятный вид на мобильных устройствах.
Следующие методы расширения (определенные в проекте MyArtist в 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;
}
Планшетом считается экран размером более 730 пикселей (в самом длинном направлении), но менее 1200 пикселей. Все, что больше 1200 пикселей, считается рабочим столом. Если устройство не является ни планшетом, ни настольным компьютером, то оно считается мобильным. Вы можете узнать больше об адаптивных точках останова на сайте Material.io . Вы можете рассмотреть возможность использования пакета Adaptive_breakpoints .
Адаптивный макет главного экрана использует AdaptiveContainer
и AdaptiveColumn
на основе сетки из 12 столбцов с использованием пакетов Adaptive_comComponents и Adaptive_breakpoints для реализации адаптивного макета сетки в 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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
Адаптивному макету нужны два макета: один для мобильных устройств и адаптивный макет для больших экранов. LayoutBuilder
в настоящее время возвращает только макет рабочего стола. В lib/src/features/home/view/home_screen.dart
создайте макет для мобильных устройств в виде TabBar
и TabBarView
с 4 вкладками.
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Проблемы?
Если ваше приложение работает неправильно, используйте код по следующей ссылке, чтобы вернуться в нужное русло.
Используйте пробелы
Пробелы — важный визуальный инструмент вашего приложения, создающий организационный разрыв между разделами.
Лучше иметь слишком много пробелов, чем недостаточно. Добавление большего количества пробелов предпочтительнее, чем уменьшение размера шрифта или визуальных элементов, чтобы они больше вписывались в пространство.
Недостаток свободного пространства может стать проблемой для людей с проблемами зрения. Слишком много пробелов может привести к недостаточной связности и сделать ваш пользовательский интерфейс плохо организованным. Например, посмотрите следующие скриншоты:
Далее вы добавите пробелы на главный экран, чтобы освободить ему больше места. Затем вы дополнительно настроите макет, чтобы точно настроить интервал.
Оберните виджет объектом Padding
, чтобы добавить пробелы вокруг этого виджета. Увеличьте все текущие значения заполнения в lib/src/features/home/view/home_screen.dart
до 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,
),
),
],
),
),
],
),
),
),
],
),
),
);
Горячая перезагрузка приложения. Он должен выглядеть так же, как и раньше, но с большим количеством пробелов между виджетами. Дополнительные отступы выглядят лучше, но баннер подсветки вверху все еще находится слишком близко к краям.
В lib/src/features/home/view/home_highlight.dart
измените отступ баннера на 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'),
),
),
),
],
);
}
}
Горячая перезагрузка приложения. Между двумя списками воспроизведения внизу нет пробелов, поэтому они выглядят так, как будто принадлежат к одной таблице. Это не так, и вы исправите это дальше.
Добавьте пробелы между списками воспроизведения, вставив виджет размера в Row
, содержащую их. В lib/src/features/home/view/home_screen.dart
добавьте SizedBox
шириной 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,
),
],
),
),
],
),
),
Горячая перезагрузка приложения. Приложение должно выглядеть следующим образом:
Теперь на главном экране достаточно места, но все выглядит слишком разрозненным, и между разделами нет связи.
До сих пор вы установили все отступы (как по горизонтали, так и по вертикали) для виджетов на главном экране равным 35 с помощью EdgeInsets.all(35)
, но вы также можете установить отступы для каждого из краев независимо. Настройте отступы так, чтобы они лучше вписывались в пространство.
-
EdgeInsets.LTRB()
устанавливает значение слева, сверху, справа и снизу индивидуально. -
EdgeInsets.symmetric()
устанавливает эквивалентное отступы по вертикали (сверху и внизу), а по горизонтали (слева и справа) — эквивалентно. -
EdgeInsets.only()
устанавливает только указанные края.
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
В lib/src/features/home/view/home_highlight.dart
установите для левого и правого отступов баннера значение 35, а для верхнего и нижнего отступов — 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'),
),
),
),
],
);
}
}
Горячая перезагрузка приложения. Планировка и пространство выглядят намного лучше! В качестве завершающего штриха добавьте немного движения и анимации.
Проблемы?
Если ваше приложение работает неправильно, используйте код по следующей ссылке, чтобы вернуться в нужное русло.
7. Добавьте движение и анимацию.
Движение и анимация — отличные способы представить движение и энергию, а также обеспечить обратную связь, когда пользователь взаимодействует с приложением.
Анимация между экранами
ThemeProvider
определяет PageTransitionsTheme
с анимацией перехода экрана для мобильных платформ (iOS, Android). Пользователи настольных компьютеров уже получают обратную связь по щелчку мыши или трекпада, поэтому анимация перехода между страницами не требуется.
Flutter предоставляет анимацию перехода экрана, которую вы можете настроить для своего приложения в зависимости от целевой платформы, как показано в 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(),
},
);
Передайте PageTransitionsTheme
как светлой, так и темной темам в 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,
);
}
Без анимации на iOS
С анимацией на iOS
Проблемы?
Если ваше приложение работает неправильно, используйте код по следующей ссылке, чтобы вернуться в нужное русло.
Добавить состояния при наведении
Один из способов добавить движение в настольное приложение — использовать состояния наведения , когда виджет меняет свое состояние (например, цвет, форму или содержимое), когда пользователь наводит на него курсор.
По умолчанию класс _OutlinedCardState
(используемый для плиток списка воспроизведения «недавно воспроизведенных») возвращает MouseRegion
, который превращает стрелку курсора в указатель при наведении курсора, но вы можете добавить дополнительную визуальную обратную связь.
Откройте lib/src/shared/views/outlined_card.dart и замените его содержимое следующей реализацией, чтобы ввести состояние _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,
),
),
);
}
}
Горячая перезагрузка приложения, а затем наведение курсора мыши на одну из плиток недавно воспроизведенного плейлиста.
OutlinedCard
меняет непрозрачность и закругляет углы.
Наконец, анимируйте номер песни в списке воспроизведения в кнопку воспроизведения с помощью виджета HoverableSongPlayButton
определенного в lib/src/shared/views/hoverable_song_play_button.dart
. В lib/src/features/playlists/view/playlist_songs.dart
оберните виджет Center
(который содержит номер песни) с помощью 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
Горячая перезагрузка приложения, а затем наведение курсора на номер песни в плейлисте «Лучшие песни сегодня» или в плейлисте «Новые релизы» .
Число анимируется в кнопку воспроизведения , которая воспроизводит песню, когда вы нажимаете на нее.
Окончательный код проекта смотрите на GitHub .
8. Поздравляем!
Вы завершили эту кодовую работу! Вы узнали, что существует множество небольших изменений, которые вы можете интегрировать в приложение, чтобы сделать его более красивым, а также более доступным, более локализуемым и более подходящим для нескольких платформ. Эти методы включают, помимо прочего:
- Типографика: Текст — это больше, чем просто инструмент коммуникации. Используйте способ отображения текста, чтобы оказать положительное влияние на восприятие пользователями вашего приложения.
- Тематика: создайте систему дизайна, которую вы сможете надежно использовать без необходимости принимать дизайнерские решения для каждого виджета.
- Адаптивность. Учитывайте устройство и платформу, на которой пользователь запускает ваше приложение, и его возможности. Учитывайте размер экрана и способ отображения вашего приложения.
- Движение и анимация. Добавление движения в ваше приложение добавляет энергии пользовательскому опыту и, что более практично, обеспечивает обратную связь для пользователей.
С помощью нескольких небольших настроек ваше приложение может превратиться из скучного в красивое:
До
После
Следующие шаги
Мы надеемся, что вы узнали больше о создании красивых приложений во Flutter!
Если вы примените какой-либо из советов или приемов, упомянутых здесь (или у вас есть собственный совет), мы будем рады услышать ваше мнение! Свяжитесь с нами в Твиттере по адресу @rodydavis и @kanhnwin !
Вам также могут оказаться полезными следующие ресурсы.
Тематика
- Конструктор тем материалов (инструмент)
Адаптивные и отзывчивые ресурсы:
- Декодирование Flutter на адаптивном и адаптивном устройствах (видео)
- Адаптивные макеты (видео с The Boring Flutter Development Show)
- Создание отзывчивых и адаптивных приложений (flutter.dev)
- Компоненты Adaptive Material для Flutter (библиотека на GitHub)
- 5 вещей, которые вы можете сделать, чтобы подготовить свое приложение для больших экранов (видео с Google I/O 2021)
Общие ресурсы дизайна:
- Мелочи: Стать мифическим дизайнером-разработчиком (видео с Flutter Engage)
- Material Design 3 для складных устройств (material.io)
Также присоединяйтесь к сообществу Flutter !
Идите вперед и сделайте мир приложений красивым!