开始

在Android中我们要实现一个布局需要继承ViewGroup, 重写其中的onLayoutonMeasure方法. 其中onLayout负责给子控件设置布局区域, onMeaseure度量子控件大小和自身大小. 今天我们就研究下Flutter是如何实现布局的.

Flutter布局

首先我们挑选一个Flutter控件去看源码, 我们就选Stack, 因为它足够简单. 从表象上讲它只要重叠摆放一组子控件即可. 先看下Stack的源码:

class Stack extends MultiChildRenderObjectWidget {
Stack({
Key key,
this.alignment: AlignmentDirectional.topStart,
this.textDirection,
this.fit: StackFit.loose,
this.overflow: Overflow.clip,
List<Widget> children: const <Widget>[],
}) : super(key: key, children: children); final AlignmentGeometry alignment;
final StackFit fit;
final Overflow overflow; @override
RenderStack createRenderObject(BuildContext context) {
return new RenderStack(
alignment: alignment,
textDirection: textDirection ?? Directionality.of(context),
fit: fit,
overflow: overflow,
);
} @override
void updateRenderObject(BuildContext context, RenderStack renderObject) {
renderObject
..alignment = alignment
..textDirection = textDirection ?? Directionality.of(context)
..fit = fit
..overflow = overflow;
} @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(new EnumProperty<StackFit>('fit', fit));
properties.add(new EnumProperty<Overflow>('overflow', overflow));
}
}

Stack继承自MultiChildRenderObjectWidget, 重写了createRenderObject其返回了一个RenderStack对象, 实际的工作者. 而updateRenderObject则只是修改RenderStack对象的属性. debugFillProperties方法则是填充该类属性的参数值到DiagnosticPropertiesBuilder中.

我们看看Flex, 也是如此, 重写了createRenderObject其返回了一个RenderFlex对象, 实际的工作者. 而updateRenderObject则只是修改RenderFlex对象的属性.

所以我们接下来看看RenderStack, 精简代码如下:

class RenderStack extends RenderBox
with ContainerRenderObjectMixin<RenderBox, StackParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> {
RenderStack({
List<RenderBox> children,
AlignmentGeometry alignment: AlignmentDirectional.topStart,
TextDirection textDirection,
StackFit fit: StackFit.loose,
Overflow overflow: Overflow.clip,
}) : assert(alignment != null),
assert(fit != null),
assert(overflow != null),
_alignment = alignment,
_textDirection = textDirection,
_fit = fit,
_overflow = overflow {
addAll(children);
} bool _hasVisualOverflow = false; @override
void performLayout() {
_resolve();
assert(_resolvedAlignment != null);
_hasVisualOverflow = false;
bool hasNonPositionedChildren = false;
if (childCount == ) {
size = constraints.biggest;
assert(size.isFinite);
return;
} double width = constraints.minWidth;
double height = constraints.minHeight; BoxConstraints nonPositionedConstraints;
assert(fit != null);
switch (fit) {
case StackFit.loose:
nonPositionedConstraints = constraints.loosen();
break;
case StackFit.expand:
nonPositionedConstraints = new BoxConstraints.tight(constraints.biggest);
break;
case StackFit.passthrough:
nonPositionedConstraints = constraints;
break;
}
assert(nonPositionedConstraints != null); RenderBox child = firstChild;
while (child != null) {
final StackParentData childParentData = child.parentData; if (!childParentData.isPositioned) {
hasNonPositionedChildren = true; child.layout(nonPositionedConstraints, parentUsesSize: true); final Size childSize = child.size;
width = math.max(width, childSize.width);
height = math.max(height, childSize.height);
} child = childParentData.nextSibling;
} if (hasNonPositionedChildren) {
size = new Size(width, height);
assert(size.width == constraints.constrainWidth(width));
assert(size.height == constraints.constrainHeight(height));
} else {
size = constraints.biggest;
} assert(size.isFinite); child = firstChild;
while (child != null) {
final StackParentData childParentData = child.parentData; if (!childParentData.isPositioned) {
childParentData.offset = _resolvedAlignment.alongOffset(size - child.size);
} else {
BoxConstraints childConstraints = const BoxConstraints(); if (childParentData.left != null && childParentData.right != null)
childConstraints = childConstraints.tighten(width: size.width - childParentData.right - childParentData.left);
else if (childParentData.width != null)
childConstraints = childConstraints.tighten(width: childParentData.width); if (childParentData.top != null && childParentData.bottom != null)
childConstraints = childConstraints.tighten(height: size.height - childParentData.bottom - childParentData.top);
else if (childParentData.height != null)
childConstraints = childConstraints.tighten(height: childParentData.height); child.layout(childConstraints, parentUsesSize: true); double x;
if (childParentData.left != null) {
x = childParentData.left;
} else if (childParentData.right != null) {
x = size.width - childParentData.right - child.size.width;
} else {
x = _resolvedAlignment.alongOffset(size - child.size).dx;
} if (x < 0.0 || x + child.size.width > size.width)
_hasVisualOverflow = true; double y;
if (childParentData.top != null) {
y = childParentData.top;
} else if (childParentData.bottom != null) {
y = size.height - childParentData.bottom - child.size.height;
} else {
y = _resolvedAlignment.alongOffset(size - child.size).dy;
} if (y < 0.0 || y + child.size.height > size.height)
_hasVisualOverflow = true; childParentData.offset = new Offset(x, y);
} assert(child.parentData == childParentData);
child = childParentData.nextSibling;
}
} @protected
void paintStack(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
} @override
void paint(PaintingContext context, Offset offset) {
if (_overflow == Overflow.clip && _hasVisualOverflow) {
context.pushClipRect(needsCompositing, offset, Offset.zero & size, paintStack);
} else {
paintStack(context, offset);
}
}
}

