[Flutter翻译] 5 - 滑动组件

793 阅读18分钟

本文由简悦 SimpRead转码, 原文地址www.raywenderlich.com

可滚动的内容在任何应用程序中都是必须的。在本章中,你将学习如何使用列表和网格widg......。

构建可滚动的内容是UI开发的一个重要部分。用户一次只能处理这么多的信息,更不用说放在手掌上的整个屏幕上了!

在本章中,你将学习到关于可滚动部件的所有知识。特别是,你将学习。

  • 如何使用ListView。
  • 如何嵌套滚动视图。
  • 如何利用GridView的力量。

你将继续建立你的食谱应用程序Fooderlich,添加两个新屏幕。探索和菜谱。第一个屏幕显示当天的流行菜谱以及你的朋友正在做的菜。

第二个屏幕显示食谱库,如果你还在为今天做什么菜而犹豫不决,那就方便了。]

在本章结束时,你将成为一个可滚动的小工具向导

开始干吧

在Android Studio中打开启动项目,然后运行flutter pub get,如果有必要的话,运行该应用。

你会看到上一章中的Fooderlich应用程序。

项目文件

在这个启动项目中,有一些新的文件可以帮助你。在你学习如何创建可滚动部件之前,先看看它们。

资产文件夹

assets目录包含了所有的JSON文件和图片,你将使用它们来构建你的应用程序。

图片样本

  • food_pics。包含你将在整个应用程序中显示的食物图片。
  • magazine_pics: 你将在卡片小部件上显示的所有食品杂志背景图片。
  • profile_pics。包含raywenderlich.com团队成员的图片。

JSON数据

sample_data目录包含三个JSON文件。

  • sample_explore_recipes.json。在主屏幕上显示的菜谱列表。有时,用户可能希望得到今天做什么菜的建议
  • sample_friends_feed.json。这个列表包含了你朋友的帖子的样本,如果你对你的朋友在做什么感到好奇的话 👩🍳
  • sample_recipes.json。一个食谱列表,包括每个食谱的持续时间和烹饪难度的细节。

新 class

在lib目录中,你还会注意到三个新的文件夹,如下所示。

API文件夹

api文件夹包含一个模拟服务类。

MockFooderlichService是一个模拟服务器响应的服务类。它有异步函数,可以等待样本JSON文件被读取并解码为配方模型对象。

在本章中,你将使用两个API调用。

  • getExploreData()。返回ExploreData。在内部,它发出一个批处理请求,并返回两个列表:要探索的食谱和朋友的帖子。
  • getRecipes()。返回食谱的列表。

注意:不熟悉Dart中的异步工作方式?请查看Dart Apprentice中的异步章节或阅读这篇文章以了解更多:dart.dev/codelabs/as…

专业提示:有时你的后端服务还没有准备好消费。创建一个模拟服务对象是建立你的用户界面的一种灵活方式。你所要做的就是改变一个JSON文件,而不是创建许多配方模拟对象。

Models文件夹

你将使用这六个模型对象来构建你的应用程序的用户界面。

  • ExploreRecipe:关于一个食谱的所有细节。它包含了成分、说明、持续时间和一大堆其他内容。
  • Ingredient:一个单一的成分。这是ExploreRecipe的一部分。
  • Instruction:烹饪食谱的单一指令。它是ExploreRecipe的一部分。
  • Post:描述了一个朋友的帖子。一个帖子类似于一条推特,代表你的社交网络正在烹饪的内容。
  • ExploreData:将两个数据集分组。它包含一个ExploreRecipes的列表和一个Post的列表。
  • SimpleRecipe:一个菜谱的烹饪难度。

请自由探索每个模型对象所包含的不同属性!

注意:models.dart是一个桶状文件。它导出了你所有的模型对象,方便以后的导入。可以认为这是将许多导入文件分组到一个文件中。

Components 文件夹

lib/components包含了你所有的自定义小工具。

注意: components.dart是另一个桶状文件,它将所有的导入文件分组在一个文件中。

打开home.dart,查看页面。

