适用于 Flutter 应用程序的自适应材料组件

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检索的约束信息。AdaptiveColumnAdaptiveContainer用作子元素,因为AdaptiveContainer有一个columnSpan属性来帮助放置子元素。

如何使用自适应容器?

通过使用AdaptiveContainer,您将能够创建上面的应用程序。该应用程序会将 UI 拉伸到一个点,并在一个点之后显示详细视图,而无需我们定义任何“断点”。

要使用AdaptiveContainerAdaptiveColumn,我们需要将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 中,您可以看到一些小部件在同一“行”中,而一些小部件是一个接一个地绘制的。AdaptiveColumnAdaptiveContainer的组合实际上帮助我们实现了这一点。

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(...) 帮助我们在需要时将数据映射到小部件中。所有元素的 acolumnSpan为 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):是当窗口约束绑定到smallxsmall自适应断点 smallBody时要显示的页面。
  • (6)/(7):secondaryBodysmallSecondaryBody分别显示在secondaryBody槽的中断点或小断点处。例如,当桌面宽度过宽时,辅助体会出现以提供更多信息。
  • (8):生成导航元素以跟踪目的地。

结论

这篇博文的目的是让您了解这个在幕后进行的惊人项目。他们有几个我在此过程中发现的问题,但随着更多的关注和使用,我相信它会成为一个好地方。

文章来源:https://salih.dev/adaptive-material-components-for-your-flutter-applications

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容