Flutter 对移动、桌面和 Web 的支持给我们的生活带来了新的挑战:支持不同的屏幕尺寸并调整我们的设计。
大多数时候,开发人员倾向于考虑他们拥有的设备并相应地构建他们的应用程序。但在所有目标设备上都有不同的屏幕尺寸、方向、像素密度。作为一名优秀的开发人员,我们应该相应地调整您的设计。
这个话题的重要性已经引起了很多人的关注,所以很多人开始创建关于它的内容和解决方案。引起我注意的解决方案之一是 Material Design 团队的自适应材料组件库。
在这篇博文中,您将了解如何使用自适应材料组件库,以及它如何帮助您轻松实现自适应设计。
一点历史
如果您不想阅读一些谈论他的学习经历的家伙,您可以跳过并转到下一部分进行实施
Flutter 开发人员长期以来一直依赖LayoutBuilder、MediaQuery.of、FractionallySizedBox、AspectRatio、Column、Flexible和许多其他小部件来实现应用程序的实际自适应设计。但他们总是有类似的问题,例如“我从平板电脑到手机再回到电脑屏幕的断点到底是什么?”。他们寻找了几种不同的资源,但找不到明确的答案。一方面他们的设计师可能会给他们一些建议,但另一方面他们只会问自己“难道没有更好的方法来做到这一点吗?”
幸运的是,Flutter 的人们已经听到并感受到了 Flutter 开发人员试图实现这一目标的问题和绝望的尝试,他们想要提供帮助。他们开始研究一个名为flutter_adaptive_scaffold的库。这个库处理屏幕大小的变化、导航和在正确的时间显示应用程序的适当部分实际上是社区的游戏规则改变库。
当我在 Flutter Vikings 的演讲中玩弄这个库时,我意识到flutter_adaptive_scaffold库很好,但它太自以为是了。如果我只需要 sccren 更改侦听器,我就无法拥有它,因为我受制于他们实现 UI 的方式。我仍然保留它在我的演讲中向人们展示它是如何在 Flutter 应用程序上工作的,但我的一部分渴望更好的方法。
在 Flutter 期间,我主持了几次演讲,我有机会看到一些伟大人物的精彩演讲。其中之一来自Anthony Robledo关于google_fonts的文章,但我关注的是他提到的自适应材料库。所以我开始深入研究,现在你会从中看到我的发现。
英雄:adaptive_components
在我寻找一个易于使用的自适应设计库时,adaptive_components是处理不同屏幕尺寸的 UI 变化的最直接的方法。该库遵循不同屏幕尺寸的材料设计指南。
截至 2022 年 9 月 15 日,adaptive_components库有两个实体组件。其中一个是AdaptiveContainer,另一个是AdaptiveColumn。
AdaptiveContainer是一个专用容器,可让您创建具有自适应约束的容器。它在内部使用LayoutBuilder来检查容器是否处于所需的约束中。您可以将AdaptiveContainer与多个子小部件(例如 Column、Stack 等)一起使用。
AdaptiveColumn 是一个小部件,用于保留子小部件并将它们放置在Wrap小部件中,并使用LayoutBuilder检索的约束信息。AdaptiveColumn将AdaptiveContainer用作子元素,因为AdaptiveContainer有一个columnSpan
属性来帮助放置子元素。
如何使用自适应容器?
通过使用AdaptiveContainer,您将能够创建上面的应用程序。该应用程序会将 UI 拉伸到一个点,并在一个点之后显示详细视图,而无需我们定义任何“断点”。
要使用AdaptiveContainer或AdaptiveColumn,我们需要将Adaptive_components库添加到我们的项目中。打开您的pubspec.yamldependencies
文件并在标签下添加以下内容:
dependencies:
flutter:
sdk: flutter
adaptive_components: ^0.0.7
添加此调用flutter pub get
以将库下载到您的项目后。一旦你有了项目,现在是时候使用这个库了:
import 'package:adaptive_components/adaptive_components.dart';
import 'package:adaptive_libraries_showcase/game.dart';
import 'package:adaptive_libraries_showcase/game_detail.dart';
import 'package:adaptive_libraries_showcase/game_list.dart';
import 'package:adaptive_libraries_showcase/main_page_large.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const AdaptiveContainerExample());
}
class AdaptiveContainerExample extends StatefulWidget {
const AdaptiveContainerExample({Key? key}) : super(key: key);
@override
State<AdaptiveContainerExample> createState() =>
_AdaptiveContainerExampleState();
}
class _AdaptiveContainerExampleState extends State<AdaptiveContainerExample> {
Game game = games.first;
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFF030403),
// (1)
body: Stack(
children: [
// (2)
AdaptiveContainer(
// (3)
constraints: const AdaptiveConstraints(
xsmall: false,
small: false,
medium: false,
large: true,
xlarge: true,
),
child: Row(
children: [
Expanded(
child: GameList(
games: games,
onGameSelected: (game) {
setState(() {
this.game = game;
});
},
),
),
Expanded(
child: GameDetail(game: game),
),
],
),
),
AdaptiveContainer(
constraints: const AdaptiveConstraints(
xsmall: true,
small: true,
medium: true,
large: false,
xlarge: false,
),
child: GameList(
games: games,
onGameSelected: (game) {},
),
),
],
),
),
);
}
}
- (1) : Stack, Column 等类型的小部件用于具有多个子元素,因此当它们显示时,可见的子元素可以协调它们的定位
- (2)/(4) :所有 AdaptiveContainers 都用于为某些屏幕尺寸创建自适应 UI 元素
- (3) : AdaptiveConstraints 可用于决定哪些约束适用于 AdaptiveContainer。您可以像上面那样调用常规构造函数并组合不同的屏幕尺寸,也可以调用命名构造函数
AdaptiveConstraints.small()
以支持一种约束类型。
如何使用自适应列?
使用AdaptiveColumn不是火箭科学。每当您计划使用(像任何其他多子小部件一样)时,您都会将其添加到您的代码中并在其中添加一些子小部件。
您之前可能没有意识到这一点(因为您还没有看到源代码),但是在游戏详细信息页面中,我们实际上是使用AdaptiveColumn来布置子小部件。在上面的 UI 中,您可以看到一些小部件在同一“行”中,而一些小部件是一个接一个地绘制的。AdaptiveColumn和AdaptiveContainer的组合实际上帮助我们实现了这一点。
import 'package:adaptive_components/adaptive_components.dart';
import 'package:adaptive_libraries_showcase/game.dart';
import 'package:adaptive_libraries_showcase/game_item_small.dart';
import 'package:flutter/material.dart';
class GameDetail extends StatelessWidget {
const GameDetail({
required this.game,
Key? key,
}) : super(key: key);
final Game game;
@override
Widget build(BuildContext context) {
final similarGames = games.where((element) =>
element.category == game.category && element.name != game.name);
return Padding(
padding: const EdgeInsets.only(top: 8),
// (1)
child: Align(
alignment: Alignment.topCenter,
// (2)
child: AdaptiveColumn(
children: [
// (3)
AdaptiveContainer(
// (4)
columnSpan: 12,
color: Colors.blue,
child: Image.network(
game.backdropImage,
height: 300,
fit: BoxFit.cover,
),
),
AdaptiveContainer(
columnSpan: 6,
child: Text(
game.name,
style: Theme.of(context)
.textTheme
.headline2
?.copyWith(color: Colors.white),
),
),
AdaptiveContainer(
columnSpan: 6,
child: Text(
game.description,
style: Theme.of(context)
.textTheme
.headline6
?.copyWith(color: Colors.white),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Text(
'Other ${game.category.name} games',
style: Theme.of(context)
.textTheme
.headline6
?.copyWith(color: Colors.white),
),
),
// (5)
...similarGames.map(
(e) => AdaptiveContainer(
columnSpan: 1,
child: GameItemSmall(game: e),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Text(
game.releaseDate,
style: Theme.of(context)
.textTheme
.headline6
?.copyWith(color: Colors.white),
),
),
],
),
),
);
}
}
- (1):对齐小部件帮助我们将小部件定位在父小部件的任何可用部分。如果不使用对齐,子小部件位于中心。
- (2):您可以在小部件树的任何位置使用AdaptiveColumn。它将根据父级在绘制子小部件之前的大小进行计算。
- (3): AdaptiveColumn的每个子元素都应该是一个AdaptiveContainer。
- (4): AdaptiveContainer有一个字段叫
columnSpan
,默认为 1。内容放置在包含列的屏幕区域中。在响应式布局中,列宽是用百分比定义的,而不是固定值。这允许内容适应任何屏幕尺寸。网格中显示的列数取决于断点范围,即预先确定的屏幕尺寸范围。断点可以对应于手机、平板电脑或其他屏幕类型。每个弹性范围最多可以有 12 列。如需更多信息,请查看此处。 - (5):Spread operator(...) 帮助我们在需要时将数据映射到小部件中。所有元素的 a
columnSpan
为 1,这就是它们彼此相邻的原因。
导航器:adaptive_navigation
Material Design 对导航有强烈的看法。他们使用此包根据屏幕大小使用 Drawer、NavigationRail 或 BottomNavigationBar。每个导航目的地都是预定义的。
为了使用AdaptiveNavigationScaffold,我们需要将Adaptive_navigation库添加到我们的项目中。打开您的pubspec.yamldependencies
文件并在标签下添加以下内容:
dependencies:
flutter:
sdk: flutter
adaptive_navigation: ^0.0.7
添加此调用flutter pub get
以将库下载到您的项目后。一旦你有了项目,现在你可以在你的项目中使用这个库:
import 'package:adaptive_libraries_showcase/game.dart';
import 'package:adaptive_libraries_showcase/game_list.dart';
import 'package:adaptive_navigation/adaptive_navigation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(home: DefaultScaffoldDemo()));
}
class DefaultScaffoldDemo extends StatefulWidget {
const DefaultScaffoldDemo({Key? key}) : super(key: key);
@override
State<DefaultScaffoldDemo> createState() => _DefaultScaffoldDemoState();
}
class _DefaultScaffoldDemoState extends State<DefaultScaffoldDemo> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
// (1)
return AdaptiveNavigationScaffold(
selectedIndex: _selectedIndex,
// (2)
bottomNavigationOverflow: 10,
// (3)
onDestinationSelected: (index) {
setState(() {
_selectedIndex = index;
});
},
destinations: _getDestinations(),
body: GameList(
games: games
.where(
(element) => element.category == Category.values[_selectedIndex],
)
.toList(growable: false),
onGameSelected: (value) {},
),
);
}
List<AdaptiveScaffoldDestination> _getDestinations() {
int index = -1;
return Category.values.map(
(e) {
index++;
// (4)
return AdaptiveScaffoldDestination(
title: e.name,
icon: icons[index],
);
},
).toList(growable: false);
}
}
const icons = [
Icons.accessibility_new,
Icons.power_settings_new_outlined,
Icons.open_in_new_off_rounded,
Icons.backpack,
Icons.cable_outlined,
Icons.dark_mode_outlined,
Icons.earbuds,
Icons.face_outlined,
Icons.games,
Icons.hail
];
- (1): AdaptiveNavigationScaffold是用于实现库的默认 Scaffold。它适应屏幕尺寸并显示适当的导航元素
- (2):您可以决定底部导航栏中的元素。默认值为 5。我选择了 10,因为添加了 10 个类别。
- (3):每次选择导航元素时都会调用此回调,您可以选择一个要分配的值来跟踪它
- (4): AdaptiveScaffoldDestination是跟踪目的地的类。
您可以充分利用它的用途。您可以为每个选定元素显示不同的 UI,或者像我一样,过滤掉与目标无关的任何数据。
如果您对用于选择导航类型的导航器的断点不满意,您可以覆盖该navigationTypeResolver
属性,但 IMO,如果您想要一种自以为是的导航方式,那么坚持使用库是有意义的。
完整包:flutter_adaptive_scaffold
自 2022 年 9 月 15 日起,包adaptive_scaffold重命名为flutter_adaptive_scaffold,因为其他人已经获得了adaptive_scaffold这个名称。:)
但是这个AdaptiveScaffold是什么,我们为什么要使用它呢?AdaptiveScaffold对来自用户、设备和屏幕元素的输入做出反应,并根据 Material 3 指南渲染您的 Flutter 应用程序。它将导航和组件库与组件之间的精美动画相结合。
为了使用任一AdaptiveScaffold,我们需要将Adaptive_navigation库添加到我们的项目中。打开您的pubspec.yamldependencies
文件并在标签下添加以下内容:
dependencies:
flutter:
sdk: flutter
flutter_adaptive_scaffold: ^0.0.3
添加此调用flutter pub get
以将库下载到您的项目后。是时候使用这个库了:
import 'package:adaptive_libraries_showcase/game.dart';
import 'package:adaptive_libraries_showcase/game_detail.dart';
import 'package:adaptive_libraries_showcase/game_list.dart';
import 'package:adaptive_libraries_showcase/main_navigation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
void main() {
runApp(const _MyApp());
}
class _MyApp extends StatelessWidget {
const _MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(home: MyHomePage());
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _selectedIndex = 0;
Game game = games.first;
@override
Widget build(BuildContext context) {
// (1)
return BottomNavigationBarTheme(
data: const BottomNavigationBarThemeData(
unselectedItemColor: Colors.black,
selectedItemColor: Colors.black,
backgroundColor: Colors.white,
),
// (2)
child: AdaptiveScaffold(
selectedIndex: _selectedIndex,
onSelectedIndexChange: (index) {
setState(() {
_selectedIndex = index;
});
},
useDrawer: false,
// (3)
destinations: _getDestinations(),
// (4)
body: (_) => Row(
children: [
Expanded(
child: GameList(
games: games,
onGameSelected: (game) {
setState(() {
this.game = game;
});
},
),
),
Expanded(
child: GameDetail(
game: game,
hasDarkText: true,
),
),
],
),
// (5)
smallBody: (_) => GameList(
games: games
.where(
(element) =>
element.category == Category.values[_selectedIndex],
)
.toList(growable: false),
onGameSelected: (value) {},
),
// (6)
// secondaryBody: AdaptiveScaffold.emptyBuilder,
// (7)
// smallSecondaryBody: AdaptiveScaffold.emptyBuilder,
),
);
}
// (8)
List<NavigationDestination> _getDestinations() {
int index = -1;
return Category.values.map(
(e) {
index++;
return NavigationDestination(
label: e.name,
icon: Icon(icons[index]),
);
},
).toList(growable: false);
}
}
现在让我们检查上面的代码:
- (1):
BottomNavigationBarTheme
帮助我们为显示在较小屏幕中的底部导航栏定义主题系统。 - (2):
AdaptiveScaffold
是一种特殊的 Scaffold,使用一种自以为是的方式来处理布局和导航。它是图书馆的基地。 - (3):
destinations
是用于生成导航类型的导航元素。 - (4):对于每个可能的页面,“尝试”显示的第一个主体是
body
属性。 - (5):是当窗口约束绑定到small或xsmall自适应断点
smallBody
时要显示的页面。 - (6)/(7):
secondaryBody
或smallSecondaryBody
分别显示在secondaryBody槽的中断点或小断点处。例如,当桌面宽度过宽时,辅助体会出现以提供更多信息。 - (8):生成导航元素以跟踪目的地。
结论
这篇博文的目的是让您了解这个在幕后进行的惊人项目。他们有几个我在此过程中发现的问题,但随着更多的关注和使用,我相信它会成为一个好地方。
文章来源:https://salih.dev/adaptive-material-components-for-your-flutter-applications