給 Web 開發者的 Flutter 指南

本文是為那些熟悉用 HTML 與 CSS 語法來管理應用頁面中元素的開發者準備的。本文會將 HTML/CSS 程式碼片段替換為等價的 Flutter/Dart 程式碼。

Flutter 是一個用於建構跨平臺應用的框架,它使用 Dart 程式語言。要了解 Dart 程式語言與 Javascript 程式語言的異同,請參考文件 給 JavaSript 開發者的 Dart 程式語言指南

在 Web 和 Flutter 的佈局基礎條件中, 佈局限制、widget 的大小確定和定位 是重要的區別之一。想要了解更多,你可以閱讀 深入理解 Flutter 佈局約束

以下的範例基於如下假設:

  • HTML 檔案以 <!DOCTYPE html> 開頭,且為了與 Flutter 模型保持一致,所有 HTML 元素的 CSS 盒模型被設定為 border-box

    {
        box-sizing: border-box;
    }
    
  • 在 Flutter 中,為了保持語法簡潔, “Lorem ipsum” 文字的預設樣式由如下 bold24Roboto 變數定義:

    TextStyle bold24Roboto = const TextStyle(
      color: Colors.white,
      fontSize: 24,
      fontWeight: FontWeight.bold,
    );

執行基礎佈局操作

以下範例將向你展示如何執行最常見的 UI 佈局操作。

文字樣式與對齊

CSS 所處理的字型樣式、大小以及其他文字屬性,都是一個 Text widget 子元素 TextStyle 中單獨的屬性。

Text widget 中的 textAlign 屬性與 CSS 中的 text-align 屬性作用相同,用來控制文字的對齊方向。

在 HTML 和 Flutter 中,子元素或者 widget 的位置都預設在左上方。

<div class="grey-box">
  Lorem ipsum
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Georgia;
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  child: const Text(
    'Lorem ipsum',
    style: TextStyle(
      fontFamily: 'Georgia',
      fontSize: 24,
      fontWeight: FontWeight.bold,
    ),
    
    textAlign: TextAlign.center, 
  ),
);

設定背景顏色

在 Flutter 中,你可以透過 Containerdecoration 或者 color 屬性來設定背景顏色。但是,你不能同時設定這兩個屬性,這有可能導致 decoration 覆蓋掉 color。當背景是簡單的顏色時,應首選 color 屬性,對於其他情況漸變或影象等情況,推薦使用 decoration 屬性。

CSS 範例使用十六進位制顏色,這等價於 Material 的調色盤。

<div class="grey-box">
  Lorem ipsum
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  
  child: Text(
    'Lorem ipsum',
    style: bold24Roboto,
  ),
);
final container = Container(
  // grey box
  width: 320,
  height: 240,
  decoration: BoxDecoration(
    color: Colors.grey[300],
  ),
  
  child: Text(
    'Lorem ipsum',
    style: bold24Roboto,
  ),
);

居中元素

一個 Center widget 可以將它的子 widget 同時以水平和垂直方向居中。

要用 CSS 實現相似的效果,父元素需要使用一個 flex 或者 table-cell 顯示佈局。本節範例使用的是 flex 佈局。

<div class="grey-box">
  Lorem ipsum
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
    display: flex;
    align-items: center;
    justify-content: center;
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  child: Center(
    child: Text(
      'Lorem ipsum',
      style: bold24Roboto,
    ),
  ),
);

設定容器寬度

要指定一個 Container widget 的寬度,請使用它的 width 屬性。與 CSS 中的 max-width 屬性用於指定容器可調整的寬度最大值不同的是,這裡指定的是一個固定寬度。要在 Flutter 中模擬該效果,可以使用 Container 的 constraints 屬性。新建一個帶有 minWidthmaxWidth 屬性的 BoxConstraints widget。

對巢狀(Nesting)的 Container 來說,如果其父元素寬度小於子元素寬度,則子元素會調整尺寸以匹配父元素大小。

<div class="grey-box">
  <div class="red-box">
    Lorem ipsum
  </div>
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
    display: flex;
    align-items: center;
    justify-content: center;
}
.red-box {
    background-color: #ef5350; /* red 400 */
    padding: 16px;
    color: #ffffff;
    width: 100%;
    max-width: 240px;
}
final container = Container(
  // grey box
  width: 320,
  
  height: 240,
  color: Colors.grey[300],
  child: Center(
    child: Container(
      // red box
      width: 240,
      // max-width is 240
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.red[400],
      ),
      child: Text(
        'Lorem ipsum',
        style: bold24Roboto,
      ),
    ),
  ),
);

