Flutter
做爲時下最流行的技術之一,憑藉其出色的性能以及抹平多端的差別優點,早已引發大批技術愛好者的關注,甚至一些閒魚
,美團
,騰訊
等大公司均已開始使用。雖然目前其生態尚未徹底成熟,但身靠背後的Google
加持,其發展速度已經足夠驚人,能夠預見未來對Flutter
開發人員的需求也會隨之增加。html
不管是爲了如今的技術嚐鮮仍是未來的潮流趨勢,都9102年了,做爲一個前端開發者,彷佛沒有理由不去嘗試它。正是帶着這樣的心理,筆者也開始學習Flutter
,同時建了一個用於練習的倉庫,後續全部代碼都會託管在上面,歡迎star,一塊兒學習。這是我寫的Flutter系列文章:前端
今天分享的是Flutter中最經常使用到的一些基礎組件,它們是構成UI界面的基礎元素:容器
,行
,列
,絕對定位佈局
,文本
,圖片
和圖標
等。git
Container
組件是最經常使用的佈局組件之一,能夠認爲它是web開發中的div
,rn開發中的View
。其每每能夠用來控制大小、背景顏色、邊框、陰影、內外邊距和內容排列方式等。咱們先來看下其構造函數:github
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, })
width
,height
,margin
,padding
這些屬性的含義和咱們已經熟知的並無區別。惟一須要注意的是,margin
和padding
的賦值不是一個簡單的數字,由於其有left
, top
, right
, bottom
四個方向的值須要設置。Flutter
提供了EdgeInsets
這個類,幫助咱們方便地生成四個方向的值。一般狀況下,咱們可能會用到EdgeInsets
的4種構造方法:web
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個方向的值。color
該屬性的含義是背景顏色,等同於web/rn中的backgroundColor。須要注意的是Flutter
中有一個專門表示顏色的Color
類,而非咱們經常使用的字符串。不過咱們能夠很是輕鬆地進行轉換,舉個栗子:segmentfault
在web/rn中咱們會用'#FF0000'
或'red'
來表示紅色,而在Flutter中,咱們能夠用Color(0xFFFF0000)
或Colors.red
來表示。api
alignment
該屬性是用來決定Container
組件的子組件將以何種方式進行排列(PS:不再用爲怎麼居中操心了)。其可選值一般會用到:網絡
Alignment.topLeft
: 左上Alignment.topCenter
: 上中Alignment.topRight
: 右上Alignment.centerLeft
: 左中Alignment.center
: 居中Alignment.centerRight
: 右中Alignment.bottomLeft
: 左下Alignment.bottomCenter
: 下中Alignment.bottomRight
: 右下constraints
在web/rn中咱們一般會用minWidth
/maxWidth
/minHeight
/maxHeight
等屬性來限制容器的寬高。在Flutter
中,你須要使用BoxConstraints
(盒約束)來實現該功能。app
// 容器的大小將被限制在[100*100 ~ 200*200]內 BoxConstraints( minWidth: 100, maxWidth: 200, minHeight: 100, maxHeight: 200, )
decoration
該屬性很是強大,字面意思是裝飾,由於經過它你能夠設置邊框
,陰影
,漸變
,圓角
等經常使用屬性。BoxDecoration
繼承自Decoration
類,所以咱們一般會生成一個BoxDecoration
實例來設置這些屬性。less
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), ), )
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度;Container
組件的屬性很豐富,雖然有些用法上和web/rn有些許差別,但基本上大同小異,因此過渡起來也不會有什麼障礙。另外,因爲Container
組件是單子節點組件,也就是隻容許子節點有一個。因此在佈局上,不少時候咱們會用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>[], })
mainAxisAlignment
該屬性的含義是主軸排列方式,根據上述構造函數能夠知道Row
和Column
組件在主軸方向上默認都是從start開始,也就是說Row
組件默認從左到右開始排列子組件,Column
組件默認從上到下開始排列子組件。
固然,你還可使用其餘的可選值:
crossAxisAlignment
該屬性的含義是次軸排列方式,根據上述構造函數能夠知道Row
和Column
組件在次軸方向上默認都是居中。
這裏有一點須要特別注意:因爲Column
組件次軸方向上(即水平)默認是居中對齊,因此水平方向上不會撐滿其父容器,此時須要指定CrossAxisAlignment.stretch
才能夠。
另外,crossAxisAlignment其餘的可選值有:
mainAxisSize
字面意思上來講,該屬性指的是在主軸上的尺寸。其實就是指在主軸方向上,是包裹其內容,仍是撐滿其父容器。它的可選值有MainAxisSize.min
和MainAxisSize.max
。因爲其默認值都是MainAxisSize.max
,因此主軸方向上默認大小都是儘量撐滿父容器的。
因爲Row
/Column
組件和咱們熟悉的Flex佈局
很是類似,因此上手起來很是容易,幾乎零學習成本。
絕對定位佈局在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), ), ], ), )
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), ), ), ] ))
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, )
Icon
(圖標組件)Icon
圖標組件相比於圖片有着放大不會失真的優點,在平常開發中也是常常會被用到。Flutter
更是直接內置了一套Material
風格的圖標(你能夠在這裏預覽全部的圖標類型)。看下構造函數:
const Icon( this.icon, { Key key, this.size, this.color, })
icon
: 圖標類型;size
: 圖標大小;color
: 圖標顏色。經過上一節的介紹,咱們對Container
,Row
,Column
,Stack
,Positioned
,Text
,Image
和Icon
組件有了初步的認識。接下來,就讓咱們經過一個實際的例子來加深理解和記憶。
根據上述卡片中的內容,咱們能夠定義一些字段。爲了規範開發流程,咱們先給卡片定義一個數據類型的類,這樣在後續的開發過程當中也能更好地對數據進行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, }); }
根據給的視覺圖,咱們能夠將總體進行拆分,一共劃分紅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(), ], ), ); } }
爲了更好的凸現圖片的效果,這裏加了一個蒙層,因此此處恰好能夠用得上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), ], ), ), ), ), ], ); }
用戶信息區域就很是適合使用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), ), ), ], ), ); }
經過這塊區域的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), ), ), ], ), ); }
在這個模塊,咱們會用到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), ), ), ], ), ], ), ); }
經過上面的一個例子,咱們成功地把一個看起來複雜的UI界面一步步拆解,將以前提到的組件都用了個遍,而且最終獲得了不錯的效果。其實,平常開發中90%以上的需求都離不開上述提到的基礎組件。所以,只要稍加練習,熟悉了Flutter
中的基礎組件用法,就已經算是邁出了一大步哦~
這裏還有銀行卡和朋友圈的UI練習例子,因爲篇幅緣由就不貼代碼了,能夠去github倉庫看。
本文首先介紹了Flutter
中構建UI界面最經常使用的基礎組件(容器
,行
,列
,絕對定位佈局
,文本
,圖片
和圖標
)用法。接着,介紹了一個較複雜的UI實戰例子。經過對Dom結構的層層拆解,前文提到過的組件獲得一個綜合運用,也算是鞏固了前面所學的概念知識。
不過最後不得不吐槽一句:Flutter
的嵌套真的很難受。。。若是不對UI佈局進行模塊拆分,那絕對是噩夢般的體驗。並且不像web/rn開發樣式能夠單獨抽離,Flutter
這種將樣式當作屬性的處理方式,一眼看去真的很難理清dom結構,對於新接手代碼的開發人員而言,須要費點時間理解。。。