Flutter第一部分(UI)第五篇:为Widget添加交互

前言:Flutter系列的文章我应该会持续更新至少一个月左右,从User Interface(UI)到数据相关(文件、数据库、网络)再到Flutter进阶(平台特定代码编写、测试、插件开发等),欢迎感兴趣的读者持续关注(可以扫描左边栏二维码或者微信搜索”IT工匠“关注微信公众号哦,会同步推送)。

本文的主要内容:

  • 如何响应点击事件
  • 如何自定义Widget
  • Stateless WidgetStateful Widget的不同

Flutter中按照是否自身可直接响应用户交互可以将Widget分为两类:

  • 第一类:自身可直接响应用户交互,指的是Widget自身具有如onTap这类的属性,可以通过这类属性直接监听用户的点击等事件,典型的比如FlatButton
  • 第二类:自身不能直接响应用户交互,指的是Widget自身没有入onTap这类属性,不能直接监听用户的点击等事件,典型的比如Icon

由于第一类比较简单,本文重点介绍一下第二类,即如何为非交互性(不能直接响应用户交互)的Widget添加交互性, 具体来说,我们将通过创建一个自定义的Statful Widget来让Icon具有交互性。

在上一篇文章中我们介绍了如何构建一个下面这样的UI页面:

The finished app

当这个app第一次运行的时候那个星星是红色的,代表这个屏幕中展示的那个图片被用户点击了喜欢,星星后面的数字47代表一共有47个用户点击了喜欢。本文将实现,点击星星后移除喜欢状态,用空心星星替换实心星星并减少星星后面的计数。 再次点击空心星星代表添加喜欢,会绘制一颗实心的星星并增加星星后的数字。

The custom widget you'll create

要实现此功能,您将创建一个包含星星和计数的自定义Widget, 点击星星会更改两个子Widget的状态,因此自定义的Widget应该同时管理这两个子Widget(星星和计数)。

首先我将会介绍一点前备知识,如果你只对最终的代码实现感兴趣,你可以直接跳到第2步:创建StatefulWidget的子类,如果你想尝试其他的管理状态的方法,可以直接跳到管理状态一节。

Stateful WidgetStateless Widget

一个Widget要么是有状态(stateful)的,要么是无状态(stateless)的,如果一个Widget是可改变的,比如当用户与其交互的时候其会产生变化,这个Widget就是有状态的(stateful)。

一个无状态(stateless)的Widget是永远不会发生改变的,IconIconButtonText都是典型的无状态的Widget,无状态(stateless)的Widget都是StatelessWidget的子类。

一个有状态(stateful)的Widget是动态的,比如它可以更改其外观以响应用户交互或接收数据时触发的事件。CheckBoxRadioSliderInkWellFormTextField都是典型的有状态的Widget,有状态(stateful)的Widget都是StatefulWidget的子类。

Widget的状态都是保存在State对象中的,从外观上分析小部件的状态。 状态由可以更改的值组成,例如滑块(slider)的当前值、是否选中复选框(CheckBox)。 当Widget的状态发生变化时,State对象调用会setState()方法来告诉框架重绘该Widget

创建一个有状态的(statefulWidget

明确几点概念:

  • 一个有状态的Widget一定实现了2个类:StatefulWidgetState
  • State类包含Widget的可变状态以及build()方法
  • Widget的状态(state)发生了改变,State对象将会调用setState()方法高速Flutter框架需要重绘当前Widget

本节将创建一个自定义的有状态(Stateful)的Widget,我们将用我们自定义的包含一个IconButton和一个TextWidget来替代原有的红色星星Widget和计数Widget

实现一个自定义的Widget需要创建2个类:

  • 一个StatefulWidget类的子类,用于定义Widget
  • 一个State类的子类,包含了State对象,并且定义build()方法

我们通过简单的几步来构建一个名为FavoriteWidget的自定义Widget

第1步:决定由那个对象来管理Widget的状态(State)

Widget的状态(State)可以有多种管理方式,在此处由于切换星星的状态(实心还是空心)是一个独立的操作,不会影响父WidgetUI的其余部分,所以我们让Widget自己管理自己的状态(State)。

关于详细的状态管理的内容,我会在后面的管理状态一节介绍。

第2步:创建StatefulWidget的子类

由于第1步我们已经决定了FavoriteWidget自己管理自己的状态(State),所以我们应该重写createState()方法来创建一个State对象。Flutter框架会在构建Widget的时候调用对应WidgetcreateState()方法。在这个例子中,我们应该在createState()方法中返回一个我们将在下一步定义的_FavoriteWidgetState类的实例对象:

class FavoriteWidget extends StatefulWidget {
  @override
  _FavoriteWidgetState createState() => _FavoriteWidgetState();
}

注意:这里的_开头指的是定义的对应类是私有的。

第3步:创建State类的子类

我们定义一个_FavoriteWidgetState类来存储会在Widget不同生命周期变化的数据,当app第一次运行的时候,UI界面应该展示红色的实心星星,代表当前已经选择了"喜欢"状态,并且傍边展示的文字为"41",我们本别使用bool _isFavoritedint _favoriteCount变量来存储这两个状态:

class _FavoriteWidgetState extends State<FavoriteWidget> {
  bool _isFavorited = true;
  int _favoriteCount = 41;
  // ···
}

_FavoriteWidgetState类同样也定义了一个build()方法,在该方法中创建一个Row(行),Row中包含有一个Iconbutton和一个Text,我们使用Iconbutton而不是Icon的原因是IconButtononPressed属性,我们可以通过这个onPressed属性定义处理点击事件的回调函数(_toggleFavorite),我们将在后面具体定义这个_toggleFavorite函数:

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'),
          ),
        ),
      ],
    );
  }
}

