Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter: Widget for U shape

Visual graphic of the intended result

I have been using flutter quite a while now but I have encountered a problem wherein I need to create a widget that would give me a shape in the form of U. I am trying to display the timeline of a user and the time he/she spent in the day doing different activities (represented by the colors in the shape). Can you suggest me a way to solve this problem and come up with a solution that allows me to control the length of the colored segment in the shape. Thanks!

I have tried using CustomPainter in flutter but I am not able to achieve what I intend to do.

like image 291
SSL Avatar asked Aug 13 '26 17:08

SSL


1 Answers

Using CustomPainter, it's achievable. I have attached two screenshots, one is demo screenshot and other is explanation. Also the code is posted here and dart pad link is shared too.

enter image description here enter image description here

import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: MyHomePage(),
    );
  }
}

class MyActivity {
  String startTime;
  Duration duration;
  Color color;
  String title;
  Path path = Path();

  MyActivity({
    required this.startTime,
    required this.duration,
    required this.color,
    required this.title,
  });
}

class MyHomePage extends StatelessWidget {
  MyHomePage({super.key});

  final myActivities = <MyActivity>[
    MyActivity(
        startTime: "9 AM",
        duration: const Duration(hours: 1),
        color: const Color.fromRGBO(236, 33, 43, 1),
        title: "Activity A"),
    MyActivity(
        startTime: "10 AM",
        duration: const Duration(hours: 3, minutes: 30),
        color: const Color.fromRGBO(10, 162, 228, 1),
        title: "Activity B"),
    MyActivity(
        startTime: "1.30PM",
        duration: const Duration(hours: 2, minutes: 30),
        color: const Color.fromRGBO(40, 177, 84, 1),
        title: "Activity C"),
    MyActivity(
        startTime: "4 PM",
        duration: const Duration(hours: 3),
        color: const Color.fromRGBO(135, 4, 24, 1),
        title: "Activity D"),
    MyActivity(
        startTime: "7 PM",
        duration: const Duration(minutes: 30),
        color: const Color.fromRGBO(162, 74, 161, 1),
        title: "Activity E"),
  ];

  @override
  Widget build(BuildContext context) {
    const inset = 24.0;
    final myActivityPainter = MyActivityPainter(myActivities: myActivities);
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text("Flutter Demo"),
      ),
      backgroundColor: Colors.white,
      body: Container(
        padding: const EdgeInsets.all(inset),
        child: LayoutBuilder(builder: (context, constraints) {
          return Container(
            height: constraints.maxHeight,
            width: constraints.maxWidth,
            child: CustomPaint(
              foregroundPainter: myActivityPainter,
            ),
          );
        }),
      ),
    );
  }
}

class MyActivityPainter extends CustomPainter {
  List<MyActivity> myActivities;
  double strokeWidth = 0;
  double strokeWidthMidpoint = 0;
  double xStartPoint = 0;
  double yStartPoint = 0;
  double yBottomEndPoint = 0;
  double xEndPoint = 0;
  double distBetweenXPoints = 0;

  //doublerimeter of circle is 2*pi*r (our arc is semi-circle like)
  double radius = 0;
  double arcPerimeter = 0;
  double totalPerimeter = 0;
  Offset bottomCenter = const Offset(0, 0);

  MyActivityPainter({required this.myActivities});

  @override
  void paint(Canvas canvas, Size size) {
    _initializeFields(canvas, size);
    _initializeActivities(canvas, size);

    for (var element in myActivities) {
      var paint = Paint()
        ..color = element.color
        ..strokeWidth = strokeWidth
        ..style = PaintingStyle.stroke;
      var path = element.path;
      canvas.drawPath(path, paint);

      var textStyle = const TextStyle(
        color: Colors.black,
        fontSize: 30,
      );
      var activityTextPainter = TextPainter(
        text: TextSpan(
          text: element.title,
          style: textStyle,
        ),
        textDirection: TextDirection.ltr,
      );
      var timeTextPainter = TextPainter(
        text: TextSpan(
          text: element.startTime,
          style: textStyle,
        ),
        textDirection: TextDirection.ltr,
      );
      var bounds = element.path.getBounds();
      activityTextPainter.layout(minWidth: 0, maxWidth: xEndPoint / 2);
      timeTextPainter.layout(minWidth: 0, maxWidth: xEndPoint / 2);
      var activityPos = Offset(
          bounds.centerRight.dx - activityTextPainter.width / 2,
          bounds.centerLeft.dy);
      var timeTextPos = Offset(strokeWidth, bounds.topRight.dy);
      // if the element is on the right side
      if (bounds.right > size.width / 2) {
        timeTextPos = Offset(
            bounds.right - timeTextPainter.size.width - strokeWidth / 2,
            bounds.topLeft.dy);
      }
      activityTextPainter.paint(canvas, activityPos);
      timeTextPainter.paint(canvas, timeTextPos);
    }
  }

