Flutter时间轴实现,自定义点边框PointBorder

前言:简书菜鸟一枚,如有侵权,望告知,我会下架文章;

时间轴,推荐文章:https://blog.csdn.net/m0_37667770/article/details/93589084
这篇文章写得比较齐全,但是与我实际遇到的情况还是要略加变动的。
实际中遇到想要的效果图:因为涉及公司权益,故仅保留时间轴效果。
先上效果图:

QQ-04.jpg

实现方法:

1.垂直列表,首选ListView。

即时间轴中最外层应该是个ListView,每个具体的日期是一个item。初步实现:(大的点是与日期同级的,故这里用row水平布局类似Android中的LinearLayout的horizon;)

import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';

import 'DashBorder.dart';
import 'JourneyBean.dart';

/**
 * author walke
 * date: 2020/4/28
 * des:我的旅程页面
 * 时间轴效果 参考 :https://blog.csdn.net/m0_37667770/article/details/93589084
 */
class MyJourneyPageTest extends StatefulWidget {
  static const routeName = 'MyJourneyPageTest';
  Map<String, dynamic> jsonData = {
    "result": 0,
    "description": "操作成功",
    "list": [
      {
        "timeLine": "2020-05-20",
        "journeyList": [
          {
            "place": "华山论剑",
            "templateText": "Flutter —— 时间轴 实现,自定义点边框PointBorder 这一天,我完成了",
          },
          {"place": "华山煮茶", "templateText": "这一天,我完成了"}
        ]
      },
      {
        "timeLine": "2020-05-19",
        "journeyList": [
          {
            "place": "泰山悟道",
            "templateText": "Flutter —— 时间轴 实现,自定义点边框PointBorder 这一天,我完成了",
          },
          {
            "place": "泰山喝茶",
            "templateText": "这一天,我完成了",
          }
        ]
      },
      {
        "timeLine": "2020-05-10",
        "journeyList": [
          {
            "place": "嵩山比武",
            "templateText": "Flutter —— 时间轴 实现,自定义点边框PointBorder 这一天,我完成了",
          },
          {
            "place": "嵩山习武",
            "templateText": "Flutter —— 时间轴 实现,自定义点边框PointBorder 这一天,我完成了",
          }
        ]
      },
      {
        "timeLine": "2020-05-09",
        "journeyList": [
          {
            "place": "衡山打酱油",
            "templateText": "Flutter —— 时间轴 实现,自定义点边框PointBorder 这一天,我完成了",
          }
        ]
      }
    ],
  };

  @override
  State<StatefulWidget> createState() => _MyJourneyPageState();
}

class _MyJourneyPageState extends State<MyJourneyPageTest> {
  JourneyBean _journeyResultBean;
  List<JourneyTimeLine> _listJourneyTimeLine = [];
  bool isEmpty = false;

  bool isLoading = false;
  ScrollController _scrollController = new ScrollController();

  @override
  void initState() {
    super.initState();
    _journeyResultBean = JourneyBean.fromJson(widget.jsonData);
    _scrollController.addListener(() {});
  }

  @override
  Widget build(BuildContext context) {
    _listJourneyTimeLine.addAll(_journeyResultBean.list);
    _listJourneyTimeLine.add(_journeyResultBean.list[0]);
    return Stack(
      children: <Widget>[
        Container(
          color: Colors.green,
          height: 150,
        ),
        Scaffold(
          backgroundColor: Colors.transparent,
          appBar: PreferredSize(
              child: AppBar(
                elevation: 0,
                centerTitle: true,
                title: Text(
                  '我的旅程',
                ),
                backgroundColor: Colors.green,
              ),
              preferredSize: Size.fromHeight(40.0)),
          body: Card(
            margin: EdgeInsets.only(top: 30, left: 15.0, right: 15.0, bottom: 15.0),
            color: Colors.white,
            child: _getMainContainer(),
          ),
        ),
      ],
    );
  }

  _getMainContainer() {
    return MediaQuery.removePadding(
        removeTop: true,
        context: context,
        child: ListView.builder(
          controller: _scrollController,
          itemCount: _listJourneyTimeLine.length ,
          itemBuilder: (context, index) {
              return _detailView(_listJourneyTimeLine[index ]);
          },
        ));
  }

