本文的主要内容: Flutter中按照是否自身可直接响应用户交互可以将Widget分为两类: 由于第一类比较简单,本文重点介绍一下第二类,即如何为非交互性(不能直接响应用户交互)的Widget添加交互性, 具体来说,我们将通过创建一个自定义的Statful Widget来让Icon具有交互性。 在上一篇文章中我们介绍了如何构建一个下面这样的UI页面: 当这个app第一次运行的时候那个星星是红色的,代表这个屏幕中展示的那个图片被用户点击了喜欢,星星后面的数字47代表一共有47个用户点击了喜欢。本文将实现,点击星星后移除喜欢状态,用空心星星替换实心星星并减少星星后面的计数。 再次点击空心星星代表添加喜欢,会绘制一颗实心的星星并增加星星后的数字。 要实现此功能,您将创建一个包含星星和计数的自定义Widget, 点击星星会更改两个子Widget的状态,因此自定义的Widget应该同时管理这两个子Widget(星星和计数)。 首先我将会介绍一点前备知识,如果你只对最终的代码实现感兴趣,你可以直接跳到第2步:创建StatefulWidget的子类,如果你想尝试其他的管理状态的方法,可以直接跳到管理状态一节。 一个Widget要么是有状态(stateful)的,要么是无状态(stateless)的,如果一个Widget是可改变的,比如当用户与其交互的时候其会产生变化,这个Widget就是有状态的(stateful)。 一个无状态(stateless)的Widget是永远不会发生改变的,Icon、IconButton、Text都是典型的无状态的Widget,无状态(stateless)的Widget都是StatelessWidget的子类。 一个有状态(stateful)的Widget是动态的,比如它可以更改其外观以响应用户交互或接收数据时触发的事件。CheckBox、Radio、Slider、InkWell、Form、TextField都是典型的有状态的Widget,有状态(stateful)的Widget都是StatefulWidget的子类。 Widget的状态都是保存在State对象中的,从外观上分析小部件的状态。 状态由可以更改的值组成,例如滑块(slider)的当前值、是否选中复选框(CheckBox)。 当Widget的状态发生变化时,State对象调用会setState()方法来告诉框架重绘该Widget。 明确几点概念: 本节将创建一个自定义的有状态(Stateful)的Widget,我们将用我们自定义的包含一个IconButton和一个Text的Widget来替代原有的红色星星Widget和计数Widget。 实现一个自定义的Widget需要创建2个类: 我们通过简单的几步来构建一个名为FavoriteWidget的自定义Widget: 第1步:决定由那个对象来管理Widget的状态(State) Widget的状态(State)可以有多种管理方式,在此处由于切换星星的状态(实心还是空心)是一个独立的操作,不会影响父Widget或UI的其余部分,所以我们让Widget自己管理自己的状态(State)。 关于详细的状态管理的内容,我会在后面的管理状态一节介绍。 <span id="subclass-statefulWidget">第2步:创建StatefulWidget的子类</span> 由于第1步我们已经决定了FavoriteWidget自己管理自己的状态(State),所以我们应该重写createState()方法来创建一个State对象。Flutter框架会在构建Widget的时候调用对应Widget的createState()方法。在这个例子中,我们应该在createState()方法中返回一个我们将在下一步定义的_FavoriteWidgetState类的实例对象: 注意:这里的_开头指的是定义的对应类是私有的。 第3步:创建State类的子类 我们定义一个_FavoriteWidgetState类来存储会在Widget不同生命周期变化的数据,当app第一次运行的时候,UI界面应该展示红色的实心星星,代表当前已经选择了”喜欢”状态,并且傍边展示的文字为”41”,我们本别使用bool _isFavorited和int _favoriteCount变量来存储这两个状态: _FavoriteWidgetState类同样也定义了一个build()方法,在该方法中创建一个Row(行),Row中包含有一个Iconbutton和一个Text,我们使用Iconbutton而不是Icon的原因是IconButton有onPressed属性,我们可以通过这个onPressed属性定义处理点击事件的回调函数(_toggleFavorite),我们将在后面具体定义这个_toggleFavorite函数: 注意:我们这里将Text作为子Widget放置在了SizedBox中,并且设置了SizedBox的宽度,这样做的作用是固定Text的宽度,设想一下,当Text中只显示1位数字的时候Text的宽度和显示2位数字的宽度一定是不一样的,如果不固定Text的宽度,当数字变化的时候就会出现Text宽度发生跳变的情况,导致视觉效果很不好。 当IconButton被点击的时候将会调用_toggleFavorite()方法,我们在_toggleFavorite()方法中调用setstate()方法并更新状态,这样Flutter框架就会知道需要重新绘制当前Widget了,从而达到更新界面的效果: setState()方法中的代码逻辑很简单,首先判断当前_isFavorited的状态,然后对_isFavorited和_isFavorited的值进行更新。 第4步:将我们自定义的Stateful Widget加入到Widget树中 我们应该在app的build()方法中将我们自定义的Stateful Widget加入到Widget 树中,首先找到原先Icon和Text的位置,然后删除原来的代码,加入新的我们创建的Stateful Widget: 然后运行代码(推荐使用热更新),可以看到效果图: 在我们的设计中,到底应该由谁来管理Widget的状态(State)?是Widget本身?是Widget的父Widget?还是二者共同管理?还是另一个对象来管理? 事实上有不止一种有效的方法可以使你的Widget小部件具有交互性, 作为Widget的设计者,你可以根据预期的Widget的使用方式做出决策。 以下是几种最常用的管理状态的方法: 你可能会有疑问,你应该如何决定具体使用哪一种状态管理方法?这里提供几个原则供你参考: 如果你不太确定自己的场景属于以上哪种,可以直接使用父级Widget管理的方法,因为这个方法是通用的。 接下来我将通过创建三个简单示例(TapboxA,TapboxB和TapboxC)来举例说明管理状态的不同方法。 这几个示例的工作方式类似: 每个都创建了一个Container,当点击时,可以在绿色或灰色框之间切换, _active布尔值确定颜色:true代表绿色,false代表灰色。 Widget自己管理自己本身的State 有时,由Widget自己管理自己的状态可以产生很强大的功能。例如,ListView在其内容的总尺寸超出其最大渲染框的尺寸时会自动进行滚动,这个滚动的状态是由ListView自己管理的,不需要我们开发人员去手动设置它什么时候应该开始滚动、什么时候应该停止滚动。 我们通过一个示例来进行说明,我们创建一个_TapboxAState类: 代码如下: 运行效果如下图所示: Widget的父级Widget管理其State 父Widget管理子Widget状态的最大用处是在合适的时机通知子Widget进行UI更新。 例如,IconButton允许你将Icon视为可点击的按钮, IconButton是一个无状态的Widget,所以我们应该通过父Widget来确定Iconutton是否已被点击。 在以下例子中,TapboxB将其状态回调给父Widget,因为TapboxB不管理任何状态,所以它是StatelessWidget的子类。 在这个示例中我们应该实现2个类:ParentWidgetState(代表父Widget)、TapboxB(代表子Widget) ParentWidgetState的主要功能: TapboxB的主要功能: 代码实现如下: 代码的运行效果如下: 混合使用前两种管理方法 对于某些Widget,使用混合的方法管理其状态很有有意义。 在这种情况下,有状态(stateful)的Widget和其父Widget分别管理其一部分状态(State)。 在TapboxC示例中,在点击时,框周围会出现深绿色边框,点击后,边框消失,框的颜色也会改变。 TapboxC将其 _active状态导出到其父Widget,在内部管理只其 _highlight状态,所以 此示例有两个State对象,_ParentWidgetState和_TapboxCState: _ParentWidgetState的功能: _TapboxCState的功能: 运行效果如下所示: 替代实现可能已将高亮状态导出到父级,同时保持活动状态为内部,但如果您要求某人使用该分接框,他们可能会抱怨它没有多大意义。 开发人员关心该框是否处于活动状态。 开发人员可能并不关心如何管理突出显示,并且更喜欢点按框处理这些细节。 Flutter提供了很多按钮和类似的交互式Widget。 这些Widget中的大多数都实现了Material Design准则,该准则定义了一组具有固定用户界面的组件。 如果您愿意,可以使用GestureDetector在任何自定义的Widget中构建交互性。 您可以在管理状态一节中找到GestureDetector的使用示例。 提示:Flutter还提供了一些IOS风格的Widget,称之为Cupertino,具体地址:https://api.flutter.dev/flutter/cupertino/cupertino-library.html 当您需要交互性时,最简单的方法是使用Flutter已经给我提供好的Widget,下面是一个部分列表: 标准库中的Widget Material库中的WidgetFlutter系列的文章我会持续更新一个月左右,力求利用1个月带大家入门Flutter,抓住这波技术风口,欢迎大家关注。同时如果觉得这里代码排版不是很舒服的读者可以关注我的微信公众号“IT工匠”,我会同步更新,另外微信公众号上还有很多互联网必备资源(涉及算法、数据结构、java、深度学习、计算机网络、python、Android等互联网技术资料),欢迎大家关注、交流。
Stateful Widget和Stateless Widget
创建一个有状态的(stateful)Widget
class FavoriteWidget extends StatefulWidget {
@override
_FavoriteWidgetState createState() => _FavoriteWidgetState();
}
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = true;
int _favoriteCount = 41;
// ···
}
class _FavoriteWidgetState extends State<FavoriteWidget> {
// ···
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_isFavorited ? Icon(Icons.star) : Icon(Icons.star_border)),
color: Colors.red[500],
onPressed: _toggleFavorite,
),
),
SizedBox(
width: 18,
child: Container(
child: Text('$_favoriteCount'),
),
),
],
);
}
}
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
Widget titleSection = Container(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
/*1*/
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/*2*/
Container(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Oeschinen Lake Campground',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Text(
'Kandersteg, Switzerland',
style: TextStyle(
color: Colors.grey[500],
),
),
],
),
),
FavoriteWidget(),
],
),
);
管理状态
// TapboxA 自己管理自己的状态
//------------------------- TapboxA ----------------------------------
class TapboxA extends StatefulWidget {
TapboxA({Key key}) : super(key: key);
@override
_TapboxAState createState() => _TapboxAState();
}
class _TapboxAState extends State<TapboxA> {
bool _active = false;
void _handleTap() {
setState(() {
_active = !_active;
});
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
_active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
//------------------------- MyApp ----------------------------------
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: Center(
child: TapboxA(),
),
),
);
}
}
// ParentWidget为TapboxB管理状态.
//------------------------ ParentWidget --------------------------------
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return Container(
child: TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//------------------------- TapboxB ----------------------------------
class TapboxB extends StatelessWidget {
TapboxB({Key key, this.active: false, @required this.onChanged})
: super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
void _handleTap() {
onChanged(!active);
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
//---------------------------- ParentWidget ----------------------------
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return Container(
child: TapboxC(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//----------------------------- TapboxC ------------------------------
class TapboxC extends StatefulWidget {
TapboxC({Key key, this.active: false, @required this.onChanged})
: super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
_TapboxCState createState() => _TapboxCState();
}
class _TapboxCState extends State<TapboxC> {
bool _highlight = false;
void _handleTapDown(TapDownDetails details) {
setState(() {
_highlight = true;
});
}
void _handleTapUp(TapUpDetails details) {
setState(() {
_highlight = false;
});
}
void _handleTapCancel() {
setState(() {
_highlight = false;
});
}
void _handleTap() {
widget.onChanged(!widget.active);
}
Widget build(BuildContext context) {
// This example adds a green border on tap down.
// On tap up, the square changes to the opposite state.
return GestureDetector(
onTapDown: _handleTapDown, // Handle the tap events in the order that
onTapUp: _handleTapUp, // they occur: down, up, tap, cancel
onTap: _handleTap,
onTapCancel: _handleTapCancel,
child: Container(
child: Center(
child: Text(widget.active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white)),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color:
widget.active ? Colors.lightGreen[700] : Colors.grey[600],
border: _highlight
? Border.all(
color: Colors.teal[700],
width: 10.0,
)
: null,
),
),
);
}
}
其他的交互式Widget
好了,本文的全部内容到这里就结束了,总结一下,本文我们主要介绍了如何给Flutter中的Widget添加交互性,下一篇文章我们将介绍如何油压地给Flutter项目中添加并引用资源和图片,欢迎大家关注。