控件拓展

Flutter Widgets 目录

Flutter 中提供了上百种控件,这些控件组成了Flutter绚丽的UI效果。在学习时,不可能将时间都花在学习控件上,这里仅选取较为常用的,可能会用到的控件进行专题学习。

可滚动控件

可滚动控件官方文档

Flutter 为我们提供了多种可滚动控件用于显示各种复杂的列表长布局

  • BoxScrollView子类系列
  • Sliver家族系列
  • 长布局SingleChildScrollView 控件
  • 其他列表系列

ScrollView 总览

ScrollView的类继承关系如下,其中ScrollViewBoxScrollView都是抽象类

  • CustomScrollView

    包含多个子布局模型。用于创建各种自定义滚动效果的ScrollView

  • BoxScrollView

    包含单个子布局模型。通常使用其两个子类创建UI

ScrollView 由三部分组成:

  1. Scrollable

    可滚动控件会直接或间接包含一个Scrollable控件,它主要用于监听各种用户手势并实现滚动的交互模型,但不包含UI显示相关的逻辑。

  2. Viewport

    指视口。即Widget在屏幕上的实际显示区域,通常滚动列表中会有无数的项,但只有该项滑动到视口区域才是可见的,滑出视口,则不可见。

  3. Sliver

    可以组合以创建各种滚动效果的小控件,如列表,网格和扩展标题。通常可滚动控件的子项会非常多,累积的总高度非常大,如果一次性将子控件全部构建出将会非常耗费性能,因此Flutter提出一个Sliver的(“薄片”)概念。如果可滚动控件支持Sliver模型,则该滚动可以将子控件分成多个“薄片”(Sliver),只有当Sliver出现在视口中时才会去构建它,这种模型也称为“基于Sliver的延迟构建模型”。如ListViewGridView都支持该模型

ScrollView 常见属性

属性名 类型 简述
scrollDirection Axis 滚动视图的滚动轴,即滚动的方向
reverse bool 是否逆向。本质上是决定可滚动控件的初始滚动位置是在“头”还是“尾”
controller ScrollController 控制滚动位置和监听滚动事件
primary bool 是否是与父级PrimaryScrollController关联的主滚动视图。当此值为true时,滚动视图是可滚动的,即使它没有足够的内容来实际滚动。否则,默认情况下,用户只能在视图有足够内容的情况下滚动视图。需注意,为true时,controller应为null
physics ScrollPhysics 决定可滚动控件如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示
shrinkWrap bool 是否根据正在查看的内容确定滚动视图的范围。如果在指定滚动轴上的滚动视图没有包裹内容,那么滚动视图将扩展到滚动轴允许的最大尺寸。如果滚动视图在滚动轴上没有边界约束,那么该属性必须为true。包裹滚动视图的内容比扩展到最大尺寸要更消耗性能,因为在滚动过程中,内容可以扩展和收缩,意味着每次滚动时,需要重新计算滚动视图的尺寸。
cacheExtent double 视口在可见区域之前和之后有一个区域,用于缓存用户滚动时即将可见的项目

physics参数的使用

  • BouncingScrollPhysics :允许滚动超出边界,但之后内容会反弹回来, iOS的原生效果
  • ClampingScrollPhysics : 防止滚动超出边界,滚动到列表末尾时有一个蓝色水波纹效果,Android的原生效果
  • AlwaysScrollableScrollPhysics :始终响应用户的滚动,即使没有足够的内容。
  • PageScrollPhysics:用于PageView使用的ScrollPhysics
  • FixedExtentScrollPhysics:仅适用于使用FixedExtendScrollControllers 的列表。表示仅滚动到子项而不存在任何偏移
  • NeverScrollableScrollPhysics :不响应用户的滚动,即禁用滚动。

ScrollController

  • jumpTo() 直接跳转到指定的位置
  • animateTo() 以动画方式跳转到指定的位置
  • addListener() 添加滚动监听
  • offset 获取当前的滚动位置
