本文主要介绍Flutter布局中的Stack、IndexedStack、GridView控件,详细介绍了其布局行为以及使用场景,并对源码进行了分析。html
A widget that positions its children relative to the edges of its box.git
Stack能够类比web中的absolute,绝对布局。绝对布局通常在移动端开发中用的较少,可是在某些场景下,仍是有其做用。固然,能用Stack绝对布局完成的,用其余控件组合也都能实现。github
Stack的布局行为,根据child是positioned仍是non-positioned来区分。web
对于绘制child的顺序,则是第一个child被绘制在最底端,后面的依次在前一个child的上面,相似于web中的z-index。若是想调整显示的顺序,则能够经过摆放child的顺序来进行。缓存
Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Stack
Stack( alignment: const Alignment(0.6, 0.6), children: [ CircleAvatar( backgroundImage: AssetImage('images/pic.jpg'), radius: 100.0, ), Container( decoration: BoxDecoration( color: Colors.black45, ), child: Text( 'Mia B', style: TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ], );
示例代码我就直接用的Building Layouts in Flutter中的例子,效果以下less
构造函数以下:ide
Stack({ Key key, this.alignment = AlignmentDirectional.topStart, this.textDirection, this.fit = StackFit.loose, this.overflow = Overflow.clip, List<Widget> children = const <Widget>[], })
alignment:对齐方式,默认是左上角(topStart)。函数
textDirection:文本的方向,绝大部分不须要处理。布局
fit:定义如何设置non-positioned节点尺寸,默认为loose。学习
其中StackFit有以下几种:
overflow:超过的部分是否裁剪掉(clipped)。
Stack的布局代码有些长,在此分段进行讲解。
if (childCount == 0) { size = constraints.biggest; return; }
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; }
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); } else { size = constraints.biggest; }
第一步是根据positioned的绝对位置,计算出约束条件后进行布局。
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);
Stack的场景仍是比较多的,对于须要叠加显示的布局,通常均可以使用Stack。有些场景下,也能够被其余控件替代,咱们应该选择开销较小的控件去实现。
A Stack that shows a single child from a list of children.
IndexedStack继承自Stack,它的做用是显示第index个child,其余child都是不可见的。因此IndexedStack的尺寸永远是跟最大的子节点尺寸一致。
在此仍是将Stack的例子稍加改造,将index设置为1,也就是显示含文本的Container的节点。
Container( color: Colors.yellow, child: IndexedStack( index: 1, alignment: const Alignment(0.6, 0.6), children: [ CircleAvatar( backgroundImage: AssetImage('images/pic.jpg'), radius: 100.0, ), Container( decoration: BoxDecoration( color: Colors.black45, ), child: Text( 'Mia B', style: TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ], ), )
其绘制代码很简单,由于继承自Stack,布局方面表现基本一致,不一样之处在于其绘制的时候,只是将第Index个child进行了绘制。
@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); }
若是须要展现一堆控件中的一个,可使用IndexedStack。有必定的使用场景,可是也有控件能够实现其功能,只不过操做起来可能会复杂一些。
A scrollable, 2D array of widgets.
GridView在移动端上很是的常见,就是一个滚动的多列列表,实际的使用场景也很是的多。
GridView的布局行为不复杂,自己是尽可能占满空间区域,布局行为上彻底继承自ScrollView。
Object > Diagnosticable > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > GridView
从继承关系看,GridView是在ScrollView的基础上封装而来的,这跟移动端的相似。
GridView.count( crossAxisCount: 2, children: List.generate( 100, (index) { return Center( child: Text( 'Item $index', style: Theme.of(context).textTheme.headline, ), ); }, ), );
示例代码直接用了Creating a Grid List中的例子,建立了一个2列总共100个子节点的列表。
默认构造函数以下:
GridView({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, @required this.gridDelegate, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, double cacheExtent, List<Widget> children = const <Widget>[], })
同时也提供了以下额外的四种构造方法,方便开发者使用。
GridView.builder GridView.custom GridView.count GridView.extent
scrollDirection:滚动的方向,有垂直和水平两种,默认为垂直方向(Axis.vertical)。
reverse:默认是从上或者左向下或者右滚动的,这个属性控制是否反向,默认值为false,不反向滚动。
controller:控制child滚动时候的位置。
primary:是不是与父节点的PrimaryScrollController所关联的主滚动视图。
physics:滚动的视图如何响应用户的输入。
shrinkWrap:滚动方向的滚动视图内容是否应该由正在查看的内容所决定。
padding:四周的空白区域。
gridDelegate:控制GridView中子节点布局的delegate。
cacheExtent:缓存区域。
@override Widget build(BuildContext context) { final List<Widget> slivers = buildSlivers(context); final AxisDirection axisDirection = getDirection(context); final ScrollController scrollController = primary ? PrimaryScrollController.of(context) : controller; final Scrollable scrollable = new Scrollable( axisDirection: axisDirection, controller: scrollController, physics: physics, viewportBuilder: (BuildContext context, ViewportOffset offset) { return buildViewport(context, offset, axisDirection, slivers); }, ); return primary && scrollController != null ? new PrimaryScrollController.none(child: scrollable) : scrollable; }
上面这段代码是ScrollView的build方法,GridView就是一个特殊的ScrollView。GridView自己代码没有什么,基本上都是ScrollView上的东西,主要会涉及到Scrollable、Sliver、Viewport等内容,这些内容比较多,所以源码就先略了,后面单独出一篇文章对ScrollView进行分析吧。
使用场景不少,很是常见的控件。也有控件能够实现其功能,例如官方说的,GridView其实是一个silvers只包含一个SilverGrid的CustomScrollView。
笔者建了一个Flutter学习相关的项目,Github地址,里面包含了笔者写的关于Flutter学习相关的一些文章,会按期更新,也会上传一些学习Demo,欢迎你们关注。