static List<Widget> pages = <Widget>[
  Card1(
    recipe: ExploreRecipe(
      authorName: 'Ray Wenderlich',
      title: 'The Art of Dough',
      subtitle: 'Editor\'s Choice',
      message: 'Learn to make the perfect bread.',
      backgroundImage: 'assets/magazine_pics/mag1.jpg')),
  Card2(
    recipe: ExploreRecipe(
      authorName: 'Mike Katz',
      role: 'Smoothie Connoisseur',
      profileImage: 'assets/profile_pics/person_katz.jpeg',
      title: 'Recipe',
      subtitle: 'Smoothies',
      backgroundImage: 'assets/magazine_pics/mag2.png')),
  Card3(
    recipe: ExploreRecipe(
      title: 'Vegan Trends',
      tags: [
        'Healthy', 'Vegan', 'Carrots', 'Greens', 'Wheat',
        'Pescetarian', 'Mint', 'Lemongrass',
        'Salad', 'Water'
      ],
      backgroundImage: 'assets/magazine_pics/mag3.png')),
];

正如你在上面看到的,现在每张卡片都需要一个ExploreRecipe实例。

这就是新的启动项目文件的速度!现在你已经有了一个模拟服务。

现在你已经有了一个模拟服务和模型对象,你可以专注于可滚动的小部件了!

介绍一下ListView

ListView是一个非常流行的Flutter组件。它是一个线性的可滚动部件,线性地排列它的孩子,支持水平和垂直滚动。

有趣的是:列和行部件就像ListView,但没有滚动视图。

构造函数介绍

一个ListView有四个构造函数。

  • 默认的构造函数需要一个显式的widget列表,称为children。这将构建列表中的每一个孩子,甚至是不可见的孩子。如果你有少量的孩子,你应该使用这个构造函数。
  • ListView.builder()接收一个IndexedWidgetBuilder并按要求构建列表。它将只构建屏幕上可见的子节点。如果你需要显示大量或无限多的项目,你应该使用它。
  • ListView.separate()需要两个indexedWidgetBuilder:itemBuilder和seperatorBuilder。如果你想在你的项目之间放置一个分隔器部件,这很有用。
  • ListView.custom()为你提供了对子项更精细的控制。

注意:关于ListView构造函数的更多细节,请查看官方文档:api.flutter.dev/flutter/wid…

接下来,你将学习如何使用前三个构造函数!

设置ExploreScreen

你要创建的第一个屏幕是ExploreScreen。它包含两个部分。

  • TodayRecipeListView。一个水平滚动视图,可以让你在不同的卡片中平移。
  • FriendPostListView。一个垂直滚动视图,显示你的朋友在做什么。

在lib文件夹中,创建一个名为screen的新目录。

在这个新目录中,创建一个名为explore_screen.dart的新文件,并添加以下代码。

import 'package:flutter/material.dart';
import '../api/mock_fooderlich_service.dart';
import '../components/components.dart';

class ExploreScreen extends StatelessWidget {
  // 1
  final mockService = MockFooderlichService();

  ExploreScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 2
    // TODO 1: Add TodayRecipeListView FutureBuilder
    return const Center(
      child: Text('Explore Screen'));
  }
}

以下是代码的运作方式。

  1. 创建一个MockFooderlichService,来模拟服务器的响应。
  2. 显示一个占位符文本。稍后你会替换这个。

设置底部导航栏

打开home.dart,用以下内容替换BottomNavigationBar的项目。

const BottomNavigationBarItem(
  icon: Icon(Icons.explore), label: 'Explore'),
const BottomNavigationBarItem(
  icon: Icon(Icons.book), label: 'Recipes'),
const BottomNavigationBarItem(
  icon: Icon(Icons.list), label: 'To Buy'),

这里,你只是在更新每个BottomNavigationBarItem的图标和标签。

更新导航页面

在home.dart中,用以下内容替换pages。

static List<Widget> pages = <Widget>[
  ExploreScreen(),
  // TODO: Replace with RecipesScreen
  Container(color: Colors.green),
  Container(color: Colors.blue)
];

这将在第一个标签中显示新创建的ExploreScreen。

确保新的ExploreScreen已经导入。如果你的IDE没有自动添加它,添加这个导入。

import 'screens/explore_screen.dart';

热重启应用程序。它将看起来像这样。

注意:如果你没有看到上面的三个标签,请执行热重启,或完全重启应用程序。

你将在本章的后面替换容器。

