用Android原生实现流式布局【实现例如app的很多标签】需要自定义View继承自ViewGroup,然后代码量也不小。
相关自定义View实现流式布局,请参考这位老兄的博客:
https://blog.csdn.net/u013107751/article/details/81701606
或者慕课网上的android免费课程有一门课是专门实现自定义流式布局的。
而在Flutter中实现此效果却非常简单。
1.布局类Widget的介绍:
布局类Widget都会包含一个或多个子widget,不同的布局类Widget对子widget排版(layout)方式 不同。Element树才是最终的绘制树,Element树是通过widget树来创建的(通 过 Widget.createElement() ),widget其实就是Element的配置数据。
Flutter中,根据 Widget是否需要包含子节点将Widget分为了三类,分别对应三种Element:
Widget | 对应的Element | 用途 |
LeafRenderObjectWidget | LeafRenderObjectElement | Widget树的叶子节点, 用于没有子节点的 widget,通常基础 widget都属于这一类, 如Text、Image。 |
SingleChildRenderObjectWidget | SingleChildRenderObjectElement | 包含一个子Widget, 如: ConstrainedBox、 DecoratedBox等 |
MultiChildRenderObjectWidget | MultiChildRenderObjectElement | 包含多个子Widget,一 般都有一个children 参数,接受一个Widget 数组。如Row、 Column、Stack等 |
Flutter中的很多Widget是直接继承自StatelessWidget或StatefulWidget,然后 在 build() 方法中构建真正的RenderObjectWidget,如Text,它其实是继承自StatelessWidget, 然后在 build() 方法中通过RichText来构建其子树,而RichText才是继承自 LeafRenderObjectWidget。所以为了方便叙述,我们也可以直接说Text属于 LeafRenderObjectWidget(其它widget也可以这么描述),这才是本质。StatelessWidget和StatefulWidget就是两个用于组合Widget的基类,它们本身并不关联最终的渲染对象 (RenderObjectWidget)。
布局类Widget就是指直接或间接继承(包含)MultiChildRenderObjectWidget的Widget的 Widget,它们一般都会有一个children属性用于接收子Widget。我们看一下继承关系 Widget > RenderObject > (Leaf/SingleChild/MultiChild)RenderObjectWidget 。RenderObjectWidget类中定义了创建、更新RenderObject的方法,子类必须实现他们,关于 RenderObject我们现在只需要知道它是最终布局、渲染UI界面的对象即可,也就是说,对于布局类 Widget来说,其布局算法都是通过对应的RenderObject对象来实现的.
2.弹性布局Flex:
弹性布局允许子widget按照一定比例来分配父容器空间,弹性布局的概念在其UI系统中也都存在,如 H5中的弹性盒子布局,Android中的FlexboxLayout。Flutter中的弹性布局主要通过Flex和 Expanded来配合实现。
2.1 Flex:
Flex可以沿着水平或垂直方向排列子widget,如果你知道主轴方向,使用Row或Column会方便一些, 因为Row和Column都继承自Flex,参数基本相同,所以能使用Flex的地方一定可以使用Row或 Column。Flex本身功能是很强大的,它也可以和Expanded配合实现弹性布局.
Flex({
...
@required this.direction, //弹性布局的方向, Row默认为水平方向,Column默认为垂直方向
List<Widget> children = const <Widget>[],
})
Flex继承自MultiChildRenderObjectWidget,对应的RenderObject为RenderFlex, RenderFlex中实现了其布局算法。
2.2 Expanded:
可以按比例“扩伸”Row、Column和Flex子widget所占用的空间【类似于android布局中的layout: width/height = 0 ,layout:weight=n (n>0) 然后系统根据 (每个weight的值/所有的weight值相加)=每个组件所占布局的比例。】
const Expanded({
int flex = 1,
@required Widget child,
})
flex为弹性系数,如果为0或null,则child是没有弹性的,即不会被扩伸占用的空间。如果大于0, 所有的Expanded按照其flex的比例来分割主轴的全部空闲空间。
示例代码如下【Expanded和Flex的配合使用】:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 表单Form',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// home: FormRouteDemo(),
home: FlexLayoutRouteDemo(),
);
}
}
import 'package:flutter/material.dart';
class FlexLayoutRouteDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: <Widget>[
//Flex的2个子widget按1:2来占据水平空间
Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
flex: 1,
child: Container(
height: 30.0,
color: Colors.red,
),
),
Expanded(
flex: 2,
child: Container(
height: 30.0,
color: Colors.green,
),
),
],
),
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: SizedBox(
height: 100.0,
//Flex的3个子widget,在垂直方向按2:1:1 来占用100像素的空间
child: Flex(
direction: Axis.vertical,
children: <Widget>[
Expanded(
flex: 2,
child: Container(
height: 30.0,
color: Colors.blue,
),
),
Spacer(
flex: 1,
),
Expanded(
flex: 1,
child: Container(
height: 30.0,
color: Colors.greenAccent,
)
),
],
),
),
)
],
),
);
}
}
3.流式布局:
3.1 Wrap:
在使用Row和Colum时,如果子widget超出屏幕范围,则会报溢出错误,如:
Row(
children: <Widget>[
Text("xxx"*100)//显示100次“xxx”
],
);
运行后:
右边溢出部分报错。这是因为Row默认只有一行,如果超出屏幕不会折行。我们把超出屏幕 显示范围会自动折行的布局称为流式布局。Flutter中通过Wrap和Flow来支持流式布局。将上例中的 Row换成Wrap后溢出部分则会自动折行。
Wrap的定义:
Wrap({
. ...
this.direction = Axis.horizontal,
this.alignment = WrapAlignment.start,
this.spacing = 0.0,
this.runAlignment = WrapAlignment.start,
this.runSpacing = 0.0,
this.crossAxisAlignment = WrapCrossAlignment.start,
this.textDirection,
this.verticalDirection = VerticalDirection.down,
List children = const [],
})
Wrap的很多属性在Row(包括Flex和Column)中也有,如direction、 crossAxisAlignment、textDirection、verticalDirection等,这些参数意义是相同的.
参数:
spacing:主轴方向子widget的间距
runSpacing:纵轴方向的间距
runAlignment:纵轴方向的对齐方式
可以认为Wrap和Flex(包括Row和 Column)除了超出显示范围后Wrap会折行外,其它行为基本相同。
示例代码如下【wrap实现流式布局效果】:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 表单Form',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// home: FormRouteDemo(),
// home: FlexLayoutRouteDemo(),
home: WrapDemo(),
);
}
}
import 'package:flutter/material.dart';
class WrapDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Wrap(
/**
* 这里区分一下主轴和纵轴的概念:
* 当水平方向的时候,其主轴就是水平,纵轴就是垂直。
* 当垂直方向的时候,其主轴就是垂直,纵轴就是水平。
*/
direction: Axis.horizontal,//不设置默认为horizontal
alignment: WrapAlignment.center,//沿主轴方向居中
spacing: 0.0,//主轴(水平)方向间距
runSpacing: 4.0,//纵轴(垂直)方向间距
children: <Widget>[
new Chip(
avatar: new CircleAvatar(backgroundColor: Colors.blue,child: Text('F'),),
label: new Text('flutter'),
),
new Chip(
avatar: new CircleAvatar(backgroundColor: Colors.orange,child: Text('F'),),
label: new Text('android'),
),
new Chip(
avatar: new CircleAvatar(backgroundColor: Colors.grey,child: Text('F'),),
label: new Text('微信小程序'),
),
new Chip(
avatar: new CircleAvatar(backgroundColor: Colors.red,child: Text('F'),),
label: new Text('python'),
),
new Chip(
avatar: new CircleAvatar(backgroundColor: Colors.greenAccent,child: Text('F'),),
label: new Text('jni与Ndk'),
),
new Chip(
avatar: new CircleAvatar(backgroundColor: Colors.yellow,child: Text('F'),),
label: new Text('springboot'),
),
new Chip(
avatar: new CircleAvatar(backgroundColor: Colors.purple,child: Text('F'),),
label: new Text('kotlin'),
),
],
),
);
}
}
注意:上述代码中的某些参数不设置也是可以的,因为dart语法中的构造函数是具备默认参数的。这点和kotlin一样完美。
3.2 Flow:
其过于复杂,需要自己实现子widget的位置转换,在很多场景下首先 要考虑的是Wrap是否满足需求。Flow主要用于一些需要自定义布局策略或性能要求较高(如动画中)的 场景。
其优点:
性能好;Flow是一个对child尺寸以及位置调整非常高效的控件,Flow用转换矩阵 (transformation matrices)在对child进行位置调整的时候进行了优化:在Flow定位过 后,如果child的尺寸或者位置发生了变化,在FlowDelegate中的 paintChildren() 方法中 调用 context.paintChild 进行重绘,而 context.paintChild 在重绘时使用了转换矩阵 (transformation matrices),并没有实际调整Widget位置。
灵活;由于我们需要自己实现FlowDelegate的 paintChildren() 方法,所以我们需要自己计 算每一个widget的位置,因此,可以自定义布局策略。
缺点:
使用复杂. 不能自适应子widget大小,必须通过指定父容器大小或实现TestFlowDelegate 的 getSize 返回固定大小。
示例【对六个色块进行自定义流式布局】:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 表单Form',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// home: FormRouteDemo(),
// home: FlexLayoutRouteDemo(),
// home: WrapDemo(),
home: FlowDemo(),
);
}
}
import 'package:flutter/material.dart';
class FlowDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
//Flow布局:需要自己实现FlowDelegate的paintChildren()方法。【自定义布局策略】
body: Flow(
delegate: FlowDelegateDemo(margin: EdgeInsets.all(10.0)),
children: <Widget>[
new Container(width: 80.0,height: 80.0,color: Colors.grey,),
new Container(width: 80.0,height: 80.0,color: Colors.blue,),
new Container(width: 80.0,height: 80.0,color: Colors.greenAccent,),
new Container(width: 80.0,height: 80.0,color: Colors.yellow,),
new Container(width: 80.0,height: 80.0,color: Colors.red,),
new Container(width: 80.0,height: 80.0,color: Colors.purple,),
],
),
);
}
}
//自定义类继承FlowDelegate
class FlowDelegateDemo extends FlowDelegate{
EdgeInsets margin = EdgeInsets.zero; //zero是0的意思,在此处是初始化赋值。具体的值由构造函数中传递过来的margin决定
/**
* 这是一个构造方法,其参数是由new出本对象外部传递过来。
* 注意的是,dart语法中,构造函数中参数是可以被{}包围的,包围后就是可选参数,可传可不传。
* 当然你也完全可以使用默认参数赋值一个默认值,达到外部不传递参数的效果。
*/
FlowDelegateDemo({this.margin});
@override
void paintChildren(FlowPaintingContext context) {
var x = margin.left;
var y = margin.top;
//计算每一个子widget的位置
for(int i=0;i<context.childCount;i++){
var w = context.getChildSize(i).width+x+margin.right;
if(w < context.size.width){
context.paintChild(i,transform: new Matrix4.translationValues(x, y, 0.0));
x=w+margin.left;
}else{
x=margin.left;
y+=context.getChildSize(i).height+margin.top+margin.bottom;
//绘制子widget(有优化)
context.paintChild(i,transform: new Matrix4.translationValues(x, y, 0.0));
x+=context.getChildSize(i).width+margin.left+margin.right;
}
}
}
@override
Size getSize(BoxConstraints constraints) {
// return super.getSize(constraints);
//指定flow的大小
return Size(double.infinity, 200.0);
}
@override
bool shouldRepaint(FlowDelegate oldDelegate) {
return oldDelegate != this;
}
}
总结:
dart似乎不被看好,且flutter的嵌套恶心。但是其性能和思维的简洁对开发者写起来还是有nice的。
flutter解决地狱回调用类似前端promise语法【then(xx,onError()).catchError()】或者 async await写出具有异步的同步代码.
Koltin的协程和rxjava也是解决地狱回调。