Flutter 时间轴

作者: 旺仔_100 | 来源:发表于2023-01-10 17:53 被阅读0次
先上效果图,方便伸手党
image.png
前言

之前其实写过一次类似时间轴的控件Flutter中的IntrinsicHeight,不过当时只是学习记录.这次是项目中遇到了重新写了下.有人会说,这玩意就直接使用一个listview不就完事了么,当然是可以这么实现的,但是时光轴的每一个item大多时候并不是一样高,意味着左边的虚线高度实际不定,右边的高度最好也不写死,根据内容变化.本文自定义绘制最大的好处是右边高度根据内容变化,然后左边绘制的虚线动态跟随右边item变化.还有个点就是本文绘制两种图片svg和image.

一、定义model

这个是根据项目定制的,大家可以按照自己的项目修改

import 'package:flutter/cupertino.dart';

class TimeLineModel {
  //已到达(亮色 1)、当前(亮色+当前图标 2)、未到达(灰色 0)
  int status;

//状态0、1时候显示原点,2的时候根据节点状态(成功和失败)显示两种图标
  String? image;
  String title;
  String? content;
  String? btnText;
  GestureTapCallback? onTap;
  bool isLast;

  TimeLineModel(this.status, this.title, {this.isLast = false, this.image, this.content, this.btnText, this.onTap});
}

二、每个节点控件

时间轴的实现还是会拆成一个个item,下面就是item控件代码.
主要是分为左边自定义的点绘制控件,包含原点,虚线,图片(svg和image)
代码里面是svg的绘制实现,注意:svg.fromSvgString不能在BoxPainter中调用,会报错.然后代码里面还有些注释的代码是绘制image的. 同样loadImageFromAssets也不能在BoxPainter中调用.

import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:writer_assistant/common/easy_theme/easy_minix/easy_theme_minix.dart';
import 'package:writer_assistant/ui/common_widget/screen_sp.dart';
import 'package:writer_assistant/ui/write/widget/time_line/time_line_model.dart';

import '../../../common_widget/common_all.dart';
import 'dash_line.dart';

DrawableRoot? svgRoot;
const String rawSvg = '''<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <!-- Generator: Sketch 59.1 (86144) - https://sketch.com -->
    <title>icon_step_present</title>
    <desc>Created with Sketch.</desc>
    <defs>
        <rect id="path-1" x="0" y="0" width="20" height="20"></rect>
    </defs>
    <g id="接入纵横、奇妙" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="七猫/奇妙-创作指引-发布章节" transform="translate(-18.000000, -538.000000)">
            <g id="编组-2" transform="translate(18.000000, 538.000000)">
                <g id="编组">
                    <mask id="mask-2" fill="white">
                        <use xlink:href="#path-1"></use>
                    </mask>
                    <g id="蒙版"></g>
                    <g mask="url(#mask-2)" id="椭圆形">
                        <g transform="translate(2.000000, 0.500000)">
                            <path d="M8,19 C13.3333333,14.4295636 16,10.7040734 16,7.82352941 C16,3.50271343 12.418278,0 8,0 C3.581722,0 0,3.50271343 0,7.82352941 C0,10.7040734 2.66666667,14.4295636 8,19 Z" fill="#FCC800"></path>
                            <ellipse fill="#FFFFFF" cx="8" cy="7.82352941" rx="2.28571429" ry="2.23529412"></ellipse>
                        </g>
                    </g>
                </g>
            </g>
        </g>
    </g>
</svg>''';

Future loadImage() async {
  svgRoot ??= await svg.fromSvgString(rawSvg, rawSvg);
}

///时间轴的一个节点
class TimeLineNode extends StatefulWidget {
  TimeLineModel model;

  TimeLineNode({Key? key, required this.model}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _TimeLineNodeState();
  }
}

class _TimeLineNodeState extends State<TimeLineNode> {
  //读取 assets 中的图片
  Future<ui.Image>? loadImageFromAssets(String path) async {
    ByteData data = await rootBundle.load(path);
    return decodeImageFromList(data.buffer.asUint8List());
  }

  // ui.Image? _image;