import 'package:flutter/material.dart';

class ScrollControllerTest extends StatefulWidget {
  @override
  ScrollControllerTestState createState() {
    return ScrollControllerTestState();
  }
}

class ScrollControllerTestState extends State<ScrollControllerTest> {
  ScrollController _controller =  ScrollController();

  @override
  void initState() {
    super.initState();
    //监听滚动事件,打印滚动位置
    _controller.addListener(() {
      print(_controller.offset);
    });
  }

  @override
  void dispose() {
    //避免内存泄露
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("滚动控制")),
      body: Scrollbar(
        child: ListView.builder(
            itemCount: 100,
            itemExtent: 50.0, //列表项高度固定时,显式指定高度(提高性能)
            controller: _controller,
            itemBuilder: (context, index) {
              return ListTile(title: Text("$index"),);
            }
        ),
      ),
      floatingActionButton: FloatingActionButton(
          child: Icon(Icons.arrow_upward),
          onPressed: () {
            //以动画方式返回到顶部
            _controller.animateTo(0.0, duration: Duration(milliseconds: 200),
                curve: Curves.ease
            );
          }
      ),
    );
  }
}

需要注意,一个ScrollController可以同时被多个Scrollable使用,ScrollController会为每一个Scrollable创建一个ScrollPosition对象,这些ScrollPosition保存在ScrollControllerpositions属性中(它是一个数组)。

如果ScrollController被设置给多个滚动控件使用,那么要获取某个滚动控件的offset ,可以使用如下方式

// 查看controller的offset属性实现
double get offset => position.pixels;

// 读取相关的滚动位置的方式
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels

Scrollbar

它是一个滚动条控件,如果要给可滚动控件添加滚动条,只需使用Scrollbar作为父控件包裹可滚动控件即可。

Scrollbar(
  child: ListView(
    ///...
  ),
);

ScrollConfiguration

用于控制可滚动小控件在子树中的行为方式。它实际上是继承自InheritedWidget,内部共享了一个ScrollBehavior对象。我们使用该功能控件的目的,其实就是为了设置它的ScrollBehavior对象,从而更改某些滚动行为。

通常的,我们可以使用该控件去除那种Android式的蓝色的列表回弹效果

class MyBehavior extends ScrollBehavior{
 @override
 Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
    if(Platform.isAndroid||Platform.isFuchsia){
     return child;
  }else{
    return super.buildViewportChrome(context,child,axisDirection);
  }
 }
}

/// 使用ScrollConfiguration包裹列表
ScrollConfiguration(
       behavior: MyBehavior(),
        child: ListView(),
);

滚动监听

我们可以使用NotificationListener来监听ScrollNotification事件,这方式比ScrollController中的监听更强大。

NotificationListener<ScrollNotification>(
  onNotification: (ScrollNotification notification) {
    print("onNotification: ${notification.metrics.pixels}");
    return true;
  },
  child: ListView.builder(
      itemCount: 100,
      itemExtent: 50.0,
      itemBuilder: (context, index) {
        return ListTile(title: Text("$index"));
      }
  ),
),

onNotification回调的参数类型为ScrollNotification,它包括一个metrics属性,该属性包含一些信息:

  • pixels:当前滚动位置
  • maxScrollExtent:最大可滚动长度
  • extentBefore:滑出视口顶部的长度
  • extentInside:视口内部长度。当没有内边距时,相当于列表的长度
  • extentAfter:列表中未滑入视口部分的长度
  • atEdge:是否滑到了可滚动控件的边界(相当于列表顶或底部)

常用滚动控件

这些控件提供了更简洁的使用方式,在实际开发中使用最多。

ListView

