欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Flutter 的基本控件

程序员文章站 2022-06-24 09:42:33
文本控件 Text 支持两种类型的文本展示,一个是默认的展示单一样式文本 Text,另一个是支持多种混合样式的富文本 Text.rich。 单一样式文本 Text 单一样式文本 Text 的初始化,是要传入需要展示的字符串。而这个字符串的具体展示效果,受构造函数中的其他参数控制。这些参数大致可以分为 ......

文本控件

text 支持两种类型的文本展示,一个是默认的展示单一样式文本 text,另一个是支持多种混合样式的富文本 text.rich。

单一样式文本 text

单一样式文本 text 的初始化,是要传入需要展示的字符串。而这个字符串的具体展示效果,受构造函数中的其他参数控制。这些参数大致可以分为两类:

  • 控制整体文本布局的参数,如文本对齐方式 textalign、文本排版方向 textdirection,文本显示最大行数 maxlines、文本截断规则 overflow 等等,这些都是构造函数中的参数;
  • 控制文本展示样式的参数,如字体名称 fontfamily、字体大小 fontsize、文本颜色 color、文本阴影 shadows 等等,这些参数被统一封装到构造函数中的参数 style 中。

示例代码 - 定义了一段剧中布局、20号红色粗体展示样式的字符串:

text(
    '文本是视图系统中的常见空间,用来显示一段特定样式的字符串,就比如 android 里的 textview,或是 ios 中的 uilabel。',
    textalign: textalign.center, // 居中显示
    style: textstyle(fontweight: fontweight.bold, fontsize: 20, color: colors.red),// 20 号,红色粗体展示
);

支持多种混合样式的富文本 text.rich

混合展示样式与单一样式的关键区别在于分片,即如何把一段字符串分为几个片段来管理,给每个片段单独设置样式。在 flutter 中可以使用 textspan。

textspan 定义来一个字符串片段该如何控制其展示样式,而将这些有着独立展示样式的字符串组装在一起,则可以支持混合样式的富文本展示。

示例代码 - 分别定义黑色与红色两种展示样式

textstyle blackstyle = textstyle(fontweight: fontweight.normal, fontsize: 20, color: colors.black); // 黑色样式
    textstyle redstyle = textstyle(fontweight: fontweight.bold, fontsize: 20, color: colors.red); // 红色样式
    text.rich(
      textspan(
        children: <textspan>[
          textspan(text: '文本是视图系统中的常见空间,用来显示一段特定样式的字符串,就比如', style: redstyle), // 第 1 个片段,红色样式
          textspan(text: 'android', style: blackstyle), // 第 1 个片段,黑色样式
          textspan(text: '中的', style: redstyle), // 第 1 个片段,红色样式
          textspan(text: 'textview', style: blackstyle), // 第 1 个片段,黑色样式
        ]
      ),
    textalign: textalign.center,
);

图片

在 flutter 中有多张方式,用来加载不同形式、支持不同格式的图片:

  • 加载本地资源图片,如 image.asset('images/log.png');
  • 加载本地(file 文件)图片,如 image.file(new file('/storage/xxx/xxx/test.jpg'));
  • 加载网络图片,如 image.network('')。

除了可以根据图片的显示方式设置不同的图片源之外,图片的构造方法还提供了填充模式 fit、拉伸模式 centerslice、重复模式 repeat 等属性,可以针对图片与目标区域的宽高比差异制定排版模式。

fadeinimage 控件

在加载网络图片的时候,为了提升用户的等待体验,往往会加入占位图、加载动画等元素,但是默认的 image.network 构造方法并不支持这些高级功能,这时候 fadeinimage 控件就派上用场了。

fadeinimage 控件提供了图片占位的功能,并且支持在图片加载完成时淡入淡出的视觉效果。此外,由于 image 支持 gif 格式,甚至还可以将一些炫酷的加载动画作为占位图。

示例代码 - loading 的 gif 作为占位图展示:

fadeinimage.assetnetwork(
    placeholder: 'asssets/loading.gif', // gif 占位
    image: 'https://xxx/xxx/xxx.jpg',
    fit: boxfit.cover, // 图片拉伸模式
    width: 200,
    height: 200,
);