创建一个FutureBuilder

如何用一个异步任务来显示你的UI?

MockFooderlichService包含返回Future对象的异步函数。FutureBuilder在这里派上了用场,因为它可以帮助你确定一个未来的状态。例如,它可以告诉你数据是否仍在加载或获取已经完成。

在explore_screen.dart中,替换注释下面的返回语句 // TODO 1: 添加TodayRecipeListView FutureBuilder和现有的返回语句,代码如下。

// 1
return FutureBuilder(
    // 2
    future: mockService.getExploreData(),
    // 3
    builder: (context, snapshot) {
      // TODO: Add Nested List Views
      // 4
      if (snapshot.connectionState == ConnectionState.done) {
        // 5
        final recipes = snapshot.data.todayRecipes;
        // TODO: Replace this with TodayRecipeListView
        return Center(
            child: Container(
                child: const Text('Show TodayRecipeListView')));
      } else {
        // 6
        return const Center(
            child: CircularProgressIndicator());
      }
    });

以下是代码的内容:

  1. 在widget的build()中,你创建了一个FutureBuilder。

  2. getExploreData()创建了一个未来,反过来,它将返回一个ExploreData实例。该实例将包含两个列表:todayRecipes和friendPosts。

  3. 在构建器中,你使用快照来检查Future的当前状态。

  4. 现在,Future已经完成,你可以提取数据传递给你的widget。

  5. snapshot.data 返回 ExploreData,从中提取 todayRecipes 并传递给列表视图。现在,你显示一个简单的文本作为占位符。你将很快建立一个 TodayRecipeListView。

  6. 未来仍然在加载,所以你显示一个旋转器,让用户知道正在发生的事情。

注意:更多信息请查看Flutter的FutureBuilder文档:api.flutter.dev/flutter/wid…

执行一次热重载。你会先看到加载旋钮。在未来完成后,它会显示占位符文本。

现在你已经设置好了加载UI,是时候建立实际的列表视图了

构建当天的菜谱

你要构建的第一个可滚动组件是TodayRecipeListView。这是ExploreScreen的顶部部分。它将是一个水平的列表视图。

在lib/components中,创建一个名为 today_recipe_list_view.dart的新文件。添加以下代码。

import 'package:flutter/material.dart';
// 1
import '../components/components.dart';
import '../models/models.dart';

class TodayRecipeListView extends StatelessWidget {
  // 2
  final List<ExploreRecipe> recipes;

  const TodayRecipeListView({Key key, this.recipes})
    : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 3
    return Padding(
      padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
      // 4
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 5
          Text(
            'Recipes of the Day 🍳',
            style: Theme.of(context).textTheme.headline1),
          // 6
          const SizedBox(height: 16),
          // 7
          Container(
            height: 400,
            // TODO: Add ListView Here
            color: Colors.grey,
          )
        ]
      )
    );
  }
}

以下是代码的工作方式。

  1. 导入桶状文件,component.dart和models.dart,这样你就可以使用数据模型和UI组件。
  2. TodayRecipeListView需要一个要显示的食谱列表。
  3. 在build()中,首先应用一些padding。
  4. 添加一个Column,以垂直布局的方式放置小部件。
  5. 在该列中,添加一个Text。这是 "每日食谱 "的标题。
  6. 添加一个16点高的SizedBox,以提供一些填充。
  7. 添加一个400点高的容器,并将背景颜色设为灰色。这个容器将容纳你的水平列表视图。

添加TodayRecipeListView

打开 components.dart 并添加以下导出。

export 'today_recipe_list_view.dart';

这意味着当你使用新的组件时,你不需要调用额外的导入。

接下来,打开explore_screen.dart,替换注释下面的返回语句 // TODO。将其替换为TodayRecipeListView,内容如下。

return TodayRecipeListView(recipes: recipes);

如果你的应用程序仍在运行,现在它将看起来像这样。

现在终于到了添加ListView的时候了。

添加列表视图

在 today_recipe_list_view.dart 中,将注释 // TODO: Add ListView Here 替换为以下内容。

