阅读 1813

【-Flutter 组件-】ListWheelViewport 组件介绍

秉承着 有对象,用对象;没对象,找对象;找不到,造对象 的思想方针,终于将 ListWheelViewport 组件跑起来了。以前由于认知的局限,一直没能玩转ListWheelViewport,如今,确实成长了一些。此组件已收录于 FlutterUnit ,目前收录组件已破 310+,可喜可贺,欢迎 star 。

先看一下 ListWheelViewport 的基本信息:

源码位置:   flutter/lib/src/widgets/list_wheel_scroll_view.dart
父类:	     RenderObjectWidget 
相关组件:   ListWheelScrollView、CupertinoPicker、CupertinoDatePicker
复制代码

ListWheelViewport 可以实现如下的滚动视口效果,可能你用过 Cupertino 风格的选择器,觉得很类似。不错,它们的底层都有 ListWheelViewport 的参与。了解 ListWheelViewport后,其他的都是弟弟。

2020年12月19日14-25-39

2020年12月19日16-20-18


一、 ListWheelViewport 三个必须属性

属性名类型默认值介绍
itemExtentdoublerequired主轴方向 item 尺寸
offsetViewportOffsetrequired视口偏移
childDelegateListWheelChildDelegaterequired孩子代理构造器

下面先用一个最简的 demo 来测试一下 ListWheelViewport 的使用。上面的三个属性是必须给出的。
其中 itemExtent 是最简单的,代表 主轴方向 item 尺寸。如下轮子上下滑滚动 ,主轴就是 Y 轴,itemExtent 就表示每个 item 的高度

滚轮

childDelegate 属性是 ListWheelChildDelegate 类型的,其为抽象类,实现类有如下三个,
其中: ListWheelChildListDelegate 接受 List<Widget> 进行展示。
ListWheelChildBuilderDelegate 通过 builder 构造器创建 item。
ListWheelChildLoopingListDelegate 是可以无限滑动的列表,接受 List<Widget> 进行展示。

image-20201219135526757

offset属性需要传入 ViewportOffset 对象,这个对象造是很难造出来。不过凭借着之前的经验知道,这个对象可以通过 Scrollable 中获得。在 viewportBuilder 属性赋值时,可以回调 ViewportOffset 对象。

typedef ViewportBuilder = Widget Function(BuildContext context, ViewportOffset position);

class Scrollable extends StatefulWidget {
  const Scrollable({
    Key? key,
    this.axisDirection = AxisDirection.down,
    this.controller,
    this.physics,
    required this.viewportBuilder, //<---- viewportBuilder
    this.incrementCalculator,
    this.excludeFromSemantics = false,
    this.semanticChildCount,
    this.dragStartBehavior = DragStartBehavior.start,
    this.restorationId,
复制代码

使用将这几个要素合在一起,就可以将 ListWheelViewport 用起来。代码如下:

class ListWheelViewportDemo extends StatelessWidget {
  final List<Color> data = [
    Colors.blue[50], Colors.blue[100], Colors.blue[200],
    Colors.blue[300], Colors.blue[400], Colors.blue[500],
    Colors.blue[600], Colors.blue[700], Colors.blue[800],
    Colors.blue[900], Colors.blue[800], Colors.blue[700],
    Colors.blue[600], Colors.blue[500], Colors.blue[400],
    Colors.blue[300], Colors.blue[200], Colors.blue[100],
  ];

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 250,
      width: 320,
      child: Scrollable(
          axisDirection: AxisDirection.down,
          physics: BouncingScrollPhysics(),
          dragStartBehavior: DragStartBehavior.start,
          viewportBuilder: (ctx, position) => ListWheelViewport(
                itemExtent: 50,
                offset: position,
                childDelegate: ListWheelChildLoopingListDelegate(
                    children: data.map((e) => _buildItem(e)).toList()),
              )),
    );
  }

  Widget _buildItem(Color color) => Container(
        alignment: Alignment.center,
        color: color,
        child: Text(colorString(color),
            style: TextStyle(color: Colors.white, shadows: [
              Shadow(color: Colors.black, offset: Offset(.5, .5), blurRadius: 2)
            ])),
      );

  String colorString(Color color) =>
      "#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
}
复制代码
  • 看下 itemExtent 属性的作用
itemExtent = 80itemExtent = 100
image-20201219141001629image-20201219141041934

二、 perspective、squeeze、diameterRatio 效果属性

属性名类型默认值介绍
perspectivedouble0.003透视参数 0~0.01
squeezedouble1.0挤压值
diameterRatiodouble2.0直径分率

1. perspective 属性

perspective透视的意思,默认是 0.003 ,取值范围在 0~0.01 之间。

---->[RenderListWheelViewport]----
static const double defaultPerspective = 0.003;
复制代码
  • 这是 perspective:0.01 的效果

2020年12月19日14-25-39