image 控件需要根据图片资源异步加载的情况,决定自身的显示效果,因此是一个 statefulwidget。图片加载过程由 imageprovider 触发,而 imageprovider 表示异步获取图片数据的操作,可以从资源、文件和网络等不同的渠道获取图片。

首先,imageprovider 根据 _imagesate 中传递的图片配置生成对应的图片缓存 key;然后,去 imagecache 中查找是否有对应的图片缓存,如果有,则通知 _imagestate 刷新 ui;如果没有,则启动 imagestream 开始异步加载,加载完毕后,更新缓存;最后通知 _imagesate 刷新 ui。

值得注意的是,imagecache 使用 lru(least recently used,最近最少使用)算法进行缓存更新策略,并且默认最多存储 1000 张图片,最大缓存限制为 100 mb,当限定的空间已经存满数据时,把最久没有被访问到的图片清除。图片 缓存只会在运行期间生效,也就是只缓存在内存中。如果想要支持缓存到文件系统,可以使用第三方的 cachednetworkimage 控件。

按钮

通过按钮,可以相应用户的交互事件。flutter 提供了三个基本的按钮空间,即 floatingactionbutton、flatbutton 和 raisedbutton。

  • floatingactionbutton:一个圆形的按钮,一般出现在屏幕内容的前面,用来处理界面中最常用、最基础的用户动作。
  • raisedbutton:凸起的按钮,默认带有灰色背景,被点击后灰色背景会加深。
  • flatbutton:扁平化的按钮,默认透明背景,被点击后呈现灰色背景。

既然是按钮,因此除了控制基本样式之外,还需要响应用户点击行为。这就对应着按钮空间中的两个最重要的参数:

  • onpressed 参数用于设置点击回调,告诉 flutter 在按钮被点击时通知我们。如果 onpressed 参数为空,则按钮会处于禁用状态,不响应用户点击。
  • child 参数用于设置按钮的内容,告诉 flutter 控件应该长成什么样,也就是控制着按钮控件的基本样式。child 可以接收任意的 widget。

除此之外,还可以进行样式定制(以 flatbutton 为例):

flatbutton(
    color: colors.yellow, // 设置背景色为黄色
    shape: beveledrectangleborder(borderradius: borderradius.circular(20.0)), // 设置斜角矩形边框
    colorbrightness: brightness.light, // 确保文字按钮为深色
    onpressed: () => print('flatbutton pressed'),
    child: row(children: <widget>[icon(icons.add), text("add")],),
);

listview

若相关基本元素的排列布局超过屏幕显示尺寸(即超过一屏)时,就需要引入列表控件来展示视图的完整内容,并根据元素的多少进行自适应滚动展示。

这样的需求,在 android 中是由 listview 或 recyclerview 实现的,在 ios 中是用 uitableview 实现的;而在 flutter 中,实现这种需求的则是列表空间 listview。

在 flutter 中,listview 可以沿着一个方向(垂直或水平方向)来排列其所有子 widget,因此常用于需要展示一组连续视图元素的场景,比如通讯录、优惠劵、商家列表等。

listview 提供了一个默认构造函数 listview,可以通过设置它的 children 参数,很方便地将所有的子 widget 包含到 listview 中。

但是,这种创建方式要求提前将所有的子 widget 一次性创建好,而不是等到它们真正在屏幕上需要显示时才创建,所以有一个很明显的缺点,就是性能不好。因此,这种方式仅适用于列表中含有少量元素的场景。

listview(
    scrolldirection: axis.horizontal, // 设置滚动方向
    children: <widget>[
        // 设置 listtile 组件的标题与图标
        listtile(leading: icon(icons.map), title: text('map'),),
        listtile(leading: icon(icons.mail), title: text('mail'),),
        listtile(leading: icon(icons.message), title: text('message'),),
    ],
);

**listview 的另外一个构造函数 listview.builder,则适用于子 widget 比较多的场景。这个构造函数有两个关键参数:

  • itembuilder,是列表项的创建方法。当列表滚动到相应位置时,listview 会调用该方法创建对应的子 widget。
  • itemcount,表示列表项的数量,如果为空,则表示 listview 为无限列表。

