流式布局
- 我们在学习 Row 时默认只有一行,如果超出屏幕不会换行。我们把超出屏幕显示范围会自动折行的布局称为流式布局。Flutter中通过Wrap和Flow来支持流式布局,将上例中的Row换成Wrap后溢出部分则会自动换行
Wrap
Wrap({
Key key,
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<Widget> children = const <Widget>[],
})
- direction:主轴(mainAxis)的方向,默认为水平
- alignment:主轴方向上的对齐方式,默认为start
- spacing:主轴方向上的间距
- runAlignment:纵轴方向的对齐方式,默认为start
- runSpacing:纵轴方向的间距
- crossAxisAlignment:交叉轴(crossAxis)方向上的对齐方式
- textDirection:文本方向
- verticalDirection:定义了children摆放顺序,默认是down
在需要让子控件自动换行的布局场景 Wrap基本可以满足,特别说一点的是Wrap能做到的Flow一定可以实现,只不过会复杂很多。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Flutter Demo",
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text("Row Sample"),
),
body: Container(
alignment: Alignment.topCenter,
height: 300,
child: Wrap(
spacing: 8.0,
runSpacing: 4.0,
alignment: WrapAlignment.center,
children: <Widget>[
new Chip(
avatar: new CircleAvatar(
backgroundColor: Colors.blue, child: Text('A')),
label: new Text('Hamilton'),
),
new Chip(
avatar: new CircleAvatar(
backgroundColor: Colors.blue, child: Text('M')),
label: new Text('Lafayette'),
),
new Chip(
avatar: new CircleAvatar(
backgroundColor: Colors.blue, child: Text('H')),
label: new Text('Mulligan'),
),
new Chip(
avatar: new CircleAvatar(
backgroundColor: Colors.blue, child: Text('J')),
label: new Text('Laurens'),
),
],
))));
}
}
Flow
- Flow用起来远比Wrap麻烦,它的子控件所有的放置位置都需要你自己计算,但是它可以实现更加个性化的需求,我们可以通过delegate属性自己设置子控件排列规则和移动轨迹动画效果。
Flow({
Key key,
@required this.delegate,
List<Widget> children = const <Widget>[],
})
import 'package:flutter/material.dart';
import 'dart:math' as math;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Flow Example'),
),
body: FlowMenu(),
),
);
}
}
class FlowMenu extends StatefulWidget {
@override
_FlowMenuState createState() => _FlowMenuState();
}
class _FlowMenuState extends State<FlowMenu>
with SingleTickerProviderStateMixin {
AnimationController menuAnimation;
IconData lastTapped = Icons.notifications;
final List<IconData> menuItems = <IconData>[
Icons.home,
Icons.new_releases,
Icons.notifications,
Icons.settings,
Icons.launch,
Icons.access_alarm,
Icons.account_circle,
Icons.cloud_download,
Icons.menu,
];
void _updateMenu(IconData icon) {
if (icon != Icons.menu) setState(() => lastTapped = icon);
}
@override
void initState() {
super.initState();
menuAnimation = AnimationController(
duration: const Duration(milliseconds: 250),
vsync: this,
);
}
Widget flowMenuItem(IconData icon) {
return RawMaterialButton(
fillColor: lastTapped == icon ? Colors.amber[700] : Colors.blue,
splashColor: Colors.amber[100],
shape: CircleBorder(),
onPressed: () {
_updateMenu(icon);
menuAnimation.status == AnimationStatus.completed
? menuAnimation.reverse()
: menuAnimation.forward();
},
child: Icon(
icon,
color: Colors.white,
size: 45.0,
),
);
}
@override
Widget build(BuildContext context) {
return Container(
child: Flow(
delegate: FlowMenuDelegate(menuAnimation: menuAnimation),
children: menuItems
.map<Widget>((IconData icon) => flowMenuItem(icon))
.toList(),
),
);
}
}
class FlowMenuDelegate extends FlowDelegate {
FlowMenuDelegate({this.menuAnimation}) : super(repaint: menuAnimation);
final Animation<double> menuAnimation;
@override
bool shouldRepaint(FlowMenuDelegate oldDelegate) {
return menuAnimation != oldDelegate.menuAnimation;
}
@override
void paintChildren(FlowPaintingContext context) {
double dx = 0.0;
double dy = 0.0;
double offestX = 0.0;
double offestY = 0.0;
double angle = 360 / (context.childCount - 1);
for (int i = 0; i < context.childCount; ++i) {
double radius = context.getChildSize(i).width / 2;
dx = context.size.width / 2 - radius;
dy = context.size.height / 2 - radius;
offestX = 2 * radius * math.cos(angle * i * math.pi / 180) * menuAnimation.value;
offestY = 2 * radius * math.sin(angle * i * math.pi / 180) * menuAnimation.value;
if (i == context.childCount - 1) {
context.paintChild(i, transform: Matrix4.translationValues(dx, dy, 0,),
);
} else {
context.paintChild(i, transform: Matrix4.translationValues(dx + offestX, dy + offestY, 0,),
);
}
}
}
}