写在前面
\Flow
组件需要搭配 \FlowDelegate
使用,在 \FlowDelegate
里编写逻辑,\Flow
就可以通过它来调整其 children 的尺寸和位置。
使用 \Flow
是用来优化 children 使用变换矩阵(transformation matrices)重新布局。类似于 Stack。
也就是说当我们想自定义一种对 children 布局的 Widget 的时候,可以使用 \Flow
。
内容
基本用法
从官方的例子来看一个基本的用法。在这个例子里,除了展示对 children 的布局,还演示了如何通过使用 Animation
来让 children 使用的变换矩阵发生变化,从而达到位置的变更。
class FlowMenu extends StatefulWidget {
const FlowMenu({Key? key}) : super(key: key);
@override
_FlowMenuState createState() => _FlowMenuState();
}
class _FlowMenuState extends State<FlowMenu> with SingleTickerProviderStateMixin {
late AnimationController menuAnimation;
IconData lastTapped = Icons.notifications;
final List<IconData> menuItems = <IconData>[
Icons.home,
Icons.new_releases,
Icons.notifications,
Icons.settings,
Icons.menu,
];
void _updateMenu(IconData icon) {
if (icon != Icons.menu) {
setState(() => lastTapped = icon);
}
}
@override
void initState() {
super.initState();
menuAnimation = AnimationController(
duration: const Duration(milliseconds: 250),
vsync: this,
);
}
Widget flowMenuItem(IconData icon) {
final double buttonDiameter = MediaQuery.of(context).size.width / menuItems.length;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: RawMaterialButton(
fillColor: lastTapped == icon ? Colors.amber[700] : Colors.blue,
splashColor: Colors.amber[100],
shape: const CircleBorder(),
constraints: BoxConstraints.tight(Size(buttonDiameter, buttonDiameter)),
onPressed: () {
_updateMenu(icon);
menuAnimation.status == AnimationStatus.completed
? menuAnimation.reverse()
: menuAnimation.forward();
},
child: Icon(
icon,
color: Colors.white,
size: 45.0,
),
),
);
}
@override
Widget build(BuildContext context) {
return Flow(
delegate: FlowMenuDelegate(menuAnimation: menuAnimation),
children: menuItems.map<Widget>((IconData icon) => flowMenuItem(icon)).toList(),
);
}
}
class FlowMenuDelegate extends FlowDelegate {
FlowMenuDelegate({required this.menuAnimation}) : super(repaint: menuAnimation);
final Animation<double> menuAnimation;
@override
bool shouldRepaint(FlowMenuDelegate oldDelegate) {
return menuAnimation != oldDelegate.menuAnimation;
}
@override
void paintChildren(FlowPaintingContext context) {
double dx = 0.0;
for (int i = 0; i < context.childCount; ++i) {
dx = context.getChildSize(i)!.width * i;
context.paintChild(
i,
transform: Matrix4.translationValues(
dx * menuAnimation.value,
0,
0,
),
);
}
}
}
相比在 layout 阶段布局 children,这里是在 paint 阶段,在 \FlowDelegate.paintChildren
方法里,让 children 使用变换矩阵来确定位置。这样通过简单地对\Flow
进行重绘,从而达到 children 的高效重新定位。就无需让 children 再重新布局。
最有效地促使 \Flow
进行重绘地方法就是给 \FlowDelegate
的构造方法里提供一个 Animation
,这样 \Flow
就会监听这个 Animation
,然后当 Animation
有回调时,\Flow
就会进行重绘,避免了 build 和 layout。
构造方法
class Flow extends MultiChildRenderObjectWidget {
Flow({
Key? key,
required this.delegate,
List<Widget> children = const <Widget>[],
this.clipBehavior = Clip.hardEdge,
}) : assert(delegate != null),
assert(clipBehavior != null),
super(key: key, children: RepaintBoundary.wrapAll(children));
Flow.unwrapped({
Key? key,
required this.delegate,
List<Widget> children = const <Widget>[],
this.clipBehavior = Clip.hardEdge,
}) : assert(delegate != null),
assert(clipBehavior != null),
super(key: key, children: children);
...
}
\Flow
有两个构造方法,区别在于默认情况下会用 RepaintBoundary
进行包裹,它包裹住每个 children,避免当 \Flow
进行重绘的时候,导致 children 也进行重绘。
一个例子
假如我们想实现一种多个 Widget 之间有重叠的效果:
我们当然可以使用 Stack,然后给每个 Widget 套上 Position,计算 x 来进行。但如果使用\Flow
,我们可以做出一个封装性更好的 Widget 出来。
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Widget _circle(Color color) {
return Container(
height: 30,
width: 30,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: SizedBox(
height: 30,
child: MyHorizontalOverlayOffsetWidget(xOffset: 16, children: [
_circle(Colors.red),
_circle(Colors.amber),
_circle(Colors.black),
_circle(Colors.blue)
]),
)),
),
);
}
}
class MyHorizontalOverlayOffsetWidget extends StatelessWidget {
final List<Widget> children;
/// children 之间水平方向的偏移量
final double xOffset;
/// 将 children 里的多个 Widget 进行一定的重叠偏移的 Widget
const MyHorizontalOverlayOffsetWidget(
{Key? key, required this.children, this.xOffset = 0.0})
: super(key: key);
@override
Widget build(BuildContext context) {
return Flow(
delegate: OverlayOffsetDelegate(xOffset: xOffset), children: children);
}
}
class OverlayOffsetDelegate extends FlowDelegate {
final double xOffset;
OverlayOffsetDelegate({this.xOffset = 0.0});
@override
void paintChildren(FlowPaintingContext context) {
var currentXOffset = 0.0;
for (var index = 0; index < context.childCount; index++) {
context.paintChild(index,
transform: Matrix4.translationValues(currentXOffset, 0, 0));
currentXOffset += xOffset;
}
}
@override
bool shouldRepaint(covariant FlowDelegate oldDelegate) {
return oldDelegate is OverlayOffsetDelegate &&
oldDelegate.xOffset != xOffset;
}
}