Widget
简介
在Flutter中几乎所有的对象都是一个Widget。与原生开发中“控件”不同的是,Flutter中的Widget的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件,而原生开发中的控件通常只是指UI元素。
Widget 是 Flutter 功能的抽象描述,是视图的配置信息,同样也是数据的映射,是 Flutter 开发框架中最基本的概念。前端框架中常见的名词,比如视图(View)、视图控制器(View Controller)、活动(Activity)、应用(Application)、布局(Layout)等,在 Flutter 中都是 Widget。
Flutter将视图树的概念进行了扩展,把视图数据的组织和渲染分为三部分:
- Widget 对视图的一种结构化描述。
- Element 是Widget的一个实例化对象,承载了视图构建的上下文数据。
- RenderObject 是主要负责实现视图渲染的对象。
主要接口
- Widget 继承自DiagnosticableTree,主要作用是提供调试信息。
- Key 是决定是否在下一次build时复用旧的widget。
- createElement() 在构建UI树时,会先调用此方法生成对应节点的Element对象。
- debugFillProperties(…) 重写父类的方法,主要是设置诊断树的一些特性。
- canUpdate 用于在Widget树重新build时复用旧的widget。
分类
在Flutter开发中,我们一般都不用直接继承Widget类来实现一个新组件,通常会通过继承StatelessWidget或StatefulWidget来间接继承Widget类来实现。StatelessWidget和StatefulWidget都是直接继承自Widget类,而这两个类也正是Flutter中非常重要的两个抽象类,它们引入了两种Widget模型。
StatelessWidget
- StatelessWidget 继承自 Widget 类,并重写了 createElement() 方法。
- StatelessWidget 用于不需要维护状态的场景,通常在 build 方法中通过嵌套其它Widget来构建UI。
- build 方法有一个 context 参数,它是 BuildContext 类的一个实例,表示当前widget在widget树中的上下文。
Builder(builder: (context) {
// 在Widget树中向上查找最近的父级’Scaffold’widget
Scaffold scaffold = context.ancestorWidgetOfExactType(Scaffold);
// 返回AppBar的title
return (scaffold.appBar as AppBar).title;
}),
StatefulWidget
- StatefulWidget 也继承自 Widget 类,并重写了 createElement() 方法。
- StatefulElement 间接继承自 Element 类,与StatefulWidget 相对应。
- StatefulElement 中可能多次调用 createState() 来创建状态对象。
- createState() 用于创建和SatefulWidget相关的状态,在StatefulWidget的生命周期可能多次被调用。
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => new StatefulElement(this);
@protected
State createState();
}
State
- 一个 StatefulWidget 类会对应一个 State 类,State 表示与其对应的 StatefulWidget 要维护的状态。
- State 中保存的状态信息可以:
- 在 Widget 构建时,可以被同步读取。
- 在 Widget 生命周期中可以被改变,当 State 被改变时,可以手动调用 setState() 方法通知 Flutter framework 状态发生改变,重新调用 build 方法构建 Widget 树,达到更新UI的目的。
- State 中的两个常用属性:
- widget ,它表示与该 State 实例关联的 Widget 实例。
- context ,StatefulWidget 对应的 BuildContext ,作用与 StatelessWidget 的BuildContext 相同。
案例
// StatelessWidget
class MyStatelessWidget extends StatelessWidget {
final String title;
final String imgUrl;
const MyStatelessWidget({Key key, this.title, this.imgUrl}) : super(key: key);
@override
Widget build(BuildContext context) {
return MyStatefulWidget(
width: 10,
height: 100,
color: Colors.white,
);
;
}
}
// StatefulWidget
class MyStatefulWidget extends StatefulWidget {
num width;
num height;
Color color;
MyStatefulWidget({Key key, this.width, this.height, this.color})
: assert(width != null),
assert(height != null),
assert(color != null),
super(key: key);
@override
State<MyStatefulWidget> createState() => MyStatefulWidgetState();
}
class MyStatefulWidgetState extends State<MyStatefulWidget> {
@override
Widget build(BuildContext context) {
print(widget.width);
return Text('文本组件');
}
}
// 快速生成的方式
// stl
class MyMy extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
// stf
class MyMyMy extends StatefulWidget {
@override
_MyMyMyState createState() => _MyMyMyState();
}
class _MyMyMyState extends State<MyMyMy> {
@override
Widget build(BuildContext context) {
return Container();
}
}
MaterialApp
内置组件库
Material风格组件库
它可以帮助我们构建遵循Material Design设计规范的应用程序, 应用程序以MaterialApp 组件开始, 该组件在应用程序的根部创建了一些必要的组件。
组件:MaterialApp、Scaffold etc
Cupertino风格组件库
Flutter也提供了一套丰富的Cupertino风格的组件,尽管目前还没有Material 组件那么丰富,但是它仍在不断的完善中。
组件:CupertinoApp etc
MaterialApp组件
字段 | 类型 | 描述 |
---|---|---|
home | Widget | 应用入口主页 |
title | String | 应用的标题 |
color | Color | 应用程序使用的主色 |
theme | ThemeData | 应用程序使用的主题信息 |
routes | Map<String, WidgetBuilder> | 应用程序顶级路由表 |
initialRoute | String | 应用程序初始路由 |
onUnknownRoute | RouteFactory | 未知路由生成器 |
debugShowCheckedModeBanner | bool | 调试模式是否显示debug banner |
etc. |
Scaffold
一个完整的路由页可能会包含导航栏、抽屉菜单(Drawer)以及底部导航菜单等。如果每个路由页面都需要开发者自己手动去实现这些,这会是一件非常麻烦且无聊的事。幸运的是,Flutter Material组件库提供了一些现成的组件来减少我们的开发任务。Scaffold是一个路由页的骨架,我们使用它可以很容易地拼装出一个完整的页面。
布局
该组件实现了基本的Material Design布局结构:
字段 | 类型 | 描述 |
---|---|---|
appBar | PreferredSizeWidget | 界面顶部AppBar |
body | Widget | 界面所显示的主要内容 |
floatingActionButton | Widget | 界面主要功能按钮 |
drawer | Widget | 左边侧边栏抽屉控件 |
endDrawer | Widget | 右边侧边栏抽屉控件 |
backgroundColor | Color | 背景颜色 |
bottomNavigationBar | Widget | 页面底部导航栏 |
AppBar
AppBar是一个Material风格的导航栏,通过它可以设置导航栏标题、导航栏菜单、导航栏底部的Tab标题等。
AppBar({
Key key,
this.leading, //导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。
this.automaticallyImplyLeading = true, //如果leading为null,是否自动实现默认的leading按钮
this.title,// 页面标题
this.actions, // 导航栏右侧菜单
this.bottom, // 导航栏底部菜单,通常为Tab按钮组
this.elevation = 4.0, // 导航栏阴影
this.centerTitle, //标题是否居中
this.backgroundColor,
... //其它属性见源码注释
})
FloatingActionButton
FloatingActionButton是Material设计规范中的一种特殊Button,通常悬浮在页面的某一个位置作为某种常用动作的快捷入口。
通过Scaffold的bottomNavigationBar属性来设置底部导航。
bottomNavigationBar: BottomAppBar(
color: Colors.white,
shape: CircularNotchedRectangle(), // 底部导航栏打一个圆形的洞
child: Row(
children: [
IconButton(icon: Icon(Icons.home)),
SizedBox(), //中间位置空出
IconButton(icon: Icon(Icons.business)),
],
mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部导航栏横向空间
),
),
floatingActionButton: FloatingActionButton(
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
),
TabBar
我们可以通过AppBar的“bottom”属性来添加一个导航栏底部Tab按钮组
const TabBar({
Key key,
@required this.tabs,//显示的标签内容,一般使用Tab对象,也可以是其他的Widget
this.controller,//TabController对象
this.isScrollable = false,//是否可滚动
this.indicatorColor,//指示器颜色
this.indicatorWeight = 2.0,//指示器高度
this.indicatorPadding = EdgeInsets.zero,//底部指示器的Padding
this.indicator,//指示器decoration,例如边框等
this.indicatorSize,//指示器大小计算方式,TabBarIndicatorSize.label跟文字等宽,TabBarIndicatorSize.tab跟每个tab等宽
this.labelColor,//选中label颜色
this.labelStyle,//选中label的Style
this.labelPadding,//每个label的padding值
this.unselectedLabelColor,//未选中label颜色
this.unselectedLabelStyle,//未选中label的Style
}) : assert(tabs != null),
assert(isScrollable != null),
assert(indicator != null || (indicatorWeight != null && indicatorWeight > 0.0)),
assert(indicator != null || (indicatorPadding != null)),
super(key: key);
TabBarView
Material库提供了一个TabBarView组件,通过它不仅可以轻松的实现Tab页,而且可以非常容易的配合TabBar来实现同步切换和滑动状态同步
const TabBarView({
Key key,
@required this.children,
this.controller,
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
}) : assert(children != null),
assert(dragStartBehavior != null),
super(key: key);
Drawer
Scaffold的drawer和endDrawer属性可以分别接受一个Widget来作为页面的左、右抽屉菜单。
const Drawer({
Key key,
this.elevation = 16.0,
this.child,
this.semanticLabel,
}) : assert(elevation != null && elevation >= 0.0),
super(key: key);
yaml文件
简介
YAML是一个类似 XML、JSON 的标记性语言。YAML 强调以数据为中心,并不是以标识语言为重点。因而 YAML 本身的定义比较简单,号称“一种人性化的数据格式语言”。在 Dart 中,库和应用都属于包。pubspec.yaml 是包的配置文件,包含了包的元数据(比如,包的名称和版本)、运行环境(也就是 Dart SDK 与 Fluter SDK 版本)、外部依赖、内部配置(比如,资源管理)。
规范
- 大小写敏感
- 缩进代表层级,使用空格,默认2个空格
#
表示注释内容- : 表示键值对,注意后面要空格
- {} 表示键值表
-
表示列表,注意后面要空格- [] 表示数组,注意每项之间有空格
- …
pubpec.yaml
name: demo16 # 包名
description: A new Flutter project. # 描述
version: 1.0.0+1 # 版本号
environment: # dart sdk版本
sdk: ">=2.7.0 <3.0.0"
dependencies: # 依赖管理
flutter:
sdk: flutter
cupertino_icons: ^0.1.3
dev_dependencies: # 开发依赖管理
flutter_test:
sdk: flutter
flutter: # flutter 资源管理
uses-material-design: true # 使用 Material图标
assets: # 声明图片资源
- images/a_dot_burr.jpeg
- images/a_dot_ham.jpeg
fonts:
- family: Schyler
fonts:
- asset: fonts/Schyler-Regular.ttf
- asset: fonts/Schyler-Italic.ttf
style: italic
- family: Trajan Pro
fonts:
- asset: fonts/TrajanPro.ttf
- asset: fonts/TrajanPro_Bold.ttf
weight: 700
静态资源管理
在移动开发中,资源文件都会被打包到 App 安装包中,而 App 中的代码可以在运行时访问这些资源。
- iOS 使用 Images.xcassets 来管理图片,其他的资源直接拖进工程项目即可;
- Android 的资源管理粒度则更为细致,使用以 drawable+ 分辨率命名的文件夹来分别存放不同分辨率的图片,其他类型的资源也都有各自的存放方式;
- 在 Flutter 中,资源管理则简单得多:资源(assets)可以是任意类型的文件,比如 JSON 配置文件或是字体文件等,而不仅仅是图片。
资源声明
- 使用根目录下的 pubspec.yaml 文件,对资源的所在位置进行显式声明;
- 可以对每一个文件进行挨个指定;
- 也可以采用子目录批量指定的方式;
- 目录批量指定并不递归,只有在该目录下的文件才可以被包括。
flutter:
assets:
- assets/background.jpg #挨个指定资源路径
- assets/loading.gif #挨个指定资源路径
- assets/result.json #挨个指定资源路径
- assets/icons/ #子目录批量指定
- assets/ #根目录也是可以批量指定的
图片的像素密度管理
与 Android、iOS 开发类似,Flutter 也遵循了基于像素密度的管理方式。
如果想适配不同的分辨率,将其他分辨率的图片放到对应的分辨率子目录中。
assets
├── background.jpg //1.0x图
├── 2.0x
│ └── background.jpg //2.0x图
└── 3.0x
└── background.jpg //3.0x图
而在 pubspec.yaml 文件声明这个图片资源时,仅声明 1.0x 图资源即可
flutter:
assets:
- assets/background.jpg #1.0x图资源
字体资源
- 使用自定义字体同样需要在 pubspec.yaml 文件中提前声明。
- 除了正常字体文件外,如果你的应用需要支持粗体和斜体,同样也需要有对应的粗体和斜体字体文件。
- 在将 RobotoCondensed 字体放至 assets 目录下的 fonts 子目录下。
fonts:
- family: RobotoCondensed #字体名字
fonts:
- asset: assets/fonts/RobotoCondensed-Regular.ttf #普通字体
- asset: assets/fonts/RobotoCondensed-Italic.ttf
style: italic #斜体
- asset: assets/fonts/RobotoCondensed-Bold.ttf
weight: 700 #粗体
包管理
Dart 提供了包管理工具 Pub,用来管理代码和资源。从本质上说,包(package)实际上就是一个包含了 pubspec.yaml 文件的目录,其内部可以包含代码、资源、脚本、测试和文档等文件。包中包含了需要被外部依赖的功能抽象,也可以依赖其他包。
包版本管理
- 对于包,我们通常是指定版本区间,而很少直接指定特定版本。
- 对于运行环境,建议将Dart和Flutter的SDK环境写死,统一团队的开发环境。
environment:
sdk: 2.8.4
flutter: 1.17.5
#Flutter依赖库
dependencies:
flutter:
sdk: flutter
cupertino_icons: ">0.1.1"
第三方包引用
- 基于版本的方式引用第三方包,需要在其 Pub 上进行公开发布。
- 不对外公开发布或者目前处于开发调试阶段的包,我们需要设置数据源。
- 使用本地路径或 Git 地址的方式设置数据源进行包的声明。
- 在完成了依赖包的下载后,Pub 会在根目录下创建.packages 文件。
dependencies:
package1:
path: ../package1/ #路径依赖
date_format:
git:
url: https://github.com/xxx/package2.git #git依赖
包中的资源依赖
除了提供功能和代码的依赖之外,包还可以提供资源的依赖。
pubspec.yaml 文件已经声明了同样资源的情况下,可以复用依赖包中的资源。
pubspec.yaml
└──assets
├──2.0x
│ └── placeholder.png
└──3.0x
└── placeholder.png
通过 Image 和 AssetImage 提供的 package 参数加载图像。
Image.asset('assets/placeholder.png', package: 'package4');
AssetImage('assets/placeholder.png', package: 'package4');
文本组件
单行文本
Text用于显示简单样式文本,它包含一些控制文本显示样式的一些属性:
属性名 | 类型 | 描述 |
---|---|---|
data | String | 组件中的文字 |
textAlign | TextAlign | 组件中文本对齐方式 |
textDirection | TextDirection | 组件中文字方向 |
softWrap | bool | 组件中文本是否自动换行 |
maxLines | int | 组件中文本最大行数 |
overflow | TextOverflow | 文本超出屏幕宽度时截取方式 |
textScaleFactor | double | 组件中字体显示倍率 |
style | TextStyle | 定义文字样式 |
-
TextStyle用于指定文本显示的样式如颜色、字体、粗细、背景等
属性名 类型 color 字体颜色 fontSize 字体大小 fontWeight 字重(粗细) fontStyle 倾斜 letterSpacing 字母间隙 wordSpacing 单词间隙 fontFamily 字体
多段文本
-
富文本:包含多段文本片段
RichText // 组件 Text.rich() // 构造函数
-
文本片段 TextSpan
字段 类型 描述 TextStyle style 设置文本片段的样式 String text 设置文本片段的内容 List TextSpan 设置文本多段文本片段 GestureRecognizer recognizer 设置文本片段的手势识别处理 -
案例
// 单行文本 RichText( text: TextSpan( children: [ TextSpan(text:'黑色',style: TextStyle(color: Colors.black)), TextSpan(text:'粉色',style: TextStyle(color: Colors.pink)), ] ), )
-
文本点击
class _MyAppState extends State<MyApp> { TapGestureRecognizer _recognizer = TapGestureRecognizer(); @override void initState() { super.initState(); // 监听识别 _recognizer.onTap = (){ print('111'); }; } // ... RichText( text: TextSpan(children: [ TextSpan(text: '222', style: TextStyle(color: Colors.black)), TextSpan( text: '111', style: TextStyle(color: Colors.pink), recognizer: _recognizer, // 设置文本片段的手势识别处理 ), ]), ),
默认文本样式
文本的样式默认是可以被继承的,因此,如果在Widget树的某一个节点处设置一个默认的文本样式,那么该节点的子树中所有文本都会默认使用这个样式,而DefaultTextStyle正是用于设置默认文本样式的。
DefaultTextStyle(
// 1.设置文本默认样式
style: TextStyle(
color:Colors.red,
fontSize: 20.0,
),
textAlign: TextAlign.start,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("hello world"),
Text("I am Jack"),
Text("I am Jack",
style: TextStyle(
inherit: false, // 2.不继承默认样式
color: Colors.grey
),
),
],
),
)
图片组件
ImageProvider
ImageProvider 是一个抽象类,主要定义了图片数据获取的接口load(),从不同的数据源获取图片需要实现不同的ImageProvider ,如AssetImage是实现了从Asset中加载图片的ImageProvider,而NetworkImage实现了从网络加载图片的ImageProvider。
本地图片
直接使用Image的构造函数加载
Image(
image: AssetImage("images/avatar.png"),
width: 100.0
);
使用命名构造函数 asset 加载
Image.asset("images/avatar.png",
width: 100.0,
)
网络图片
可以直接使用NetworkImage构造函数加载网络图片
Image(
image: NetworkImage(
"https://www.xxx.com/xxx.png"),
width: 100.0,
)
使用Image.network加载网络图片
Image.network(
" https://www.xxx.com/xxx.png",
width: 100.0,
)
图标组件
MaterialIcons
使用图标就像使用文本一样,但是这种方式需要我们提供每个图标的码点,这并对开发者不友好,所以,Flutter封装了IconData和Icon来专门显示字体图标。
String icons = "";
// accessible:  or 0xE914 or E914
icons += "\uE914";
// error:  or 0xE000 or E000
icons += " \uE000";
// fingerprint:  or 0xE90D or E90D
icons += " \uE90D";
Text(icons,
style: TextStyle(
fontFamily: "MaterialIcons",
fontSize: 24.0,
color: Colors.green
),
);
Icon组件
Icons类中包含了所有Material Design图标的IconData静态变量定义。
图标地址: https://material.io/resources/icons/?style=baseline
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.accessible,color: Colors.green,),
Icon(Icons.error,color: Colors.green,),
Icon(Icons.fingerprint,color: Colors.green,),
],
)
自定义字体图标
我们也可以使用自定义字体图标(阿里图标),在Flutter中,我们使用ttf格式即可。
步骤:
-
导入字体
fonts: - family: MyIcons #字体名字 fonts: - asset: assets/fonts/iconfont/iconfont.ttf #普通字体
-
然后定义一个MyIcons类,功能和Icons类一样
-
将字体文件中的所有图标都定义成静态变量。
class MyIcons{ // book 图标 static const IconData book = const IconData( 0xe614, fontFamily: 'myIcon', matchTextDirection: true ); ... }
按钮组件
Material 组件库中提供了多种按钮组件如RaisedButton、FlatButton、OutlineButton等,它们都是直接或间接对RawMaterialButton组件的包装定制,所以他们大多数属性都和RawMaterialButton一样。按钮外观可以通过其属性来定义,不同按钮属性大同小异,所以我们还可以自定义按钮的外观。
文本按钮
-
RaisedButton 它默认带有阴影和灰色背景,按下后,阴影会变大
RaisedButton( child: Text("normal"), onPressed: () {}, );
-
FlatButton即扁平按钮,默认背景透明并不带阴影,按下后,会有背景色
FlatButton( child: Text("normal"), onPressed: () {}, )
-
OutlineButton默认有一个边框,不带阴影且背景透明。按下后,边框颜色会变亮、同时出现背景和阴影
OutlineButton( child: Text("normal"), onPressed: () {}, )
-
IconButton是一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景。
IconButton( icon: Icon(Icons.thumb_up), onPressed: () {}, )
图标按钮
RaisedButton、FlatButton、OutlineButton都有一个icon 构造函数,通过它可以轻松创建带图标的按钮。
RaisedButton.icon(
icon: Icon(Icons.send),
label: Text("发送"),
onPressed: _onPressed,
),
OutlineButton.icon(
icon: Icon(Icons.add),
label: Text("添加"),
onPressed: _onPressed,
),
FlatButton.icon(
icon: Icon(Icons.info),
label: Text("详情"),
onPressed: _onPressed,
)
自定义按钮
RaisedButton({
Key key,
//点击按钮的回调出发事件
@required VoidCallback onPressed,
//水波纹高亮变化回调
ValueChanged<bool> onHighlightChanged,
//按钮的样式(文字颜色、按钮的最小大小,内边距以及shape)[ Used with [ButtonTheme] and [ButtonThemeData] to define a button's base
//colors, and the defaults for the button's minimum size, internal padding,and shape.]
ButtonTextTheme textTheme,
//文字颜色
Color textColor,
//按钮被禁用时的文字颜色
Color disabledTextColor,
//按钮的颜色
Color color,
//按钮被禁用时的颜色
Color disabledColor,
//按钮的水波纹亮起的颜色
Color highlightColor,
//水波纹的颜色
Color splashColor,
//按钮主题高亮
Brightness colorBrightness,
//按钮下面的阴影长度
double elevation,
//按钮高亮时的下面的阴影长度
double highlightElevation,
double disabledElevation,
EdgeInsetsGeometry padding,
ShapeBorder shape,
Clip clipBehavior = Clip.none,
MaterialTapTargetSize materialTapTargetSize,
Duration animationDuration,
Widget child,
}
圆角按键
FlatButton(
child: Text("Submit"),
shape:RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
onPressed: () {},
)
表单控件
输入框组件
TextField用于文本输入,它提供了很多属性,右侧是TextField的构造函数
主要属性
- controller 通过它可以设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件。
- focusNode 用于控制TextField是否占有当前键盘的输入焦点。
- InputDecoration 用于控制TextField的外观显示。
- keyboardType 用于设置该输入框默认的键盘输入类型。
- textInputAction 键盘动作按钮图标(即回车键位图标)。
- style 正在编辑的文本样式。
- textAlign 输入框内编辑文本在水平方向的对齐方式。
- autofocus 是否自动获取焦点。
- obscureText 是否隐藏正在编辑的文本,文本内容会用“•”替换。
- maxLines 输入框的最大行数,默认为1;如果为null,则无行数限制。
- maxLength和maxLengthEnforced 输入框文本的最大长度及超出长度后是否阻止输入。
- onChange 输入框内容改变时的回调函数。
- onEditingComplete和onSubmitted 在输入框输入完成时触发。
- inputFormatters 当用户输入内容改变时,会根据指定的格式来校验。
- enable 是否可用。
- cursorWidth、cursorRadius和cursorColor 用于自定义输入框光标样式。
- validator 验证内容函数返回提醒文本或null
- decoration 文本标题、提醒文本、图标设置
decoration实例
TextField(
decoration: InputDecoration(
labelText: '账号',
hintText: '请输入账号',
icon: Icon(Icons.person),
),
),
TextField(
obscureText: true, // 隐藏文本用*代替
decoration: InputDecoration(
labelText: '密码',
hintText: '请输入密码',
icon: Icon(Icons.lock),
),
),
内容改变回调
// 方法一
TextField(
// 输入框内容改变时的回调函数
onChanged: (value) {
setState(() {
_username = value;
});
},
),
// 方法二
TextEditingController _controller1 = TextEditingController();
// 定义input绑定对象,也可以用于设置值 _controller1.text = xxx
@override
void initState() {
super.initState();
// 监听输入状态
_controller1.addListener(() {
setState(() {
_username = _controller1.text;
});
});
}
TextField(
controller: _controller1, // 绑定对象
),
焦点方法
FocusNode _focusNode1 = FocusNode(); // 焦点对象
TextField(
focusNode: _focusNode1, // 绑定焦点对象
),
// 监听焦点
@override
void initState() {
super.initState();
// 监听焦点
_focusNode2.addListener(() {
// 焦点判断,返回布尔值是否聚焦
print(_focusNode1.hasFocus );
});
}
// 触发方法
FocusScope.of(context).requestFocus(_focusNode1);
表单组件
Form
Form的子孙元素必须是FormField类型,FormField是一个抽象类,定义几个属性,FormState内部通过它们来完成操作。
TextFormField组件,它继承自FormField类,也是TextField的一个包装类,所以除了FormField定义的属性之外,它还包括TextField的属性。
Form({
@required Widget child,
bool autovalidate = false,
WillPopCallback onWillPop,
VoidCallback onChanged,
})
FormField
Form的子孙元素必须是FormField类型,FormField是一个抽象类,定义几个属性,FormState内部通过它们来完成操作。
TextFormField组件,它继承自FormField类,也是TextField的一个包装类,所以除了FormField定义的属性之外,它还包括TextField的属性。
const FormField({
...
FormFieldSetter<T> onSaved, //保存回调
FormFieldValidator<T> validator, //验证回调
T initialValue, //初始值
bool autovalidate = false, //是否自动校验。
})
FormState
- FormState.validate() 调用此方法后,会调用Form子孙FormField的validate回调,如果有一个校验失败,则返回false,所有校验失败项都会返回用户返回的错误提示。
- FormState.save() 调用此方法后,会调用Form子孙FormField的save回调,用于保存表单内容。
- FormState.reset() 调用此方法后,会将子孙FormField的内容清空。
TextEditingController _unameController = TextEditingController();
TextEditingController _upassController = TextEditingController();
GlobalKey _formKey = GlobalKey<FormState>();
Form(
key: _formKey, // 绑定表单key
child: Column(
children: <Widget>[
TextFormField(
controller: _unameController,
decoration: InputDecoration(
labelText: '用户名',
hintText: '请输入用户名',
icon: Icon(Icons.person),
),
validator: (v) => v.trim().length > 0 ? null : '用户名不能为空',
),
TextFormField(
obscureText: true,
controller: _upassController,
decoration: InputDecoration(
labelText: '密码',
hintText: '请输入密码',
icon: Icon(Icons.lock),
),
validator: (v) => v.trim().length > 0 ? null : '密码不能为空',
),
RaisedButton(
child: Text('登陆'),
onPressed: (){
// 返回检验结果布尔值
print((_formKey.currentState as FormState).validate());
},
),
],
)
),
单选框 / 复选框
Material 组件库中提供了Material风格的单选开关Switch和复选框Checkbox,虽然它们都是继承自StatefulWidget,但它们本身不会保存当前选中状态,选中状态都是由父组件来管理的。当Switch或Checkbox被点击时,会触发它们的onChanged回调,我们可以在此回调中处理选中状态改变逻辑。
- 由于需要维护Switch和Checkbox的选中状态,所以需要继承自StatefulWidget
- 在其build方法中分别构建了一个Switch和Checkbox,初始状态都为选中状态,当用户点击时,会将状态置反,然后回调用setState()通知Flutter framework重新构建UI
Switch
bool _switchSelected = false;
Switch(
activeColor: Colors.green, // 激活颜色
value: _switchSelected, // 当前状态
onChanged: (v) {
// 重构页面
setState(() {
_switchSelected = v;
});
},
),
Checkbox
bool _checkboxSelectd = true;
Checkbox(
value: _checkboxSelected,
onChanged: (v) {
setState(() {
_checkboxSelected = v;
});
},
activeColor: Colors.red,
tristate: true, // 增加为三态
)
进度指示器
Material 组件库中提供了两种进度指示器:LinearProgressIndicator和CircularProgressIndicator,它们都可以同时用于精确的进度指示和模糊的进度指示。
- value表示当前的进度,取值范围为[0,1],不设置就会渲染成模糊进度指示器
- backgroundColor:指示器的背景色
- valueColor: 指示器的进度条颜色
- strokeWidth 表示圆形进度条的粗细
LinearProgressIndicator
LinearProgressIndicator是一个线性、条状的进度条
LinearProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.red),
semanticsLabel: '提示文本',
),
CircularProgressIndicator
CircularProgressIndicator是一个圆形进度条
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Colors.pink),
strokeWidth: 5.0,
),
布局组件
简介
布局类组件都会包含一个或多个子组件,不同的布局类组件对子组件排版方式不同。我们在前面说过Element树才是绘制树,Element树是通过Widget树来创建的(通过Widget.createElement()),Widget其实就是Element的配置数据。
根据Widget是否需要包含子节点将Widget分为了三类。
Widget | Element | 用途 |
---|---|---|
LeafRenderObjectWidget | LeafRenderObjectElement | Widget树的叶子节点,用于没有子节点的Widget,通常基础组件都属于这一类,如Image。 |
SingleChildRenderObjectWidget | SingleChildRenderObjectElement | 包含一个子Widget,如ConstrainedBox、DecoratedBox等。 |
MultiChildRenderObjectWidget | MultiChildRenderObjectElement | 包含多个子Widget,一般有一个children参数,接收一个Widget数组,如Row、Column、Stack等。 |
线性布局
所谓线性布局,即指沿水平或垂直方向排布子组件。Flutter中通过Row和Column来实现线性布局,类似于Android中的LinearLayout控件。
对于线性布局,有主轴和纵轴之分,如果布局是沿水平方向,那么主轴就是指水平方向,而纵轴即垂直方向;如果布局沿垂直方向,那么主轴就是指垂直方向,而纵轴就是水平方向。
Row可以在水平方向排列其子widget。Column可以在垂直方向排列其子组件。
主要属性
- textDirection 表示水平方向子组件的布局顺序
- mainAxisSize 表示主轴方向占用的空间,默认是MainAxisSize.max
- mainAxisAlignment 表示子组件在主轴空间内对齐方式,只有当mainAxisSize为max时,才有意义
- verticalDirection 表示纵轴的对齐方向
- crossAxisAlignment 表示子组件在纵轴方向的对齐方式,该属性的参考系是verticalDirection的取值
- children 子组件数组
特殊情况
如果Row里面嵌套Row,或者Column里面再嵌套Column,那么只有最外面的Row或Column会占用尽可能大的空间,里面Row或Column所占用的空间为实际大小
Container(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text("hello world "),
Text("I am Jack "),
],
),
)
],
),
),
);
弹性布局
弹性布局允许子组件按照一定比例来分配父容器空间。弹性布局的概念在其它UI系统中也都存在,如H5中的弹性盒子布局,Android中的FlexboxLayout等。Flutter中的弹性布局主要通过Flex和Expanded来配合实现。
Flex
Flex组件可以沿着水平或垂直方向排列子组件,Row和Column都继承自Flex,参数基本相同。Flex可以和Expanded组件配合实现弹性布局。
Flex继承自MultiChildRenderObjectWidget,对应的RenderObject为RenderFlex,RenderFlex中实现了其布局算法。
Flex(
direction: Axis.horizontal, // 相当于Row
children: <Widget>[
],
),
Flex(
direction: Axis.vertical, // Column
children: <Widget>[
],
),
Expanded
Expanded组件可以按比例“扩伸” Row、Column和Flex子组件所占用的空间。
flex参数为弹性系数,如果为0或null,则child是没有弹性的,即不会被扩伸占用的空间。如果大于0,所有的Expanded按照其flex的比例来分割主轴的全部空闲空间。
Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
flex: 1, // 按比例分配
child: Container(
color: Colors.red,
height: 15.0,
),
),
Expanded(
flex: 2,
child: Container(
color: Colors.green,
height: 15.0,
),
)
],
),
流式布局
在使用Row和Colum时,如果子widget超出屏幕范围,则会报溢出错误。这是因为Row和Column默认只有一行或一列,如果超出屏幕不会折行。我们把超出屏幕显示范围会自动折行的布局称为流式布局。Flutter中通过Wrap和Flow来支持流式布局。
Wrap
Wrap的很多属性在Flex中也有,这些参数意义是相同的。除了超出显示范围后Wrap会折行外,其它行为与Flex基本相同。
- spacing:主轴方向子widget的间距
- runSpacing:纵轴方向的间距
- runAlignment:纵轴方向的对齐方式
Wrap(
direction: Axis.horizontal,
spacing: 20.0,
runSpacing: 10.0,
runAlignment: WrapAlignment.start,
children: <Widget>[
Container(width: 80.0,height: 80.0,color: Colors.red,),
Container(width: 80.0,height: 80.0,color: Colors.green,),
Container(width: 80.0,height: 80.0,color: Colors.blue,),
Container(width: 80.0,height: 80.0,color: Colors.black,),
Container(width: 80.0,height: 80.0,color: Colors.yellow,),
Container(width: 80.0,height: 80.0,color: Colors.pink,),
Container(width: 80.0,height: 80.0,color: Colors.purple,),
],
)
Flow
Flow主要用于一些需要自定义布局策略或性能要求较高(如动画中)的场景。其构造函数只需要一个delegete参数。
我们需要自己实现FlowDelegate的paintChildren()方法、shouldRepaint()方法,Flow布局不能自适应子组件大小,所以必须指定父容器大小或者实现Delegate的getSize()方法。
Flow(
delegate: TestFlowDelegate(margin: EdgeInsets.all(10.0)),
children: <Widget>[
Container(height: 80.0, width: 80.0, color: Colors.red),
Container(height: 80.0, width: 80.0, color: Colors.orange),
Container(height: 80.0, width: 80.0, color: Colors.blue),
Container(height: 80.0, width: 80.0, color: Colors.green),
Container(height: 80.0, width: 80.0, color: Colors.cyan),
Container(height: 80.0, width: 80.0, color: Colors.yellow),
Container(height: 80.0, width: 80.0, color: Colors.pink),
Container(height: 80.0, width: 80.0, color: Colors.purple),
],
),
class TestFlowDelegate extends FlowDelegate {
EdgeInsets margin = EdgeInsets.zero;
TestFlowDelegate({this.margin});
@override
void paintChildren(FlowPaintingContext context) {
var x = margin.left; // 顶点坐标
var y = margin.top;
print(context.size);
for (int i = 0; i < context.childCount; i++) {
// 计算每一个 子 widget的位置
var w = context.getChildSize(i).width + x + margin.right;
// w 每一次循环的 宽
if (w < context.size.width) {
// 绘制
context.paintChild(i, transform: Matrix4.translationValues(x, y, .0));
x = w + margin.left;
} else {
x = margin.left;
y += context.getChildSize(i).height + margin.top + margin.bottom;
// 绘制
context.paintChild(i, transform: Matrix4.translationValues(x, y, .0));
x += context.getChildSize(i).width + margin.left + margin.right;
}
}
}
// 获取父元素的 尺寸
@override
Size getSize(BoxConstraints constraints) {
return Size(double.infinity, 300.0);
}
@override
bool shouldRepaint(FlowDelegate oldDelegate) {
return oldDelegate != this;
}
}
层叠布局
层叠布局和Web中的绝对定位相似,子组件可以根据距父容器四个角的位置来确定自身的位置。绝对定位允许子组件堆叠起来(按照代码中声明的顺序)。Flutter中使用Stack和Positioned这两个组件来配合实现绝对定位。Stack允许子组件堆叠,而Positioned用于根据Stack的四个角来确定子组件的位置。
Stack
- alignment 没有定位或部分定位的子组件的对齐方式
- textDirection 确定alignment对齐的参考系
- fit 确定没有定位的子组件如何去适应Stack的大小
- overflow 超出Stack显示空间的子组件显示方式
Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: <Widget>[
Container(height: 80.0, width: 80.0, color: Colors.red),
],
)
Positioned
left、top 、right、 bottom分别代表离Stack左、上、右、底四边的距离。
width和height用于指定需要定位元素的宽度和高度。
Stack(
overflow: Overflow.visible, // 超出部分显示方式
children: <Widget>[
Container(height: 80.0, width: 80.0, color: Colors.red),
Positioned(
top: 30.0,
left: 50.0,
child: Container(height: 80.0, width: 80.0, color: Colors.green),
)
],
)
对齐与定位
Stack和Positioned可以指定一个或多个子元素相对于父元素各个边的精确偏移,并且可以重叠。但如果我们只想简单的调整一个子元素在父元素中的位置的话,使用Align组件会更简单一些。
Align
- alignment 需要一个AlignmentGeometry类型的值,表示子组件在父组件中的起始位置
- widthFactor 确定Align 组件本身的宽缩放因子 ,
- HeightFactor 确定Align 组件本身的高缩放因子
Align(
widthFactor: 3, // Align的宽度将会是child的3倍
heightFactor: 5, // Align的高度将会是child的5倍
child: Container(height: 80.0, width: 80.0, color: Colors.green),
)
Alignment、FractionalOffset:
-
Alignment继承自AlignmentGeometry,表示矩形内的一个点。
Alignment(this.x, this.y)
-
Widget会以矩形的中心点作为坐标原点,即Alignment(0.0, 0.0)。
-
x、y的值从-1到1分别代表矩形左边到右边的距离和顶部到底边的距离。
-
具体偏移坐标可以通过坐标转换公式进行计算:
(Alignment.x*childWidth/2+childWidth/2,Alignment.y*childHeight/2+childHeight/2)
-
FractionalOffset 继承自 Alignment,它和 Alignment唯一的区别就是坐标原点为矩形的左侧顶点。
(FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)
Align(
widthFactor: 4,
heightFactor: 4,
alignment: FractionalOffset(0,0.5),
child: FlutterLogo(size: 30),
)
Center
Center继承自Align,它比Align只少了一个alignment 参数;由于Align的构造函数中alignment 值为Alignment.center,所以,我们可以认为Center组件其实是对齐方式确定(Alignment.center)了的Align。
Center(
child: Container(height: 80.0, width: 80.0, color: Colors.green),
)
容器组件
容器类组件和布局类组件都作用于其子组件:
布局类一般都需要接收一个widget数组;而容器类一般只需要接收一个子widget。
布局类是按照一定的排列方式对子widget进行排列;而容器类一般只是包装其子widegt,对其添加一些修饰、变换或尺寸限制。
Padding填充
Padding可以给其子节点添加填充(留白),和边距效果类似。
EdgeInsetsGeometry是一个抽象类,开发中,我们一般都使用EdgeInsets类,它是EdgeInsetsGeometry的一个子类,定义了一些设置填充的便捷方法。
- fromLTRB 分别指向左上右下四个方向的填充。
- all 所有方向均使用相同数值的填充。
- only 可以设置某个具体方向的填充。
- symmetric 用于设置对称方向的填充。
Padding({
...
EdgeInsetsGeometry padding,
Widget child,
})
尺寸限制
尺寸限制类容器用于限制容器大小,Flutter中提供了多种这样的容器,如ConstrainedBox、SizedBox、UnconstrainedBox。
ConstrainedBox
ConstrainedBox用于对子组件添加额外的约束。例如,如果你想让子组件的最小高度是80像素,你可以使用const BoxConstraints(minHeight: 80.0)作为子组件的约束
ConstrainedBox用于对子组件添加额外的约束。BoxConstraints用于设置限制条件。
BoxConstraints还定义了一些便捷的构造函数,用于快速生成特定限制规则的BoxConstraints,如BoxConstraints.tight(Size size)、const BoxConstraints.expand()等。
ConstrainedBox({
Key key,
@required this.constraints,
Widget child,
});
const BoxConstraints({
this.minWidth = 0.0, //最小宽度
this.maxWidth = double.infinity, //最大宽度
this.minHeight = 0.0, //最小高度
this.maxHeight = double.infinity //最大高度
})
示例 :
我们先定义一个redBox,它是一个背景颜色为红色的盒子,不指定它的宽度和高度
Widget redBox=DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
);
我们实现一个最小高度为50,宽度尽可能大的红色容器。
ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity, //宽度尽可能大
minHeight: 50.0 //最小高度为50像素
),
child: Container(
height: 5.0,
child: redBox
),
)
虽然将Container的高度设置为5像素,但是最终却是50像素,这正是ConstrainedBox的最小高度限制生效了。如果将Container的高度设置为80像素,那么最终红色区域的高度也会是80像素,因为在此示例中,ConstrainedBox只限制了最小高度,并未限制最大高度。
SizedBox
SizedBox用于给子元素指定固定的宽高。实际上SizedBox只是ConstrainedBox的一个定制。
ConstrainedBox和SizedBox都是通过RenderConstrainedBox来渲染的。
// 能强制子控件具有特定宽度、高度或两者都有,使子控件设置的宽高失效
const SizedBox({
Key key,
this.width,
this.height,
Widget child
});
SizedBox(
width: 80.0,
height: 80.0,
child: redBox
)
多重限制
如果某一个组件有多个父级ConstrainedBox限制,那么最终会是哪个生效?
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), //父
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
child: redBox,
)
)
最终显示效果是宽90,高60,也就是说是子ConstrainedBox的minWidth生效,而minHeight是父ConstrainedBox生效。单凭这个例子,我们还总结不出什么规律,我们将上例中父子限制条件换一下:
ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0),
child: redBox,
)
)
最终的显示效果仍然是90,高60,效果相同,但意义不同,因为此时minWidth生效的是父ConstrainedBox,而minHeight是子ConstrainedBox生效。
通过上面示例,我们发现有多重限制时,对于minWidth和minHeight来说,是取父子中相应数值较大的。实际上,只有这样才能保证父限制与子限制不冲突
UnconstrainedBox
UnconstrainedBox不会对子组件产生任何限制,它允许其子组件按照其本身大小绘制。一般情况下,我们会很少直接使用此组件,但在“去除”多重限制的时候也许会有帮助。
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0), //父
child: UnconstrainedBox( //“去除”父级限制
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
child: redBox,
),
)
)
上面代码中,如果没有中间的UnconstrainedBox,那么根据上面所述的多重限制规则,那么最终将显示一个90×100的红色框。但是由于UnconstrainedBox “去除”了父ConstrainedBox的限制,则最终会按照子ConstrainedBox的限制来绘制redBox,即90×20
但是,请注意,UnconstrainedBox对父组件限制的“去除”并非是真正的去除:上面例子中虽然红色区域大小是90×20,但上方仍然有80的空白空间。也就是说父限制的minHeight(100.0)仍然是生效的,只不过它不影响最终子元素redBox的大小,但仍然还是占有相应的空间,可以认为此时的父ConstrainedBox是作用于子UnconstrainedBox上,而redBox只受子ConstrainedBox限制
其他尺寸限制容器
上面介绍的这些常用的尺寸限制类容器外,还有一些其他的尺寸限制类容器。
- AspectRatio,它可以指定子组件的长宽比
- LimitedBox 用于指定最大宽高
- FractionallySizedBox 可以根据父容器宽高的百分比来设置子组件宽高
由于这些容器使用起来都比较简单,我们便不再赘述。
DecoratedBox装饰容器
DecoratedBox可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框、渐变等。
-
decoration 代表将要绘制的装饰,它的类型为Decoration。
我们通常会直接使用BoxDecoration类,它是一个Decoration的子类,实现了常用的装饰元素的绘制。
BoxDecoration({ Color color, //颜色 DecorationImage image,//图片 BoxBorder border, //边框 BorderRadiusGeometry borderRadius, //圆角 List<BoxShadow> boxShadow, //阴影,可以指定多个 Gradient gradient, //渐变 BlendMode backgroundBlendMode, //背景混合模式 BoxShape shape = BoxShape.rectangle, //形状 })
-
position 此属性决定在Decoration为前景装饰还是背景装饰。
const DecoratedBox({
Decoration decoration,
DecorationPosition position = DecorationPosition.background,
Widget child
})
案例
DecoratedBox(
decoration: BoxDecoration(
// color: Colors.green,
gradient: LinearGradient(
colors: [Colors.blue,Colors.red],
),
borderRadius: BorderRadius.circular(20.0),
boxShadow: [
BoxShadow(
color: Colors.green,
offset: Offset(2.0, 3.0),
blurRadius: 5.0,
),
],
),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10,horizontal: 30),
child: Text(
'登录',
style: TextStyle(color: Colors.white),
),
),
),
Transform变换
Transform可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效。
Matrix4是一个4D矩阵,通过它我们可以实现各种矩阵操作。
// 倾斜
Transform(
transform: Matrix4.skewX(0.5), // 弧度 沿着Y轴倾斜0.3弧度
child: Container(
color: Colors.green,
child: Text('倾斜'),
),
)
Transform提供了几个快捷的方法,用于不同的矩阵变换:
-
Transform.translate 在绘制时沿x、y轴对子组件平移指定的距离。
Transform.translate( offset: Offset(-30.0, -5.0), child: Text('位移'), )
-
Transform.rotate 可以对子组件进行旋转变换
import 'dart:math' as math; Transform.rotate( angle: math.pi / 2, child: Text('旋转'), )
-
Transform.scale 可以对子组件进行缩小或放大变换。
Transform.scale( scale: 3.0, child: Text('缩放'), )
Container
Container是一个组合类容器,它本身不对应具体的RenderObject,它是DecoratedBox、ConstrainedBox、Transform、Padding、Align等组件组合的一个多功能容器,所以我们只需通过一个Container组件可以实现同时需要装饰、变换、限制的场景。
- 容器的大小可以通过width、height属性来指定,也可以通过constraints来指定。
- color和decoration是互斥的,如果同时设置它们则会报错
补充:Padding和Margin
margin的留白是在容器外部,而padding的留白是在容器内部。事实上,Container内margin和padding都是通过Padding 组件来实现的。
所有属性:
Container({
this.alignment,
this.padding,
Color color,
Decoration decoration,
Decoration foregroundDecoration,
double width,
double height,
BoxConstraints constraints,
this.margin,
this.transform,
this.child,
})
裁剪
ClipOval
子组件为正方形时,裁剪为内贴圆,为矩形时裁剪为内贴椭圆。
ClipOval(
child: Container(
width: 60.0,
height: 60.0,
decoration: BoxDecoration(color: Colors.red),
),
)
ClipRRect
将子组件裁剪为圆角矩形
ClipRRect(
borderRadius: BorderRadius.circular(15.0), // 圆角数值
child: Container(
width: 60.0,
height: 60.0,
decoration: BoxDecoration(color: Colors.red),
),
)
ClipRect
裁剪子组件到实际占用的矩形大小(溢出部分裁剪)
ClipRRect(
child: Align(
widthFactor: .5, // 切换为1调试查看
child: Container(
width: 60.0,
height: 60.0,
decoration: BoxDecoration(color: Colors.red),
),
),
)
自定义裁剪
如果想剪裁子组件的特定区域,可以使用CustomClipper来自定义剪裁区域。
- getClip() 是用于获取剪裁区域的接口
- shouldReclip() 接口决定是否重新剪裁
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: ClipRect(
clipper: MyClipper(),
child: Container(
width: 60.0,
height: 60.0,
decoration: BoxDecoration(color: Colors.green),
),
),
)
class MyClipper extends CustomClipper<Rect> {
@override
Rect getClip(Size size) =>
Rect.fromLTWH(10.0, 10.0, 30.0, 30.0); // 裁剪初始点与裁剪区域
@override
bool shouldReclip(CustomClipper<Rect> oldClipper) => false; // 只裁剪一次,永远不更新
}
可滚动组件
简介
当组件内容超过当前显示视口(ViewPort)时,如果没有特殊处理,Flutter则会提示Overflow错误。为此,Flutter提供了多种可滚动组件(Scrollable Widget)用于显示列表和长布局。常用的可滚动组件包含ListView、GridView等。可滚动组件都直接或间接包含一个Scrollable组件,因此它们包括一些共同的属性。
Scrollable
- axisDirection 滚动方向
- physics 接受一个ScrollPhysics类型的对象,它决定可滚动组件如何响应用户操作
- controller 接受一个ScrollController对象,ScrollController的主要作用是控制滚动位置和监听滚动事件
概念
-
ViewPort视口
在很多布局系统中都有ViewPort的概念,在Flutter中,视口指一个Widget的实际显示区域。
-
基于Sliver的延迟构建
通常可滚动组件的子组件可能会非常多、占用的总高度也会非常大。
支持Sliver模型的可滚动可以将子组件分成好多个Sliver,只有Sliver出现在视口中才会去构建。
支持基于Sliver的延迟构建模型有:ListView、GridView 。
不支持Sliver模型的有:SingleChildScrollView。
滚动条
Scrollbar是一个Material风格的滚动指示器
- 如果要给可滚动组件添加滚动条,只需将Scrollbar作为可滚动组件的任意一个父级组件即可。
- 可以通过监听滚动通知来确定滚动条位置。
CupertinoScrollbar是iOS风格的滚动条
- 如果你使用的是Scrollbar,那么在iOS平台它会自动切换为CupertinoScrollbar
String str = 'ABCDEFGHJHGGUIsjdhsahjdshadhadhashksd';
List showList = str
.split('')
.map((e) => Text(
e,
textScaleFactor: 2.0,
))
.toList();
Scrollbar(
child: SingleChildScrollView(
child: Center(
child: Column(
children: showList,
),
),
),
),
SingleChildScrollView
- reverse 可滚动组件初始滚动位置(startand)
- primary 是否使用widget树中默认的PrimaryScrollController
补充:
需要注意的是,通常SingleChildScrollView只应在期望的内容不会超过屏幕太多时使用,这是因为SingleChildScrollView不支持基于Sliver的延迟实例化模型,所以如果预计视口可能包含超出屏幕尺寸太多的内容时,那么使用SingleChildScrollView将会非常昂贵(性能差),此时应该使用一些支持Sliver延迟加载的可滚动组件,如ListView。
String str = 'ABCDEFGHJHGGUIsjdhsahjdshadhadhashksd';
List showList = str
.split('')
.map((e) => Text(
e,
textScaleFactor: 2.0,
))
.toList();
SingleChildScrollView(
// physics: ClampingScrollPhysics(), // Android下的微光效果
physics: BouncingScrollPhysics(), // ios下的回弹
child: Center(
child: Column(
children: showList,
),
),
),
ListView
ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持基于Sliver的延迟构建模型。
ListView各个构造函数的共同参数:
- itemExtent 该参数如果不为null,则会强制children的“长度”为itemExtent的值
- shrinkWrap 该属性表示是否根据子组件的总长度来设置ListView的长度
- addAutomaticKeepAlives 该属性表示是否将列表项(子组件)包裹在AutomaticKeepAlive 组件中
- addRepaintBoundaries 该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中
构造函数
默认构造函数有一个children参数,它接受一个Widget列表(List)
通过默认构造函数构建的ListView没有应用基于Sliver的懒加载模型。实际上通过此方式创建的ListView和使用SingleChildScrollView+Column的方式没有本质的区别。
ListView(
shrinkWrap: true,
padding: EdgeInsets.all(20.0),
children: <Widget>[
Text('我是谁'),
Text('我在哪'),
Text('我要做啥'),
Text('谁能告诉我'),
],
)
ListView.builder
ListView.builder适合列表项比较多(或者无限)的情况,因为只有当子组件真正显示的时候才会被创建,也就说通过该构造函数创建的ListView是支持基于Sliver的懒加载模型的。
- itemBuilder 列表项的构建器
- itemCount 列表项的数量
ListView.builder(
itemCount: 100,
itemExtent: 50.0,
itemBuilder: (BuildContext context, int index) {
return Text("$index");
},
)
ListView.separated
ListView.separated可以在生成的列表项之间添加一个分割组件,它比ListView.builder多了一个separatorBuilder参数,该参数是一个分割组件生成器。
ListView.separated(
itemBuilder: (BuildContext context, int index) {
return Text("$index");
},
separatorBuilder: (BuildContext context, int index) {
return Divider(color: Colors.red);
},
itemCount: 100,
)
GridView
GridView可以构建一个二维网格列表
构造函数
gridDelegate参数,类型是SliverGridDelegate,它的作用是控制GridView子组件如何排列(layout)。
GridView({
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required SliverGridDelegate gridDelegate, //控制子widget layout的委托
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
})
SliverridDelegateWithFixedCrossAxisCount
该类实现了一个横轴为固定数量子元素的layout算法。
- crossAxisCount 横轴子元素的数量
- mainAxisSpacing 主轴方向的间距
- crossAxisSpacing 横轴方向子元素的间距
- childAspectRatio 子元素在横轴长度和主轴长度的比例
GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 2.0,
),
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.backup),
Icon(Icons.cake),
Icon(Icons.data_usage),
Icon(Icons.email),
Icon(Icons.face),
Icon(Icons.games),
Icon(Icons.help),
],
)
SliverGridDelegateWithFixedCrossAxisCount
该子类实现了一个横轴子元素为固定最大长度的layout算法。
- maxCrossAxisExtent 子元素在横轴上的最大长度
GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 70.0,
mainAxisSpacing: 0.0,
crossAxisSpacing: 0.0,
childAspectRatio: 1.0,
),
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.backup),
Icon(Icons.cake),
Icon(Icons.data_usage),
Icon(Icons.email),
Icon(Icons.face),
Icon(Icons.games),
Icon(Icons.help),
],
)
GridView.count
函数内部使用了SliverGridDelegateWithFixedCrossAxisCount,我们通过它可以快速的创建横轴固定数量子元素的GridView。
GridView.count(
crossAxisCount: 3,
childAspectRatio: 1,
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast),
],
),
GridView.extent
函数内部使用了SliverGridDelegateWithMaxCrossAxisExtent,我们通过它可以快速的创建纵轴子元素为固定最大长度的的GridView。
GridView.extent(
maxCrossAxisExtent: 180.0,
childAspectRatio: 1.0,
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast),
],
)
GridView.builder
前面这些方式都会提前将所有子widget都构建好,所以只适用于子widget数量比较少时,当子widget比较多时,我们可以通过GridView.builder来动态创建子widget。
List<IconData> _icons = [];
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 0.0,
mainAxisSpacing: 0.0,
childAspectRatio: 1.0,
),
itemCount: _icons.length,
itemBuilder: (BuildContext context, int index) {
if (index == _icons.length - 1 && _icons.length < 300) {
_receiveIconData();
}
return Icon(_icons[index]);
},
)
// ... 延迟添加
_receiveIconData() {
Future.delayed(Duration(seconds: 2)).then((value) {
setState(() {
_icons.add(Icons.add);
});
});
}
CustomScrollView
CustomScrollView才可以将多个Sliver“粘”在一起,这些Sliver共用CustomScrollView的Scrollable,所以最终才实现了统一的滑动效果。
CustomScrollView的子组件必须都是Sliver。
Flutter提供了一些可滚动组件的Sliver版,如SliverList、SliverGrid等。Sliver系列Widget比较多,我们不会一一介绍。大多数Sliver都和可滚动组件对应,还有一些Sliver是和可滚动组件无关的,如SliverPadding、SliverAppBar、SliverToBoxAdapter等,它们主要是为了结合CustomScrollView一起使用,这是因为CustomScrollView的子组件必须都是Sliver。
SliverAppbar
导航条
const SliverAppBar({
Key key,
this.leading, //在标题左侧显示的一个控件,在首页通常显示应用的 logo;在其他界面通常显示为返回按钮
this.automaticallyImplyLeading = true,//? 控制是否应该尝试暗示前导小部件为null
this.title, //当前界面的标题文字
this.actions, //一个 Widget 列表,代表 Toolbar 中所显示的菜单,对于常用的菜单,通常使用 IconButton 来表示;对于不常用的菜单通常使用 PopupMenuButton 来显示为三个点,点击后弹出二级菜单
this.flexibleSpace, //一个显示在 AppBar 下方的控件,高度和 AppBar 高度一样, // 可以实现一些特殊的效果,该属性通常在 SliverAppBar 中使用
this.bottom, //一个 AppBarBottomWidget 对象,通常是 TabBar。用来在 Toolbar 标题下面显示一个 Tab 导航栏
this.elevation, //阴影
this.forceElevated = false,
this.backgroundColor, //APP bar 的颜色,默认值为 ThemeData.primaryColor。改值通常和下面的三个属性一起使用
this.brightness, //App bar 的亮度,有白色和黑色两种主题,默认值为 ThemeData.primaryColorBrightness
this.iconTheme, //App bar 上图标的颜色、透明度、和尺寸信息。默认值为 ThemeData().primaryIconTheme
this.textTheme, //App bar 上的文字主题。默认值为 ThemeData().primaryTextTheme
this.primary = true, //此应用栏是否显示在屏幕顶部
this.centerTitle, //标题是否居中显示,默认值根据不同的操作系统,显示方式不一样,true居中 false居左
this.titleSpacing = NavigationToolbar.kMiddleSpacing,//横轴上标题内容 周围的间距
this.expandedHeight, //展开高度
this.floating = false, //是否随着滑动隐藏标题
this.pinned = false, //是否固定在顶部
this.snap = false, //与floating结合使用
})
CustomScrollView(
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: 250.0,
title: Text('22'),
flexibleSpace: FlexibleSpaceBar(
title: Text('伸缩的AppBar'),
),
)
],
)
SliverToBoxAdapter
设置普通的控件,例如区头等 这个位置是不固定的随意的
CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
child: RaisedButton(
child: Text('按钮'),
onPressed: () {},
),
),
)
],
)
SliverPadding
设置padding的,子控件必须是sliver类型
CustomScrollView(
slivers: <Widget>[
SliverPadding(
padding: EdgeInsets.all(20.0),
sliver: SliverToBoxAdapter(
child: Container(
child: RaisedButton(
child: Text('按钮'),
onPressed: () {},
),
),
),
),
],
)
SliverGrid
同GridView,但有两个代理,第一个管理多少行,第二个管理多少个
CustomScrollView(
slivers: <Widget>[
SliverGrid(
// 构建代理,管理多少行
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
color: Colors.blue[100 * (index % 9)],
alignment: Alignment.center,
child: Text('grid Item ${index}'),
),
childCount: 20, //限制数量
),
// 布局代理,管理多少个
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 1.0,
)),
],
)
SliverFixedExtentList
类似ListView
CustomScrollView(
slivers: <Widget>[
SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
color: Colors.pink[100 * (index % 9)],
alignment: Alignment.center,
child: Text('grid Item ${index + 1}'),
),
childCount: 20,
),
itemExtent: 50.0,
)
],
)
滚动监听及控制
ScrollController
ScrollController间接继承自Listenable,可以根据ScrollController来监听滚动事件
- offset 属性 可滚动组件当前的滚动位置。
- jumpTo 方法 跳转到指定的位置。
- animateTo 方法 跳转到指定的位置并执行一个动画。
ScrollController _controller = ScrollController();
bool showTopBtn = false;
@override
void initState() {
super.initState();
// 监听滚动
_controller.addListener(() {
// _controller.offset 滚动当前位置
if (_controller.offset < 100 && showTopBtn) {
setState(() {
showTopBtn = false;
});
} else if (_controller.offset >= 1000 && !showTopBtn) {
setState(() {
showTopBtn = true;
});
}
});
@override
Widget build(BuildContext context) {
// debugPaintSizeEnabled = true;
return MaterialApp(
home: Material(
child: Scaffold(
appBar: AppBar(
title: Text('监听滚动'),
),
body: Scrollbar(
child: ListView.builder(
controller: _controller,
itemCount: 100,
itemExtent: 50.0,
itemBuilder: (context, index) => Container(
alignment: Alignment.center,
color: index % 2 == 0 ? Colors.grey[200] : null,
child: Text('第 ${index + 1} 项'),
),
),
),
floatingActionButton: !showTopBtn
? null
: FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
_controller.jumpTo(0.0);
},
),
),
),
);
}
}
ScrollPosition
-
ScrollPosition是用来保存可滚动组件的滚动位置的
-
ScrollPosition是真正保存滑动位置信息的对象
double get offset => position.pixels;
-
一个ScrollController对象可以同时被多个可滚动组件使用
-
ScrollController会为每一个可滚动组件创建一个ScrollPosition对象
-
ScrollPosition保存在ScrollController的positions属性中
-
通过controller.positions.length来确定controller被几个可滚动组件使用
controller.positions.elementAt(0).pixels controller.positions.elementAt(1).pixels
控制原理
- createScrollPosition()方法来创建一个ScrollPosition来存储滚动位置信息
- 可滚动组件会调用attach()方法,将创建的ScrollPosition添加到positions属性中
- 当可滚动组件销毁时,会调用ScrollController的detach()方法
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition,
);
void attach(ScrollPosition position) ;
void detach(ScrollPosition position) ;
notification
通过NotificationListener可以监控到该notification
Scrollbar(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
double progress = notification.metrics.pixels /
notification.metrics.maxScrollExtent;
// 显示
setState(() {
_progress = '${(progress * 100).toInt()}%';
});
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
ListView.builder(
itemBuilder: (context, index) =>
ListTile(title: Text('第 $index 项')),
itemCount: 100,
itemExtent: 50.0,
),
Positioned(
child: CircleAvatar(
radius: 30.0,
child: Text('${_progress}'),
),
),
],
),
),
)
颜色和主题
颜色
颜色值
Color类中颜色以一个int值保存:
Bit(位) | 颜色 |
---|---|
0-7 | 蓝色 |
8-15 | 绿色 |
16-23 | 红色 |
24-31 | Alpha(不透明度) |
创建颜色
- Color 默认构造函数,通过value创建颜色值
- Color.fromARGB 通过alpha、red、green、blue属性创建颜色值
- Color.fromRGBO 通过red、green、blue、opacity属性创建颜色值
字符串转Color对象
将颜色字符串转成Color对象:
class TestColor2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
// #dc380d
var colorA = Color(0xffdc380d);
var str = 'dc380d';
// 颜色转16进制
var colorB = Color(int.parse(str, radix: 16) | 0xff000000); // alpha
var colorC = Color(int.parse(str, radix: 16)).withAlpha(100);
var colorD = Color(int.parse(str, radix: 16)).withOpacity(.5);
return Container(
height: 30.0,
color: colorD,
);
}
}
计算颜色亮度
// Color颜色值
// 返回一个[0-1]的值,数值越大颜色越浅
color.computeLuminance()
获取颜色值
print(colorA.value);
print(colorA.value.bitLength);
print(colorA.alpha); // alpha
print(colorA.red); // red
print(colorA.green); // green
MaterialColor
MaterialColor是实现Material Design中的颜色的类,它包含一种颜色的10个级别的渐变色。MaterialColor通过"[]"运算符的索引值来代表颜色的深度,有效的索引有:50,100,200,…,900,数字越大,颜色越深。
static const MaterialColor blue = MaterialColor(
_bluePrimaryValue,
<int, Color>{
50: Color(0xFFE3F2FD),
100: Color(0xFFBBDEFB),
200: Color(0xFF90CAF9),
300: Color(0xFF64B5F6),
400: Color(0xFF42A5F5),
500: Color(_bluePrimaryValue),
600: Color(0xFF1E88E5),
700: Color(0xFF1976D2),
800: Color(0xFF1565C0),
900: Color(0xFF0D47A1),
},
);
static const int _bluePrimaryValue = 0xFF2196F3;
主题
Theme组件可以为Material APP定义主题数据(ThemeData)。Material组件库里很多组件都使用了主题数据,如导航栏颜色、标题字体、Icon样式等。Theme内会使用InheritedWidget来为其子树共享样式数据。
ThemeData
ThemeData用于保存是Material 组件库的主题数据,Material组件可自定义部分都定义在ThemeData中了,所以我们可以通过ThemeData来自定义应用主题。在子组件中,我们可以通过Theme.of方法来获取当前的ThemeData。
ThemeData({
Brightness brightness, //深色还是浅色
MaterialColor primarySwatch, //主题颜色样本,见下面介绍
Color primaryColor, //主色,决定导航栏颜色
Color accentColor, //次级色,决定大多数Widget的颜色,如进度条、开关等。
Color cardColor, //卡片颜色
Color dividerColor, //分割线颜色
ButtonThemeData buttonTheme, //按钮主题
Color cursorColor, //输入框光标颜色
Color dialogBackgroundColor,//对话框背景颜色
String fontFamily, //文字字体
TextTheme textTheme,// 字体主题,包括标题、body等文字样式
IconThemeData iconTheme, // Icon的默认样式
TargetPlatform platform, //指定平台,应用特定平台控件风格
...
})
全局主题
在 Flutter 中,应用程序类 MaterialApp 的初始化方法,为我们提供了设置主题的能力。我们可以通过参数 theme,选择改变 App 的主题色、字体等,设置界面在 MaterialApp 下的展示样式。
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.cyan,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
局部主题
-
如果我们不想继承任何 App 全局的颜色或字体样式,可以直接新建一个 ThemeData 实例,依次设置对应的样式
MaterialApp( theme: ThemeData( brightness: Brightness.dark, primaryColor: Colors.cyan, ), home: Theme( // 新建ThemeData实例 data: ThemeData( primarySwatch: _themeColor, iconTheme: IconThemeData(color: _themeColor), textTheme: TextTheme( bodyText2: TextStyle( color: Colors.red, ))), child: // ... ), )
-
如果我们不想在局部重写所有的样式,则可以继承 App 的主题,使用 copyWith 方法,只更新部分样式
MaterialApp( theme: ThemeData( brightness: Brightness.dark, primaryColor: Colors.cyan, ), home: Theme( // 继承更新部分实例 data: Theme.of(context).copyWith( iconTheme: IconThemeData(color: Colors.green), textTheme: TextTheme( bodyText2: TextStyle( color: Colors.red, ) ), ), child: // ... ), )
样式复用
Theme.of(context) 方法将向上查找 Widget 树,并返回 Widget 树中最近的主题 Theme。
Container( color: Theme.of(context).primaryColor,//容器背景色复用应用主题色 child: Text( 'Text with a background color', style: Theme.of(context).textTheme.title,//Text组件文本样式复用应用文本样式 ), );
自定义组件
当Flutter提供的现有组件无法满足我们的需求,或者我们为了共享代码需要封装一些通用组件,这时我们就需要自定义组件。在Flutter中自定义组件有三种方式:通过组合其它组件、自绘和实现RenderObject。
简介
“组合”是自定义组件最简单的方法,在任何需要自定义组件的场景下,我们都应该优先考虑是否能够通过组合来实现。而自绘和通过实现RenderObject的方法本质上是一样的,都需要开发者调用Canvas API手动去绘制UI,优点是强大灵活,理论上可以实现任何外观的UI,而缺点是必须了解Canvas API细节,并且得自己去实现绘制逻辑。
方法
- 组合其它Widget
这种方式是通过拼装其它组件来组合成一个新的组件。Flutter提供了非常多的基础组件,而我们的界面开发其实就是按照需要组合这些组件来实现各种不同的布局而已。
- 自绘组件
如果遇到无法通过现有的组件来实现需要的UI时,我们可以通过自绘组件的方式来实现,我们可以通过Flutter中提供的CustomPaint和Canvas来实现UI自绘。
- 实现RenderObject
Flutter提供的自身具有UI外观的组件,RenderObject是一个抽象类,它定义了一个抽象方法paint(…)。RenderObject中最终也是通过Canvas API来绘制的。
组合现有组件
在Flutter中页面UI通常都是由一些低阶别的组件组合而成,当我们需要封装一些通用组件时,应该首先考虑是否可以通过组合其它组件来实现,如果可以,则应优先使用组合,因为直接通过现有组件拼装会非常简单、灵活、高效。
案例:
- 自定义按键
components/customButton.dart
import 'package:flutter/material.dart';
class CustomButton extends StatelessWidget {
final List<Color> colors; // 渐变颜色数组
final double width; // 按钮宽度
final double height; // 按钮高度
final Widget child;
final BorderRadius borderRadius; // 圆角
final GestureTapCallback onPressed; // 点击回调
// 初始化构建
CustomButton({
Key key,
this.colors,
this.width,
this.height,
this.borderRadius,
@required this.child,
@required this.onPressed,
}) : assert(child != null),
super(key: key);
@override
Widget build(BuildContext context) {
// 渐变颜色需要ThemeData包裹
ThemeData theme = Theme.of(context);
List<Color> _colors = colors ??
[theme.primaryColor, theme.primaryColorDark ?? theme.primaryColor];
return DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(colors: _colors),
borderRadius: borderRadius,
),
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: onPressed,
splashColor: _colors.last,
highlightColor: Colors.transparent,
child: ConstrainedBox(
constraints: BoxConstraints.tightFor(width: width, height: height),
child: Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: DefaultTextStyle(
style: TextStyle(fontWeight: FontWeight.bold),
child: child,
),
),
),
),
),
),
);
}
}
import 'package:flutter/material.dart';
import './components/customButton.dart';
main(List<String> args) {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
// debugPaintSizeEnabled = true;
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('自定义组件'),
),
body: Center(
child: Column(
children: <Widget>[
// 自定义按键
CustomButton(
height: 46.0,
child: Text('按键'),
borderRadius: BorderRadius.circular(20.0),
colors: [Colors.red, Colors.yellow],
onPressed: () {},
)
],
),
),
),
);
}
}
- 自定义控制器
components/turnBox.dart
import 'package:flutter/material.dart';
class TurnBox extends StatefulWidget {
final double turns;
final int speed;
final Widget child;
TurnBox({
Key key,
this.turns = 0.0,
this.speed = 200,
@required this.child,
}) : assert(child != null),
super(key: key);
@override
_TurnBoxState createState() => _TurnBoxState();
}
class _TurnBoxState extends State<TurnBox> with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
lowerBound: -double.infinity,
upperBound: double.infinity,
);
_controller.value = widget.turns;
}
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: _controller,
child: widget.child,
);
}
@override
void didUpdateWidget(TurnBox oldWidget) {
super.didUpdateWidget(oldWidget);
// 数值发生变化更新动画
if (widget.turns != oldWidget.turns) {
_controller.animateTo(
widget.turns,
duration: Duration(milliseconds: widget.speed),
curve: Curves.easeInOut,
);
}
}
}
import 'package:flutter/material.dart';
import './components/turnBox.dart';
main(List<String> args) {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
double _turns = 0.0;
@override
Widget build(BuildContext context) {
// debugPaintSizeEnabled = true;
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('自定义组件'),
),
body: Center(
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TurnBox(
child: Icon(Icons.refresh, size: 50.0),
turns: _turns,
),
RaisedButton(
child: Text('顺时针旋转'),
onPressed: () {
setState(() {
_turns += 0.2;
});
},
),
RaisedButton(
child: Text('逆时针旋转'),
onPressed: () {
setState(() {
_turns -= 0.2;
});
},
),
],
),
],
),
),
),
);
}
}
自绘组件
对于一些复杂或不规则的UI,我们可能无法通过组合其它组件的方式来实现,当然,有时候我们可以使用图片来实现,但在一些需要动态交互的场景静态图片也是实现不了的,我们就需要来自己绘制UI外观。
CustomPaint
几乎所有的UI系统都会提供一个自绘UI的接口,这个接口通常会提供一块2D画布Canvas,Canvas内部封装了一些基本绘制的API,开发者可以通过Canvas绘制各种自定义图形。
在Flutter中,提供了一个CustomPaint 组件,它可以结合画笔CustomPainter来实现自定义图形绘制
CustomPaint({
Key key,
this.painter,
this.foregroundPainter,
this.size = Size.zero, // 指定画布大小
this.isComplex = false,
this.willChange = false,
Widget child, // 子节点,可以为空
})
绘制时,画笔需要继承CustomPainter类,我们在画笔类中实现真正的绘制逻辑。
- painter 背景画笔,会显示在子节点后面。
- foregroundPainter 前景画笔,会显示在子节点前面。
- size 当child为null时,代表默认绘制区域大小。
- isComplex 是否复杂的绘制,如果是,Flutter会应用一些缓存策略来减少重复渲染的开销。
- willChange 和isComplex配合使用,当启用缓存时,该属性代表在下一帧中绘制是否会改变。
- 如果CustomPaint有子节点,通常情况下将子节点包裹在RepaintBoundary组件中。
- RepaintBoundary 子组件的绘制将独立于父组件的绘制,RepaintBoundary会隔离其子节点和CustomPaint本身的绘制边界。
paint
- drawLine 画线
- drawPoint 画点
- drawPath 画路径
- drawImage 画图像
- drawRect 画矩形
- drawCircle 画圆
- drawOval 画椭圆
- drawArc 画圆弧
案例
- 棋盘
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class Go extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(300.0, 300.0), // 设置画布范围
painter: MyPainter(), // 绘制图案
);
}
}
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 区间数量
int step = 15;
// 棋盘距离
double goWidth = size.width / step;
double goHeight = size.height / step;
// 绘制背景
Paint paint = Paint()
..style = PaintingStyle.fill
..color = Color(0x77cdb175);
Rect rect = Offset.zero & size;
canvas.drawRect(rect, paint);
// 绘线
paint
..style = PaintingStyle.stroke
..strokeWidth = 0.3
..color = Colors.black87;
for (int i = 0; i <= step; i++) {
double dx = goWidth * i;
canvas.drawLine(Offset(dx, 0.0), Offset(dx, size.height), paint);
}
for (int i = 0; i <= step; i++) {
double dy = goHeight * i;
canvas.drawLine(Offset(0.0, dy), Offset(size.width, dy), paint);
}
// 子半径
double radius = goWidth / 2 - 2.0;
// 黑子
paint
..style = PaintingStyle.fill
..color = Colors.black;
canvas.drawCircle(
Offset(size.width / 2 - goWidth / 2, size.height / 2 - goHeight / 2),
radius,
paint,
);
// 白子
paint
..style = PaintingStyle.fill
..color = Colors.white;
canvas.drawCircle(
Offset(size.width / 2 + goWidth / 2, size.height / 2 - goHeight / 2),
radius,
paint,
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
- 圆形进度器
import 'package:flutter/material.dart';
import 'dart:math';
class CircularProgress extends StatelessWidget {
final double strokeWidth;
final double radius;
final bool strokeCapRound;
final double value;
final Color backgroundColor;
final List<Color> colors;
CircularProgress({
Key key,
this.strokeWidth = 5.0,
@required this.radius,
this.strokeCapRound = false,
this.value = 0.0, // [0-1]
this.backgroundColor = const Color(0xFFEEEEEE),
@required this.colors,
}) : assert(radius != null),
assert(colors != null && colors.isNotEmpty),
assert(value >= 0.0 && value <= 1.0),
super(key: key);
@override
Widget build(BuildContext context) {
double _offset =
strokeCapRound ? asin(strokeWidth / (radius * 2 - strokeWidth)) : 0.0;
return Transform.rotate(
angle: -pi / 2 - _offset,
child: CustomPaint(
size: Size.fromRadius(radius),
painter: _GradientCircularProgressPainter(
strokeWidth: strokeWidth,
strokeCapRound: strokeCapRound,
radius: radius,
backgroundColor: backgroundColor,
value: value,
colors: colors,
),
),
);
}
}
class _GradientCircularProgressPainter extends CustomPainter {
final double strokeWidth;
final double radius;
final bool strokeCapRound;
final double value;
final Color backgroundColor;
final List<Color> colors;
_GradientCircularProgressPainter({
this.strokeWidth,
this.radius,
this.strokeCapRound,
this.value,
this.backgroundColor,
this.colors,
});
@override
void paint(Canvas canvas, Size size) {
double _value = (value ?? 0.0) * pi * 2;
double _offset = strokeWidth / 2;
double _start =
strokeCapRound ? asin(strokeWidth / (radius * 2 - strokeWidth)) : 0.0;
// 绘制逻辑
// Rect rect = Offset.zero & size;
Rect rect = Offset(_offset, _offset) &
Size(size.width - strokeWidth, size.height - strokeWidth);
Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..color = backgroundColor;
canvas.drawCircle(Offset(radius, radius), radius - _offset, paint);
if (_value > 0) {
paint
..shader = SweepGradient(
startAngle: 0.0,
endAngle: _value,
colors: colors,
).createShader(rect)
..strokeCap = strokeCapRound ? StrokeCap.round : StrokeCap.butt;
canvas.drawArc(rect, _start, _value, false, paint);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
调用
import 'package:flutter/material.dart';
import './components/go.dart';
import './components/circularProgress.dart';
main(List<String> args) {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('自定义组件'),
),
body: Center(
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.all(20.0),
child: Go(),
),
Divider(),
Padding(
padding: EdgeInsets.only(top: 30.0),
child: CircularProgress(
radius: 50.0,
colors: [Colors.black, Colors.yellow, Colors.green],
strokeWidth: 15.0,
value: 0.5,
),
),
],
),
),
),
);
}
}
路由管理
页面
路由在移动开发中通常指页面,Route在Android中通常指一个Activity,在iOS中指一个ViewController。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。Flutter中的路由管理和原生开发类似,无论是Android还是iOS,导航管理都会维护一个路由栈,路由入栈操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。
页面跳转
MaterialPageRoute
- MaterialPageRoute继承自PageRoute类,表示一个模态路由页面,它定义了路由构建及切换时过渡动画的相关接口及属性。
- 对于Android,打开页面会从屏幕底部滑动到屏幕顶部;关闭页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
- 对于iOS,打开页面会从屏幕右侧边缘一致滑动到屏幕左边;关闭页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。
参数:
- builder 一个WidgetBuilder类型的回调函数,用于构建路由页面的具体内容。
- settings 包含路由的配置信息,如路由名称、是否初始路由(首页)。
- maintainState 在路由没用的时候释放其所占用的所有资源,可以设置为false。
- fullscreenDialog 表示新的路由页面是否是一个全屏的模态对话框。
// 页面跳转
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NewRoute(), // 打开的页面NewRoute
),
).then(data=>{
// 接收返回参数
});
// 返回上一页
Navigator.pop(context, value); // valu返回上一页参数
- 案例
main.dart
import 'package:flutter/material.dart';
import './pages/myHome.dart';
main(List<String> args) => runApp(MaterialApp(
home: MyHome(),
));
./pages/myHome.dart
import 'package:flutter/material.dart';
import 'package:flutter_app/pages/goodsList.dart';
class MyHome extends StatefulWidget {
@override
_MyHomeState createState() => _MyHomeState();
}
class _MyHomeState extends State<MyHome> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('主页'),
),
body: Center(
child: Column(
children: <Widget>[
RaisedButton(
child: Text('商品列表'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => GoodsList()),
);
},
),
],
),
),
);
}
}
页面传值
很多时候,在路由跳转时我们需要带一些参数,比如打开商品详情页时,我们需要带一个商品id,这样商品详情页才知道展示哪个商品信息;又比如我们在填写订单时需要选择收货地址,打开地址选择页并选择地址后,可以将用户选择的地址返回到订单页等。
Navigator
-
Navigator是一个路由管理的组件,它提供了打开和退出路由页方法。
-
Navigator提供了两个常用方法来管理路由栈:
// 将给定的路由入栈 Future push(BuildContext context, Route route) // 将栈顶路由出栈 bool pop(BuildContext context, [ result ])
-
Navigator 还有很多其它方法,如Navigator.replace、Navigator.popUntil等。
-
Navigator继承自StatefulWidget,of方法可以返回其State实例:
static NavigatorState of( BuildContext context, })
案例:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => GoodsDetail(id: 5)), // 传值id
);
class GoodsDetail extends StatelessWidget {
final int id;
GoodsDetail({
Key key,
@required this.id,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('商品详情-$id'),
),
);
}
}
路由表
- 要想使用命名路由,我们必须先提供并注册一个路由表。
- 注册路由表就是给路由起名字,它是一个Map类型数据。
- 路由表的key为路由的名字,是个字符串。
- value是个builder回调函数,用于生成相应的路由widget。
- 通过命名路由打开新路由时,应用会根据路由名字在路由表中查找到对应的WidgetBuilder回调函数,然后调用该回调函数生成路由widget并返回。
Map<String, WidgetBuilder> routes;
案例:
main.dart
import 'package:flutter/material.dart';
import './router/index.dart' as router;
main(List<String> args) {
return runApp(
MaterialApp(
initialRoute: '/', // 初始路由
routes: router.routes, // 路由表
),
);
}
router/index.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter_app/pages/goodsDetail.dart';
import 'package:flutter_app/pages/goodsList.dart';
import 'package:flutter_app/pages/myHome.dart';
Map routes = <String, WidgetBuilder>{
'/': (context) => MyHome(),
'/goodsList': (context) => GoodsList(),
'/detail': (context) => GoodsDetail(),
};
命名路由跳转
Navigator.pushNamed(context, routerUrl); // routerUrl路由名称
RaisedButton(
child: Text('商品列表'),
onPressed: () {
Navigator.pushNamed(context, '/goodsList');
},
),
命名路由传参
- 要通过路由名称来打开新路由,可以使用Navigator 的pushNamed方法
- 命名路由可以通过参数来传参
- 当命名路由不接受参数时,还可以通过RouteSettings来传参
- 获取RouteSettings参数可通过ModalRoute来获取
Navigator.pushNamed(context, '/goodsDetail', arguments: 2);
接收命名路由参数
int id = ModalRoute.of(context).settings.arguments;
import 'package:flutter/material.dart';
class GoodsDetail extends StatelessWidget {
@override
Widget build(BuildContext context) {
int id = ModalRoute.of(context).settings.arguments;
return Scaffold(
appBar: AppBar(
title: Text('商品详情ID-$id'),
),
);
}
}
onGenerateRoute
MaterialApp有一个onGenerateRoute属性,它在打开命名路由时可能会被调用,之所以说可能,是因为当调用Navigator.pushNamed(…)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。
onGenerateRoute
有了onGenerateRoute回调,要实现上面控制页面权限的功能就非常容易:
放弃使用路由表,取而代之的是提供一个onGenerateRoute回调,然后在该回调中进行统一的权限控制。
main配置
main.dart
import 'package:flutter/material.dart';
import './router/index.dart' as router;
main(List<String> args) {
return runApp(
MaterialApp(
initialRoute: '/', // 初始路由
onGenerateRoute: router.generateRoute, // 统一的权限控
),
);
}
路由守卫
router/index.dart
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_app/pages/goodsDetail.dart';
import 'package:flutter_app/pages/goodsList.dart';
import 'package:flutter_app/pages/myHome.dart';
import 'package:flutter_app/pages/noLogin.dart';
Map routes = {
'/': {
'isLogin': false,
'components': (settings) => MyHome(),
},
'/goodsList': {
'isLogin': true,
'components': (settings) => GoodsList(),
},
'/goodsDetail': {
'isLogin': false,
'components': (settings) => GoodsDetail(id: settings.arguments),
},
};
Route generateRoute(RouteSettings settings) {
return MaterialPageRoute(builder: (context) {
// 路由名称
String routeName = settings.name;
var routeData = routes[routeName];
// 是否进行登录检测
if (routeData['isLogin']) return NoLogin();
return routeData['components'](settings);
});
}
路由跳转
Navigator.pushNamed(context, '/goodsDetail', arguments: 2);
接收路由参数
class GoodsDetail extends StatelessWidget {
final int id;
GoodsDetail({
Key key,
@required this.id,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('商品详情-$id'),
),
);
}
}
生命周期
生命周期概述
生命周期是一个组件加载到卸载的整个周期,熟悉生命周期可以让我们在合适的时机做该做的事情,flutter中无论是普通 Widget还是 App,框架都给我们提供了生命周期的回调。深入理解生命周期,可以让我们写出更加连贯流畅、体验优良的程序。
-
普通视图的生命周期
通过父 Widget 初始化时传入的静态配置,StatelessWidget 就能完全控制其静态展示。 而 StatefulWidget,还需要借助于 State 对象,在特定的阶段来处理用户的交互或其内部数据的变化。 视图的生命周期主要通过State来体现。
-
App的生命周期
App 则是一个特殊的 Widget。 App除了需要处理视图显示的各个阶段之外,还需要应对应用从启动到退出所经历的各个状态。
State生命周期
State 的生命周期,指的是在用户参与的情况下,其关联的 Widget 所经历的,从创建到显示再到更新最后到停止,直至销毁等各个过程阶段。
创建阶段:
- 构造方法 构造方法是 State 生命周期的起点,通过调用createState() 来创建一个 State
- initState 该方法会在 State 对象被插入视图树的时候调用,整个生命周期只会被调用一次
- didChangeDependencies 用来处理 State 对象依赖关系变化,会在 initState() 调用结束后被调用
- build 作用是构建视图,该方法会创建一个 Widget 然后返回。
更新阶段:
- setState 当状态数据发生变化时,我们可以通过调用该方法通知框架刷新视图。
- didChangeDependencies State 对象的依赖关系发生变化后,框架会回调该方法,随后触发组件构建。
- didUpdateWidget 当 Widget 的配置发生变化时,系统会调用这个函数。
- 以上三个方法被调用,框架就会销毁老Widget,并调用build方法重建Widget。
销毁阶段:
-
deactivate 当组件的可见状态发生变化时,该方法会被调用。
-
dispose 当 State 被永久地从视图树中移除时,框架会调用该方法。
-
页面切换时,由于 State 对象在视图树中的位置发生了变化,需要先暂时移除后再重新添加,重新触发组件构建,
此deactivate函数也会被调用。
-
一旦进入dispose函数,组件就要被销毁了,所以我们可以在这里进行最终的资源释放、移除监听、清理环境,等等。
import 'package:flutter/material.dart';
import 'package:flutter_app/detail.dart';
import 'package:flutter_app/showNum.dart';
class MyHome extends StatefulWidget {
@override
_MyHomeState createState() => _MyHomeState();
}
class _MyHomeState extends State<MyHome> {
int _num = 0;
// 构造函数
_MyHomeState() {
print('MyHome 构造函数');
}
@override
void initState() {
super.initState();
print('MyHome initState');
}
@override
Widget build(BuildContext context) {
print('MyHome build构建视图');
return Scaffold(
appBar: AppBar(
title: Text('生命周期'),
),
body: Column(
children: <Widget>[
RaisedButton(
child: Text('++'),
onPressed: () {
setState(() {
print('setState');
_num++;
});
},
),
RaisedButton(
child: Text('detail'),
onPressed: () {
// 进入页面,退出页面pop时会触发销毁deactivate、dispose
Navigator.push(
context,
MaterialPageRoute(builder: (BuildContent) => Detail()),
);
},
),
// _num数值更新,导致组件里didUpdateWidget发生变化
ShowNum(num: _num),
],
),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('MyHome didChangeDependencies');
}
@override
void didUpdateWidget(MyHome oldWidget) {
super.didUpdateWidget(oldWidget);
print('---- MyHome ----');
print('MyHome didUpdateWidget');
print(oldWidget); // oldWidget获取之前的值
}
}
App生命周期
视图的生命周期,定义了视图的加载到构建的全过程;而 App 的生命周期,则定义了 App 从启动到退出的全过程。
在原生开发中,我们可以通过重写 Activity、ViewController 生命周期回调方法,或是注册应用程序的相关通知,来监听 App 的生命周期并做相应的处理。而在 Flutter 中,我们可以利用 WidgetsBindingObserver 类,来实现同样的需求。
WidgetsBindingObserver
WidgetsBindingObserver 提供的了很多回调函数,常见的屏幕旋转、屏幕亮度、语言变化、内存警告都可以通过这个实现进行回调。我们通过给 WidgetsBinding 的单例对象设置监听器,就可以监听对应的回调方法。
AppLifecycleState 的枚举类,这个枚举类是 Flutter 对 App 生命周期状态的封装。
- resumed 可见的,并能响应用户的输入。
- inactive 处在不活动状态,无法处理用户响应。
- paused 不可见并不能响应用户的输入,但是在后台继续活动中。
帧绘制回调
-
WidgetsBinding 提供了单次 Frame 绘制回调、实时 Frame 绘制回调两种机制。
-
单次 Frame 绘制回调,通过 addPostFrameCallback 实现。
WidgetsBinding.instance.addPostFrameCallback((_){ print("单次Frame绘制回调");//只回调一次 });
-
实时 Frame 绘制回调,则通过 addPersistentFrameCallback 实现。
WidgetsBinding.instance.addPersistentFrameCallback((_){ print("实时Frame绘制回调");//每帧都回调 });
案例
import 'package:flutter/material.dart';
import 'package:flutter_app/detail.dart';
import 'package:flutter_app/showNum.dart';
class MyHome extends StatefulWidget {
@override
_MyHomeState createState() => _MyHomeState();
}
// WidgetsBindingObserver
class _MyHomeState extends State<MyHome> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this); // 注册监听
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
print('单次渲染回调');
});
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) {
print('每次渲染都回调');
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('生命周期'),
),
);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.resumed:
print('当前应用处于活跃状态');
break;
case AppLifecycleState.inactive:
print('不活跃状态');
break;
case AppLifecycleState.paused:
print('后台运行');
break;
case AppLifecycleState.detached:
print('app与任何视图分离');
break;
case AppLifecycleState.resumed:
print('请求网络');
break;
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this); // 解除监听
super.dispose();
}
}
动画
简介
在任何系统的UI框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观。不同的UI系统对动画都进行了一些抽象,比如在Android中可以通过XML来描述一个动画然后设置给View。Flutter中也对动画进行了抽象,主要涉及Animation、Curve、Controller、Tween这四个角色,它们一起配合来完成一个完整动画。
Animation
- Animation是一个抽象类,主要的功能是保存动画的插值和状态。
- Animation对象是一个在一段时间内依次生成一个区间值的类。
- Animation对象在整个动画执行过程中输出的值由Curve来决定。
- Animation对象的控制方式,可以正向运行,也可以反向运行,甚至在中途切换方向。
Curve
- 通过Curve用来描述动画过程,匀速动画为线性的,而非匀速动画称为非线性的。
- CurvedAnimation和AnimationController都是Animation类型。
- CurvedAnimation可以通过包装AnimationController和Curve生成一个新的动画对象。
final CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
Curves曲线 | 动画过程 |
---|---|
linear | 匀速 |
decelerate | 匀减速 |
ease | 开始加速,后减速 |
easeIn | 开始慢,后加速 |
easeOut | 开始快,后减速 |
easeInOut | 开始慢,后加速,最后减速 |
AnimationController
- AnimationController用于控制动画,它包含动画的启动forward()、停止stop() 、反向播放 reverse()等方法
- AnimationController会在动画的每一帧,就会生成一个新的值
- 默认情况下,生成从0.0到1.0(默认区间)的数字,可以通过lowerBound和upperBound来指定区间
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 20.0,
vsync: this
);
Ticker
- 当创建AnimationController时,需要传递一个vsync参数,它接收一个TickerProvider类型的对象,用于创建Ticker。
- Ticker是通过SchedulerBinding来添加屏幕刷新回调,这样一来,每次屏幕刷新都会调用TickerCallback。
- 使用Ticker来驱动动画会防止屏幕外动画消耗不必要的资源。
- 通常我们会将SingleTickerProviderStateMixin添加到State的定义中。
abstract class TickerProvider {
//通过一个回调创建一个Ticker
Ticker createTicker(TickerCallback onTick);
}
Tween
-
如果构建不同的范围或不同的数据类型动画值,可以使用Tween来添加映射。
-
Tween构造函数需要begin和end两个参数。
final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0); final Tween colorTween = new ColorTween(begin: Colors.transparent, end: Colors.black54);
-
要使用Tween对象,需要调用其animate()方法,然后传入一个控制器对象。
final AnimationController controller = new AnimationController( duration: const Duration(milliseconds: 500), vsync: this ); final Animation curve = new CurvedAnimation(parent: controller, curve: Curves.easeOut); Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);
动画监听
- addListener() 给Animation添加帧监听器,在每一帧都会被调用。
- addStatusListener() 给Animation添加“动画状态改变”监听器;动画开始、结束、正向或反向时会调用状态改变的监听器。
基础动画案例
import 'package:flutter/material.dart';
class AnimationBase extends StatefulWidget {
@override
_AnimationBaseState createState() => _AnimationBaseState();
}
// 需混入SingleTickerProviderStateMixin
class _AnimationBaseState extends State<AnimationBase>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
@override
void initState() {
super.initState();
// 设置动画控制器
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 3), // 动画时间
);
// 设置曲线
animation = CurvedAnimation(curve: Curves.bounceIn, parent: controller);
// 设置区间值
animation = Tween(begin: 0.0, end: 300.0).animate(animation);
// 监听动画实时渲染
animation.addListener(() {
setState(() {});
});
// 启动动画
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: animation.value, // animation.value 动画值
height: animation.value,
color: Colors.red,
),
);
}
// 销毁动画
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
AnimatedWidget
通过addListener()和setState() 来更新UI这一步其实是通用的,如果每个动画中都加这么一句是比较繁琐的。AnimatedWidget类封装了调用setState()的细节,并允许我们将widget分离出来。
import 'package:flutter/material.dart';
class AnimationWidget extends StatefulWidget {
@override
_AnimationWidgetState createState() => _AnimationWidgetState();
}
class _AnimationWidgetState extends State<AnimationWidget>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
@override
void initState() {
super.initState();
// 动画控制器
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 3),
);
// 设置曲线
animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
// 设置区间
animation = Tween(begin: 0.0, end: 300.0).animate(animation);
// 启动动画
controller.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('animatedWidget'),
),
body: Center(
child: AnimatedContainer(
animation: animation,
),
),
);
}
// 销毁动画
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
// 使用AnimatedWidget简化动画
class AnimatedContainer extends AnimatedWidget {
AnimatedContainer({
Key key,
Animation<double> animation,
}) : super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return Container(
width: animation.value, // animation.value 动画值
height: animation.value,
color: Colors.yellow,
);
}
}
AnimatedBuilder重构
用AnimatedWidget可以从动画中分离出widget,而动画的渲染过程仍然在AnimatedWidget中,这样不是很优雅,如果我们能把渲染过程也抽象出来,那就会好很多,而AnimatedBuilder正是将渲染逻辑分离出来。
import 'package:flutter/material.dart';
class AnimatedBuilderText extends StatefulWidget {
@override
_AnimatedBuilderTextState createState() => _AnimatedBuilderTextState();
}
class _AnimatedBuilderTextState extends State<AnimatedBuilderText>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation animation;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 3),
);
animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
animation = Tween(begin: 0.0, end: 300.0).animate(animation);
controller.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('AnimatedBuilder'),
),
body: BiggerTransition(
myChild: Container(
color: Colors.green,
),
myAnimation: animation,
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class BiggerTransition extends StatelessWidget {
final Widget myChild;
final Animation<double> myAnimation;
BiggerTransition({
Key key,
@required this.myChild,
@required this.myAnimation,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: myAnimation,
child: myChild,
builder: (context, child) => Container(
child: child,
width: myAnimation.value,
height: myAnimation.value,
),
),
);
}
}
动画状态
有四种动画状态,在AnimationStatus枚举类中定义:
枚举值 | 含义 |
---|---|
dismissed | 动画在起始点停止 |
forward | 动画在正向执行 |
reverse | 动画在反向执行 |
completed | 动画在终点停止 |
controller.addStatusListener((status) {
switch(status){
case AnimationStatus.dismissed:
print('动画在起点停止');
break;
case AnimationStatus.completed:
print('动画在终点停止');
break;
case AnimationStatus.forward:
print('动画在正向执行');
break;
case AnimationStatus.reverse:
print('动画在反向执行');
break;
}
});
路由过渡动画
-
MaterialPageRoute
Material组件库中提供了一个MaterialPageRoute组件,它可以使用和平台风格一致的路由切换动画,如在iOS上会左右滑动切换,而在Android上会上下滑动切换。
RaisedButton(
child: Text('上下切换'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute( // 路由上下切换
builder: (_) => GoodsList(),
),
);
},
),
-
CupertinoPageRoute
Cupertino组件库提供的iOS风格的路由切换组件,它实现的就是左右滑动切换。
RaisedButton(
child: Text('左右切换'),
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute( // 路由左右切换
builder: (_) => GoodsList(),
),
);
},
),
-
自定义路由切换动画
Flutter提供了PageRouteBuilder组件来自定义路由切换动画。
内置自定义路由动画
RaisedButton(
child: Text('自定义路由切换'),
onPressed: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
// RotationTransition 过渡动画
return RotationTransition(
turns: animation,
child: GoodsList(),
);
},
),
);
},
),
自定义路由动画
import 'package:flutter/material.dart';
class RotationRoute extends PageRoute {
final WidgetBuilder builder;
@override
final Color barrierColor;
@override
final String barrierLabel;
@override
final bool maintainState;
@override
final Duration transitionDuration;
@override
final bool opaque;
@override
final bool barrierDismissble;
RotationRoute({
this.barrierColor,
this.barrierLabel,
this.maintainState: true,
this.barrierDismissble: false,
this.transitionDuration: const Duration(milliseconds: 500),
this.opaque: true,
@required this.builder,
});
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) =>
builder(context);
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
// isActive路由状态判断
if (isActive) {
// 进入路由
return RotationTransition(
turns: animation,
child: builder(context),
);
} else {
// 退出路由
return ScaleTransition(
scale: animation,
child: builder(context),
);
}
}
}
// RotationRoute 自定义路由跳转器
RaisedButton(
child: Text('自定义路由切换'),
onPressed: () {
Navigator.push(
context,
RotationRoute(
builder: (_) => GoodsList(),
),
);
},
),
Hero动画
Hero指的是可以在路由(页面)之间“飞行”的widget,简单来说Hero动画就是在路由切换时,有一个共享的widget可以在新旧路由间切换。由于共享的widget在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会从旧路逐渐过渡到新路由中的指定位置,这样就会产生一个Hero动画。
InkWell(
child: Hero(
tag: 'avatar', // tag 标签标记
child: ClipOval(
child: Image.asset(
'assets/images/1.jpg',
width: 80.0,
height: 80.0,
),
),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => BigImage()),
);
},
),
import 'package:flutter/material.dart';
class BigImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头像大图'),
),
body: Hero(
tag: 'avatar', // tag标签标记,与上面对应
child: Center(
child: Image.asset('assets/images/1.jpg'),
),
),
);
}
}
交织动画
有些时候我们可能会需要一些复杂的动画,这些动画可能由一个动画序列或重叠的动画组成,要实现这种效果,使用交织动画(Stagger Animation)会非常简单。
交织动画需要注意一下几点:
- 要创建交织动画,需要使用多个动画对象(Animation)
- 一个AnimationController控制所有的动画对象
- 给每一个动画对象指定时间间隔(Interval)
所有动画都由同一个AnimationController驱动,无论动画需要持续多长时间,控制器的值必须在0.0到1.0之间,而每个动画的间隔(Interval)也必须介于0.0和1.0之间。对于在间隔中设置动画的每个属性,需要分别创建一个Tween 用于指定该属性的开始值和结束值。也就是说0.0到1.0代表整个动画过程,我们可以给不同动画指定不同的起始点和终止点来决定它们的开始时间和终止时间。
案例:
import 'package:flutter/material.dart';
class AnimationInterlaced extends StatefulWidget {
@override
_AnimationInterlacedState createState() => _AnimationInterlacedState();
}
class _AnimationInterlacedState extends State<AnimationInterlaced>
with TickerProviderStateMixin {
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 3000),
);
}
Future _playAnimation() async {
try {
await controller.forward().orCancel;
await controller.reverse().orCancel;
} on TickerCanceled {
// 动画中止报错
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('交织动画'),
),
body: Center(
child: InkWell(
onTap: _playAnimation,
child: Container(
width: 300.0,
height: 300.0,
decoration: BoxDecoration(
// color: Colors.green,
border: Border.all(color: Colors.green),
),
child: StagerAnimation(controller: controller), // child: Stag,
),
),
),
);
}
}
class StagerAnimation extends StatelessWidget {
final AnimationController controller;
Animation<EdgeInsets> padding;
Animation<double> height;
Animation<Color> color;
StagerAnimation({Key key, @required this.controller}) : super(key: key) {
// padding动画
padding = Tween<EdgeInsets>(
begin: EdgeInsets.only(left: 0),
end: EdgeInsets.only(left: 250.0),
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(0.0, 0.6, curve: Curves.bounceIn),
),
);
// color动画
color = ColorTween(
begin: Colors.transparent,
end: Colors.yellow,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(0.0, 0.6, curve: Curves.bounceIn),
),
);
// height动画
height = Tween<double>(
begin: 0.0,
end: 250.0,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(0.0, 0.6, curve: Curves.bounceIn),
),
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, widget) => Container(
alignment: Alignment.bottomLeft,
padding: padding.value,
child: Container(
color: color.value,
width: 50.0,
height: height.value,
),
),
);
}
}