Google移動UI框架Flutter教程之Widget
- 2020 年 2 月 14 日
- 筆記
引言
在之間我已經介紹了關於Flutter的下載安裝以及配置,還有開發工具Android Studio的配置,還不知道的同學可以看看我這篇部落格——Google移動UI框架Flutter入門。這裡為什麼非要用Android Studio,我可以解釋一下。Android Studio是Google的親兒子,由Google一手開發,而Flutter也是Google推出的技術,所以在支援和兼容問題上,Android Studio是非常有優勢的。老話說得好,肥水不流外人田,Google內部肯定是將Android Studio對Flutter的優化做到最佳的。
Widget基本組件
那麼話不多說,我們先來熟悉一下關於Flutter的Widget組件,在Flutter中,一切皆組件,TextView、Image、Row、Column等等,都統稱組件。
1.文本組件(Text)
首先,我們就來了解一下文本組件(Text)。學過前端的同學對UI部分應該都很了解,那Flutter當然也沒有什麼特別的,無非也就是文本內容、大小、字體樣式、顏色等等的設置,那麼首先我們就先來編寫一個案例。找到lib目錄下的main.dart,我們將在這個文件中編寫程式碼。
import 'package:flutter/material.dart'; void main() { runApp(MyTextApp()); } /** * 文本組件(Text)的使用 */ class MyTextApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Text Demo', home: Scaffold( appBar: AppBar(title: Text('文本控制項的使用'),), body: Center( child: Text( '這是一個文本控制項', //文本內容 textAlign: TextAlign.center, //居中 maxLines: 1, //最大顯示行數 style: TextStyle( fontSize: 25.0, //字體大小 color: Colors.lightBlue, //字體顏色 ), //樣式 ), ), ), ); } }有語言基礎的同學相信可以很好理解這些程式碼,第一行導入了Material相關的類庫。程式會先執行main()方法,該方法又執行了runApp()方法,並將MyTextApp類作為參數傳遞。而MyTextApp類就是我們自定義的一個類,該類需要去繼承StatelessWidget,並重寫build()方法,該方法需要返回一個組件。具體的程式碼我就不一一介紹了,可以先不用理解每一行程式碼的意思。其中的Text便是文本組件,只需將值寫入括弧,便可以在文本框中顯示,然後是文本框的一些屬性。接下來我們運行起來看一下。

2.圖片組件(Image)
接下來是圖片組件,圖片組件的作用無非就是顯示圖片,在Flutter中,Image有四種方式顯示圖片,我只介紹一種,就是顯示網路圖片,其它三種方式沒有太大差別。
import 'package:flutter/material.dart'; void main() { runApp(MyImageApp()); } /** * 圖片組件(Image)的使用 */ class MyImageApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Text Demo', home: Scaffold( appBar: AppBar(title: Text('圖片組件的使用')), body: Center( child: Image.network( 'https://www.baidu.com/img/baidu_jgylogo3.gif', //圖片地址 scale: 1.0, //縮放比 ), ), ), ); } }運行效果如下:

