如何在颤振中围绕圆形头像创建虚线边框
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),
),
),
),
),
),
无论我如何调整 dottedLength
和 space
参数,我都无法获得相同数量的空格或破折号。
我也尝试过使用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(),
),
);
}
点击查看图片:
我们应该确定两件事
- 颜色宽度
- 分隔宽度\
色宽可以通过以下函数测量
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,
),
),
],
)),
),
),
);
}
}
我想在我的 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),
),
),
),
),
),
无论我如何调整 dottedLength
和 space
参数,我都无法获得相同数量的空格或破折号。
我也尝试过使用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(),
),
);
}
点击查看图片:
我们应该确定两件事
- 颜色宽度
- 分隔宽度\
色宽可以通过以下函数测量
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,
),
),
],
)),
),
),
);
}
}