可以看出RenderStack接收了所有传递给Stack的参数, 毕竟RenderStack才是实际干活的^^. performLayout负责了所有布局相关的工作. performLayout首先分析StackFit参数, 该参数有3个值:

  • StackFit.loose 按最小的来.
  • StackFit.expand 按最大的来.
  • StackFit.passthrough Stack上层为->Expanded->Row, 横向尽量大, 纵向尽量小.

得出BoxConstraints. 然后遍历所有子控件, 如果不是Positioned类型子控件, 则将BoxConstraints传给子控件让它根据父控件大小自己内部布局. 并且记录下所有子控件结合RenderStack自生大小得出的最大高度和宽度. 将其设置为当前控件大小.

接着再继续从头遍历子控件, 如果不是Positioned类型子控件, 根据alignment参数, 设置子控件在父控件中的偏移量, 比如Stack设置了居中, 上面计算出宽100, 高200, 而子控件宽30, 高30, 那么子控件需要偏移x=35, y=85. 如果是Positioned类型的子控件, 先将RenderStacksize大小, 减去Positioned属性里的大小. 再来计算便宜量.

这个里面有_hasVisualOverflow变量, 如果内容超出RenderStack大小, 其值为true. 也就是我们写布局时, 内容超过范围了, 报出来一个色块提示, 就是如此得出的.
_overflow属性则指定了子控件的绘制区域是否能超过父控件, 跟Android中的clipChildren属性很像.

另外我们再分析下IndexedStack, 该控件一次只能显示一个子控件. 其实际差异在RenderIndexedStack

class RenderIndexedStack extends RenderStack {
...
@override
bool hitTestChildren(HitTestResult result, { @required Offset position }) {
if (firstChild == null || index == null)
return false;
assert(position != null);
final RenderBox child = _childAtIndex();
final StackParentData childParentData = child.parentData;
return child.hitTest(result, position: position - childParentData.offset);
} @override
void paintStack(PaintingContext context, Offset offset) {
if (firstChild == null || index == null)
return;
final RenderBox child = _childAtIndex();
final StackParentData childParentData = child.parentData;
context.paintChild(child, childParentData.offset + offset);
}
...
}

重写了RenderStackpaintStackhitTestChildren方法, 只绘制选中的子控件, 和接收事件.

总结

实现一个自定义布局, 我们需要先继承MultiChildRenderObjectWidget, 然后重写createRenderObjectupdateRenderObject方法, 前者返回我们自定义的RenderBox的对象. 后者更新想要传递的属性. 然后需要我们继承RenderBox, 来扩展我们想要的功能特性.

