Flutter开发者

自定义View案例【LabelView】

2018-10-26

上期回顾

在前面的几篇文章中我们介绍了Flutter中自定义view的用法,学习了canvas中常用的绘制方法,在这篇及以后的几篇文章中我会给大家写几个自定义View的例子。

标签(我们给它命名LabelView)

提起标签相信大家都不会陌生,在平时使用应用或者网页中会经常看到这种效果

比如这种

或者这种?

今天我们就使用前面学过的知识来完成这个效果如何?

自定义LabelView

1.LabelViewPainter

首先我们还是先建立类继承于CustomPainter

1
2
3
4
5
6
7
8
9
10

class LabelViewPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {}

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

这个时候我们先不着急去绘制图形,我们先来你想下canvas的哪个方法绘制比较方便。

由于这个LabelView是一个不算很规则的图形,所以我们用 canvas.drawPath()方法来实现比较合适。

接下来我们先来完成一个简单的绘制,先实现左上角的三角形效果

在这里我们先假设这个label的边长为100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

class LabelViewPainter extends CustomPainter {
var _paint;

LabelViewPainter() {
_paint = new Paint()
..color = Colors.redAccent
..strokeCap = StrokeCap.round
..isAntiAlias = true
..style = PaintingStyle.fill
..strokeWidth = 5.0;
}

@override
void paint(Canvas canvas, Size size) {
Path path = new Path();
path.lineTo(100, 0);
path.lineTo(0, 100);
path.close();
canvas.drawPath(path, _paint);
}

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

可以看到我们使用Path构建了一个三角形的区域,并设置画笔的绘制风格为fill

当然,我们也可以改变这个label显示位置

比如右上角:

1
2
3
4
5
6
7
8
9
10

@override
void paint(Canvas canvas, Size size) {
Path path = new Path();
path.moveTo(200, 0);
path.lineTo(200, 100);
path.lineTo(100, 0);
path.close();
canvas.drawPath(path, _paint);
}

比如左下角:

1
2
3
4
5
6
7
8
9
10

@override
void paint(Canvas canvas, Size size) {
Path path = new Path();
path.moveTo(0, 200);
path.lineTo(100, 200);
path.lineTo(0, 100);
path.close();
canvas.drawPath(path, _paint);
}

比如右下角:

处理参数传入

当然这个时候我们绘制的label的颜色和大小都是我们写死在自定义的View里面的,包括标签显示的位置也都是我们默认写在左上角的。

接下来我们尝试把这些属性由外部传入进来。

首先我们新建类LabelAlignment来标注Label的方向


常量名 作用
leftTop 显示在左上角
leftBottom 显示在左下角
rightTop 显示在右上角
rightBottom 显示在右下角
1
2
3
4
5
6
7
8
9
10
11

class LabelAlignment {
int labelAlignment;

LabelAlignment(this.labelAlignment);

static const leftTop = 0;
static const leftBottom = 1;
static const rightTop = 2;
static const rightBottom = 3;
}

然后我们让我们自定义的LabelViewPainter的构造方法传入这个参数。

同样的我们让LabelViewPainter的构造方法中传入label的颜色,方便我们绘制

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

}

我们使用paint传入的size属性来获取label的尺寸。

在这里我们取size的宽和高最小值的二分之一为我们label的边长。

var drawSize = size.height > size.width ? size.width / 2 : size.height / 2;

然后我们根据传入的LabelAlignment类型来绘制不同方向的label即可。

下面看下改过的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

void main() {
runApp(new MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("TestDemo"),
),
body: Container(
child: LabelView(Size(600, 200),Colors.redAccent,LabelAlignment.leftTop),
),
)));
}

class LabelView extends StatefulWidget {
final Size size;
final Color labelColor;
final labelAlignment;


LabelView(this.size, this.labelColor, this.labelAlignment);

@override
State<StatefulWidget> createState() {
return LabelViewState();
}
}

class LabelViewState extends State<LabelView> {
@override
Widget build(BuildContext context) {
return Container(
width: widget.size.width,
height: widget.size.height,
color: Colors.grey,
child: CustomPaint(
size: widget.size,
painter: LabelViewPainter(widget.labelColor, widget.labelAlignment),
),
);
}
}