操控位置與大小

以下範例將展示如何對 widget 的位置、大小以及背景進行更復雜的操作。

設定絕對位置

預設情況下,widget 相對於其父元素定位。

想要透過 x-y 座標指定一個 widget 的絕對位置,可以把它放在一個 Positioned widget 中,而 Positioned 則需被放在一個 Stack widget 中。

<div class="grey-box">
  <div class="red-box">
    Lorem ipsum
  </div>
</div>

.grey-box {
    position: relative;
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
}
.red-box {
    background-color: #ef5350; /* red 400 */
    padding: 16px;
    color: #ffffff;
    position: absolute;
    top: 24px;
    left: 24px;
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  child: Stack(
    children: [
      Positioned(
        // red box
        left: 24,
        top: 24,
        
        child: Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: Colors.red[400],
          ),
          child: Text(
            'Lorem ipsum',
            style: bold24Roboto,
          ),
        ),
      ),
    ],
  ),
);

旋轉元素

想要旋轉一個 widget,請將它放在 Transform widget 中。使用 Transform widget 的 alignmentorigin 屬性分別來指定轉換原點(支點)的相對和絕對位置資訊。

對於簡單的 2D 旋轉,例如在 Z 軸上旋轉的,建立一個新的 Matrix4 物件,並使用它的 rotateZ() 方法以弧度單位(角度 × π / 180)指定旋轉系數。

<div class="grey-box">
  <div class="red-box">
    Lorem ipsum
  </div>
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
    display: flex;
    align-items: center;
    justify-content: center;
}
.red-box {
    background-color: #ef5350; /* red 400 */
    padding: 16px;
    color: #ffffff;
    transform: rotate(15deg);
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  child: Center(
    child: Transform(
      alignment: Alignment.center,
      transform: Matrix4.identity()..rotateZ(15 * 3.1415927 / 180),
      child: Container(
        // red box
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Colors.red[400],
        ),
        child: Text(
          'Lorem ipsum',
          style: bold24Roboto,
          textAlign: TextAlign.center,
        ),
      ),
    ),
  ),
);

縮放元素

想要縮放一個 widget,請同樣將它放在一個 Transform widget 中。使用 Transform widget 的 alignmentorigin 屬性分別來指定縮放原點(支點)的相對和絕對資訊。

對於沿 x 軸的簡單縮放操作,新建一個 Matrix4 物件並用它的 scale() 方法來指定縮放因係數。

當你縮放一個父 widget 時,它的子 widget 也會相應被縮放。

<div class="grey-box">
  <div class="red-box">
    Lorem ipsum
  </div>
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
    display: flex;
    align-items: center;
    justify-content: center;
}
.red-box {
    background-color: #ef5350; /* red 400 */
    padding: 16px;
    color: #ffffff;
    transform: scale(1.5);
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  child: Center(
    child: Transform(
      alignment: Alignment.center,
      transform: Matrix4.identity()..scale(1.5),
      child: Container(
        // red box
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Colors.red[400],
        ),
        child: Text(
          'Lorem ipsum',
          style: bold24Roboto,
          textAlign: TextAlign.center,
        ),
      ),
    ),
  ),
);

應用線性變換

想要將線性顏色漸變在 widget 的背景上應用,請將它巢狀(Nesting)在一個 Container widget 中。接著將一個 BoxDecoration 物件傳遞至 Containerdecoration,然後使用 BoxDecorationgradient 屬性來變換背景填充內容。

變換「角度」基於 Alignment (x, y) 取值來定:

  • If the beginning and ending x values are equal, the gradient is vertical (0° | 180°).

    如果開始和結束的 x 值相同,變換將是垂直的 (0° 180°)。
  • If the beginning and ending y values are equal, the gradient is horizontal (90° | 270°).

    如果開始和結束的 y 值相同,變換將是水平的 (90° 270°)。

垂直變換