它的同名构造方法中有几个参数需要说明

  • shrinkWrap: 当为 false 时,列表会在主轴方向上扩展到可占用的最大空间,反之列表占用的空间是其列表项高度之和,此时会耗费更多性能,每当列表项发生变化时,都会重新计算高度

  • itemExtent :如果主轴是垂直方向,则代表的是子项的高度,如果主轴为水平方向,则代表的是子项的长度。指定 该值能提升性能,每当列表项发生变化时,都不需要重新计算

  • addAutomaticKeepAlives :表示是否将列表项包裹在AutomaticKeepAlive中。在一个懒加载列表里,如果子项需要保证自己在滑出视口时不被回收,就需要设置为 true,内保就会使用KeepAliveNotification来保存其状态。如果子项要自己维护其KeepAlive 状态,此参数必须置为false

  • addRepaintBoundaries :表示是否将列表项包裹在RepaintBoundary中。为 true 时,可以避免列表项重绘,提高性能。但当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加 RepaintBoundary 反而会更高效

ListView配套使用的,还有一个ListTile控件,用于列表的子项,当然也可以单独使用

属性名 类型 简述
leading Widget 列表项左侧的图标
title Widget 标题,通常放置文本控件
subtitle Widget 副标题
trailing Widget 列表项右侧的图标
isThreeLine bool 内容是否可显示3行。为true,副标题最多显示2行
dense bool 是否密集显示。为true时,内容及图标将会变小,显示更紧密
contentPadding EdgeInsetsGeometry 内边距
enabled bool 设为 false,可禁止点击事件
onTap GestureTapCallback 单击事件回调
onLongPress GestureLongPressCallback 长按事件回调
selected bool 是否选中。为true时,文本和图标的颜色变为主题的主色

GridView

主要构建网格视图。在用法上与ListView非常相似,可参照其用法。

该控件主要有一个gridDelegate参数需要设置,该参数类型是SliverGridDelegate,用于控制子项如何排列。该类是一个抽象类,Flutter框架给我们提供了两个子类实现

  • SliverGridDelegateWithFixedCrossAxisCount
  • SliverGridDelegateWithMaxCrossAxisExtent

GridView配套的,也有一个GridTile控件,用于网格的子项,它带有页眉和页脚的显示功能。其headerfooter参数通常使用GridTileBar控件,该控件属性与ListTile有些类似,可参照ListTile

GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2
  ),
  itemBuilder: (context, index) {
    return GridTile(
      header: GridTileBar(
        backgroundColor:Color.fromRGBO(0, 0, 0, 0.4),
        title: Text('Header'),),
      child: Container(
        child: Image.network(
          'https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/fbb.jpg',
          fit: BoxFit.cover,),
      ),
      footer: GridTileBar(
        backgroundColor:Color.fromRGBO(0, 0, 0, 0.6),
        title: Text('Footer'),),
    );
  },
  itemCount: 20,
)

需要注意,框架的GridView默认子元素显示空间是相等的,但在实际开发中,可能会遇到子元素大小不等的情况,这里推荐一个第三方库实现

flutter_staggered_grid_view

PageView

用于页面滑动的控件,子项会占据当前屏幕的所有可视区域。它有三种构造方式

  • PageView()
  • PageView.builder()
  • PageView.custom()

它的主轴方法默认是水平方向,通常的我们也将之用于左右滑动切换,需要注意,我们也能将其主轴设置为垂直方向,用作列表式的上下滚动。

SingleChildScrollView

该控件通常用于长布局的滚动。它只能包含一个子项,通常只应在期望的内容不会超过屏幕太多时使用,因为它不支持基于Sliver的延迟实例化模型,所以包含的长布局超出屏幕尺寸太多时,性能会变差,此时应该考虑使用一些支持Sliver延迟加载的可滚动控件,如ListView之类。

如果非要使用SingleChildScrollView来实现一个短列表,可以使用SingleChildScrollView + ListBody组合的方式实现。ListBody是一个依照给定的轴方向,并按照顺序排列子元素的控件。

Sliver 家族

CustomScrollView是一种可以使用Sliver来自定义滚动模型(效果)的控件,它可以包含多种滚动模型。具体的,是在它的slivers属性里放置各种Sliver 系列控件。

