Github项目地址:项目地址
上一篇博客讲解了该应用的基础结构,如底部导航栏等。基础结构篇
这篇博客将详细讲解美团首页的界面实现,在下一篇博客实现推荐卡片无限加载,带插入和移除动画的列表,弹出菜单等细节。
效果图:
首先对首页进行拆解,将较为复杂的界面切成一个个小部件方便理解:
1. AppBar
2. 三行由图片和标题组成的按纽栏
3. 定时滚动的轮播图
4. 推荐卡片
///主界面AppBar
AppBar _buildHomeAppBar() { //构建一个美团首页AppBar
return AppBar(
automaticallyImplyLeading: false, //不自动显示一个返回按钮
elevation: 0.0, //关闭阴影高度
backgroundColor: Colors.white, //背景颜色设为白色
flexibleSpace: SafeArea(
////适配刘海,自动避开刘海,IOS底部导航的非显示区域
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ClipOval( //以圆形截取子控件
child: Image.asset("images/protrait.png",
width: 35.0, height: 35.0),
),
),
Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
"三河",
style: TextStyle(color: Colors.black, fontSize: 15.0),
),
Icon(
Icons.keyboard_arrow_down,
size: 15.0,
),
],
),
Text(
"晴 20°",
style: TextStyle(fontSize: 10.0),
)
],
),
padding: const EdgeInsets.all(8.0),
),
Expanded( //占满剩余控件
child: GestureDetector(
onTap: () { //以IOS风格动画导航到搜索界面
Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => SearchPage()
));
},
child: Container(
height: 45.0,
child: Card(
elevation: 0.0,
shape: RoundedRectangleBorder( //设置形状为弧度10的圆角矩形
borderRadius: BorderRadius.all(Radius.circular(10.0))),
color: Colors.grey[200],
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.search,
color: Colors.black87,
size: 20.0,
),
Text(
"自助烤肉",
style: TextStyle(fontSize: 15.0, color: Colors.black87),
),
],
),
),
),
),
),
IconButton(
iconSize: 30,
splashColor: Colors.transparent, //水波纹特效颜色为透明
highlightColor: Colors.transparent,
padding: EdgeInsets.zero, //设置各方向外边距为0
icon: Icon(
Icons.add,
color: Colors.black,
),
onPressed: () {
Navigator.of(context)
.push(CupertinoPageRoute(builder: (context) {
return TextPage();
}));
},
)
],
),
),
);
}
为了演示我们先将显示的文本都写死,实际开发时从服务器端获取数据后加载进去即可。
先看看AppBar的构造函数
AppBar({
Key key, //同于在一个容器里出现多个同类对象时区分彼此的标记,大部分时候用不上
this.leading, //最左侧区域,通常放置返回按钮
this.automaticallyImplyLeading = true, //通过该属性控制是否自动添加返回按钮
this.title, //标题,安卓平台显示在左侧,IOS平台显示在中间
this.actions, //最右侧区域,通常用于放置菜单按钮等
this.flexibleSpace, //可伸缩的空闲区域
this.bottom, //底部区域,通常用于放置顶部导航栏
this.elevation, //阴影高度
this.shape, //形状
this.backgroundColor, //背景色
this.brightness,
this.iconTheme, //按钮主题
this.actionsIconTheme,
this.textTheme,
this.primary = true,
this.centerTitle, //是否将标题显示在中间
this.titleSpacing = NavigationToolbar.kMiddleSpacing,
this.toolbarOpacity = 1.0,
this.bottomOpacity = 1.0,
})
在这里我们直接将一个Row控件放置在flexibleSpace区域,实现更为灵活的布局。Row控件的作用是将children属性里的多个控件按水平排列,通过mainAxisAlignment 和 crossAxisAlignment 属性控制子控件在主轴和横轴方向上的对齐方式。与其相似的是Column控件(将子控件按垂直方向排列)。Row控件的主轴对应的是水平方向,横轴是垂直方向,Column控件与其相反。而将Row控件放置在flexibleSpace区域的原因是当leading, title, actions 这三个属性的任何一个不为空时,另外两个属性即使赋空值也会占用一定的空间。接下来我们逐个实现Row中的子控件。
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ClipOval(
child: Image.asset("images/protrait.png",
width: 35.0, height: 35.0),
),
),
Padding控件指定子控件在上下左右方向上的外边距,padding属性接收一个EdgeInsets对象。该对象有三种模式(这里将命名构造函数理解为构造该对象的一种模式),all, symmetric, only 。all模式接收一个double类型的参数,表示四个方向上的外边距都为这个值,symmetric模式控制水平方向和垂直方向,only指定单个方向。如
EdgeInsets.only(bottom: 12, top: 10, left: 3)
ClipOval控件使子控件显示在一个圆心区域,Image控件用于显示图片。有四种模式,assets用于从本地资源加载图片,network用于从网络加载图片,memory用于从内存加载图片,file用于从文件路径加载图片。从assets加载图片需要先在pubspec.yaml文件中声明依赖。
assets:
- images/test.png //加载本地目录的images文件夹下的test.png
- images/protrait.png
- images/title/ //加载title文件夹下的所有资源(不包括其子文件夹)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
"三河",
style: TextStyle(color: Colors.black, fontSize: 15.0),
),
Icon(
Icons.keyboard_arrow_down,
size: 15.0,
),
],
),
Text(
"晴 20°",
style: TextStyle(fontSize: 10.0),
)
],
),
这个标题由一个Column组成,第一个子控件是一个水平排列的文字和图标,第二个子控件是一个文字。Text控件的style属性接收一个TextStyle对象用于设置文字大小,颜色,加粗等。Icons是Material包里的一个图标工具类,提供了许多 Material Design 图标。
Expanded(
child: GestureDetector(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => SearchPage()
));
},
child: Container(
height: 45.0,
child: Card(
elevation: 0.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0))),
color: Colors.grey[200],
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.search,
color: Colors.black87,
size: 20.0,
),
Text(
"自助烤肉",
style: TextStyle(fontSize: 15.0, color: Colors.black87),
),
],
),
),
),
),
),
由于这个搜索框只是作为一个按钮来弹出搜索界面,所以只需绘制出一个搜索框的样式出来即可。通过Expaned控件可以使子控件填满父容器的剩余空间。GestureDetector可以捕获多种手势事件,这里我们只需要点击事件。用Container控件限制高度,Card控件绘制圆角边框及背景色,用Row控件在Card内部显示一个搜索图标和一个文字控件。
在Flutter中,页面跳转通过 Navigator.of(context).push() 方法实现,该方法接收一个抽象类Route的对象。Route类的实现类主要有 MaterialPageRoute 和 CupertinoPageRoute 。MaterialPageRoute在安卓平台的跳转动画为从下方进入,在IOS平台的跳转动画为从左侧进入。CupertinoPageRoute的跳转动画都为从左侧进入。
IconButton(
iconSize: 30,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
padding: EdgeInsets.zero,
icon: Icon(
Icons.add,
color: Colors.black,
),
onPressed: () {
Navigator.of(context)
.push(CupertinoPageRoute(builder: (context) {
return TextPage();
}));
},
)
Flutter中可以通过GestureDetector使子控件可以响应多种手势,也有大量现成的Button类,常用的有 FlatButton,IconButton, RaisedButton等Material包下的Button,Cupertino包下的CupertinoButton。Material包中提供了许多符合谷歌 Material Design 的控件,而Cupertino包提供了许多IOS风格的控件,如之前用到的CupertinoTabBar等。Material包下的Button大多都提供了点击时的水波纹特效,splashColor属性便是控制水波纹特效颜色的,highlightColor属性控制被点击时按钮的颜色。
至此美团首页的AppBar就搞定喽,关于弹出菜单,选择地点界面,搜索界面在后面的文章中解析。
可以看到在首页界面中有三排由图片和文字组成的按钮栏,考虑到重用的问题我们先新建一个MyImageButton类继承自StatelessWidget。
class MyImageButton extends StatelessWidget {
MyImageButton({@required this.image, @required this.title, this.width, this.tip});
final Widget image;
final String title;
final double width;
final String tip;
@override
Widget build(BuildContext context) {
return Container(
width: width,
child: Stack(
children: <Widget>[
Center(
child: Column(
children: <Widget>[
SizedBox(height: 12,),
image,
SizedBox(
height: 5.0,
),
Text(
title,
style: TextStyle(fontSize: 12.0),
)
],
),
),
Align(
alignment: Alignment.topRight,
child: tip != null ? MyTag(tag: tip, isEmphasize: true, radius: 15.0,) : null,
),
],
),
);
}
}
当控件在其生命周期中无需改变显示内容或无需在销毁时释放资源就选择继承StatelessWidget。
在Flutter中推荐在构造函数的参数列表中使用大括号包裹参数,使其成为可选参数,在必须的参数前面用@required标记。同时Flutter也支持类似于Java的构造函数形式。
MyImageButton({@required this.image, @required this.title, this.width, this.tip});
MyImageButton(this.image, this.title, this.width, this.tip);
MyImageButton(this.image, this.title, {this.width, this.tip});
MyImageButton(Widget image, String title, doublewidth, String tip){
this.image = image;
...
}
StatelessWidget要求其属性全都为 final ,该类只有一个 build 方法需要覆盖,在 build 方法中返回一个Widget作为该类实例化时显示的内容。StatefulWidget的build方法会在其生命周期中多次被调用,而StatelessWidget的build方法只会在初次插入到Widget树时,父控件发生变化时,当InheritedWidget所依赖的数据发生变化时这三种情况下调用。InheritedWidget用于实现底层Widget访问高层Widget中的数据,通常用在主题类及状态管理框架中。
在MyImageButton所用到的大部分控件前面都已讲解过了,这里就不再赘述了。Stack控件也是Flutter里为数不多的具有多个子控件的控件(前面提到过的Row,Column,以及常用的ListView等)。Stack控件的效果是使子控件层叠排列,children属性中的Widget列表中越靠前的显示在越底层,通常配合Align控件使用。Align控件用于控制子控件在父容器中的对齐方式。SizedBox用于限定子控件的大小,也可以用于占据一片空白区域(因为其child属性是可选的)。
使用Column控件将构造函数接收到的image和tile显示成一列,并且使用SizedBox在顶部和中间留白。外层嵌套的Stack控件是用于实现图片右上角的红色标签效果
class MyTag extends StatelessWidget {
MyTag({@required this.tag, this.isEmphasize = false, this.radius = 3.0});
final String tag;
final bool isEmphasize;
final double radius;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: isEmphasize ? Colors.red : Colors.black, width: 0.5),
color: isEmphasize ? Colors.red : Colors.white,
borderRadius: BorderRadius.circular(radius),
),
child: Padding(
padding: const EdgeInsets.only(left: 3.0, right: 3.0, bottom: 1.0),
child: Text(
tag,
style: TextStyle(
fontSize: 10.0, color: isEmphasize ? Colors.white : Colors.black),
),
),
);
}
}
这个标签控件用于实现标题栏右上角的红色标签与推荐卡片中的白色标签。Container除了之前介绍的控制高度,宽度,背景色,内外边距等作用外还有一个特别常用的属性便是decoration,该属性接受一个BoxDecoration对象,通过该对象可以控制边框和形状等属性。由于Container中的color属性是通过BoxDecoration实现的,所以当decoration的值不为空时背景色只能通过BoxDecoration中的color属性控制。
通过border属性控制颜色和边框宽度,borderRadius属性控制圆角的弧度值便可绘制出圆角边框。
最后将MyImageButton控件使用Row控件包裹起来便可实现标题栏。
import 'dart:async';
import 'package:flutter/material.dart';
class SlidesShowWidget extends StatefulWidget {
SlidesShowWidget({this.height = 100});
final double height;
@override
_SlidesShowWidgetState createState() => _SlidesShowWidgetState();
}
class _SlidesShowWidgetState extends State<SlidesShowWidget>
with SingleTickerProviderStateMixin {
PageController _pageController = PageController();
TabController _tabController;
Timer _timer;
int _index = 0;
@override
void initState() {
_timer = Timer.periodic(Duration(seconds: 2), _handleTimeout);
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
@override
void dispose() {
_tabController?.dispose();
_pageController?.dispose();
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
Widget _buildImage(String imageUrl) {
return Card(
color: Colors.transparent,
elevation: 0.0,
child: ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: Image.network(
imageUrl,
fit: BoxFit.fitWidth,
),
),
);
}
return Container(
height: widget.height,
child: Stack(
children: <Widget>[
PageView(
onPageChanged: _handlePageChanged,
controller: _pageController,
children: <Widget>[
_buildImage("http://5b0988e595225.cdn.sohucs.com/images/20171105/2e36a4b9c5764a5cb1b6a7ee84f85146.jpeg"),
_buildImage("https://b-ssl.duitang.com/uploads/item/201602/17/20160217155320_FUCuw.thumb.700_0.jpeg"),
_buildImage("http://img3.duitang.com/uploads/item/201505/27/20150527174204_aThSR.jpeg"),
_buildImage("http://5b0988e595225.cdn.sohucs.com/images/20171105/2e36a4b9c5764a5cb1b6a7ee84f85146.jpeg"),
_buildImage("https://b-ssl.duitang.com/uploads/item/201602/17/20160217155320_FUCuw.thumb.700_0.jpeg"),
],
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Opacity(
opacity: 0.5,
child: TabPageSelector(
indicatorSize: 10.0,
color: Colors.white,
selectedColor: Colors.blue,
controller: _tabController,
),
),
),
)
],
),
);
}
void _handleTimeout(Timer timer) {
_index++;
_pageController.animateToPage(_index % 3,
duration: Duration(microseconds: 16), curve: Curves.fastOutSlowIn);
_tabController.animateTo(_index % 3);
}
void _handlePageChanged(int value) {
_index = value;
if (value == 0) {
_tabController.animateTo(_tabController.length - 1);
_pageController.jumpToPage(5 - 2);
} else if (value == 5 - 1) {
_tabController.animateTo(0);
_pageController.jumpToPage(1);
} else {
_tabController.animateTo(value - 1);
}
}
}
先简单介绍一下StatefulWidget,通常我们会覆盖的方法只有 intiState, dispose, build 三个方法。initState方法在控件首次插入到控件树时调用,可以在里面初始化一些数据,dispose方法在控件被销毁时调用,通常在该方法内部释放资源。build方法在初次插入控件树时,setState方法调用时被调用(还有四种情况在此不在赘述,StatefulWidget控件的完整生命周期请参考官方文档)。大部分的控件此前都已经介绍过了,这里只简单介绍一下几个关键点。
将与界面相关联的数据存放在State类的属性中,当需要改变界面时调用setState方法,在setState方法内部改变对应属性的值,build方法会自动被调用,此时显示的界面也就随之改变了。
ClipRRect控件用于使子控件呈现为圆角,Opacity控件用于调整子控件透明度。
_tabController?.dispose(); 的意思是在_tabController对象不为空时调用该对象的dispose()方法,通常用在网络请求等异步操作时确保不会触发空对象异常。
PageView控件内置了水平或垂直滚动操作,具体参考https://api.flutter.dev/flutter/widgets/PageView-class.html
PageController用来控制与其绑定的PageView当前显示的Page,TabController用来控制与其绑定的TabPageSelector当前高亮的指示器。
dart:async包里面存放着与异步相关的类,其中的Timer类可以帮助我们完成定时滚动。_timer = Timer.periodic(Duration(seconds: 2), _handleTimeout); 意思是每两秒中调用一次_handleTimeout函数。
通过在滚动列表的最前面添加一个与滚动列表最后一个控件相同的控件,在滚动列表的最后面添加一个与滚动列表第一个相同的控件实现无限滚动的列表。如 [蓝,红, 绿,蓝,红],当滚动到最后一个红时跳转到第一个红,当滚动到第一个蓝时跳转到第二个蓝处。
void _handlePageChanged(int value) {
_index = value;
if (value == 0) {
_tabController.animateTo(_tabController.length - 1);
_pageController.jumpToPage(5 - 2);
} else if (value == 5 - 1) {
_tabController.animateTo(0);
_pageController.jumpToPage(1);
} else {
_tabController.animateTo(value - 1);
}
}
若自带的TabController不符合界面设计,也可以用几个Container绘制特定的形状颜色,通过PageController获取当前的显示的Page的索引改变对应Container的形状颜色即可。
class ScenicCard extends StatelessWidget {
ScenicCard(
{@required this.price,
@required this.title,
@required this.imageUrls,
@required this.score,
@required this.address,
this.onPress,
this.tags = const <Widget>[]})
: assert(imageUrls.length == 3);
final Widget price;
final List<Widget> tags;
final String title;
final List<String> imageUrls;
final String score;
final String address;
final VoidCallback onPress;
@override
Widget build(BuildContext context) {
final imageWidth = (MediaQuery.of(context).size.width - 60.0) / 3.0;
final imageHeight = imageWidth - 20.0;
final tagList = <Widget>[
price,
];
if (tags.length > 0) {
tags.forEach((tag) {
tagList.add(SizedBox(
width: 5.0,
));
tagList.add(tag);
});
}
return Card(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
),
elevation: 0.0,
margin: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 5.0),
color: Colors.white,
child: Padding(
padding: const EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
//粗体标题
Text(
title,
style: CardTitleTextStyle,
),
//卡片删除图标
Container(
height: 20,
width: 20,
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.highlight_off,
size: 20.0,
),
onPressed: onPress,
),
),
],
),
SizedBox(
height: 5,
),
Row(
children: <Widget>[
Text(
score,
style: GradeTextStyle,
),
Text(
address,
style: BehindGradeTextStyle,
)
],
),
SizedBox(
height: 7.0,
),
Row(
children: tagList,
),
SizedBox(
height: 7.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_buildImage(imageWidth, imageHeight, imageUrls[0]),
_buildImage(imageWidth, imageHeight, imageUrls[1]),
_buildImage(imageWidth, imageHeight, imageUrls[2]),
],
),
SizedBox(
height: 15,
)
],
),
),
);
}
}
推荐卡片的实现与前面提到的控件大同小异,在Card控件内放入一个Column控件,竖直排列四个水平行。第一行显示标题和关闭图片,主轴对齐方式为 MainAxisAlignment.spaceBetween (在控件中间均匀插入空白)。第二行显示评分与地区,第三行显示价格与标签,第四行显示三张图片。这里是以景区卡片举例,美食卡片与其差别很小,不再赘述。
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_meituan/src/Route/searchPage.dart';
import 'package:flutter_meituan/src/Route/testPage.dart';
import 'package:flutter_meituan/src/Style/myTheme.dart';
import 'package:flutter_meituan/src/Widget/commonWidget.dart';
import 'package:flutter_meituan/src/Widget/slidesShow.dart';
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
///主界面AppBar
AppBar _buildHomeAppBar() {
return AppBar(
automaticallyImplyLeading: false,
elevation: 0.0,
backgroundColor: Colors.white,
flexibleSpace: SafeArea(
//适配刘海
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ClipOval(
child: Image.asset("images/protrait.png",
width: 35.0, height: 35.0),
),
),
Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
"三河",
style: TextStyle(color: Colors.black, fontSize: 15.0),
),
Icon(
Icons.keyboard_arrow_down,
size: 15.0,
),
],
),
Text(
"晴 20°",
style: TextStyle(fontSize: 10.0),
)
],
),
padding: const EdgeInsets.all(8.0),
),
Expanded(
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(builder: (context) => SearchPage()));
},
child: Container(
height: 45.0,
child: Card(
elevation: 0.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0))),
color: Colors.grey[200],
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.search,
color: Colors.black87,
size: 20.0,
),
Text(
"自助烤肉",
style:
TextStyle(fontSize: 15.0, color: Colors.black87),
),
],
),
),
),
),
),
IconButton(
iconSize: 30,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
padding: EdgeInsets.zero,
icon: Icon(
Icons.add,
color: Colors.black,
),
onPressed: () {
Navigator.of(context)
.push(CupertinoPageRoute(builder: (context) {
return TextPage();
}));
},
),
],
),
),
);
}
Widget _buildMyButton(String title) {
return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black, width: 0.5),
color: Colors.white,
borderRadius: BorderRadius.circular(5),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
child: Center(
child: Text(
title,
style: TextStyle(fontSize: 12.0, color: Colors.black),
),
),
),
),
);
}
void _showDeleteDialog() {
var dialog = SimpleDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
titlePadding: EdgeInsets.only(top: 20),
title: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"选择具体理由,会减少相关推荐呦",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_buildMyButton("去过了"),
SizedBox(
width: 10,
),
_buildMyButton("不感兴趣"),
SizedBox(
width: 10,
),
_buildMyButton("价格不合适"),
],
),
SizedBox(
height: 15,
),
Container(
decoration: BoxDecoration(
color: CupertinoColors.lightBackgroundGray,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(15),
bottomRight: Radius.circular(15))),
child: Center(
child: FlatButton(
child: Text(
"不感兴趣",
style: TextStyle(fontSize: 12, color: Colors.teal),
),
onPressed: () => Navigator.of(context).pop(),
),
),
)
],
),
);
showDialog(
context: context,
builder: (context) => dialog,
);
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
const title1 = <String>[
"美食",
"电影/演出",
"酒店住宿",
"休闲娱乐",
"外卖",
];
const url1 = <String>[
"images/title/18.png",
"images/title/17.png",
"images/title/16.png",
"images/title/19.png",
"images/title/20.png",
];
const title2 = <String>[
"亲子",
"健身/游泳",
"周边游/旅游",
"丽人/美发",
"超市/生鲜",
];
const url2 = <String>[
"images/title/6.png",
"images/title/7.png",
"images/title/8.png",
"images/title/9.png",
"images/title/10.png",
];
const title3 = <String>[
"医疗/牙科",
"生活服务",
"景点/门票",
"签到领现金",
"更多",
];
const url3 = <String>[
"images/title/11.png",
"images/title/12.png",
"images/title/13.png",
"images/title/14.png",
"images/title/15.png",
];
List<Widget> _buildTitle(
List<String> strs, List<String> urls, double width) {
List<Widget> titleList = <Widget>[];
for (int i = 0; i < strs.length; i++) {
titleList.add(MyImageButton(
image: Image.asset(
urls[i],
width: width,
height: width,
),
title: strs[i],
width: (screenWidth - 30) / 5.0));
}
return titleList;
}
List<Widget> _buildBody() {
return <Widget>[
//第一行标题栏
Container(
padding: const EdgeInsets.only(
left: 15.0, right: 15.0, bottom: 5.0, top: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
MyImageButton(
image: Image.asset(
url1[0],
width: screenWidth / 7,
height: screenWidth / 7,
),
title: title1[0],
width: (screenWidth - 30) / 5.0),
MyImageButton(
image: Image.asset(
url1[1],
width: screenWidth / 7,
height: screenWidth / 7,
),
title: title1[1],
width: (screenWidth - 30) / 5.0),
MyImageButton(
image: Image.asset(
url1[2],
width: screenWidth / 7,
height: screenWidth / 7,
),
title: title1[2],
width: (screenWidth - 30) / 5.0,
tip: "嗨抢",
),
MyImageButton(
image: Image.asset(
url1[3],
width: screenWidth / 7,
height: screenWidth / 7,
),
title: title1[3],
tip: "网咖",
width: (screenWidth - 30) / 5.0),
MyImageButton(
image: Image.asset(
url1[4],
width: screenWidth / 7,
height: screenWidth / 7,
),
title: title1[4],
width: (screenWidth - 30) / 5.0),
],
),
),
SizedBox(
height: 20.0,
),
//第二行标题栏
Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildTitle(title2, url2, screenWidth / 14.0),
),
),
SizedBox(
height: 10.0,
),
//第三行标题栏
Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildTitle(title3, url3, screenWidth / 14.0),
),
),
Container(
padding: const EdgeInsets.only(
left: 10.0, right: 10.0, top: 30.0, bottom: 0.0),
child: SlidesShowWidget(
height: 80,
)),
ScenicCard(
onPress: _showDeleteDialog,
price: PriceText("5"),
score: "4.8分",
address: " | 东城区",
title: "故宫博物院(故宫) (5A)",
tags: <Widget>[
MyTag(tag: "网红地打卡"),
MyTag(tag: "帝王宫殿"),
MyTag(tag: "5A景点"),
],
imageUrls: <String>[
"http://p0.meituan.net/travel/83544ca4b38bbe0f7644982c3528defd117921.jpg@660w_500h_1e_1c",
"http://p1.meituan.net/poi/e732ed2314a1a2619e6c3254fd2f1fd0112611.jpg",
"http://p0.meituan.net/poi/e7d94c4d609e5dd4d71bcea6a5eb0c5e220371.jpg"
],
),
BigPictureCateCard(
onPress: _showDeleteDialog,
title: "老北京涮肉 4 人餐",
content: "套餐包括:羔羊肉,肥牛,香辣锅,鱼丸,炸灌肠,...",
address: "南锣鼓巷",
price: RichText(
text: TextSpan(children: <TextSpan>[
TextSpan(
text: "¥",
style: TextStyle(fontSize: 10.0, color: Colors.red)),
TextSpan(
text: "139",
style: TextStyle(
fontSize: 15.0,
color: Colors.red,
fontWeight: FontWeight.bold)),
TextSpan(
text: "¥190",
style: TextStyle(
decoration: TextDecoration.lineThrough,
color: Colors.black,
fontSize: 10.0))
]),
),
tags: <Widget>[
MyTag(
tag: "5.7折",
isEmphasize: true,
),
MyTag(tag: "销量火爆")
],
imageUrls: <String>[
"http://p1.meituan.net/deal/87d9fbf3dba19daf2becbca8c8daee74145248.jpg@428w_320h_1e_1c",
"http://p0.meituan.net/deal/2d65c591c7b02f9ca9bc61f667262319220693.jpg@428w_320h_1e_1c",
"http://p1.meituan.net/deal/4aea58490b74263d7170177fe3ab9f4c26990.jpg@428w_320h_1e_1c"
],
),
ScenicCard(
onPress: _showDeleteDialog,
price: Text(
"免费",
style: TextStyle(
color: Colors.red, fontSize: 10.0, fontWeight: FontWeight.bold),
),
score: "4.6分",
address: " | 后海/什刹海",
title: "后海",
tags: <Widget>[MyTag(tag: "城市地标"), MyTag(tag: "陪爸妈")],
imageUrls: <String>[
"https://p1.meituan.net/hotel/828cc5794f92e40c5de5182cb1b30993316981.jpg@220w_125h_1e_1c",
"http://p1.meituan.net/hoteltdc/998c2b9face5e48942e10b90bf42803a154752.jpg",
"http://p0.meituan.net/hotel/aaa8a7aed2ce2fe43aea50d6616293b2119956.jpg"
],
),
];
}
return Scaffold(
appBar: _buildHomeAppBar(),
body: Container(
decoration: GradientDecoration,
child: ListView(
children: _buildBody(),
),
),
);
}
}
ListView控件使控件列表中的控件水平或垂直显示,并且支持滚动。如果需要显示滚动轴,在外层嵌套一个Scrollbar即可。ListView作为Flutter最常用的滚动块推荐前往官方文档细致的学习一下:ListView
下一篇文章将完善首页各个按钮的点击事件及滑动到底部时加载新数据等内容。