Slint 中的元素定位 (Positioning) 和布局 (Layout)
基本逻辑
Slint 当中进行元素定位的基本逻辑是这样的:所有的可见元素都需要放置在窗口 (window) 中,每个元素都有 x
和 y
属性,这两个属性表示当前元素 相对父元素的位置偏移。Slint 计算某个元素在整个窗口中的位置时,会按照层级关系,一级一级将这个 x
和 y
的值进行累加,最终得到元素相对顶层窗口的位置。
相应的,width
和 height
两个属性决定了可见元素的宽度和高度。结合使用这两对属性,我们有如下两种定位元素的基本方式:
- 显式指定:直接设置每个元素的
x
、y
、width
和height
属性; - 自动定位:通过使用布局元素 (layout element)。
前者适合简单的或者静态的布局,而后者适合复杂、可缩放的用户界面。布局描述了元素之间的位置关系。
显式指定
我们从一个案例[1]来理解元素的显式定位:
export component Example inherits Window {
preferred-width: 200px;
preferred-height: 200px;
Rectangle {
x: 100px;
y: 70px;
width: parent.width - self.x;
height: parent.height - self.y;
background: blue;
Rectangle {
x: 10px;
y: 5px;
width: 50px;
height: 30px;
background: green;
}
}
}
在这个例子中,两个 Rectangle 的位置和大小都是固定[2]的,其中内层 Rectangle
的大小和位置都是固定的值(相对于窗口的左上角是不变的),外层的位置固定,但大小由于与 parent
的宽高绑定,所以是动态适应的。
单位
在使用显式的值定义元素的位置和大小时,Slint 需要我们给出具体使用的数值单位。具体有以下两种:
- 逻辑像素:使用
px
,也是推荐使用的单位; - 物理像素:使用
phx
。
之所以更推荐逻辑像素 px
,是因为它能够自适应高分辨率屏幕的需要,可以避免我们自行对界面执行缩放,是更简便、通用的方法。
此外,宽度和高度还可以写成百分比的形式,表示 元素相对父元素的宽度/高度值。
默认值
- 如果没有特别给出
x
和y
属性的值,那么元素默认会被放置在父元素居中的位置。 - 如果没有特别给出
width
和height
属性的值,那么元素会按照:- 对于
Image
、Text
和大多数微件 (widget),这些值会按照具体内容(不是子元素)的大小进行设定; - 对于下列几个内置组件,它们不需要填充内容,且当没有子元素时,会默认填满父元素:
Rectangle
、TouchArea
、FocusScope
和Flickable
。 - 对于布局元素,不管设定了怎样的尺寸倾向 (preferred size),默认都会填满父元素。
- 对于其他元素(包括自定义组件),默认会按照尺寸倾向的设定来设定尺寸。
- 对于
尺寸倾向
通过设定 preferred-width
和 preferred-height
属性可以定义元素的尺寸倾向。
在 没有内容 的情况下,尺寸倾向取决于子元素,并且是所有子元素中,尺寸倾向值最大,且 x
和 y
属性没有设置的那个。所以,尺寸倾向属性默认值的计算方式是:从子元素向父元素逐级进行,除非被显式设定。
唯一一种特殊的情况就是,当我们把 preferred-width
和 preferred-height
设置为 100%
时,当前组件的尺寸默认会使用父元素的 尺寸(非尺寸倾向)。
自动布局
我们可以在 Slint 中通过不同的布局元素来自动计算子元素的位置和尺寸:
VerticalLayout
/HorizontalLayout
会将子元素沿着垂直或水平轴向排布;GridLayout
会将子元素按照网格的行、列排布。
布局元素是可以进行嵌套的。我们也可以使用一些限定属性来进一步调整布局。比如每个元素都有最大最小尺寸和尺寸倾向属性,可以通过以下属性值来调整:
min-width
min-height
max-width
max-height
preferred-width
preferred-height
当一个元素被显式指定了 width
和 height
时,它在布局元素中就有了固定的尺寸。
我们可以通过 horizontal-stretch
和 vertical-stretch
属性调整元素及其相邻元素对布局中轴向剩余空间的利用方式。比如 0
值表示即使有多余空间,也不应该缩放元素。当所有元素的这个属性都为 0
时,所有元素会均匀利用剩余空间。
所有限定属性值也具有默认值,它们取决于元素的 内容。如果元素的 x
和 y
都没有显式指定,那么这些限定属性也会自动被应用到其父元素上。
布局元素的公共属性
所有的布局元素都有这样两个公共属性:
spacing
:表示布局元素的子元素之间的留白,通常会被应用到轴向分布的场景中;padding
:表示布局内部元素与布局边界之间的留白。
如果需要更细致得调节布局内部元素与四个方向边界间的留白,可以使用这四个额外的属性:
padding-left
padding-right
padding-top
padding-bottom
VerticalLayout
和 HorizontalLayout
VerticalLayout
和 HorizontalLayout
分别将子元素按行和列轴向分布。默认情况下,所有子元素会被适当拉伸或挤压以适应布局元素的大小。我们以可以通过额外属性调节它们的对齐方式 (alignment)。对其方式会影响子元素的拉伸情况。
在下面的例子中,两个矩形 (Rectangle
) 放置在同一个水平布局 (HorizontalLayout
) 中,两个矩形都设置了尺寸限定属性 min-width
,它们在父组件中的排布是这样的:
export component Example inherits Window {
width: 256px;
height: 256px;
HorizontalLayout {
Rectangle {
background: gray;
min-width: 64px;
}
Rectangle {
background: yellow;
min-width: 64px;
}
}
}
假如我们为水平分布增加对齐限定属性 (alignment constraint) alignment: start
,那么 Slint 在进行水平布局时会按照此限定条件,取消原本的默认拉伸行为,转而使所有元素尽可能小,即按照子元素 Rectangle
的限定属性 min-width
进行布局:
export component Example inherits Window {
width: 256px;
height: 256px;
HorizontalLayout {
alignment: start;
Rectangle {
background: gray;
min-width: 64px;
}
Rectangle {
background: yellow;
min-width: 64px;
}
}
}
甚至如果我们再极端一点,去掉两个 Rectangle
的 min-width
属性,那么当水平布局的对齐方式限定为 alignment: start
时,整个界面中将不存在这两个矩形!
对齐方式 (alignment)
在水平和垂直布局中,如果我们为子元素设置了 width
和 height
属性,本应该在布局时尊重这些属性的值。但假如布局的 alignment
设置为 stretch
(同时也是默认值),它们在实际布局中的尺寸就取决于:
- 这些子元素本身的
min-width
和min-height
,或 - 这些子元素内部的布局、元素尺寸经过计算后得到的最小尺寸
而最终的结果取决于上述两者中更大的那个。
在 Slint 中,水平和垂直布局可以采用的 alignment
对齐属性值包括:
stretch
(默认值)start
end
center
space-between
:布局剩余空间等量分布各子元素之间space-around
:布局剩余空间等量分布在最外侧的子元素与边界之间
拉伸算法
当我们将布局的 alignment
设置为 stretch
,布局中所有子元素都会首先按照它们的最小尺寸计算实际尺寸。然后如果剩余空间将会按照各个子元素的 horizontal-stretch
和 vertical-stretch
属性分配给各个子元素,得到更新后的子元素尺寸。但各元素更新后的尺寸不能超过其自身的最大尺寸值。
horizontal-stretch
和 vertical-stretch
属性的值被称为拉伸因子 (stretch factor),它是个浮点数。默认情况下,按照其内容大小计算尺寸元素的元素,其拉伸因子的默认值是 0
;而按照其父元素尺寸计算尺寸的元素,其拉伸因子的默认值是 1
。在还有剩余空间的情况下,一个拉伸因子为 0
的元素,只要不满足以下任何一个条件,它的尺寸就会维持最小尺寸限定值:
- 其他所有元素的拉伸因子也都是
0
; - 其他所有元素都已经拉伸到了它们的最大尺寸限定值。
比如下面的例子:
export component Example inherits Window {
width: 256px;
height: 256px;
VerticalLayout {
HorizontalLayout {
alignment: start;
Rectangle {
background: gray;
min-width: 32px;
horizontal-stretch: 0;
padding-left: 16px;
Text {
text: 0;
}
}
Rectangle {
background: yellow;
min-width: 64px;
horizontal-stretch: 0;
padding-left: 16px;
Text {
text: 0;
}
}
Rectangle {
background: green;
min-width: 128px;
horizontal-stretch: 0;
padding-left: 16px;
Text {
text: 0;
}
}
}
HorizontalLayout {
Rectangle {
background: gray;
min-width: 32px;
horizontal-stretch: 0.5;
padding-left: 16px;
Text {
text: 0.5;
}
}
Rectangle {
background: yellow;
min-width: 64px;
horizontal-stretch: 1;
padding-left: 16px;
Text {
text: 1;
}
}
Rectangle {
background: green;
min-width: 128px;
horizontal-stretch: 0.5;
padding-left: 16px;
Text {
text: 0.5;
}
}
}
HorizontalLayout {
Rectangle {
background: gray;
min-width: 32px;
horizontal-stretch: 0;
padding-left: 16px;
Text {
text: 0;
}
}
Rectangle {
background: yellow;
min-width: 64px;
horizontal-stretch: 1;
padding-left: 16px;
Text {
text: 1;
}
}
Rectangle {
background: green;
min-width: 128px;
horizontal-stretch: 1;
padding-left: 16px;
Text {
text: 1;
}
}
}
HorizontalLayout {
Rectangle {
background: gray;
min-width: 32px;
horizontal-stretch: 0;
padding-left: 16px;
Text {
text: 0;
}
}
Rectangle {
background: yellow;
min-width: 64px;
horizontal-stretch: 1;
padding-left: 16px;
Text {
text: 1;
}
}
Rectangle {
background: green;
min-width: 128px;
max-width: 128px;
horizontal-stretch: 1;
padding-left: 16px;
Text {
text: 1;
}
}
}
HorizontalLayout {
Rectangle {
background: gray;
min-width: 32px;
horizontal-stretch: 1;
padding-left: 16px;
Text {
text: 1;
}
}
Rectangle {
background: yellow;
min-width: 64px;
horizontal-stretch: 1;
padding-left: 16px;
Text {
text: 1;
}
}
Rectangle {
background: green;
min-width: 128px;
horizontal-stretch: 1;
padding-left: 16px;
Text {
text: 1;
}
}
}
HorizontalLayout {
Rectangle {
background: gray;
min-width: 32px;
horizontal-stretch: 1;
padding-left: 16px;
Text {
text: 1;
}
}
Rectangle {
background: yellow;
min-width: 64px;
horizontal-stretch: 1;
padding-left: 16px;
Text {
text: 1;
}
}
Rectangle {
background: green;
min-width: 128px;
max-width: 128px;
horizontal-stretch: 1;
padding-left: 16px;
Text {
text: 1;
}
}
}
}
}
例子很长,我们先看结果:
第一行是为水平布局设置了 alignment: start
的结果,剩余各行则都是用了默认的拉伸行为,即 alignment: stretch
。每个色块中的文字表示这个 Rectangle
的 horizontal-stretch
的值。
- 第一行:任何子元素都不拉伸;
- 第二行:剩余空间 (
64px
) 被三个元素按照 \(1:2:1\) 的比例分配,即16px
、32px
、16px
; - 第三行:剩余空间 (
64px
) 被第二、三个元素按照 \(1:1\) 均分,即32px
、32px
; - 第四行:原本和第三行一样,应该按照 \(1:1\) 均分,即
32px
、32px
分配,但是第三个元素设置了max-width: 128px
,因此它不应该继续拉伸,所以只有第二个元素独占额外的64px
; - 第五行:剩余空间被三个元素按照 \(1:1:1\) 分配,即
21.33px
、21.33px
、21.33px
; - 第五行:原本和第五行一样,应该按照 \(1:1:1\) 均分,即
21.33px
、21.33px
、21.33px
分配,但是第三个元素设置了max-width: 128px
,因此它不应该继续拉伸,所以前两个元素均分额外的64px
,即32px
、32px
;
for
循环表达式
在水平和垂直布局中,我们可以使用 for
和 if
表达式:
export component Example inherits Window {
width: 256px;
height: 256px;
VerticalLayout {
HorizontalLayout {
spacing: 16px;
for t in ["Hello", "Beautiful World","from", "Slint!"] : Rectangle {
background: gray;
min-width: 32px;
horizontal-stretch: 0;
padding-left: 16px;
Text {
text: t;
}
}
}
}
}
网格布局 (GridLayout)
网格布局用于将子元素按行和列依次放入网格中。每个元素都有 row
、col
、rowspan
和 colspan
几个属性[3]。
子元素在网格中的排布方式是这样的:
row
和col
计数器都从0
开始分配;- 如果没有指定
Row
作为子元素的容器:- 下一个子元素将会保持
row
值,递增col
值; - 显式指定
row
值 (N
) 后,从当前元素开始,将会重新将元素放置在row=N
和col=0
的位置(除非显式指定col
值改变当前col
计数器);
- 下一个子元素将会保持
- 如果指定
Row
作为子元素的容器:- 当子元素没有指定
row
属性时,同一个Row
内的子元素会按照row
不变,col
递增的逻辑依次排布; - 每变化一个
Row
,会在 当前row
计数器的基础上增加1
。 - 在同一个
Row
中,如果显式指定了row
的值,会重置row
计数器,并且同一个Row
中后续的元素都会在新的row
中依次递增col
排布。
- 当子元素没有指定
- 如果因为显式指定计数器
row
和/或col
导致当前元素的位置与已经放置的元素重叠,那么该元素位置会被覆盖。
值得注意的是,row
、col
、rowspan
和 colspan
几个属性都必须在编译时确定,也就是无法通过算术表达式和属性依赖的方式指定。另外,当前版本的 Slint 尚不支持在网格布局中使用 for
和 if
表达式。
案例来自 Slint 官方文档,稍作修改。 ↩︎
指父级元素不变的情况下。这里的歧义主要来自于外部
Rectangle
的大小实际取决于顶层Window
的大小。 ↩︎row
和col
都是从 0 开始的索引值,除非显式指定,它们的值都是由计数器进行的。 ↩︎