摘要:當發送按鈕觸發事件后調用函數,在中執行了方法,此時根據中的變量變更重新渲染對象,然后大家就可以看到消息記錄框中底部新增了一行消息。
熟悉了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)共同成長,謝謝大家~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93428.html
閱讀 466·2021-10-09 09:57
閱讀 470·2019-08-29 18:39
閱讀 811·2019-08-29 12:27
閱讀 3028·2019-08-26 11:38
閱讀 2667·2019-08-26 11:37
閱讀 1285·2019-08-26 10:59
閱讀 1376·2019-08-26 10:58
閱讀 989·2019-08-26 10:48