<div class="grey-box">
  <div class="red-box">
    Lorem ipsum
  </div>
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
    display: flex;
    align-items: center;
    justify-content: center;
}
.red-box {
    padding: 16px;
    color: #ffffff;
    background: linear-gradient(180deg, #ef5350, rgba(0, 0, 0, 0) 80%);
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  child: Center(
    child: Container(
      // red box
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment(0.0, 0.6),
          colors: <Color>[
            Color(0xffef5350),
            Color(0x00ef5350),
          ],
        ),
      ),
      
      padding: const EdgeInsets.all(16),
      child: Text(
        'Lorem ipsum',
        style: bold24Roboto,
      ),
    ),
  ),
);

水平變換

<div class="grey-box">
  <div class="red-box">
    Lorem ipsum
  </div>
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
    display: flex;
    align-items: center;
    justify-content: center;
}
.red-box {
    padding: 16px;
    color: #ffffff;
    background: linear-gradient(90deg, #ef5350, rgba(0, 0, 0, 0) 80%);
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  child: Center(
    child: Container(
      // red box
      padding: const EdgeInsets.all(16),
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment(-1.0, 0.0),
          end: Alignment(0.6, 0.0),
          colors: <Color>[
            Color(0xffef5350),
            Color(0x00ef5350),
          ],
        ),
      ),
      
      child: Text(
        'Lorem ipsum',
        style: bold24Roboto,
      ),
    ),
  ),
);

操控圖形

以下範例將展示如何新建和自訂圖形。

圓角

想要在矩形上實現圓角,請用 BoxDecoration 物件的 borderRadius 屬性。新建一個 BorderRadius 物件來指定各個圓角的半徑大小。

<div class="grey-box">
  <div class="red-box">
    Lorem ipsum
  </div>
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
    display: flex;
    align-items: center;
    justify-content: center;
}
.red-box {
    background-color: #ef5350; /* red 400 */
    padding: 16px;
    color: #ffffff;
    border-radius: 8px;
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  child: Center(
    child: Container(
      // red circle
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.red[400],
        borderRadius: const BorderRadius.all(
          Radius.circular(8),
        ), 
      ),
      child: Text(
        'Lorem ipsum',
        style: bold24Roboto,
      ),
    ),
  ),
);

為盒子新增陰影 (box shadows)

在 CSS 中你可以透過 box-shadow 屬性快速指定陰影偏移與模糊範圍。本例展示了兩個盒陰影的屬性設定:

  • xOffset: 0px, yOffset: 2px, blur: 4px, color: black @80% alpha
  • xOffset: 0px, yOffset: 06x, blur: 20px, color: black @50% alpha

在 Flutter 中,每個屬性與其取值都是單獨指定的。請使用 BoxDecoration 的 boxShadow 屬性來產生一系列 BoxShadow widget。你可以定義一個或多個 BoxShadow widget,這些 widget 共同用於設定陰影深度、顏色等等。

<div class="grey-box">
  <div class="red-box">
    Lorem ipsum
  </div>
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
    display: flex;
    align-items: center;
    justify-content: center;
}
.red-box {
    background-color: #ef5350; /* red 400 */
    padding: 16px;
    color: #ffffff;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.8),
              0 6px 20px rgba(0, 0, 0, 0.5);
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  margin: const EdgeInsets.only(bottom: 16),
  decoration: BoxDecoration(
    color: Colors.grey[300],
  ),
  child: Center(
    child: Container(
      // red box
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.red[400],
        boxShadow: const <BoxShadow>[
          BoxShadow(
            color: Color(0xcc000000),
            offset: Offset(0, 2),
            blurRadius: 4,
          ),
          BoxShadow(
            color: Color(0x80000000),
            offset: Offset(0, 6),
            blurRadius: 20,
          ),
        ], 
      ),
      child: Text(
        'Lorem ipsum',
        style: bold24Roboto,
      ),
    ),
  ),
);

產生圓與橢圓

儘管 CSS 中有 基礎圖形,用 CSS 產生圓可以用一個變通方案,即將矩形的四邊 border-radius 均設成 50%。

雖然 BoxDecorationborderRadius 屬性支援這樣設定, Flutter 提供了一個 shape 屬性用於實現同樣的目的,它的型別是 BoxShape 列舉