  // 旅程详情
  _detailView(JourneyTimeLine timeLine) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.start,
      children: <Widget>[
       // 日期行 , 点+日期
        Row( 
          children: <Widget>[
            Container( // 点
              child: CircleAvatar(
                backgroundColor: Colors.greenAccent,
                radius: 6,
              ),
              alignment: Alignment.center,
              width: 30,
              margin: EdgeInsets.only(left: 8, right: 3),
            ),
            Text( // 日期
              dateChane(timeLine.timeLine),
              style: TextStyle(fontWeight: FontWeight.bold),
              textAlign: TextAlign.left,
            ),
          ],
        ),
       // 单个日期具体行程
        _detailView2(timeLine),
      ],
    );
  }

  // 单个日期具体行程
  _detailView2(JourneyTimeLine timeLine) {
    return Row(

      children: <Widget>[
        Container(
          // 点
          child: CircleAvatar(
            backgroundColor: Colors.greenAccent,
            radius: 2,
          ),
          alignment: Alignment.center,
          width: 30,
          margin: EdgeInsets.only(left: 8, right: 3),
        ),
        Container(
          alignment: Alignment.centerLeft,
          margin: EdgeInsets.only(
            left: 0,
            top: 1,
          ),
          padding: EdgeInsets.only(top: 0, bottom: 8),
          color: Colors.blue[100],
          child: Text("稍后实现具体日期的游玩列表"),
        )
      ],
    );
  }
}

/// 时间日期转化
String dateChane(String timeLine) {
  print('walke MyJourneyPageState._dateChane() -----> $timeLine');
  List<String> dlist = timeLine.split('-');
  if (dlist == null || dlist.length == 0) return timeLine;
  int month = int.parse(dlist[1]); //
  int day = int.parse(dlist[2]);
  return dlist[0] + '年' + month.toString() + '月' + day.toString() + '日';
}

效果如图:
QQ-01.jpg

2.然后适当改变实现ListView的代码,用于添加顶部的点。

 _getMainContainer() {
    return MediaQuery.removePadding(
        removeTop: true,
        context: context,
        child: ListView.builder(
          controller: _scrollController,
          itemCount: _listJourneyTimeLine.length + 1,
          itemBuilder: (context, index) {
            if (index == 0) {
              return Column( // 顶部的点
                mainAxisAlignment: MainAxisAlignment.start,
                children: [1, 2, 3].map((ite) {
                  return Container(  // 点
                    child: CircleAvatar(
                      backgroundColor: Colors.greenAccent,
                      radius: 2,
                    ),
                    alignment: Alignment.topLeft,
                    margin: EdgeInsets.only(left: 21, right: 10, top: (ite == 1 ? 3 : 8)), // 区别间距
//                    color: Color(0x36FFA726),
                  );
                }).toList(),
              );
            } else {
              return _detailView(_listJourneyTimeLine[index - 1]);
            }
          },
        ));
  }

3.观察可知具体日期里的行程个数是不定的,每个行程的内容(字数也是不定的)。故具体日期中,行程列表与左边的小点不是一对一的关系,不该用一起在一个listviewi的tem中,即具体行程不用listview。尝试:使用Row左右分开,小点与行程左右分开(参考顶部的点的方式,暂时假定点的个数是行程数*3):主要代码:

// 单个日期具体行程
  _detailView2(JourneyTimeLine timeLine) {
    List<Journey> points = [];
    points.addAll(timeLine.journeyList);
    points.addAll(timeLine.journeyList);
    points.addAll(timeLine.journeyList);
    return Row(
      children: <Widget>[
        // 左边的点
        Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: points.map((ite) {
            return Container(
              // 点
              child: CircleAvatar(
                backgroundColor: Colors.green,
                radius: 1,
              ),
              alignment: Alignment.topLeft,
              margin: EdgeInsets.only(left: 22, right: 10, top: 8),
//                    color: Color(0x36FFA726),
            );
          }).toList(),
        ),
        // 右边具体的行程 
        Expanded(
            child: Container( // 有超出左边故外层加了Expanded
          alignment: Alignment.centerLeft,
          margin: EdgeInsets.only(
            left: 0,
            top: 1,
            right: 10,
          ),
          padding: EdgeInsets.only(top: 0, bottom: 8),
          color: Colors.blue[100],
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.end,
            children: timeLine.journeyList.map((Journey item) {
              return Container(
                  alignment: Alignment.topLeft,
                  margin: EdgeInsets.only(right: 12, top: 9),
                  child: RichText(
                    // 富文本显示
                    text: TextSpan(children: [
                      TextSpan(
                        text: item.templateText,
                        style: TextStyle(
                          color: Colors.black87,
                          fontSize: 13,
                        ),
                      ),
                      TextSpan(
                          text: " " + item.place,
                          style: TextStyle(color: Colors.green, fontSize: 13),
                          recognizer: TapGestureRecognizer()
                            ..onTap = () {
                              // 点击事件
//                         ToastUtils.showTs(item.keyText);
                            }),
                    ]),
                  ));
            }).toList(),
          ),
        )),
      ],
    );
  }

