Flutter 布局基础教程
欢迎来到 Flutter 布局 codelab!你将在这里学到如何构建 Flutter UI,更棒的是这一切都不需要安装 Flutter 或者 Dart!
Flutter 与其他框架有着明显的差异,原因在于它使用代码来构建 UI,而不是 XML 或其他东西。其中,widget 是构建 Flutter UI 的基本单元。当你逐渐深入这个 codelab,你将会发现在 Flutter 中几乎所有的东西都是 widget。 widget 是一个不会改变的对象,它是 UI 中一个特定部分的描述。你还会学到 Flutter 的 widget 非常容易组合,这意味着你能够通过组合已有的 widgets 来创造更多复杂的 widgets。到这篇文章的最后,你会运用这里所学的知识构建一个显示名片的 Flutter UI。
本 codelab 的预期完成时间约为 45 - 60 分钟
Row 和 Column 类
Row
和 Column
是两个用来容纳和布局 widgets 的类。在它们内部的 widgets 我们称为 children,
Row
和 Column
就作为它们的父级。
Row
将会让 widgets 水平排列,而 Column 则会让其竖直排列。
样例:创建一个 Column
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
BlueBox(),
BlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
runApp(MyApp());
final controller = LiveWidgetController(WidgetsBinding.instance);
final columns = controller.widgetList(find.byType(Column));
if (columns.isEmpty) {
_result(false, ['The Row contains three BlueBox widgets and lays them out horizontally.']);
return;
}
if (columns.length > 1) {
_result(false, ['Found ${columns.length} Rows, rather than just one.']);
return;
}
final column = columns.first as Column;
if (column.children.length != 3 || column.children.any((w) => w is! BlueBox)) {
_result(false, ['Row/Column should contain three children, all BlueBox widgets.']);
return;
}
_result(true, ['The Column contains three BlueBox widgets and lays them out vertically.']);
}
{$ end test.dart $}
轴大小和对齐方式
至此,BlueBox
widget 已经在一起被压扁了 (在界面的左边或者上面 )。你可以通过轴大小和对齐属性来改变 BlueBox
widget 的间距。
mainAxisSize 属性
Row
和 Column
分别占据了不同的主轴。Row
的主轴是水平的。
mainAxisSize
决定了 Row
和 Column
能够在主轴上占据多大空间。
mainAxisSize
有两个可选属性:
MainAxisSize.max
Row
和 Column
占据它们主轴上所有空间。如果子 widget 的总宽度小于主轴上的空间,它们就会充满剩余的空间。
MainAxisSize.min
Row
和 Column
仅占据它的 children 在主轴上所需的空间,它的 children 在主轴之间将没有额外空间。
样例:自定义轴大小
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
children: [
BlueBox(),
BlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
final rows = controller.widgetList(find.byType(Row));
if (rows.isEmpty) {
_result(false, ['Couldn\'t find Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.min) {
_result(false, ['Row lays out the BlueBox widgets with extra space. Change MainAxisSize.max to MainAxisSize.min']);
return;
}
if (row.children.length != 3 || row.children.any((w) => w is! BlueBox)) {
_result(false, ['There should only be three children, all BlueBox widgets.']);
return;
}
_result(true, ['Row lays out the BlueBox widgets without extra space, and the BlueBox widgets are positioned at the middle of Row\'s main axis.']);
}
{$ end test.dart $}
mainAxisAlignment 属性
当 mainAxisSize
被设为 MainAxisSize.max
,
Row
和 Column
将会使用额外空间来对齐它的 children。
mainAxisAlignment
属性决定了 Row
和 Column
将会在额外空间中如何对齐它的 children。
mainAxisAlignment
有以下六个可选属性:
MainAxisAlignment.start
将其 children 从主轴起点处开始对齐。 (Row
的起点在左边,Column
的起点在顶部 )
MainAxisAlignment.end
将其 children 从主轴终点处开始对齐。 (Row
的终点在右边,Column
的终点在底部 )
MainAxisAlignment.center
将其 children 置于主轴中心。
MainAxisAlignment.spaceBetween
在 children 之间平均分配额外空间。
MainAxisAlignment.spaceEvenly
在 children 之间,以及第一个 children 之前和最后一个 children 之后,平均分配额外空间。
MainAxisAlignment.spaceAround
与 MainAxisAlignment.spaceEvenly
相似,但在第一个 child 之前以及最后一个孩子之后减少了一半的空间,让其 children 之间宽度缩减一半。
样例:自定义主轴对齐方式
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
BlueBox(),
BlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
final rows = controller.widgetList(find.byType(Row));
if (rows.isEmpty) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.children.length != 3 || row.children.any((w) => w is! BlueBox)) {
_result(false, ['The Row should have three children, all BlueBox widgets.']);
return;
}
if (row.mainAxisAlignment == MainAxisAlignment.start) {
_result(false, ['MainAxisAlignment.start positions the BlueBox widgets on the left of the main axis. Change the value to MainAxisAlignment.end.']);
} else if (row.mainAxisAlignment == MainAxisAlignment.end) {
_result(true, ['MainAxisAlignment.end positions the BlueBox widgets on the right of the main axis.']);
} else if (row.mainAxisAlignment == MainAxisAlignment.center) {
_result(true, ['MainAxisAlignment.center positions the BlueBox widgets at the middle of the main axis.']);
} else if (row.mainAxisAlignment == MainAxisAlignment.spaceBetween) {
_result(true, ['The extra space is divided between the BlueBox widgets.']);
} else if (row.mainAxisAlignment == MainAxisAlignment.spaceEvenly) {
_result(true, ['The extra space is divided evenly between the BlueBox widgets and before and after them.']);
} else if (row.mainAxisAlignment == MainAxisAlignment.spaceAround) {
_result(true, ['Similar to MainAxisAlignment.spaceEvenly, but reduces half of the space before the first BlueBox widget and after the last BlueBox widget to half of the width between the BlueBox widgets.']);
}
}
{$ end test.dart $}
crossAxisAlignment 属性
crossAxisAlignment
属性决定了 Row
和 Column
能够如何在其横轴上定位 children。
Row
的横轴是竖直的,而 Column
则是水平的,
crossAxisAlignment
属性有五个可选属性:
CrossAxisAlignment.start
将 children 放置在交叉轴的靠前位置(即 Row
布局的顶部,Column
布局的左侧)。
CrossAxisAlignment.end
将 children 放置在交叉轴的靠后位置(即 Row
布局的底部,Column
布局的右侧)。
CrossAxisAlignment.center
将 children 放置在交叉轴的中心位置(即 Row
布局和 Column
布局的中间)。
CrossAxisAlignment.stretch
使 children 在交叉轴上进行拉伸填充(即 Row
布局是纵向的拉伸,Column
布局是横向的拉伸)。
CrossAxisAlignment.baseline
根据 children 的基线对子节点。
(仅限Text
类,并要求 textBaseline
属性设置为
TextBaseline.alphabetic
。在 Text widget 小节中查看样例。
样例:自定义横轴对齐方式
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
BlueBox(),
BiggerBlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
class BiggerBlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 100,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
final rows = controller.widgetList(find.byType(Row));
if (rows.isEmpty) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.children.length != 3 || row.children.any((w) => w is! BlueBox && w is! BiggerBlueBox)) {
_result(false, ['The Row should have three children, all BlueBox or BiggerBlueBox widgets.']);
return;
}
if (row.crossAxisAlignment == CrossAxisAlignment.start) {
_result(true, ['The BlueBox and BiggerBlueBox widgets are positioned at the top of the cross axis.']);
} else if (row.crossAxisAlignment == CrossAxisAlignment.end) {
_result(true, ['The BlueBox and BiggerBlueBox widgets are positioned at the bottom of the cross axis']);
} else if (row.crossAxisAlignment == CrossAxisAlignment.center) {
_result(false, ['The widgets are positioned at the middle of the cross axis. Change CrossAxisAlignment.center to CrossAxisAlignment.start.']);
} else if (row.crossAxisAlignment == CrossAxisAlignment.stretch) {
_result(true, ['The BlueBox and BiggerBlueBox widgets are stretched across the cross axis. Change the Row to a Column, and run again.']);
} else if(row.crossAxisAlignment == CrossAxisAlignment.baseline) {
_result(false, ['Couldn\t find a text class.']);
}
}
{$ end test.dart $}
Flexible widget
正如你所看到,mainAxisAlignment
和 crossAxisAlignment
属性决定了 Row
和 Column
在各个轴上如何布局 widget。
Row
和 Column
首先布置固定大小的 widget。固定大小的小部件被认为是 不灵活的 因为它们布局后无法自我调整大小。
Flexible
widget 包裹一个 widget 让这个 widget 变得可以调整大小。当 Flexible
widget 包裹 widget 时,这个 widget 就成为 Flexible
widget 的子节点,并被视为 flexible 的。在布置固定大小的 widget 后,
Flex 的 widget 根据其 flex
和 fit
属性调整大小:
flex
将自身的 flex
因子与其他的比较,以决定自身占剩余空间的比例。
fit
决定 Flexible
的 widget 是否能够填充所有剩余空间。
样例:改变 fit 属性
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
BlueBox(),
Flexible(
fit: FlexFit.loose,
flex: 1,
child: BlueBox(),
),
Flexible(
fit: FlexFit.loose,
flex: 1,
child: BlueBox(),
),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
final rows = controller.widgetList(find.byType(Row));
if (rows.isEmpty) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.children.length != 3) {
_result(false, ['The Row should have three children, all BlueBox or Flexible widgets.']);
return;
}
if (row.children[0] is! BlueBox) {
_result(false, ['Row\'s first child should be a BlueBox.']);
return;
}
if (row.children[1] is! Flexible) {
_result(false, ['Row\'s second child should be a Flexible class.']);
return;
}
if (row.children[2] is! Flexible) {
_result(false, ['Row\'s third child should be a Flexible class.']);
return;
}
final flexibleWidget = row.children[2] as Flexible;
if (flexibleWidget.child is! BlueBox) {
_result(false, ['The Flexible classes should have BlueBox widgets as their children.']);
return;
}
if (flexibleWidget.fit != FlexFit.tight) {
_result(false, ['The fit properties set the Flexible widgets to their preferred size. Change both fit values to FlexFit.tight.']);
return;
}
_result(true, ['The Flexible widgets now occupy the space determined by their flex values.']);
}
{$ end test.dart $}
样例:测试 flex 值
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
BlueBox(),
Flexible(
fit: FlexFit.tight,
flex: 1,
child: BlueBox(),
),
Flexible(
fit: FlexFit.tight,
flex: 1,
child: BlueBox(),
),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
final rows = controller.widgetList(find.byType(Row));
if (rows.isEmpty) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.children.length != 3) {
_result(false, ['The Row should have three children, all BlueBlox or Flexible widgets.']);
return;
}
if (row.children[0] is! BlueBox) {
_result(false, ['The Row\'s first child should be a BlueBox widget.']);
return;
}
if (row.children[1] is! Flexible) {
_result(false, ['The Row\'s second child should be a Flexible widget.']);
return;
}
if (row.children[2] is! Flexible) {
_result(false, ['The Row\'s third child should be a Flexible widget.']);
return;
}
final flexibleWidget = row.children[1] as Flexible;
if (flexibleWidget.child is! BlueBox) {
_result(false, ['The Flexible should have a BlueBox widget as its child.']);
return;
}
if (flexibleWidget.flex != 1) {
_result(false, ['Notice how the flex properties divide the extra space between the two Flexible widgets.']);
return;
}
_result(true, ['Both Flexible widgets receive half of the total remaining space.']);
}
{$ end test.dart $}
Expanded widget
Expanded
widget 能够包裹一个 widget 并强制其填满剩余空间,与 Flexible
非常相似。
样例:填补额外空间
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
BlueBox(),
BlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
final rows = controller.widgetList(find.byType(Row));
if (rows.isEmpty) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.children.length != 3) {
_result(false, ['The Row should have three children, all BlueBox widgets.']);
return;
}
if (row.children[0] is! BlueBox) {
_result(false, ['The Row\'s first child should be a BlueBox widget.']);
return;
}
if (row.children[1] is! Expanded) {
_result(false, ['Notice how Row contains extra space on its main axis. Wrap the second BlueBox widget in an Expanded widget.']);
return;
}
if (row.children[2] is! BlueBox) {
_result(false, ['The Row\'s third child should be a Flexible widget.']);
return;
}
_result(true, ['Expanded forces second BlueBox widget to fill the extra space.']);
}
{$ end test.dart $}
SizedBox widget
SizedBox
widget 的两种用途之一就是创建精确的尺寸。当 SizedBox
包裹了一个 widget 时,它会使用 height
和 width
调整其大小。如果它没有包裹 widget,它可以使用height
和width
属性创造空的空间。
样例:调整一个 widget
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
children: [
BlueBox(),
SizedBox(
width: 100,
child: BlueBox(),
),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
final rows = controller.widgetList(find.byType(Row));
if (rows.isEmpty) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.children.length != 3) {
_result(false, ['The Row should end up with three children.']);
return;
}
if (row.children[0] is! BlueBox) {
_result(false, ['The Row\'s first child should be a BlueBox widget.']);
return;
}
if (row.children[1] is! SizedBox) {
_result(false, ['The Row\'s second child should be a SizedBox widget.']);
return;
}
if (row.children[2] is! BlueBox) {
_result(false, ['The Row\'s third child should be a BlueBox widget.']);
return;
}
final sizedBox = row.children[1] as SizedBox;
if (sizedBox.width != 100) {
_result(false, ['The SizedBox should have a width of 100.']);
return;
}
if (sizedBox.height != 100) {
_result(false, ['The SizedBox widget resizes the BlueBox widget to 100 logical pixels wide. Add a height property inside SizedBox equal to 100 logical pixels.']);
return;
}
_result(true, ['The SizedBox widget resizes the BlueBox widget to 100 logical pixels wide and tall.']);
}
{$ end test.dart $}
样例:创建空间
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
BlueBox(),
const SizedBox(width: 50),
BlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
final rows = controller.widgetList(find.byType(Row));
if (rows.isEmpty) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.mainAxisAlignment == MainAxisAlignment.spaceAround
|| row.mainAxisAlignment == MainAxisAlignment.spaceBetween
|| row.mainAxisAlignment == MainAxisAlignment.spaceEvenly) {
_result(false, ['It\'s best to use MainAxisAlignment.start, MainAxisAlignment.end, or MainAxisAlignment.center to see how the SizedBox widgets work in a Row.']);
return;
}
if (row.children.length != 5) {
_result(false, ['The SizedBox widget creates space at 50 logical pixels wide. Add another SizedBox class between the second and third BlueBox widgets with a width property equal to 25 logical pixels.']);
return;
}
if (row.children[0] is! BlueBox) {
_result(false, ['The Row\'s first child should be a BlueBox widget.']);
return;
}
if (row.children[1] is! SizedBox) {
_result(false, ['The Row\'s second child should be a SizedBox widget.']);
return;
}
if (row.children[2] is! BlueBox) {
_result(false, ['The Row\'s third child should be a BlueBox widget.']);
return;
}
if (row.children[3] is! SizedBox) {
_result(false, ['The Row\'s fourth child should be a SizedBox widget.']);
return;
}
if (row.children[4] is! BlueBox) {
_result(false, ['The Row\'s fifth child should be a BlueBox widget.']);
return;
}
final sizedBox = row.children[1] as SizedBox;
if (sizedBox.width != 50) {
_result(false, ['The SizedBox should have a width of 50.']);
return;
}
final sizedBox2 = row.children[3] as SizedBox;
if (sizedBox2.width != 25) {
_result(false, ['SizedBox should have a width of 25.']);
return;
}
_result(true, ['The SizedBox widgets create space between the BlueBox widgets, one space at 50 logical pixels and one at 25 logical pixels.']);
}
{$ end test.dart $}
Spacer widget
与 SizedBox
相似,Spacer
widget 也能在 widgets 之间创建空间。
Example:创建更多空间
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
BlueBox(),
const Spacer(flex: 1),
BlueBox(),
BlueBox(),
],
);
}
}
class BlueBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(),
),
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
final rows = controller.widgetList(find.byType(Row));
if (rows.isEmpty) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.mainAxisAlignment == MainAxisAlignment.spaceAround
|| row.mainAxisAlignment == MainAxisAlignment.spaceBetween
|| row.mainAxisAlignment == MainAxisAlignment.spaceEvenly) {
_result(false, ['It\'s best to use MainAxisAlignment.start, MainAxisAlignment.end, or MainAxisAlignment.center to see how the SizedBox widgets work in a Row.']);
return;
}
if (row.children.length != 5) {
_result(false, ['What do you think would happen if you added another Spacer widget with a flex value of 1 between the second and third BlueBox widgets?']);
return;
}
if (row.children[0] is! BlueBox ||
row.children[1] is! Spacer ||
row.children[2] is! BlueBox ||
row.children[3] is! Spacer ||
row.children[4] is! BlueBox) {
_result(false, ['Not quite. Row should contain five children in this order: BlueBox, Spacer, BlueBox, Spacer, BlueBox.']);
return;
}
final spacer = row.children[3] as Spacer;
if (spacer.flex != 1) {
_result(false, ['The Spacer class should have a flex equal to 1.']);
return;
}
_result(true, ['Both Spacer widgets create equal amounts of space between all three BlueBox widgets.']);
}
{$ end test.dart $}
Text widget
Text
widget 不仅能够显示文字,并能够配置不同的字体,大小和颜色。
样例:文字对齐
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
textBaseline: TextBaseline.alphabetic,
children: const [
Text(
'Hey!',
style: TextStyle(
fontSize: 30,
fontFamily: 'Futura',
color: Colors.blue,
),
),
Text(
'Hey!',
style: TextStyle(
fontSize: 50,
fontFamily: 'Futura',
color: Colors.green,
),
),
Text(
'Hey!',
style: TextStyle(
fontSize: 40,
fontFamily: 'Futura',
color: Colors.red,
),
),
],
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
final rows = controller.widgetList(find.byType(Row));
if (rows.isEmpty) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.children.length != 3 || row.children.any((w) => w is! Text)) {
_result(false, ['The Row should have three children, all Text widgets.']);
return;
}
if (row.textBaseline == null) {
_result(false, ['To use CrossAxisAlignment.baseline, you need to set the Row\'s textBaseline property.']);
return;
}
if (row.crossAxisAlignment != CrossAxisAlignment.baseline) {
_result(false, ['The Text widgets are positioned at the middle of the cross axis. Change CrossAxisAlignment.center to CrossAxisAlignment.baseline.']);
return;
}
_result(true, ['The Text widgets are now aligned by their character baselines.']);
}
{$ end test.dart $}
Icon widget
Icon
widget 能够显示图形符号,这代表了 UI 的一个方面。
Flutter 将会为 Material 和 Cupertino
的应用提前加载 icon packages。
样例:创建一个 Icon
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
textBaseline: TextBaseline.alphabetic,
children: const [
Icon(
Icons.widgets,
size: 50,
color: Colors.blue,
),
Icon(
Icons.widgets,
size: 50,
color: Colors.red,
),
],
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
final rows = controller.widgetList(find.byType(Row));
if (rows.isEmpty) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
if (row.children.length != 3 || row.children.any((w) => w is! Icon)) {
_result(false, ['Row should have three children, all Icon widgets.']);
return;
}
final icon = row.children[2] as Icon;
if (icon.color != Colors.amber) {
_result(false, ['Add a third Icon. Give the Icon a size of 50 and a color of Colors.amber.']);
return;
}
_result(true, ['The code displays three Icons in blue, red, and amber.']);
}
{$ end test.dart $}
Image widget
Image
widget 显示了一张图片。你还能够直接引用图片 URL,或是你的应用 package 中的图片。但是由于 DartPad 无法引用包图片,所以下面的样例将会使用网络上的图片。
样例:显示一张图片
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.network('[Place an image link here!]'),
],
);
}
}
{$ end main.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xffeeeeee),
child: Center(
child: Container(
color: const Color(0xffcccccc),
child: MyWidget(),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
final rows = controller.widgetList(find.byType(Row));
if (rows.isEmpty) {
_result(false, ['Couldn\'t find a Row!']);
return;
}
if (rows.length > 1) {
_result(false, ['Found ${rows.length} Rows, rather than just one.']);
return;
}
final row = rows.first as Row;
if (row.mainAxisSize != MainAxisSize.max) {
_result(false, ['It\'s best to leave the mainAxisSize set to MainAxisSize.max, so there\'s space for the alignments to take effect.']);
return;
}
}
{$ end test.dart $}
综合练习
你就要完成这个 codelab 了!如果你想要检验你刚学的知识,为何不讲这些结合起来,构建一个显示名片的 Flutter UI 呢!
你将会把 Flutter 的布局分解成几个部分,这就是如何在实际开发中构建 Flutter UI 方式!
在第一部分, 你将会实现包含姓名和标题的 Column
。然后你将会在 Column
包裹一个含有 icon 的 Row
,它将会被放在姓名和标题的左边。
在第二部分中,你将会在 Row
外包裹一个 Column
,所以你的代码中就包含了一个 Column (Row (Column) )。然后你将调整最外面的Column
的布局,所以它看起来不错。最后,您将添加联系信息到最外面的Column
的 children 中,所以它将显示在名称,标题和图标下方。
在第三部分,你将会完成添加了更多图标的名片,它们会被放在联系信息的下方。
第一部分
练习:创建 name 和 title
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
TODO('Begin implementing the Column here.');
}
}
{$ end main.dart $}
{$ begin solution.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Flutter McFlutter',
style: Theme.of(context).textTheme.headlineSmall,
),
const Text('Experienced App Developer'),
],
);
}
}
{$ end solution.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
scaffoldBackgroundColor: const Color(0xffeeeeee),
textTheme: const TextTheme(
bodyMedium: TextStyle(
fontSize: 16,
),
),
),
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Container(
decoration: BoxDecoration(
color: const Color(0xffffffff),
border: Border.all(),
boxShadow: const [
BoxShadow(
blurRadius: 10,
color: Color(0x80000000),
),
],
),
padding: const EdgeInsets.all(8.0),
child: MyWidget(),
),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
// Check MyWidget starts with one Column
final myWidgetElement = controller.element(find.byType(MyWidget));
final myWidgetChildElements = <Element>[];
myWidgetElement.visitChildElements((e) => myWidgetChildElements.add(e));
if (myWidgetChildElements.length != 1 ||
myWidgetChildElements[0].widget is! Column) {
_result(false, ['The root widget in MyWidget\'s build method should be a Column.']);
return;
}
// Check Column has correct properties
final innerColumnElement = myWidgetChildElements[0];
final innerColumnWidget = innerColumnElement.widget as Column;
if (innerColumnWidget.crossAxisAlignment != CrossAxisAlignment.start) {
_result(false, ['The Column that contains the name and title should use CrossAxisAlignment.start as its CrossAxisAlignment value.']);
return;
}
if (innerColumnWidget.mainAxisSize != MainAxisSize.min) {
_result(false, ['The Column that contains the name and title should use MainAxisSize.min as its MainAxisSize value.']);
return;
}
// Check inner Column has two Text children
if (innerColumnWidget.children.any((w) => w is! Text)) {
_result(false, ['The Column that contains the name and title should have two children, both Text widgets.']);
return;
}
// Check first Text has headline style
final nameText = innerColumnWidget.children[0] as Text;
if (nameText.style?.fontSize != 24) {
_result(false, ['The Text widget for the name should use the "headlineSmall" textStyle.']);
return;
}
_result(true);
}
{$ end test.dart $}
练习:在 Column 外包裹一个 Row
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Flutter McFlutter',
style: Theme.of(context).textTheme.headlineSmall,
),
const Text('Experienced App Developer'),
],
);
}
}
{$ end main.dart $}
{$ begin solution.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.account_circle, size: 50),
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Flutter McFlutter',
style: Theme.of(context).textTheme.headlineSmall,
),
const Text('Experienced App Developer'),
],
),
],
);
}
}
{$ end solution.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
scaffoldBackgroundColor: const Color(0xffeeeeee),
textTheme: const TextTheme(
bodyMedium: TextStyle(
fontSize: 16,
),
),
),
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Container(
decoration: BoxDecoration(
color: const Color(0xffffffff),
border: Border.all(),
boxShadow: const [
BoxShadow(
blurRadius: 10,
color: Color(0x80000000),
),
],
),
padding: const EdgeInsets.all(8.0),
child: MyWidget(),
),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
// Check MyWidget starts with one Column
final myWidgetElement = controller.element(find.byType(MyWidget));
final myWidgetChildElements = <Element>[];
myWidgetElement.visitChildElements((e) => myWidgetChildElements.add(e));
if (myWidgetChildElements.length != 1 ||
myWidgetChildElements[0].widget is! Row) {
_result(false, ['The root widget in MyWidget\'s build method should be a Column.']);
return;
}
// Check first Row has two children: Padding and Column
final firstRowElement = myWidgetChildElements[0];
final firstRowChildElements = <Element>[];
firstRowElement.visitChildElements((e) => firstRowChildElements.add(e));
if (firstRowChildElements.length != 2 ||
firstRowChildElements[0].widget is! Padding ||
firstRowChildElements[1].widget is! Column) {
_result(false, ['The first Row should have two children: first a Padding, and then a Column.']);
return;
}
// Check Padding has correct padding
final paddingElement = firstRowChildElements[0];
if ((paddingElement.widget as Padding).padding != const EdgeInsets.all(8)) {
_result(false, ['The Padding widget in the first Row should have a padding of 8.']);
return;
}
// Check Padding has an Icon as its child
final paddingChildren = <Element>[];
paddingElement.visitChildElements((e) => paddingChildren.add(e));
if (paddingChildren.length != 1 || paddingChildren[0].widget is! Icon) {
_result(false, ['The Padding widget in the first Row should have an Icon as its child.']);
return;
}
// Check icon has a size of 50
if ((paddingChildren[0].widget as Icon).size != 50) {
_result(false, ['The Icon in the top-left corner should have a size of 50.']);
return;
}
// Check inner Column has correct properties
final innerColumnElement = firstRowChildElements[1];
final innerColumnWidget = innerColumnElement.widget as Column;
if (innerColumnWidget.crossAxisAlignment != CrossAxisAlignment.start) {
_result(false, ['The Column for the name and title should use CrossAxisAlignment.start as its crosAxisAlignment.']);
return;
}
if (innerColumnWidget.mainAxisSize != MainAxisSize.min) {
_result(false, ['The Column for the name and title should use MainAxisSize.min as its mainAxisSize.']);
return;
}
// Check inner Column has two Text children
if (innerColumnWidget.children.any((w) => w is! Text)) {
_result(false, ['The Column for the name and title should have two children, both Text widgets.']);
return;
}
// Check first Text has headline style
final nameText = innerColumnWidget.children[0] as Text;
if (nameText.style?.fontSize != 24) {
_result(false, ['The Text widget for the name should use the "headlineSmall" textStyle.']);
return;
}
_result(true);
}
{$ end test.dart $}
第二部分
练习:调整布局
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.account_circle, size: 50),
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Flutter McFlutter',
style: Theme.of(context).textTheme.headlineSmall,
),
const Text('Experienced App Developer'),
],
),
],
);
}
}
{$ end main.dart $}
{$ begin solution.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.account_circle, size: 50),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Flutter McFlutter',
style: Theme.of(context).textTheme.headlineSmall,
),
const Text('Experienced App Developer'),
],
),
],
),
const SizedBox(height: 8),
Row(),
const SizedBox(height: 16),
Row(),
],
);
}
}
{$ end solution.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
scaffoldBackgroundColor: const Color(0xffeeeeee),
textTheme: const TextTheme(
bodyMedium: TextStyle(
fontSize: 16,
),
),
),
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Container(
decoration: BoxDecoration(
color: const Color(0xffffffff),
border: Border.all(),
boxShadow: const [
BoxShadow(
blurRadius: 10,
color: Color(0x80000000),
),
],
),
padding: const EdgeInsets.all(8.0),
child: MyWidget(),
),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
// Check MyWidget starts with one Column
final myWidgetElement = controller.element(find.byType(MyWidget));
final myWidgetChildElements = <Element>[];
myWidgetElement.visitChildElements((e) => myWidgetChildElements.add(e));
if (myWidgetChildElements.length != 1 ||
myWidgetChildElements[0].widget is! Column) {
_result(false, ['The root widget in MyWidget\'s build method should be a Column.']);
return;
}
// Check outermost Column has 5 correct children.
final outerColumnElement = myWidgetChildElements[0];
final outerColumnChildWidgets =
(outerColumnElement.widget as Column).children;
final outerColumnChildElements = <Element>[];
outerColumnElement.visitChildElements((e) => outerColumnChildElements.add(e));
if (outerColumnChildWidgets.length != 5 ||
outerColumnChildWidgets[0] is! Row ||
outerColumnChildWidgets[1] is! SizedBox ||
outerColumnChildWidgets[2] is! Row ||
outerColumnChildWidgets[3] is! SizedBox ||
outerColumnChildWidgets[4] is! Row) {
_result(false, ['The children of the outermost Column should be [Row, SizedBox, Row, SizedBox, Row] in that order.']);
return;
}
// Check outermost Column's properties
if ((outerColumnElement.widget as Column).mainAxisSize != MainAxisSize.min) {
_result(false, ['The outermost Column should use MainAxisSize.min for its mainAxisSize.']);
return;
}
if ((outerColumnElement.widget as Column).crossAxisAlignment !=
CrossAxisAlignment.stretch) {
_result(false, ['The outermost Column should use CrossAxisAlignment.stretch for its crossAxisAlignment.']);
return;
}
// Check first Row has two children: Padding and Column
final firstRowElement = outerColumnChildElements
.firstWhere((e) => e.widget == outerColumnChildWidgets[0]);
final firstRowChildElements = <Element>[];
firstRowElement.visitChildElements((e) => firstRowChildElements.add(e));
if (firstRowChildElements.length != 2 ||
firstRowChildElements[0].widget is! Padding ||
firstRowChildElements[1].widget is! Column) {
_result(false, ['The first Row should have two children: first a Padding, and then a Column.']);
return;
}
// Check Padding has correct padding
final paddingElement = firstRowChildElements[0];
if ((paddingElement.widget as Padding).padding != const EdgeInsets.all(8)) {
_result(false, ['The Padding widget in the first Row should have a padding of 8.']);
return;
}
// Check Padding has an Icon as its child
final paddingChildren = <Element>[];
paddingElement.visitChildElements((e) => paddingChildren.add(e));
if (paddingChildren.length != 1 || paddingChildren[0].widget is! Icon) {
_result(false, ['The Padding widget in the first Row should have an Icon as its child.']);
return;
}
// Check icon has a size of 50
if ((paddingChildren[0].widget as Icon).size != 50) {
_result(false, ['The Icon in the top-left corner should have a size of 50.']);
return;
}
// Check inner Column has correct properties
final innerColumnElement = firstRowChildElements[1];
final innerColumnWidget = innerColumnElement.widget as Column;
if (innerColumnWidget.crossAxisAlignment != CrossAxisAlignment.start) {
_result(false, ['The Column for the name and title should use CrossAxisAlignment.start as its crosAxisAlignment.']);
return;
}
if (innerColumnWidget.mainAxisSize != MainAxisSize.min) {
_result(false, ['The Column for the name and title should use MainAxisSize.min as its mainAxisSize.']);
return;
}
// Check inner Column has two Text children
if (innerColumnWidget.children.any((w) => w is! Text)) {
_result(false, ['The Column for the name and title should have two children, both Text widgets.']);
return;
}
// Check first Text has headline style
final nameText = innerColumnWidget.children[0] as Text;
if (nameText.style?.fontSize != 24) {
_result(false, ['The Text widget for the name should use the "headlineSmall" textStyle.']);
return;
}
// Check first SizedBox has correct properties
final firstSizedBoxElement = outerColumnChildElements
.firstWhere((e) => e.widget == outerColumnChildWidgets[1]);
if ((firstSizedBoxElement.widget as SizedBox).height != 8) {
_result(false, ['The SizedBox before the first empty Row should have a height of 8.']);
return;
}
// Check second SizedBox has correct properties
final secondSizedBoxElement = outerColumnChildElements
.firstWhere((e) => e.widget == outerColumnChildWidgets[3]);
if ((secondSizedBoxElement.widget as SizedBox).height != 16) {
_result(false, ['The SizedBox between the first and second empty Rows should have a height of 16.']);
return;
}
_result(true);
}
{$ end test.dart $}
练习:输入联系信息
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.account_circle, size: 50),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Flutter McFlutter',
style: Theme.of(context).textTheme.headlineSmall,
),
const Text('Experienced App Developer'),
],
),
],
),
const SizedBox(height: 8),
Row(
children: const [],
),
const SizedBox(height: 16),
Row(
children: const [],
),
],
);
}
}
{$ end main.dart $}
{$ begin solution.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.account_circle, size: 50),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Flutter McFlutter',
style: Theme.of(context).textTheme.headlineSmall,
),
const Text('Experienced App Developer'),
],
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text(
'123 Main Street',
),
Text(
'(415) 555-0198',
),
],
),
const SizedBox(height: 16),
Row(
children: const [],
),
],
);
}
}
{$ end solution.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
scaffoldBackgroundColor: const Color(0xffeeeeee),
textTheme: const TextTheme(
bodyMedium: TextStyle(
fontSize: 16,
),
),
),
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Container(
decoration: BoxDecoration(
color: const Color(0xffffffff),
border: Border.all(),
boxShadow: const [
BoxShadow(
blurRadius: 10,
color: Color(0x80000000),
),
],
),
padding: const EdgeInsets.all(8.0),
child: MyWidget(),
),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
// Check MyWidget starts with one Column
final myWidgetElement = controller.element(find.byType(MyWidget));
final myWidgetChildElements = <Element>[];
myWidgetElement.visitChildElements((e) => myWidgetChildElements.add(e));
if (myWidgetChildElements.length != 1 ||
myWidgetChildElements[0].widget is! Column) {
_result(false, ['The root widget in MyWidget\'s build method should be a Column.']);
return;
}
// Check outermost Column has 5 correct children.
final outerColumnElement = myWidgetChildElements[0];
final outerColumnChildWidgets =
(outerColumnElement.widget as Column).children;
final outerColumnChildElements = <Element>[];
outerColumnElement.visitChildElements((e) => outerColumnChildElements.add(e));
if (outerColumnChildWidgets.length != 5 ||
outerColumnChildWidgets[0] is! Row ||
outerColumnChildWidgets[1] is! SizedBox ||
outerColumnChildWidgets[2] is! Row ||
outerColumnChildWidgets[3] is! SizedBox ||
outerColumnChildWidgets[4] is! Row) {
_result(false, ['The children of the outermost Column should be [Row, SizedBox, Row, SizedBox, Row] in that order.']);
return;
}
// Check outermost Column's properties
if ((outerColumnElement.widget as Column).mainAxisSize != MainAxisSize.min) {
_result(false, ['The outermost Column should use MainAxisSize.min for its mainAxisSize.']);
return;
}
if ((outerColumnElement.widget as Column).crossAxisAlignment !=
CrossAxisAlignment.stretch) {
_result(false, ['The outermost Column should use CrossAxisAlignment.stretch for its crossAxisAlignment.']);
return;
}
// Check first Row has two children: Padding and Column
final firstRowElement = outerColumnChildElements
.firstWhere((e) => e.widget == outerColumnChildWidgets[0]);
final firstRowChildElements = <Element>[];
firstRowElement.visitChildElements((e) => firstRowChildElements.add(e));
if (firstRowChildElements.length != 2 ||
firstRowChildElements[0].widget is! Padding ||
firstRowChildElements[1].widget is! Column) {
_result(false, ['The first Row should have two children: first a Padding, and then a Column.']);
return;
}
// Check Padding has correct padding
final paddingElement = firstRowChildElements[0];
if ((paddingElement.widget as Padding).padding != const EdgeInsets.all(8)) {
_result(false, ['The Padding widget in the first Row should have a padding of 8.']);
return;
}
// Check Padding has an Icon as its child
final paddingChildren = <Element>[];
paddingElement.visitChildElements((e) => paddingChildren.add(e));
if (paddingChildren.length != 1 || paddingChildren[0].widget is! Icon) {
_result(false, ['The Padding widget in the first Row should have an Icon as its child.']);
return;
}
// Check icon has a size of 50
if ((paddingChildren[0].widget as Icon).size != 50) {
_result(false, ['The Icon in the top-left corner should have a size of 50.']);
return;
}
// Check inner Column has correct properties
final innerColumnElement = firstRowChildElements[1];
final innerColumnWidget = innerColumnElement.widget as Column;
if (innerColumnWidget.crossAxisAlignment != CrossAxisAlignment.start) {
_result(false, ['The Column for the name and title should use CrossAxisAlignment.start as its crosAxisAlignment.']);
return;
}
if (innerColumnWidget.mainAxisSize != MainAxisSize.min) {
_result(false, ['The Column for the name and title should use MainAxisSize.min as its mainAxisSize.']);
return;
}
// Check inner Column has two Text children
if (innerColumnWidget.children.any((w) => w is! Text)) {
_result(false, ['The Column for the name and title should have two children, both Text widgets.']);
return;
}
// Check first Text has headline style
final nameText = innerColumnWidget.children[0] as Text;
if (nameText.style?.fontSize != 24) {
_result(false, ['The Text widget for the name should use the "headlineSmall" textStyle.']);
return;
}
// Check first SizedBox has correct properties
final firstSizedBoxElement = outerColumnChildElements
.firstWhere((e) => e.widget == outerColumnChildWidgets[1]);
if ((firstSizedBoxElement.widget as SizedBox).height != 8) {
_result(false, ['The SizedBox before the first empty Row widget should have a height of 8.']);
return;
}
// Check second Row has two Text children
final secondRowElement = outerColumnChildElements
.firstWhere((e) => e.widget == outerColumnChildWidgets[2]);
final secondRowChildElements = <Element>[];
secondRowElement.visitChildElements((e) => secondRowChildElements.add(e));
if (secondRowChildElements.length != 2 ||
secondRowChildElements.any((e) => e.widget is! Text)) {
_result(false, ['The first empty Row widget should have two children, both Text widgets.']);
return;
}
// Check second Row has correct properties
if ((secondRowElement.widget as Row).mainAxisAlignment !=
MainAxisAlignment.spaceBetween) {
_result(false, ['The first empty Row widget should use MainAxisAlignment.spaceBetween as its MainAxisAlignment value.']);
return;
}
// Check second SizedBox has correct properties
final secondSizedBoxElement = outerColumnChildElements
.firstWhere((e) => e.widget == outerColumnChildWidgets[3]);
if ((secondSizedBoxElement.widget as SizedBox).height != 16) {
_result(false, ['The SizedBox between the first and second empty Row widgets should have a height of 16.']);
return;
}
_result(true);
}
{$ end test.dart $}
第三部分
练习:添加四个图标
{$ begin main.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.account_circle, size: 50),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Flutter McFlutter',
style: Theme.of(context).textTheme.headlineSmall,
),
const Text('Experienced App Developer'),
],
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text('123 Main Street'),
Text('415-555-0198'),
],
),
const SizedBox(height: 16),
Row(
children: const [],
),
],
);
}
}
{$ end main.dart $}
{$ begin solution.dart $}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.account_circle, size: 50),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Flutter McFlutter',
style: Theme.of(context).textTheme.headlineSmall,
),
const Text('Experienced App Developer'),
],
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text(
'123 Main Street',
),
Text(
'(415) 555-0198',
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: const [
Icon(Icons.accessibility),
Icon(Icons.timer),
Icon(Icons.phone_android),
Icon(Icons.phone_iphone),
],
),
],
);
}
}
{$ end solution.dart $}
{$ begin test.dart $}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
scaffoldBackgroundColor: const Color(0xffeeeeee),
textTheme: const TextTheme(
bodyMedium: TextStyle(
fontSize: 16,
),
),
),
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Container(
decoration: BoxDecoration(
color: const Color(0xffffffff),
border: Border.all(),
boxShadow: const [
BoxShadow(
blurRadius: 10,
color: Color(0x80000000),
),
],
),
padding: const EdgeInsets.all(8.0),
child: MyWidget(),
),
),
),
),
);
}
}
Future<void> main() async {
final completer = Completer<void>();
runApp(MyApp());
WidgetsFlutterBinding.ensureInitialized()
.addPostFrameCallback((timestamp) async {
completer.complete();
});
await completer.future;
final controller = LiveWidgetController(WidgetsBinding.instance);
// Check MyWidget starts with one Column
final myWidgetElement = controller.element(find.byType(MyWidget));
final myWidgetChildElements = <Element>[];
myWidgetElement.visitChildElements((e) => myWidgetChildElements.add(e));
if (myWidgetChildElements.length != 1 ||
myWidgetChildElements[0].widget is! Column) {
_result(false, ['The root widget in MyWidget\'s build method should be a Column.']);
return;
}
// Check outermost Column has 5 correct children.
final outerColumnElement = myWidgetChildElements[0];
final outerColumnChildWidgets =
(outerColumnElement.widget as Column).children;
final outerColumnChildElements = <Element>[];
outerColumnElement.visitChildElements((e) => outerColumnChildElements.add(e));
if (outerColumnChildWidgets.length != 5 ||
outerColumnChildWidgets[0] is! Row ||
outerColumnChildWidgets[1] is! SizedBox ||
outerColumnChildWidgets[2] is! Row ||
outerColumnChildWidgets[3] is! SizedBox ||
outerColumnChildWidgets[4] is! Row) {
_result(false, ['The children of the outermost Column should be [Row, SizedBox, Row, SizedBox, Row] in that order.']);
return;
}
// Check outermost Column's properties
if ((outerColumnElement.widget as Column).mainAxisSize != MainAxisSize.min) {
_result(false, ['The outermost Column should use MainAxisSize.min for its mainAxisSize.']);
return;
}
if ((outerColumnElement.widget as Column).crossAxisAlignment !=
CrossAxisAlignment.stretch) {
_result(false, ['The outermost Column should use CrossAxisAlignment.stretch for its crossAxisAlignment.']);
return;
}
// Check first Row has two children: Padding and Column
final firstRowElement = outerColumnChildElements
.firstWhere((e) => e.widget == outerColumnChildWidgets[0]);
final firstRowChildElements = <Element>[];
firstRowElement.visitChildElements((e) => firstRowChildElements.add(e));
if (firstRowChildElements.length != 2 ||
firstRowChildElements[0].widget is! Padding ||
firstRowChildElements[1].widget is! Column) {
_result(false, ['The first Row should have two children: first a Padding, and then a Column.']);
return;
}
// Check Padding has correct padding
final paddingElement = firstRowChildElements[0];
if ((paddingElement.widget as Padding).padding != const EdgeInsets.all(8)) {
_result(false, ['The Padding widget in the first Row should have a padding of 8.']);
return;
}
// Check Padding has an Icon as its child
final paddingChildren = <Element>[];
paddingElement.visitChildElements((e) => paddingChildren.add(e));
if (paddingChildren.length != 1 || paddingChildren[0].widget is! Icon) {
_result(false, ['The Padding widget in the first Row should have an Icon as its child.']);
return;
}
// Check icon has a size of 50
if ((paddingChildren[0].widget as Icon).size != 50) {
_result(false, ['The Icon in the top-left corner should have a size of 50.']);
return;
}
// Check inner Column has correct properties
final innerColumnElement = firstRowChildElements[1];
final innerColumnWidget = innerColumnElement.widget as Column;
if (innerColumnWidget.crossAxisAlignment != CrossAxisAlignment.start) {
_result(false, ['The Column for the name and title should use CrossAxisAlignment.start as its crosAxisAlignment.']);
return;
}
// Check inner Column has two Text children
if (innerColumnWidget.children.any((w) => w is! Text)) {
_result(false, ['The Column for the name and title should have two children, both Text widgets.']);
return;
}
if (innerColumnWidget.mainAxisSize != MainAxisSize.min) {
_result(false, ['The Column for the name and title should use MainAxisSize.min as its mainAxisSize.']);
return;
}
// Check first Text has headline style
final nameText = innerColumnWidget.children[0] as Text;
if (nameText.style?.fontSize != 24) {
_result(false, ['The Text widget for the name should use the "headlineSmall" textStyle.']);
return;
}
// Check first SizedBox has correct properties
final firstSizedBoxElement = outerColumnChildElements
.firstWhere((e) => e.widget == outerColumnChildWidgets[1]);
if ((firstSizedBoxElement.widget as SizedBox).height != 8) {
_result(false, ['The SizedBox before the first empty Row widget should have a height of 8.']);
return;
}
// Check second Row has two Text children
final secondRowElement = outerColumnChildElements
.firstWhere((e) => e.widget == outerColumnChildWidgets[2]);
final secondRowChildElements = <Element>[];
secondRowElement.visitChildElements((e) => secondRowChildElements.add(e));
if (secondRowChildElements.length != 2 ||
secondRowChildElements.any((e) => e.widget is! Text)) {
_result(false, ['The first Row widget should have two children, both Text widgets.']);
return;
}
// Check second Row has correct properties
if ((secondRowElement.widget as Row).mainAxisAlignment !=
MainAxisAlignment.spaceBetween) {
_result(false, ['The first Row widget should use MainAxisAlignment.spaceBetween as its mainAxisAlignment.']);
return;
}
// Check second SizedBox has correct properties
final secondSizedBoxElement = outerColumnChildElements
.firstWhere((e) => e.widget == outerColumnChildWidgets[3]);
if ((secondSizedBoxElement.widget as SizedBox).height != 16) {
_result(false, ['The SizedBox between the first and second Row widgets should have a height of 16.']);
return;
}
// Check second empty Row has four Icon children
final thirdRowElement = outerColumnChildElements
.firstWhere((e) => e.widget == outerColumnChildWidgets[4]);
final thirdRowChildElements = <Element>[];
thirdRowElement.visitChildElements((e) => thirdRowChildElements.add(e));
if (thirdRowChildElements.length != 4 ||
thirdRowChildElements.any((e) => e.widget is! Icon)) {
_result(false, ['The second empty Row widget should have four children, all Icon widgets.']);
return;
}
// Check second empty Row has correct properties
if ((thirdRowElement.widget as Row).mainAxisAlignment !=
MainAxisAlignment.spaceAround) {
_result(false, ['The second empty Row widget should use MainAxisAlignment.spaceAround as its MainAxisAligment value.']);
return;
}
_result(true);
}
{$ end test.dart $}
下一步是什么?
恭喜你,已经完成了这个 codelab!如果你想要了解关于 Flutter 的更多信息,这里有些值得探索的资源要推荐给你:
-
在 Building layouts 页面中学习关于 Flutter 的布局。
-
查看 sample apps。
-
访问 Flutter’s YouTube channel,你将能够观看大量专注于独立的 widget 以及开发者如何构建应用的视频。
你可以在 安装 页面中下载 Flutter。