【布局 widget】Flutter CustomSingleChildLayout

简介: 【布局 widget】Flutter CustomSingleChildLayout

image.png

作为 single child 布局组件的收关之作,CustomSingleChildLayout 可以很大限度的自定义组件的各个方面。

一般来说,single child 布局有三板斧

  1. 确定 child  的 constrains
  2. 确定 自己的 大小
  3. 摆放 child

第 3 条是可选的,有的 single child 布局组件没有摆放这个步骤,或自己和 child 一样大。

我们就分这三步来看下如何使用 CustomSingleChildLayout。在开始之前,需要先定义一个扩展自 SingleChildLayoutDelegate 的类,负责实现这些功能。

class MyLayout extends SingleChildLayoutDelegate {
  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    // TODO: implement getConstraintsForChild
    return super.getConstraintsForChild(constraints);
  }
  @override
  Size getSize(BoxConstraints constraints) {
    // TODO: implement getSize
    return super.getSize(constraints);
  }
  @override
  Offset getPositionForChild(Size size, Size childSize) {
    // TODO: implement getPositionForChild
    return super.getPositionForChild(size, childSize);
  }
  @override
  bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) {
    // 不需要重绘
    return false;
  }
}
复制代码

MyLayout 已经有了雏形了。我们来一步一步完善。

确定 child  的 constrains

BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    constraints = constraints / 2;
    return constraints;
  }
复制代码

在这里可以随意设置 constrains ,本例我们只是简单的把 constrains 减半。现在看下效果

image.png

Center(child:  Container(
          color: Colors.green[400],
          width: 200,
          height: 200,
          child: CustomSingleChildLayout(
            delegate: MyLayout(),
            child: Container(
              width: 300,
              height: 500,
              color: Colors.blueAccent,
            ),
          )));
复制代码

无论你把 width,height 设多大,蓝色块的大小都不会超过 100。大家可以自己玩玩,随便改下 constrains 看看是什么效果,甚至可以改成无约束,让 child 彻底放飞。constraints = constraints / 2;  改成 constraints = BoxConstraints(); 没有约束后,child 可以任意大小,也不会报错。

确定 MyLayout 的大小

第二步是确定自己的大小,也就是 size。默认的super.getSize(constraints)是返回允许的最大值。size 必须在 constrains 之内。比如我们尝试把 size 扩大为两倍 constraints.biggest * 2; 是无效的。最终的 size 为 constraints.biggest; 如果 constraints 是 tight ,那么任何尝试都是无效的。如果是 loose, size 可以在最大最小值之间。

本例我们直接调用父级的 getSize。

Size getSize(BoxConstraints constraints) {
    return super.getSize(constraints);
 }
复制代码

摆放 child

到最后一步了,马上大功告成!摆放 child 就是确定 child 的位置。通过 size 和 child size 返回 child相对于 MyLayout 的偏移。super.getPositionForChild(size, childSize) 的返回值是 Offset.zero 也就是把 child 摆放在左上角。我们可以修改这个值,比如摆放在距离左上角 20 的位置。

image.png

Offset getPositionForChild(Size size, Size childSize) {
    return Offset(20,20);
 }
复制代码

这个偏移量是没有限制的,可以超出父元素  Offset(20, 20) 改成 Offset(120, 20)

image.png

这种自由度给了我们很大的发挥空间。不但是可以超出父元素,超出屏幕也没人管。负值也可以!

CustomSingleChildLayout 重绘

在前面我们直接给shouldRelayout 返回 false ,指示 CustomSingleChildLayout 不需要重绘。这种粗暴的方式可能会引起问题。

比如我们给 SingleChildLayoutDelegate 增加一个参数 offset 来控制 child 的偏移量,用一个按钮来控制 offset 的值,如果 shouldRelayout 返回 false 的话 无论 offset 怎么改变,都是没有效果的。正确的做法是当 offset 改变的时候  shouldRelayout 应该返回 true

class MyLayout extends SingleChildLayoutDelegate {
  const MyLayout({required this.offset});
  final Offset offset;
  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    constraints = constraints / 2;
    return super.getConstraintsForChild(constraints);
  }
  @override
  Size getSize(BoxConstraints constraints) {
    return constraints.biggest;
  }
  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return offset;
  }
  @override
  bool shouldRelayout(covariant MyLayout oldDelegate) {
    return oldDelegate.offset != offset;
  }
}
复制代码
MaterialApp(
      home: Scaffold(
      body: Center(
          child: Container(
              color: Colors.green[400],
              width: 200,
              height: 200,
              alignment: Alignment.bottomRight,
              child: CustomSingleChildLayout(
                delegate: MyLayout(offset: offset),
                child: Container(
                  width: 300,
                  height: 500,
                  color: Colors.blueAccent,
                ),
              ))),
      floatingActionButton: ElevatedButton(
        onPressed: () {
          setState(() {
            offset = offset + const Offset(10, 10);
          });
        },
        child: const Text('增加 offset'),
      ),
));
复制代码

CustomSingleChildLayout child 执行动画

有的时候,child 可能要执行动画。比如我们把上面例子的 offset 改由动画驱动自己变化,先看下效果。

image.png

这个需要需要用到 relayout 参数。relayout 是一个 listenable ,可以高效的监听变化进行重绘。

