Flutter自定义单子布局
单子布局,顾名思义也就是在这个layout中只有一个child。其实它存在的意义就是为了像自定义“view”一样,提供给开发者一种便利的方式来自己定义系统没有提供的“组件”,以便于满足不同的需求。要想自定义单子组件的布局,需要使用CustomSingleChildLayout
这个类。
CustomSingleChildLayout类介绍
要介绍一个类,那么首先就从类的源码开始
class CustomSingleChildLayout extends SingleChildRenderObjectWidget {
/// Creates a custom single child layout.
///
/// The [delegate] argument must not be null.
const CustomSingleChildLayout({
super.key,
required this.delegate,
super.child,
}) : assert(delegate != null);
/// The delegate that controls the layout of the child.
final SingleChildLayoutDelegate delegate;
@override
RenderCustomSingleChildLayoutBox createRenderObject(BuildContext context) {
return RenderCustomSingleChildLayoutBox(delegate: delegate);
}
@override
void updateRenderObject(BuildContext context, RenderCustomSingleChildLayoutBox renderObject) {
renderObject.delegate = delegate;
}
}
可以看到这里有三个参数,其中delegate
则是必传的,类型为SingleChildLayoutDelegate
,主要用于布局使用,child则是单子布局中传入的子widget。自定义单子布局其实就是为了定义child在布局中的位置,所以最关键的地方就是这个delegate了。
SingleChildLayoutDelegate类分析
自定义单组件布局需要解决的问题主要有三个
- 布局的大小是多大
- 给child的大小是多大
- child放在布局的什么位置
所以带着这几个问题我们去源码中找寻答案
abstract class SingleChildLayoutDelegate {
const SingleChildLayoutDelegate({Listenable? relayout})
: _relayout = relayout;
/// 这个跟自定义view一样,主要是当这个可监听对象的值发生变化时更新界面
final Listenable? _relayout;
/// 这个layout的大小,入参是父级传入的约束,默认值是传入约束的最大值
Size getSize(BoxConstraints constraints) => constraints.biggest;
/// 根据传入的约束,来生成对于child的约束
/// 子对象需要为自己选择一个满足这些约束的大小,
BoxConstraints getConstraintsForChild(BoxConstraints constraints) =>
constraints;
/// 返回child摆放的位置
/// `size` 是这个CustomSingleChildLayout的大小,它与上面的[getSize]返回的大小可能不一致
/// 原因在于如果父级传递过来的是紧约束,那么就以这个约束为准了,如果是松约束则才是[getSize]中自定义大小的返回。
/// `childSize`是这里的child的大小,它的大小与[getConstraintsForChild]返回的一致
Offset getPositionForChild(Size size, Size childSize) => Offset.zero;
/// 这个方法返回的是新旧Listenable的对比
/// 这个方法返回 false, 那么 [getSize], [getConstraintsForChild], 和 [getPositionForChild] 方法的调用就会被优化调
/// 也就是说除了第一次创建,再也不会变。如果返回true则会重新layout。
bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate);
}
通过上面的源码及注释可以看到我们通过getSize
来确定layout的大小,通过getConstraintsForChild
来确定child的大小,通过getPositionForChild
确定在布局的什么位置。所以接下来就可以实际操作一下看看能不能达到效果。
使用CustomSingleChildLayout自定义布局
比如我们想定义一个正方形的layout,在距离其左边和上边1/4的地方放置child
void main() {
runApp(
ColoredBox(
color: Colors.limeAccent,
child: CustomSingleChildLayout(
delegate: SquareLayoutDelegate(),
child: const SizedBox(
child: ColoredBox(color: Colors.red),
width: 50,
height: 50,
),
),
)
);
}
class SquareLayoutDelegate extends SingleChildLayoutDelegate {
@override
bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) {
return false;
}
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
//使用父布局大小
return super.getConstraintsForChild(constraints);
}
@override
Offset getPositionForChild(Size size, Size childSize) {
//摆放位置1/4处
return Offset(size.width / 4, size.height / 4);
}
@override
Size getSize(BoxConstraints constraints) {
final radius = constraints.biggest.shortestSide;
//布局为正方形
return Size(radius, radius);
}
}
class PolarLayoutDelegate extends SingleChildLayoutDelegate {
@override
bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) => false;
//传入父级约束,返回尺寸,这个尺寸就是这个自定义layout的大小
@override
Size getSize(BoxConstraints constraints) {
final radius = constraints.biggest.shortestSide;
return Size(radius, radius);
}
//对子级进行偏移定位
//size是父级的尺寸,childSize则为子级的尺寸与getConstraintsForChild返回的一致
@override
Offset getPositionForChild(Size size, Size childSize) {
return const Offset(10, 10);
}
//传入父级约束,生成传递约束
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return super.getConstraintsForChild(constraints);
}
}
得到的效果图如下,
可以看到并不是我们预期的效果,首先我们调试一下,通过断点运行,首先会调用getSize方法,调用栈如图
实际上是在RenderCustomSingleChildLayoutBox中的_getSize调用的
可以看到这里传入了一个紧约束,然后利用这个紧约束和delegate的大小来生成真正的约束. delegate中设置的约束如下
实际生成的约束如下
可以看到结果就是传递进来的紧约束(所以可以得出结论,在紧约束的情况下 CustomSingleChildLayout
的尺寸会被锁死)。
其次会调用getConstraintsForChild
方法,可以看到这里传入的仍然是父级的constraints
最后调用的是getPositionForChild方法
可以看到这里的size确实和getSize返回的不一致,而childSize确实与getConstraintsForChild的返回值一致。通过上面的调试可以看出来父级的紧约束会影响自定义布局中尺寸约束,所以我们要把紧约束变成松约束。解除紧约束的方式主要有
- 使用UnconstrainedBox,让自身约束变为无约束
- 使用Align,Row,Column,Flex等布局组件,将自身约束变为松约束
- 在CustomSingleChildLayout中通过BoxConstraints生成新的约束(只能改变child的大小)
通过以上我们先来让子child显示为规定的大小(上面定义的红色sizeBox),其实就是修改getConstraintsForChild
的返回值。
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
//使用父布局大小
// return super.getConstraintsForChild(constraints);
//对其添加松约束,即规定范围,只要child的自身尺寸不超过这个范围就是定义的大小
return constraints.loosen();
}
我们修改为constraints.loosen()。效果如下
可以看到确实是50像素,如果我们用BoxConstraints来生成新的约束
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
//使用父布局大小
// return super.getConstraintsForChild(constraints);
//对其添加松约束,即规定范围,只要child的自身尺寸不超过这个范围就是定义的大小
//return constraints.loosen();
//对其添加紧约束,则child自身的大小被忽略,直接使用这里定义的大小
return BoxConstraints.tight(Size(200,200));
}
可以看到child的大小已经不是50了而是定义的200。接下来我们来修改layout的大小为正方形,也就是让CustomSingleChildLayout收到的约束为松约束,那就用布局组件来嵌套
void main() {
runApp(Align(
child: ColoredBox(
color: Colors.limeAccent,
child: CustomSingleChildLayout(
delegate: SquareLayoutDelegate(),
child: const SizedBox(
child: ColoredBox(color: Colors.red),
width: 50,
height: 50,
),
),
),
));
}
可以看到我们达到了预期,此时再看看传递过去的约束是什么。首先是_getSize方法获取Size时
传入的约束是松约束,所以最终的结果就是自己通过getSize
方法定义的大小。至此就剩最后一个方法没有介绍了那就是shouldRelayout
。这个其实就是用来刷新的,比如自定义view(Flutter自定义view的实现时使用的)CustomPainter
中的shuoldRepaint
是一样的。这里就直接走起。
shouldRelayout的使用
首先我们需要创建一个可监听对象,也就是SingleChildLayoutDelegate
中的Listenable
,这个Listenable主要是定义影响布局的因素,比如layout的大小或者是child的大小,child的位置等等。也就是说它的值要用在getSize
或getPositionForChild
或getConstraintsForChild
中。
其次直接在shouldRelayout中返回这个listenable变量的新旧值对比。比如我们动态更改child的大小
void main() {
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: Material(child: HomePage()),
));
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
ValueNotifier<double> sizeNotifier = ValueNotifier(50);
@override
Widget build(BuildContext context) {
return Column(
children: [
Align(
alignment: Alignment.center,
child: ColoredBox(
color: Colors.limeAccent,
child: CustomSingleChildLayout(
delegate: SquareLayoutDelegate(listenable: sizeNotifier),
child: const SizedBox(
child: ColoredBox(color: Colors.red),
width: 50,
height: 50,
),
),
),
),
FilledButton(
onPressed: () {
sizeNotifier.value = 200;
},
child: Text("更改child尺寸为200"))
],
);
}
}
class SquareLayoutDelegate extends SingleChildLayoutDelegate {
final ValueListenable<double> listenable;
SquareLayoutDelegate({required this.listenable})
: super(relayout: listenable);
@override
bool shouldRelayout(covariant SquareLayoutDelegate oldDelegate) {
return oldDelegate.listenable != listenable;
}
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
//使用父布局大小
// return super.getConstraintsForChild(constraints);
//对其添加松约束,即规定范围,只要child的自身尺寸不超过这个范围就是定义的大小
// return constraints.loosen();
//对其添加紧约束,则child自身的大小被忽略,直接使用这里定义的大小
return BoxConstraints.tight(Size(listenable.value, listenable.value));
}
@override
Offset getPositionForChild(Size size, Size childSize) {
//摆放位置1/4处
return Offset(size.width / 4, size.height / 4);
}
@override
Size getSize(BoxConstraints constraints) {
final radius = constraints.biggest.shortestSide;
//布局为正方形
return Size(radius, radius);
}
}
点击按钮前和点击按钮后的效果如下
当然也可以改变其他值,这个就不做介绍了,仁者见仁智者见智了。至此自定义单子布局已经介绍完毕,动手搞起吧。
转载自:https://juejin.cn/post/7242145395724222521