3.列表組件(ListView)
列表組件在移動端的開發中使用非常頻繁,那麼在Flutter中,該如何使用ListView呢?
import 'package:flutter/material.dart'; void main() { runApp(MyListViewApp()); } /** * 列表組件(List)的使用 */ class MyListViewApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Text Demo', home: Scaffold( appBar: AppBar(title: Text('圖片組件的使用')), body: Center( child: Container( height: 200.0, child: ListView( scrollDirection: Axis.horizontal,,//列表方向(縱向) children: <Widget>[ Container( width: 180.0, color: Colors.lightBlue, ), Container( width: 180.0, color: Colors.amber, ), Container( width: 180.0, color: Colors.deepOrange, ), Container( width: 180.0, color: Colors.deepPurple, ), //Container ], //Widget[] ), ), ), ), ); } }有些同學看到這樣的程式碼可能驚呆了,這麼多層的嵌套維護起來豈不是很麻煩,其實這也是Dart語法的特點,避免不了,但是還是有辦法的,我們可以把ListView單獨抽出來,這樣主體的程式碼將會簡潔很多。
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'FlutterApp', home: Scaffold( //主頁 appBar: AppBar(title: Text('FlutterDemo')), //標題 body: Center( child: Container( height: 200.0, child: MyList(), //ListView ), //Container ), //主體 ), //Scaffold ); //MaterialApp } } class MyList extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( scrollDirection: Axis.horizontal, children: <Widget>[ Container( width: 180.0, color: Colors.lightBlue, ), Container( width: 180.0, color: Colors.amber, ), Container( width: 180.0, color: Colors.deepOrange, ), Container( width: 180.0, color: Colors.deepPurple, ), //Container ], //Widget[] ); } }現在運行看一下效果。

當然,這樣編寫列表在實際開發中是不現實的,我們應該讓列表活起來,所以,下面介紹如何實現動態列表。
import 'package:flutter/material.dart'; void main() { runApp(MyApp( items: List < String>.generate(1000, (i) => "Item $i") ) ); } class MyApp extends StatelessWidget { final List<String> items; MyApp({Key key, @required this.items}) :super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'FlutterApp', home: Scaffold( //主頁 appBar: AppBar(title: Text('FlutterDemo')), //標題 body: ListView.builder( itemCount: items.length, itemBuilder: (context, index) { return ListTile( title: Text('${items[index]}'), ); }, ), ), //Scaffold ); //MaterialApp } }這樣就實現了動態列表,只不過這個數據還是自己提供的,只需要後期通過網路獲取數據再封裝成集合然後傳遞即可。不懂Dart語法的同學對於裡面的某些程式碼可能會覺得難以理解,但是不用擔心。即使沒有一點Dart語言基礎的同學也是可以很容易地學會Flutter的,只不過在某些Dart語法上就只能死記了,記住它,不用管為什麼。那麼現在來運行看下效果。

4.列表組件(GridView)
第二個列表組件,網格組件,該組件在如今的移動應用中也非常常見,最典型的便是系統相冊。那麼我們關心的是在Flutter中該如何去使用GridView呢?通過一個例子來了解一下。
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'FlutterApp', home: Scaffold( //主頁 appBar: AppBar(title: Text('FlutterDemo')), //標題 body: GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 2.0, //縱軸邊距 crossAxisSpacing: 2.0, //橫軸邊距 childAspectRatio: 0.7 //縮放比例(寬高比) ), children: <Widget>[ Image.network( 'http://img5.mtime.cn/mg/2019/05/31/163641.36482297_270X405X4.jpg', fit: BoxFit.cover), Image.network( 'http://img5.mtime.cn/mg/2019/07/01/091243.35485139_270X405X4.jpg', fit: BoxFit.cover), Image.network( 'http://img5.mtime.cn/mg/2019/06/28/141449.40971533_270X405X4.jpg', fit: BoxFit.cover), Image.network( 'http://img5.mtime.cn/mg/2019/05/31/163641.36482297_270X405X4.jpg', fit: BoxFit.cover), Image.network( 'http://img5.mtime.cn/mg/2019/07/01/091243.35485139_270X405X4.jpg', fit: BoxFit.cover), Image.network( 'http://img5.mtime.cn/mg/2019/06/28/141449.40971533_270X405X4.jpg', fit: BoxFit.cover), ], //Widget[] ) //GridView ), //Scaffold ); //MaterialApp } }網格組件其實也非常的簡單,和ListView其實沒有什麼差別,最主要的就是它獨特的屬性,這些屬性在官網文檔中都有解釋和示例。那麼這段程式碼運行的效果如何呢?我們看一下:

布局
Flutter中基本的一些組件就介紹完了,但是光知道如何編寫組件可遠遠不夠,UI設計中的布局管理也尤為重要,那麼,我們繼續深入,了解一下Flutter中的布局。
1.水平布局(Row)
經過前面基本組件的學習,會發現Flutter無非就是一些組件的嵌套,但注意嵌套級別,不要被自己的程式碼搞暈了,那麼布局其實是一樣的。我們看一個例子。
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Row Widget Demo', home: Scaffold( appBar: AppBar( title: Text('水平方向布局'), ), //AppBar body: Row( children: <Widget>[ RaisedButton( onPressed: () {}, color: Colors.redAccent, child: Text('Red Button'), ),RaisedButton( onPressed: () {}, color: Colors.orangeAccent, child: Text('Orange Button'), ),RaisedButton( onPressed: () {}, color: Colors.lightBlue, child: Text('Blue Button'), ), ], //Widget[] ), //Row ), //Scaffold ); //MaterialApp } }Row即是水平布局,那麼水平布局中我們放置了三個按鈕,現在,運行看效果。

會發現 ,這個按鈕的右邊空出了一塊,這是為什麼呢?其實是因為我們使用的是一個不靈活的水平布局,那麼既然有不靈活的水平布局,那就肯定會有靈活的水平布局。
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Row Widget Demo', home: Scaffold( appBar: AppBar( title: Text('水平方向布局'), ), //AppBar body: Row( children: <Widget>[ Expanded(child: RaisedButton( onPressed: () {}, color: Colors.redAccent, child: Text('Red Button'), )), Expanded(child: RaisedButton( onPressed: () {}, color: Colors.orangeAccent, child: Text('Orange Button'), )), Expanded(child: RaisedButton( onPressed: () {}, color: Colors.lightBlue, child: Text('Blue Button'), )), ], //Widget[] ), //Row ), //Scaffold ); //MaterialApp } }我們並沒有對程式碼進行過多的修改,只是在每個按鈕外部包了一個Expanded組件,那麼現在我們來看一下運行效果:

會發現,按鈕成功自適應螢幕了,這才是我們想要的效果。
2.垂直布局(Column)
既然有水平布局,當然就有垂直布局。現在通過一個例子來理解一下垂直布局。
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Row Widget Demo', home: Scaffold( appBar: AppBar( title: Text('水平方向布局'), ), //AppBar body: Column( children: <Widget>[ Text('Column 1'), Text('This is Column 2'), Text('Column 3'), ], //Widget ) //Column ), //Scaffold ); //MaterialApp } }應該不難理解,道理是一樣的,現在看一下效果:

細心的同學會發現,它默認會有一個居中的對齊方式。但有同學提出疑問了,這也沒居中啊,這不還是在螢幕的左側嗎?其實這個對齊是相對Column來說的,這個Column的大小是由最長的Text組件決定的。通過crossAxisAlignment屬性可以設置Column的對齊方式。
3.層疊布局(Stack)
使用水平布局和垂直布局雖然可以實現大部分的布局效果,但是如果要在一張圖片上顯示一段文字,這兩種布局將無法實現。所以,這裡我們學習一種層疊布局,它能夠很輕鬆地實現這個效果。
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { var stack = Stack( alignment: const FractionalOffset(0.5, 0.8), children: <Widget>[ CircleAvatar( backgroundImage: NetworkImage( 'https://i0.hdslb.com/bfs/archive/79c30cf5850cb9ec9d6129b200145e1644f696f8.jpg@880w_440h.jpg'), radius: 100.0, ), //CircleAvatar Container( decoration: BoxDecoration( color: Colors.lightBlue ), padding: EdgeInsets.all(5.0), child: Text('層疊布局'), ) ], //Widget[] ); //Stack return MaterialApp( title: 'Row Widget Demo', home: Scaffold( appBar: AppBar( title: Text('水平方向布局'), ), //AppBar body: Center( child: stack, ) ), //Scaffold ); //MaterialApp } }我們首先創建一個組件變數,將我們的圖片和文字都定義在裡面,然後通過alignment屬性可以決定文本組件的相對位置,看一下效果:

4.卡片布局(Card)
最後一個布局,卡片布局。來看例子。
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { var card = Card( child: Column( children: <Widget>[ ListTile( title: Text( '江西省南昌市青雲譜區', style: TextStyle(fontWeight: FontWeight.w500),), subtitle: Text('Temptation:123456789'), leading: Icon(Icons.account_box, color: Colors.lightBlue,), ), new Divider(), ListTile( title: Text( '北京市海淀區中國科技大學', style: TextStyle(fontWeight: FontWeight.w500),), subtitle: Text('Temptation:123456789'), leading: Icon(Icons.account_box, color: Colors.lightBlue,), ), new Divider(), ListTile( title: Text( '河南省濮陽市百姓辦公樓', style: TextStyle(fontWeight: FontWeight.w500),), subtitle: Text('Temptation:123456789'), leading: Icon(Icons.account_box, color: Colors.lightBlue,), ) //ListTile ], //Widget[] ), //Column ); //Card return MaterialApp( title: 'Row Widget Demo', home: Scaffold( appBar: AppBar( title: Text('水平方向布局'), ), //AppBar body: Center( child: card ) ), //Scaffold ); //MaterialApp } }運行看效果:

篇幅有限,關於Flutter的組件和布局就介紹到這裡,接下來還會有一篇關於Flutter的進階部落格,感興趣的同學可以看一看。
