我如何在 flutter 中实现这个时间轴 Ui

How can i implement this Timeline Ui in flutter

我想实现这样的时间线ui

我尝试使用 listview builder 和 container inside transform widget for lines 但我最终在这些线和圆圈之间得到了很大的距离,我找不到正确的角度。

有什么方法可以实现这个 ui。我需要那些可点击的圆圈。

这是我的实现

return Scaffold(
  body: Padding(
    padding: const EdgeInsets.symmetric(horizontal: 50),
    child: ListView.builder(itemCount: 5,itemBuilder: (context, index) {
      return Column(
        children: [
          Align(alignment: index%2 == 0? Alignment.centerLeft : Alignment.centerRight,child: Container(width: 50,height: 50,child: CustomPaint(painter: CirclePainter(),))),
         index == 4 ? Container() : Transform.rotate(
            angle: index%2 != 0?-angle: angle,
            //-math.pi / 3.5
            child: Container(height: 50,width: 1,color: Colors.black,)
            // Container(
            //   height: 300,
            //   width: 1,
            //   color: Colors.black,
            //
            // ),
          )
        ],
      );
    },),
  ),
);

这是我实现的(UI)

我没有适合你的完美解决方案,但你可以尝试一下我的代码(使用 Stack 而不是 column)

输出:-

代码:-

import 'package:flutter/material.dart';

class TimelineExample extends StatefulWidget {
  const TimelineExample({Key? key}) : super(key: key);

  @override
  State<TimelineExample> createState() => _TimelineExampleState();
}

class _TimelineExampleState extends State<TimelineExample> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 50),
        child: ListView.builder(
          itemCount: 5,
          itemBuilder: (context, index) {
            return Stack(
              alignment: Alignment.bottomCenter,
              children: [
                Align(
                  alignment: index % 2 == 0
                      ? Alignment.centerLeft
                      : Alignment.centerRight,
                  child: Container(
                    width: 50,
                    height: 50,
                    decoration: BoxDecoration(
                      color: Colors.teal,
                      borderRadius: BorderRadius.circular(50.0),
                    ),
                  ),
                ),
                index == 4
                    ? Container()
                    : Transform.rotate(
                        angle: index % 2 != 0 ? -0.3 : 0.3,
                        child: Container(
                          margin: const EdgeInsets.only(
                            left: 48.0,
                            right: 48.0,
                          ),
                          height: 1,
                          color: Colors.black,
                        ),
                      ),
              ],
            );
          },
        ),
      ),
    );
  }
}

如果你不是在寻找特定的 UI 然后尝试使用 flutter 包 https://pub.dev/packages/timeline_tile

对于这种情况,我认为ListView.separated会是更好的选择。以上答案在更改屏幕宽度后不起作用。

我们需要得到 Chapter X 和圆(半径为 24)的宽度。要获得 Text 大小,我们将使用它。

Size _textSize(String text, TextStyle style) {
  final TextPainter textPainter = TextPainter(
      text: TextSpan(text: text, style: style),
      maxLines: 1,
      textDirection: TextDirection.ltr)
    ..layout(minWidth: 0, maxWidth: double.infinity);
  return textPainter.size;
}

您可以查看getting text size的原始问题和答案。

画线我使用CustomPainter

class DivPainter extends CustomPainter {
  int index;

  DivPainter({required this.index});

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.grey
      ..style = PaintingStyle.stroke
      ..strokeWidth = 5;

    final path1 = Path()
      ..moveTo(0, 24)
      ..lineTo(size.width, size.height + 24);

    final path2 = Path()
      ..moveTo(0, size.height + 24)
      ..lineTo(size.width, 24);

    index.isEven
        ? canvas.drawPath(path1, paint)
        : canvas.drawPath(path2, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

我将 ListView.builderStack[customPaint, widget] 一起用于此解决方案

LayoutBuilder listview() {
    return LayoutBuilder(
      builder: (context, constraints) => Container(
        color: Colors.cyanAccent.withOpacity(.3),
        width: constraints.maxWidth,
        child: ListView.builder(
          itemCount: itemLength,
          itemBuilder: (context, index) {
            final textWidth = _textSize("Chapter $index", textStyle).width;

            final painterWidth = constraints.maxWidth -
                ((textWidth + 24) *
                    2); //24 for CircleAvatar, contains boths side

            return SizedBox(
              height: index == itemLength - 1 ? 24 * 2 : 100 + 24,
              width: constraints.maxWidth,
              child: Stack(
                children: [
                  /// skip render for last item
                  if (index != itemLength - 1)
                    Align(
                      alignment: Alignment.center,
                      child: SizedBox(
                        width: painterWidth,
                        height: height,
                        child: CustomPaint(
                          painter: DivPainter(index: index),
                        ),
                      ),
                    ),
                  Row(
                    mainAxisAlignment: index.isEven
                        ? MainAxisAlignment.start
                        : MainAxisAlignment.end,
                    children: [
                      if (index.isEven)
                        Text(
                          "Chapter $index",
                          style: textStyle,
                        ),
                      const CircleAvatar(radius: 24),
                      if (index.isOdd)
                        Text(
                          "Chapter $index",
                          style: textStyle,
                        ),
                    ],
                  ),
                ],
              ),
            );
          },
        ),
      ),
    );
  }

make sure of screenWidth > lineHeight*2, you can replace 100 with constraints dynamic value

您可以在 dartPad 上查看完整代码段和 运行。 另外,如果你不介意没有宝贵的定位,你可以使用 ListView.separate。您可以在 dartPad 中找到它,同时确保删除 Path 上的多余间距。您可以简单地将路径替换为 drawLine.