第一个flutter应用
flutter中布局等已经不再等同于web页面的开发方式了,首先我们需要引入material 包,那么什么是material呢?material是谷歌公司推行的一套设计风格,或者叫做设计语言,设计规范等
里面有很多规范,比如颜色文字排版,响应式动画与过度,填充等等
在Flutter中高度集成了 Material风格的 widget, 在我们应用中,我们可以直接使用这些Widget来创建我们的应用
-
第一个程序 Hello World
-
注意一个坑,flutter项目路径中包含中文会导致模拟器运行项目出错失败
-
import 'package:flutter/material.dart'; main(List<String> args){ runApp( Text('Hello World', textDirection:TextDirection.ltr ) ); }
以上代码展示了 Hello World 但是,样式颜色都没有去设置,文本所在位置也是在最左上端
我们需要对上面代码进行一些改进
-
import 'package:flutter/material.dart'; main(){ runApp( Center( child:Text( 'Hello World', textDirection:TextDirection.ltr, style: TextStyle(fontSize: 36) ) ); ); }
此时,HelloWorld 就在正中央显示了,并且字体大小也变成了36字号了
那么我们通过 runApp() 这个函数进行渲染,该函数传入的参数是一个 Widget。那么什么是widget呢
- 在ios或者android开发中,我们的界面又很多种类的划分:应用【application】视图控制器/活动/视图/按钮等
- 但是在Flutter开发中,这些东西都是不同的widget而已
- 也就是我们整个应用程序中所看到的内容几乎都是Widget,甚至连 内边距的设置,我们也需要使用一个叫Padding的Widget来做
Material库的使用
在开发中,我们并不需要从零去搭建结构,我们可以使用Material 库,直接使用其中的一些封装好的组件来完成一些结构的搭建
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('HelloWorld'),
),
body: Center(
child:Text(
'HelloWorld',
textDirection: TextDirection.ltr,
style: TextStyle(fontSize: 36),
)
)
),
)
);
}
我们要使用Material库 ,因此最外层包裹一层 MaterialApp
- 这意味我们整个应用都会采用MaterialApp风格的一些东西,方便我们对应的设计,并且我们使用了其中两个属性
- title: 这个是定义android 系统中打开多任务切换窗口时显示的标题
- home: 是该应用启动时显示的页面,我们传入一个Scaffold
那么 什么是 Scaffold ?
- 翻译过来是脚手架的意思
- 所以我们给MaterialApp 的home属性 传入了一个Scaffold 对象,作为启动显示的Widget
- Scaffold也有一些属性,比如appBar , 和 body
- appBar 是用于设计导航栏的,我们传入了一个title属性
- body是页面的内容部分,我们传入之前已经创建好的center中包裹的一个Text的Widget
进阶代码
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text(
'我是标题',
style: TextStyle(
fontSize: 20
),
),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Checkbox(
value: true,
onChanged: (value) {
print('HelloWorld');
},
),
Text(
'同意协议',
textDirection: TextDirection.ltr,
style: TextStyle(
fontSize: 30
),
)
],
),
),
),
)
);
}
上面代码有种回调地狱的感觉,那么flutter如此多的嵌套,该怎么解决这个问题
- flutter整个开发过程就是形成一个Widget树,所以形成嵌套是很正常的
- 关于Flutter的代码缩进,更多开发中我们使用的是2个空格
但是,我们开发这么一个简单的程序就出现这么多嵌套,如果程序复杂的话嵌套不是更加吓人,其实不必担忧,我们可以对我们的代码进行封装,将他们封装到自己的Widget中,创建自己的widget
如何创建自己的Widget ?
- StatelessWidget: 没有状态改变的Widget,通常这种Widget仅仅是做一些展示工作而已
- StatefulWidget: 需要保存状态,并且可能出现状态改变的Widget
class MyStatelessWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return <返回我们的Widget要渲染的Widget,比如一个Text Widget>;
}
}
如上:我们可以将嵌套的代码封装成一个个自定义的 Widget类 ,我们通过实例化这个类来解决嵌套过多的情况
build方法的解析
- Flutter 在拿到我们自己创建的StatelessWidget时,就会执行他的build方法
- 我们需要在build方法中告诉Flutter ,我们的Widget希望渲染什么元素,比如一个TextWidget
- StatelessWidget没办法主动去执行build方法,当我们使用的数据发生变化时候,build方法就会被重新执行
有状态的StatefulWidget
在开发中,某些Widget在一些情况下展示的数据不是一层不变的,而StatelessWidget 通常用来展示的数据都是不变的,如果数据变化的话我们就需要使用StatefulWidget
Flutter如何做到我们在开发中定义到Widget中的数据一定是final的呢
@immutable
abstract class Widget extends DiagnosticableTree {
// ...省略代码
}
这里有一个很关键的东西@immutable
- 我们似乎在Dart中没有见过这种语法,这实际上是一个
注解
,这设计到Dart的元编程,我们这里不展开讲; - 这里我就说明一下这个@immutable是干什么的;
- 说明: 被@immutable注解标明的类或者子类都必须是不可变的
- 结论:定义到Widget中的数据一定是不可变的,需要用final来修饰
如何存储Widget状态?
既然widget是不可变的,那么StatefulWidget如何来存储可变的状态呢?
- StatelessWidget无所谓,里的数据通常是直接定义完后不再修改
- 但是StatefulWidget 需要有状态(可以理解成变量)的改变,着如何做到呢
Flutter将StatefulWidget 设计成两个类
- 也就是你创建StatefulWidget 时,必须创建两个类
- 一个类继承自StatefulWidget ,作为Widget树的一部分
- 一个类继承自State,用于记录StatefulWidget 会变化的状态,并且根据状态的变化,构建出新的Widget
class MyStatefulWidget extends StatefulWidget {
@override
State<StatefulWidget> creatceState() {
// 将创建的State返回
return MyState();
}
}
class MyState extends State<MyStatefulWidget> {
@override
Widget build(BuildContext context) {
return <构建自己的Widget>;
}
}
按钮点击状态改变
我们想要见状态的改变,当状态改变时要修改count 变量
- 但是直接修改变量可以改变界面吗?不可以!【参照小程序,react】
- 这是因为Flutter并不知道我们的数据发生了改变,需要来重新构建我们界面中的Widget
如何可以让Flutter知道我们的状态发生改变了,重新构建我们呢的Widget呢?
- 我们需要调用一个State中默认给我们提供的setState方法
- 可以再其中的回调函数中修改我们的变量
onPressed: () {
setState(() {
counter++;
});
},
StatefulWidget生命周期
首先,执行 StatefulWidget 中相关方法:
- 1.执行StatefulWidget 的构造函数来创建StatefulWidget
- 2.执行initState ,我们通常会再这个方法中执行一些数据初始化的操作,或者也可能会发送网络请求
- 注意:这个方法是重写父类的方法,必须调用super,因为父类中会进行一些其他操作
- 并且如果你阅读源码,你会发现这里有一个注解(annotation):@mustCallSuper
- 3.执行didChangeDependencies 方法,这个方法在两种情况下会调用
- 情况一:调用initState 会调用
- 情况二:从其他对象中依赖一些数据发生变化时,比如前面我们呢提到的InheritedWidget
- 4.Flutter 执行build方法,来看一下我们当前的Widget需要渲染哪些Widget
- 5.当前的Widget 不再使用时,会带调用 dispose 进行销毁
- 6.手动调用setState 方法,会根据最新的状态(数据)来重新调用build方法,构建对于的Widgets
- 7.执行 didUpdateWidget 方法是再当父Widget 触发重建(rebuild)时,系统会调用didUpdateWidget方法
生命周期的复杂内容
- 1.mounted是State内部设置的一个属性,事实上我们不了解他也可以,如果你想深入了解它,会对State的机制理解更加清晰
- 很多资料没有提到整个属性,他是内部设置的,不需要我们手动进行修改
- 2.dirty state的含义是脏的State
- 它实际是通过一个Element的属性来标记的
- 将他标记为dirty 会等待下一次重绘,强制调用build 方法来构建我们的Widget
- clean state 的含义是 干净的State
- 它表示当前build出来的Widget,下一次重绘检查时不需要重新build
文本Widget
在Flutter中,我们可以将文本的控制显示分成两类
- 控制文本布局的参数:如文本对齐方式 textAlign 文本排版方向 textDirection 文本显示最大行数maxLines , 文本截断规则 overflow 等等, 这些都是构造函数中的 参数
- 控制文本样式的参数: 如字体名称 fontFamily 字体带下fontSize 文本颜色color 文本阴影 shadows等等,这些参数被统一封装到了构造函数中的参数style中
按钮Widget
Material widget 库中提供了多种按钮Widget 如 FloationgActionButton【蓝色圆形按钮】 / RaisedButton【灰色底色长条按钮】 / FlatButton【无边框无底色长条按钮】 / OutlineButton【有边框无底色长条按钮】
当然我们也可以自定义我们按钮的样式
RaisedButton(
child: Text("同意协议", style: TextStyle(color: Colors.white)),
color: Colors.orange, // 按钮的颜色
highlightColor: Colors.orange[700], // 按下去高亮颜色
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), // 圆角的实现
onPressed: () {
print("同意协议");
},
)
事实上这里还有一个比较常见的属性:elevation,用于控制阴影的大小,很多地方都会有这个属性
图片Widget
图片可以让我们应用更加丰富,Image组件有很多的构造函数,我们这里主要学习两个
- Image.asset 加载本地资源图片
- Image.network 加载网络中的图片
Image可以设置的属性:
const Image({
...
this.width, //图片的宽
this.height, //图片高度
this.color, //图片的混合色值
this.colorBlendMode, //混合模式
this.fit,//缩放模式
this.alignment = Alignment.center, //对齐方式
this.repeat = ImageRepeat.noRepeat, //重复方式
...
})
- width height : 用于设置图片的宽,高,当不指定宽高时,图片会根据当前父容器的限制,尽可能的显示其原始大小,如果只设置 width, height 的其中一个,那么另一个属性默认会按比例缩放,但是可以通过下面介绍的 fit 属性来指定适应规则
- fit:该属性用于在图片的显示空间和图片本身大小不同的时指定图片的适应模式。适应模式是在BoxFit中定义,它是一个枚举类型
fill
:会拉伸充满显示空间,图片本身长宽比会发生变化,图片会变形cover
: 会按图片的长度比放大后剧中填满显示空间,图片不会变形,超出显示空间部分会被裁剪contain
: 这是图片的默认适应规则,图片会在保证图片本身长宽比不变的情况下缩放以适应当前显示空间fitWidth
: 图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。fitHeight
:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁none
:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。color
和colorBlendMode
:在图片绘制时可以对每一个像素进行颜色混合处理,color
指定混合色,而colorBlendMode
指定混合模式;repeat
:当图片本身大小小于显示空间时,指定图片的重复规则。
我们对其中某些属性演练:
- 注意,这里我用了一个Container,大家可以将它理解成一个UIView 或者 View ,就是一个容器
- 后面会专门讲到整个组件使用
class MyHomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
child: Image.network(
"http://img0.dili360.com/ga/M01/48/3C/wKgBy1kj49qAMVd7ADKmuZ9jug8377.tub.jpg",
alignment: Alignment.topCenter,
repeat: ImageRepeat.repeatY,
color: Colors.red,
colorBlendMode: BlendMode.colorDodge,
),
width: 300,
height: 300,
color: Colors.yellow,
),
);
}
}
以上是网络图片的加载方式,本地图片加载需要麻烦一些。
- 首先要创建存放本地图片的文件夹,之后需要进行图片地址配置
- 在配置文件
pubspec.yaml
将注释 assets 解开,注意空格,下面跟上需要应用的本地图片地址 - 之后即可使用该地址的本地图片了
实现圆角头像
方式一: CircleAvatar
CircleAvatar可以实现圆角头像,也可以添加一个子 Widget
const CircleAvatar({
Key key,
this.child, // 子Widget
this.backgroundColor, // 背景颜色
this.backgroundImage, // 背景图像
this.foregroundColor, // 前景颜色
this.radius, // 半径
this.minRadius, // 最小半径
this.maxRadius, // 最大半径
})
实现一个圆形头像
- 注意一: 这里我们使用的是NetworkImage 因为 backgroundImage要求我们传入一个ImageProvider
- ImageProvider 是一个抽象类,事实上所有我们前面创建的Image对象都有包含image属性,该属性就是一个ImageProvider
- 注意二:这里我还在里面添加了一个文字,但是我在文字外层包裹了一个Container
- 这里Container 的作用是为了可以控制文字在其中的位置调整
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Center(
child: CircleAvatar(
radius: 80,
backgroundImage: NetworkImage(
"https://tva1.sinaimg.cn/large/006y8mN6gy1g7aa03bmfpj3069069mx8.jpg",
),
child: Container(
alignment: Alignment(0, .5),
width: 200,
height: 200,
child:Text("一只菜狗",style: TextStyle(color: Colors.white),),
),
),
);
}
}
方式二: ClipOval
ClipOval 也可以实现圆角头像,并且通常是在只有头像时使用
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Center(
child: ClipOval(
child: Image.network(
"https://tva1.sinaimg.cn/large/006y8mN6gy1g7aa03bmfpj3069069mx8.jpg",
width: 200,
height: 200,
color: Colors.green,
colorBlendMode: BlendMode.colorDodge,
),
),
);
}
}
方式三: Container+BoxDecoration
之后container时候补充
实现圆角图片
方式一: ClipRRect
ClipRRect 用于实现圆角效果,可以设置圆角的大小
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(40),
child: Image.network(
"https://tva1.sinaimg.cn/large/006y8mN6gy1g7aa03bmfpj3069069mx8.jpg",
width: 200,
height: 200,
),
),
);
}
}
方式二:Container+BoxDecoration
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(200),
image: DecorationImage(
image: NetworkImage("https://tva1.sinaimg.cn/large/006y8mN6gy1g7aa03bmfpj3069069mx8.jpg"),
)
),
),
);
}
}
表单Widget
TextField的使用
TextField用于接收用户的文本输入,它提供了非常多的属性,我们没有必要一个个去学习,很多时候用到某个功能时去查看是否包含某个属性即可, 以下学习几个比较常见的属性
- 一些属性比较简单:keyboardType 键盘类型,style 设置样式, textAlign 文本对齐方式,maxLength最大显示行数等等
- decoration :用于设置输入框相关样式
- icon: 设置左边显示的图标
- labelText: 在输入框上面显示一个提示的文c本
- hintText:显示提示的占位文字
- border:输入框的边框,默认底部有一个边框,可以通过 InputBorder.none删除掉
- filled: 是否填充输入框, 默认 false
- fillcolor : 输入框填充的颜色
- controller:
- onChanged:监听输入框内容的改变,传入一个回调函数
- onSubmitted: 点击键盘中右下角down时,会回调的一个函数
TextField的controller 使用
我们可以给TextField添加一个控制器,可以使用它设置文本的初始值,也可以使用它来监听文本的改变
事实上,如果我们没有为TextField提供一个Controller,那么Flutter会默认创建一个TextEditingController的,整个结论可以通过阅读源码得到
@override
void initState() {
super.initState();
// ...其他代码
if (widget.controller == null)
_controller = TextEditingController();
}
我们也可以自己创建一个Controller控制一些内容
注意: 本身TextFiled 我们常常会输入后改变一些状态,因此继承StatefulWidget比较妥当,因此如下
想要表单有默认值,那么我们在初始化数据的时候就要给这个表单默认值,而设置文本使用 controller这个控制器比较好,因此我们先创建变量final textEditingController = TextEditingController();
,之后再重写initState()方法,在里面对 textEditingController 实例对象的 text属性赋值即可
当然我们也可以在里面对文本框进行监听,这样我们就也可以实时拿到改变的值
class TextFieldWidget extends StatefulWidget {
@override
State<StatefulWidget> createState(){
return TextFieldState();
}
}
class TextFieldState extends State<TextFieldWidget> {
final textEditingController = TextEditingController();
@override
void initState() {
// TODO: implement initState
super.initState();
// 设置默认值
textEditingController.text = "Hello World";
textEditingController.addListener(() {
print("textEditingController:${textEditingController.text}");
});
}
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Center(
child: Container(
padding: EdgeInsets.all(20),
child: TextField(
style: TextStyle(color: Colors.white),
decoration: InputDecoration(
icon: Icon(Icons.ac_unit),
labelText:"UserName",
hintText: '请输入用户名',
hintStyle: TextStyle(color: Colors.white),
border: InputBorder.none,
labelStyle: TextStyle(color: Colors.white),
filled: true,
fillColor: Colors.lightBlue
),
onChanged: (val){
print("当前输入框的值:$val");
},
onSubmitted: (val){
print("当前提交的值:$val");
},
controller: textEditingController,
),
),
);
}
}
Form表单的使用
在我们开发注册/登录页面时,通常会有多个表单需要同时获取内容或者进行一些验证,如果对每一个TextField都分别进行验证,是一件非常麻烦的事情
Form 表单也是一个Widget ,可以在里面放入我们的输入框
但是Form表单中输入框必须是FormField类型的
- 我们查看刚刚学过的TextField是继承自 StatefulWidget ,并不是一个FormField类型
- 我们可以使用TextFormField 它的使用类似于TextField 并且是继承自FormField的
我们通过Form包裹实现一个注册界面
class FormStateWidget extends State<FormWidget>{
String user = "";
String passWord = "";
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
padding: EdgeInsets.all(20),
child: Center(
child: Form(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
decoration: InputDecoration(
icon: Icon(Icons.people),
labelText: "账号"
),
onChanged: (val) {
this.user = val;
},
),
TextFormField(
// 密码不显示
obscureText: true,
decoration: InputDecoration(
icon: Icon(Icons.lock),
labelText: "密码"
),
onChanged: (val) {
this.passWord = val;
},
),
SizedBox(
height: 20,
),
Container(
width: double.infinity,
child: RaisedButton(
color: Colors.green,
child: Text(
"注册",
style: TextStyle(color:Colors.white),
),
onPressed: () {
print("点击了注册,账号是:${this.user},密码是:${this.passWord}");
}
),
)
],
),
),
),
);
}
保存和获取表单数据
以上对数据的保存方式不是很妥当,这里其实有更妥当的处理方式
1.需要监听注册按钮的点击,在之前我们已经监听的onPressed传入回调函数即可。 (当然,如果嵌套太多,我们待会可以将它抽取到一个单独的方法中)
2.监听到按钮点击时,同时获取用户名和密码的表单信息
如何同时获取用户名和密码的表单信息?
-
如果我们调用 Form的State 对象的save方法,就会调用Form 中放入的
TextFormField
的onSave
回调TextFormField( decoration: InputDecoration( icon: Icon(Icons.people), labelText: "用户名或手机号" ), onSaved: (value) { print("用户名:$value"); }, ),
-
但是,我们有没有办法在点击按钮时,拿到Form对象,来调用它的save方法
知识点:在Flutter如何可以获取一个通过一个引用获取一个StatefulWidget的State对象呢、
答案:通过绑定一个GlobalKey即可
class FormDemo extends StatefulWidget {
@override
_FormState createState() => _FormState();
}
class _FormState extends State<FormDemo> {
final registerFormKey = GlobalKey<FormState>();
String userName;
String passWord;
void registerForm() {
registerFormKey.currentState.save();
registerFormKey.currentState.validate();
print("username:$userName password:$passWord");
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20),
child: Form(
key: registerFormKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
autovalidate: true,
decoration: InputDecoration(
icon: Icon(Icons.people),
labelText: "userName",
hintText: "请输入用户名",
),
style: TextStyle(color: Colors.blue),
onSaved: (val) {
this.userName = val;
print("我改变的值为:$val");
},
validator: (value) {
if(value.isEmpty){
return "账号不可为空";
}
return null;
},
),
SizedBox(height: 20,),
TextFormField(
autovalidate: true,
obscureText: true,
decoration: InputDecoration(
icon: Icon(Icons.lock),
labelText: "passWord",
hintText: "请输入密码",
),
style: TextStyle(color: Colors.blue),
onSaved: (val) {
this.passWord = val;
print("我改变的值为:$val");
},
validator: (value) {
if(value.isEmpty){
return "密码不可为空";
}else if(value.length < 6){
return "密码不可小于6位";
}
return null;
},
),
SizedBox(height: 20,),
Container(
width: double.infinity,
height: 40,
child: RaisedButton(
child: Text(
"注册",
style: TextStyle(color:Colors.white),
),
highlightColor: Colors.orange,
color: Colors.blue,
onPressed: registerForm,
),
)
],
),
),
);
}
}
验证填写的表单数据
在表单中,我们可以添加验证器,如果不符合某些特定的规则,那么给用户一定的提示信息,比如我们需要账号和密码有这样的规则:账号和密码都不能为空
按照如下步骤就可以完成整个验证过程:
- 为了TextFormField 添加validator 的回调函数
- 调用Form的State对象的validate 方法,就会回调validator传入函数
也可以为TextFormField添加一个属性:autovalidate 不需要调用validate方法,会自动验证是否符合要求
一.单子布局组件
Align介绍
看到Align
这个词,我们就知道它有我们的对齐方式有关。在其他端的开发中(iOS、Android、前端)Align通常只是一个属性而已,但是Flutter中Align也是一个组件。
// 源码
const Align({
Key key,
this.alignment: Alignment.center, // 对齐方式,默认居中对齐
this.widthFactor, // 宽度因子,不设置的情况,会尽可能大
this.heightFactor, // 高度因子,不设置的情况,会尽可能大
Widget child // 要布局的子Widget
})
- 因为子组件在父组件中的对齐方式必须有一个前提,就是父组件得知道自己的范围(宽度和高度)
- 如果
widthFactor
和heightFactor
不设置,那么默认Align会尽可能的大(尽可能的占据自己所在的父组件) - 我们也可以对他们进行设置,比如widthFactor 设置3,那么相对于Align的宽度是子组件跨度的3倍
class DemoWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
decoration: BoxDecoration(border: Border.all(color: Colors.red)),
child: Align(
alignment:Alignment.bottomRight ,
widthFactor: 3,
heightFactor: 3,
child: Icon(Icons.pets, color: Colors.red,),
),
);
}
}
Center介绍
Center组件我们在前面已经用过好多次了,事实上Center组件继承自Align,只是将alignment设置为Alignment.center
// 源码
class Center extends Align {
const Center({
Key key,
double widthFactor,
double heightFactor,
Widget child
}) : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}
因此我们可以将代码Align 换成Center ====》 代码不动,改掉Align,换成Center, alignment删掉即可
Padding介绍
Padding组件在其他端也是一个属性而已,在Flutter中是一个Widget,但是Flutter中没有Margin这样的Widget,这是因为外边距也可以通过padding来完成 Padding通常用于设置子Widget到父Widget的边距(你可以称之为是父组件的内边距或者子Widget的外边距)
// 源码分析
const Padding({
Key key,
@required this.padding, // EdgeInsetsGeometry类型(抽象类),使用EdgeInsets
Widget child,
})
padding演练
class MyHomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(20),
child: Text(
"莫听穿林打叶声,何妨吟啸且徐行。竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生。",
style: TextStyle(
color: Colors.redAccent,
fontSize: 18
),
),
);
}
}
Container介绍
// 源码分析
Container({
this.alignment,
this.padding, //容器内补白,属于decoration的装饰范围
Color color, // 背景色
Decoration decoration, // 背景装饰
Decoration foregroundDecoration, //前景装饰
double width,//容器的宽度
double height, //容器的高度
BoxConstraints constraints, //容器大小的限制条件
this.margin,//容器外补白,不属于decoration的装饰范围
this.transform, //变换
this.child,
})
大多数属性在介绍其他容器时候已经介绍,不再赘述,两点注意
- 容器的大小可以通过 width / height属性来指定,也可以通过 constraints来指定,如果同时存在时,width,height优先,实际上Container内部会根据width。height来生产一个constrains
- color 和 decoration 是互斥的,实际上当指定color时,Container会自动创建一个decoration
- decoration属性稍后详细学习
BoxDecoration
Container有一个非常重要的属性 decoration
- 他对应的类型Decoration类型,但是它是一个抽象类
- 在研发 中,我们经常使用它的实现类
BoxDecoration
来进行实例化
BoxDecoration 常见属性
const BoxDecoration({
this.color, // 颜色,会和Container中的color属性冲突
this.image, // 背景图片
this.border, // 边框,对应类型是Border类型,里面每一个边框使用BorderSide
this.borderRadius, // 圆角效果
this.boxShadow, // 阴影效果
this.gradient, // 渐变效果
this.backgroundBlendMode, // 背景混合
this.shape = BoxShape.rectangle, // 形变
})
注意: shape: BoxShape.circle, // 会和borderRadius冲突
class MyHomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
// color: Color.fromRGBO(3, 3, 255, .5),
width: 150,
height: 150,
child: Icon(Icons.pets, size: 32, color: Colors.white),
decoration: BoxDecoration(
color: Colors.amber, // 背景颜色
border: Border.all(
color: Colors.redAccent,
width: 3,
style: BorderStyle.solid
), // 这里也可以使用Border.all统一设置
// top: BorderSide(
// color: Colors.redAccent,
// width: 3,
// style: BorderStyle.solid
// ),
borderRadius: BorderRadius.circular(20), // 这里也可以使用.only分别设置
boxShadow: [
BoxShadow(
offset: Offset(5, 5),
color: Colors.purple,
blurRadius: 5
)
],
// shape: BoxShape.circle, // 会和borderRadius冲突
gradient: LinearGradient(
colors: [
Colors.green,
Colors.red
]
)
),
),
);
}
}
二.多子布局组件
在开发中,我们乘车将多个Widget 放在一起进行布局,比如水平方向,垂直方向,垂直方向排列,甚至有时候需要他们进行层叠,比如图片上面放一段文字等,这个时候我们呢就需要想到使用 多子布局组件(Multi-child layout widgets)比较常用的多子布局组件是Row.Column Stack 我们来学习一下他们的使用
Flex介绍
事实上,我们即将学习的Row组件和 Column组件都继承自Flex组件
- Flex组件和Row/Column属性主要的区别就是多了一个direction【方向】
- 当direction 的值为Axis.horizontal 的时候,则是Row
- 当direction 的值为Axis.vertical的时候,则是Column
在学习Row 和 Column 之前,我们先学习主轴和交叉轴的概念:
- 对于Row来说,主轴【MainAxis】 和 交叉轴 【CrossAxis】 分别是下图
- 对于Column来说,主轴(MainAxis)和交叉轴(CrossAxis)分别是下图
Row介绍
Row组件用于将所有的子Widget排成一行,实际上这种布局应该是借鉴由于Web的 Flex布局
// 源码
Row({
Key key,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, // 主轴对齐方式
MainAxisSize mainAxisSize = MainAxisSize.max, // 水平方向尽可能大
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, // 交叉处对齐方式
TextDirection textDirection, // 水平方向子widget的布局顺序(默认为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左))
VerticalDirection verticalDirection = VerticalDirection.down, // 表示Row纵轴(垂直)的对齐方向
TextBaseline textBaseline, // 如果上面是baseline对齐方式,那么选择什么模式(有两种可选)
List<Widget> children = const <Widget>[],
})
mainAxisSize:
- 表示Row在主轴(水平)方向占用的空间,默认是
MainAxisSize.max
,表示尽可能多的占用水平方向的空间,此时无论子widgets实际占用多少水平空间,Row的宽度始终等于水平方向的最大宽度 - 而
MainAxisSize.min
表示尽可能少的占用水平空间,当子widgets没有占满水平剩余空间,则Row的实际宽度等于所有子widgets占用的的水平空间;
mainAxisAlignment:表示子Widgets在Row所占用的水平空间内对齐方式
- 如果mainAxisSize值为
MainAxisSize.min
,则此属性无意义,因为子widgets的宽度等于Row的宽度 - 只有当mainAxisSize的值为
MainAxisSize.max
时,此属性才有意义 MainAxisAlignment.start
表示沿textDirection的初始方向对齐,- 如textDirection取值为
TextDirection.ltr
时,则MainAxisAlignment.start
表示左对齐,textDirection取值为TextDirection.rtl
时表示从右对齐。 - 而
MainAxisAlignment.end
和MainAxisAlignment.start
正好相反; MainAxisAlignment.center
表示居中对齐。
crossAxisAlignment:表示子Widgets在纵轴方向的对齐方式
Row的高度等于子Widgets中最高的子元素高度
它的取值和MainAxisAlignment一样(包含start
、end
、 center
三个值)
不同的是crossAxisAlignment的参考系是verticalDirection,即verticalDirection值为VerticalDirection.down
时crossAxisAlignment.start
指顶部对齐,verticalDirection值为VerticalDirection.up
时,crossAxisAlignment.start
指底部对齐;而crossAxisAlignment.end
和crossAxisAlignment.start
正好相反;
class MyHomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(color: Colors.red, width: 60, height: 60),
Container(color: Colors.blue, width: 80, height: 80),
Container(color: Colors.green, width: 70, height: 70),
Container(color: Colors.orange, width: 100, height: 100),
],
);
}
}
mainAxisSize
默认情况下,Row会尽可能占据多的宽度,让子Widget在其中进行排布,这是因为mainAxisSize属性默认值是MainAxisSize.max
。
TextBaseline 关于TextBaseline的取值解析
Expanded介绍
上面示例,如果我们呢希望红色和黄色的Container Widget 不要设置固定的宽度,而是占据剩余的部分,这个时候应该如何处理呢?
这个时候我们可以使用Expanded 来包裹Container Widget , 并且将他的宽度不设置值
- flex属性,弹性系数,Row会根据两个Expanded的弹性系数来决定他们呢占据剩下空间的比例
Column介绍
Column组件用于将所有的子Widget排成一列,学会了前面的Row后,Column只是和row 的方向不同而已,从源码可看出,Column属性 和 Row属性是一致的
Column({
Key key,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline textBaseline,
List<Widget> children = const <Widget>[],
})
Stack介绍
在开发中,我们多个组件很有可能需要重叠显示,比如在一张图片显示文字或者一个按钮等。 在Android 中可以使用Frame来实现,在Web端可以使用绝对定位,在Flutter中我们需要使用层叠布局 Stack
// 源码
Stack({
Key key,
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.overflow = Overflow.clip,
List<Widget> children = const <Widget>[],
})
参数解析:
- alignment:此参数决定如何去对齐没有定位(没有使用Positioned)或部分定位的子widget。所谓部分定位,在这里**特指没有在某一个轴上定位:**left、right为横轴,top、bottom为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。
- textDirection:和Row、Wrap的textDirection功能一样,都用于决定alignment对齐的参考系即:textDirection的值为
TextDirection.ltr
,则alignment的start
代表左,end
代表右;textDirection的值为TextDirection.rtl
,则alignment的start
代表右,end
代表左。 - fit:此参数用于决定没有定位的子widget如何去适应Stack的大小。
StackFit.loose
表示使用子widget的大小,StackFit.expand
表示扩伸到Stack的大小。 - overflow:此属性决定如何显示超出Stack显示空间的子widget,值为
Overflow.clip
时,超出部分会被剪裁(隐藏),值为Overflow.visible
时则不会。
Stack**演练 ** Stack会经常和Position一起来使用,Positioned 可以决定组件在Stack中的位置,用于实现类似于Web中的绝对定位效果 演练↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Stack(
children: <Widget>[
Container(
width: 200,
height: 200,
color: Colors.pink,
),
Positioned(
left: 10,
top: 10,
child: Icon(Icons.local_cafe,color:Colors.white,),
),
Positioned(
bottom:10,
right:10,
child:Text(
"CodeWang",
style: TextStyle(fontSize: 20, color: Colors.white, fontWeight: FontWeight.bold),
)
)
],
),
);
}
}
路由导航
MaterialApp 风格中怎么定义顶部导航
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
primaryColor: Colors.green,
),
title: "豆瓣影评",
home: Scaffold(
appBar: AppBar(centerTitle: true,title: Text("首页",style: TextStyle(fontSize:25),),
),
body: Center(child: Text("Hello World",style: TextStyle(fontSize: 30),),),
bottomNavigationBar: BottomNavWeight(),
),
);
}
}
class BottomNavWeight extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return BottomNavState();
}
}
class BottomNavState extends State<BottomNavWeight> {
var _currentIndex = 0;
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: _currentIndex,
items: [c
BottomNavigationBarItem(icon: Icon(Icons.home),title: Text("首页")),
BottomNavigationBarItem(icon: Icon(Icons.category),title: Text("分类"))
],
onTap: (index){
setState(()=> this._currentIndex = index);
},
);
}
}
- 首先肯定要先使用
MaterialApp Widget
, 他有很多属性,包括title
,home
, 以及设置主题的theme
,其中我们可以在theme
中设置我们的主题色, 通过parimaryColor
设置, 当然也可以关闭水波纹,以及点击出现的高亮效果, 分别通过highlightColor
和splashColor
将颜色设置为透明色transparent
- 之后再Home中传入 实例化的Scaffold Widget , 之后我们就可以 通过 bottomNavigationBar 属性传入一个自己封装好的 BottomNavigationBar widget 即可 具体参照上面的代码