Flutter学习指南:UI布局和控件(1)

}
}

class _BarWidgetState extends State {
var i = 0;

@override
Widget build(BuildContext context) {
return Row(
children: [
Text(‘i = $i’),
RaisedButton(
onPressed: () {
setState(() {
++i;
});
},
child: Text(‘click’),
)
],
);
}
}

下面我们开始学习一些具体的控件。

文本

为了展示文本,我们使用 Text:

class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(“Put your text here”);
}
}

这就是最简单的文本了,它使用的是默认的样式。很多情况下,我们都需要对文本的样式进行修改,这个时候,可以使用 TextStyle:

class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
“Put your text here”,
style: TextStyle(
color: Colors.blue,
fontSize: 16.0,
fontWeight: FontWeight.bold
),
);
}
}

图片

使用 Image,可以让我们向用户展示一张图片。图片的来源可以是网络、文件、资源和内存,它们对应的构造函数分别是:

Image.asset(name);
Image.file(file);
Image.memory(bytes);
Image.network(src);

比方说,为了展示一张来自网络的图片,我们可以这样:

class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Image.network(
“http://www.example.com/xxx.png”,
width: 200.0,
height: 150.0,
);
}
}

按钮

Flutter 提供了两个基本的按钮控件:FlatButton 和 RaisedButton,它们的使用方法是类似的:

class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
var flatBtn = FlatButton(
onPressed: () => print(‘FlatButton pressed’),
child: Text(‘BUTTON’),
);
var raisedButton = RaisedButton(
onPressed: () => print(‘RaisedButton pressed’),
child: Text(‘BUTTON’),
);
return raisedButton;
}
}

通过设置 onPressed 回调,我们可以在按钮被点击的时候得到回调。child 参数用于设置按钮的内容。虽然我们给 child 传递的是 Text,但这不是必需的,它可以接受任意的 Widget,比方说,Image。

注意,由于我们只是在按钮点击的时候打印一个字符串,这里使用 StatelessWidget 是没有问题的。但如果有其他 UI 动作(比如弹出一个 dialog,则必须使用 StatefulWidget)。

它们的区别只是样式不同而已的:

FlatButton:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

flat-button

RaiseButton:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

raised-button

文本输入框

Flutter 的文本输入框叫 TextField。为了获取用户输入的文本,我们需要给他设置一个 controller。通过这个 controller,就可以拿到文本框里的内容:

class MessageForm extends StatefulWidget {
@override
State createState() {
return _MessageFormState();
}
}

class _MessageFormState extends State {
var editController = TextEditingController();

@override
Widget build(BuildContext context) {
// Row、Expand 都是用于布局的控件,这里可以先忽略它们
return Row(
children: [
Expanded(
child: TextField(
controller: editController,
),
),
RaisedButton(
child: Text(“click”),
onPressed: () => print(‘text inputted: ${editController.text}’),
)
],
);
}

@override
void dispose() {
super.dispose();
// 手动调用 controller 的 dispose 方法以释放资源
editController.dispose();
}
}

显示弹框

在前面的 TextField 例子中,我们只是把用户的输入通过 print 打印出来,这未免也太无趣了。在这一小节,我们要把它显示在 dialog 里。为了弹出一个 dialog,我们需要调用 showDialog 方法并传递一个 builder:

class _MessageFormState extends State {
var editController = TextEditingController();

@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
controller: editController,
),
),
RaisedButton(
child: Text(“click”),
onPressed: () {
showDialog(
// 第一个 context 是参数名,第二个 context 是 State 的成员变量
context: context,
builder: (_) {
return AlertDialog(
// dialog 的内容
content: Text(editController.text),
// actions 设置 dialog 的按钮
actions: [
FlatButton(
child: Text(‘OK’),
// 用户点击按钮后,关闭弹框
onPressed: () => Navigator.pop(context),
)
],
);
}
);
}
)
],
);
}

@override
void dispose() {
super.dispose();
editController.dispose();
}
}