Sliver通常指可滚动控件的子元素(像一个个薄片),即那些需要粘合起来的可滚动控件就Sliver。但在CustomScrollView中,如果直接将ListViewGridView作为Sliver是不行的,因为它们本身就是可滚动控件而不是Sliver。为了能让可滚动控件和CustomScrollView配合使用,Flutter为我们提供了一些Sliver版可滚动控件,如SliverListSliverGrid等等。

Sliver版的可滚动控件和非Sliver版的区别主要是前者不包含滚动模型(自身不能再滚动),而后者包含滚动模型 ,这些Sliver共用CustomScrollViewScrollable,最终实现统一的滑动效果

SliverAppBar

它类似于Android中的CollapsingToolbarLayout,可轻松实现页面头部可伸缩效果,且与AppBar的大部分的属性重合,很像AppBar的加强版。需要注意,它通常要结合 CustomScrollView 或者 NestedScrollView 来使用,且作为第一个Sliver元素。

AppBar相比,SliverAppBar 特有的属性

属性名 类型 简述
forceElevated bool 结合 elevation 使用,当elevation 不为 0 时,表示是否显示阴影
expandedHeight double 展开时的高度
floating bool true时下滑,则AppBar优先滑动展示,展示完成后才给滑动控件滑动
snap bool true时则 floating 也要为 true 。会根据手指松开的位置展开或者收缩AppBar
pinned bool appBar 收缩到最小高度的时候是否可见

示例

Scaffold(
  appBar: AppBar(title: Text("滚动列表")),
  body: CustomScrollView(
      slivers:<Widget>[
        SliverAppBar(
          floating:true,
          pinned: true,
          snap: false,
          expandedHeight: 250.0,
          flexibleSpace: FlexibleSpaceBar(
            title: const Text('一人之下'),
            background: Image.network(
                "https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/timg.jpg",fit: BoxFit.cover,),
          ),
        ),
        SliverFixedExtentList(
          itemExtent: 50.0,
          delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
                return ListTile(
                  title: Text("item $index"),
                );
              },
              childCount: 50
          ),
        ),
      ]
  ),
);

这里FlexibleSpaceBar 是一个具有折叠功能的控件,它的collapseMode属性有三个值,可以实现一些折叠效果

  • CollapseMode.none 背景不跟随滚动
  • CollapseMode.parallax 背景滚动,且具有一些收缩效果
  • CollapseMode.pin 背景跟随滚动

另外,我们还可以在SliverAppBarbottom属性中组合TabBar来使用。

SliverPadding

相当于Sliver 系列的Padding控件,可以给Sliver 系列控件加边距。如果在Sliver 系列中直接使用Padding会报错

SliverPadding(
  padding: const EdgeInsets.all(8.0),
  sliver: SliverAppBar(
    /// ......
  ),
)

SliverSafeArea

SafeArea功能相同,区别在于该控件是用于Sliver中,包装Sliver系列的控件。

SliverList

它类似于ListView,有两种表现形式

  • SliverChildBuilderDelegate 用于加载不确定数量的列表
  • SliverChildListDelegate 只能加载固定的已知数量的列表
      SliverList(
        delegate: SliverChildBuilderDelegate(
                    (context, index){
                      return ListTile(title: Text('item ${index+1}'),);
                    }, 
                   childCount: 10),
      )

      /// -------------------------------------------------
      SliverList(
        delegate: SliverChildListDelegate([
              ListTile(title: Text('item 1'),),
              ListTile(title: Text('item 2'),),
              ListTile(title: Text('item 3'),),
            ]),
       )

SliverFixedExtentList

SliverList 多一个itemExtent 属性,可用于固定 item 的高度 ,item 里面的子控件无法再改动高度。

SliverGrid