Flutter自定义布局套路的更多相关文章

  1. Flutter的布局方法

    重点是什么? Widgets 是用于构建UI的类. Widgets 用于布局和UI元素. 通过简单的widget来构建复杂的widget Flutter布局机制的核心就是widget.在Flutter ...

  2. 干货之UIButton的title和image自定义布局

    当需要实现一个自定义布局图片和标题的按钮时候,不知道有多少少年直接布局了UIButton,亦或是自定义一个UIView,然后以空白UIButton.UILabel.UIImageVew作为subVie ...

  3. SharePoint 2013 设置自定义布局页

    在SharePoint中,我们经常需要自定义登陆页面.错误页面.拒绝访问等:不知道大家如何操作,以前自己经常在原来页面改或者跳转,其实SharePoint为我们提供了PowerShell命令,来修改这 ...

  4. Collection View 自定义布局(custom flow layout)

    Collection view自定义布局 一般我们自定义布局都会新建一个类,继承自UICollectionViewFlowLayout,然后重写几个方法: prepareLayout():当准备开始布 ...

  5. iOS-UICollectionView自定义布局

    UICollectionView自定义布局 转载: http://answerhuang.duapp.com/index.php/2013/11/20/custom_collection_view_l ...

  6. 详细分享UICollectionView的自定义布局(瀑布流, 线性, 圆形…)

    前言: 本篇文章不是分享collectionView的详细使用教程, 而是属于比较’高级’的collectionView使用技巧, 阅读之前, 我想你已经很熟悉collectionView的基本使用, ...

  7. OC - 31.通过封装的自定义布局快速实现商品展示

    概述 实现效果 设计思路 采用MVC架构,即模型—视图-控制器架构 使用MJExtension框架实现字典转模型 使用MJRefresh框架实现上拉和下拉刷新 上拉刷新,加载新的数据 下拉刷新,加载更 ...

  8. OC - 30.如何封装自定义布局

    概述 对于经常使用的控件或类,通常将其分装为一个单独的类来供外界使用,以此达到事半功倍的效果 由于分装的类不依赖于其他的类,所以若要使用该类,可直接将该类拖进项目文件即可 在进行分装的时候,通常需要用 ...

  9. OC - 29.自定义布局实现瀑布流

    概述 瀑布流是电商应用展示商品通常采用的一种方式,如图示例 瀑布流的实现方式,通常有以下几种 通过UITableView实现(不常用) 通过UIScrollView实现(工作量较大) 通过UIColl ...

随机推荐

  1. 关于oracle RAC心跳线采用直连 还是交换机连接的建议

    关于oracle RAC心跳线的连接方式,各个论坛,包括网上文章的说法是:官方说是不建议直连,建议采用交换机连接的方式!PS:但是,一直没有找到官方文档的出处,有知道的兄弟,烦请评论区提供下地址!!! ...

  2. 【Pthreads】Pipeline Model(Assembly Line)示例

    前言 Pthreads 有几种工作模型,例如 Boss/Workder Model.Pileline Model(Assembly Line).Background Task Model.Interf ...

  3. Python做int()强制类型转换的时候,小数是如何取舍的?

    白月黑羽今天给大家分享一个冷知识:) int()强制类型转换小数是如何取舍的? 使用 int() 将小数转换为整数,小数取整会采用比较暴力的截断方式,即小数点后面的会被强制舍去,向下取整. 例如:5. ...

  4. Silverlight中使用MVVM(1)--基础

    Silverlight中使用MVVM(1)--基础 Silverlight中使用MVVM(2)—提高 Silverlight中使用MVVM(3)—进阶 Silverlight中使用MVVM(4)—演练 ...

  5. 理解 async/await 的执行

    这是一篇简单的短文章,方便理解. 开局先丢官宣:sec-async-function-definitions 这个链接是对 await 的解释,解释了它的执行. await 的执行意味着(官宣巴拉巴拉 ...

  6. tsung压力测试——Tsung测试统计报告说明【转】

    1.主要统计信息 Tsung统计数据是平均每十秒重置一次,所以这里的响应时间(连接.请求.页面.会话)是指每十秒的平均响应时间: connect: 表示 每个连接持续时间: Hightest 10se ...

  7. Solidity的自定义结构体深入详解

    一.结构体定义 结构体,Solidity中的自定义类型.我们可以使用Solidity的关键字struct来进行自定义.结构体内可以包含字符串,整型等基本数据类型,以及数组,映射,结构体等复杂类型.数组 ...

  8. Ice简介+Qt代码示例

    1.ICE是什么? ICE是ZEROC的开源通信协议产品,它的全称是:The Internet Communications Engine,翻译为中文是互联网通信引擎,是一个面向对象的中间件,它封装并 ...

  9. js的轮播效果

    图片的轮播效果!主要运用了元素的style样式属性,与 setInterval(); <!DOCTYPE html> <html> <head lang="en ...

  10. spring mvc requestmapping 配置多个

    参考 import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation. ...