  • 这是 perspective:0.001 的效果,可见 perspective 数值越大,透视效果越强。

2020年12月19日14-22-33


2. squeeze 属性

squeeze挤压的意思,默认是 1.0

  • 这是squeeze:0.8 的效果

2020年12月19日14-31-20

  • 这是squeeze:1.5 的效果,可见 squeeze 可以控制 item 的松散程度

2020年12月19日14-55-38


3. diameterRatio 属性

diameterRatio : 圆柱直径与主轴视口大小比率。

diameterRatio = 2diameterRatio = pi/2
image-20201219155954632image-20201219160026558

三、 其他属性

属性名类型默认值介绍
magnificationdouble1.0放大比例
useMagnifierboolfalse是否放大
clipBehaviorClipClip.hardEdge剪裁行为
renderChildrenOutsideViewportboolfalse出视野是否渲染
offAxisFractiondouble0.0轴中心偏移比
overAndUnderCenterOpacitydouble1放大器之外的透明度

1. 放大效果

该组件自带如下 放大效果,通过 magnificationuseMagnifier 控制。

2020年12月19日16-20-18

@override
Widget build(BuildContext context) {
  return Container(
    height: 250,
    width: 320,
    child: Scrollable(
        axisDirection: AxisDirection.down,
        physics: BouncingScrollPhysics(),
        dragStartBehavior: DragStartBehavior.start,
        viewportBuilder: (ctx, position) => ListWheelViewport(
          perspective: 0.008,
          squeeze: 1,
          diameterRatio: 2,
          itemExtent: 50,
          useMagnifier: true,
          magnification: 2,
          offset: position,
          childDelegate: ListWheelChildLoopingListDelegate(
              children: data.map((e) => _buildItem(e)).toList()),
        )),
  );
}
复制代码

2. 出界渲染与裁剪

默认情况下,item 不在视野区域内不会渲染,可以通过 renderChildrenOutsideViewport:true 让其显示,注意 此时 clipBehavior 必须为 Clip.none 。效果如下:

2020年12月19日16-39-21

@override
Widget build(BuildContext context) {
  return Container(
    height: 250,
    width: 320,
    child: Scrollable(
        axisDirection: AxisDirection.down,
        physics: BouncingScrollPhysics(),
        dragStartBehavior: DragStartBehavior.start,
        viewportBuilder: (ctx, position) => ListWheelViewport(
          perspective: 0.008,
          squeeze: 1,
          diameterRatio: 2,
          renderChildrenOutsideViewport: true,
          clipBehavior: Clip.none,
          itemExtent: 50,
          offset: position,
          childDelegate: ListWheelChildLoopingListDelegate(
              children: data.map((e) => _buildItem(e)).toList()),
        )),
  );
}
复制代码

3. offAxisFractionoverAndUnderCenterOpacity 属性

offAxisFraction: 0.2 效果

2020年12月19日17-19-04

overAndUnderCenterOpacity:0.4 效果

2020年12月19日17-47-14


四、 基于 ListWheelViewport 实现的组件们

1. ListWheelScrollView 组件

底层基于 _FixedExtentScrollable(Scrollable子类)ListWheelViewport 实现,此组件除了视口之外,还额外拥有监听滑动 item 的能力。如下,在上面的小圆颜色有下面滚轮滑动时选中色决定。ListWheelViewport 的相关属性,在 ListWheelScrollView 中效果是一致的。

2020年12月19日17-55-47

class CustomListWheelScrollView extends StatefulWidget {
  @override
  _CustomListWheelScrollViewState createState() =>
      _CustomListWheelScrollViewState();
}

class _CustomListWheelScrollViewState extends State<CustomListWheelScrollView> {
  var data = <Color>[
    Colors.orange[50],  Colors.orange[100], Colors.orange[200],
    Colors.orange[300], Colors.orange[400], Colors.orange[500],
    Colors.orange[600], Colors.orange[700], Colors.orange[800],
    Colors.orange[900]  ];