// 1
color: Colors.transparent,
// 2
child: ListView.separated(
  // 3
  scrollDirection: Axis.horizontal,
  // 4
  itemCount: recipes.length,
  // 5
  itemBuilder: (context, index) {
    // 6
    final recipe = recipes[index];
    return buildCard(recipe);
  },
  // 7
  separatorBuilder: (context, index) {
    // 8
    return const SizedBox(width: 16);
})

请确保删除容器的现有颜色。 下面是代码的工作原理。

  1. 将颜色从灰色改为透明。
  2. 创建ListView.separate。记住,这个部件会创建两个IndexedWidgetBuilders。
  3. 设置滚动方向为水平轴。
  4. 设置列表视图中的项目数。
  5. 创建 itemBuilder 回调,它将浏览列表中的每个项目。
  6. 获取当前索引的配方并建立卡片。
  7. 创建separatorBuilder回调,它将浏览列表中的每个项目。
  8. 对于每一个项目,你都要创建一个SizedBox,将每一个项目间隔为16点。 接下来,你需要实际建立卡片。就在build()下面,添加以下内容。
Widget buildCard(ExploreRecipe recipe) {
  if (recipe.cardType == RecipeCardType.card1) {
    return Card1(recipe: recipe);
  } else if (recipe.cardType == RecipeCardType.card2) {
    return Card2(recipe: recipe);
  } else if (recipe.cardType == RecipeCardType.card3) {
    return Card3(recipe: recipe);
  } else {
    throw Exception('This card doesn\'t exist yet');
  }
}

这个函数为每个项目建立了卡片。每个ExploreRecipe都有一个cardType。这有助于你确定为该食谱创建哪种卡片。

重新启动,Fooderlich现在看起来会是这样。

最后,你可以滚动浏览当天的精美食谱列表。别忘了,你可以将main.dart中的主题切换为暗色模式

接下来,你将建立ExploreScreen的底部部分。

嵌套的列表视图

构建底部部分有两种方法:列式方法和嵌套式列表视图方法。你现在就来看看它们各自的情况。

列式方法

你可以把这两个列表视图放在一个Column中。柱子将项目安排在一个垂直的布局中,所以这很有意义,对吗?

图中显示了两个矩形的边界,代表两个可滚动的区域。

这种方法的优点和缺点是。

  • TodayRecipeListView是可以的,因为滚动是在水平方向。所有的卡片也都适合在屏幕上显示,看起来很不错
  • FriendPostListView在垂直方向上滚动,但它只有一个小的滚动区域。所以作为一个用户,你不能一次看到很多朋友的帖子。 这种方法的用户体验很差,因为内容区域太小了! 卡片已经占据了屏幕的大部分。在小设备上,垂直滚动区域会有多少空间?

嵌套式列表视图方法

在第二种方法中,你将多个列表视图嵌套在一个父列表视图中。

图中显示了一个大的矩形边界。

ExploreScreen持有父列表视图。由于只有两个子列表视图,你可以使用默认的构造函数,它返回一个明确的子列表。

这种方法的好处是。

  1. 滚动区域大了很多,使用了70-80%的屏幕。
  2. 你可以查看更多你朋友的帖子。
  3. 您可以继续在水平方向上滚动TodayRecipeListView。
  4. 当你向上滚动时,Flutter实际上是在监听父ListView的滚动事件。所以它会同时向上滚动TodayRecipeListView和FriendPostListView,给你更多的空间来查看所有的内容 嵌套式ListView听起来是个更好的方法,不是吗?

添加嵌套的列表视图

首先,打开explore_screen.dart,将build()改为以下内容。

@override
Widget build(BuildContext context) {
  // 1
  return FutureBuilder(
    // 2
    future: mockService.getExploreData(),
    // 3
    builder: (context, snapshot) {
      // 4
      if (snapshot.connectionState == ConnectionState.done) {
        // 5
        return ListView(
          // 6
          scrollDirection: Axis.vertical,
          children: [
            // 7
            TodayRecipeListView(recipes: snapshot.data.todayRecipes),
            // 8
            const SizedBox(height: 16),
            // 9
            // TODO: Replace this with FriendPostListView
            Container(height: 400, color: Colors.green)
          ]
        );
      } else {
        // 10
        return const Center(child: CircularProgressIndicator());
      }
    }
  );
}