注意:我们这里将Text作为子Widget放置在了SizedBox中,并且设置了SizedBox的宽度,这样做的作用是固定Text的宽度,设想一下,当Text中只显示1位数字的时候Text的宽度和显示2位数字的宽度一定是不一样的,如果不固定Text的宽度,当数字变化的时候就会出现Text宽度发生跳变的情况,导致视觉效果很不好。

IconButton被点击的时候将会调用_toggleFavorite()方法,我们在_toggleFavorite()方法中调用setstate()方法并更新状态,这样Flutter框架就会知道需要重新绘制当前Widget了,从而达到更新界面的效果:

void _toggleFavorite() {
  setState(() {
    if (_isFavorited) {
      _favoriteCount -= 1;
      _isFavorited = false;
    } else {
      _favoriteCount += 1;
      _isFavorited = true;
    }
  });
}

setState()方法中的代码逻辑很简单,首先判断当前_isFavorited的状态,然后对_isFavorited_isFavorited的值进行更新。

第4步:将我们自定义的Stateful Widget加入到Widget树中

我们应该在appbuild()方法中将我们自定义的Stateful Widget加入到Widget 树中,首先找到原先IconText的位置,然后删除原来的代码,加入新的我们创建的Stateful Widget

 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(),
      ],
    ),
  );

然后运行代码(推荐使用热更新),可以看到效果图:

img

管理状态

在我们的设计中,到底应该由谁来管理Widget的状态(State)?是Widget本身?是Widget的父Widget?还是二者共同管理?还是另一个对象来管理? 事实上有不止一种有效的方法可以使你的Widget小部件具有交互性, 作为Widget的设计者,你可以根据预期的Widget的使用方式做出决策。 以下是几种最常用的管理状态的方法:

你可能会有疑问,你应该如何决定具体使用哪一种状态管理方法?这里提供几个原则供你参考:

  • 如果状态代表的是用户数据,比如CheckBox是否被选中,或者Slider(进度条)的当前进度,这种情况下最好让Widget的父级Widget去管理其状态
  • 如果状态代表的是界面展示方面的,比如动画,这种情况最好由Widget自己来管理自己的状态

如果你不太确定自己的场景属于以上哪种,可以直接使用父级Widget管理的方法,因为这个方法是通用的。

接下来我将通过创建三个简单示例(TapboxATapboxBTapboxC)来举例说明管理状态的不同方法。 这几个示例的工作方式类似: 每个都创建了一个Container,当点击时,可以在绿色或灰色框之间切换, _active布尔值确定颜色:true代表绿色,false代表灰色。

image-20190610193224492

Widget自己管理自己本身的State

有时,由Widget自己管理自己的状态可以产生很强大的功能。例如,ListView在其内容的总尺寸超出其最大渲染框的尺寸时会自动进行滚动,这个滚动的状态是由ListView自己管理的,不需要我们开发人员去手动设置它什么时候应该开始滚动、什么时候应该停止滚动。

我们通过一个示例来进行说明,我们创建一个_TapboxAState类:

  • 管理TapboxA的状态
  • 定义布尔值_activity,代表当前Widget的颜色
  • 定义_handleTap()方法,当Widget被点击时在该方法中调用setState()并更新 _activity的值从而达到更新UI的目的
  • 实现Widget的所有交互行为

代码如下:

// 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(),
        ),
      ),
    );
  }
}

