本文由简悦 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'));
}
}
以下是代码的运作方式。
- 创建一个MockFooderlichService,来模拟服务器的响应。
- 显示一个占位符文本。稍后你会替换这个。
设置底部导航栏
打开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());
}
});
以下是代码的内容:
-
在widget的build()中,你创建了一个FutureBuilder。
-
getExploreData()创建了一个未来,反过来,它将返回一个ExploreData实例。该实例将包含两个列表:todayRecipes和friendPosts。
-
在构建器中,你使用快照来检查Future的当前状态。
-
现在,Future已经完成,你可以提取数据传递给你的widget。
-
snapshot.data 返回 ExploreData,从中提取 todayRecipes 并传递给列表视图。现在,你显示一个简单的文本作为占位符。你将很快建立一个 TodayRecipeListView。
-
未来仍然在加载,所以你显示一个旋转器,让用户知道正在发生的事情。
注意:更多信息请查看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,
)
]
)
);
}
}
以下是代码的工作方式。
- 导入桶状文件,component.dart和models.dart,这样你就可以使用数据模型和UI组件。
- TodayRecipeListView需要一个要显示的食谱列表。
- 在build()中,首先应用一些padding。
- 添加一个Column,以垂直布局的方式放置小部件。
- 在该列中,添加一个Text。这是 "每日食谱 "的标题。
- 添加一个16点高的SizedBox,以提供一些填充。
- 添加一个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);
})
请确保删除容器的现有颜色。 下面是代码的工作原理。
- 将颜色从灰色改为透明。
- 创建ListView.separate。记住,这个部件会创建两个IndexedWidgetBuilders。
- 设置滚动方向为水平轴。
- 设置列表视图中的项目数。
- 创建 itemBuilder 回调,它将浏览列表中的每个项目。
- 获取当前索引的配方并建立卡片。
- 创建separatorBuilder回调,它将浏览列表中的每个项目。
- 对于每一个项目,你都要创建一个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持有父列表视图。由于只有两个子列表视图,你可以使用默认的构造函数,它返回一个明确的子列表。
这种方法的好处是。
- 滚动区域大了很多,使用了70-80%的屏幕。
- 你可以查看更多你朋友的帖子。
- 您可以继续在水平方向上滚动TodayRecipeListView。
- 当你向上滚动时,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());
}
}
);
}
下面是代码的工作原理。
- 这就是之前的FutureBuilder。它运行一个异步任务,让你知道未来的状态。
- 使用你的模拟服务来调用getExploreData()。这将返回一个ExploreData对象的未来。
- 在构建器回调中检查未来的状态。
- 检查未来是否已经完成。
- 当future完成时,返回主ListView。这持有一个明确的儿童列表。在这种情况下,主ListView将持有其他两个ListView作为子代。
- 设置滚动方向为垂直,尽管这是默认值。
- children中的第一项是TodayRecipeListView。你从ExploreData传入 todayRecipes的列表。
- 添加一个16点的垂直空间,这样列表之间就不会太近。
- 添加一个绿色的占位符容器。你将在后面创建并添加FriendPostListView。
- 如果未来还没有完成加载,显示一个圆形的进度指示器。
你的应用程序现在看起来像这样。
注意,你仍然可以水平滚动卡片。当你向上和向下滚动时,你会发现整个区域都在滚动
现在你有了想要的滚动行为,是时候建立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))
]))
]);
}
}
以下是代码的工作方式。
- 创建一个行来水平排列小部件。
- 第一个元素是一个圆形的头像,它显示与帖子相关的图像资产。
- 应用一个16点的padding。
- 创建Expanded,使子元素充满容器的其余部分。
- 建立一个柱子来垂直排列小部件。
- 创建一个文本来显示你朋友的评论。
- 创建另一个文本来显示一个帖子的时间戳。
注意: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),
]));
}
}
下面是代码的工作原理。
- FriendPostListView需要一个帖子的列表。
- 应用一个16点的左和右的padding widget。
- 创建一个Column来定位Text,后面是垂直布局的帖子。
- 创建文本小组件的标题。
- 在垂直方向上应用16点的间距。
- 在列表的末尾留一些填充。
接下来,添加下面的代码 // 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的。
- 用两个IndexWidgetBuilder回调创建ListView.separate。
- 由于你在嵌套两个列表视图,最好将primary设置为false。这让Flutter知道这不是主要的滚动视图。
- 将滚动物理学设置为NeverScrollableScrollPhysics。尽管你把primary设置为false,但禁用这个列表视图的滚动也是个好主意。这将传播到父列表视图。
- 设置 shrinkWrap 为 true 来创建一个固定长度的可滚动的项目列表。这给了它一个固定的高度。如果这是错误的,你会得到一个无限制的高度错误。
- 为列表中的每一个项目,创建一个FriendPostTile。
- 对于每个项目,也要创建一个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类似。为了创建它,你
- 创建一个模拟服务。
- 创建一个FutureBuilder。
- 使用getRecipes()来返回要显示的食谱列表。这个函数返回一个 SimpleRecipes 的未来列表。
- 检查未来是否完整。
- 添加一个占位符文本,直到构建RecipesGridView。
- 如果未来还没有完成,显示一个循环加载指示器。 在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)
]
)
);
}
}
下面是代码的工作原理。
- 这个类需要一个SimpleRecipe作为参数。这有助于配置你的小部件。
- 创建一个周围有8点填充的容器。
- 使用一个列来应用一个垂直布局。
- 该列的第一个元素是Expanded。该小组件连接到一个容器,然后容器连接到你的图像。你希望图像能填满剩余的空间。
- 图像在ClipRRect内,它将图像夹住,使边框变圆。
- 在图像和其他部件之间添加一些空间。
- 添加其余的文本:一个用于显示食谱的标题,另一个用于显示持续时间。
接下来,打开 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。下面是它的工作原理。
- RecipesGridView需要在网格中显示一个食谱列表。
- 在左边、右边和上面应用一个16点的padding。
- 创建一个GridView.builder,它只显示屏幕上可见的项目。
- 告诉网格视图将有多少个项目在网格中。
- 添加SliverGridDelegateWithFixedCrossAxisCount并将crossAxisCount设置为2,这意味着将只有两列。
- 对于每个索引,获取配方并创建一个相应的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添加一个滚动控制器。在控制台中打印两个语句。
- print('I am at the bottom!') 如果用户滚动到底部。
- 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/#/,其中展示了一些可…
在下一章中,你将会看到一些更多的互动小部件。