Flutter 布局实例

控件 Text,Image,Contianer,Column,Row,ListView,Expand  Stack 等
通过基础 Widget 或 Widget 的组合基本能实现需求

基础控件来实现完整的 Flutter 应用程序


widget layout

两个 tab 页 主页面点击 item 跳转到详情页

主程序搭建

四个文件 一个是主程序架构页面 一个是主页 一个是我的 一个是详情

项目的主体及底部导航

两个 tab 点击 tab  切换界面

Flutter 里 Scaffold 这个 Widget 代表脚手架
Flutter 封装好基本的控件 Scaffold 里面包含了

  • appBar 导航栏

  • drawer 抽屉菜单

  • bottomNavigationBar 底部导航

  • fab:floatingActionButton

Scaffold 快速开发一个页面    return Scaffold(

backgroundColor: Colors.white,
body: list[_index],
floatingActionButton:
Container(
  width: 50,
  height: 50,
  child:
  FloatingActionButton(
	heroTag: "main_fab",
	isExtended:true,
	onPressed: () {
		  print("to do sth");
	  }, //图标颜色
	elevation: 8,
	highlightElevation: 5,
	child: Icon(Icons.camera),
  ),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar:
Container(
  child:BottomAppBar(  //这个 shape 是 底部导航的压缩效果
	shape: CircularNotchedRectangle(),
	//child: tabs(),  //阴影效果
	elevation: 20,    //圆弧弧度
	notchMargin: 15,
	child:tabs(),
  ),
  height: 70,
)
);
body是主页面 list 集合中包含 事先定义好的页面 点击按钮 修改 _index 的值  相当于切换了页面floatingActionButton

这里 fab 图标使用了 icon  也能使用其他的 widget 如 image  或者 svg 等

floatingActionButtonLocation

floatingActionButtonLocation:FloatingActionButtonLocation.centerDocked 这个表示 Fab 的样式 centerDocked就是表示居中陷入的样式 还有其他的样式 如右下角显示等 这里可以自行尝试

bottomNavigationBar

bottomNavigationBar 也就是底部导航 这里我指定了一个函数 tabs(),返回底部 widget :

Row tabs() {
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
   new Container(
	child: IconButton(
	  icon: Icon(Icons.home),
	  color:_index == 0? Colors.blue:Colors.orangeAccent,
	  onPressed: () {
 		setState(() {
		  _index = 0;
		});
 	  },
	),
  ),
   IconButton(
	icon: Icon(Icons.person),
	color:_index == 1? Colors.blue:Colors.orangeAccent,
	onPressed: () {
	  setState(() {
		_index = 1;
	  });
	},
  ),
 ],
);
}

这里的 tabs 函数直接返回了行布局 也就是 看到的底部的两个 tab 按钮 这里的 按钮用 Icon 来实现 当然也能用其他的 widget 来实现 如在用一个列布局 上面是图标 下面是文字等

每个 tab 的 onPressed 中监听点击事件 当点击是修改 -index 的值 通过 setState 来刷新 这样就达到了切换页面的效果 同事通过对 _index 的判断修改点击时的颜色

最后 完整的代码:

import 'dart:io';import 'package:flutter/material.dart';import 'package:flutter/services.dart';import 'package:flutter_widget/home_page.dart';import 'package:flutter_widget/my_page.dart';void main() {
 if (Platform.isAndroid) {
 /**
* 设置状态栏颜色
*/
SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
 /**
* 强制竖屏
*/
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown    ]);
 }
 runApp(MyApp());}class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Widget',
theme: ThemeData(
primarySwatch: Colors.blue,
primaryColor: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}}class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
 final String title;
 @override
_MyHomePageState createState() => _MyHomePageState();}class _MyHomePageState extends State<MyHomePage> {
//主页 tab 索引
int _index = 0;
Color _tabColor = Colors.blue;
 @override
void initState() {
super.initState();
list..add(new HomePage())..add(new MyPage());
}
 List<Widget> list = new List();
 @override
Widget build(BuildContext context) {
Row tabs() {
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
   new Container(
	child: IconButton(
	  icon: Icon(Icons.home),
	  color:_index == 0? Colors.blue:Colors.orangeAccent,
	  onPressed: () {
 		setState(() {
		  _index = 0;
		});
 	  },
	),
  ),
   IconButton(
	icon: Icon(Icons.person),
	color:_index == 1? Colors.blue:Colors.orangeAccent,
	onPressed: () {
	  setState(() {
		_index = 1;
	  });
	},
  ),
 ],
);
}
 return Scaffold(
backgroundColor: Colors.white,
body: list[_index],
floatingActionButton:
Container(
  width: 50,
  height: 50,
  child:
  FloatingActionButton(
	heroTag: "main_fab",
	isExtended:true,
	onPressed: () {
		  print("to do sth");
	  },
	//图标颜色
	elevation: 8,
	highlightElevation: 5,
	child: Icon(Icons.camera),
  ),
 ),
 floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar:
Container(
  child:BottomAppBar(
	//这个 shape 是 底部导航的压缩效果
	shape: CircularNotchedRectangle(),
	//child: tabs(),
	//阴影效果
	elevation: 20,
	//圆弧弧度
	notchMargin: 15,
	child:tabs(),
  ),
  height: 70,
)
);
}}