最简单的布局——Container、Padding 和 Center:

我们经常说,Flutter 里面所有的东西都是 Widget,所以,布局也是 Widget。

控件 Container 可以让我们设置一个控件的尺寸、背景、margin 等:

class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Text(‘text’),
padding: EdgeInsets.all(8.0),
margin: EdgeInsets.all(4.0),
width: 80.0,
decoration: BoxDecoration(
// 背景色
color: Colors.grey,
// 圆角
borderRadius: BorderRadius.circular(5.0),
),
);
}
}

如果我们只需要 padding,可以使用控件 Padding:

class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(8.0),
child: Text(‘text’),
);
}
}

Center 就跟它的名字一样,把一个控件放在中间:

class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(8.0),
margin: EdgeInsets.all(4.0),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
// 背景色
color: Colors.grey,
// 圆角
borderRadius: BorderRadius.circular(5.0),
),

// 把文本放在 Container 的中间
child: Center(
child: Text(‘text’),
),
);
}
}

水平、竖直布局和 Expand

我们经常说,Flutter 里面所有的东西都是 Widget,所以,布局也是 Widget。水平布局我们可以使用 Row,竖直布局使用 Column。

class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
// 只有一个子元素的 widget,一般使用 child 参数来设置;Row 可以包含多个子控件,
// 对应的则是 children。
children: [
Text(‘text1’),
Text(‘text2’),
Text(‘text3’),
Text(‘text4’),
],
);
}
}

Column 的使用是一样的:

class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(‘text1’),
Text(‘text2’),
Text(‘text3’),
Text(‘text4’),
],
);
}
}

关于 Expand 控件,我们来看看 TextField 的那个例子:

class MessageForm extends StatefulWidget {
@override
State createState() {
return _MessageFormState();
}
}

class _MessageFormState extends State {
var editController = TextEditingController();

@override
Widget build(BuildContext context) {
return Row(
children: [
// 占满一行里除 RaisedButton 外的所有空间
Expanded(
child: TextField(
controller: editController,
),
),
RaisedButton(
child: Text(“click”),
onPressed: () => print(‘text inputted: ${editController.text}’),
)
],
);
}

@override
void dispose() {
super.dispose();
editController.dispose();
}
}

这里通过使用 Expand,TextField 才能够占满一行里除按钮外的所有空间。此外,当一行/列里有多个 Expand 时,我们还可以通过设置它的 flex 参数,在多个 Expand 之间按比例划分可用空间。

class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
// 占一行的 2/3
flex: 2,
child: RaisedButton(child: Text(‘btn1’),),
),
Expanded(
// 占一行的 1/3
flex: 1,
child: RaisedButton(child: Text(‘btn2’),),
),
],
);
}
}

Stack 布局

有些时候,我们可能会希望一个控件叠在另一个控件的上面。于是,Stack 应运而生:

class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Stack(
children: [
Text(‘foobar’),
Text(‘barfoo’),
],
);
}
}

默认情况下,子控件都按 Stack 的左上角对齐,于是,上面的两个文本完全一上一下堆叠在一起。我们还可以通过设置 alignment 参数来改变这个对齐的位置:

class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Stack(
// Aligment 的取值范围为 [-1, 1],Stack 中心为 (0, 0),
// 这里设置为 (-0.5, -0.5) 后,可以让文本对齐到 Container 的 1/4 处
alignment: const Alignment(-0.5, -0.5),
children: [
Container(
width: 200.0,
height: 200.0,
color: Colors.blue,
),
Text(‘foobar’),
],
);
}
}

效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

screenshot-stack

通过组合 Row/Column 和 Stack,已经能够完成绝大部分的布局了,所以 Flutter 里没有相对布局之类的东西。更多的 Flutter 控件,读者可以参考 flutter.io/widgets/

示例一

在这一节里,我们综合前面所学的知识,来实现下面这个界面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

lakes-diagram