  @override
  void initState() {
    super.initState();
    loadImage().then((value) {
      if (mounted) setState(() {});
    });

    // _image = await loadImageFromAssets('images/png/icon_back_circle@3x.png');
    // if (mounted) setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return IntrinsicHeight(
      child: Row(
        children: [_buildDecoration(), Expanded(child: rightItem(context))],
      ),
    );
  }

  Widget _buildDecoration() {
    if (svgRoot == null) return Container();
    // if (_image == null) return Container();
    return Container(
      margin: const EdgeInsets.only(left: 20),
      width: 20,
      decoration: MyDashDecoration(widget.model, circleColor: Colors.yellow, circleRadius: 4.csp, lineColor: Colors.yellow, lineGrayColor: Colors.grey, circleOffset: const Offset(0, 10), svgRoot: svgRoot),
    );
  }

  Widget rightItem(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(bottom: 20.csp),
      constraints: BoxConstraints(minHeight: 64.csp),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            widget.model.title,
            style: TextStyle(color: context.colorTheme.navBack, fontSize: 16.sp),
          ),
          SizedBox(
            height: 2.csp,
          ),
          Text(
            widget.model.content ?? "",
            style: TextStyle(color: context.colorTheme.divider999999, fontSize: 12.sp),
          ),
          SizedBox(
            height: 6.csp,
          ),
          Visibility(visible: widget.model.btnText != null && widget.model.btnText!.isNotEmpty, child: button(context, widget.model.btnText ?? "", onTap: widget.model.onTap)),
        ],
      ),
    );
  }
}

三、虚线、圆点、图片绘制代码
就是些简单的绘制api,需要注意下绘制时候的偏移位置.

import 'dart:ui';

import 'package:dash_painter/dash_painter.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:writer_assistant/ui/common_widget/screen_sp.dart';
import 'package:writer_assistant/ui/write/widget/time_line/time_line_model.dart';

class MyDashDecoration extends Decoration {
  final Color circleColor;
  final double circleRadius;
  final Color lineColor;
  final Color lineGrayColor;

  ///偏移到圆心到位置
  final Offset circleOffset;
  TimeLineModel model;

  // ui.Image? image;
  DrawableRoot? svgRoot;

  MyDashDecoration(
    this.model, {
    required this.circleColor,
    required this.circleRadius,
    required this.lineColor,
    required this.lineGrayColor,
    required this.circleOffset,
    // required this.image,
    required this.svgRoot,
  });

  @override
  BoxPainter createBoxPainter([VoidCallback? onChanged]) {
    return DashBoxPainter(
      this,
    );
  }
}

class DashBoxPainter extends BoxPainter {
  final MyDashDecoration decoration;

  DashBoxPainter(this.decoration);

  ///Offset 表示左上角的位置,configuration.size是可以拿到对应的控件大小的
  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) async {
    canvas.save();
    final Color color = decoration.model.status == 0 ? decoration.lineGrayColor : decoration.lineColor;

    final Paint paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1
      ..color = color;
    canvas.translate(offset.dx, offset.dy);
    if (!decoration.model.isLast) {
      final Path path = Path();
      path
        ..moveTo(0, decoration.circleRadius)
        ..relativeLineTo(0, configuration.size!.height);
      const DashPainter(span: 2, step: 3).paint(canvas, path, paint);
    }

    //绘制圆点
    final Paint paint2 = Paint()..color = color;
    if (decoration.model.status == 2) {
      // canvas.drawImageRect(
      //   decoration.image!,
      //   Rect.fromCenter(center: Offset(decoration.image!.width / 2, decoration.image!.height / 2), width: decoration.image!.width.toDouble(), height: decoration.image!.height.toDouble()),
      //   Rect.fromCenter(center: Offset(0, 10.csp), width: 16.csp, height: 19.csp),
      //   paint,
      // );
      canvas.save();
      canvas.translate(-9.csp, 2.csp);
      final Picture picture = decoration.svgRoot!.toPicture();
      canvas.drawPicture(picture);
      picture.dispose();

      canvas.restore();
    } else {
      canvas.drawCircle(Offset(decoration.circleOffset.dx, decoration.circleOffset.dy), decoration.circleRadius, paint2);
    }

    canvas.restore();
  }
}

页面代码

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:writer_assistant/ui/write/widget/time_line/time_line_node.dart';

import '../write/widget/time_line/time_line_model.dart';

class TestTimeLinePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        color: Colors.white,
        child: Column(
          children: [
            TimeLineNode(
              model: TimeLineModel(1, "新书创建"),
            ),
            TimeLineNode(
              model: TimeLineModel(2, "发布章节"),
            ),
            TimeLineNode(
              model: TimeLineModel(0, "新书审核"),
            ),
            TimeLineNode(
              model: TimeLineModel(0, "新书审核", content: "已写2000字,快申请审核,审核通过后作品对外可见", btnText: "提交审核", onTap: () => debugPrint("点击了提交审核")),
            ),
            TimeLineNode(
              model: TimeLineModel(0, "作品签约", content: "还差xxx字即可申请签约,来看看签约政策", btnText: "查看签约政策", onTap: () => debugPrint("点击了查看签约政策")),
            ),
            TimeLineNode(
              model: TimeLineModel(0, "作品推荐", isLast: true),
            ),
          ],
        ),
      ),
    );
  }
}

相关文章

网友评论

    本文标题:Flutter 时间轴

    本文链接:https://www.haomeiwen.com/subject/koyvcdtx.html