三、界面布局

主页的的整体结构:

class HomePageState extends State<HomePage> {
 List subjects = [];
@override
void initState() {
loadData();
}
 @override
Widget build(BuildContext context) {
 return Scaffold(
appBar: AppBar(
title: Text("当前热映电影"),
),
body: Center(
child: getBody(),
),
);
 }}

initState() 去加载数据 这里是通过 dio 去请求数据的

body 中根据数据构造布局

loadData
loadData() async {
String loadRUL = "https://douban.uieee.com/v2/movie/in_theaters";
try {
Response response = await Dio().get(loadRUL);
print(response);
var result = json.decode(response.toString());
setState(() {
subjects = result['subjects'];
});
 } catch (e) {
print(e);
}
}

loadData() 会通过 Dio 请求数据 返回结果保存到 subjects 集合中

getBody
  getBody() {
if (subjects.length != 0) {
return
ListView.builder(
	itemCount: subjects.length,
	itemBuilder: (BuildContext context, int position) {
	  return getItem(subjects[position]);
	});
 } else {
///这个是 ios 风格的加载菊花return CupertinoActivityIndicator();
}
}

返回 listView 通过 getItem 返回每一项布局

getItem
  getItem(var subject) {//    演员列表
var avatars = List.generate(subject['casts'].length, (int index) =>
Container(
  margin: EdgeInsets.only(left: index.toDouble() == 0.0 ? 0.0 : 16.0),
  child: CircleAvatar(
	  backgroundColor: Colors.white10,
	  backgroundImage: NetworkImage(
		  subject['casts'][index]['avatars']['small']
	  )
  ),
),
);
 var row = Container(
margin: EdgeInsets.all(4.0),
child:
Row(
children: <Widget>[
  ClipRRect(
	borderRadius: BorderRadius.circular(4.0),
	child: Image.network(
	  subject['images']['large'],
	  width: 100.0, height: 150.0,
	  fit: BoxFit.fill,
	),
  ),
   Expanded(
	  child:
	  Stack(
		children: <Widget>[
		  Container(
		  margin: EdgeInsets.only(left: 8.0),
		  height: 150.0,
		  alignment: Alignment.centerLeft,
		  child: Column(
			crossAxisAlignment: CrossAxisAlignment.start,
			mainAxisAlignment: MainAxisAlignment.center,
 			children: <Widget>[
			  Text(
				subject['title'],
				style: TextStyle(
				  fontWeight: FontWeight.bold,
				  fontSize: 18.0,
				),
				maxLines: 1,
			  ),
 			  Text(
				  "类型:${subject['genres'].join("、")}"
			  ),
 			  Text(
				  '导演:${subject['directors'][0]['name']}'
			  ),
 			  Container(
				margin: EdgeInsets.only(top: 8.0),
				child: Row(
				  children: <Widget>[
					Text('主演:'),
					Row(
					  children: avatars,
					)
				  ],
				),
			  )
			],
		  ),
		),
 		  Positioned(
			top: 15,
			right: 2,
			child: new     Text(
			  '${subject['rating']['average']} 分',
			  style: TextStyle(
				  fontFamily: 'GloriaHallelujah',
				  color: Colors.redAccent,
				  fontSize: 16.0
			  ),
			),
 		  )
		],
	  ),
  )
 ],
),
);
return InkWell(
child:  Card(
child: row,
),
onTap: (){
 Navigator.push(context,
	PageRouteBuilder(
		transitionDuration: Duration(microseconds: 100),
		pageBuilder: (BuildContext context, Animation animation,
			Animation secondaryAnimation) {
		  return new FadeTransition(
			opacity: animation,
			child: DetailPage(id:  subject['id'].toString(),),
		  );
		})
);
 },
);
 }

getItem 里是每一项的布局 根布局是一个 InkWell 这个 widget有水波纹效果 同时能够响应点击事件 跳转到详情页面 跳转路由里面重写了 PageRouteBuilder 可以自定义过度动画 在跳转到DetailPage 的时候 传递了一个参数 id 过去

