1. 前言
Flutter
作爲時下最流行的技術之一,憑藉其出色的性能以及抹平多端的差異優勢,早已引起大批技術愛好者的關注,甚至一些閒魚
,美團
,騰訊
等大公司均已投入生產使用。雖然目前其生態還沒有完全成熟,但身靠背後的Google
加持,其發展速度已經足夠驚人,可以預見將來對Flutter
開發人員的需求也會隨之增長。
無論是爲了現在的技術嚐鮮還是將來的潮流趨勢,都9102年了,作爲一個前端開發者,似乎沒有理由不去嘗試它。正是帶着這樣的心理,筆者也開始學習Flutter
,同時建了一個用於練習的倉庫,後續所有代碼都會託管在上面,歡迎star,一起學習。
今天分享的是Flutter中最常用到的一些基礎組件,它們是構成UI界面的基礎元素:容器
,行
,列
,絕對定位佈局
,文本
,圖片
和圖標
等。
2. 基礎組件
2.1 Container(容器組件)
Container
組件是最常用的佈局組件之一,可以認爲它是web開發中的div
,rn開發中的View
。其往往可以用來控制大小、背景顏色、邊框、陰影、內外邊距和內容排列方式等。我們先來看下其構造函數:
Container({
Key key,
double width,
double height,
this.margin,
this.padding,
Color color,
this.alignment,
BoxConstraints constraints,
Decoration decoration,
this.foregroundDecoration,
this.transform,
this.child,
})
2.1.1 width
,height
,margin
,padding
這些屬性的含義和我們已經熟知的並沒有區別。唯一需要注意的是,margin
和padding
的賦值不是一個簡單的數字,因爲其有left
, top
, right
, bottom
四個方向的值需要設置。Flutter
提供了EdgeInsets
這個類,幫助我們方便地生成四個方向的值。通常情況下,我們可能會用到EdgeInsets
的4種構造方法:
-
EdgeInsets.all(value)
: 用於設置4個方向一樣的值; -
EdgeInsets.only(left: val1, top: val2, right: val3, bottom: val4)
: 可以單獨設置某個方向的值; -
EdgeInsets.symmetric(horizontal: val1, vertical: val2)
: 用於設置水平/垂直方向上的值; -
EdgeInsets.fromLTRB(left, top, right, bottom)
: 按照左上右下的順序設置4個方向的值。
2.1.2 color
該屬性的含義是背景顏色,等同於web/rn中的backgroundColor。需要注意的是Flutter
中有一個專門表示顏色的Color
類,而非我們常用的字符串。不過我們可以非常輕鬆地進行轉換,舉個栗子:
在web/rn中我們會用'#FF0000'
或'red'
來表示紅色,而在Flutter中,我們可以用Color(0xFFFF0000)
或Colors.red
來表示。
2.1.3 alignment
該屬性是用來決定Container
組件的子組件將以何種方式進行排列(PS:再也不用爲怎麼居中操心了)。其可選值通常會用到:
-
Alignment.topLeft
: 左上 -
Alignment.topCenter
: 上中 -
Alignment.topRight
: 右上 -
Alignment.centerLeft
: 左中 -
Alignment.center
: 居中 -
Alignment.centerRight
: 右中 -
Alignment.bottomLeft
: 左下 -
Alignment.bottomCenter
: 下中 -
Alignment.bottomRight
: 右下
2.1.4 constraints
在web/rn中我們通常會用minWidth
/maxWidth
/minHeight
/maxHeight
等屬性來限制容器的寬高。在Flutter
中,你需要使用BoxConstraints
(盒約束)來實現該功能。
// 容器的大小將被限制在[100*100 ~ 200*200]內
BoxConstraints(
minWidth: 100,
maxWidth: 200,
minHeight: 100,
maxHeight: 200,
)
2.1.5 decoration
該屬性非常強大,字面意思是裝飾,因爲通過它你可以設置邊框
,陰影
,漸變
,圓角
等常用屬性。BoxDecoration
繼承自Decoration
類,因此我們通常會生成一個BoxDecoration
實例來設置這些屬性。
1) 邊框
可以用Border.all
構造函數直接生成4條邊框,也可以用Border
構造函數單獨設置不同方向上的邊框。不過令人驚訝的是官方提供的邊框竟然不支持虛線
(issue在這裏)。
// 同時設置4條邊框:1px粗細的黑色實線邊框
BoxDecoration(
border: Border.all(color: Colors.black, width: 1, style: BorderStyle.solid)
)
// 設置單邊框:上邊框爲1px粗細的黑色實線邊框,右邊框爲1px粗細的紅色實線邊框
BoxDecoration(
border: Border(
top: BorderSide(color: Colors.black, width: 1, style: BorderStyle.solid),
right: BorderSide(color: Colors.red, width: 1, style: BorderStyle.solid),
),
)
2) 陰影
陰影屬性和web中的boxShadow
幾乎沒有區別,可以指定x
,y
,blur
,spread
,color
等屬性。
BoxDecoration(
boxShadow: [
BoxShadow(
offset: Offset(0, 0),
blurRadius: 6,
spreadRadius: 10,
color: Color.fromARGB(20, 0, 0, 0),
),
],
)
3) 漸變
如果你不想容器的背景顏色是單調的,可以嘗試用gradient
屬性。Flutter
同時支持線性漸變
和徑向漸變
:
// 從左到右,紅色到藍色的線性漸變
BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Colors.red, Colors.blue],
),
)
// 從中心向四周擴散,紅色到藍色的徑向漸變
BoxDecoration(
gradient: RadialGradient(
center: Alignment.center,
colors: [Colors.red, Colors.blue],
),
)
4) 圓角
通常情況下,你可能會用到BorderRadius.circular
構造函數來同時設置4個角的圓角,或是BorderRadius.only
構造函數來單獨設置某幾個角的圓角:
// 同時設置4個角的圓角爲5
BoxDecoration(
borderRadius: BorderRadius.circular(5),
)
// 設置單圓角:左上角的圓角爲5,右上角的圓角爲10
BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(5),
topRight: Radius.circular(10),
),
)
2.1.6 transform
transform
屬性和我們在web/rn中經常用到的基本也沒有差別,主要包括:平移
,縮放
、旋轉
和傾斜
。在Flutter中,封裝了矩陣變換類Matrix4
幫助我們進行變換:
-
translationValues(x, y, z)
: 平移x, y, z; -
rotationX(radians)
: x軸旋轉radians弧度; -
rotationY(radians)
: y軸旋轉radians弧度; -
rotationZ(radians)
: z軸旋轉radians弧度; -
skew(alpha, beta)
: x軸傾斜alpha度,y軸傾斜beta度; -
skewX(alpha)
: x軸傾斜alpha度; -
skewY(beta)
: y軸傾斜beta度;
2.1.7 小結
Container
組件的屬性很豐富,雖然有些用法上和web/rn有些許差異,但基本上大同小異,所以過渡起來也不會有什麼障礙。另外,由於Container
組件是單子節點組件,也就是隻允許子節點有一個。所以在佈局上,很多時候我們會用Row
和Column
組件進行行
/列
佈局。
2.2 Row/Column(行/列組件)
Row
和Column
組件其實和web/rn中的Flex佈局
(彈性盒子)特別相似,或者我們可以就這麼理解。使用Flex佈局
的同學對主軸
和次軸
的概念肯定都已經十分熟悉,Row
組件的主軸就是橫向,Column
組件的主軸就是縱向。且它們的構造函數十分相似(已省略不常用屬性):
Row({
Key key,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
MainAxisSize mainAxisSize = MainAxisSize.max,
List<Widget> children = const <Widget>[],
})
Column({
Key key,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
MainAxisSize mainAxisSize = MainAxisSize.max,
List<Widget> children = const <Widget>[],
})
2.2.1 mainAxisAlignment
該屬性的含義是主軸排列方式,根據上述構造函數可以知道Row
和Column
組件在主軸方向上默認都是從start開始,也就是說Row
組件默認從左到右開始排列子組件,Column
組件默認從上到下開始排列子組件。
當然,你還可以使用其他的可選值:
- MainAxisAlignment.start
- MainAxisAlignment.end
- MainAxisAlignment.center
- MainAxisAlignment.spaceBetween
- MainAxisAlignment.spaceAround
- MainAxisAlignment.spaceEvenly
2.2.2 crossAxisAlignment
該屬性的含義是次軸排列方式,根據上述構造函數可以知道Row
和Column
組件在次軸方向上默認都是居中。
這裏有一點需要特別注意:由於Column
組件次軸方向上(即水平)默認是居中對齊,所以水平方向上不會撐滿其父容器,此時需要指定CrossAxisAlignment.stretch
纔可以。
另外,crossAxisAlignment其他的可選值有:
- crossAxisAlignment.start
- crossAxisAlignment.end
- crossAxisAlignment.center
- crossAxisAlignment.stretch
- crossAxisAlignment.baseline
2.2.3 mainAxisSize
字面意思上來說,該屬性指的是在主軸上的尺寸。其實就是指在主軸方向上,是包裹其內容,還是撐滿其父容器。它的可選值有MainAxisSize.min
和MainAxisSize.max
。由於其默認值都是MainAxisSize.max
,所以主軸方向上默認大小都是儘可能撐滿父容器的。
2.2.4 小結
由於Row
/Column
組件和我們熟悉的Flex佈局
非常相似,所以上手起來非常容易,幾乎零學習成本。
2.3 Stack/Positoned(絕對定位佈局組件)
絕對定位佈局在web/rn開發中也是使用頻率較高的一種佈局方式,Flutter
也提供了相應的組件實現,需要將Stack
和Positioned
組件搭配在一起使用。比如下方的這個例子就是創建了一個黃色的盒子,並且在其四個角落放置了4個紅色的小正方形。Stack
組件就是絕對定位的容器,Positioned
組件通過left
,top
,right
,bottom
四個方向上的屬性值來決定其在父容器中的位置。
Container(
height: 100,
color: Colors.yellow,
child: Stack(
children: <Widget>[
Positioned(
left: 10,
top: 10,
child: Container(width: 10, height: 10, color: Colors.red),
),
Positioned(
right: 10,
top: 10,
child: Container(width: 10, height: 10, color: Colors.red),
),
Positioned(
left: 10,
bottom: 10,
child: Container(width: 10, height: 10, color: Colors.red),
),
Positioned(
right: 10,
bottom: 10,
child: Container(width: 10, height: 10, color: Colors.red),
),
],
),
)
2.4 Text(文本組件)
Text
組件也是日常開發中最常用的基礎組件之一,我們通常用它來展示文本信息。來看下其構造函數(已省略不常用屬性):
const Text(
this.data, {
Key key,
this.style,
this.textAlign,
this.softWrap,
this.overflow,
this.maxLines,
})
-
data
: 顯示的文本信息; -
style
: 文本樣式,Flutter
提供了一個TextStyle
類,最常用的fontSize
,fontWeight
,color
,backgroundColor
和shadows
等屬性都是通過它設置的; -
textAlign
: 文字對齊方式,常用可選值有TextAlign
的left
,right
,center
和justify
; -
softWrap
: 文字是否換行; -
overflow
: 當文字溢出的時候,以何種方式處理(默認直接截斷)。可選值有TextOverflow
的clip
,fade
,ellipsis
和visible
; -
maxLines
: 當文字超過最大行數還沒顯示完的時候,就會根據overflow
屬性決定如何截斷處理。
Flutter
的Text
組件足夠靈活,提供了各種屬性讓我們定製,不過一般情況下,我們更多地只需下方几行代碼就足夠了:
Text(
'這是測試文本',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Color(0xFF999999),
),
)
除了上述的應用場景外,有時我們還會遇到富文本
的需求(即一段文本中,可能需要不同的字體樣式)。比如在一些UI設計中經常會遇到表示價格的時候,¥
符號比金額
的字號小點。對於此類需求,我們可以用Flutter
提供的Text.rich
構造函數來創建相應的文本組件:
Text.rich(TextSpan(
children: [
TextSpan(
'¥',
style: TextStyle(
fontSize: 12,
color: Color(0xFFFF7528),
),
),
TextSpan(
'258',
style: TextStyle(
fontSize: 15,
color: Color(0xFFFF7528),
),
),
]
))
2.5 Image(圖片組件)
Image
圖片組件作爲豐富內容的基礎組件之一,日常開發中的使用頻率也非常高。看下其構造函數(已省略不常用屬性):
Image({
Key key,
@required this.image,
this.width,
this.height,
this.color,
this.fit,
this.repeat = ImageRepeat.noRepeat,
})
-
image
: 圖片源,最常用到主要有兩種(AssetImage
和NetworkImage
)。使用AssetImage
之前,需要在pubspec.yaml
文件中聲明好圖片資源,然後才能使用;而NextworkImage
指定圖片的網絡地址即可,主要是在加載一些網絡圖片時會用到; -
width
: 圖片寬度; -
height
: 圖片高度; -
color
: 圖片的背景顏色,當網絡圖片未加載完畢之前,會顯示該背景顏色; -
fit
: 當我們希望圖片根據容器大小進行適配而不是指定固定的寬高值時,可以通過該屬性來實現。其可選值有BoxFit
的fill
,contain
,cover
,fitWidth
,fitHeight
,none
和scaleDown
; -
repeat
: 決定當圖片實際大小不足指定大小時是否使用重複效果。
另外,Flutter
還提供了Image.network
和Image.asset
構造函數,其實是語法糖。比如下方的兩段代碼結果是完全一樣的:
Image(
image: NetworkImage('https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1402367109,4157195964&fm=27&gp=0.jpg'),
width: 100,
height: 100,
)
Image.network(
'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1402367109,4157195964&fm=27&gp=0.jpg',
width: 100,
height: 100,
)
2.6 Icon
(圖標組件)
Icon
圖標組件相比於圖片有着放大不會失真的優勢,在日常開發中也是經常會被用到。Flutter
更是直接內置了一套Material
風格的圖標(你可以在這裏預覽所有的圖標類型)。看下構造函數:
const Icon(
this.icon, {
Key key,
this.size,
this.color,
})
-
icon
: 圖標類型; -
size
: 圖標大小; -
color
: 圖標顏色。
3. 佈局實戰
通過上一節的介紹,我們對Container
,Row
,Column
,Stack
,Positioned
,Text
,Image
和Icon
組件有了初步的認識。接下來,就讓我們通過一個實際的例子來加深理解和記憶。
3.1 準備工作 - 數據類型
根據上述卡片中的內容,我們可以定義一些字段。爲了規範開發流程,我們先給卡片定義一個數據類型的類,這樣在後續的開發過程中也能更好地對數據進行Mock和管理:
class PetCardViewModel {
/// 封面地址
final String coverUrl;
/// 用戶頭像地址
final String userImgUrl;
/// 用戶名
final String userName;
/// 用戶描述
final String description;
/// 話題
final String topic;
/// 發佈時間
final String publishTime;
/// 發佈內容
final String publishContent;
/// 回覆數量
final int replies;
/// 喜歡數量
final int likes;
/// 分享數量
final int shares;
const PetCardViewModel({
this.coverUrl,
this.userImgUrl,
this.userName,
this.description,
this.topic,
this.publishTime,
this.publishContent,
this.replies,
this.likes,
this.shares,
});
}
3.2 搭建骨架,佈局拆分
根據給的視覺圖,我們可以將整體進行拆分,一共劃分成4個部分:Cover
,UserInfo
,PublishContent
和InteractionArea
。爲此,我們可以搭起代碼的基本骨架:
class PetCard extends StatelessWidget {
final PetCardViewModel data;
const PetCard({
Key key,
this.data,
}) : super(key: key);
Widget renderCover() {
}
Widget renderUserInfo() {
}
Widget renderPublishContent() {
}
Widget renderInteractionArea() {
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
blurRadius: 6,
spreadRadius: 4,
color: Color.fromARGB(20, 0, 0, 0),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
this.renderCover(),
this.renderUserInfo(),
this.renderPublishContent(),
this.renderInteractionArea(),
],
),
);
}
}
3.3 封面區域
爲了更好的凸現圖片的效果,這裏加了一個蒙層,所以此處剛好可以用得上Stack
/Positioned
佈局和LinearGradient
漸變,Dom結構如下:
Widget renderCover() {
return Stack(
fit: StackFit.passthrough,
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: Image.network(
data.coverUrl,
height: 200,
fit: BoxFit.fitWidth,
),
),
Positioned(
left: 0,
top: 100,
right: 0,
bottom: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromARGB(0, 0, 0, 0),
Color.fromARGB(80, 0, 0, 0),
],
),
),
),
),
],
);
}
3.4 用戶信息區域
用戶信息區域就非常適合使用Row
和Column
組件來進行佈局,Dom結構如下:
Widget renderUserInfo() {
return Container(
margin: EdgeInsets.only(top: 16),
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
CircleAvatar(
radius: 20,
backgroundColor: Color(0xFFCCCCCC),
backgroundImage: NetworkImage(data.userImgUrl),
),
Padding(padding: EdgeInsets.only(left: 8)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
data.userName,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
Padding(padding: EdgeInsets.only(top: 2)),
Text(
data.description,
style: TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
),
],
),
Text(
data.publishTime,
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
),
),
],
),
);
}
3.5 發佈內容區域
通過這塊區域的UI練習,我們可以實踐Container
組件設置不同的borderRadius
,以及Text
組件文本內容超出時的截斷處理,Dom結構如下:
Widget renderPublishContent() {
return Container(
margin: EdgeInsets.only(top: 16),
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
margin: EdgeInsets.only(bottom: 14),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Color(0xFFFFC600),
borderRadius: BorderRadius.only(
topRight: Radius.circular(8),
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
),
),
child: Text(
'# ${data.topic}',
style: TextStyle(
fontSize: 12,
color: Colors.white,
),
),
),
Text(
data.publishContent,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
],
),
);
}
3.6 互動區域
在這個模塊,我們會用到Icon
圖標組件,可以控制其大小和顏色等屬性,Dom結構如下:
Widget renderInteractionArea() {
return Container(
margin: EdgeInsets.symmetric(vertical: 16),
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
Icon(
Icons.message,
size: 16,
color: Color(0xFF999999),
),
Padding(padding: EdgeInsets.only(left: 6)),
Text(
data.replies.toString(),
style: TextStyle(
fontSize: 15,
color: Color(0xFF999999),
),
),
],
),
Row(
children: <Widget>[
Icon(
Icons.favorite,
size: 16,
color: Color(0xFFFFC600),
),
Padding(padding: EdgeInsets.only(left: 6)),
Text(
data.likes.toString(),
style: TextStyle(
fontSize: 15,
color: Color(0xFF999999),
),
),
],
),
Row(
children: <Widget>[
Icon(
Icons.share,
size: 16,
color: Color(0xFF999999),
),
Padding(padding: EdgeInsets.only(left: 6)),
Text(
data.shares.toString(),
style: TextStyle(
fontSize: 15,
color: Color(0xFF999999),
),
),
],
),
],
),
);
}
3.7 小結
通過上面的一個例子,我們成功地把一個看起來複雜的UI界面一步步拆解,將之前提到的組件都用了個遍,並且最終得到了不錯的效果。其實,日常開發中90%以上的需求都離不開上述提到的基礎組件。因此,只要稍加練習,熟悉了Flutter
中的基礎組件用法,就已經算是邁出了一大步哦~
這裏還有銀行卡和朋友圈的UI練習例子,由於篇幅原因就不貼代碼了,可以去github倉庫看。
4. 總結
本文首先介紹了Flutter
中構建UI界面最常用的基礎組件(容器
,行
,列
,絕對定位佈局
,文本
,圖片
和圖標
)用法。接着,介紹了一個較複雜的UI實戰例子。通過對Dom結構的層層拆解,前文提到過的組件得到一個綜合運用,也算是鞏固了前面所學的概念知識。
不過最後不得不吐槽一句:Flutter
的嵌套真的很難受。。。如果不對UI佈局進行模塊拆分,那絕對是噩夢般的體驗。而且不像web/rn開發樣式可以單獨抽離,Flutter
這種將樣式當做屬性的處理方式,一眼看去真的很難理清dom結構,對於新接手代碼的開發人員而言,需要費點時間理解。。。