下面是代码的工作原理。

  1. 这就是之前的FutureBuilder。它运行一个异步任务,让你知道未来的状态。
  2. 使用你的模拟服务来调用getExploreData()。这将返回一个ExploreData对象的未来。
  3. 在构建器回调中检查未来的状态。
  4. 检查未来是否已经完成。
  5. 当future完成时,返回主ListView。这持有一个明确的儿童列表。在这种情况下,主ListView将持有其他两个ListView作为子代。
  6. 设置滚动方向为垂直,尽管这是默认值。
  7. children中的第一项是TodayRecipeListView。你从ExploreData传入 todayRecipes的列表。
  8. 添加一个16点的垂直空间,这样列表之间就不会太近。
  9. 添加一个绿色的占位符容器。你将在后面创建并添加FriendPostListView。
  10. 如果未来还没有完成加载,显示一个圆形的进度指示器。 你的应用程序现在看起来像这样。

注意,你仍然可以水平滚动卡片。当你向上和向下滚动时,你会发现整个区域都在滚动

现在你有了想要的滚动行为,是时候建立FriendPostListView了。

创建FriendPostListView

首先,你将为列表视图创建要显示的项目。当这些准备好了,你将建立一个垂直的列表视图来显示它们。

下面是FriendPostTile的样子。

是时候开始了

建立FriendPostTile

在lib/components中,创建一个名为friend_post_tile.dart的新文件。添加以下代码。

import 'package:flutter/material.dart';
import '../models/models.dart';
import '../components/components.dart';

class FriendPostTile extends StatelessWidget {
  final Post post;

  const FriendPostTile({Key key, this.post}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 1
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        // 2
        CircleImage(imageProvider: AssetImage(post.profileImageUrl),
            imageRadius: 20),
        // 3
        const SizedBox(width: 16),
        // 4
        Expanded(
            // 5
            child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // 6
                  Text(post.comment),
                  // 7
                  Text('${post.timestamp} mins ago',
                      style: const TextStyle(fontWeight: FontWeight.w700))
                ]))
      ]);
  }
}

以下是代码的工作方式。

  1. 创建一个行来水平排列小部件。
  2. 第一个元素是一个圆形的头像,它显示与帖子相关的图像资产。
  3. 应用一个16点的padding。
  4. 创建Expanded,使子元素充满容器的其余部分。
  5. 建立一个柱子来垂直排列小部件。
  6. 创建一个文本来显示你朋友的评论。
  7. 创建另一个文本来显示一个帖子的时间戳。

注意:FriendPostTile上没有高度限制。这意味着只要在滚动视图中,文本就可以扩展到很多行 这就像iOS的动态表格视图和Android中的自动调整大小的TextViews。

打开 components.dart 并添加以下内容。

export 'friend_post_tile.dart';

现在,是时候创建你的垂直ListView了。

创建FriendPostListView

在lib/components中,创建一个名为friend_post_list_view.dart的新文件并添加以下代码。

import 'package:flutter/material.dart';
import '../models/models.dart';
import 'components.dart';

class FriendPostListView extends StatelessWidget {
  // 1
  final List<Post> friendPosts;

  const FriendPostListView({Key key, this.friendPosts}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 2
    return Padding(
        padding: const EdgeInsets.only(left: 16, right: 16, top: 0),
        // 3
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 4
            Text(
              'Social Chefs 👩‍🍳',
              style: Theme.of(context).textTheme.headline1),
            // 5
            const SizedBox(height: 16),
            // TODO: Add PostListView here
            // 6
            const SizedBox(height: 16),
        ]));
  }
}

下面是代码的工作原理。

  1. FriendPostListView需要一个帖子的列表。
  2. 应用一个16点的左和右的padding widget。
  3. 创建一个Column来定位Text,后面是垂直布局的帖子。
  4. 创建文本小组件的标题。
  5. 在垂直方向上应用16点的间距。
  6. 在列表的末尾留一些填充。

接下来,添加下面的代码 // TODO:在这里添加PostListView。

// 1
ListView.separated(
  // 2
  primary: false,
  // 3
  physics: const NeverScrollableScrollPhysics(),
  // 4
  shrinkWrap: true,
  scrollDirection: Axis.vertical,
  itemCount: friendPosts.length,
  itemBuilder: (context, index) {
    // 5
    final post = friendPosts[index];
    return FriendPostTile(post: post);
  },
  separatorBuilder: (context, index) {
    // 6
    return const SizedBox(height: 16);
  }),