效果如图,有点像想要的效果了

QQ-02.jpg

4.这时我们就要思考,具体行程的内容与左边小的点的对应关系了。

比较UI给的效果图(图1)。左边小点应该与行程所占高度范围应该是相对一样的。这时,可以考虑两个方法。①获取完成绘制时,行程内容所占高度,即对应的Colum的高度cHeight。然后通过cHeight/小点间隔求出小点个数。从而实现左边小点。②.小点高度与行程高度一样,结合从文章https://blog.csdn.net/m0_37667770/article/details/93589084给的灵感。我想起Android在边框的实现边框时可以通过xml的shap简易实现虚线边框、圆点边框、单边框。那我们能不能通过实现一个左边框为圆点边框的方式去实现呢?这样能有效解耦代码,减少Widget树的层级和代码。由于我是Android搬砖经验丰富所以很倾向于方法②,弄好了也没去尝试方法①,所以①就有由各位有兴趣的去烟酒一下了。以下是实现单边框的尝试及步骤。

5.实现单边框。

1). 先试试边框合适不合适, 调整_detailView2代码:(发现flutter已经有API实现左边宽Border(left: BorderSide(color: Color(0x66FF9800), width: 3)),但无点边框)

 // 单个日期具体行程
  _detailView2(JourneyTimeLine timeLine) {
    List<Journey> points = [];
    points.addAll(timeLine.journeyList);
    points.addAll(timeLine.journeyList);
    points.addAll(timeLine.journeyList);
    return Stack(
      children: <Widget>[
        // 左边的点
        Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: points.map((ite) {
            return Container(
              // 点
              child: CircleAvatar(
                backgroundColor: Colors.green,
                radius: 1,
              ),
              alignment: Alignment.topLeft,
              margin: EdgeInsets.only(left: 22, right: 10, top: 8),
//                    color: Color(0x36FFA726),
            );
          }).toList(),
        ),
        // 右边具体的行程
        Container( // 有超出左边故外层加了Expanded【外层改为Stack,就要去掉Expanded】
          alignment: Alignment.centerLeft,
          margin: EdgeInsets.only(
            left: 22,
            top: 1,
            right: 10,
          ),
          padding: EdgeInsets.only(top: 0, bottom: 8, left: 13),
//          color: Colors.blue[100],// 使用了decoration就不能使用这个属性了,报错:Cannot provide both a color and a decoratio
          decoration: BoxDecoration(
            border: Border(left: BorderSide(color: Color(0x66FF9800), width: 3)), // 左边边框
          ),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.end,
            children: timeLine.journeyList.map((Journey item) {
              return Container(
                  alignment: Alignment.topLeft,
                  margin: EdgeInsets.only(right: 12, top: 9),
                  child: RichText( // 富文本显示
                    text: TextSpan(children: [
                      TextSpan(
                        text: item.templateText,
                        style: TextStyle(
                          color: Colors.black87,
                          fontSize: 13,
                        ),
                      ),
                      TextSpan(
                          text: " " + item.place,
                          style: TextStyle(color: Colors.green, fontSize: 13),
                          recognizer: TapGestureRecognizer()
                            ..onTap = () {
                              // 点击事件
//                         ToastUtils.showTs(item.keyText);
                            }),
                    ]),
                  ));
            }).toList(),
          ),
        )
      ],
    );
  }

效果如图:有戏!

QQ-03.jpg

2). 查看Border源码,去看看左边宽是如何实现的。
Border部分源码:【】对应文件:box_border.dart】

class Border extends BoxBorder {
  // ... 省略大部分代码
  const Border({
    this.top = BorderSide.none,
    this.right = BorderSide.none,
    this.bottom = BorderSide.none,
    this.left = BorderSide.none,
  }) : assert(top != null),
       assert(right != null),
       assert(bottom != null),
       assert(left != null);

