摘要:当发送按钮触发事件后调用函数,在中执行了方法,此时根据中的变量变更重新渲染对象,然后大家就可以看到消息记录框中底部新增了一行消息。
熟悉了flutter的各种控件和相互嵌套的代码结构后,可以再加深一点难度:加入动画特效。
虽然flutter的内置Metarial控件已经封装好了符合其设计语言的动画特效,使开发者节约了不少视觉处理上的精力,比如点击或长按listTile控件时自带水波纹动画、页面切换时切入向上或向下的动画、列表上拉或下拉到尽头有回弹波纹等。flutter也提供了用户可自定义的动画处理方案,使产品交互更加生动亲切、富有情趣。
Flutter中封装了包含有值和状态(如向前,向后,完成和退出)的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:[ new Container( margin: const EdgeInsets.only(right: 16.0), child: new CircleAvatar(child: new Text(_name[0])), //显示头像圆圈 ), new Column( //单条消息记录,昵称和消息内容垂直排列 crossAxisAlignment: CrossAxisAlignment.start, children: [ 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 { final List _messages = []; //存放聊天记录的数组,数组类型为无状态控件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: [ 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: [ 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 Statewith TickerProviderStateMixin { // modified final List _messages = []; 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:[ new Container( margin: const EdgeInsets.only(right: 16.0), child: new CircleAvatar(child: new Text(_name[0])), ), new Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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 默认属性和参数 ), //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 Statewith TickerProviderStateMixin { final List _messages = []; 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:[ 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:第四步 IOS和安卓风格适配[ new Text(_name, style: Theme.of(context).textTheme.subhead), new Container( margin: const EdgeInsets.only(top: 5.0), child: new Text(text), ), ], ), ), //new ...
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:[ 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)共同成长,谢谢大家~
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/93428.html
阅读 448·2021-10-09 09:57
阅读 444·2019-08-29 18:39
阅读 792·2019-08-29 12:27
阅读 3010·2019-08-26 11:38
阅读 2636·2019-08-26 11:37
阅读 1268·2019-08-26 10:59
阅读 1347·2019-08-26 10:58
阅读 966·2019-08-26 10:48