这里是你如何定义新的ListView的。

  1. 用两个IndexWidgetBuilder回调创建ListView.separate。
  2. 由于你在嵌套两个列表视图,最好将primary设置为false。这让Flutter知道这不是主要的滚动视图。
  3. 将滚动物理学设置为NeverScrollableScrollPhysics。尽管你把primary设置为false,但禁用这个列表视图的滚动也是个好主意。这将传播到父列表视图。
  4. 设置 shrinkWrap 为 true 来创建一个固定长度的可滚动的项目列表。这给了它一个固定的高度。如果这是错误的,你会得到一个无限制的高度错误。
  5. 为列表中的每一个项目,创建一个FriendPostTile。
  6. 对于每个项目,也要创建一个SizedBox,使每个项目有16点的空间。

注意:你可以使用几种不同类型的滚动物理学。

  • 总是可滚动的物理学
  • 弹跳式滚动物理学
  • 夹持滚动物理学
  • 固定范围的滚动物理学
  • 永不滚动物理学
  • 页面滚动物理学范围
  • 保持滚动物理学 在api.flutter.dev/flutter/wid… 查找更多细节。

打开 components.dart 并添加以下导出。

export 'friend_post_list_view.dart';

就这样了。现在,你只需完成ExploreScreen,你的应用程序就会有一个很酷的新功能了!

为ExploreScreen添加最后的润色

打开explore_screen.dart,替换注释下面的代码 // TODO。用下面的FriendPostListView来代替它。

FriendPostListView(friendPosts: snapshot.data.friendPosts);

在这里,你创建了一个FriendPostListView并从ExploreData中提取friendPosts。

重新启动或热重载应用程序。最终的Explore屏幕在光照模式下应该是下面的样子。

下面是黑暗模式下的样子。

嵌套的滚动视图不是一种很好的技术吗?]

现在,是时候玩玩网格视图了。

了解GridView

GridView是一个可滚动部件的二维阵列。它将子节点排列成一个网格,并支持水平和垂直滚动。

熟悉GridView很容易。和ListView一样,它继承自ScrollView,所以它们的构造函数非常相似。

GridView有五种类型的构造函数。

  • 默认情况下,需要一个明确的部件列表。
  • GridView.builder()
  • GridView.count()
  • GridView.custom()
  • GridView.extend() builder()和count()构造函数是最常见的。由于ListView使用了类似的构造函数,所以你不会有任何问题。

关键参数

以下是你应该注意的一些参数。

  • crossAxisSpacing:十字轴中每个孩子之间的间距。
  • mainAxisSpacing:主轴上每个孩子之间的间距。
  • crossAxisCount:十字轴中子节点的数量。你也可以把它看成是你在网格中想要的列数。
  • shrinkWrap:控制固定滚动区域的大小。
  • physics:控制滚动视图对用户输入的响应方式。
  • primary:帮助Flutter确定哪个滚动视图是主要的。
  • scrollDirection(滚动方向):控制视图的滚动轴。

注意 GridView有大量的参数可供实验和使用。请看Greg Perry的文章来了解更多:medium.com/@greg.perry…

了解the cross and main axis?

主轴和横轴的区别是什么?记住,列和行就像ListViews,但没有滚动视图。

主轴总是与滚动方向相对应的!

如果你的滚动方向是水平的,你可以把它看成是一个行。主轴代表水平方向,如下图所示。

如果你的滚动方向是垂直的,你可以把它看作是一个列。主轴代表垂直方向,如下图所示。

Grid delegates

Grid delegates帮助确定GridView中的子节点的间距和列数。

除了定制你自己的网格委托外,Flutter还提供了两个委托,你可以开箱即用。

  • SliverGridDelegateWithFixedCrossAxisCount
  • SliverGridDelegateWithMaxCrossAxisExtent 第一种创建一个布局,沿横轴有固定数量的瓷砖。第二种是创建一个具有最大横轴范围的瓷砖的布局。

构建菜谱屏幕

现在你已经准备好建立菜谱屏幕了! 在屏幕目录下,创建一个名为recipes_screen.dart的新文件。添加以下代码。