类似于GridView,它有三个构造函数

  • SliverGrid.count() 指定了一行展示几列 item

      /// 一行有4列
      SliverGrid.count(children: scrollItems, crossAxisCount: 4)
    
  • SliverGrid.extent() 指定item的最大宽度,然后让框架自动计算一行展示几列

      /// 任一列最大宽度为80
      SliverGrid.extent(children: scrollItems, maxCrossAxisExtent: 80.0)
    
  • SliverGrid() 自定义item排列方式

      SliverGrid(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: products.length,
        ),
        delegate: SliverChildBuilderDelegate(
          (BuildContext context, int index) {
            return _buildItem(products[index]);;
          }
      );
    

自定义SliverGrid的item展示方式,需要设置gridDelegate参数,该参数有两种选项

  • SliverGridDelegateWithMaxCrossAxisExtent

    /// 根据给定的 maxCrossAxisExtent 宽度自动分配一行展示多少列
    SliverGridDelegateWithMaxCrossAxisExtent(
                    maxCrossAxisExtent: 150,
                    mainAxisSpacing: 10.0, //主轴中间间距
                    crossAxisSpacing: 10.0, //交叉轴中间间距
                    childAspectRatio: 2.0, //item 宽高比
    ),
    
  • SliverGridDelegateWithFixedCrossAxisCount

    /// 固定一行展示多少列
    SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 4,
                    mainAxisSpacing: 10.0, //主轴中间间距
                    crossAxisSpacing: 10.0, //副轴中间间距
                    childAspectRatio: 2.0, //item 宽高比
    )
    

综合示例

CustomScrollView(
    slivers:<Widget>[
        /// 首先放一个SliverAppBar
        SliverAppBar(
          floating:true,
          pinned: true,
          snap: false,
          expandedHeight: 250.0,
          flexibleSpace: FlexibleSpaceBar(
            title: const Text('一人之下'),
            background: Image.network(
                "https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/timg.jpg",fit: BoxFit.cover,),
          ),
        ),

      /// 中间放一个SliverGrid
      SliverGrid(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          mainAxisSpacing: 10.0,
          crossAxisSpacing: 10.0,
          childAspectRatio: 4.0,
        ),
        delegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) {
            return Container(
              alignment: Alignment.center,
              color: Colors.yellow[100 * (index % 9)],
              child:GridTile(
                  child:Text('grid tile $index')
              ),
            );
          },
          childCount: 30,
        ),
      ),
      /// 最后放一个SliverFixedExtentList
      SliverFixedExtentList(
        itemExtent: 50.0,
        delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
              return ListTile(
                title: Text("item $index"),
              );
            },
            childCount: 50 //50个列表项
        ),
      ),
    ]
),

SliverPersistentHeader

SliverAppBar实际上就是封装了SliverPersistentHeader,是其的简化版本。与SliverAppBar相比,该控件可以出现在Sliver的任何位置,不一定是头部,且还能实现更多复杂的效果。

要使用SliverPersistentHeader,主要是需要一个SliverPersistentHeaderDelegate类型的参数,由于该类是一个抽象类,且没有暴露出具体的子类实现,因此我们只能自定义一个类继承自SliverPersistentHeaderDelegate,并重写四个方法

  • minExtent:收起状态下控件的高度
  • maxExtent:展开状态下控件的高度
  • shouldRebuild:是否重新构建,一般默认返回true
  • build:构建要显示的UI。其中shrinkOffset参数表示从maxExtentminExtent的距离,当shrinkOffset为0时表示完全展开
class SliverAppBarDelegate extends SliverPersistentHeaderDelegate {

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return null;
  }
  @override
  double get maxExtent => null;

  @override
  double get minExtent => null;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    return true;
  }
}

Sliver头根据上下滑动实现背景渐变的过渡效果,具体示例

import 'package:flutter/material.dart';