  void _initializeFields(Canvas canvas, Size size) {
    strokeWidth = size.width * 0.25;
    strokeWidthMidpoint = strokeWidth / 2;
    xStartPoint = strokeWidthMidpoint;
    yStartPoint = 0.0;
    xEndPoint = size.width - strokeWidthMidpoint;
    distBetweenXPoints =
        sqrt((xEndPoint - xStartPoint) * (xEndPoint - xStartPoint));
    radius = distBetweenXPoints / 2;
    yBottomEndPoint = size.height - radius - strokeWidth / 2;
    arcPerimeter = pi * radius;
    // left height + right height + bottom arc perimeter
    totalPerimeter = yBottomEndPoint * 2 + arcPerimeter;
    bottomCenter = Offset(size.width / 2, yBottomEndPoint);
  }

  void _initializeActivities(canvas, size) {
    final totalDuration = myActivities.fold(0,
        (previousValue, element) => previousValue + element.duration.inMinutes);
    var consumedPerimeter = 0.0;
    var prevOffset = Offset(xStartPoint, yStartPoint);
    for (var element in myActivities) {
      final minutes = element.duration.inMinutes;
      if (minutes == 0) {
        continue;
      }
      element.path = Path();
      var path = element.path;
      var currPerimeter = (minutes / totalDuration) * totalPerimeter;
      // we cover left side, bottom(arc perimeter) and right side
      while (currPerimeter > 0 && consumedPerimeter < totalPerimeter) {
        if (consumedPerimeter >= yBottomEndPoint + arcPerimeter) {
          // this cover cases for right side height
          var availableHeight = totalPerimeter - consumedPerimeter;
          availableHeight = min(currPerimeter, availableHeight);
          path.moveTo(prevOffset.dx, prevOffset.dy);
          path.lineTo(xEndPoint, prevOffset.dy - availableHeight);
          prevOffset = Offset(xEndPoint, prevOffset.dy - availableHeight);
          consumedPerimeter += availableHeight;
          currPerimeter -= availableHeight;
        } else if (consumedPerimeter >= yBottomEndPoint) {
          // this covers bottom arc perimeter
          var availablePerimeter =
              (yBottomEndPoint + arcPerimeter) - consumedPerimeter;
          availablePerimeter = min(currPerimeter, availablePerimeter);
          path.moveTo(prevOffset.dx, prevOffset.dy);
          // distance between two points
          var arcPoint = findArcPoint(prevOffset.dx, prevOffset.dy,
              bottomCenter.dx, bottomCenter.dy, availablePerimeter, false);
          path.arcToPoint(arcPoint,
              radius: Radius.circular(radius), clockwise: false);
          prevOffset = Offset(arcPoint.dx, arcPoint.dy);
          consumedPerimeter += availablePerimeter;
          currPerimeter -= availablePerimeter;
        } else {
          // this cover cases for left side height
          var availableHeight =
              min(currPerimeter, yBottomEndPoint - consumedPerimeter);
          path.moveTo(prevOffset.dx, prevOffset.dy+1);
          path.lineTo(xStartPoint, consumedPerimeter + availableHeight);
          prevOffset = Offset(xStartPoint, consumedPerimeter + availableHeight);
          consumedPerimeter += availableHeight;
          currPerimeter -= availableHeight;
        }
      }
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

// Ref:- https://math.stackexchange.com/questions/275201/how-to-find-an-end-point-of-an-arc-given-another-end-point-radius-and-arc-dire
Offset findArcPoint(double aX, aY, cX, cY, L, bool clockwise) {
  var r = sqrt(pow(aX - cX, 2) + pow(aY - cY, 2));
  var angle = atan2(aY - cY, aX - cX);
  if (!clockwise) {
    angle = angle - L / r;
  } else {
    angle = angle + L / r;
  }
  var bX = cX + r * cos(angle);
  var bY = cY + r * sin(angle);
  return Offset(bX, bY);
}
like image 195
Mearaj Avatar answered Aug 15 '26 12:08

Mearaj



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!