Flutter Stack 用法小结

记录一下如何使用 Flutter Stack Widget 来将文本跟图片中的某个特定点对齐。

某个产品需求要求显示本月累计签到天数,于是设计给了开发如下一张图。

-w420

设计姐姐”好心”地将文案也写好了,只留了个格让开发填,够简单吧。但其实开发很头疼:”累计签到”那一行文案是特殊字体,所以并不能吐槽设计的切图方式完全不合理。但问题是对背景图”填空”容易引起适配问题,稍有不慎就如下图一样没对齐。

-w314

接下来看如何在 Flutter 中解决上述对齐问题。涉及到的几个要点:

  • 图片缩放 - 我们知道图片适配不同大小屏幕大小
  • 了解 Image.fit 属性 - 这个属性控制着图片如何缩放
  • 了解 Stack + Positioned 的基本用法
  • 了解 Stack + Align 的基本用法

图片缩放

我们知道图片适配不同大小屏幕时必然出现不同程度的缩放。Flutter 中使用 Image 控件显示图片,Image.fit 属性则用于控制图片缩放方式。fit 属性为枚举类型 BoxFit,最常用值的包括如下几种:

  • fill - Fill the target box by distorting the source’s aspect ratio.
  • contain - As large as possible while still containing the source entirely within the target box.
  • cover - As small as possible while still covering the entire target box.

box_fit_fill.png (300×90)

box_fit_contain.png (300×90)

box_fit_cover.png (300×90)

先来看如何将背景图铺满 Stack。代码如下:

1
2
3
4
5
6
7
Stack(
children: <Widget>[
Image.asset(
'images/sign_in/sign_in_head.webp',
),
]
)

不同参数时效果分别如下:

Image.asset(width: null, fit: null)
-w595

Image.asset(width: double.infinity, fit: null)
-w579

Image.asset(width: double.infinity, fit: BoxFit.fill)
-w576

Image.asset(width: double.infinity, fit: BoxFit.cover)
-w571

就这里的场景而言(图片高度无限制),BoxFit.coverBoxFit.fill 效果无区别。不过 BoxFit.cover 保证图片不会变形,是个更好的选择。

控件定位

Stack 类用于对若干个控件以层叠方式布局。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Stack(
children: <Widget>[
Container(
width: 100,
height: 100,
color: Colors.red,
),
Container(
width: 90,
height: 90,
color: Colors.green,
),
Container(
width: 80,
height: 80,
color: Colors.blue,
),
],
)

Stack 控件的子节点要么是 positioned (PositionedAlign 控件) 要么是 non-positionedStack 控件的大小刚好包含所有的 non-positioned 子控件(这些控件默认位于 Stack 的左上角)。而 positioned 子节点的位置,则由其 top, right, bottom, left 属性来决定。

PositionedAlign 控件都能用于 Stack 控件定位和对齐。Positioned 以 top, right, bottom, left 属性来定位,这些属性分别用于指定控件到 Stack 各边框的距离;AlignAlignment(x, y) 属性来定位,这些属性分别用于指定控件水平方向和垂直方向的距离范围。

Alignment 的 x, y 属性规定如下:

  • Alignment(0.0, 0.0) - 表示矩形中点
  • Alignment(-1.0, -1.0) - 表示矩形左上角
  • Alignment(1.0, 1.0) - 表示矩形右下角

关于 Stack 有一个小细节要注意。通过如下两个例子说明:

1
2
3
4
5
6
7
8
9
10
11
Scaffold(
appBar: AppBar(title: Text('hello')),

body: Stack(
children: <Widget>[
Align(
alignment: Alignment(0, 0),
child: Text('hello, stack'),
)
],
),

这个例子中,Text 按预期居显示:

-w337

然而,当 Stack 外面嵌套一个 Column 或 ListView 之后,Align 似乎在垂直方向失去了定位的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Scaffold(
appBar: AppBar(title: Text('hello')),

body: Column(
children: <Widget>[
Stack(
children: <Widget>[
Align(
alignment: Alignment(0, 0),
child: Text('hello, stack'),
)
],
),
],
),

-w324

我们观察 Stack 外面嵌套一个 Column 或 ListView 之后控件树,截图如下:

-w549

所以这个现象不难理解:

  • Stack 外面无嵌套时,它占满了父控件,所以 Align 生效
  • Stack 外面嵌套 ColumnListView 时,Stack 大小未指定(最终以 Text 大小为准),所以 Align 从视觉效果上看在垂直方向不起作用。

给 Stack 指定大小就能解决这个问题。这里使用 SizedBox 指定 Stack 高度为 300。

-w551

解决方案

方案一

方案一使用 Stack + Positioned 定位,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  @override
Widget build(BuildContext context) {
// 计算屏幕宽度
double width = MediaQuery.of(context).size.width;
// 计算图片缩放比
final scale = width / 375.0;
return Stack(children: <Widget>[
Image.asset(
_signInHeadBg,
width: double.infinity,
fit: BoxFit.cover,
),
// 使用 Positioned 定位,指定 top 和 right 边距
Positioned(
top: 113 * scale,
right: 108 * scale,
child: Text(
'2',
style: TextStyle(color: c_FFFFFFFF, fontSize: 12),
),
),
]);
}

注意这里的实现细节,即按图片缩放比 scale 来调整 top 和 right,保证不同大小屏幕上都能完美适配。

方案二

方案二使用 Stack + Align 定位,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return SizedBox(
width: width,
height: 798.0 / 634 * width,
child: Stack(
children: <Widget>[
Image.asset(
_signInHeadBg,
width: double.infinity,
fit: BoxFit.cover,
),
Align(
child: Text(
'2',
style: TextStyle(color: c_FFFFFFFF, fontSize: 12),
),
alignment: Alignment(0.42, -0.51),
),
],
),
);

注意这里的实现细节,即为 Stack 添加一个指定大小的 SizeBox(大小与图片显示的大小相同),保证不同大小屏幕上都能完美适配。

-w547

如果不为 Stack 指定大小,则 Align 在垂直方向不生效。如下图:

-w547