class MyLayout extends SingleChildLayoutDelegate {
  MyLayout({required this.offsetAnimation}) : super(relayout: offsetAnimation);
  final Animation offsetAnimation;
  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    constraints = constraints / 2;
    return super.getConstraintsForChild(constraints);
  }
  @override
  Size getSize(BoxConstraints constraints) {
    return constraints.biggest;
  }
  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return offsetAnimation.value;
  }
  @override
  bool shouldRelayout(covariant MyLayout oldDelegate) {
    return false;
  }
}
复制代码
class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
  late AnimationController _offsetController;
  late Animation<Offset> _animation;
  @override
  void initState() {
    _offsetController =
        AnimationController(vsync: this, duration:const Duration(seconds: 2))
          ..repeat();
    _animation = _offsetController.drive(Tween<Offset>(begin: Offset.zero, end:const Offset(100,100)));
    super.initState();
  }
  ...
  Container(
     color: Colors.green[400],
     width: 200,
     height: 200,
     alignment: Alignment.bottomRight,
     child: CustomSingleChildLayout(
       delegate: MyLayout(offsetAnimation: _animation),
       child: Container(
         width: 300,
         height: 500,
         color: Colors.blueAccent,
       )));
  ...
复制代码

你可能注意到 shouldRelayout 返回 false,因为 offsetAnimation 是由 Animation ticks vsync 驱动的。用 relayout 参数来驱动动画的方式是高效的,省去了  build 的开销。

目录
相关文章
|
2天前
|
容器
Flutter Widget 解析
Flutter Widget 解析
|
2天前
|
存储 容器
Flutter 有状态Widget 和 无状态Widget
Flutter 有状态Widget 和 无状态Widget
|
1月前
深入理解Flutter鸿蒙next版本 中的Widget继承:使用extends获取数据与父类约束
本文详细介绍了Flutter中如何通过继承其他Widget来创建自定义组件。首先解释了Widget继承的基本概念,包括StatelessWidget和StatefulWidget的区别。接着通过具体示例展示了如何继承StatelessWidget和StatefulWidget,并在子类中访问父类的build方法和状态。最后,结合多个自定义Widget展示了如何在实际应用中灵活使用继承和组合来构建复杂的UI。
76 8
|
1月前
|
容器
flutter&鸿蒙next 使用 InheritedWidget 实现跨 Widget 传递状态
在 Flutter 中,状态管理至关重要。本文详细介绍了如何使用 InheritedWidget 实现跨 Widget 的状态传递。InheritedWidget 允许数据在 Widget 树中向下传递,适用于多层嵌套的场景。通过一个简单的计数器示例,展示了如何创建和使用 InheritedWidget,包括其基础概念、工作原理及代码实现。虽然 InheritedWidget 较底层,但它是许多高级状态管理解决方案的基础。
103 2
|
2月前
|
容器
flutter:第一个flutter&Widget的使用 (二)
本文介绍了Flutter框架下的基本组件及其用法,包括简单的 Stateless Widget 如文本和按钮,以及更复杂的 StatefulWidget 示例。详细解释了如何使用 `context` 获取祖先小部件的信息,并展示了 `MaterialApp` 的属性及用途。此外,还探讨了 `StatefulWidget` 与 `StatelessWidget` 的区别,以及 `AppBar` 的常见属性配置方法。适合Flutter初学者参考学习。
|
1月前
|
Dart JavaScript 前端开发
Flutter 的 Widget 概述与常用 Widgets 与鸿蒙 Next 的对比
Flutter 是 Google 开发的开源 UI 框架,用于快速构建高性能的移动、Web 和桌面应用。Flutter 通过 Widget 构建 UI,每个 UI 元素都是 Widget,包括文本、按钮、图片等。Widget 不仅描述外观,还描述行为,是不可变的。常见的 Widget 包括结构型(Container、Column、Row)、呈现型(Text、Image)、交互型(ElevatedButton)和状态管理型(StatefulWidget)。Flutter 与鸿蒙 Next 在组件化架构、开发语言、布局系统、性能和跨平台支持方面各有优势
75 0
|
5月前
Flutter-底部弹出框(Widget层级)
文章描述了如何在Flutter中使用DraggableScrollableSheet创建一个底部弹出框,同时保持其可手势滑动关闭。作者遇到问题并提出对原控件进行扩展,以支持头部和列表布局的滑动关闭功能。
199 0
|
6月前
Flutter StreamBuilder 实现局部刷新 Widget
Flutter StreamBuilder 实现局部刷新 Widget
52 0
|
7月前
|
Android开发
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
|
7月前
|
开发框架 前端开发 搜索推荐
【Flutter前端技术开发专栏】Flutter中的自定义Widget与渲染流程
【4月更文挑战第30天】探索Flutter的自定义Widget与渲染流程。自定义Widget是实现复杂UI设计的关键,优点在于个性化设计、功能扩展和代码复用,但也面临性能优化和复杂性管理的挑战。创建步骤包括设计结构、定义Widget类、实现构建逻辑和处理交互。Flutter渲染流程涉及渲染对象树、布局、绘制和合成阶段。实践案例展示如何创建带渐变背景和阴影的自定义按钮。了解这些知识能提升应用体验并应对开发挑战。查阅官方文档以深入学习。
86 0
【Flutter前端技术开发专栏】Flutter中的自定义Widget与渲染流程