具体用法如下,定义一个拥有 100 个列表元素的 listview:

listview.builder(
    itemcount: 100, // 元素个数
    itemextent: 50.0, // 列表项高度
    itembuilder: (buildcontext context, int index) => listtile(title: text("title $index"), subtitle: text("body $index"),),
);

需要注意的是,itemextent并不是一个必填参数。但,对于定高的列表项元素,强烈建议提前设置好这个参数的值。

因为如果这个参数为 null,listview 会动态地根据子 widget 创建完成的结果,决定自身的视图高度,以及子 widget 在 listview 中的相对位置。在滚动发生变化而列表项又很多时,这样的计算就会非常频繁。

在 listview 中,有两种方式支持分割线:

  • 一种是,在 itembuilder 中,根据 index 的值动态创建分割线,也就是将分割线视为列表项的一部分;
  • 另一种是,使用 listview 的另一个构造方法 listview.separated,单独设置分割线的样式。

总结 listview 常见的构造方法及适用场景如下:

构造函数名 特点 适用场景 使用频次
listview 一次性创建好全部子 widget 适用于展示少量连续子 widget 的场景
list.builder 提供了子 widget 创建方法,仅在需要展示时才创建 适用于子 widget 较多,且视觉效果呈现某种规律性的场景
listview.separated 与 listview.builder 类似,并提供了自定义分割线的功能 与 listview.builder 场景类似

customscrollview

listview 实现了单一视图下可滚动 widget 的交互模式,同时也包含了 ui 显示相关的控制逻辑和布局模型。但是,对于某些特殊交互场景,比如多个效果联动、嵌套滚动、精细滑动、视图跟随手势操作等,还需要嵌套多个 listview 来实现。这时,各自视图的滚动和布局模型就是相互独立、分离的,就很难保证整个页面统一一致的滑动效果。

在 flutter 中有一个专门的控件 customscrollview,用来处理多个需要自定义滚动效果的 widget。在 customscrollview 中,这些彼此独立的、可滚动的 widget 被统称为 sliver。

比如,listview 的 sliver 实现为 sliverlist,appbar 的 sliver 实现为 sliverappbar。这些 sliver 不再维护各自的滚动状态,而是交由 customscrollview 统一管理,最终实现滑动效果的一致性。

可以通过一个滚动视差的例子,演示 customscrollview 的使用方法。

视差滚动是指让多层背景以不同的速度移动,在形成立体滚动效果的同时,还能保证良好的视觉体验。作为移动应用交互设计的热点趋势,越来越多的移动应用使用来这项技术。

以一个有着封面头图的列表为例,封面头图和列表这两层视图的滚动联动起来,当用户滚动列表时,头图会根据用户的滚动手势,进行缩小和展开。

经过分析得出,要实现这样的需求,需要两个 sliver:作为头图的 sliverappbar,作为列表的 sliverlist。思路如下:

  • 在创建 sliverappbar 时,把 flexiblespace 参数设置为悬浮头图背景。flexiblespace 可以让背景图显示在 appbar 下方,高度和 sliverappbar 一样;
  • 而在创建 sliverlist 时,通过 sliverchildbuilderdelegate 参数实现列表项元素的创建;
  • 最后,将它们一并交由 customscrollview 的 slivers 参数统一管理。

具体的示例代码如下:

    customscrollview (
      slivers: <widget>[
        sliverappbar( // sliverappbar 作为头图控件
          title: text('customscrollview demo'), // 标题
          floating: true, // 设置悬浮样式
          flexiblespace: image.network("https://xx.jpg", fit: boxfit.cover,), // 设置悬浮头图背景
          expandedheight: 300, // 头图控件高度
        ),
        sliverlist( // sliverlist 作为列表控件
          delegate: sliverchildbuilderdelegate(
              (context, index) => listtile(title: text('item #$index'),), //列表项创建方法
              childcount: 100, // 列表元素个数
          ),
        )
      ],
    );

scrollcontroller 与 scrollnotification

使用 scrollcontroller 进行滚动信息的监听,以及相应的滚动控制;scrollnotification 通知进行滚动事件的获取。