import 'package:flutter/material.dart';
import '../api/mock_fooderlich_service.dart';
import '../components/components.dart';

class RecipesScreen extends StatelessWidget {
  // 1
  final exploreService = MockFooderlichService();

  RecipesScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 2
    return FutureBuilder(
        // 3
        future: exploreService.getRecipes(),
        builder: (context, snapshot) {
          // 4
          if (snapshot.connectionState == ConnectionState.done) {
            // TODO: Add RecipesGridView Here
            // 5
            return const Center(child: Text('Recipes Screen'));
          } else {
            // 6
            return const Center(child: CircularProgressIndicator());
          }
        });
  }
}

该代码的设置与ExploreScreen类似。为了创建它,你

  1. 创建一个模拟服务。
  2. 创建一个FutureBuilder。
  3. 使用getRecipes()来返回要显示的食谱列表。这个函数返回一个 SimpleRecipes 的未来列表。
  4. 检查未来是否完整。
  5. 添加一个占位符文本,直到构建RecipesGridView。
  6. 如果未来还没有完成,显示一个循环加载指示器。 在home.dart中,将页面替换为。
static List<Widget> pages = <Widget>[
  ExploreScreen(),
  RecipesScreen(),
  Container(color: Colors.blue)
];

接下来,添加以下导入。

import 'screen/recipes_screen.dart';

执行热重启或重建,并运行应用程序以看到新的食谱屏幕的开始。

创建菜谱缩略图

在你创建网格视图之前,你需要一个小部件来显示在网格中。这是你要创建的缩略图小部件。

它是一个简单的瓦片,显示食谱的图片、名称和时间。

在lib/components中,创建一个名为recipe_thumbnail.dart的新文件并添加以下代码。

import 'package:flutter/material.dart';
import '../models/models.dart';

class RecipeThumbnail extends StatelessWidget {
  // 1
  final SimpleRecipe recipe;

  const RecipeThumbnail({Key key, this.recipe}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 2
    return Container(
      padding: const EdgeInsets.all(8),
      // 3
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 4
          Expanded(
                  // 5
                  child: ClipRRect(
                      child: Image.asset('${recipe.dishImage}',
                          fit: BoxFit.cover),
                      borderRadius: BorderRadius.circular(12))),
          // 6
          const SizedBox(height: 10),
          // 7
          Text(
              recipe.title,
              maxLines: 1,
              style: Theme.of(context).textTheme.bodyText1),
          Text(
              recipe.duration,
              style: Theme.of(context).textTheme.bodyText1)
        ]
      )
    );
  }
}

下面是代码的工作原理。

  1. 这个类需要一个SimpleRecipe作为参数。这有助于配置你的小部件。
  2. 创建一个周围有8点填充的容器。
  3. 使用一个列来应用一个垂直布局。
  4. 该列的第一个元素是Expanded。该小组件连接到一个容器,然后容器连接到你的图像。你希望图像能填满剩余的空间。
  5. 图像在ClipRRect内,它将图像夹住,使边框变圆。
  6. 在图像和其他部件之间添加一些空间。
  7. 添加其余的文本:一个用于显示食谱的标题,另一个用于显示持续时间。

接下来,打开 components.dart 并添加以下导出。

export 'recipe_thumbnail.dart';

现在,你已经准备好创建你的网格视图了!

创建RecipesGridView

在lib/components中,创建一个名为recipes_grid_view.dart的新文件并添加以下代码。

import 'package:flutter/material.dart';
import '../components/components.dart';
import '../models/models.dart';

class RecipesGridView extends StatelessWidget {
  // 1
  final List<SimpleRecipe> recipes;

  const RecipesGridView({Key key, this.recipes}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 2
    return Padding(
        padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
        // 3
        child: GridView.builder(
            // 4
            itemCount: recipes.length,
            // 5
            gridDelegate:
                const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2),
            itemBuilder: (context, index) {
              // 6
              final simpleRecipe = recipes[index];
              return RecipeThumbnail(recipe: simpleRecipe);
            }));
  }
}