  Color _color = Colors.blue;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        _buildCircle(),
        Container(
          height: 150,
          width: 300,
          child: ListWheelScrollView.useDelegate(
            childDelegate: ListWheelChildLoopingListDelegate(
                children: data.map((e) => _buildItem(e)).toList()),
            perspective: 0.006,
            itemExtent: 50,
            onSelectedItemChanged: (index) {
              setState(() => _color = data[index]);
            },
          ),
        ),
      ],
    );
  }

  Widget _buildCircle() => Container(
        margin: EdgeInsets.only(bottom: 5),
        width: 30,
        height: 30,
        decoration: BoxDecoration(color: _color, shape: BoxShape.circle),
      );

  Widget _buildItem(Color color) {
    return Container(
      key: ValueKey(color),
      alignment: Alignment.center,
      height: 50,
      color: color,
      child: Text(
        colorString(color),
        style: TextStyle(color: Colors.white, shadows: [
          Shadow(color: Colors.black, offset: Offset(.5, .5), blurRadius: 2)
        ]),
      ),
    );
  }

  String colorString(Color color) =>
      "#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
}
复制代码

2. CupertinoPicker 组件

CupertinoPicker 的内部源码实现依赖于 ListWheelScrollView。所以最终的效果还是ListWheelViewport 的功劳。

2020年12月19日19-23-50

class CustomCupertinoPicker extends StatelessWidget {
  final names = [
    'Java', 'Kotlin', 'Dart',
    'Swift', 'C++', 'Python',
    "JavaScript", "PHP", "Go", "Object-c"
  ];

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 150,
      width: 300,
      child: CupertinoPicker(
          backgroundColor: CupertinoColors.systemGrey.withAlpha(33),
          diameterRatio: 1,
          offAxisFraction: 0.2,
          squeeze: 1.5,
          itemExtent: 40,
          onSelectedItemChanged: (position) {
            print('当前条目  ${names[position]}');
          },
          children: names.map((e) => Center(child: Text(e))).toList()),
    );
  }
}
复制代码

3. CupertinoDatePicker 组件

CupertinoDatePicker 内部是基于 CupertinoPicker 实现的。

2020年12月19日19-33-55

class CustomCupertinoDatePicker extends StatefulWidget {
  @override
  _CustomCupertinoDatePickerState createState() =>
      _CustomCupertinoDatePickerState();
}

class _CustomCupertinoDatePickerState extends State<CustomCupertinoDatePicker> {
  DateTime _date = DateTime.now();

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 350,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Text(
            '当前日期:${_date.toIso8601String()}',
            style: TextStyle(color: Colors.grey, fontSize: 16),
          ),
          _buildInfoTitle('CupertinoDatePickerMode.dateAndTime'),
          buildPicker(CupertinoDatePickerMode.dateAndTime),
        ],
      ),
    );
  }

  Container buildPicker(CupertinoDatePickerMode mode) {
    return Container(
      margin: EdgeInsets.all(10),
      height: 150,
      child: CupertinoDatePicker(
        mode: mode,
        initialDateTime: DateTime.now(),
        minimumYear: 2018,
        maximumYear: 2030,
        use24hFormat: false,
        minuteInterval: 1,
        backgroundColor: CupertinoColors.white,
        onDateTimeChanged: (date) {
          print(date);
          setState(() => _date = date);
        },
      ),
    );
  }

  Widget _buildInfoTitle(info) {
    return Padding(
      padding: const EdgeInsets.only(left: 20, top: 20, bottom: 5),
      child: Text(
        info,
        style: TextStyle(
            color: Colors.blue, fontSize: 16, fontWeight: FontWeight.bold),
      ),
    );
  }
}
复制代码

4. 小结一下

这样来看 滚轮 相关的组件,追其本源都与 ListWheelViewport 相关。所以认识了 ListWheelViewport 各属性的意义,则其他衍生出来的组件就更容易理解了。这便是以不变,应万变。也许某一天,你会遇到自定义某种 滚轮效果,这时候 ListWheelViewport 定可住你一臂之力。

【1】ListWheelScrollView 是基于 Scrollable + ListWheelViewport 实现的。
【2】CupertinoPicker 是基于 ListWheelScrollView 实现的。
【3】CupertinoDatePicker 是基于 CupertinoPicker 实现的。
复制代码