熟悉了flutter的各种控件和相互嵌套的代码结构后,可以再加深一点难度:加入动画特效。
虽然flutter的内置Metarial控件已经封装好了符合其设计语言的动画特效,使开发者节约了不少视觉处理上的精力,比如点击或长按listTile控件时自带水波纹动画、页面切换时切入向上或向下的动画、列表上拉或下拉到尽头有回弹波纹等。flutter也提供了用户可自定义的动画处理方案,使产品交互更加生动亲切、富有情趣。
Flutter中封装了包含有值和状态(如forward向前播放,reverse向后播放,completed完成播放和dismissed释放退出)的Animation对象。把Animation
对象附加到控件中或直接监听动画对象属性, Flutter会根据对Animation
对象属性的变化,修改控件的呈现效果并重新构建控件树。
这次,敲一个APP的聊天页面,试试加入Animation
后的效果,再尝试APP根据运行的操作系统进行风格适配。
第一步 构建一个聊天界面
先创建一个新项目:
flutter create chatPage
进入main.dart,贴入如下代码:
import 'package:flutter/material.dart';
//程序入口
void main() {
runApp(new FriendlychatApp());
}
const String _name = "CYC"; //聊天帐号昵称
class FriendlychatApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp( //创建一个MaterialApp控件对象,其下可塞入支持Material设计语言特性的控件
title: "Friendlychat",
home: new ChatScreen(), //主页面为用户自定义ChatScreen控件
);
}
}
//单条聊天信息控件
class ChatMessage extends StatelessWidget {
ChatMessage({this.text});
final String text;
@override
Widget build(BuildContext context) {
return new Container(
margin: const EdgeInsets.symmetric(vertical: 10.0),
child: new Row( //聊天记录的头像和文本信息横向排列
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
margin: const EdgeInsets.only(right: 16.0),
child: new CircleAvatar(child: new Text(_name[0])), //显示头像圆圈
),
new Column( //单条消息记录,昵称和消息内容垂直排列
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(_name, style: Theme.of(context).textTheme.subhead), //昵称
new Container(
margin: const EdgeInsets.only(top: 5.0),
child: new Text(text), //消息文字
),
],
),
],
),
);
}
}
//聊天主页面ChatScreen控件定义为一个有状态控件
class ChatScreen extends StatefulWidget {
@override
State createState() => new ChatScreenState(); //ChatScreenState作为控制ChatScreen控件状态的子类
}
//ChatScreenState状态中实现聊天内容的动态更新
class ChatScreenState extends State<ChatScreen> {
final List<ChatMessage> _messages = <ChatMessage>[]; //存放聊天记录的数组,数组类型为无状态控件ChatMessage
final TextEditingController _textController = new TextEditingController(); //聊天窗口的文本输入控件
//定义发送文本事件的处理函数
void _handleSubmitted(String text) {
_textController.clear(); //清空输入框
ChatMessage message = new ChatMessage( //定义新的消息记录控件对象
text: text,
);
//状态变更,向聊天记录中插入新记录
setState(() {
_messages.insert(0, message); //插入新的消息记录
});
}
//定义文本输入框控件
Widget _buildTextComposer() {
return new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row( //文本输入和发送按钮都在同一行,使用Row控件包裹实现
children: <Widget>[
new Flexible(
child: new TextField(
controller: _textController, //载入文本输入控件
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(hintText: "Send a message"), //输入框中默认提示文字
),
),
new Container(
margin: new EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton( //发送按钮
icon: new Icon(Icons.send), //发送按钮图标
onPressed: () => _handleSubmitted(_textController.text)), //触发发送消息事件执行的函数_handleSubmitted
),
]
)
);
}
//定义整个聊天窗口的页面元素布局
Widget build(BuildContext context) {
return new Scaffold( //页面脚手架
appBar: new AppBar(title: new Text("Friendlychat")), //页面标题
body: new Column( //Column使消息记录和消息输入框垂直排列
children: <Widget>[
new Flexible( //子控件可柔性填充,如果下方弹出输入框,使消息记录列表可适当缩小高度
child: new ListView.builder( //可滚动显示的消息列表
padding: new EdgeInsets.all(8.0),
reverse: true, //反转排序,列表信息从下至上排列
itemBuilder: (_, int index) => _messages[index], //插入聊天信息控件
itemCount: _messages.length,
)
),
new Divider(height: 1.0), //聊天记录和输入框之间的分隔
new Container(
decoration: new BoxDecoration(
color: Theme.of(context).cardColor),
child: _buildTextComposer(), //页面下方的文本输入控件
),
]
),
);
}
}
运行上面的代码,可以看到这个聊天窗口已经生成,并且可以实现文本输入和发送了:
如上图标注的控件,最终通过放置在状态对象ChatScreenState
控件中的Scaffold
脚手架完成安置,小伙伴可以输入一些文本,点击发送按钮试试ListView
控件发生的变化。
当发送按钮IconButton
触发onPressed事件后调用_handleSubmitted
函数,在_handleSubmitted
中执行了setState()
方法,此时flutter根据setState()
中的变量_messages
变更重新渲染_messages
对象,然后大家就可以看到消息记录框ListView
中底部新增了一行消息。
由于ListView
中的每一行都是瞬间添加完成,没有过度动画,使交互显得非常生硬,因此向ListView
中的每个Item的加入添加动画效果,提升一下交互体验。
第二步 消息记录加入动效
-
改造
ChatScreen
控件
要让主页面ChatScreen
支持动效,要在它的定义中附加mixin类型的对象TickerProviderStateMixin
:
class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin { // modified
final List<ChatMessage> _messages = <ChatMessage>[];
final TextEditingController _textController = new TextEditingController();
...
}
-
向ChatMessage中植入动画控制器控制动画效果
class ChatMessage extends StatelessWidget {
ChatMessage({this.text, this.animationController}); //new 加入动画控制器对象
final String text;
final AnimationController animationController;
@override
Widget build(BuildContext context) {
return new SizeTransition( //new 用SizeTransition动效控件包裹整个控件,定义从小变大的动画效果
sizeFactor: new CurvedAnimation( //new CurvedAnimation定义动画播放的时间曲线
parent: animationController, curve: Curves.easeOut), //new 指定曲线类型
axisAlignment: 0.0, //new 对齐
child: new Container( //modified Container控件被包裹到SizeTransition中
margin: const EdgeInsets.symmetric(vertical: 10.0),
child: new Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
margin: const EdgeInsets.only(right: 16.0),
child: new CircleAvatar(child: new Text(_name[0])),
),
new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(_name, style: Theme.of(context).textTheme.subhead),
new Container(
margin: const EdgeInsets.only(top: 5.0),
child: new Text(text),
),
],
),
],
),
) //new
);
}
}
-
修改_handleSubmitted()处理函数
由于ChatMessage
对象的构造函数中添加了动画控制器对象animationController
,因此创建新ChatMessage
对象时也需要加入animationController
的定义:
void _handleSubmitted(String text) {
_textController.clear();
ChatMessage message = new ChatMessage(
text: text,
animationController: new AnimationController( //new
duration: new Duration(milliseconds: 700), //new 动画持续时间
vsync: this, //new TickerProviderStateMixin内默认的属性和参数
), //new
); //new
setState(() {
_messages.insert(0, message);
});
message.animationController.forward(); //new 启动动画
}
-
释放控件
由于附加了动效的控件比较耗费内存,当不需要用到此页面时最好释放掉这些控件,Flutter会在复杂页面中自动调用dispose()
释放冗余的对象,玩家可以通过重写dispose()
指定页面中需要释放的内容,当然由于本案例只有这一个页面,因此Flutter不会自动执行到dispose()
。
@override
void dispose() { //new
for (ChatMessage message in _messages) //new 遍历_messages数组
message.animationController.dispose(); //new 释放动效
super.dispose(); //new
}
按上面的代码改造完后,用R而不是r重启一下APP,可以把之前没有加入动效的ChatMessage
对象清除掉,使整体显示效果更和谐。这时候试试点击发送按钮后的效果吧~
可以通过调整在
_handleSubmitted
中AnimationController
对象的Duration
函数参数值(单位:毫秒),改变动效持续时间。可通过改变
CurvedAnimation
对象的curve
参数值,改变动效时间曲线(和CSS的贝塞尔曲线类似),参数值可参考Curves可以尝试使用FadeTransition替代SizeTransition,试试动画效果如何
实现了消息列表的滑动,但是这个聊天窗口还有很多问题,比如输入框的文本只能横向增加不会自动换行,可以空字符发送消息等,接下来就修复这些交互上的BUG,顺便再复习下setState()的用法。
第三步 优化交互
-
杜绝发送空字符
当TextField
控件中的文本正在被编辑时,会触发onChanged事件,我们通过这个事件检查文本框中是否有字符串,如果没有则点击发送按钮失效,如果有则可以发送消息。
class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
final List<ChatMessage> _messages = <ChatMessage>[];
final TextEditingController _textController = new TextEditingController();
bool _isComposing = false; //new 到ChatScreenState对象中定义一个标志位
...
}
向文本输入控件_buildTextComposer中加入这个标志位的控制:
Widget _buildTextComposer() {
return new IconTheme(
data: new IconThemeData(color: Theme.of(context).accentColor),
child: new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row(
children: <Widget>[
new Flexible(
child: new TextField(
controller: _textController,
onChanged: (String text) { //new 通过onChanged事件更新_isComposing 标志位的值
setState(() { //new 调用setState函数重新渲染受到_isComposing变量影响的IconButton控件
_isComposing = text.length > 0; //new 如果文本输入框中的字符串长度大于0则允许发送消息
}); //new
}, //new
onSubmitted: _handleSubmitted,
decoration:
new InputDecoration.collapsed(hintText: "Send a message"),
),
),
new Container(
margin: new EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton(
icon: new Icon(Icons.send),
onPressed: _isComposing
? () => _handleSubmitted(_textController.text) //modified
: null, //modified 当没有为onPressed绑定处理函数时,IconButton默认为禁用状态
),
),
],
),
),
);
}
当点击发送按钮后,重置标志位为false:
void _handleSubmitted(String text) {
_textController.clear();
setState(() { //new 你们懂的
_isComposing = false; //new 重置_isComposing 值
}); //new
ChatMessage message = new ChatMessage(
text: text,
animationController: new AnimationController(
duration: new Duration(milliseconds: 700),
vsync: this,
),
);
setState(() {
_messages.insert(0, message);
});
message.animationController.forward();
}
这时候热更新一下,再发送一条消息试试:
-
自动换行
当发送的文本消息超出一行时,会看到以下效果:
遇到这种情况,使用Expanded
控件包裹一下ChatMessage的消息内容区域即可:
...
new Expanded( //new Expanded控件
child: new Column( //modified Column被Expanded包裹起来,使其内部文本可自动换行
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(_name, style: Theme.of(context).textTheme.subhead),
new Container(
margin: const EdgeInsets.only(top: 5.0),
child: new Text(text),
),
],
),
), //new
...
第四步 IOS和安卓风格适配
flutter虽然可以一套代码生成安卓和IOS的APP,但是这两者有着各自的设计语言:Material和Cupertino。因此为了让APP能够更好的融合进对应的系统设计语言,我们要对页面中的控件进行一些处理。
-
引入IOS控件库:
前面已经引入Material.dart控件库,但还缺少了IOS的Cupertino控件库,因此在main.dart的头部中引入:
import 'package:flutter/cupertino.dart';
-
定义Material和Cupertino的主题风格
Material为默认主题,当检测到APP运行在IOS时使用Cupertino主题,因此在最外部定义操作系统各自适配的主题风格:
final ThemeData kIOSTheme = new ThemeData( //Cupertino主题风格
primarySwatch: Colors.orange,
primaryColor: Colors.grey[100],
primaryColorBrightness: Brightness.light,
);
final ThemeData kDefaultTheme = new ThemeData( //默认的Material主题风格
primarySwatch: Colors.purple,
accentColor: Colors.orangeAccent[400],
);
-
根据运行的操作系统判断对应的主题:
首先要引入一个用于识别操作系统的工具库,其内的defaultTargetPlatform值可帮助我们识别操作系统:
import 'package:flutter/foundation.dart';
到程序的入口控件FriendlychatApp
中应用对应的操作系统主题:
class FriendlychatApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: "Friendlychat",
theme: defaultTargetPlatform == TargetPlatform.iOS //newdefaultTargetPlatform用于识别操作系统
? kIOSTheme //new
: kDefaultTheme, //new
home: new ChatScreen(),
);
}
}
-
页面标题的风格适配
页面顶部显示Friendlychat的标题栏的下方,在IOS的Cupertino设计语言中没有阴影,与下面的应用主体通过一条灰色的线分隔开,而Material则通过标题栏下方的阴影达到这一效果,因此将两种特性应用到代码中:
// Modify the build() method of the ChatScreenState class.
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Friendlychat"),
elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0), //new 适配IOS的扁平化无阴影效果
body: new Container( //modified 使用Container控件,方便加入主题风格装饰
child: new Column( //modified
children: <Widget>[
new Flexible(
child: new ListView.builder(
padding: new EdgeInsets.all(8.0),
reverse: true,
itemBuilder: (_, int index) => _messages[index],
itemCount: _messages.length,
),
),
new Divider(height: 1.0),
new Container(
decoration: new BoxDecoration(color: Theme.of(context).cardColor),
child: _buildTextComposer(),
),
],
),
decoration: Theme.of(context).platform == TargetPlatform.iOS //new 加入主题风格
? new BoxDecoration( //new
border: new Border( //new 为适应IOS加入边框特性
top: new BorderSide(color: Colors.grey[200]), //new 顶部加入灰色边框
), //new
) //new
: null), //modified
);
}
-
发送按钮的风格适配
发送按钮在APP遇到IOS时,使用Cupertino风格的按钮:
// Modify the _buildTextComposer method.
new Container(
margin: new EdgeInsets.symmetric(horizontal: 4.0),
child: Theme.of(context).platform == TargetPlatform.iOS ? //modified
new CupertinoButton( //new 使用Cupertino控件库的CupertinoButton控件作为IOS端的发送按钮
child: new Text("Send"), //new
onPressed: _isComposing //new
? () => _handleSubmitted(_textController.text) //new
: null,) : //new
new IconButton( //modified
icon: new Icon(Icons.send),
onPressed: _isComposing ?
() => _handleSubmitted(_textController.text) : null,
)
),
获取最终代码请点击此处。
总结一下,为控件加入动画效果,就是把控件用动画控件包裹起来实现目的。动画控件有很多种,今天只选用了一个大小变化的控件SizeTransition
作为示例,由于每种动画控件内部的属性不同,都需要单独配置,大家可参考官网了解这些动画控件的详情。
除此之外为了适应不同操作系统的设计语言,用到了IOS的控件库和操作系统识别的控件库,这是跨平台开发中常用的功能。
好啦,flutter的入门笔记到本篇就结束了,今后将更新flutter的进阶篇和实战,由于近期工作任务较重,加上日常还有跟前端大神狐神学习golang的任务,以后的更新会比较慢,因此也欢迎大家到我的Flutter圈子中投稿,分享自己的成果,把这个专题热度搞起来,赶上谷歌这次跨平台的小火车,也可以加入flutter 中文社区(官方QQ群:338252156)共同成长,谢谢大家~