GridView类似于ListView。下面是它的工作原理。

  1. RecipesGridView需要在网格中显示一个食谱列表。
  2. 在左边、右边和上面应用一个16点的padding。
  3. 创建一个GridView.builder,它只显示屏幕上可见的项目。
  4. 告诉网格视图将有多少个项目在网格中。
  5. 添加SliverGridDelegateWithFixedCrossAxisCount并将crossAxisCount设置为2,这意味着将只有两列。
  6. 对于每个索引,获取配方并创建一个相应的RecipeThumbnail。 打开 components.dart 并添加以下导出。
export 'recipes_grid_view.dart';

添加RecipesGridView

打开recipes_screen.dart,将注释下面的返回语句//TODO:在这里添加RecipesGridView,内容如下。

return RecipesGridView(recipes: snapshot.data);

当食谱列表被加载后,这将以网格布局的方式显示它们。

恭喜你,你现在已经设置好了你的RecipesScreen!

如果你的应用程序仍在运行,请执行热重载。新的屏幕将看起来像这样。

现在有两个未使用的导入语句。打开home.dart并删除以下内容。

import 'models/explore_recipe.dart';
import 'components/components.dart';

就这样,你完成了。祝贺你!

其他可滚动的部件

还有更多的可滚动部件用于不同的使用情况。下面是一些本章没有涉及的。

  • PageView。一个可滚动的小组件,可以逐页滚动,使其成为入职流程的完美选择。它还支持垂直滚动方向。

  • CustomScrollView。一个小部件,可以使用分片创建自定义的滚动效果。有没有想过如何在滚动时折叠你的导航标题?分流器和自定义滚动视图可以做到这一点

  • StaggeredGridView。一个网格视图包,支持不同大小的列和行。如果你需要支持动态高度和自定义布局,这是最受欢迎的包。

现在,是时候进行一些挑战了。

挑战

挑战1:添加一个滚动监听器

到目前为止,你已经建立了一些可滚动的小部件,但你如何监听滚动事件?

在这个挑战中,试着给ExploreScreen添加一个滚动控制器。在控制台中打印两个语句。

  1. print('I am at the bottom!') 如果用户滚动到底部。
  2. print('I am at the top!') 如果用户滚动到顶部。

你可以在这里查看滚动控制器的API文档:api.flutter.dev/flutter/wid…

这里有一个步骤的提示。

  • 让ExploreScreen成为一个有状态的widget。
  • 在initState()中创建一个ScrollController的实例。
  • 创建rollListener()来监听滚动的位置。
  • 给滚动控制器添加一个滚动监听器。
  • 将滚动控制器添加到ListView中。
  • 处理你的滚动Listener()。

解决方案

见附录A。

挑战2:添加一个新的GridView布局

尝试使用SliverGridDelegateWithMaxCrossAxisExtent来创建下面的网格布局,它只在一列中显示食谱。

解决方案

见附录B。

关键点

  • ListView和GridView同时支持水平和垂直滚动方向。
  • primary属性让Flutter知道哪个滚动视图是主滚动视图。
  • 在一个滚动视图中的物理学让您改变用户的滚动交互。
  • 特别是在一个嵌套的列表视图中,记得将shrinkWrap设置为true,这样你就可以为列表中的所有项目赋予滚动视图一个固定高度。
  • 使用FutureBuilder来等待一个异步任务的完成。
  • 你可以嵌套可滚动的小部件。例如,你可以在一个列表视图中放置一个网格视图。释放你最疯狂的想象力吧
  • 使用ScrollController和ScrollNotification来控制或倾听滚动行为。
  • 桶状文件可以方便地将导入的内容组合在一起。它们还可以让你用一个文件导入许多部件。

接下来该怎么做?

在这一点上,你已经学会了如何创建ListViews和GridViews。它们比iOS的UITableView和Android的RecyclerView更容易使用,对吗?构建可滚动的小部件是你应该掌握的一项重要技能!

Flutter使构建和使用这种可滚动的部件变得容易。它提供了向任何方向滚动的灵活性和对可滚动部件进行嵌套的能力。利用你所学到的技能,你可以建立很酷的滚动互动。

你已经准备好在你的朋友面前表现得像个专家了 :] 。

要想获得更多的例子,请查看Flutter画廊,网址是gallery.flutter.dev/#/,其中展示了一些可…

在下一章中,你将会看到一些更多的互动小部件。


www.deepl.com 翻译