【布局 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 的开销。

目录
相关文章
|
5天前
|
前端开发 开发者 UED
Flutter的自定义Painter:深入探索自定义绘制Widget的技术实现
【4月更文挑战第26天】Flutter的自定义Painter允许开发者根据需求绘制独特UI,通过继承`CustomPaint`类和重写`paint`方法实现。在`paint`中使用`Canvas`绘制图形、路径等。创建自定义Painter类后,将其作为`CustomPaint` Widget的`painter`属性使用。此技术可实现自定义形状、渐变、动画等复杂效果,提升应用视觉体验。随着Flutter的进化,自定义Painter将提供更丰富的功能。
|
5天前
|
编解码 算法 开发者
Flutter的布局系统:深入探索布局Widget与布局原则
【4月更文挑战第26天】Flutter布局系统详解,涵盖布局Widget(Row/Column、Stack、GridView/ListView、CustomSingleChildLayout)和布局原则(弹性布局、约束优先、流式布局、简洁明了)。文章旨在帮助开发者理解并运用Flutter的布局系统,创建适应性强、用户体验佳的界面。通过选择合适的布局Widget和遵循原则,可实现复杂且高效的UI设计。
|
5天前
|
开发框架 搜索推荐 Android开发
Flutter的Widget基础:概念、分类与深入探索
【4月更文挑战第26天】Flutter Widget详解:基础、分类与工作原理。Widget是Flutter UI的核心,描述界面外观而非直接渲染。分为基础、布局、可滚动及状态管理四大类。基于响应式编程,状态变化时自动更新UI。了解其概念、分类和原理,能助开发者高效构建精美应用。随着Flutter生态发展,Widget系统潜力无限。
|
8月前
|
Dart 前端开发 开发工具
谷歌移动UI框架Flutter教程之Widget
谷歌移动UI框架Flutter教程之Widget
|
9月前
Flutter源码分析笔记:Widget类源码分析
本文记录阅读与分析Flutter源码 - Widget类源码分析。
62 0
Flutter源码分析笔记:Widget类源码分析
|
10月前
|
API Android开发 容器
Flutter控件之基类Widget封装
基类的Widget主要确定以下几个方面,第一就是,自定义一个抽象类还是非抽象类,第二、继承方式,采取有状态还是无状态,第三、关于组件的点击方式,如何进行实现。
105 0
Flutter万物皆为Widget
Flutter 中的 Widget 是描述界面元素的基本单元,可以包含视觉和交互元素。Widget 可以嵌套、组合和扩展,从而构建出复杂的 UI 界面。在 Flutter 中,Widget 可以分为两种类型:StatelessWidget 和 StatefulWidget。
Flutter万物皆为Widget
|
JavaScript 前端开发 开发工具
Flutter | 基础 Widget
Flutter | 基础 Widget
【布局 widget】 Flutter GridView
GridView 独有的参数其实只有一个 gridDelegate,gridDelegate 的作用是为 GridView 布局,制定每行几个 child,空白多少等。
111 0
【布局 widget】 Flutter GridView
|
缓存 前端开发 Java
手写一个Flutter State Widget,来让你彻底理解State的来龙去脉
手写一个Flutter State Widget,来让你彻底理解State的来龙去脉
177 0
手写一个Flutter State Widget,来让你彻底理解State的来龙去脉