InkWell 中指定子 widget 为 row 也就是行布局 可以看到每个 item 是一个行布局
左边的封面 右边的三行文字 同时还有一个分数
这里的分数通过 Positioned 来定位 当然就需要 Stack 了

import 'dart:convert';import 'package:flutter/cupertino.dart';import 'package:flutter/material.dart';import 'package:flutter_widget/detail_page.dart';import 'package:dio/dio.dart';class HomePage extends StatefulWidget {
 @override
State<StatefulWidget> createState() => HomePageState();}class HomePageState extends State<HomePage> {
 List subjects = [];
@override
void initState() {
loadData();
}
 @override
Widget build(BuildContext context) {
 return Scaffold(
appBar: AppBar(
title: Text("当前热映电影"),
),
body: Center(
child: getBody(),
),
);
}
 loadData() async {
String loadRUL = "https://douban.uieee.com/v2/movie/in_theaters";
try {
Response response = await Dio().get(loadRUL);
print(response);
var result = json.decode(response.toString());
setState(() {
subjects = result['subjects'];
});
 } catch (e) {
print(e);
}
}
getItem(var subject) {//    演员列表
var avatars = List.generate(subject['casts'].length, (int index) =>
Container(
  margin: EdgeInsets.only(left: index.toDouble() == 0.0 ? 0.0 : 16.0),
  child: CircleAvatar(
	  backgroundColor: Colors.white10,
	  backgroundImage: NetworkImage(
		  subject['casts'][index]['avatars']['small']
	  )
  ),
),
);
 var row = Container(
margin: EdgeInsets.all(4.0),
child:
Row(
children: <Widget>[
  ClipRRect(
	borderRadius: BorderRadius.circular(4.0),
	child: Image.network(
	  subject['images']['large'],
	  width: 100.0, height: 150.0,
	  fit: BoxFit.fill,
	),
  ),
   Expanded(
	  child:
	  Stack(
		children: <Widget>[
		  Container(
		  margin: EdgeInsets.only(left: 8.0),
		  height: 150.0,
		  alignment: Alignment.centerLeft,
		  child: Column(
			crossAxisAlignment: CrossAxisAlignment.start,
			mainAxisAlignment: MainAxisAlignment.center,
 			children: <Widget>[
			  Text(
				subject['title'],
				style: TextStyle(
				  fontWeight: FontWeight.bold,
				  fontSize: 18.0,
				),
				maxLines: 1,
			  ),
 			  Text(
				  "类型:${subject['genres'].join("、")}"
			  ),
 			  Text(
				  '导演:${subject['directors'][0]['name']}'
			  ),
 			  Container(
				margin: EdgeInsets.only(top: 8.0),
				child: Row(
				  children: <Widget>[
					Text('主演:'),
					Row(
					  children: avatars,
					)
				  ],
				),
			  )
			],
		  ),
		),
 		  Positioned(
			top: 15,
			right: 2,
			child: new     Text(
			  '${subject['rating']['average']} 分',
			  style: TextStyle(
				  fontFamily: 'GloriaHallelujah',
				  color: Colors.redAccent,
				  fontSize: 16.0
			  ),
			),
 		  )
		],
	  ),
  )
 ],
),
);
return InkWell(
child:  Card(
child: row,
),
onTap: (){
 Navigator.push(context,
	PageRouteBuilder(
		transitionDuration: Duration(microseconds: 100),
		pageBuilder: (BuildContext context, Animation animation,
			Animation secondaryAnimation) {
		  return new FadeTransition(
			opacity: animation,
			child: DetailPage(id:  subject['id'].toString(),),
		  );
		})
);
 },
);
 }
 getBody() {
if (subjects.length != 0) {
return
ListView.builder(
	itemCount: subjects.length,
	itemBuilder: (BuildContext context, int position) {
	  return getItem(subjects[position]);
	});
 } else {
///这个是 ios 风格的加载菊花return CupertinoActivityIndicator();
}
}}

详情页比较简单