class LabelViewPainter extends CustomPainter {
var labelColor;
var labelAlignment;
var _paint;

LabelViewPainter(this.labelColor, this.labelAlignment) {
_paint = new Paint()
..color = labelColor
..strokeCap = StrokeCap.round
..isAntiAlias = true
..style = PaintingStyle.fill
..strokeWidth = 5.0;
}

@override
void paint(Canvas canvas, Size size) {
var drawSize = size.height > size.width ? size.width / 2 : size.height / 2;
Path path = new Path();

switch (labelAlignment) {
case LabelAlignment.leftTop:

path.lineTo(0, drawSize);
path.lineTo(drawSize, 0);

break;
case LabelAlignment.leftBottom:
path.moveTo(0, size.height - drawSize);
path.lineTo(drawSize, size.height);
path.lineTo(0, size.height);

break;
case LabelAlignment.rightTop:

path.moveTo(size.width - drawSize, 0);
path.lineTo(size.width, 0);
path.lineTo(size.width, drawSize);

break;
case LabelAlignment.rightBottom:
path.moveTo(size.width, size.height);
path.lineTo(size.width - drawSize, size.height);
path.lineTo(size.width, size.height - drawSize);

break;
default:
path.lineTo(0, drawSize);
path.lineTo(drawSize, 0);
break;
}

path.close();
canvas.drawPath(path, _paint);
}

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

class LabelAlignment {
int labelAlignment;

LabelAlignment(this.labelAlignment);

static const leftTop = 0;
static const leftBottom = 1;
static const rightTop = 2;
static const rightBottom = 3;
}

可以看到,我们只需要在调用的地方传入Size、color以及label方向即可调用LabelView,为了方便看出效果,我们给外层加了一个灰色的背景。

当然,我们依然可以把Size、color以及label设置为可选参数

LabelView({this.size=Size.infinite, this.labelColor = Colors.blue, this.labelAlignment=LabelAlignment.leftTop});

设置LabelView的默认尺寸为充满屏幕、默认颜色为蓝色、默认位置在左上角。这样一来我们使用起来就会简单很多了

“去角”的Label

在上面的文章中我们实现了带角的LabelView的效果,但是在平时的使用中我们也会常常使用哪种没有角的Label效果。

下面我们就来看下如何实现:

其实在上面代码的基础上实现起来就显得非常的简单,我们只需要根据显示的位置控制绘制的path即可。

为了方便使用,同样的我们给它新增了一个属性叫做“useAngle”默认我们让它是true,当它为false时显示去角的label效果。

看下实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

class LabelPainter extends CustomPainter {
var labelColor;
var labelAlignment;
var useAngle;
var _paint;

LabelPainter(this.labelColor, this.labelAlignment, this.useAngle) {
_paint = new Paint()
..color = labelColor
..strokeCap = StrokeCap.round
..isAntiAlias = true
..style = PaintingStyle.fill
..strokeWidth = 5.0;
}

@override
void paint(Canvas canvas, Size size) {
var drawSize = size.height > size.width ? size.width / 2 : size.height / 2;
Path path = new Path();

switch (labelAlignment) {
case LabelAlignment.leftTop:
if (!useAngle) {

path.moveTo(drawSize/2, 0);
path.lineTo(0, drawSize/2);
}
path.lineTo(0, drawSize);
path.lineTo(drawSize, 0);

break;
case LabelAlignment.leftBottom:

path.moveTo(0, size.height - drawSize);

if(useAngle){
path.lineTo(drawSize, size.height);
path.lineTo(0, size.height);
}else{
path.lineTo(0, size.height-drawSize/2);
path.lineTo(drawSize/2, size.height);
path.lineTo(drawSize, size.height);
}

break;
case LabelAlignment.rightTop:
path.moveTo(size.width - drawSize, 0);
if (useAngle) {
path.lineTo(size.width, 0);

}else{
path.lineTo(size.width - drawSize / 2, 0);
path.lineTo(size.width, drawSize / 2);
}

path.lineTo(size.width, drawSize);
break;
case LabelAlignment.rightBottom:
if(useAngle){
path.moveTo(size.width, size.height);

path.lineTo(size.width - drawSize, size.height);
path.lineTo(size.width, size.height - drawSize);
}else{
path.moveTo(size.width-drawSize, size.height);
path.lineTo(size.width - drawSize/2, size.height);
path.lineTo(size.width, size.height - drawSize/2);
path.lineTo(size.width, size.height - drawSize);
}

break;
default:
if (!useAngle) {

path.moveTo(drawSize/2, 0);
path.lineTo(0, drawSize/2);
}
path.lineTo(0, drawSize);
path.lineTo(drawSize, 0);
break;
}

path.close();
canvas.drawPath(path, _paint);
}

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

给LabelView加上文字

说了这么多,我们上面也仅仅是画出来了一个Label图形,文字没有显示啊,怎么让它显示文字呢?

但是,根据我们前面学过的东西我们知道,在CustomPainter中canvas是没有办法绘制文字的,那怎么办?总不能不显示文字吧?

方法当然是有的,大家都知道CustomPainter的使用时需要借助于CustomPaint,而CustomPaint可以传入child哦。

所以我们就可以从这里出发,把我们想要的文字放在这个child里。

但是我们并不能简简单单的就直接把我们要绘制的文字放在child里,因为我们的Label是倾斜且在一定的位置显示的。

所以我们需要控制文字的切斜和位置在我们绘制的label上才行。

在这里我们需要借助 Transform.rotate()来实现这个效果

Transform.rotate

1
2
3
4
5
6
7
8
Transform.rotate({
Key key,
@required double angle,//旋转角度
this.origin,//起始位置
this.alignment = Alignment.center,
this.transformHitTests = true,
Widget child,
})

构造方法非常的简单,我们只需要计算出旋转的角度和起始位置的坐标即可,数学计算就不再具体讲了,由于这里我没有计算文字的宽高所以可能会有一点的误差(原谅我比较懒)

当然,这个计算肯定也是要根据你label显示的位置来算的。

所以,我们需要在爱我们LabelView的构造方法中再增加几个关于文字的属性,文字内容、文字风格

改动比较多,所以把LabelView部分的代码都放了出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180

class LabelView extends StatefulWidget {
final Size size;
final Color labelColor;
final labelAlignment;
final bool useAngle;
final String labelText;
final TextStyle textStyle;

LabelView(
{this.size = Size.infinite,
this.labelText = "HOT",
this.textStyle,
this.labelColor = Colors.blue,
this.labelAlignment = LabelAlignment.leftTop,
this.useAngle = true});

@override
State<StatefulWidget> createState() {
return LabelViewState();
}
}

class LabelViewState extends State<LabelView> {
static final double PI = 3.1415926;
var textAngle;
var textAlignment;

var offset;

@override
Widget build(BuildContext context) {
var offsetX = widget.size.width > widget.size.height
? widget.size.height / 4.5
: widget.size.width / 4.5;
switch (widget.labelAlignment) {
case LabelAlignment.leftTop:
offset = Offset(offsetX, 0);
textAlignment = Alignment.topLeft;
textAngle = -PI / 4;
break;
case LabelAlignment.rightTop:
offset = Offset(-offsetX, 0);
textAlignment = Alignment.topRight;
textAngle = PI / 4;
break;
case LabelAlignment.leftBottom:
offset = Offset(offsetX, 0);
textAlignment = Alignment.bottomLeft;
textAngle = PI / 4;
break;
case LabelAlignment.rightBottom:
offset = Offset(-offsetX, 0);
textAlignment = Alignment.bottomRight;
textAngle = -PI / 4;
break;
}
return Container(
width: widget.size.width,
height: widget.size.height,
color: Colors.grey,
child: CustomPaint(
size: widget.size,
painter: LabelViewPainter(
widget.labelColor, widget.labelAlignment, widget.useAngle),
child: Align(
alignment: textAlignment,
child: Transform.rotate(
angle: textAngle,
child: Text(
widget.labelText,
style: widget.textStyle == null
? TextStyle(color: Colors.white)
: widget.textStyle,
),
origin: offset,
)),
),
);
}
}

class LabelViewPainter extends CustomPainter {
var labelColor;
var labelAlignment;
var useAngle;
var _paint;

LabelViewPainter(this.labelColor, this.labelAlignment, this.useAngle) {
_paint = new Paint()
..color = labelColor
..strokeCap = StrokeCap.round
..isAntiAlias = true
..style = PaintingStyle.fill
..strokeWidth = 5.0;
}

@override
void paint(Canvas canvas, Size size) {
var drawSize = size.height > size.width ? size.width / 2 : size.height / 2;
Path path = new Path();

switch (labelAlignment) {
case LabelAlignment.leftTop:
if (!useAngle) {
path.moveTo(drawSize / 2, 0);
path.lineTo(0, drawSize / 2);
}
path.lineTo(0, drawSize);
path.lineTo(drawSize, 0);

break;
case LabelAlignment.leftBottom:
path.moveTo(0, size.height - drawSize);

if (useAngle) {
path.lineTo(drawSize, size.height);
path.lineTo(0, size.height);
} else {
path.lineTo(0, size.height - drawSize / 2);
path.lineTo(drawSize / 2, size.height);
path.lineTo(drawSize, size.height);
}

break;
case LabelAlignment.rightTop:
path.moveTo(size.width - drawSize, 0);
if (useAngle) {
path.lineTo(size.width, 0);
} else {
path.lineTo(size.width - drawSize / 2, 0);
path.lineTo(size.width, drawSize / 2);
}

path.lineTo(size.width, drawSize);
break;
case LabelAlignment.rightBottom:
if (useAngle) {
path.moveTo(size.width, size.height);

path.lineTo(size.width - drawSize, size.height);
path.lineTo(size.width, size.height - drawSize);
} else {
path.moveTo(size.width - drawSize, size.height);
path.lineTo(size.width - drawSize / 2, size.height);
path.lineTo(size.width, size.height - drawSize / 2);
path.lineTo(size.width, size.height - drawSize);
}

break;
default:
if (!useAngle) {
path.moveTo(drawSize / 2, 0);
path.lineTo(0, drawSize / 2);
}
path.lineTo(0, drawSize);
path.lineTo(drawSize, 0);
break;
}

path.close();
canvas.drawPath(path, _paint);
}

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

class LabelAlignment {
int labelAlignment;

LabelAlignment(this.labelAlignment);

static const leftTop = 0;
static const leftBottom = 1;
static const rightTop = 2;
static const rightBottom = 3;
}

在调用的地方我们只需要更改labelView的颜色、位置、文字即可。

包含其他child

你以为实现到上面的效果就结束了吗?不是这个样子的,因为我们LabView作为一个Widget只能显示不能组合其他Widget就不符合Flutter Widget设计的理念(组合大于继承)啊,所以我们的LabelView肯定也要支持组合其他的Widget的。

但是,看上面的代码可以发现我们CustomPaint的child已经被文字给占用了,但是我们现在还是需要在这个child里去放子Widget,怎么办?

肯定是要在这个child放置一个支持多child的Widget啊,想想也就那个几个


Widget 说明
ROW 横向显示
Colum 纵向显示
ListView 列表显示
Expanded 子Widget按照比例分布
Table 表格布局显示
Stack 堆栈布局
IndexedStack 可控制堆栈布局

根据我们以前学过的知识不难发现,我们这里使用Stack时最好的,位于栈定的Widget会覆盖位于栈底的Widget。

所以,我们把用于组合的子child放在栈底(第一个位置,优先入栈),把我们自定义的LabelView放在第二个位置,这样我们就实现了LabelView覆盖在子child上的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

return Container(
color: widget.backgroundColor,
child: Stack(
children: <Widget>[
widget.child,
CustomPaint(
size: widget.size,
painter: LabelPainter(
widget.labelColor, widget.labelAlignment, widget.useAngle),
child: Align(
alignment: textAlignment,
child: Transform.rotate(
angle: textAngle,
child: Text(
widget.labelText,
style: widget.textStyle == null
? TextStyle(color: Colors.white)
: widget.textStyle,
),
origin: offset,
)),
)
],
),
width: widget.size.width,
height: widget.size.height,
);

在调用的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

void main() {
runApp(new MaterialApp(
home: LabelViewDemo(),
));
}

class LabelViewDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("LabelViewDemo"),
),
body: LabelView(
Container(
child: ListView(
children: <Widget>[
Image.asset(
"images/cover.jpg",
height: 120,
width: 500,
fit: BoxFit.cover,
alignment: Alignment.topLeft,
),
SizedBox(
height: 5,
),
Text(
"根据我们以前学过的知识不难发现,我们这里使用Stack时最好的,位于栈定的Widget会覆盖位于栈底的Widget。\n所以,我们把用于组合的子child放在栈底(第一个位置,优先入栈),把我们自定义的LabelView放在第二个位置,这样我们就实现了LabelView覆盖在子child上的效果。",
style: TextStyle(height: 1.2),
),
Divider(
height: 2.0,
color: Colors.grey,
)
],
),
),
Size(500, 240),
labelColor: Colors.redAccent,
labelAlignment: LabelAlignment.leftTop,
useAngle: false,
),
);
}
}

效果如下:

小结

  • 使用drawPath绘制不规则图形
  • 掌握CustomPaint的用法
  • 结合其他Widget完成目标widget效果

当然,这个labelView实现的还是比较粗糙的,感兴趣的小伙伴可以继续去完善,如如labelView半透明效果,文字的测量等等。

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

打赏请备注姓名或者昵称,方便我后期统计哦

关注公众号,及时查阅最新文章