  ///  * [paintBorder], which is used if the border is not uniform.
  @override
  void paint(
    Canvas canvas,
    Rect rect, {
    TextDirection textDirection,
    BoxShape shape = BoxShape.rectangle,
    BorderRadius borderRadius,
  }) {
    if (isUniform) {
      switch (top.style) {
        case BorderStyle.none:
          return;
        case BorderStyle.solid:
          switch (shape) {
            case BoxShape.circle:
              assert(borderRadius == null, 'A borderRadius can only be given for rectangular boxes.');
              BoxBorder._paintUniformBorderWithCircle(canvas, rect, top);
              break;
            case BoxShape.rectangle:
              if (borderRadius != null) {
                BoxBorder._paintUniformBorderWithRadius(canvas, rect, top, borderRadius);
                return;
              }
              BoxBorder._paintUniformBorderWithRectangle(canvas, rect, top);
              break;
          }
          return;
      }
    }
    assert(borderRadius == null, 'A borderRadius can only be given for uniform borders.');
    assert(shape == BoxShape.rectangle, 'A border can only be drawn as a circle if it is uniform.');

    paintBorder(canvas, rect, top: top, right: right, bottom: bottom, left: left);
  }
}

点进Border源码首页查看构造方法,发现有个left,即我们关心的。然后全局搜索,中的在于与paint、draw这些绘制有关的。或者看继续传给了哪个类,继续去看,这里我们较容易就可以看到paint()方法,然后有paintBorder方法调用。进而我们查看paintBorder的代码:

void paintBorder(
  Canvas canvas,
  Rect rect, {
  BorderSide top = BorderSide.none,
  BorderSide right = BorderSide.none,
  BorderSide bottom = BorderSide.none,
  BorderSide left = BorderSide.none,
}) {
  assert(canvas != null);
  assert(rect != null);
  assert(top != null);
  assert(right != null);
  assert(bottom != null);
  assert(left != null);

  // We draw the borders as filled shapes, unless the borders are hairline
  // borders, in which case we use PaintingStyle.stroke, with the stroke width
  // specified here.
  final Paint paint = Paint()
    ..strokeWidth = 0.0;

  final Path path = Path();

  switch (top.style) {
    case BorderStyle.solid:
      paint.color = top.color;
      path.reset();
      path.moveTo(rect.left, rect.top);
      path.lineTo(rect.right, rect.top);
      if (top.width == 0.0) {
        paint.style = PaintingStyle.stroke;
      } else {
        paint.style = PaintingStyle.fill;
        path.lineTo(rect.right - right.width, rect.top + top.width);
        path.lineTo(rect.left + left.width, rect.top + top.width);
      }
      canvas.drawPath(path, paint);
      break;
    case BorderStyle.none:
      break;
  }

  switch (right.style) {
    case BorderStyle.solid:
      paint.color = right.color;
      path.reset();
      path.moveTo(rect.right, rect.top);
      path.lineTo(rect.right, rect.bottom);
      if (right.width == 0.0) {
        paint.style = PaintingStyle.stroke;
      } else {
        paint.style = PaintingStyle.fill;
        path.lineTo(rect.right - right.width, rect.bottom - bottom.width);
        path.lineTo(rect.right - right.width, rect.top + top.width);
      }
      canvas.drawPath(path, paint);
      break;
    case BorderStyle.none:
      break;
  }

  switch (bottom.style) {
    case BorderStyle.solid:
      paint.color = bottom.color;
      path.reset();
      path.moveTo(rect.right, rect.bottom);
      path.lineTo(rect.left, rect.bottom);
      if (bottom.width == 0.0) {
        paint.style = PaintingStyle.stroke;
      } else {
        paint.style = PaintingStyle.fill;
        path.lineTo(rect.left + left.width, rect.bottom - bottom.width);
        path.lineTo(rect.right - right.width, rect.bottom - bottom.width);
      }
      canvas.drawPath(path, paint);
      break;
    case BorderStyle.none:
      break;
  }

  switch (left.style) {
    case BorderStyle.solid:
      paint.color = left.color;
      path.reset();
      path.moveTo(rect.left, rect.bottom);
      path.lineTo(rect.left, rect.top);
      if (left.width == 0.0) {
        paint.style = PaintingStyle.stroke;
      } else {
        paint.style = PaintingStyle.fill;
        path.lineTo(rect.left + left.width, rect.top + top.width);
        path.lineTo(rect.left + left.width, rect.bottom - bottom.width);
      }
      canvas.drawPath(path, paint);
      break;
    case BorderStyle.none:
      break;
  }
}