import 'dart:convert';import 'package:flutter/material.dart';import 'package:dio/dio.dart';class DetailPage extends StatefulWidget {
 String id;
DetailPage({this.id});
@override
State<StatefulWidget> createState() => DetailPageState();}class DetailPageState extends State<DetailPage> {
 ///movie id
String movieId = "";
///封面图片 url
String imageUrl = "";
///豆瓣评分
String score = "";
///简介
String summary = "";
///电影名称
String alt_titile = "";
 @override
void initState() {
movieId = widget.id;
initMovieData();
}
 initMovieData() async {///电影详情地址
String  movieDetail = "https://douban.uieee.com/v2/movie/$movieId";
 try {
Response response2 = await Dio().get(movieDetail);
var result = json.decode(response2.toString());
 score = result['rating']['average'];
alt_titile = result['alt_title'];
imageUrl = result['image'];
summary = result['summary'];
 setState(() {
});
 } catch (e) {
print(e);
}
}
 @override
Widget build(BuildContext context) {
 return Scaffold(
body: CustomScrollView(
primary:false,
slivers: <Widget>[
  SliverAppBar(
	automaticallyImplyLeading:false,
	pinned: true,
	expandedHeight:150,
	flexibleSpace: FlexibleSpaceBar(
	  titlePadding: EdgeInsets.only(left:40,top: 0,bottom: 30),
	  background:
	  Image.network(
		imageUrl, fit: BoxFit.cover,)
	),
  ),
   new SliverFixedExtentList(
	itemExtent: 50,
	delegate: new SliverChildBuilderDelegate(
			(BuildContext context, int index) {
		  //创建列表项
		  return new Container(
			color: Colors.white,
			alignment: Alignment.centerLeft,
			padding: EdgeInsets.only(left: 25),
			child:  Text("$alt_titile",),
		  );
		},
		childCount: 1 //50个列表项
	),
  ),
   new SliverFixedExtentList(
	itemExtent:10,
	delegate: new SliverChildBuilderDelegate((BuildContext context, int index) {
	  return new Container(
		alignment: Alignment.center,
	  );
	},
		childCount: 1
	),
  ),
   new SliverFixedExtentList(
	itemExtent:50,
	delegate: new SliverChildBuilderDelegate(
			(BuildContext context, int index) {
 			  return new Container(
				color: Colors.white,
				alignment: Alignment.centerLeft,
				padding: EdgeInsets.only(left: 25),
				child:  Text("豆瓣评分:$score",),
			  );
		},
		childCount: 1 //50个列表项
	),
  ),
   new SliverFixedExtentList(
	itemExtent:10,
	delegate: new SliverChildBuilderDelegate((BuildContext context, int index) {
	  return new Container(
		alignment: Alignment.center,
	  );
	},
		childCount: 1
	),
  ),
   new SliverFixedExtentList(
	itemExtent:250,
 	delegate: new SliverChildBuilderDelegate(
			(BuildContext context, int index) {
		  return new Container(
			padding: EdgeInsets.symmetric(horizontal: 25),
			color: Colors.white,
			child: new ListView(
			  children: <Widget>[
				Text("简介:$summary"),
			  ],
			),
		  );
		},
		childCount: 1 //50个列表项
	),
  ),
 ],
),
);
 }}

详情界面里面 用到了头部SliverAppBar
SliverAppBar对应AppBar 两者不同之处在于SliverAppBar可以集成到CustomScrollView SliverAppBar可以结合FlexibleSpaceBar实现MaterialDesign中头部伸缩的模型 为了使用 SliverAppBar 因此 body 根布局指定为 CustomScrollView
我为了显示一个分割的效果 这个分割条我也用 SliverFixedExtentList 来实现

我的界面同详情页差不多