展示图片

  1. 把图片 lake 放到项目根目录的 images 文件夹下(如果没有,你需要自己创建一个)

  2. 修改 pubspec.yaml,找到下面这个地方,然后把图片加进来

flutter:

# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true

# To add assets to your application, add an assets section, like this:
# assets:
#  - images/a_dot_burr.jpeg
#  - images/a_dot_ham.jpeg

修改后如下:

flutter:

# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true

# To add assets to your application, add an assets section, like this:
assets:
- images/lake.jpg

  1. 现在,我们可以把这张图片展示出来了:

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: ‘Flutter UI basic 1’,
home: Scaffold(
appBar: AppBar(
title: Text(‘Top Lakes’),
),
body: Image.asset(
‘images/lake.jpg’,
width: 600.0,
height: 240.0,
// cover 类似于 Android 开发中的 centerCrop,其他一些类型,读者可以查看
// https://docs.flutter.io/flutter/painting/BoxFit-class.html
fit: BoxFit.cover,
)
),
);
}
}

如果读者是初学 Flutter,强烈建议在遇到不熟悉的 API 时翻一翻文档,并在文档中找到 demo 所使用的 API。我们的例子不可能覆盖所有的 API,通过这种方式熟悉文档后,读者就可以根据文档实现出自己想要的效果。不妨就从 Image 开始吧,在 docs.flutter.io/flutter/wid… 找出上面我们使用的 Image.asset 构造函数的几个参数的含义,还有 BoxFit 的其他几个枚举值。

布局

在这一小节,我们来实现图片下方的标题区域。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们直接来看代码:

class _TitleSection extends StatelessWidget {
final String title;
final String subtitle;
final int starCount;

_TitleSection(this.title, this.subtitle, this.starCount);

@override
Widget build(BuildContext context) {
// 为了给 title section 加上 padding,这里我们给内容套一个 Container
return Container(
// 设置上下左右的 padding 都是 32。类似的还有 EdgeInsets.only/symmetric 等
padding: EdgeInsets.all(32.0),
child: Row(
children: [
// 这里为了让标题占满屏幕宽度的剩余空间,用 Expanded 把标题包了起来
Expanded(
// 再次提醒读者,Expanded 只能包含一个子元素,使用的参数名是 child。接下来,
// 为了在竖直方向放两个标题,加入一个 Column。
child: Column(
// Column 是竖直方向的,cross 为交叉的意思,也就是说,这里设置的是水平方向
// 的对齐。在水平方向,我们让文本对齐到 start(读者可以修改为 end 看看效果)
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 聪明的你,这个时候肯定知道为什么突然加入一个 Container 了。
// 跟前面一样,只是为了设置一个 padding
Container(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
title,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Text(
subtitle,
style: TextStyle(color: Colors.grey[500]),
)
],
),
),

// 这里是 Row 的第二个子元素,下面这两个就没用太多值得说的东西了。
Icon(
Icons.star,
color: Colors.red[500],
),

Text(starCount.toString())
],
),
);
}
}

对齐

接下来我们要做的这一部分在布局上所用到的知识,基本知识在上一小节我们都已经学习了。这里唯一的区别在于,三个按钮是水平分布的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实现如下:

Widget _buildButtonColumn(BuildContext context, IconData icon, String label) {
final color = Theme.of(context).primaryColor;

return Column(
// main axis 跟我们前面提到的 cross axis 相对应,对 Column 来说,指的就是竖直方向。
// 在放置完子控件后,屏幕上可能还会有一些剩余的空间(free space),min 表示尽量少占用
// free space;类似于 Android 的 wrap_content。
// 对应的,还有 MainAxisSize.max
mainAxisSize: MainAxisSize.min,
// 沿着 main axis 居中放置
mainAxisAlignment: MainAxisAlignment.center,

children: [
Icon(icon, color: color),
Container(
margin: const EdgeInsets.only(top: 8.0),
child: Text(
label,
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.w400,
color: color,
),
),
)
],
);
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//…