在 flutter 中,因为 widget 并不是渲染到屏幕的最终视觉元素(renderobject 才是),所以无法像原生的 android 或 ios 系统那样,向持有的 widget 对象获取或者设置最终渲染相关的视觉信息,而必须通过对应的组件控制器才能实现。

listview 的组件控制器则是 scrollcontroller,我们可以通过它来获取视图的滚动信息,更新视图的滚动位置。

一般而言,获取视图的滚动信息往往是为了进行界面的状态控制,因此 scrollcontroller 的初始化、监听及销毁需要与 statefulwidget 的状态保持同步。

代码示例所示,声明一个有着 100 个元素的列表项,当滚动视图到特定位置后,用户可以点击按钮返回列表顶部:

  • 首先,在 state 的初始化方法里,创建了 scrollcontroller,并通过 _controller.addlistener 注册了滚动监听方法回调,根据当前视图的滚动位置,判断当前是否需要展示“top”按钮。
  • 随后,在视图构建方法 build 中,将 scrollcontroller 对象与 listview 进行了关联,并且在 raisedbutton 中注册了对应的回调方法,可以在点击按钮时通过 _controller.animateto 方法返回列表顶部。
  • 最后,在 state 的销毁方法中,对 scrollcontroller 进行了资源释放。
class _myappstate extends state<myapp> {

  scrollcontroller _controller; // listview 控制器
  bool istotop = false; // 标示目前是否需要启用 "top" 按钮
  @override
  void initstate() {

    _controller = scrollcontroller();
    _controller.addlistener(() { // 为控制器注册滚动监听方法

      if (_controller.offset > 1000) { // 如果 listview 已经向下滚动了 1000,则启用 top 按钮
        setstate(() {
          istotop = true;
        });
      }
      else if (_controller.offset < 300) { // 如果 listview 向下滚动距离不足 300,则禁用 top 按钮
        setstate(() {
          istotop = false;
        });
        super.initstate();
      }
    });
  }

  widget build(buildcontext context) {
    return scaffold(
      appbar: appbar(title: text("scroll controller widget"),),
      body: column(
        children: <widget>[
          container(
            height: 40.0,
            child: raisedbutton(onpressed: (istotop ? () {
              if (istotop) {
                _controller.animateto(.0, duration: duration(milliseconds: 200), curve: curves.ease);
              }
            } : null), child: text("top"),),
          ),
          expanded(
            child: listview.builder(
                controller: _controller, // 初始化传入控制器
                itemcount: 100, // 列表元素总和
                itembuilder: (context, index) => listtile(title: text("index : $index"),) // 列表项构造方法
            ),
          )
        ],
      ),
    );
  }
  
  @override
  void dispose() {
    _controller.dispose(); // 销毁控制器
    super.dispose();
  }
}

在 flutter 中, scronotification 通知的获取是通过 notificationlistener 来实现的。与 scrollcontroller 不同的是,notificationlistener 是一个 widget,为了监听滚动类型的事件,我们需要将 notificationlistener 添加为 listview 的父容器,从而捕获 listview 中的通知。而这些通知,需要通过 onnotification 回调函数实现监听逻辑:

  widget build(buildcontext context) {
    return materialapp(
      title: 'scrollcontroller demo',
      home: scaffold(
        appbar: appbar(title: text('scrollcontroller demo')),
        body: notificationlistener<scrollnotification>(
          onnotification: (scrollnotification) {
            if (scrollnotification is scrollstartnotification) { // 开始滚动
              
            }
            else if ( scrollnotification is scrollupdatenotification) { // 滚动位置更新

            }
            else if (scrollstartnotification is scrollendnotification) { // 滚动结束
              
            }
          }, 
            child: listview.builder(itembuilder: (context, index) => listtile(title: text("index : $index"),));
        ), 
          
      ),
        
    );
    
  }

相比于 scrollcontroller 只能和具体的 listview 关联后才可以监听到滚动信息;通过 notificationlistener 则可以监听其子 widget 中的任意 listview,不仅可以得到这些 listview 的当前滚动位置信息,还可以获取当前的滚动事件信息。