如何在颤振中围绕圆形头像创建虚线边框

How to create dotted border around circular avatar in flutter

我想在我的 Flutter 应用程序上显示类似 Instagram 的故事,并想通过在用户头像周围使用边框来显示用户上传的故事数量。

假设用户上传了 3 个故事,我将在头像图像周围显示 3 条圆形边框线,并以相同数量的空格分隔;如果用户上传了 80 个故事,我将显示 80 个小圆形边框线,以空格分隔相等的空格数。

我尝试使用 pub.dev 的插件,例如

仅举几例,但我似乎无法准确计算空格和破折号的数量来满足上述要求。

下面是一个例子:

FDottedLine(
  color: Colors.black,
  strokeWidth: 2.0,
  dottedLength: 30,
  space: 4,
  corner: FDottedLineCorner.all(100.0),
  child: Padding(
    padding: const EdgeInsets.all(3.0),
    child: SizedBox.square(
      dimension: 0.055.h,
      child: ClipRRect(
        borderRadius: BorderRadius.circular(100),
        child: ImageBox.network(
          photo: user.photo.getOrEmpty,
          elevation: 2,
          replacement: Image.asset(AppAssets.defaultUserImage(user.gender.getOrNull)),
          borderRadius: BorderRadius.circular(100),
        ),
      ),
    ),
  ),
),

无论我如何调整 dottedLengthspace 参数,我都无法获得相同数量的空格或破折号。

我也尝试过使用Path()CustomPainter(),但我对如何使用它知之甚少。

知道如何使用 CustomPainter() 或插件实现此目的吗?

感谢您发布所有尝试,因为它让我直接跳转到 CustomPath() 进行尝试

(可能)(未测试好)有效的方法是 drawArc

逻辑很简单,就是根据故事的数量画一条弧线,留一些space

开始下一条弧线

下面的代码循环绘制每个故事弧并在开始添加一些值(故事之间的space)后开始下一个故事弧(如果故事> 1)下一个圆弧位置(在圆上)。

    for(int i =0;i<numberOfStories;i++){
        canvas.drawArc(
            rect,
            inRads(startOfArcInDegree),
            inRads(arcLength),
            false,
            Paint()
             ..color = i==0||i==1?Colors.grey:Colors.teal
             ..strokeWidth =14.0
             ..style = PaintingStyle.stroke

  );

  
           startOfArcInDegree += arcLength + spaceLength;
}

带有详细解释的完整代码:

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

点击查看图片:

我们应该确定两件事

  1. 颜色宽度
  2. 分隔宽度\

色宽可以通过以下函数测量

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

2 * PI * radius >> Circumference of a circle

SO >> 周长减去所需的总分离像素,然后结果除以总状态计数。

现在每个状态的宽度都相等,以适合圆形边框

正在测量分离像素宽度

根据状态数进一步增强为 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;
}

现在我们将dotted_border包添加到我们的项目中并导入它

https://pub.dev/packages/dotted_border

import 'package:dotted_border/dotted_border.dart';

假设我们上面有一些声明,它们是:

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

完整代码:

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