先上效果图,方便伸手党

前言
之前其实写过一次类似时间轴的控件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),
),
],
),
),
);
}
}
网友评论