Harish Vadaparty
Harish Vadaparty

Reputation: 88

How can i implement this Timeline Ui in flutter

I want to implement a timeline ui like this TimeLine UI.

i tried by using listview builder and container inside transform widget for lines but i ended up getting large distance between those lines and circles and i can't find the correct angle.

is there any way to implement this ui. i need those circles clickable.

here is my implementation

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,
            //
            // ),
          )
        ],
      );
    },),
  ),
);

Heres what ive implemented(UI) enter image description here

Upvotes: 1

Views: 933

Answers (3)

Md. Yeasin Sheikh
Md. Yeasin Sheikh

Reputation: 63709

For this case, I think ListView.separated will be the better choice. Above answer doesn't work after changing screen width.

We need to get the width of Chapter X and circle (having radius 24). To get the Text size, we will use this.

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;
}

You can check original question and answer of getting text size.

To draw lines I am using 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;
}

I am using ListView.builder with Stack[customPaint, widget] for this solution

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,
                        ),
                    ],
                  ),
                ],
              ),
            );
          },
        ),
      ),
    );
  }

enter image description here

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

You can check full snippet and run on dartPad. Also, if you are ok with not having precious positioning, you can use ListView.separate. You can find that inside dartPad, Also make sure to remove extra spacing on Path. You can simply replace path with drawLine.

Upvotes: 1

user3376717
user3376717

Reputation: 11

If you are not looking for specific UI then give a try with flutter package https://pub.dev/packages/timeline_tile

Upvotes: 0

MohammedAli
MohammedAli

Reputation: 2519

i don't have a perfect solution for you, but you play around with my code (Use Stack instead of column)

Output :-

enter image description here

Code :-

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,
                        ),
                      ),
              ],
            );
          },
        ),
      ),
    );
  }
}

Upvotes: 1

Related Questions