用Flutter構建漂亮的UI界面 - 基礎組件篇

1. 前言

Flutter做爲時下最流行的技術之一,憑藉其出色的性能以及抹平多端的差別優點,早已引發大批技術愛好者的關注,甚至一些閒魚美團騰訊等大公司均已開始使用。雖然目前其生態尚未徹底成熟,但身靠背後的Google加持,其發展速度已經足夠驚人,能夠預見未來對Flutter開發人員的需求也會隨之增加。html

不管是爲了如今的技術嚐鮮仍是未來的潮流趨勢,都9102年了,做爲一個前端開發者,彷佛沒有理由不去嘗試它。正是帶着這樣的心理,筆者也開始學習Flutter,同時建了一個用於練習的倉庫,後續全部代碼都會託管在上面,歡迎star,一塊兒學習。這是我寫的Flutter系列文章:前端

今天分享的是Flutter中最經常使用到的一些基礎組件,它們是構成UI界面的基礎元素:容器絕對定位佈局文本圖片圖標等。git

2. 基礎組件

2.1 Container(容器組件)

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,
})
複製代碼

2.1.1 widthheightmarginpadding

這些屬性的含義和咱們已經熟知的並無區別。惟一須要注意的是,marginpadding的賦值不是一個簡單的數字,由於其有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個方向的值。

2.1.2 color

該屬性的含義是背景顏色,等同於web/rn中的backgroundColor。須要注意的是Flutter中有一個專門表示顏色的Color類,而非咱們經常使用的字符串。不過咱們能夠很是輕鬆地進行轉換,舉個栗子:api

在web/rn中咱們會用'#FF0000''red'來表示紅色,而在Flutter中,咱們能夠用Color(0xFFFF0000)Colors.red來表示。markdown

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(盒約束)來實現該功能。app

// 容器的大小將被限制在[100*100 ~ 200*200]內
BoxConstraints(
  minWidth: 100,
  maxWidth: 200,
  minHeight: 100,
  maxHeight: 200,
)
複製代碼

2.1.5 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幾乎沒有區別,能夠指定xyblurspreadcolor等屬性。

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組件是單子節點組件,也就是隻容許子節點有一個。因此在佈局上,不少時候咱們會用RowColumn組件進行/佈局。

2.2 Row/Column(行/列組件)

RowColumn組件其實和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

該屬性的含義是主軸排列方式,根據上述構造函數能夠知道RowColumn組件在主軸方向上默認都是從start開始,也就是說Row組件默認從左到右開始排列子組件,Column組件默認從上到下開始排列子組件。

固然,你還可使用其餘的可選值:

  • MainAxisAlignment.start
  • MainAxisAlignment.end
  • MainAxisAlignment.center
  • MainAxisAlignment.spaceBetween
  • MainAxisAlignment.spaceAround
  • MainAxisAlignment.spaceEvenly

2.2.2 crossAxisAlignment

該屬性的含義是次軸排列方式,根據上述構造函數能夠知道RowColumn組件在次軸方向上默認都是居中。

這裏有一點須要特別注意:因爲Column組件次軸方向上(即水平)默認是居中對齊,因此水平方向上不會撐滿其父容器,此時須要指定CrossAxisAlignment.stretch才能夠。

另外,crossAxisAlignment其餘的可選值有:

  • crossAxisAlignment.start
  • crossAxisAlignment.end
  • crossAxisAlignment.center
  • crossAxisAlignment.stretch
  • crossAxisAlignment.baseline

2.2.3 mainAxisSize

字面意思上來講,該屬性指的是在主軸上的尺寸。其實就是指在主軸方向上,是包裹其內容,仍是撐滿其父容器。它的可選值有MainAxisSize.minMainAxisSize.max。因爲其默認值都是MainAxisSize.max,因此主軸方向上默認大小都是儘量撐滿父容器的。

2.2.4 小結

因爲Row/Column組件和咱們熟悉的Flex佈局很是類似,因此上手起來很是容易,幾乎零學習成本。

2.3 Stack/Positoned(絕對定位佈局組件)

絕對定位佈局在web/rn開發中也是使用頻率較高的一種佈局方式,Flutter也提供了相應的組件實現,須要將StackPositioned組件搭配在一塊兒使用。好比下方的這個例子就是建立了一個黃色的盒子,而且在其四個角落放置了4個紅色的小正方形。Stack組件就是絕對定位的容器,Positioned組件經過lefttoprightbottom四個方向上的屬性值來決定其在父容器中的位置。

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類,最經常使用的fontSizefontWeightcolorbackgroundColorshadows等屬性都是經過它設置的;
  • textAlign: 文字對齊方式,經常使用可選值有TextAlignleftrightcenterjustify
  • softWrap: 文字是否換行;
  • overflow: 當文字溢出的時候,以何種方式處理(默認直接截斷)。可選值有TextOverflowclipfadeellipsisvisible
  • maxLines: 當文字超過最大行數還沒顯示完的時候,就會根據overflow屬性決定如何截斷處理。

FlutterText組件足夠靈活,提供了各類屬性讓咱們定製,不過通常狀況下,咱們更多地只需下方几行代碼就足夠了:

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: 圖片源,最經常使用到主要有兩種(AssetImageNetworkImage)。使用AssetImage以前,須要在pubspec.yaml文件中聲明好圖片資源,而後才能使用;而NextworkImage指定圖片的網絡地址便可,主要是在加載一些網絡圖片時會用到;
  • width: 圖片寬度;
  • height: 圖片高度;
  • color: 圖片的背景顏色,當網絡圖片未加載完畢以前,會顯示該背景顏色;
  • fit: 當咱們但願圖片根據容器大小進行適配而不是指定固定的寬高值時,能夠經過該屬性來實現。其可選值有BoxFitfillcontaincoverfitWidthfitHeightnonescaleDown
  • repeat: 決定當圖片實際大小不足指定大小時是否使用重複效果。

另外,Flutter還提供了Image.networkImage.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. 佈局實戰

經過上一節的介紹,咱們對ContainerRowColumnStackPositionedTextImageIcon組件有了初步的認識。接下來,就讓咱們經過一個實際的例子來加深理解和記憶。

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個部分:CoverUserInfoPublishContentInteractionArea。爲此,咱們能夠搭起代碼的基本骨架:

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結構以下:

code-cover.png

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 用戶信息區域

用戶信息區域就很是適合使用RowColumn組件來進行佈局,Dom結構以下:

code-user-info.png

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結構以下:

code-publish-content.png

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結構以下:

code-interaction-area.png

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結構,對於新接手代碼的開發人員而言,須要費點時間理解。。。

本文全部代碼託管在這兒,也能夠關注個人Blog

相關文章
相關標籤/搜索