Widget buttonSection = Container(
child: Row(
// 沿水平方向平均放置
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildButtonColumn(context, Icons.call, ‘CALL’),
_buildButtonColumn(context, Icons.near_me, ‘ROUTE’),
_buildButtonColumn(context, Icons.share, ‘SHARE’),
],
),
);
//…
}

关于 cross/main axis,看看下面这两个图就很清楚了:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

MainAxisAlignment 的更多的信息,可以查看 docs.flutter.io/flutter/ren…

全部放到一起

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final titleSection = _TitleSection(
‘Oeschinen Lake Campground’, ‘Kandersteg, Switzerland’, 41);
final buttonSection = …;
final textSection = Container(

《设计思想解读开源框架》

第一章、 热修复设计

  • 第一节、 AOT/JIT & dexopt 与 dex2oat

  • 第二节、 热修复设计之 CLASS_ISPREVERIFIED 问题

  • 第三节、热修复设计之热修复原理

  • 第四节、Tinker 的集成与使用(自动补丁包生成)

    第二章、 插件化框架设计

  • 第一节、 Class 文件与 Dex 文件的结构解读

  • 第二节、 Android 资源加载机制详解

  • 第三节、 四大组件调用原理

  • 第四节、 so 文件加载机制

  • 第五节、 Android 系统服务实现原理

    第三章、 组件化框架设计

  • 第一节、阿里巴巴开源路由框——ARouter 原理分析

  • 第二节、APT 编译时期自动生成代码&动态类加载

  • 第三节、 Java SPI 机制

  • 第四节、 AOP&IOC

  • 第五节、 手写组件化架构

    第四章、图片加载框架

  • 第一节、图片加载框架选型

  • 第二节、Glide 原理分析

  • 第三节、手写图片加载框架实战

    第五章、网络访问框架设计

  • 第一节、网络通信必备基础

  • 第二节、OkHttp 源码解读

  • 第三节、Retrofit 源码解析

    第六章、 RXJava 响应式编程框架设计

  • 第一节、链式调用

  • 第二节、 扩展的观察者模式

  • 第三节、事件变换设计

  • 第四节、Scheduler 线程控制

    第七章、 IOC 架构设计

  • 第一节、 依赖注入与控制反转

  • 第二节、ButterKnife 原理上篇、中篇、下篇

  • 第三节、Dagger 架构设计核心解密

    第八章、 Android 架构组件 Jetpack

  • 第一节、 LiveData 原理

  • 第二节、 Navigation 如何解决 tabLayout 问题

  • 第三节、 ViewModel 如何感知 View 生命周期及内核原理

  • 第四节、 Room 架构方式方法

  • 第五节、 dataBinding 为什么能够支持 MVVM

  • 第六节、 WorkManager 内核揭秘

  • 第七节、 Lifecycles 生命周期


    本文包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

    《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
    节、链式调用**

  • 第二节、 扩展的观察者模式

  • 第三节、事件变换设计

  • 第四节、Scheduler 线程控制

    [外链图片转存中…(img-b4uUI9D1-1714707064892)]

    第七章、 IOC 架构设计

  • 第一节、 依赖注入与控制反转

  • 第二节、ButterKnife 原理上篇、中篇、下篇

  • 第三节、Dagger 架构设计核心解密

    [外链图片转存中…(img-tBsh2tDB-1714707064892)]

    第八章、 Android 架构组件 Jetpack

  • 第一节、 LiveData 原理

  • 第二节、 Navigation 如何解决 tabLayout 问题

  • 第三节、 ViewModel 如何感知 View 生命周期及内核原理

  • 第四节、 Room 架构方式方法

  • 第五节、 dataBinding 为什么能够支持 MVVM

  • 第六节、 WorkManager 内核揭秘

  • 第七节、 Lifecycles 生命周期

    [外链图片转存中…(img-pyueIV7C-1714707064893)]
    本文包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
    [外链图片转存中…(img-OeK8ONfN-1714707064894)]
    《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 29
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值