class CustomSliverHeader extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: <Widget>[
          SliverPersistentHeader(
            pinned: true,
            delegate: SliverMyHeaderDelegate(
                title: '一人之下',
                appBarHeight: 60,
                expandedHeight: 300,
                paddingTop: MediaQuery.of(context).padding.top,
                background: Image.network("https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/timg.jpg",fit: BoxFit.cover,)
            ),
          ),
          SliverFillRemaining(
            child: Container(
              padding: const EdgeInsets.all(16),
              color: Colors.grey[300],
              child: Text('这是一段文章'),
            ),
          )
        ],
      ),
    );
  }
}

class SliverMyHeaderDelegate extends SliverPersistentHeaderDelegate {
  final double appBarHeight;
  final double expandedHeight;
  final double paddingTop;
  final Widget background;
  final String title;

  SliverMyHeaderDelegate({
    this.appBarHeight,
    this.expandedHeight,
    this.paddingTop,
    this.background,
    this.title,
  });

  @override
  double get minExtent => this.appBarHeight + this.paddingTop;

  @override
  double get maxExtent => this.expandedHeight;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    return true;
  }

  Color _updateAppBarColor(shrinkOffset) {
    final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255).clamp(0, 255).toInt();
    return Color.fromARGB(alpha, 255, 255, 255);
  }

  Color _updateAppBarTitleColor(shrinkOffset, isIcon) {
    if(shrinkOffset <= 50) {
      return isIcon ? Colors.white : Colors.transparent;
    } else {
      final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255).clamp(0, 255).toInt();
      return Color.fromARGB(alpha, 0, 0, 0);
    }
  }

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return SizedBox(
      height: maxExtent,
      width: MediaQuery.of(context).size.width,
      child: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          background,
          Positioned(
            left: 0,
            right: 0,
            top: 0,
            child: Container(
              color: _updateAppBarColor(shrinkOffset),
              child: SafeArea(
                child: SizedBox(
                  height: appBarHeight,
                  child: Row(
                    children: <Widget>[
                      IconButton(
                        icon: Icon(
                          Icons.arrow_back_ios,
                          color: _updateAppBarTitleColor(shrinkOffset, true),
                        ),
                      ),
                      Text(
                        title,
                        style: TextStyle(
                          fontSize: 20,
                          color: _updateAppBarTitleColor(shrinkOffset, false),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

SliverToBoxAdapter

如果想要在Sliver系列的可滚动视图中添加一个非Sliver控件,就可以使用SliverToBoxAdapter来包装,将各种其他的控件组合在一起。

SliverFillRemaining

可创建一个填充视口中剩余空间的Sliver控件。

SliverFillViewport

创建包含多个框状子项的Sliver控件,每个子项都会填充视口。它会将其子项沿主轴放置在线性数组中。

SliverFillViewport(
  delegate: SliverChildBuilderDelegate(
          (BuildContext context, int index) {
        return  Container(
          color: Colors.brown[200],
          child: Text('SliverFillViewport item $index'),
        );
      }, childCount: 5
  ),
  viewportFraction:1.0,//占屏幕的比例
),

滑动嵌套

NestedScrollView

该控件是一个可以实现滑动嵌套的 ScrollView。有些时候,我们可能需要将一个列表嵌套入另一个可滚动控件中作为子项,这时候可能会存在滑动冲突,而NestedScrollView则可以作为父布局实现嵌套。

import 'package:flutter/material.dart';

class NestedScrollViewTest extends StatefulWidget {
  @override
  NestedScrollViewTestState createState() {
    return NestedScrollViewTestState();
  }
}

class NestedScrollViewTestState extends State<NestedScrollViewTest>
    with SingleTickerProviderStateMixin {
  TabController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TabController(length: 2, vsync: this);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return <Widget>[
            SliverAppBar(
              floating: true,
              pinned: true,
              snap: true,
              expandedHeight: 250.0,
              flexibleSpace: FlexibleSpaceBar(
                title: const Text('一人之下'),
                background: PageView(
                  children: <Widget>[
                    Image.network(
                      "https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/timg.jpg",
                      fit: BoxFit.cover,
                    ),
                    Image.network(
                      "https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/fbb.jpg",
                      fit: BoxFit.cover,
                    )
                  ],
                ),
              ),
            ),
            SliverPersistentHeader(
              pinned: true,
              delegate: StickyTabBarDelegate(
                child: TabBar(
                  labelColor: Colors.black,
                  controller: _controller,
                  tabs: <Widget>[
                    Tab(text: '标签1'),
                    Tab(text: '标签2'),
                  ],
                ),
              ),
            ),
          ];
        },
        body: TabBarView(
          controller: _controller,
          children: <Widget>[
            _buildTabPage(0),
            _buildTabPage(1),
          ],
        ),
      ),
    );
  }

  _buildTabPage(int index) {
    if (index == 0) {
      return ListView.builder(
          itemExtent: 60,
          itemBuilder: (ctx, index) {
            return ListTile(
              title: Text("item $index"),
            );
          });
    } else {
      return Container(
        alignment: Alignment.center,
        child: Text("这是一个新的Tab页"),
      );
    }
  }
}

/// 吸附式TabBar效果
class StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
  final TabBar child;

  StickyTabBarDelegate({@required this.child});

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: Theme.of(context).backgroundColor,
      child: this.child,
    );
  }

  @override
  double get maxExtent => this.child.preferredSize.height;

  @override
  double get minExtent => this.child.preferredSize.height;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    return true;
  }
}

其他滚动控件

ListWheelScrollView

它是一个带滚筒效果的ListView,用法上也和ListView相似。它有两种构造方式

  • ListWheelScrollView()
  • ListWheelScrollView.useDelegate()
ListWheelScrollView.useDelegate(
  itemExtent: 80,
  childDelegate: ListWheelChildBuilderDelegate(
      builder: (context, index) {
        return Container(
          margin: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
          color: Colors.primaries[index % 10],
          alignment: Alignment.center,
          child: Text('$index'),
        );
      },
      childCount: 50),
)

ListWheelScrollView 常见属性

属性名 类型 简述
diameterRatio double 圆筒直径和主轴窗口的高度比。值越小越小表示圆筒越圆
perspective double 取值在(0,0.01]之间。为0时表示从无限远处看圆筒,0.01表示无限近处看
offAxisFraction double 表示车轮水平偏离中心的程度
useMagnifier bool 是否启用放大镜
magnification double 放大倍率,与useMagnifier配合使用
squeeze double 挤压程度。值越大,一屏展示的子项越多,也越挤压。
onSelectedItemChanged ValueChanged<int> 选中回调

ExpansionPanelList

是一个子项可展开的列表控件。

class ExpansionTest extends StatefulWidget {
  @override
  ExpansionTestState createState() {
    return ExpansionTestState();
  }
}

class ItemData{
  ItemData(this.index,this.isExpanded);

  int index;
  bool isExpanded;
}

class ExpansionTestState extends State<ExpansionTest> {
  List<ItemData> dataList ;

  @override
  void initState() {
    super.initState();
    dataList = List.generate(30, (i){
      return ItemData(i,false);
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: SingleChildScrollView(
          child: ExpansionPanelList(
            expansionCallback: (index, isExpanded) {
              setState(() {
                dataList[index].isExpanded = !isExpanded;
              });
            },
            children: dataList.map((value) {
              return ExpansionPanel(
                isExpanded: value.isExpanded,
                headerBuilder: (context, isExpanded) {
                  return ListTile(
                    title: Text('子项 - ${value.index}'),
                  );
                },
                body: Container(
                  alignment: Alignment.center,
                  height: 80,
                  color: Colors.grey[200],
                  child: Text("这是展开的内容"),
                ),
              );
            }).toList(),
          ),
        ),
      ),
    );
  }
}

ExpansionTile

是一个折叠菜单列表,严格的说,它并不是一个可滚动的列表。

ExpansionTile(
    title: Text('折叠菜单'),
    leading: Icon(Icons.label, color: Colors.lightBlue),
    backgroundColor: Colors.white,
    initiallyExpanded: false, /// 是否默认展开
    children: <Widget>[
      ListTile(
          title:Text('这是主标题1'),
          subtitle:Text('这是副标题1')
      ),
      ListTile(
          title:Text('这是主标题2'),
          subtitle:Text('这是副标题2')
      )
    ]
)

滚动相关控件小结

  • ListView
  • GridView
  • PageView
  • SingleChildScrollView
  • NestedScrollView
  • CustomScrollView
  • Scrollbar
  • NotificationListener
  • ScrollConfiguration
  • ListWheelScrollView
  • ExpansionPanelList
  • AnimatedList

其他控件

RefreshIndicator

Material Design下拉刷新指示器,用于包装一个可滚动控件。

List _data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: RefreshIndicator(
      onRefresh: () async {
        setState(() {
          _data = List.from(_data.reversed);
        });
      },
      child: ListView.builder(
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('item ${_data[index]}'),
          );
        },
        itemExtent: 50,
        itemCount: _data.length,
      ),
    ),
  );
}

