Skip to content

过渡 ​

当布局改变一个元素时,Transition 让它平滑移动:从上一帧显示的位置到现在的位置、在父元素中的位置、尺寸,以及背景和边框颜色。视图只说明东西在哪里;过渡负责动画地到达那里。

go
var rowMotion = ui.ElementTransition{
	Enter: &ui.Motion{Collapse: true},
	Exit:  &ui.Motion{Collapse: true},
}

ui.Column(c).Children(func() {
	removed := -1
	for _, it := range app.items {
		ui.Row(c).Key(it.ID).Padding(8).Transition(rowMotion).Children(func() {
			ui.Text(c, it.Title).Grow(1)
			if ui.Button(c, "Remove").Clicked() {
				removed = it.ID // 等循环结束后再处理(见 Views)
			}
		})
	}
	if removed >= 0 {
		app.remove(removed)
	}
})

新增的行展开进入,被删除的行收起,其余各行滑向自己的位置——无论条目是被添加、删除、排序还是打乱。

什么在动 ​

ElementTransition 在其 Duration(为零时默认 200 ms)内,沿其 Ease(为 nil 时是 ui.EaseOut,或任意缓动函数)播放动画;若设置了 Animation,则按其指定的方式:使用弹簧(如 ui.Snappy)时,元素在途中被发往别处也会保持速度平滑转向(见动态效果):

  • 位置:元素在其父元素中的位置。随父元素一起移动(如滚动视图滚动时)不算变化。
  • 尺寸:其宽度和高度。沿途每个尺寸下内容都会重新布局,就像面板打开时文本不断重新换行。
  • 颜色:其背景和边框颜色,如悬停效果的淡入淡出。

三者默认全部参与动画,除非用 Position、Size 或 Colors 只选其中几项:

go
tile := ui.Box(c).Size(64, 48).Radius(8).Background(t.Surface)
if tile.Hovered() {
	tile.Background(t.SurfaceHover)
}
tile.Transition(ui.ElementTransition{Colors: true, Duration: 120 * time.Millisecond})

元素需要在各帧之间保持相同的 ID:一个 Key,或在兄弟元素中的相同位置。过渡只为该元素本身做动画:一次变化波及的其他元素需要自己的过渡才能平滑移动;用相同的过渡即可同步移动,如上面的各行,以及当尺寸变化时容纳它们的列:

go
open := app.sidebar
w := float32(56)
if open {
	w = 240
}
ui.Row(c).Fill().Children(func() {
	ui.Column(c).Width(w).ClipX().Transition(ui.ElementTransition{Size: true})
	ui.Column(c).Grow(1).Transition(ui.ElementTransition{})
})

Animation 中的 Delay 会让变化先保持一段时间:随索引递增的延迟可以让列表的行错开入场:

go
for i, it := range app.items {
	a := ui.Snappy
	a.Delay = time.Duration(i) * 25 * time.Millisecond
	ui.Row(c.Key(it.ID)).Transition(ui.ElementTransition{Animation: &a, Enter: &ui.Motion{Y: 8}})
}

Transitioning() 报告在上一帧中该元素的过渡是否正在移动它。

入场与退场 ​

Enter 指元素出现时从何处来,Exit 指元素离开时往何处去——都是相对于它在布局中的位置:Motion 用 X 和 Y 位移,用 Opacity 设置不透明度(未设置时为 0,即完全透明),用 Scale 缩放(0 表示保持原尺寸),用 Rotate 绕 TransformOrigin 旋转若干度,并可用 Collapse——在列中不占高度、在行中不占宽度,内容被裁剪。

go
toast := ui.ElementTransition{
	Enter: &ui.Motion{Y: 12},
	Exit:  &ui.Motion{Opacity: 1, X: 300},
}
popup := ui.ElementTransition{
	Animation: &ui.Snappy,
	Enter:     &ui.Motion{Scale: 0.95},
	Exit:      &ui.Motion{Scale: 0.95},
}

只有当父元素在上一帧就已存在时,新出现的元素才会播放其 Enter,这样一次性显示的页面或对话框不会让其中每个元素都做动画。入场过程中,若布局移动了它(例如上方的行落位),在其过渡不驱动动画的维度上(Position、Size)它会立即移动;若其尺寸改变,Collapse 所占空间也按比例调整:只有 Enter 在播放。当元素不再被构建而其父元素仍在时,它的副本(按上一帧的样子)会在兄弟元素之下播放 Exit:副本不占空间,因此兄弟元素会立即移入它的位置——若它们也有过渡,则平滑移动;副本既不接收指针也不接收焦点。若在退场结束前该元素重新被构建,它会从副本所在的位置回来。

共享元素 ​

Match 赋予元素一个与其他元素共享的身份,例如网格中的照片缩略图与其详情视图中的大图。上一帧中某 Match 显示过,而带相同 Match 的元素这一帧出现,那么新元素就从那里来——以那个元素的尺寸和颜色,即使父元素不同——就像 SwiftUI 的 matchedGeometryEffect 和 Motion 的 layoutId:

go
type photoMatch struct{ id int }

if app.open == nil {
	for _, p := range app.photos {
		thumb := ui.Image(c.Key(p.ID), p.Thumb).Size(64, 64).
			Transition(ui.ElementTransition{Animation: &ui.Snappy, Match: photoMatch{p.ID}})
		if thumb.Clicked() {
			app.open = p
		}
	}
} else {
	ui.Image(c.Key("hero"), app.open.Full).Size(320, 240).
		Transition(ui.ElementTransition{Animation: &ui.Snappy, Match: photoMatch{app.open.ID}})
}

请用你自己定义的类型作为身份,以免与其他人撞车。出现的元素从对方的包围盒移动到自己的包围盒,沿途每个尺寸下内容都会重新布局,与 Size 的布局方式相同,因此其中没有任何东西被拉伸,圆角也是如此;同一时刻只有一个元素持有某个 Match;离开的那个按其自身的 Exit(若有)退场。

不动的时候 ​

调整窗口大小引起的变化不做动画。当桌面要求减少动态效果时(c.Preferences().ReduceMotion),什么都不移动:元素立即出现在布局给定的位置,共享元素也不会"飞"。颜色仍然平滑变化;只淡入淡出而不折叠的 Enter 或 Exit 会在原地淡入淡出,正如 Apple 的指南所建议;会折叠(从而移动兄弟元素)的则立即完成。只有在有东西移动时才绘制帧。

List的行由列表摆放,而列表只构建可见的行:行的内容可以有过渡,但行本身不会随过渡滑动、入场或退场。