搜索传入的left,即可快速定位: switch (left.style)这就是我们要找的。发现有paint(画笔)、path(轨迹)、canvas(画布)【自定义Widget方面的知识点】,即这里就是具体实现左边宽的代码。

3). 实现点边框,其实就是想办法改造2)发现的switch (left.style)中的代码。
简单粗暴的方式--Copy源码新建自定义类。先动手试试。新建一个PointBorder。复制Border的代码到PointBorder,然后改导包、改关联。处理完报错。注:如果发现处理不完报错,可能是不适合这种方式,这个合适。
可得:也顺道改成想要的效果

import 'dart:ui';

import 'package:flutter/material.dart';

/**
 * author walke
 * date: 2020/4/30
 * des:  左边框变成点边框,用于弄时间轴
 */
///
class PointBorder extends BoxBorder {
 
  const PointBorder({
    this.top = BorderSide.none,
    this.right = BorderSide.none,
    this.bottom = BorderSide.none,
    this.left = BorderSide.none,
  })
      : assert(top != null),
        assert(right != null),
        assert(bottom != null),
        assert(left != null);

  const PointBorder.fromBorderSide(BorderSide side)
      : assert(side != null),
        top = side,
        right = side,
        bottom = side,
        left = side;
  factory PointBorder.all({
    Color color = const Color(0xFF000000),
    double width = 1.0,
    BorderStyle style = BorderStyle.solid,
  }) {
    final BorderSide side = BorderSide(color: color, width: width, style: style);
    return PointBorder.fromBorderSide(side);
  }

  static PointBorder merge(PointBorder a, PointBorder b) {
    assert(a != null);
    assert(b != null);
    assert(BorderSide.canMerge(a.top, b.top));
    assert(BorderSide.canMerge(a.right, b.right));
    assert(BorderSide.canMerge(a.bottom, b.bottom));
    assert(BorderSide.canMerge(a.left, b.left));
    return PointBorder(
      top: BorderSide.merge(a.top, b.top),
      right: BorderSide.merge(a.right, b.right),
      bottom: BorderSide.merge(a.bottom, b.bottom),
      left: BorderSide.merge(a.left, b.left),
    );
  }

  @override
  final BorderSide top;

  /// The right side of this border.
  final BorderSide right;

  @override
  final BorderSide bottom;

  /// The left side of this border.
  final BorderSide left;

  @override
  EdgeInsetsGeometry get dimensions {
    return EdgeInsets.fromLTRB(left.width, top.width, right.width, bottom.width);
  }

  @override
  bool get isUniform {
    final Color topColor = top.color;
    if (right.color != topColor ||
        bottom.color != topColor ||
        left.color != topColor)
      return false;

    final double topWidth = top.width;
    if (right.width != topWidth ||
        bottom.width != topWidth ||
        left.width != topWidth)
      return false;

    final BorderStyle topStyle = top.style;
    if (right.style != topStyle ||
        bottom.style != topStyle ||
        left.style != topStyle)
      return false;

    return true;
  }

  @override
  PointBorder add(ShapeBorder other, { bool reversed = false }) {
    if (other is! PointBorder)
      return null;
    final PointBorder typedOther = other;
    if (BorderSide.canMerge(top, typedOther.top) &&
        BorderSide.canMerge(right, typedOther.right) &&
        BorderSide.canMerge(bottom, typedOther.bottom) &&
        BorderSide.canMerge(left, typedOther.left)) {
      return PointBorder.merge(this, typedOther);
    }
    return null;
  }

  @override
  PointBorder scale(double t) {
    return PointBorder(
      top: top.scale(t),
      right: right.scale(t),
      bottom: bottom.scale(t),
      left: left.scale(t),
    );
  }

  @override
  ShapeBorder lerpFrom(ShapeBorder a, double t) {
    if (a is PointBorder)
      return PointBorder.lerp(a, this, t);
    return super.lerpFrom(a, t);
  }

  @override
  ShapeBorder lerpTo(ShapeBorder b, double t) {
    if (b is PointBorder)
      return PointBorder.lerp(this, b, t);
    return super.lerpTo(b, t);
  }