import 'package:flutter/material.dart';class MyPage extends StatefulWidget {
 @override
State<StatefulWidget> createState() => MyPageState();}class MyPageState extends State<MyPage> {
@override
Widget build(BuildContext context) {
 return Scaffold(
body: CustomScrollView(
primary:false,
slivers: <Widget>[
  SliverAppBar(
	automaticallyImplyLeading:false,
	pinned: true,
	expandedHeight:150,
	flexibleSpace: FlexibleSpaceBar(
	  title: Container(
		margin: EdgeInsets.only(top: 80),
		alignment: Alignment.center,
		child: Column(
		  mainAxisAlignment: MainAxisAlignment.center,
		  crossAxisAlignment: CrossAxisAlignment.center,
		  children: <Widget>[
			Container(
			  width: 40,
			  height: 40,
			  child:
			  ClipOval(
				child:  Image.asset("images/avatar.jpg",fit: BoxFit.cover,))),
			Container(
			  child:  new Text("个人中心", ),
			),
		  ],
		),
	  ),
	  centerTitle: true,
	  background: Image.asset(
		"images/bg.jpg", fit: BoxFit.cover,),
	),
  ),
   new SliverFixedExtentList(
	itemExtent: 50,
	delegate: new SliverChildBuilderDelegate(
			(BuildContext context, int index) {
		  //创建列表项
		  return new Container(
			alignment: Alignment.centerLeft,
			margin: EdgeInsets.only(left: 25),
			child:  Text("简介:介绍一下自己吧~",),
		  );
		},
		childCount: 1 //50个列表项
	),
  ),
   new SliverFixedExtentList(
	itemExtent:10,
	delegate: new SliverChildBuilderDelegate((BuildContext context, int index) {
		  return new Container(
			alignment: Alignment.center,
			child: new Text(''),
		  );
		},
		childCount: 1
	),
  ),
  new SliverFixedExtentList(
	itemExtent:50,
	delegate: new SliverChildBuilderDelegate(
			(BuildContext context, int index) {
		  //创建列表项
		  return new InkWell(
			  child:
			  new Container(
				alignment: Alignment.centerLeft,
				child:Row(
				  children: <Widget>[
					Expanded(
					  child: Row(
						children: <Widget>[
 						  Container(
							margin: EdgeInsets.only(left: 25),
							child: Icon(Icons.settings,color: Colors.blue,),
						  ),
						  Container(
							margin: EdgeInsets.only(left: 10),
							child:  Text("设置",style: TextStyle(color: Colors.blue),),
						  ),
						],
					  ),
					  flex: 1,
					),
 					Expanded(
					  child: Container(
						width: 20,
						height:20,
						alignment: Alignment.centerRight,
						margin: EdgeInsets.only(right: 5),
						child:Icon(Icons.arrow_forward_ios,color:Colors.blue),
					  ),
					  flex: 1,
					),
				  ],
				),
			  ),
			  );
 		},
		childCount: 1 //50个列表项
	),
  ),
  new SliverFixedExtentList(
	itemExtent: 4,
	delegate: new SliverChildBuilderDelegate(
			(BuildContext context, int index) {//创建列表项
		  return new Container(
			alignment: Alignment.center,
			child: new Text(''),
		  );
		},
		childCount: 1 //50个列表项
	),
  ),///分享
  new SliverFixedExtentList(
	itemExtent: 50,
	delegate: new SliverChildBuilderDelegate(
			(BuildContext context, int index) {  //创建列表项
 		  return new InkWell(
 			  child: new Container(
				alignment: Alignment.centerLeft,
				child:Row(
				  children: <Widget>[
 					Expanded(
					  child: Row(
						children: <Widget>[
						  Container(
							margin: EdgeInsets.only(left: 25),
							child:  Icon(Icons.share,color: Colors.blue,),
						  ),
						  Container(
							margin: EdgeInsets.only(left: 10),
							child:  Text("分享",style: TextStyle(color: Colors.blue),),
						  ),
 						],
					  ),
					  flex: 1,
					),
 				   Expanded(
					 child:  Container(
					   width: 20,
					   height:20,
					   alignment: Alignment.centerRight,
					   margin: EdgeInsets.only(right: 5),
					   child:Icon(Icons.arrow_forward_ios,color:Colors.blue),
					 ),
					 flex: 1,
				   ),
 				  ],
				),
			  ),
		  );
 		},
		childCount: 1 //50个列表项
	),
  ),
  new SliverFixedExtentList(
	itemExtent: 4,
	delegate: new SliverChildBuilderDelegate(
			(BuildContext context, int index) {  //创建列表项
		  return new Container(
			alignment: Alignment.center,
			child: new Text(''),
		  );
		},
		childCount: 1 //50个列表项
	),
  ),//关于
  new SliverFixedExtentList(
	itemExtent: 50,
	delegate: new SliverChildBuilderDelegate(
			(BuildContext context, int index) {
 		  return new InkWell(
			  child: new Container(
				alignment: Alignment.centerLeft,
				child:Row(
				  children: <Widget>[
 					Expanded(
					  child: Row(
						children: <Widget>[
						  Container(
							margin: EdgeInsets.only(left: 25),
							child: Icon(Icons.insert_drive_file,color: Colors.blue,),
						  ),
 						  Container(
							margin: EdgeInsets.only(left: 10),
							child:  Text("关于",style: TextStyle(color: Colors.blue),),
						  ),
 						],
					  ),
					  flex: 1,
					),
				   Expanded(
					 child:  Container(
					   width: 20,
					   height: 20,
					   alignment: Alignment.centerRight,
					   margin: EdgeInsets.only(right: 5),
					   child:Icon(Icons.arrow_forward_ios,color:Colors.blue),
					 ),
					 flex: 1,
				   ),
 				  ],
				),
			  ),
 		  );
 		},
		childCount: 1 //50个列表项
	),
  ),
],
),
);
}}

SliverAppBar 里面的头像和名字和用 title
来实现 圆形头像用 ClipOvalFlutter 实现界面布局
很容易 实现复杂布局也不是很难
主要就是 widget 的组合嵌套等 就是代码可能显着乱一些