CupertinoSliverRefreshControl

ios风格的下拉刷新控件。它和RefreshIndicator稍有不同,它需要放在CustomScrollView中使用。

List _data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: SafeArea(
      child: CustomScrollView(
        physics: BouncingScrollPhysics(),
        slivers: <Widget>[
          CupertinoSliverRefreshControl(
            onRefresh: () async {
              setState(() {
                _data = List.from(_data.reversed);
              });
            },
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate((content, index) {
              return ListTile(
                title: Text('item ${_data[index]}'),
              );
            }, childCount: _data.length),
          )
        ],
      ),
    ),
  );
}

Flutter框架没有提供上拉加载功能,我们通过滚动监听也可以自行实现。这里推荐使用第三方库

GridPaper

可用于绘制一个像素宽度的网格背景,结合Stack可以实现表格效果。

Container(
  constraints: BoxConstraints.expand(),
  child: GridPaper(
    color: Colors.red,
    divisions: 1,
    subdivisions: 4,
    child: Text("这是 GridPaper 的显示效果"),
  ),
)

Form

通常在向服务器提交数据前,都会对各个输入框数据进行合法性校验,但是对每一个TextField都分别进行校验将会是一件很麻烦的事。有时候用户想清除一组TextField的内容,只能一个个清除非常麻烦。为此,Flutter提供了一个Form 组件,它可以对输入框进行分组,然后进行一些统一操作,如内容校验、输入框重置以及输入内容保存。

由于Form的子孙元素必须是FormField类型,为了方便使用,Flutter提供了一个TextFormField组件,它继承自FormField类,也是TextField的一个包装类。

var _account = '';
var _pwd = '';
final _formKey = GlobalKey<FormState>();


Form(
  key: _formKey,
  child: Column(
    children: <Widget>[
      TextFormField(
        decoration: InputDecoration(hintText: '请输入账号'),
        onSaved: (value) {
          _name = value;
        },
        validator: (String value) {
          return value.length >= 6 ? null : '最少6个字符';
        },
      ),
      TextFormField(
        decoration: InputDecoration(hintText: '请输入密码'),
        obscureText: true,
        onSaved: (value) {
          _pwd = value;
        },
        validator: (String value) {
          return value.length >= 6 ? null : '最少6个字符';
        },
      ),
      RaisedButton(
        child: Text('登录'),
        onPressed: () {
          var _state = _formKey.currentState;
          if(_state.validate()){
            _state.save();
            login(_name,_pwd);
          }
        },
      )
    ],
  ),
)

公众号“编程之路从0到1”

20190301102949549

Copyright © Arcticfox 2020 all right reserved,powered by Gitbook文档修订于: 2022-05-01 12:00:54

results matching ""

    No results matching ""