【Flutter】使用timelines制作纵向嵌套任务进度

854 阅读3分钟

一、简介

现在有一个需求是要实现一个纵向的操作步聚进度指示功能,我想使用时间轴来实现,在pub.dev上Flutter的时间轴package有很多,本篇介绍使用timelines这个package制作如图所示的纵向嵌套步聚效果。

Snipaste_2023-03-11_17-58-30.png

此示例是从官方Package delivery tracking示例改造而来,官方示例如下图所示:

image.png

Github链接:timelines/example at main · chulwoo-park/timelines (github.com)

二、静态实现

先说一下静态效果如何实现,安装timelines很简单,执行flutter pub add timelines即可,下面讲主要代码:

  1. 主步聚核心代码
FixedTimeline.tileBuilder(
  theme: TimelineThemeData(
    nodePosition: 0,
    color: Theme.of(context).primaryColor,
    indicatorTheme: const IndicatorThemeData(
      size: 15,
    ),
    connectorTheme: const ConnectorThemeData(
      thickness: 1,
    ),
  ),
  builder: TimelineTileBuilder.connected(
    itemCount: 4,
    contentsAlign: ContentsAlign.basic,
    indicatorBuilder: (_, index) {
      return DotIndicator(
        color: Theme.of(context).primaryColor,
        position: index == 2 ? 0.13 : null, // 根据索引是否为2,设置偏移
      );
    },
    connectorBuilder: (_, index, ___) {
      return SolidLineConnector(
        color: Theme.of(context).dividerColor,
      );
    },
    contentsBuilder: (context, index) {
      return Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: [
          Padding(
            padding: const EdgeInsets.fromLTRB(10, 20, 0, 20),
            child: Text(
              '主步聚$index',
              style: DefaultTextStyle.of(context)
                  .style
                  .copyWith(fontSize: 14),
            ),
          ),
          _InnerStep(
            index: index,
          ),
        ],
      );
    },
  ),
)

上述代码是使用FixedTimeline构建了一个不可滑动的时间轴,并采用只在右侧显示内容的方式,其中有一句话要注意,position: index == 2 ? 0.13 : null,该值不是像素值,而一个0~1的小数,表示百分比,代码片段:

DotIndicator(
  color: Theme.of(context).primaryColor,
  position: index == 2 ? 0.13 : null, // 根据索引是否为2,设置偏移
);

如果不加此参数,则左侧的绿点就会默认居中,就像下图这样:

Snipaste_2023-03-11_18-09-48.png

但是如果像官方例子中,直接在indicatorTheme中加上position: 0

indicatorTheme: const IndicatorThemeData(
  size: 15,
  position: 0, // 这里直接加,位置太靠上
),

则绿点的位置又太靠上了,成了这样:

Snipaste_2023-03-11_18-16-01.png

而我只需要在有子步聚的时候,才调整绿点的偏移位置,所以加了此行代码。

  1. 子步聚
class _InnerStep extends StatelessWidget {
  final int index;

  const _InnerStep({Key? key, required this.index}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    if (index != 2) {
      return const SizedBox.shrink();
    }
    return FixedTimeline.tileBuilder(
      theme: TimelineTheme.of(context).copyWith(
        nodePosition: 0,
        color: Theme.of(context).primaryColor,
        indicatorTheme: const IndicatorThemeData(
          position: 0,
          size: 10,
        ),
      ),
      builder: TimelineTileBuilder(
        contentsAlign: ContentsAlign.basic,
        contentsBuilder: (context, index) {
          return Padding(
            padding: const EdgeInsets.all(10),
            child: Text(
              '子步聚$index',
              style: DefaultTextStyle.of(context).style.copyWith(fontSize: 14),
            ),
          );
        },
        indicatorBuilder: (_, index) {
          return Padding(
            padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
            child: DotIndicator(
              color: Theme.of(context).primaryColor,
            ),
          );
        },
        itemCount: 3,
      ),
    );
  }
}

我这里的子步聚不需要前后的连接线,所以比较简单。官方需要连接线的话,可参考官方示例。

三、切换进度

现在要将整个进度封装成一个TaskProgress组件,并实现外部传入一个值,就自动切换前面圆点的颜色,我是这么实现的:

  1. 在该TaskProgress组件中定义一个step参数,这一步相信大家都会。
  2. 因为几个步骤是固定,所以在组件内部使用枚举实现。
enum ProgressStep {
  stepItem0(0.0, '主步骤0'),
  stepItem1(1.0, '主步骤1'),
  stepItem20(2.0, '主步骤2', '子步骤0'),
  stepItem21(2.1, '主步骤2', '子步骤1'),
  stepItem22(2.2, '主步骤2', '子步骤2'),
  stepItem3(3.0, '主步骤3'),
  stepItemNull(-1.0, '无效状态'); // 注意这里是分号

  final double stepNum; // 步骤数,整数表示主步骤,小数表示子步骤
  final String stepName; // 步骤名称
  final String stepSubName; // 子步骤名称

  const ProgressStep(this.stepNum, this.stepName, [this.stepSubName = '']);

  static ProgressStep parse({required int mainNum, int subNum = 0}) {
    return values.firstWhere(
      (element) => element.stepNum == (mainNum + subNum / 10),
      orElse: () => ProgressStep.stepItemNull,
    );
  }
}

定义了一个枚举,并将每一个枚举元素上都加入了步骤数和步骤名称,步骤名称又分为主名称和子名称。

  1. 遍历展示时,增加步骤数的判断,并根据步骤数显示名称。
  • 圆点状态处理

主步骤显示圆点的DotIndicator组件改为:

DotIndicator(
  color: step.stepNum.toInt() >= index
      ? Theme.of(context).primaryColor
      : Theme.of(context).disabledColor,
  position: step.stepNum.toInt() >= 2 && index == 2
      ? 0.13
      : null, // 根据索引是否为2,设置指示器偏移
)

子步骤显示圆点的DotIndicator组件改为:

DotIndicator(
  color: step.stepNum.toInt() > 2 ||
          (step.stepNum.toInt() == 2 &&
              ((step.stepNum % 1) * 10).toInt() >= index)
      ? Theme.of(context).primaryColor
      : Theme.of(context).disabledColor,
)
  • 显示步骤名称

主步骤显示名称的Text组件改为:

Text(
  ProgressStep.parse(mainNum: index).stepName,
  style: DefaultTextStyle.of(context)
      .style
      .copyWith(fontSize: 14),
)

子步骤显示名称的Text组件改为:

Text(
  ProgressStep.parse(
    mainNum: 2,
    subNum: index,
  ).stepSubName,
  style: DefaultTextStyle.of(context).style.copyWith(fontSize: 14),
)
  • 控制子步骤显示

因为我这里只有当状态进入主步骤2时,才会显示子步骤,所以在子步骤的build方法最开始加入了如下代码:

Widget build(BuildContext context) {
  if (step.stepNum.toInt() != 2 || parentIndex != 2) {
    return const SizedBox.shrink();
  }
  //...

完成后的效果

Screen-2023-03-12-004555_0002.gif


以上就是我自己使用timelines实现一个纵向时间轴的过程,若有错误之处,还望指出。