Flutter 布局实例
控件 Text,Image,Contianer,Column,Row,ListView,Expand Stack 等通过基础 Widget 或 Widget 的组合基本能实现需求
基础控件来实现完整的 Flutter 应用程序

两个 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 等
floatingActionButtonLocationfloatingActionButtonLocation:FloatingActionButtonLocation.centerDocked 这个表示 Fab 的样式 centerDocked就是表示居中陷入的样式 还有其他的样式 如右下角显示等 这里可以自行尝试
bottomNavigationBarbottomNavigationBar 也就是底部导航 这里我指定了一个函数 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 中根据数据构造布局
loadDataloadData() 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 集合中
getBodygetBody() { 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 返回每一项布局
getItemgetItem(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 的组合嵌套等 就是代码可能显着乱一些
尊贵的董事大人
英文标题不为空时 视为本栏投稿
需要关键字 描述 英文标题