一、前言
前一天,學習了Dart
語法,對Dart
的語法和特性有了更深一步的瞭解。今天,來學習Flutter
的基礎控制元件,身為Android
開發者都知道,一開始入坑Android
就要熟悉學習其控制元件,如:TextView
,ImageView
,Button
,ListView
,RecycleView
等。為什麼要學習呢?因為平時的開發都離不開這些控制元件,UI
的呈現都是有這些控制元件組成的,因此,其重要性就不用說了。對於Flutter
來講,基礎控制元件(widget
)就更加重要了。Flutter
和Android
有所不一樣,Android
佈局包含佈局(RelativeLayout,LinearLayout,ConstrainLayou)和元件。Flutter
的一切都是Widget
,包括最頂層佈局也是Widget
,一個頁面有很多很多的Widget
組合而成,Widget
也稱為裝飾品,視窗小部件。
二、Widget簡介
在Flutter
裡,UI
控制元件就是Widget
,Widget
根據不同的功能可以分為結構元素(如按鈕或選單),文字樣式(字型或者顏色方案),佈局屬性(如填充,對齊,居中),可以這麼理解,一個flutter
的頁面是有一棵樹型的Widget
組成,包括根節點,樹枝和樹葉,全都是Widget
,只是Widget
巢狀Widget
,那就可以用下面這張圖來表示:
Flutter
中,Widget
是一切的基礎,作為響應式渲染,屬於MVVM
的實現機制,通過修改資料,再用setState
設定資料,Flutter
會自動通過繫結的資料更新Widget
,所以在平時開發中,開發者需要的就是實現Widget
介面,和資料繫結起來。在平時,用的最多就是StatelessWidget
和StatefulWidget
這兩種Widget
,StatelessWidget
表示無狀態的,StatefulWidget
表示有狀態的。這裡怎麼理解呢?在Flutter
中每個頁面都是一幀,無狀態就是保持在那一幀,總而言之就是不能跟使用者互動,當有狀態的Widget
當資料更新時,其實是繪製了新的Widget
,也就是UI發生了變化,只是State
實現了跨幀資料同步儲存。這裡給大家說下,在Android Studio看原始碼的兩個工具:
左邊一欄Structure
結構(看當前檔案,win下的快捷鍵是(Alt+7
))和右邊Hierarchy
繼承關係(看當前類,win下快捷鍵是F4
)都可以幫助你閱讀原始碼。因為StatelessWidget
和StatefulWidget
用的最多,現在只需要用到這兩個,就先學習這兩個Widget
。
1.StatelessWidget
原始碼StatelessWidget
只有三個方法:
- const StatelessWidget({Key key}):super(key:key):初始化子類的[key]。這個key類是
Widget
、Element
、SemanticsNode
的唯一識別符號,是用來控制Widget
數中替換Widget
的時候使用的。 - StatelessElement createElement():建立一個[StatelessElement]來管理這個小部件在樹中的位置,原始碼解釋:子類重寫此方法是不常見的,那這個方法也不用管,只需要知道這個方法用來管理自身在
Widget
樹中的位置。 - Widget build(BuildContext context):描述這部件呈現使用者介面的部分。對於
StatelessWidget
,當Widget
第一次插入到樹中,或者父節點更改了配置和所依賴的[InheritedWidget]改變,都會被重新呼叫。
這裡說下如何啟動一個Flutter
應用,並使用Flutter
框架:
import 'package:flutter/material.dart';
void main() {
return runApp(Widget app);
}
複製程式碼
其實就是在main()
函式中呼叫runApp
函式。下面直接直接上例子,繼承StatelessWidget
,通過build
方法返回一個控制元件:
import 'package:flutter/material.dart';
//使用`flutter/material.dart` 目的是使用Matrial風格的小控制元件
void main(){
//執行程式
runApp(MyApp(null));
}
//繼承無狀態的StatelessWidget 使程式自身變為Wiget
class MyApp extends StatelessWidget{
//要顯示的內容
final String text;
//資料內容可以通過構造方法傳遞進來
MyApp(this.text);
//重寫build方法 返回你需要的控制元件
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
//紅色背景
color: Colors.red,
//高度 現在沒用 會撐滿整個螢幕
height: 200,
//寬度 執行效果會撐滿整個螢幕
width: 200,
//內容居中
alignment: Alignment.center,
//Text控制元件
child: new Text(
//Dart語法中 ?? 表示如果text為空,就會返回??號的內容
text ?? "my name is Knight",
textDirection: TextDirection.ltr,//需要加上這句不然報 RichText widgets require a Directionality widget ancestor.
),
);
}
}
複製程式碼
Widget
和Widget
之間通過child
進行巢狀,有些Widget
只能有一個child
。就像上面的Container
,有些Widget
可以有多個child
,像Colum
佈局。上面例子根佈局是Container
,Container
巢狀了Text
。
2.StatefulWidget
什麼是有狀態的控制元件呢?狀態是在建立控制元件可以同步讀取資訊,並且在控制元件的生命週期內可以改變,當控制元件狀態發生改變時使用State.setState
來及時更新,原始碼也是隻有三個方法:
StatelessWidget
一樣的,而createState()
這個方法原始碼註釋是:在Widget
樹中給定的位置建立此可變狀態的小部件,子類應該重寫此方法返回新建的,關聯子類的例項。當呼叫一個StatefulWidget
,框架就會呼叫createState
這個方法,當一個StatefulWidget
從Widget
樹中移除,再次插入樹中,那麼會再次呼叫createState
來建立一個新的State
物件,這樣做簡化了State
物件的生命週期。
需要建立管理的是主要是State
,StatefulWidget
用起來麻煩一些,他需要一個State
,例子如下:
//繼承StatefulWidget
class StateWidget extends StatefulWidget{
@override
State createState(){
return _StateWidget();
}
}
class _StateWidget extends State<StateWidget>{
//重寫build方法
@override
Widget build(BuildContext context){
}
}
複製程式碼
簡單觀察上面程式碼,大致流程還是和StatelessWidget
一樣的,build
方法照樣返回Widget
,不過在StatefulWidget
將這個方法放在createState
裡面。這裡細想一下,也知道為什麼要這樣做,因為當狀態改變,就會回撥createState
方法,重新呼叫build
方法重新建立UI,下面通過每兩秒改變UI這個例子來加深理解:
import 'package:flutter/material.dart';
//使用`flutter/material.dart` 目的是使用Matrial風格的小控制元件
import 'dart:async';//記得導庫
void main(){
//執行程式
runApp(StateWidget());
}
//控制元件繼承State
class _StateWidget extends State<StateWidget>{
int Number = 0;
String text;
//建構函式
_StateWidget(this.text);
@override
void initState(){
//初始化,這個函式在控制元件的生命週期內呼叫一次
super.initState();
print("進入initState");
//3秒後改變text的內容
new Future.delayed(const Duration(seconds: 3),(){
setState(() {
Number++;
text = "已經改變數值,數值現在是$Number";
});
});
}
@override
void dispose(){
//銷燬
super.dispose();
print('銷燬');
}
@override
void didChangeDependencies(){
//在initState之後調
super.didChangeDependencies();
print('進入didChange');
}
//重寫build方法
@override
Widget build(BuildContext context){
return Container(
//紅色背景
color: Colors.red,
//內容居中
alignment: Alignment.center,
//Text控制元件
child: new Text(
//Dart語法中 ?? 表示如果text為空,就會返回??號的內容
text ?? "沒改變數值",
textDirection: TextDirection.ltr,//需要加上這句不然報 RichText widgets require a Directionality widget ancestor.
),
);
}
}
複製程式碼
上面例子可以知道知道:在State
可以動態更改資料,在呼叫setState
後,改變的資料會除法Widget
重新構建,上面程式碼還寫了三個生命週期方法,這裡簡單說一下:
- initState:初始化操作
- didChangeDependencies:在
initState
之後呼叫,可以獲取其他State
- dispose:銷燬
平時開發中在build
實現佈局的擺放,把資料新增Widget
,通過setState
改變資料。那如果很高頻率取改變資料,效能肯定受影響,以下三點可以減少重新構建有狀態控制元件的影響:
- 樹根上儘量不用狀態控制元件,因為如果資料有變化樹根每次都更新,那就是整棵樹都要重建,把狀態用在樹葉上,這樣更新的時候只會更新自己。
- 減少
build
方法所建立的節點數量和控制元件數量。 - 利用快取,如果子樹中不更改,將子樹中快取起來,每次使用其子樹時重新使用它,學會重用思想。
- 儘可能使用
const
修飾控制元件。 怎麼去選擇有狀態和無狀態,最簡單就是可以跟使用者進行互動應該使用StatefulWidget
,例如:點選,滑動螢幕資訊流資料更新,如果只是僅僅顯示資料,那就可以選擇使用StatelessWidget
建立一個無狀態控制元件。
三、Flutter頁面
Flutter有顯示的Widget
和完整頁面呈現的Widget
,常見的有MaterialApp
、Scaffold
、Appbar
、Text
、Image
、FlatButton
,下面以表格形式簡單列一下:
1.MaterialApp
import 'package:flutter/material.dart';
//使用`flutter/material.dart` 目的是使用Matrial風格的小控制元件
void main(){
//執行程式
runApp(MyApp());
}
//用無狀態控制元件顯示
class MyApp extends StatelessWidget{
@override
Widget build(BuildContext context){
return MaterialApp(
//標題
title:'Widget_Demo',
//主題色
theme:ThemeData(
//設定為藍色
primarySwatch: Colors.blue
),
//這是一個Widget物件,用來定義當前應用開啟的時候,所顯示的介面
home:MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget{
@override
Widget build(BuildContext context){
return Scaffold(
//設定appbar
appBar:new AppBar(
title:new Text('This is a Demo'),
),
//主體
body:new Center(
//在螢幕中央顯示一個文字
child:new Text('Hello'),
),
);
}
}
複製程式碼
效果如下圖:
上面可以看到MaterialApp
作為了主介面入口。
2.Scaffold
上面例子home:MyHomePage()
這裡返回了Scaffold
Widget,而這個Widget
正是我們所看到的頁面,看到Scaffold
包含了appBar
和body
,一開始說到,Scaffold
也包含Drawers
,下面實現一下:
@override
Widget build(BuildContext context){
return Scaffold(
//設定appbar
appBar:new AppBar(
title:new Text('This is a Demo'),
),
//主體
body:new Center(
//在螢幕中央顯示一個文字
child:new Text('Hello'),
),
//左側抽屜
drawer:Drawer(
//新增一個空的ListView
child:ListView(),
),
);
}
複製程式碼
效果如下:
下面往抽屜裡新增點東西,就新增ListView
,程式碼如下:
//左側抽屜
drawer:Drawer(
child:ListView(
//設定padding
padding:EdgeInsets.zero,
children: <Widget>[
//據說這裡可以替換自定義的header
//userHeader,
ListTile(
//標題內容
title: Text("This is Item_one"),
//前置圖示
leading: new CircleAvatar(child:new Icon(Icons.scanner),),
),
ListTile(
//標題內容
title: Text("This is Item_two"),
//前置圖示
leading: new CircleAvatar(child:new Icon(Icons.list),),
),
ListTile(
//標題內容
title: Text("This is Item_three"),
//前置圖示
leading: new CircleAvatar(child:new Icon(Icons.score),),
),
],
),
),
複製程式碼
執行效果就是抽屜里加了三行內容的ListView
。
3.AppBar
下面設定一些AppBar
屬性,玩玩:
//設定appbar
appBar: new AppBar(
//AppBar內容顯示
title: new Text('This is a Demo'),
//前置圖示
leading: new Icon(Icons.home),
//背景顏色 改為紅色
backgroundColor: Colors.red,
//設定為標題內容居中
centerTitle: true,
//一個 Widget 列表,代表 Toolbar 中所顯示的選單,
// 對於常用的選單,通常使用 IconButton 來表示;對於不常用的選單通常使用 PopupMenuButton 來顯示為三個點,點選後彈出二級選單
actions: <Widget>[
//IconButton
new IconButton(
//圖示
icon: new Icon(Icons.add_a_photo),
//提示
tooltip: 'Add photo',
//點選事件
onPressed: () {},
),
//選單彈出按鈕
new PopupMenuButton<String>(
itemBuilder: (BuildContext context) {
return <PopupMenuItem<String>>[
new PopupMenuItem<String>(
value: "one", child: new Text('This one')),
new PopupMenuItem<String>(
value: "two", child: new Text('This two')),
];
},
//選擇點選事件
onSelected: (String action) {
switch (action) {
case "one":
//增加點選邏輯
break;
case "two":
//增加點選邏輯
break;
}
},
),
],
),
複製程式碼
效果如下:
可以看到,上面Appbar
上加了前置圖示、拍照圖示
、選單彈出按鈕、陰影。
4.Text
下面用Text
來展示文字,把上面例子用文字顯示中間的Hello
單獨抽出來,如下:
//主體
body: new Center(
//在螢幕中央顯示一個文字 改為自定義樣式
child: new CustomTextStyle('This is a Text'),
),
//單獨文字樣式
class CustomTextStyle extends StatelessWidget{
String text;
//建構函式 引數外部傳進來
CustomTextStyle(this.text);
@override
Widget build(BuildContext context){
return Text(text ?? "Hello");
}
}
複製程式碼
下面把文字字型大小修改,字型樣式修改,背景顏色改改:
//文字 : 單獨文字樣式
class CustomTextStyle extends StatelessWidget {
Paint pg = Paint();
String text;
//建構函式 引數外部傳進來
CustomTextStyle(this.text);
@override
Widget build(BuildContext context) {
//設定畫筆顏色為黑色
pg.color = Color(0xFF000000);
return Text(
text ?? "Hello",
style: TextStyle(
//顏色
color: Colors.blue,
//字型大小
fontSize: 14,
//字型加粗
fontWeight: FontWeight.bold,
//文字背景顏色
background: pg),
);
}
}
複製程式碼
上面效果是:
還有很多的屬性,根據需要去設定就行: const TextStyle({
this.inherit = true,
this.color,//文字樣式
this.fontSize,//字型大小
this.fontWeight,//繪製文字時的字型粗細
this.fontStyle,//字型變體
this.letterSpacing,//水平字母之間的空間間隔(邏輯畫素為單位),可以負值
this.wordSpacing,//單詞之間新增的空間間隔(邏輯畫素為單位),可以負值
this.textBaseline,//對齊文字的水平線
this.height,//文字行與行的高度,作為字型代銷的倍數
this.locale,//用於選擇區域定字形的語言環境
this.foreground,//文字的前景色,不能與color共同設定
this.background,//文字背景色
this.shadows,//Flutter Decoration背景設定(邊框,圓角,陰影,漸變等)
this.decoration,//繪製文字裝飾,新增上下劃線,刪除線
this.decorationColor,//文字裝飾的顏色
this.decorationStyle,//文字裝飾的樣式,控制畫虛線,點,波浪線
this.debugLabel,
String fontFamily,//使用字型的名稱
String package,
})
複製程式碼
5.RichText
這是顯示豐富樣式的文字,這什麼意思呢?Text
只能顯示一種樣式的文字,如果想在一段文字中顯示多種樣式,就好像Android
裡面的SpannableString
,就需要使用RichText
,直接上例子:
//富文字樣式
class RichWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RichText(
text: TextSpan(
text: 'This is RichText',
style: new TextStyle(
//false的時候不顯示
inherit: true,
//字型大小
fontSize: 16,
//黑色
color: Colors.black
),
children: <TextSpan>[
new TextSpan(
text: 'Android藝術探索',
style: new TextStyle(
color: Colors.redAccent,
//字型粗細
fontWeight: FontWeight.bold,
),
),
new TextSpan(text: '第一行程式碼'),
new TextSpan(
text: 'Android進階之光',
style: new TextStyle(
color: Colors.indigo,
//字型樣式
fontSize: 20,
),
)
],
)
);
}
}
//螢幕中間改為富文字widget
//主體
body: new Center(
//Text在螢幕中央顯示一個文字 改為自定義樣式
//child: new CustomTextStyle('This is a Text'),
//富文字
child:new RichWidget()
),
複製程式碼
效果如下:
6.TextField
下面看看文字輸入框,文字輸入框平時會經常用到:
body: new Center(
//Text在螢幕中央顯示一個文字 改為自定義樣式
//child: new CustomTextStyle('This is a Text'),
//富文字
//child:new RichWidget()
//文字輸入框
child:new TextFieldWidget()
),
//文字輸入框
class TextFieldWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return TextField();
}
}
複製程式碼
上面例子只能輸入文字內容,如果想要獲取輸入框內容,就要新增一個controller
,通過這個controller
新增通知來獲取TextField
的值,我們一般點選按鈕或者需要跟後臺互動就要讀取controller.text
的值:
class MyHomePage extends StatelessWidget {
//獲取TextEditingController
final editController = TextEditingController();
//IconButton
new IconButton(
//圖示
icon: new Icon(Icons.add_a_photo),
//提示
tooltip: 'Add photo',
//點選事件
onPressed: () {
//輸出
print('text inputted: ${editController.text}');
//Toast
Fluttertoast.showToast(
msg:'text inputted: ${editController.text}',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIos: 1,
);
},
),
....
//主體
body: new Center(
//Text在螢幕中央顯示一個文字 改為自定義樣式
//child: new CustomTextStyle('This is a Text'),
//富文字
//child:new RichWidget()
//文字輸入框 以建構函式傳遞controller
child:new TextFieldWidget(editController)
),
}
//文字輸入框
class TextFieldWidget extends StatelessWidget{
final controller;
//建構函式傳值
TextFieldWidget(this.controller);
@override
Widget build(BuildContext context){
return TextField(
controller: controller,
);
}
}
複製程式碼
注意上面用到了Toast
,Toast庫這裡很簡單需要兩步:
- 在
pubspec.yaml
新增依賴庫fluttertoast: ^2.1.1
- 匯入
import 'package:fluttertoast/fluttertoast.dart';
重新執行即可,熱過載可能會出現異常。執行在iOS模擬器需要裝brew
和CocoaPods
,有問題執行flutter doctor
,它真是如名字一樣,就是幫你診斷有沒有錯誤資訊,會顯示具體資訊。效果如下:
return TextField(
controller: controller,
//最大長度,右下角會顯示一個輸入數量的字串
maxLength: 26,
//最大行數
maxLines: 1,
//是否自動更正
autocorrect: true,
//是否自動對焦
autofocus: true,
//設定密碼 true:是密碼 false:不是祕密
obscureText: true,
//文字對齊樣式
textAlign: TextAlign.center,
);
複製程式碼
效果如下:
7.Image
Image
很好理解就是在介面上區域顯示一張圖片,而這張圖片的來源可以是:本地,網路,資源圖片等。下面一一演示一下:
7.1.專案圖片資源
首先新建一個資源目錄:
在pubspec.yaml
中配置圖片路徑,來識別應用程式所需的assets:
class MyHomePage extends StatelessWidget {
//主體
body: new Center(
.....
//圖片載入
child:new ImageWidget()
),
}
//圖片
class ImageWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
//專案資源圖片 方式一
return Image(
image: new AssetImage('images/Image_fluttericon.jpeg'),
);
//專案資源圖片 方式二
// return Image.asset('images/Image_fluttericon.jpeg');
}
}
複製程式碼
效果如下:
7.2.網路圖片載入
下面進行網路圖片載入,也是很簡單:
class MyHomePage extends StatelessWidget {
//圖片路徑
String image_url = "https://ws1.sinaimg.cn/large/0065oQSqgy1fze94uew3jj30qo10cdka.jpg";
//主體
body: new Center(
.....
//圖片載入
child:new ImageWidget(image_url)
),
}
//圖片
class ImageWidget extends StatelessWidget{
String image_url;
ImageWidget(this.image_url);
@override
Widget build(BuildContext context){
return Image.network(image_url);
}
}
複製程式碼
效果如下:
下面用一個庫來載入和快取網路影象,也可以與佔位符和錯誤小部件一起使用,在pubspec.yaml
新增依賴cached_network_image: ^0.4.1+1
,在Dart
檔案匯入這個庫import 'package:cached_network_image/cached_network_image.dart';
//圖片
class ImageWidget extends StatelessWidget{
String image_url;
ImageWidget(this.image_url);
@override
Widget build(BuildContext context){
return new CachedNetworkImage(
imageUrl: image_url,
//佔位符
placeholder: new CircularProgressIndicator(),
//載入錯誤時顯示的圖片
errorWidget: new Icon(Icons.error),
//寬高
width:200,
height: 200,
);
}
}
複製程式碼
當圖片還沒載入出來的時候會顯示佔位符,當如果載入出錯會顯示errorWidget
的圖片。
7.3.宣告解析度相關的圖片
另外Flutter可以為當前裝置新增合適其解析度的影象,其實對於Android原生來說,就是在不同解析度目錄下放置不同解析度的圖片,只不過flutter並不是建立drawable-xxdpi
檔案,而是建立以下資料夾:
.../logo.png
.../Mx/logo.png
.../Nx/logo.png
複製程式碼
其中M和N是數字識別符號,對應於其中包含的影象解析度,它們指定不同素裝置像比例的圖片,主資源預設對應於1.0倍的解析度圖片。看下面例子:
在裝置畫素比率為1.8的裝置上,images/2.0x/logo.png
將被選擇。對於2.7的裝置畫素比率,images/3.0x/logo.png
將被選擇。如果未在Image控制元件上指定渲染影象的寬度和高度,以便它將佔用與主資源相同的螢幕空間量(並不是相同的物理畫素),只是解析度更高。 也就是說,如果images/logo.png
是72px乘72px,那麼images/3.0x/logo.png
應該是216px乘216px; 但如果未指定寬度和高度,它們都將渲染為72畫素×72畫素(以邏輯畫素為單位)。pubspec.yaml
中asset部分中的每一項都應與實際檔案相對應,但主資源項除外。當主資源缺少某個資源時,會按解析度從低到的順序去選擇,也就是說1.0x中沒有的話會在2.0x中找,2.0x中還沒有的話就在3.0x中找。
return Image(
// 系統會根據解析度自動選擇不同大小的圖片
image: AssetImage('images/logo.png'),
// ...
),
複製程式碼
8.FlatButton
Flutter
預先定義了一些按鈕控制元件,如FlatButton
,RaisedButton
,OutlineButton
,IconButton
。
- FlatButton:扁平化按鈕,繼承自
MaterialButton
- RaisedButton:凸起按鈕,繼承自
MaterialButton
- OutlineButton:帶邊框按鈕,繼承自
MaterialButton
- IconButton:圖示按鈕,繼承自
StatelessWidget
下面看看FlatButton
,其他的只是樣式稍微不一樣,大致用法一樣。
//按鈕
class FlatButtonWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return FlatButton(
onPressed: (){
Fluttertoast.showToast(
msg:'你點選了FlatButton',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIos: 1,
);
},
child: Text('FlatButton'),
color: Colors.blue,//按鈕背景色
textColor: Colors.white,//文字的顏色
onHighlightChanged: (bool b){//水波紋變化回撥
},
disabledColor: Colors.black,//按鈕禁用時的顯示的顏色
disabledTextColor: Colors.black38,//按鈕被禁用的時候文字顯示的顏色
splashColor: Colors.white,//水波紋的顏色
);
}
}
複製程式碼
上面也設定了一些屬性,效果圖如下:
四、Flutter佈局
Flutter
中擁有30多種預定義的佈局widget
,常用的有Container
、Padding
、Center
、Flex
、Row
、Colum
、ListView
、GridView
。用一個表格列出它們的特性和使用。
1.Container
一個擁有繪製、定位、調整大小的widget
,示意圖如下:
class MyHomePage extends StatelessWidget {
....
body:new ContainWidget(),
...
}
//Container佈局
class ContainWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return Container(
child:Text("My name is Knight"),
color: Colors.indigo,
width:200,//寬
height:200,//高
margin:EdgeInsets.fromLTRB(5,5,5,5),//設定外邊距
padding:EdgeInsets.all(30),//內邊距
);
}
}
複製程式碼
下面設定邊框,新增圓角:
//Container佈局
class ContainWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return Container(
....
padding:EdgeInsets.all(30),//內邊距
decoration: BoxDecoration(//設定邊框
//背景色
color:Colors.redAccent,
//圓角
borderRadius: BorderRadius.circular(6),
),
);
}
}
複製程式碼
執行效果如下:
2.Padding
一個Widget
,會給其子Widget
新增指定的填充,示意圖如下:
class MyHomePage extends StatelessWidget {
....
body: new PaddingWidget(),
...
}
//Padding佈局
class PaddingWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return Padding(
//設定左上右下內邊距為4,10,6,8
padding:EdgeInsets.fromLTRB(4, 10, 6, 8),
child: Text('My name is Knight'),
);
}
}
複製程式碼
效果圖如下:
下面實現Container
巢狀Padding
:
//Container巢狀Padding
class ContainPaddWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return Container(
width:200,//寬
height:200,//高
child: Padding(
padding:EdgeInsets.fromLTRB(4, 10, 6, 8),
child: Text("My name is Knight"),
),
decoration: BoxDecoration(//設定邊框
//背景色
color:Colors.redAccent,
//圓角
borderRadius: BorderRadius.circular(6),
),
);
}
}
複製程式碼
效果圖如下:
3.Center
將其子widget
居中顯示在自身內部的widget
,示意圖:
//Center
class CenterWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return Container(
width:200,//寬
height:200,//高
child: Center(
child: Text("My name is Knight"),
),
decoration: BoxDecoration(//設定邊框
//背景色
color:Colors.redAccent,
//圓角
borderRadius: BorderRadius.circular(6),
),
);
}
}
複製程式碼
執行效果如下:
Center
作為Container
的孩子,Text
所以在佈局的中間。
4.Stack
可以允許其子Widget
簡單的堆疊在一起,層疊佈局,示意圖:
class MyHomePage extends StatelessWidget {
....
body:new Center(
child:new StackWidget()
),
...
}
//層疊佈局
class StackWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return Stack(
children: <Widget>[
new Image.network('https://ws1.sinaimg.cn/large/0065oQSqgy1fze94uew3jj30qo10cdka.jpg',
width:300.0,//寬
height:300.0,//高
),
new Opacity(
opacity: 0.6,//不透明度
child:new Container(
width:100.0,
height:100.0,
color:Colors.redAccent,
),
),
new Opacity(
opacity: 0.6,
child:new Container(
width: 200.0,
height:200.0,
color:Colors.indigo,
),
),
],
);
}
}
複製程式碼
執行效果:
可以看到控制元件都按Stack
左上角對齊,疊在一起,下面改一下顯示位置:
//層疊佈局
class StackWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return Stack(
//Aliginment的範圍是[-1,1],中心是[0,0].註釋有寫
//和Android一樣,左移的取值是往1取,右移是往-1取
//這裡注意,它是取stack裡範圍最大的佈局為基準,下面是以Container為//基準對齊
alignment: new Alignment(-0.6, -0.6),
...
);
}
}
複製程式碼
執行效果圖:
5.Colum
在垂直方向上排列子Widget
,示意圖如下:
class MyHomePage extends StatelessWidget {
...
body:new ColumnWidget(),
....
}
//Column佈局
class ColumnWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
color:Colors.blue,
width: 50,
height: 50,
),
Container(
color:Colors.black,
width:50,
height:50,
),
Container(
color:Colors.green,
width:50,
height:50,
),
],
);
}
}
複製程式碼
執行效果:
下面簡單設定一下排列方式屬性:return Column(
//設定垂直方向的對齊方式
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
...
);
複製程式碼
執行效果如下:
垂直方向(主軸上)屬性:- MainAxisAlignment.start這是預設值:垂直方向頂部對齊
- MainAxisAlignment.end:垂直方向底部對齊
- MainAxisAlignment.center:垂直方向居中對齊
- MainAxisAlignment.spaceBetween:垂直方向平分剩餘空間
- MainAxisAlignment.spaceAround:放置控制元件後,剩餘空間平分成n份,n是子
widget
的數量,然後把其中一份空間分成2份,放在第一個child的前面,和最後一個child的後面,也就是子widget
的之前之後之間均勻分割空閒的一半空間 - MainAxisAlignment.spaceEvenly:放置控制元件後,把剩餘空間平分n+1份,然後平分所有的空間,在子
widget
之前之後之間均勻的分割空閒的空間
下面列一下水平方向(交叉軸)的屬性:
- CrossAxisAlignment.center這是預設值,水平居中
- CrossAxisAlignment.end:水平方向右側對齊
- CrossAxisAlignment.start:水平方向左側對齊
- CrossAxisAlignment.stretch:水平方向拉伸子
child
填充滿布局 - CrossAxisAlignment.baseline:和
textBaseline
一起使用
6.Row
在水平方向上排列子widget
的列表,示意圖:
class MyHomePage extends StatelessWidget {
....
body:new RowWidget(),
...
}
//Row
class RowWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return Row(
children: <Widget>[
Container(
color:Colors.blue,
width: 50.0,
height:50.0,
),
Container(
color:Colors.black,
width:50.0,
height:50.0,
),
Container(
color:Colors.green,
width:50.0,
height:50.0,
),
],
);
}
}
複製程式碼
效果圖:
下面簡單設定一些屬性,和Column
沒多大差別:
return Row(
//把剩餘空間平分n+1份,然後平分所有的空間
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
...
);
複製程式碼
效果圖:
水平方向上(主軸上)屬性:- MainAxisAlignment.start這是預設值,水平方向頂部對齊
- MainAxisAlignment.center:水平方向居中對齊
- MainAxisAlignment.end:水平方向底部對齊
- MainAxisAlignment.spaceBetween:水平方向上平分剩餘空間
- MainAxisAlignment.spaceAround:放置控制元件後,剩餘空間平分成n份,n是子
widget
的數量,然後把其中一份空間分成2份,放在第一個child的前面,和最後一個child的後面,也就是子widget
的之前之後之間均勻分割空閒的一半空間 - MainAxisAlignment.spaceEvenly:放置控制元件後,把剩餘空間平分n+1份,然後平分所有的空間,在子
widget
之前之後之間均勻的分割空閒的空間 而交叉軸(垂直方向)的屬性:
- CrossAxisAlignment.center這是預設,垂直居中
- CrossAxisAlignment.end:垂直方向右側對齊
- CrossAxisAlignment.start:垂直方向左側對齊
- CrossAxisAlignment.stretch:垂直方向拉伸子
child
填充滿布局 - CrossAxisAlignment.baseline:和
textBaseline
一起使用
7.Expanded
Expanded
元件可以使Row
、Column
、Fiex
等子元件在其主軸上方向展開並填充可用的空間,這裡注意:Expanded
元件必須用在Row
、Column
、Fiex
內,並且從Expanded
到封裝它的Row
、Column
、Flex
的路徑必須只包括StatelessWidgets
或者StatefulWidgets
(不能是其他型別的元件,像RenderObjectWidget
,它是渲染物件,不再改變尺寸,因此Expanded
不能放進RenderObjectWidget
),示意圖如下:
class MyHomePage extends StatelessWidget {
....
body:new RowWidget(),
...
}
class RowWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return Row(
children: <Widget>[
new RaisedButton(
onPressed: (){
},
color:Colors.green,
child:new Text('綠色按鈕1')
),
new Expanded(
child:new RaisedButton(
onPressed: (){
},
color:Colors.yellow,
child:new Text('黃色按鈕2')
),
),
new RaisedButton(
onPressed:(){
},
color:Colors.red,
child:new Text('黑色按鈕3')),
],
);
}
}
複製程式碼
執行效果如下:
class MyHomePage extends StatelessWidget {
....
body:new RowWidget(),
...
}
class RowWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return Row(
children: <Widget>[
Expanded(
child:Container(
color:Colors.green,
padding:EdgeInsets.all(8),
height: 40.0,
),
flex:1,
),
Expanded(
child:Container(
color:Colors.yellow,
padding:EdgeInsets.all(8),
height: 40.0,
),
flex:2,
),
Expanded(
child:Container(
color:Colors.red,
padding:EdgeInsets.all(8),
height: 40.0,
),
),
],
);
}
}
複製程式碼
上面程式碼設定了flex
,將一行的寬度分成四等分,第一、三child
佔1/4的區域,第二個child
佔1/2區域。 效果如下:
8.ListView
我相信這個佈局在平時開發會經常用到,這是可滾動的列表控制元件,ListView
是最常用的滾動widget
,它在滾動方向上一個接一個地顯示它的孩子。在縱軸上,孩子沒被要求填充ListView
,並且內建ListTitle
,示意圖如下:
class MyHomePage extends StatelessWidget {
....
body: new ListViewWidget(
new List<String>.generate(1000,(i){
return 'Item &i';
}),
),
...
}
//ListView
class ListViewWidget extends StatelessWidget {
final List<String> items;
ListViewWidget(this.items);
@override
Widget build(BuildContext context) {
return new ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return new ListTile(
title: new Text('This is $index'),
);
},
);
}
}
複製程式碼
效果圖如下:
下面設定水平的ListView
:
class MyHomePage extends StatelessWidget {
....
body: new ListViewWidget(
new List<String>.generate(1000, (i) {
return 'Item &i';
}),
),
...
}
Widget build(BuildContext context) {
return new ListView.builder(
itemCount: items.length,
//設定水平方向
scrollDirection:Axis.horizontal,
//豎直時:確定每一個item的高度
//水平時:確定每一個item的寬度 得要設定 不然不顯示
itemExtent: 110.0,
itemBuilder: (context, index) {
return new ListTile(
title: new Text('This is $index'),
);
},
);
複製程式碼
效果如下:
9.GridView
GridView
是一個網格佈局的列元件。GridView
繼承至CustomScrollView
,示意圖如下:
//GridView
class GridViewWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return new GridView.count(
crossAxisCount: 3, //3列
children: List.generate(40,
(i){
return Card(
child: Center(
child:Text('This is $i'),
),
);
})
);
}
}
複製程式碼
下面上水平例子:
return new GridView.count(
//3行
crossAxisCount: 3,
//設定水平
scrollDirection: Axis.horizontal,
children: List.generate(40, (i) {
return Card(
child: Center(
child: Text('This is $i'),
),
);
}),
);
複製程式碼
效果圖如下:
10.TabBar
移動開發中tab
切換是一個很常用的功能,那麼Flutter
有沒有提供這個Widget
呢?答案是有的,Flutter
通過Material
庫提供了很方便的API來使用tab
切換。
10.1.建立TabController
TabBarView
和TabBar
都有一個TabController
的引數,TabbarView
和TabBar
就是由TabController
來控制同步,點選某個Tab
後,要同步顯示對應的TabBarView
,建立TabController
有兩種方式:
- 使用系統自帶的
DefaultTabController
,在Scaffold
套一層DefaultTabController
,這種方式TabBarView
會自動查詢這個tabController
。 - 自己定義一個
TabController
,實現SingleTickerProviderStateMixin
下面就列一下第一種方式:
@override
Widget build(BuildContext context) {
return new DefaultTabController();
}
複製程式碼
10.2.構建Tab資料
final List<Tab> myTabs = <Tab>[
new Tab(text: 'Android'),
new Tab(text: 'IOS'),
new Tab(text: 'Flutter'),
new Tab(text: 'RN'),
new Tab(text: 'Java'),
new Tab(text: 'C'),
new Tab(text: 'C++'),
new Tab(text: 'Go'),
];
複製程式碼
10.3.建立TabBar
TabBar
在哪裡都可以建立,在AppBar
裡有一個bottom
引數可以接受TabBar
,就放在AppBar
下:
//設定appbar
appBar: new AppBar(
//底部
bottom: new TabBar(
indicatorColor: Colors.red, //指示器顏色 如果和標題欄顏色一樣會白色
tabs: myTabs,//繫結資料
isScrollable: true, //是否可以滑動
),
),
複製程式碼
10.4.繫結TabBar和TabBarView
class MyHomePage extends StatelessWidget {
final List<Tab> myTabs = <Tab>[
new Tab(text: 'Android'),
new Tab(text: 'IOS'),
new Tab(text: 'Flutter'),
new Tab(text: 'RN'),
new Tab(text: 'Java'),
new Tab(text: 'C'),
new Tab(text: 'C++'),
new Tab(text: 'Go'),
];
@override
Widget build(BuildContext context) {
return new DefaultTabController(
length: myTabs.length, //Tab長度
child: new Scaffold(
//設定appbar
appBar: new AppBar(
//底部
bottom: new TabBar(
indicatorColor: Colors.red, //指示器顏色 如果和標題欄顏色一樣會白色
tabs: myTabs,//繫結資料
isScrollable: true, //是否可以滑動
),
....
),
body: new TabBarView(
//選中哪個Tabs,body就會顯示
children: myTabs.map((Tab tab) {
return new Center(child: new Text(tab.text));
}).toList(),
),
....
);
}
}
複製程式碼
效果如下圖:
11.BottomNavigationBar
BottomNavigationBar
即是底部導航欄控制元件,顯示在頁面底部的設計控制元件,用於在試圖切換,底部導航欄包含多個標籤、圖示或者兩者搭配的形式,簡而言之提供了頂級檢視之間的快速導航。
11.1.構建底部標籤
//底部資料
final Map bottomMap ={
"首頁":Icon(Icons.home),
"朋友圈":Icon(Icons.camera),
"資訊":Icon(Icons.message),
"其他":Icon(Icons.devices_other),
};
複製程式碼
11.2.建立導航欄
因為點選導航欄需要對應的字型顯示,所以MyHomePage
需要繼承StatefulWidget
,增加State
,
//用無狀態控制元件顯示
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//主題色
theme: ThemeData(
//設定為紅色
primarySwatch: Colors.red),
//這是一個Widget物件,用來定義當前應用開啟的時候,所顯示的介面
home: MyHomePageWidget(),
);
}
}
class MyHomePageWidget extends StatefulWidget{
@override
State<StatefulWidget> createState(){
return new MyHomePage();
}
}
class MyHomePage extends State<MyHomePageWidget> {
//底部資料
final Map bottomMap ={
"首頁":Icon(Icons.home),
"朋友圈":Icon(Icons.camera),
"資訊":Icon(Icons.message),
"其他":Icon(Icons.devices_other),
};
int _index = 0;
bottomNavigationBar: BottomNavigationBar(
items: (){
var items = <BottomNavigationBarItem>[];
bottomMap.forEach((k,v){
items.add(BottomNavigationBarItem(
title:Text(k),//取map的值
icon : v,//取map的圖示
backgroundColor:Colors.red,//背景紅色
));
});
return items;
}(),
currentIndex: _index,//選中第幾個
onTap:(position){
Fluttertoast.showToast(
msg: 'text inputted: $position',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIos: 1,
);
setState(() {
_index = position;//狀態更新
});
}
),
}
複製程式碼
最終效果如下:
五、實踐
下面實踐Flutter
中文網的例子:
1.實現影象
再說一下如何配置影象
class MyApp extends StatelessWidget{
@override
Widget build(BuildContext context){
return MaterialApp(
home:new MyHomeWidget(),
);
}
}
class MyHomeWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return new Scaffold(
//設定標題欄
appBar: new AppBar(
title:new Text('Flutter Demo'),
),
//主體用ListView
body:new ListView(
children: <Widget>[
//圖片
new Image.asset(
'images/lake.jpg',
width:600.0,
height:240.0,
//順便設定圖片屬性
fit:BoxFit.cover,
)
],
),
);
}
}
複製程式碼
2.實現標題欄
//實現標題欄
Widget titleWidget = new Container(
//內邊距
padding:const EdgeInsets.all(30.0),
//整體是一個水平的佈局
child:new Row(
//只有一個孩子
children: <Widget>[
//用Expanded 會佔用icon之外剩餘空間
new Expanded(
//垂直佈局 放置兩個文字
child: new Column(
//設定文字一起始端對齊
crossAxisAlignment: CrossAxisAlignment.start,
//有兩個孩子
children: <Widget>[
new Container(
//底部內邊距
padding:const EdgeInsets.only(bottom:10.0),
//孩子 設定字型樣式
child:new Text(
'Oeschinen Lake Campground',
style: new TextStyle(fontWeight: FontWeight.bold),
),
),
new Text(
'Kandersteg, Switzerland',
style: new TextStyle(
color:Colors.grey[450],//設定顏色透明度
),
)
],
),
),
new Icon(
Icons.star,
color:Colors.red[400],
),
new Text('41'),
],
),
);
複製程式碼
3.實現按鈕行
因為三個按鈕樣式都是一樣的,所以抽取公共部分:
/**
* 抽取button行的程式碼複用
*
*/
Column getText(IconData icon,String text){
return new Column(
//聚集widgets
mainAxisSize:MainAxisSize.min,
//child居中
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Icon(icon,color:Colors.blue[500]),
new Container(
//上部外邊距
margin: const EdgeInsets.only(top:8.0),
//Text內容樣式設定
child:new Text(
text,
style:new TextStyle(
color:Colors.blue[500],
),
),
)
],
);
}
/**
* 按鈕實現
*/
Widget buttonWidget = new Container(
//三列
child:new Row(
//用MainAxisAlignment.spaceEvenly平均分配子空間
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
//孩子們
children: <Widget>[
getText(Icons.call, "CALL"),
getText(Icons.near_me, "ROUTE"),
getText(Icons.share, "SHARE"),
],
),
);
複製程式碼
4.實現文字
/**
* 文字實現
*/
Widget textWidget = new Container(
alignment: Alignment.center,
//設定內邊距
padding:const EdgeInsets.all(10.0),
child:new Text(
'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese Alps. Situated 1,578 meters above sea level, '
'it is one of the larger Alpine Lakes. A gondola ride from Kandersteg, '
'followed by a half-hour walk through pastures and pine forest, '
'leads you to the lake, which warms to 20 degrees Celsius in the summer. '
'Activities enjoyed here include rowing, and riding the summer toboggan run.',
// softWrap: true,//屬性表示文字是否應在軟換行符(例如句點或逗號)之間斷開。
// textAlign: TextAlign.center,
),
);
複製程式碼
5.整合
return new Scaffold(
//設定標題欄
appBar: new AppBar(
title:new Text('Flutter Demo'),
),
//主體用ListView
body:new ListView(
children: <Widget>[
//圖片
new Image.asset(
'images/lake.jpg',
width:600.0,
height:240.0,
//順便設定圖片屬性
fit:BoxFit.cover,
),
//標題欄
titleWidget,
//按鈕欄
buttonWidget,
//文字欄
textWidget,
],
),
);
複製程式碼
執行效果圖:
六、總結
Flutter
還有很多Widget
上面沒有說到,就只能自己有空再去學習了,下面直接上一張圖,今天學到的內容:
學習連結:flutterchina.club/widgets/
如有不正之處歡迎大家批評指正~