欢迎大家来到IT世界,在知识的湖畔探索吧!
Flutter系列的文章我会持续更新一个月左右,力求利用1个月带大家入门Flutter,抓住这波技术风口,欢迎大家关注。同时如果觉得这里代码排版不是很舒服的读者可以关注我的微信公众号“IT工匠”,我会同步更新,另外微信公众号上还有很多互联网必备资源(涉及算法、数据结构、java、深度学习、计算机网络、python、Android等互联网技术资料),欢迎大家关注、交流。
欢迎大家来到IT世界,在知识的湖畔探索吧!
本文目录:
首先明确几点概念:
- Widget是构建UI的类
- Widget在构建UI元素和构建布局时都会用到
- 将简单的Widget组合起来可以构建复杂的Widget
Flutter布局机制的核心是Widget。在Flutter中,几乎所有东西都是一个Widget,即使布局模型也不例外。在Flutter应用程序中看到的图像(image)、图标(icon)和文本(text)都是Widget。那些你看不到的东西也是Widget,比如用来排列(arrange)、约束(constrain)和对齐(align)可见Widget的行(Row)、列(Column)和网格(grid),都是Widget。
你可以通过组合多个Widget来创建布局,这样可以构建出更复杂的Widget。例如,下图显示了3个图标(icon),每个图标下都有一个标签(label):
我们透过现象看本质:
上图显示了真正的视觉布局,它有一行三列,其中每列包含一个图标(icon)和一个标签(label),就是这种简单Widget的组合最终构成了我们看到的复杂Widget。
注意:在本文档中的大多数插图都是在debugPaintSizeEnabled=true的前提下获取的,这个选项可以在屏幕上绘制出视觉布局(如上图,绘制出了每一个可见、不可见Widget的分隔线,即实际的布局)。
下图画出了上图布局的Widget树:
这其中的大部分看起来和我们预想的差不多,但是你可能会对Container(粉红色的圈)感到疑惑,Container是一个Widget类,允许我们自定义其子Widget。如果要添加填充(padding)、页边距(margin)、边框(border)或背景色(background color),请使用Container来设置对应功能。
在本例中,每个Text Widget都放置在一个Container中以添加页边距(margin)。整个行(Row)也放置在一个Container中,以便在该行的周围添加填充(padding)。
本例中的其余UI元素均由属性控制。使用Icon的color属性设置图标的颜色。使用Text.Style属性设置字体、颜色、粗细等。Row和Column的属性允许您指定其子级垂直或水平对齐的方式,以及子级应占用的空间。
注意:可能有的读者对margin和padding的区别不是很清楚,margin指的是当前Widget距离其他Widget的距离,padding指的是当前Widget中的内容距离当前Widget边界的距离(比如Text中的文字显示出来距离Text Widget外边缘的距离)。
对Widget进行布局
在Flutter中你应该如何对一个简单的Widget进行布局?这节将向你介绍如何创建并展示一个简单的Widget,同时会向你介绍Hello World示例app的全部代码。
在Flutter中,只需要很少的步骤就可以将Text、Icon或者Image放置在屏幕上:
1. 选择一个布局Widget
根据您希望如何对齐或约束可见的Widget从各种布局Widget中进行选择,因为这些布局Widget的特性通常会传递给他们所包含的Widget。
此示例使用Center这个Widget,该Widget的特点是将其包裹的内容水平和垂直居中。
2. 创建一个可见的Widget
比如创建一个Text Widget:
欢迎大家来到IT世界,在知识的湖畔探索吧!Text('Hello World'),
创建一个Image Widget:
Image.asset( 'images/lake.jpg', fit: BoxFit.cover, ),
创建一个Icon Widget:
欢迎大家来到IT世界,在知识的湖畔探索吧!Icon( Icons.star, color: Colors.red[500], ),
3. 将可见的Widget添加到布局Widget中
所有的布局Widget都具有以下特性:
- 如果修饰的是单个Widget则具有一个child属性,比如Center或者Container
- 如果修饰的是多个Widget则具有一个children属性,比如Row、Column、ListView、Stack。
将Text Widget添加到Center Widget中:
Center( child: Text('Hello World'), ),
4. 将布局Widget添加到页面中
一个Flutter App本身就是一个Widget,而大多数Widget都具有一个build()方法,所以我们可以在app的build()方法中实例化并返回一个Widget,这样被返回的Widget就可以被显示出来。
Material app
对于Material风格的app,你可以使用一个Scaffold Widget,这个Widget提供了一个默认的横栏、默认的背景色,而且还有一些像添加drawer、添加snack bar、添加bottom sheet这样的API。你可以直接将Center Widget作为Scaffold的body属性进而添加到页面中:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter layout demo', home: Scaffold( appBar: AppBar( title: Text('Flutter layout demo'), ), body: Center( child: Text('Hello World'), ), ), ); } }
注意:Material库(地址:https://api.flutter.dev/flutter/material/material-library.html)中实现的Widget都是遵循Material设计准则的,当你设计你的UI的时候,你可以仅仅只使用标准的Widget库(地址:https://api.flutter.dev/flutter/widgets/widgets-library.html),你也可以使用Material库中的Widget。当然,你也可以混合使用这两种Widget库中的Widget,你可以在现有Widget的基础上自定义Widget,也可以不基于现有的Widget构建完全属于自己的Widget。
Non-Material app
对于非Material风格的app,你可以直接在你app的build()方法中返回Center Widget:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration(color: Colors.white), child: Center( child: Text( 'Hello World', textDirection: TextDirection.ltr, style: TextStyle( fontSize: 32, color: Colors.black87, ), ), ), ); } }
默认的非Material风格的App是没有AppBar、没有标题、没有背景色的,如果你想在非Material风格的app中显示上述元素,就只有自己手动去构建了。在这个示例中手动将背景色设置成为了白色、将Text中的文字颜色设置成了黑色以模仿Material风格的app,运行起来是这样的:
在垂直和水平方向构建多个Widget
最常见的布局模式是垂直或水平排列多个Widget。可以使用Row Widget水平排列Widget,使用Column Widget垂直排列Widget。
首先明确几点概念:
- Row和Column是最常用的2种布局模式
- Row和Column这两个Widget都可以包裹多个Widget
- Row和Column的子Widget可以是Row或Column,或者其他任何复杂的Widget
- 你可以指定Row和Column如何垂直或者水平排列其子Widget
- 你可以拉伸(stretch)或者约束(constrain)指定的子Widget
- 你可以指定子Widget如何使用Row或者Column的剩余可用空间
为了在Flutter中创建一个行或者列,你应该将子Widget组成的List放进Row Widget或者Column Widget中。每个子元素本身可以是Row或Column,下面的示例展示了如何在Row或Column中嵌套Row或Column:
此布局整体上为一行,这一行包含了两个子元素:左侧的列和右侧的图像,而左侧的列中的Widget树又是由多个行和列组成的:
注意:Row和Column是水平和垂直布局的基本Widget,这些基础的Widget允许最大程度的自定义。Flutter还提供了功能更专一的,更高级别的Widget,足以满足你的需要。例如,你有时可能更喜欢ListTile而不是Row,它是一个易于使用的Widget,具有用于前导(leading)和尾随图标(trailing icon)的属性,并且最多有3行文本。再比如,相比于Column,您有时可能更喜欢ListView,它是一种类似于Column的布局,如果其包裹的内容太长导致可用空间无法完全容纳,它会进行自动滚动。
设置行和列中子Widget的对其方式
将多个Widget放在同一行或同一列之后,我们有时还需要对这些同一行或者同一列的Widget进行对齐方式的设置,比如:
上图中黑色框代表Row(行),1、2、3分别是3个子Widget,可以看到a、b、c展示出了3中不不同的效果,这是由于a、b、c中对子Widget的对其方式的设置策略不同导致的,所以,在Flutter中为了在Row和Column中对子Widget进行更加明确的排列设置,我们可以通过mainAxisAlignment和crossAxisAlignment属性来控制Row和Column如何排列子Widget。对于Row,mainAxis指的是水平方向,crossAxis指的是垂直方向。对于Column,mainAxis指的是垂直方向,crossAxis指的是水平方向。比如像上图中a、b、c三种效果,分别对应给Row的crossAxisAlignment属性设置了top、center、bottom。
MainAxisAlignment和CrossAxisAlignment这两个类提供了各种用于控制对齐的常量,一般设置的时候我们会这样设置:
new Column( mainAxisAlignment: MainAxisAlignment.center, ... );
注意:当你将一张本地图片添加到你的项目中以后,你应该更新你项目的pubspec文件以让你的代码可以访问到你添加的图片,如果你引用的是网络图片的话就不用更新pubspec文件了。
在下面的例子中,每一张图片都是100px宽,而其容器(在这里是整个屏幕)的宽度是大于300px的,将mainAxis设置为spaceEvently后可以保证这3张图片中的每一张在之前、之间、之后将水平方向的剩余空间自由划分掉:
Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Image.asset('images/pic1.jpg'), Image.asset('images/pic2.jpg'), Image.asset('images/pic3.jpg'), ], );
效果图:
Column和Row具有相同的工作逻辑,下面的这3张图片,每一张的高度是100px,而其容器(在这里是整个屏幕)的高度大于300px,将mainAxis设置为spaceEvenly之后可以保证这3张图片中的每一张在之上、之间、之下将垂直方向的剩余空间自由划分掉:
Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Image.asset('images/pic1.jpg'), Image.asset('images/pic2.jpg'), Image.asset('images/pic3.jpg'), ], );
效果图:
设置Widget的尺寸
当一个layout的尺寸太大以至于超过了设备屏幕的尺寸,超出的屏幕边缘会出现黄色和黑色条纹图案,就像下面这样:
、在Row或者Column中可以使用Expanded 这个Widget来设置子Widget的尺寸,为了修复上图中图像尺寸比其容器尺寸还大的现象,我们可以使用Expanded来包裹每一个Image Widget,这样就可以保证这些图片平均分割可用空间而不至于尺寸太大导致出现上图尺寸越界的问题:
Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Image.asset('images/pic1.jpg'), ), Expanded( child: Image.asset('images/pic2.jpg'), ), Expanded( child: Image.asset('images/pic3.jpg'), ), ], );
效果如下:
有时你也许想让一个Widget占用的空间是其相邻Widget所占空间的两倍,这时,你可以使用Expanded Widget的flex属性,该属性的类型是一个整数类型,默认为1,表示当前Expanded Widget所占的比例。以下代码将中间图像的弹性系数设置为2:
Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Image.asset('images/pic1.jpg'), ), Expanded( flex: 2, child: Image.asset('images/pic2.jpg'), ), Expanded( child: Image.asset('images/pic3.jpg'), ), ], );
由于其他两个Expanded默认别的flex为1,所以最终的效果是中间图像的尺寸是两边图像的2倍:
使子Widget更加紧密
默认情况下Row或Column会尽可能在主轴方向占据尽可能多的可用空间,这样就会导致如果可用空间很大但是Row或Column中的子Widget很少时出现子Widget之间距离很大的情况(分布很分散),如果你希望让Row或Column中的子Widget们尽可能近地出现,可以将mainAxisSize属性设置为MainAxisSize.min,下面的示例通过此属性的设置将星形图标紧密地放置在了一起:
Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.star, color: Colors.green[500]), Icon(Icons.star, color: Colors.green[500]), Icon(Icons.star, color: Colors.green[500]), Icon(Icons.star, color: Colors.black), Icon(Icons.star, color: Colors.black), ], )
运行效果:
嵌套Row和Column
Flutter的布局框架允许你根据你的需要不限量地在Row/Column中嵌套Row/Column,让我们来看看下图中红色框圈出来的部分:
Screenshot of the pavlova app, with the ratings and icon rows outlined in red
红色框中的内容是由2个Row构成的,上面的Row Widget包含了5个星星和一个数字Reviews,对应的Widget树如下图所示:
对应的代码实现如下:
var stars = Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.star, color: Colors.green[500]), Icon(Icons.star, color: Colors.green[500]), Icon(Icons.star, color: Colors.green[500]), Icon(Icons.star, color: Colors.black), Icon(Icons.star, color: Colors.black), ], ); final ratings = Container( padding: EdgeInsets.all(20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ stars, Text( '170 Reviews', style: TextStyle( color: Colors.black, fontWeight: FontWeight.w800, fontFamily: 'Roboto', letterSpacing: 0.5, fontSize: 20, ), ), ], ), );
可以看到,主要的组成部分是5个Icon(用于展示星星)和一个Text(用于展示数字+Reviews)。
提示:为了避免由于布局的大量嵌套导致的布局代码混乱的问题,我们可以使用变量或者函数定义一些Widget,比如上面代码的ratings。
我们再来看下面的一行,该Row Widget包含了3个Column,每个Column都包含有一个icon和2行Text,对应的Widget树如下:
对应的实现代码如下:
final descTextStyle = TextStyle( color: Colors.black, fontWeight: FontWeight.w800, fontFamily: 'Roboto', letterSpacing: 0.5, fontSize: 18, height: 2, ); // 借助DefaultTextStyle.merge()来创建一个Text的主题样式 final iconList = DefaultTextStyle.merge( style: descTextStyle, child: Container( padding: EdgeInsets.all(20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Column( children: [ Icon(Icons.kitchen, color: Colors.green[500]), Text('PREP:'), Text('25 min'), ], ), Column( children: [ Icon(Icons.timer, color: Colors.green[500]), Text('COOK:'), Text('1 hr'), ], ), Column( children: [ Icon(Icons.restaurant, color: Colors.green[500]), Text('FEEDS:'), Text('4-6'), ], ), ], ), ), );
再看这张图:
整体是一个Row,左边是一个Column,该Column由一个标题文字、一个摘要文字、以及红色框中的2个Row组成,所以左边这个Column的整体代码如下:
final leftColumn = Container( padding: EdgeInsets.fromLTRB(20, 30, 20, 20), child: Column( children: [ titleText,//标题(Strawberry Paova) subTitle,//摘要文字(Pavlova is a....) ratings,//红色框中的第一个Row iconList,//红色框中的第二个Row ], ), );
然后就可以把左边的这个leftColumn放置在整个布局中了:
body: Center( child: Container( margin: EdgeInsets.fromLTRB(0, 40, 0, 30), height: 600, child: Card( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 440, child: leftColumn,//左边的Column ), mainImage,//右边的那个图像 ], ), ), ), ),
可以看到左边的Column被整个放置在了一个Container Widget中,这样做的目的是通过Container来设置该Column的宽度。
常用的布局Widget
Flutter有丰富的布局Widget库,下面是一些最常用的布局Widget。为了避免你被一整张布局Widget的清单吓到,这里只列举出了常见的布局Widget,目的是让你快速上手,如果你希望了解更多Flutter中的布局Widget,你可以阅读官方的Widget文档(地址:https://flutter.dev/docs/development/ui/widgets)。
以下小部件分为两类:来自Widget库的标准Widget和来自Material库的专用Widget。任何应用程序都可以使用标准的Widget库,但只有Material应用程序可以使用Material库中的Widget。
标准Widget
- Container:为一个Widget添加向小部件添加填充(padding)、页边距(margin)、边框(border)、背景色(background color)等
- GridView:在一个可滚动的网格中放置子Widget
- ListView:在一个可滚动的列表中放置子Widget
- Stack:将一个Widget重叠在另一个Widget之上
Material风格的Widget
- Card:将相关内容布局到具有圆角和阴影的框中。
- ListTile:最多将3行文本(text)以及可选的前导(leading)和尾随图标(trailing icon)布局到一行。
Container
许多布局都充分利用Container来的padding属性来添加填充,或者其border属性添加边框或利用margin属性添加页边距。你可以通过将整个布局放置到Container中并更改其背景色或背景图像来更改屏幕的背景。
Container总结
- 添加填充(padding)、页边距(margin)、边框(border)
- 改变背景色或者背景图片
- 包裹单个简单的Widget或者像Row、Column这样复杂的Widget,甚至包裹Widget树的根Widget
Container实例
下面的这个布局包含了一个Column,这个Column中包含了2个Row,每一个Row包含了2个Image,使用Container达到了将Column的背景颜色改为亮灰色的目的:
Widget _buildImageColumn() => Container( decoration: BoxDecoration( color: Colors.black26, ), child: Column( children: [ _buildImageRow(1), _buildImageRow(3), ], ), );
效果图如下:
同样也可以借助Container来实现为每个Image添加圆形边框和页边距:
Widget _buildDecoratedImage(int imageIndex) => Expanded( child: Container( decoration: BoxDecoration( border: Border.all(width: 10, color: Colors.black38), borderRadius: const BorderRadius.all(const Radius.circular(8)), ), margin: const EdgeInsets.all(4), child: Image.asset('images/pic$imageIndex.jpg'), ), ); Widget _buildImageRow(int imageIndex) => Row( children: [ _buildDecoratedImage(imageIndex), _buildDecoratedImage(imageIndex + 1), ], );
GridView
使用GridView可以构建一个二维的列表,GridView默认提供两个列,当然你也可以构建自己的自定义网格。当GridView检测到它的内容太长时,会自动进行滚动。
GridView总结
- 将Widget布局在网格中
- 检测列内容何时超出可见框,并自动提供滚动
- 构建自己的自定义网格,或者使用系统提供的网格:
- GridView.count允许你设置网格有多少个列
- GridView.extent允许你设置tile的最大像素
GridView实例
使用GridView.extent 创建一个tile最大为150px的网格:
Widget _buildGrid() => GridView.extent( maxCrossAxisExtent: 150, padding: const EdgeInsets.all(4), mainAxisSpacing: 4, crossAxisSpacing: 4, children: _buildGridTileList(30)); // The images are saved with names pic0.jpg, pic1.jpg...pic29.jpg. // The List.generate() constructor allows an easy way to create // a list when objects have a predictable naming pattern. List<Container> _buildGridTileList(int count) => List.generate( count, (i) => Container(child: Image.asset('images/pic$i.jpg')));
效果图:
ListView
ListView是一个用于显示多行的Widget,当内容多到其容器不能一次性显示完全的时候会自动提供滚动机制。
ListView总结
- 用于构建列表框的Widget
- 可以设置为水平或垂直滚动
- 当内容的尺寸比容器的尺寸大时会提供滚动机制
- 相比于Column配置更加简单,而且使用滚动机制会更方便
ListView实例
使用ListView显示一个企业列表:
Widget _buildList() => ListView( children: [ _tile('CineArts at the Empire', '85 W Portal Ave', Icons.theaters), _tile('The Castro Theater', '429 Castro St', Icons.theaters), _tile('Alamo Drafthouse Cinema', '2550 Mission St', Icons.theaters), _tile('Roxie Theater', '3117 16th St', Icons.theaters), _tile('United Artists Stonestown Twin', '501 Buckingham Way', Icons.theaters), _tile('AMC Metreon 16', '135 4th St #3000', Icons.theaters), Divider(), _tile('Kescaped_code#39;s Kitchen', '757 Monterey Blvd', Icons.restaurant), _tile('Emmyescaped_code#39;s Restaurant', '1923 Ocean Ave', Icons.restaurant), _tile( 'Chaiya Thai Restaurant', '272 Claremont Blvd', Icons.restaurant), _tile('La Ciccia', '291 30th St', Icons.restaurant), ], ); ListTile _tile(String title, String subtitle, IconData icon) => ListTile( title: Text(title, style: TextStyle( fontWeight: FontWeight.w500, fontSize: 20, )), subtitle: Text(subtitle), leading: Icon( icon, color: Colors.blue[500], ), );
对应效果图:
Stack
使用stack在基Widget(通常是Image)上排列Widget。Widget可以完全或部分地与基Widget重叠。
Stack总结
- 当一个Widget需要位于另一个Widget的上层时使用
- 子列表中的第一个Widget是基Widget,之后的子Widget覆盖在该基Widget的顶部。
- 堆栈的内容无法滚动
- 可以选择切割超出渲染框的子Widget
Stack实例
借助Stack实现将一个Container(在半透明黑色背景上显示text)覆盖在CircleAvatar的顶部:
Widget _buildStack() => Stack( alignment: const Alignment(0.6, 0.6), children: [ CircleAvatar( backgroundImage: AssetImage('images/pic.jpg'), radius: 100, ), Container( decoration: BoxDecoration( color: Colors.black45, ), child: Text( 'Mia B', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ], );
效果图:
Card
来自Material库的Card Widget用来包裹相关的信息块,被包裹的信息块可以由几乎任何Widget组成,但通常与ListTile一起使用。Card只有一个child属性,即被包裹的Widget,但它的这个child属性可以接收支持包含多个子Widget的Row、Column、ListView、GridView的Widget。默认情况下,如果不设置Card的child属性,即不被设置Card包裹的Widget,Card的大小将缩小到0 x 0像素,但可以使用sizedbox限制Card的大小。
在Flutter中,一个Card默认有轻微的圆角和阴影,这样使其具有3D效果。更改Card的Elevation属性可以控制阴影的效果。例如,将“elevation”设置为24,可以从视觉上将Card提升到离表面更远的位置,并使阴影变得更加分散。
Card总结
- Material Card的实现类
- 用于呈现相关信息块
- 接受单个子项,但该子项可以是Row、Column或其他包含子项列表的Widget
- 显示圆角和阴影
- Card中的内容无法滚动
- 来自于Material库
Card实例
使用一个SizedBox包裹的包含3个列表块的Card,Card中使用一个Divider将第1个和后2个列表块分割开:
Widget _buildCard() => SizedBox( height: 210, child: Card( child: Column( children: [ ListTile( title: Text('1625 Main Street', style: TextStyle(fontWeight: FontWeight.w500)), subtitle: Text('My City, CA 99984'), leading: Icon( Icons.restaurant_menu, color: Colors.blue[500], ), ), Divider(), ListTile( title: Text('(408) 555-1212', style: TextStyle(fontWeight: FontWeight.w500)), leading: Icon( Icons.contact_phone, color: Colors.blue[500], ), ), ListTile( title: Text('costa@example.com'), leading: Icon( Icons.contact_mail, color: Colors.blue[500], ), ), ], ), ), );
效果图:
ListTile
ListTile是一个Material库中的特殊行(Row),该Widget的特性是可以很容易地创建最多包括3行Text和可选引导(leading)、可选尾标(trailing icon)的widget。ListTile大多数情况下被用在Card或者ListView中。
ListTile总结
- 一个特殊的Row,可以包含最多3行Text以及引导、尾标
- 比Row更易配置,使用起来更简单
- 是Material库中的Widget
ListTile实例
上面Card中的实例就是赤裸裸的ListTile的使用:
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/17914.html