Home > Enterprise >  How to create dotted border around circular avatar in flutter
How to create dotted border around circular avatar in flutter

Time:04-27

I'd like to display Instagram-like stories on my Flutter app and want to show the number of stories a user has uploaded by using borders around the user's avatar.

Say a user has uploaded 3 stories, i'll show 3 rounded border lines around the avatar image separated by equal number of spaces & if a user uploads 80 stories, i'll show 80 tiny round border lines separated by equal number of spaces.

I tried using plugins from This is an image of what i'm tried to achieve

Any idea how i can achieve this using either CustomPainter() or a plugin?

CodePudding user response:

Thanks for posting all your tries as it made me jump to CustomPath() directly to try

the approach that (could)(not tested well) works is Result

full code with detailed explanation:

import 'dart:math';
import 'package:flutter/material.dart';

class DottedBorder extends CustomPainter {
  //number of stories
  final int numberOfStories;
  //length of the space arc (empty one)
  final int spaceLength;
  //start of the arc painting in degree(0-360)
  double startOfArcInDegree = 0;

  DottedBorder({required this.numberOfStories, this.spaceLength = 10});

  //drawArc deals with rads, easier for me to use degrees
  //so this takes a degree and change it to rad
  double inRads(double degree){
    return (degree * pi)/180;
  }

  @override
  bool shouldRepaint(DottedBorder oldDelegate) {
    return true;
  }

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

    //circle angle is 360, remove all space arcs between the main story arc (the number of spaces(stories) times the  space length
    //then subtract the number from 360 to get ALL arcs length
    //then divide the ALL arcs length by number of Arc (number of stories) to get the exact length of one arc
    double arcLength = (360 - (numberOfStories * spaceLength))/numberOfStories;


    //be careful here when arc is a negative number
    //that happens when the number of spaces is more than 360
    //feel free to use what logic you want to take care of that
    //note that numberOfStories should be limited too here
    if(arcLength<=0){
      arcLength = 360/spaceLength -1;
    }


    Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);

    //looping for number of stories to draw every story arc
    for(int i =0;i<numberOfStories;i  ){
      //printing the arc
      canvas.drawArc(
          rect,
          inRads(startOfArcInDegree),
          //be careful here is:  "double sweepAngle", not "end"
          inRads(arcLength),
          false,
          Paint()
          //here you can compare your SEEN story index with the arc index to make it grey
            ..color = i==0||i==1?Colors.grey:Colors.teal
            ..strokeWidth =14.0
            ..style = PaintingStyle.stroke

      );

      //the logic of spaces between the arcs is to start the next arc after jumping the length of space
      startOfArcInDegree  = arcLength   spaceLength;
    }




  }
}



class DottedBorderExample extends StatelessWidget {
  const DottedBorderExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Arcs etc')),
      body:Center(
          child: Stack(
            alignment: Alignment.center,
            children: [
              SizedBox(
                width: 300,height: 300,

                child: CustomPaint(
                                    painter:  DottedBorder(numberOfStories: 13,spaceLength:4 ),
              ),),
              Container(child:const Center(child: Text("Some Image",style: TextStyle(fontSize: 18,color: Colors.black),)),width: 270,height: 270,decoration: const BoxDecoration(color: Colors.purple,shape: BoxShape.circle),)
            ],
          )

      )
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: DottedBorderExample(),
    ),
  );
}

CodePudding user response:

click to look at images:

[Status preview]1

[Status preview with different numbers]2

we should determine 2 things

  1. color width
  2. separation width\

color width can be measured through below function

double colorWidth(double radius, int statusCount, double separation) 
{
return ((2 * pi * radius) - (statusCount * separation)) / statusCount;
}

2 * PI * radius >> Circumference of a circle

SO >> Circumference minus total separation pixels needed, Then result divided by total status count.

now we have the width of each status equally, To fit the circle border

measuring separation pixels width

depending on the status number to be more enhanced as WhatsApp

double separation(int statusCount) {
if (statusCount <= 20)
  return 3.0;
else if (statusCount <= 30)
  return 1.8;
else if (statusCount <= 60)
  return 1.0;
else
  return 0.3;
}

Now we add the dotted_border package to our project and import it

https://pub.dev/packages/dotted_border

import 'package:dotted_border/dotted_border.dart';

assuming we have some declarations above they are:

  //each digit express a status number
  List status = [1, 2, 5, 4, 9, 13, 15, 20, 30, 40, 80];

  //circle radius
  double radius = 27.0;

dashPattern:

we have two states one status or more than one (multiple statuses)

    dashPattern: status[index] == 1
                      ? [
                          //one status
                          (2 * pi * (radius   2)), // take all border
                          0, //zere separators
                        ]
                      : [
                          //multiple status
                          colorWidth(radius   2, status[index],
                              separation(status[index])), 

                          separation(status[index]), 
                        ],

FULL CODE:

import 'dart:math';

import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'STATUS',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  List status = [1, 2, 5, 4, 9, 13, 15, 20, 30, 40, 80];

  double radius = 27.0;

  double colorWidth(double radius, int statusCount, double separation) {
    return ((2 * pi * radius) - (statusCount * separation)) / statusCount;
  }

  double separation(int statusCount) {
    if (statusCount <= 20)
      return 3.0;
    else if (statusCount <= 30)
      return 1.8;
    else if (statusCount <= 60)
      return 1.0;
    else
      return 0.3;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: ListView.separated(
          itemCount: status.length,
          separatorBuilder: (context, index) => Divider(
            color: Colors.black,
            height: 15,
          ),
          itemBuilder: ((context, index) => Row(
                children: [
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                    child:

                        /// Creating a circle with a dotted border.
                        DottedBorder(
                      color: Colors.teal.shade300,
                      borderType: BorderType.Circle,
                      radius: Radius.circular(radius),
                      dashPattern: status[index] == 1
                          ? [
                              //one status
                              (2 * pi * (radius   2)),
                              0,
                            ]
                          : [
                              //multiple status
                              colorWidth(radius   2, status[index],
                                  separation(status[index])),
                              separation(status[index]),
                            ],
                      strokeWidth: 3,
                      child: CircleAvatar(
                        radius: radius,
                        backgroundColor: Colors.transparent,
                        child: CircleAvatar(
                          radius: radius - 2,
                        ),
                      ),
                    ),
                  ),
                  SizedBox(
                    width: 10,
                  ),
                  Text(
                    '${status[index]}',
                    style: TextStyle(
                      fontSize: 20,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              )),
        ),
      ),
    );
  }
}
  • Related