  /// {@macro dart.ui.shadow.lerp}
  static PointBorder lerp(PointBorder a, PointBorder b, double t) {
    assert(t != null);
    if (a == null && b == null)
      return null;
    if (a == null)
      return b.scale(t);
    if (b == null)
      return a.scale(1.0 - t);
    return PointBorder(
      top: BorderSide.lerp(a.top, b.top, t),
      right: BorderSide.lerp(a.right, b.right, t),
      bottom: BorderSide.lerp(a.bottom, b.bottom, t),
      left: BorderSide.lerp(a.left, b.left, t),
    );
  }

  @override
  void paint(Canvas canvas,
      Rect rect, {
        TextDirection textDirection,
        BoxShape shape = BoxShape.rectangle,
        BorderRadius borderRadius,
      }) {
    if (isUniform) {
      switch (top.style) {
        case BorderStyle.none:
          return;
        case BorderStyle.solid:
          switch (shape) {
            case BoxShape.circle:
              assert(borderRadius == null, 'A borderRadius can only be given for rectangular boxes.');
              _paintUniformBorderWithCircle(canvas, rect, top);
              break;
            case BoxShape.rectangle:
              if (borderRadius != null) {
                _paintUniformBorderWithRadius(canvas, rect, top, borderRadius);
                return;
              }
              _paintUniformBorderWithRectangle(canvas, rect, top);
              break;
          }
          return;
      }
    }

    assert(borderRadius == null, 'A borderRadius can only be given for uniform borders.');
    assert(shape == BoxShape.rectangle, 'A border can only be drawn as a circle if it is uniform.');

    paintBorder2(canvas, rect, top: top, right: right, bottom: bottom, left: left);
  }

  static void _paintUniformBorderWithCircle(Canvas canvas, Rect rect, BorderSide side) {
    assert(side.style != BorderStyle.none);
    final double width = side.width;
    final Paint paint = side.toPaint();
    final double radius = (rect.shortestSide - width) / 2.0;
    canvas.drawCircle(rect.center, radius, paint);
  }

  static void _paintUniformBorderWithRadius(Canvas canvas, Rect rect, BorderSide side,
      BorderRadius borderRadius) {
    assert(side.style != BorderStyle.none);
    final Paint paint = Paint()
      ..color = side.color;
    final RRect outer = borderRadius.toRRect(rect);
    final double width = side.width;
    if (width == 0.0) {
      paint
        ..style = PaintingStyle.stroke
        ..strokeWidth = 0.0;
      canvas.drawRRect(outer, paint);
    } else {
      final RRect inner = outer.deflate(width);
      canvas.drawDRRect(outer, inner, paint);
    }
  }

  static void _paintUniformBorderWithRectangle(Canvas canvas, Rect rect, BorderSide side) {
    assert(side.style != BorderStyle.none);
    final double width = side.width;
    final Paint paint = side.toPaint();
//    canvas.drawRect(rect.deflate(width / 2.0), paint);
    List<Offset> pionts = [];
    double h = rect.height;
    int interval = 10; // 间距
    int length = (h / interval) as int;
    for (int i = 1; i < length-1; i++) {
      pionts.add(Offset(0, h / interval*i));
    }
    canvas.drawPoints(PointMode.points, pionts, paint);
  }

  @override
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (runtimeType != other.runtimeType)
      return false;
    final PointBorder typedOther = other;
    return top == typedOther.top &&
        right == typedOther.right &&
        bottom == typedOther.bottom &&
        left == typedOther.left;
  }

  @override
  int get hashCode => hashValues(top, right, bottom, left);

  @override
  String toString() {
    if (isUniform)
      return '$runtimeType.all($top)';
    final List<String> arguments = <String>[];
    if (top != BorderSide.none)
      arguments.add('top: $top');
    if (right != BorderSide.none)
      arguments.add('right: $right');
    if (bottom != BorderSide.none)
      arguments.add('bottom: $bottom');
    if (left != BorderSide.none)
      arguments.add('left: $left');
    return '$runtimeType(${arguments.join(", ")})';
  }
}


