首先,我要对上一篇文章进行一点补充,在添加动画效果后,需要重新启动应用程序。使用重新启动而不是热重新加载,因为需要清除任何没有动画控制器的现有消息。
目前在我们的应用程序中,即使输入字段中没有文本,也会启用“发送”按钮,我们可以根据该字段是否包含要发送的文本来决定是否启用发送按钮,并更改按钮的外观。定义_isComposing,一个私有成员变量,只要用户在输入字段中键入,该变量就是true。
class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin { final List<ChatMessage> _messages = <ChatMessage>[]; final TextEditingController _textController = new TextEditingController(); bool _isComposing = false; //...要在用户与该字段交互时通知文本的更改,需要将onChanged回调传递给TextField构造函数。当它的值随着字段的当前值而变化时,TextField将调用此方法。在我们的onChanged回调中,当字段包含一些文本时,调用setState()将_isComposing的值更改为true。然后当_isComposing为false时,将onPressed参数修改为null。
class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin { //... 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) { setState((){ _isComposing = text.length > 0; }); }, onSubmitted: _handleSubmitted, decoration: new InputDecoration.collapsed(hintText: '发送消息'), ) ), new Container( margin: new EdgeInsets.symmetric(horizontal: 4.0), child: new IconButton( icon: new Icon(Icons.send), onPressed: _isComposing ? () => _handleSubmitted(_textController.text) : null ), ) ] ) ) ); //... }当文本字段被清除时,修改_handleSubmitted将_isComposing更新为false。
class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin { //... void _handleSubmitted(String text) { _textController.clear(); setState((){ _isComposing = false; }); ChatMessage message = new ChatMessage( text: text, animationController: new AnimationController( duration: new Duration(milliseconds: 300), vsync: this ) ); setState((){ _messages.insert(0, message); }); message.animationController.forward(); } //... }_isComposing变量现在控制发送按钮的行为和视觉外观。如果用户在文本字段中键入字符串,则_isComposing为true,按钮的颜色设置为Theme.of(context).accentColor。当用户按下按钮时,系统调用_handleSubmitted()。如果用户在文本字段中不输入任何内容,则_isComposing为false,该控件的onPressed属性设置为null,禁用发送按钮。框架将自动将按钮的颜色更改为Theme.of(context).disabledColor。
接下来,为了让应用程序的UI具有自然的外观,我们可以为TalkcasuallyApp类的build()方法添加一个主题和一些简单的逻辑。在此步骤中,我们可以定义应用不同主要和重点颜色的平台主题,还可以自定义发送按钮以在iOS上使用CupertinoButton,而在Android上使用质感设计的IconButton。
首先,定义一个名为kIOSTheme的新的ThemeData对象,其颜色为iOS(浅灰色、橙色)和另一个ThemeData对象kDefaultTheme,其颜色为Android(紫色、橙色)。在main.dart文件下添加下面的代码。
final ThemeData kIOSTheme = new ThemeData( primarySwatch: Colors.orange, primaryColor: Colors.grey[100], primaryColorBrightness: Brightness.light, ); final ThemeData kDefaultTheme = new ThemeData( primarySwatch: Colors.purple, accentColor: Colors.orangeAccent[400], );修改TalkcasuallyApp类以使用应用程序的MaterialApp控件的theme属性来更改主题。使用顶级的defaultTargetPlatform属性和条件运算符构建用于选择主题的表达式。
//... import 'package:flutter/foundation.dart'; //... class TalkcasuallyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: '谈天说地', theme: defaultTargetPlatform == TargetPlatform.iOS ? kIOSTheme : kDefaultTheme, home: new ChatScreen(), ); } } //...我们还需要将所选主题应用到AppBar控件,也就是应用程序UI顶部的横幅。elevation属性定义了AppBar的z坐标。z坐标值为0.0没有阴影(iOS),4.0的值具有定义的阴影(Android)。
class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin { //... Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text('谈天说地'), elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, ), //... //... }通过在_buildTextComposer方法中修改其Container父窗口控件来自定义发送图标。使用child属性和条件运算符构建一个用于选择按钮的表达式。
//... import 'package:flutter/cupertino.dart'; //... class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin { //... 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 Container( margin: new EdgeInsets.symmetric(horizontal: 4.0), child: Theme.of(context).platform == TargetPlatform.iOS ? new CupertinoButton( child: new Text('发送'), onPressed: _isComposing ? () => _handleSubmitted(_textController.text) : null ) : new IconButton( icon: new Icon(Icons.send), onPressed: _isComposing ? () => _handleSubmitted(_textController.text) : null ), ) ] ) ) ); } //... }将顶级Column包装在Container控件中,使其在上边缘呈浅灰色边框。这个边框将有助于在iOS上将AppBar与body区分开来。同时要在Android上隐藏边框,需要在AppBar应用上一个代码段的逻辑。
class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin { //... Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text('谈天说地'), elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, ), body: new Container( child: new 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(), ) ] ), decoration: Theme.of(context).platform == TargetPlatform.iOS ? new BoxDecoration( border: new Border( top: new BorderSide(color: Colors.grey[200])) ) : null ) ); } //... }