Flutter仿美团应用开发笔记-首页 (1)
首页篇
Github项目地址:项目地址 上一篇博客讲解了该应用的基础结构,如底部导航栏等。基础结构篇 这篇博客将详细讲解美团首页的界面实现,在下一篇博客实现推荐卡片无限加载,带插入和移除动画的列表,弹出菜单等细节。
效果图: 首先对首页进行拆解,将较为复杂的界面切成一个个小部件方便理解: 1. AppBar 2. 三行由图片和标题组成的按纽栏 3. 定时滚动的轮播图 4. 推荐卡片
AppBar
///主界面AppBarAppBar _buildHomeAppBar() { //构建一个美团首页AppBarreturn 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, //设置各方向外边距为0icon: 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;@overrideWidget 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;@overrideWidget 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;@overridevoid initState() {_timer = Timer.periodic(Duration(seconds: 2), _handleTimeout);super.initState();_tabController = TabController(length: 3, vsync: this);}@overridevoid dispose() {_tabController?.dispose();_pageController?.dispose();_timer?.cancel();super.dispose();}@overrideWidget 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控件内置了水平或垂直滚动操作,具体参考api.flutter.dev/flutter/wid…
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;@overrideWidget 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> {///主界面AppBarAppBar _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,);}@overrideWidget 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
下一篇文章将完善首页各个按钮的点击事件及滑动到底部时加载新数据等内容。
转载于:https://juejin.im/post/5ceb97e66fb9a07efc4967c2
Flutter仿美团应用开发笔记-首页 (1)相关推荐
- Flutter仿美团应用开发笔记-入门篇
Flutter概览 Flutter是谷歌发布的跨平台开发框架(通过极少的改动即可运行在桌面端,web端,Android与IOS且UI具有高度一致性)并将作为谷歌新操作系统Fuchsia的UI框架.在移 ...
- 仿美团网开发笔记(持更)
问题待查: 1. vue数据驱动(数据双向绑定) 2. 所遇难点:
- nuxt全栈仿美团官网13——首页下面的格调
文档:nuxt全栈仿美团官网13--首页下面的格... 链接:http://note.youdao.com/noteshare?id=6b7b36720b665f830357b221a0d28fba& ...
- flutter仿美团APP
Flutter 仿美图APP 传送门 https://github.com/lshaoshuai/flutter_meituan_hotel 项目简介 酒店管理系统练手项目APP前端,用flutter ...
- Android开发笔记(一百六十四)仿京东首页的下拉刷新
上一篇文章介绍了高仿京东的沉浸式状态栏,可是跟京东首页的头部轮播图相比,依然有三处缺憾: 1.京东的头部Banner上方,除了有悬浮着的状态栏,状态栏下面还有一行悬浮工具栏,内嵌扫一扫图标.搜索框,以 ...
- Flutter开发实战 高仿微信(一)首页
Flutter开发实战 高仿微信(一)首页 Flutter开发实战 高仿微信(一)首页 flutter 开发微信项目 (一) 1. 开发HomePage页 2. 用到的知识点讲解 2.1 Bottom ...
- Android开发笔记(一百四十四)高仿支付宝的头部伸缩动画
Android5.0推出的MaterialDesign库包含了处理头部工具栏的多个控件,不但允许自定义顶部导航栏,而且导航栏高度是可以伸缩的.如此一来,一方面导航栏能够放得下更多控件,另一方面在用户想 ...
- 【vue】二、vue2仿去哪儿网app——首页开发
文章目录 二.vue2仿去哪儿网app--首页开发 Ⅰ 页面结构 Ⅱ 开发笔记及注意点 1.公共样式抽取 2.路径 --> 绝对路径 3.用padding-bottom实现固定宽高比 4.保证内 ...
- Android开发笔记(一百六十三)高仿京东的沉浸式状态栏
前面的文章介绍了如何实现广告轮播的Banner效果,本想可以告一段落.然而某天产品经理心血来潮,拿着苹果手机,要求像iOS那样把广告图顶到状态栏这儿.刚接到这需求,不禁倒吸一口冷气,又要安卓开发去实现 ...
最新文章
- 算法实践1_线性回归
- top 命令_Linux监控cpu以及内存使用情况之top命令
- 有趣的php实例,8个必备的PHP功能实例代码
- ASP.NET Web API 2 过滤器
- BCVP第2期:项目已完成升级.NET5.0
- mysql去重取最大值,逻辑类似oracle的over(partition by)函数
- 贝叶斯分层回归模型的推理、EM求解和Java编程
- 天津大学计算机专硕_(55)东北林业大学2020计算机与软件考研数据速览
- apm飞控制作_传统直接转矩控制
- 编译原理三大经典书籍
- 《神奇的数学》读后感_奇妙的数学王国读后感10篇
- Persistent.
- 工赋开发者社区 | 抛弃 Google,Debian 改将 DuckDuckGo 作为默认搜索引擎
- 别让Java对象逃逸(Object Escape)
- Arithmetic Progression 题解(随机数使用)
- C#9结构、类与属性
- pycharm debug 提示 Python Debugger Extension Available Cython extension speeds up Python debugging
- OpenSSL中的EVP接口
- Jconsole的使用及对java内存池的专业术语理解
- 亚洲首屈一指的Web3盛会TOKEN2049达到200名赞助商里程碑,公布新的重量级演讲嘉宾
热门文章
- 【基于arduino的esp32-cam视频监控简单使用】
- 【数据结构与算法】前端JS实现栈
- 数据库技术的变迁历史及发展趋势
- 初探RxJava(基础篇)
- 【C语言】题目:古典问题(兔子生崽):有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?
- 红皮书——红色启示录
- 15_Python3.6+selenium2.53.6自动化测试_登录126邮箱
- Z-score 和 标准正态分布的关系
- 导入excel时报错The supplied data appears to be in the Office 2007+ XML.
- 1023day5:class类属性方法、每次执行类属性+1、内建模块、时间装饰器wrapper、面向对象__slots__方法:限制类的属性等基础知识、正则表达式基础知识、多态鸭子类型