Flutter自定义View基础,重写SingleChildRenderObjectWidget
Flutter自定义View基础,重写SingleChildRenderObjectWidget
Flutter有很多种类的Widget, 其中处理界面绘制, 布局, 事件相关的则是RenderObjectWidget.
基本上大部分涉及界面绘制和事件处理的Widget都由RenderObjectWidget扩展而来.
今天我们的主角是SingleChildRenderObjectWidget, 看名字都知道这是一个含有一个Child的Widget, 在Flutter中有很多这种Widget, 比如Padding, Center, Align, Container其实应该也算, 只不过它是多个这种容器的组合.
基础实现
这里我们主要尝试重写SingleChildRenderObjectWidget来实现一个简单容器.
第一步我们实现一个SimpleContainer继承于SingleChildRenderObjectWidget, 并创建一个继承于Renderbox的SimpleRenderBox.
class SimpleContainer extends SingleChildRenderObjectWidget {
const SimpleContainer({super.key, required super.child});
@override
RenderObject createRenderObject(BuildContext context) {
return SimpleRenderBox();
}
@override
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
super.updateRenderObject(context, renderObject);
}
}
class SimpleRenderBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
@override
void performLayout() {
size = const Size(200, 200);
child!.layout(constraints, parentUsesSize: true);
}
}
其实这样我们就实现了一个最简单的容器, 我们可以写个简单页面测试下效果
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Simple Container"),
),
body: Container(
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
width: 300,
height: 300,
color: Colors.lightBlueAccent,
child: SimpleContainer(
child: Container(
width: 100,
height: 100,
color: Colors.yellowAccent,
),
),
),
),
);
}
可以看到我们这里只显示了最外层的Container.
如果我们要将SimpleContainer和最内层的Container显示出来还需要在SimpleRenderBox中实现paint方法.
我们简单实现一个
@override
void paint(PaintingContext context, Offset offset) {
//简单绘制自己的大小和位置
if (size > Size.zero) {
context.canvas.drawRect(offset & size, Paint()..color = Colors.greenAccent);
}
//绘制子控件
context.paintChild(child!, offset);
}
我们再运行看一下
这里自身的大小和位置信息还有子控件已经正常显示出来了.
大小和位置控制
以上代码很简单的设置了控件的大小和子控件的位置, 很明显是不符合大部分时候需求的.
所以这里说一下自身大小的控制和子控件的位置控制.
自身大小的控制
其实自身大小的控制是比较简单的, 只需要在performLayout中配置一下size属性就行.
不过我们需要根据constraints来配置自身的大小, constraints定义了我们的大小范围, 如果我们想跟父容器一样大可以使用
size = Size(constraints.maxWidth, constraints.maxHeight);
如果我们想大小和子控件一样大可以使用
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
注意这里一定需要先调用child的layout方法后才能拿到child的size.
子控件位置的控制
子控件的位置由子控件的parentData属性来辅助控制.
我们先添加一个方法来获取这个属性
BoxParentData _getChildParentData() {
return child!.parentData as BoxParentData;
}
然后给这个属性配置Offset, 如果我们想控件居中的话可以使用如下方法重写performLayout
@override
void performLayout() {
size = const Size(200, 200);
final childConstraints = BoxConstraints(maxWidth: size.width, maxHeight: size.height);
child!.layout(childConstraints, parentUsesSize: true);
final offsetDx = (size.width - child!.size.width) / 2;
final offsetDy = (size.height - child!.size.height) / 2;
_getChildParentData().offset = Offset(offsetDx, offsetDy);
}
然后我们还需要重写paint方法
@override
void paint(PaintingContext context, Offset offset) {
//简单绘制自己的大小和位置
if (size > Size.zero) {
context.canvas.drawRect(offset & size, Paint()..color = Colors.greenAccent);
}
//绘制子控件
context.paintChild(child!, offset + _getChildParentData().offset);
}
如此我们便能让子控件居中显示了.
如果有更复杂的需求我们还可以通过重写setupParentData来自定义ParentData数据, 这里不多做赘述, 如有需要自寻查找资料.
事件处理
还没完, 我们还可以通过Renderbox来实现点击, 滑动, 鼠标悬停等事件的处理.
这里先说一下Flutter的事件处理机制, 其实它的机制很简单, 事件分发根据Render树从上到下深度遍历访问每一个RenderBox的hitTest做命中测试, 如果在这个方法里调用
result.add(BoxHitTestEntry(this, position));
则这个Renderbox则可以收到事件, 事件会发送到handleEvent方法中.
然后只要在hitTest中返回了true, 则表示这个事件被消耗了, 被消耗后不会再继续测试其它Renderbox是否接收此事件.
以下是Renderbox的hitTest的默认实现
bool hitTest(BoxHitTestResult result, { required Offset position }) {
//......
if (_size!.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
很明显是只要child或者自身命中测试成功的话就会监听事件并拦截.
知道原理的话我们就能做很多事情, 比如我们如果想让SimpleContainer含有自己的点击事件但是在子控件范围不生效, 那我们可以尝试这样重写hitTest.
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (size.contains(position)) {
if (child!.hitTest(result, position: position - _getChildParentData().offset)) {
return true;
} else {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
然后为了效果我们在SimpleRenderBox简单增加一下handleEvent的逻辑
@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
if (event is PointerDownEvent) {
showSimpleToast("Simple Container Clicked");
}
}
作为对比我们在界面代码中给最小的Container添加一个点击事件
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Simple Container"),
),
body: Container(
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
width: 300,
height: 300,
color: Colors.lightBlueAccent,
child: SimpleContainer(
child: InkWell(
onTap: () {
showSimpleToast("Child Clicked");
},
child: Container(
width: 100,
height: 100,
color: Colors.yellowAccent,
),
),
),
),
),
);
}
然后我们看看效果
这里是符合我们一般点击事件逻辑的, 如果我们想做点不寻常的事呢, 比如我们想截断子控件的事件, 那么我们改写hitTest如下
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (size.contains(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
return false;
}
如此子控件将不会再有事件响应, 效果如下
好了至此你已经掌握了最基本的自定义控件.
转载自:https://juejin.cn/post/7242249906257641529