代码之家  ›  专栏  ›  技术社区  ›  Brendan

如何在Flatter中创建圆形化身周围的虚线边框

  •  2
  • Brendan  · 技术社区  · 2 年前

    我想在我的Flatter应用程序上显示类似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),
            ),
          ),
        ),
      ),
    ),
    

    不管我怎么调整 dottedLength & space params,我不能得到相同数量的空格和破折号。

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

    This is an image of what i'm tried to achieve

    你知道我可以用这两种方法来实现吗 定制画家() 还是插件?

    0 回复  |  直到 2 年前
        1
  •  1
  •   Zaid Alazwi    2 年前

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

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

    其逻辑就是根据层数画出一条弧线,并在留出一定空间后开始下一条弧线

    下面的代码循环计算楼层数,以绘制每个楼层弧,并在将一些值(楼层之间的间距)添加到下一个弧形位置(在圆圈上)的起点后,开始下一个楼层弧(如果楼层>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;
    }
    

    Result

    完整代码和详细解释:

    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(),
        ),
      );
    }
    
        2
  •  1
  •   Mostafa Mahmoud    2 年前

    点击查看图片:

    Status preview

    Status preview with different numbers

    我们应该决定两件事

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

    颜色宽度可以通过下面的功能来测量

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

    2*PI*半径>>圆周

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

    现在我们有了每个状态的宽度相等,以适应圆形边界

    测量分离像素宽度

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

    现在我们添加 虚线 打包到我们的项目并导入它

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