void paintBorder2(
    Canvas canvas,
    Rect rect, {
      BorderSide top = BorderSide.none,
      BorderSide right = BorderSide.none,
      BorderSide bottom = BorderSide.none,
      BorderSide left = BorderSide.none,
    }) {
  assert(canvas != null);
  assert(rect != null);
  assert(top != null);
  assert(right != null);
  assert(bottom != null);
  assert(left != null);

  final Paint paint = Paint()
    ..strokeWidth = 0.0;

  final Path path = Path();

  switch (top.style) {
    case BorderStyle.solid:
      paint.color = top.color;
      path.reset();
      path.moveTo(rect.left, rect.top);
      path.lineTo(rect.right, rect.top);
      if (top.width == 0.0) {
        paint.style = PaintingStyle.stroke;
      } else {
        paint.style = PaintingStyle.fill;
        path.lineTo(rect.right - right.width, rect.top + top.width);
        path.lineTo(rect.left + left.width, rect.top + top.width);
      }
      canvas.drawPath(path, paint);
      break;
    case BorderStyle.none:
      break;
  }

  switch (right.style) {
    case BorderStyle.solid:
      paint.color = right.color;
      path.reset();
      path.moveTo(rect.right, rect.top);
      path.lineTo(rect.right, rect.bottom);
      if (right.width == 0.0) {
        paint.style = PaintingStyle.stroke;
      } else {
        paint.style = PaintingStyle.fill;
        path.lineTo(rect.right - right.width, rect.bottom - bottom.width);
        path.lineTo(rect.right - right.width, rect.top + top.width);
      }
      canvas.drawPath(path, paint);
      break;
    case BorderStyle.none:
      break;
  }

  switch (bottom.style) {
    case BorderStyle.solid:
      paint.color = bottom.color;
      path.reset();
      path.moveTo(rect.right, rect.bottom);
      path.lineTo(rect.left, rect.bottom);
      if (bottom.width == 0.0) {
        paint.style = PaintingStyle.stroke;
      } else {
        paint.style = PaintingStyle.fill;
        path.lineTo(rect.left + left.width, rect.bottom - bottom.width);
        path.lineTo(rect.right - right.width, rect.bottom - bottom.width);
      }
      canvas.drawPath(path, paint);
      break;
    case BorderStyle.none:
      break;
  }

  switch (left.style) {
    case BorderStyle.solid:
      paint.color = left.color;
//      path.reset();
//      path.moveTo(rect.left, rect.bottom);
//      path.lineTo(rect.left, rect.top);
      if (left.width == 0.0) {
        paint.style = PaintingStyle.stroke;
      } else {
        paint.style = PaintingStyle.fill;
//        path.lineTo(rect.left + left.width, rect.top + top.width);
//        path.lineTo(rect.left + left.width, rect.bottom - bottom.width);
      }
//      canvas.drawPath(path, paint);
      paint.strokeWidth=2;
      paint.color=Colors.green;
      paint.strokeCap=StrokeCap.round;
      List<Offset> pionts = [];
      double h =rect.bottom- rect.top;
      int interval = 10; // 间距
      int length = (h / interval).toInt();
      for (int i = 0; i < length; i++) {
        pionts.add(Offset(23,interval*(i+2.5).toDouble()));
      }
      canvas.drawPoints(PointMode.points, pionts, paint);
      break;
    case BorderStyle.none:
      break;
  }
}

然后调整_detailView2代码:

 // 单个日期具体行程
  _detailView2(JourneyTimeLine timeLine) {
    return  Container(
      // 有超出左边故外层加了Expanded【外层改为Stack,就要去掉Expanded】
      alignment: Alignment.centerLeft,
      margin: EdgeInsets.only(
        left: 22,
        top: 1,
        right: 10,
      ),
      padding: EdgeInsets.only(top: 0, bottom: 8, left: 13),
//          color: Colors.blue[100],// 使用了decoration就不能使用这个属性了,报错:Cannot provide both a color and a decoratio
      decoration: BoxDecoration(
        border: PointBorder(left: BorderSide(color:Colors.greenAccent, width: 3)), // 左边边框
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: timeLine.journeyList.map((Journey item) {
          return Container(
              alignment: Alignment.topLeft,
              margin: EdgeInsets.only(right: 12, top: 9),
              child: RichText(
                // 富文本显示
                text: TextSpan(children: [
                  TextSpan(
                    text: item.templateText,
                    style: TextStyle(
                      color: Colors.black87,
                      fontSize: 13,
                    ),
                  ),
                  TextSpan(
                      text: " " + item.place,
                      style: TextStyle(color: Colors.green, fontSize: 13),
                      recognizer: TapGestureRecognizer()
                        ..onTap = () {
                          // 点击事件
//                         ToastUtils.showTs(item.keyText);
                        }),
                ]),
              ));
        }).toList(),
      ),
    );
  }

可得效果图1

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271