在移动应用开发中,ListView和GridView是非常流行的控件。如果不相信,打开手机中的任何电子商务应用,肯定会发现它们依赖于 ListView 或 GridView 来展示数据。例如,亚马逊移动应用以网格形式展示数据,而印度流行的在线钱包服务应用 PayTM 也广泛使用网格布局来展示不同的产品。
本文的目标是实现一个类似于下面图片的视图:但是,如果注意到上面的图片,它是在横屏模式下。所以这篇文章中将要做的是,当应用处于竖屏模式时,MobileApp 会以 ListView 的形式展示项目,而当它处于横屏模式时,以每行3个项目的网格形式展示。还将探索通过将 gridview 实现在一个单独的类中来创建自定义控件。
将在前一篇文章 "FlutterGetting Started: Tutorial 4ListView" 的基础上构建,在那里已经创建了一个基于 ListView 的应用程序,这里是初始的项目结构和初始 UI。
以下是开始构建的初始代码:
class HomePage extends StatelessWidget {
final List _allCities = City.allCities();
HomePage() {}
final GlobalKey scaffoldKey = new GlobalKey();
@override
Widget build(BuildContext context) {
return new Scaffold(
key: scaffoldKey,
appBar: new AppBar(
title: new Text("Cites around world", style: new TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold, color: Colors.black87)),
),
body: new Padding(
padding: EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),
child: getHomePageBody(context)
)
);
}
getHomePageBody(BuildContext context) {
return ListView.builder(
itemCount: _allCities.length,
itemBuilder: _getListItemUI,
padding: EdgeInsets.all(0.0),
);
}
Widget _getListItemUI(BuildContext context, int index, {double imgwidth: 100.0}) {
return new Card(
child: new Column(
children: [
new ListTile(
leading: new Image.asset("assets/" + _allCities[index].image, fit: BoxFit.fitHeight, width: imgwidth),
title: new Text(_allCities[index].name, style: new TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold)),
subtitle: new Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
new Text(_allCities[index].country, style: new TextStyle(fontSize: 13.0, fontWeight: FontWeight.normal)),
new Text('Population: ${_allCities[index].population}', style: new TextStyle(fontSize: 11.0, fontWeight: FontWeight.normal)),
]),
onTap: () {
_showSnackBar(context, _allCities[index]);
},
)
],
)
);
}
_showSnackBar(BuildContext context, City item) {
final SnackBar objSnackbar = new SnackBar(
content: new Text("${item.name} is a city in ${item.country}"),
backgroundColor: Colors.amber,
);
Scaffold.of(context).showSnackBar(objSnackbar);
}
}
在上面的代码中,创建了一个简单的 ListView,使用 ListView.builder,它提供了创建无限列表项视图的灵活性,因为它只调用那些可能显示在屏幕上的项目。展示了城市信息,如城市地标图片,随后是城市名称、所属国家和人口。最后,在点击时,它会在屏幕底部显示一个小型的自动消失的消息,称为 SnackBar。
现在,工作开始了,正如之前提到的,将重构一个新的控件到不同的类中,以保持代码模块化并提高代码的清晰度。所以,在 lib 文件夹下创建一个新的文件夹 widget,并添加一个新的 DART 文件 mygridview.dart。
一旦添加了文件,首先通过导入 'package:flutter/material.dart' 导入材料控件,然后添加 MyGridView类,它扩展了最喜欢的 StatelessWidget并覆盖了 Build 函数,所以基本代码看起来像这样:
import 'package:flutter/material.dart';
import 'package:flutter5_gridlist/model/city.dart';
class MyGridView extends StatelessWidget {
final List allCities;
MyGridView({Key key, this.allCities}) : super(key: key);
@override
Widget build(BuildContext context) {
return null;
}
}
将添加一个基本的 GridView,只显示城市名称,所以将在覆盖的 Build 函数中添加以下代码:
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 3,
padding: EdgeInsets.all(16.0),
childAspectRatio: 8.0,
children: _getGridViewItems(context),
);
}
_getGridViewItems(BuildContext context){
List allWidgets = new List();
for (int i = 0; i < allCities.length; i++) {
var widget = new Text(allCities[i].name);
allWidgets.add(widget);
};
return allWidgets;
}
GridView.count 方法将为应用程序提供 GridView 控件。crossAxisCount 属性用于让移动应用程序知道想要在每一行显示多少个项目。children 属性将包含所有希望在页面加载时显示的控件。childAspectRatio 是交叉轴到主轴长度的比率,因为正在显示名称,所以将保持在 8.0,以便减少两个瓦片之间的边距。
现在让使 UI 与在 ListView 中看到的类似。在这里,创建了一个新的函数,它将以 Card 的形式发送 City 类。
// Create individual item
_getGridItemUI(BuildContext context, City item) {
return new InkWell(
onTap: () {
_showSnackBar(context, item);
},
child: new Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
new Image.asset(
"assets/" + item.image,
fit: BoxFit.fill,
),
new Expanded(
child: new Center(
child: new Column(
children: [
new SizedBox(height: 8.0),
new Text(
item.name,
style: new TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
),
),
new Text(item.country),
new Text('Population: ${item.population}')
],
))
)
],
),
elevation: 2.0,
margin: EdgeInsets.all(5.0),
));
}
使用了 InkWell 类,因为 Card 类不支持直接的手势,所以将其包装在 InkWell 类中,以利用其 onTap 事件来显示 SnackBar。其余的代码与 ListView 中的卡片类似,只是没有指定宽度。
另外,由于正在显示一个完整的卡片,不要忘记将 childAspectRatio 从 8.0 改为 8.0/9.0,因为需要更多的高度。
在文章开始时,提到将在竖屏方向显示ListView,在横屏方向显示 GridView,所以为了实现这一点,将使用 MediaQuery 类来识别方向。每当方向改变时,即倾斜手机,控件将被重新绘制,所以 Build 函数被调用,可以在那里决定应该调用什么代码。所以在 homepage.dart 类中,将使用以下函数来处理方向变化。
getHomePageBody(BuildContext context) {
if (MediaQuery.of(context).orientation == Orientation.portrait)
return ListView.builder(
itemCount: _allCities.length,
itemBuilder: _getListItemUI,
padding: EdgeInsets.all(0.0),
);
else
return new MyGridView(allCities: _allCities);
}