<div class="grey-box">
  <div class="red-circle">
    Lorem ipsum
  </div>
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
    display: flex;
    align-items: center;
    justify-content: center;
}
.red-circle {
    background-color: #ef5350; /* red 400 */
    padding: 16px;
    color: #ffffff;
    text-align: center;
    width: 160px;
    height: 160px;
    border-radius: 50%;
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  child: Center(
    child: Container(
      // red circle
      decoration: BoxDecoration(
        color: Colors.red[400],
        shape: BoxShape.circle, 
      ),
      padding: const EdgeInsets.all(16),
      width: 160,
      height: 160,
      
      child: Text(
        'Lorem ipsum',
        style: bold24Roboto,
        textAlign: TextAlign.center, 
      ),
    ),
  ),
);

操控文字

以下範例展示瞭如何設定字型和其他文字屬性。它們同時還展示瞭如何變換文字字元、自訂間距以及產生摘要。

文字間距調整

在 CSS 中你可以透過分別設定 letter-spacing 和 word-spacing 屬性,來指定每個字母以及每個單詞間的空白距離。距離的單位可以是 px、pt、cm、em 等。

在 Flutter 中,你可以將 Text widget 的 TextStyle 屬性中 letterSpacingwordSpacing 設定為邏輯畫素(允許負值)。

<div class="grey-box">
  <div class="red-box">
    Lorem ipsum
  </div>
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
    display: flex;
    align-items: center;
    justify-content: center;
}
.red-box {
    background-color: #ef5350; /* red 400 */
    padding: 16px;
    color: #ffffff;
    letter-spacing: 4px;
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  child: Center(
    child: Container(
      // red box
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.red[400],
      ),
      child: const Text(
        'Lorem ipsum',
        style: TextStyle(
          color: Colors.white,
          fontSize: 24,
          fontWeight: FontWeight.w900,
          letterSpacing: 4, 
        ),
      ),
    ),
  ),
);

內聯樣式更改

一個 Text widget 允許你展示同一類樣式的文字。為了展現具有多種樣式(本例中,是一個帶重音的單詞)的文字,你需要改用 RichText widget。它的 text 屬性可以設定一個或多個可單獨設定樣式的 TextSpan

在接下來的範例中,「Lorem」位於 TextSpan 中,具有預設(繼承)文字樣式,「ipsum」位於具有自訂樣式、單獨的一個 TextSpan 中。

<div class="grey-box">
  <div class="red-box">
    Lorem <em>ipsum</em>
  </div>
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
    display: flex;
    align-items: center;
    justify-content: center;
}
.red-box {
    background-color: #ef5350; /* red 400 */
    padding: 16px;
    color: #ffffff;
}
.red-box em {
    font: 300 48px Roboto;
    font-style: italic;
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  child: Center(
    child: Container(
      // red box
      decoration: BoxDecoration(
        color: Colors.red[400],
      ),
      padding: const EdgeInsets.all(16),
      child: RichText(
        text: TextSpan(
          style: bold24Roboto,
          children: const <TextSpan>[
            TextSpan(text: 'Lorem '),
            TextSpan(
              text: 'ipsum',
              style: TextStyle(
                fontWeight: FontWeight.w300,
                fontStyle: FontStyle.italic,
                fontSize: 48,
              ),
            ),
          ],
        ),
      ), 
    ),
  ),
);

產生文字摘要

一個摘要會展示一個段落中文字的初始行內容,並常用省略號處理溢位的文字內容。

在 Flutter 中,你可以使用 Text widget 的 maxLines 屬性來指定包含在摘要中的行數,以及 overflow 屬性來處理溢位文字。

<div class="grey-box">
  <div class="red-box">
    Lorem ipsum dolor sit amet, consec etur
  </div>
</div>

.grey-box {
    background-color: #e0e0e0; /* grey 300 */
    width: 320px;
    height: 240px;
    font: 900 24px Roboto;
    display: flex;
    align-items: center;
    justify-content: center;
}
.red-box {
    background-color: #ef5350; /* red 400 */
    padding: 16px;
    color: #ffffff;
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
}
final container = Container(
  // grey box
  width: 320,
  height: 240,
  color: Colors.grey[300],
  child: Center(
    child: Container(
      // red box
      decoration: BoxDecoration(
        color: Colors.red[400],
      ),
      padding: const EdgeInsets.all(16),
      child: Text(
        'Lorem ipsum dolor sit amet, consec etur',
        style: bold24Roboto,
        overflow: TextOverflow.ellipsis,
        maxLines: 1, 
      ),
    ),
  ),
);