给 Web 开发者的 Flutter 指南

本文是为那些熟悉用 HTML 与 CSS 语法来管理应用页面中元素的开发者准备的。本文会将 HTML/CSS 代码片段替换为等价的 Flutter/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。

对嵌套的 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 的背景上应用,请将它嵌套在一个 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, 
      ),
    ),
  ),
);