运行效果如下图所示:

a8o2c-uqnut

Widget的父级Widget管理其State

Widget管理子Widget状态的最大用处是在合适的时机通知子Widget进行UI更新。 例如,IconButton允许你将Icon视为可点击的按钮, IconButton是一个无状态的Widget,所以我们应该通过父Widget来确定Iconutton是否已被点击。

在以下例子中,TapboxB将其状态回调给父Widget,因为TapboxB不管理任何状态,所以它是StatelessWidget的子类。

在这个示例中我们应该实现2个类:ParentWidgetState(代表父Widget)、TapboxB(代表子Widget)

ParentWidgetState的主要功能:

  • TapboxB管理_activity状态
  • 实现 _handleTapboxChanged()方法,该方法会在TapboxB被点击时调用
  • 当状态发生改变时调用setState()来更新UI

TapboxB的主要功能:

  • 继承StatelessWidget类,因为TapboxB不用管理自己的状态
  • 当点击事件(tap)被触发的时候通知父Widget

代码实现如下:

// 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],
        ),
      ),
    );
  }
}

代码的运行效果如下:

a8o2c-uqnut

混合使用前两种管理方法

对于某些Widget,使用混合的方法管理其状态很有有意义。 在这种情况下,有状态(stateful)的Widget和其父Widget分别管理其一部分状态(State)。

TapboxC示例中,在点击时,框周围会出现深绿色边框,点击后,边框消失,框的颜色也会改变。 TapboxC将其 _active状态导出到其父Widget,在内部管理只其 _highlight状态,所以 此示例有两个State对象,_ParentWidgetState_TapboxCState

_ParentWidgetState的功能:

  • 管理_activity状态
  • 实现_handleTapboxChanged()方法,该方法会在方框被点击后调用
  • 当点击事件发生后调用setState()并改变_activity的值以更新UI

_TapboxCState的功能:

  • 管理_highlight状态
  • GestureDetector监听所有的点击事件,当用户手指点下的时候添加高亮边框,当用户手指抬起的时候取消高亮边框
  • 当点击事件发生时,根据父Widget传递的状态进行相应操作
//---------------------------- 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,
        ),
      ),
    );
  }
}

运行效果如下所示:

img

替代实现可能已将高亮状态导出到父级,同时保持活动状态为内部,但如果您要求某人使用该分接框,他们可能会抱怨它没有多大意义。 开发人员关心该框是否处于活动状态。 开发人员可能并不关心如何管理突出显示,并且更喜欢点按框处理这些细节。

其他的交互式Widget

Flutter提供了很多按钮和类似的交互式Widget。 这些Widget中的大多数都实现了Material Design准则,该准则定义了一组具有固定用户界面的组件。

如果您愿意,可以使用GestureDetector在任何自定义的Widget中构建交互性。 您可以在管理状态一节中找到GestureDetector的使用示例。

提示:Flutter还提供了一些IOS风格的Widget,称之为Cupertino,具体地址:https://api.flutter.dev/flutter/cupertino/cupertino-library.html

当您需要交互性时,最简单的方法是使用Flutter已经给我提供好的Widget,下面是一个部分列表:

标准库中的Widget
  • Form,地址:https://api.flutter.dev/flutter/widgets/Form-class.html
  • FormField,地址:https://api.flutter.dev/flutter/widgets/FormField-class.html
Material库中的Widget
  • Checkbox 地址:https://api.flutter.dev/flutter/material/Checkbox-class.html
  • DropdownButton ,地址:https://api.flutter.dev/flutter/material/DropdownButton-class.html
  • FlatButton , 地址:https://api.flutter.dev/flutter/material/FlatButton-class.html
  • FloatingActionButton , 地址:https://api.flutter.dev/flutter/material/FloatingActionButton-class.html
  • IconButton ,地址:https://api.flutter.dev/flutter/material/IconButton-class.html
  • Radio ,地址:https://api.flutter.dev/flutter/material/Radio-class.html
  • RaisedButton ,地址:https://api.flutter.dev/flutter/material/RaisedButton-class.html
  • Slider ,地址:https://api.flutter.dev/flutter/material/Slider-class.html
  • Switch ,地址:https://api.flutter.dev/flutter/material/Switch-class.html
  • TextField ,地址:https://api.flutter.dev/flutter/material/TextField-class.html

好了,本文的全部内容到这里就结束了,总结一下,本文我们主要介绍了如何给Flutter中的Widget添加交互性,下一篇文章我们将介绍如何油压地给Flutter项目中添加并引用资源和图片,欢迎大家关注。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值