技术无止境,只怕不学习啊,Flutter 我们开始吧
有时候会遇到展示一些标签,最近项目中也遇到做一个标签标记,电商项目中多数都会用到,可能都是UI切的图,这里我们用自定义view 的方式来画一个标签
或
自定义LabelView
首先还是先建立类继承于CustomPainter
class LabelViewPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
由于这个LabelView是一个不算很规则的图形,所以用 canvas.drawPath()方法来实现比较合适。
接下来我们先来完成一个简单的绘制,先实现左上角的三角形效果
在这里我们先假设这个label的边长为100
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显示位置
右上角:
@override
void paint(Canvas canvas, Size size) {
Path path = new Path();
path.moveTo(450, 0);
path.lineTo(450, 100);
path.lineTo(300, 0);
path.close();
canvas.drawPath(path, _paint);
}
左下角:
@override
void paint(Canvas canvas, Size size) {
Path path = new Path();
path.moveTo(0, 620);
path.lineTo(100, 620);
path.lineTo(0, 510);
path.close();
canvas.drawPath(path, _paint);
}
右下角:
@override
void paint(Canvas canvas, Size size) {
Path path = new Path();
path.moveTo(450, 620);
path.lineTo(450, 520);
path.lineTo(350, 620);
path.close();
canvas.drawPath(path, _paint);
}
处理参数传入
上面绘制的label的颜色和大小都是写在自定义的View里面的,包括标签显示的位置也都是默认写在左上角的。
接下来尝试把这些属性由外部传入进来。
首先新建类LabelAlignment来标注Label的方向
常量名 | 作用 |
leftTop | 显示在左上角 |
leftBottom | 显示在左下角 |
rightTop | 显示在右上角 |
rightBottom | 显示在右下角 |
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即可。
改过的代码:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.red, // 风格颜色
visualDensity: VisualDensity.adaptivePlatformDensity,
),
routes:{
"new_page":(context) => NewRoute(),
},
home: MyHomePage(title: '测试项目'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
MyHomePage({Key key, this.title}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
// TODO: implement build
var paint = CustomPaint(
size: Size(300,300),
// 传入参数 颜色和方向
painter: LabelViewPainter(Colors.redAccent,LabelAlignment.leftBottom),
);
return Scaffold(
appBar: AppBar(
title: Text( "Canvas的用法"),
),
body: Container(
width: 600,
height: 200,
color: Colors.grey,
child: paint,
),
);
}
}
LabelViewPainter:
class LabelViewPainter extends CustomPainter {
var _paint;
var labelColor;
var labelAlignment;
LabelViewPainter(this.labelColor, this.labelAlignment) {
_paint = new Paint()
..color = Colors.redAccent
..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:
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;
}
可以看到,调用的地方传入color以及label方向即可调用LabelView,为了方便看出效果,给外层加了一个灰色的背景。
“去角”的Label
其实在上面代码的基础上实现起来就显得非常的简单,只需要根据显示的位置控制绘制的path即可。
为了方便使用,同样的给它新增了一个属性叫做“useAngle”默认让它是true,当它为false时显示去角的label效果。
class LabelViewPainter extends CustomPainter {
var _paint; //画笔
var labelColor; // 标签颜色
var labelAlignment; // 标签方向类
var useAngle; // 是否去角
LabelViewPainter(this.labelColor, this.labelAlignment,this.useAngle) {
_paint = new Paint()
..color = Colors.redAccent
..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;
print(drawSize);
Path path = new Path();
switch (labelAlignment) {
case LabelAlignment.leftTop:
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 false;
}
}
效果:
给LabelView加上文字
绘制文字可以使用drawParagraph,要先获取到ParagraphBuilder文字相关的设置 ,通过ParagraphConstraints设置文本的宽度,然后设置build 和layout
/// 绘制文字
ParagraphBuilder pb = new ParagraphBuilder(ParagraphStyle(
textAlign: TextAlign.left,
fontWeight: FontWeight.w300,
fontStyle: FontStyle.normal,
fontSize: 15.0,
));
pb.pushStyle(ui.TextStyle(color: Colors.greenAccent));
pb.addText("热销");
// 设置文本的宽度约束
ParagraphConstraints pc = ParagraphConstraints(width: 400);
// 这里需要先layout,将宽度约束填入,否则无法绘制
var paragraph = pb.build()..layout(pc);
然后在canvas.drawParagraph绘制
canvas.translate(drawSize/3-57, drawSize/2); // 位移
canvas.rotate(-0.85); // 旋转
canvas.drawParagraph(paragraph, offset);
位移和旋转的角度,可以自行计算,这里是随便进行了绘制
也可以使用Text添加文字,借助Transform.rotate()
Transform.rotate({
Key key,
@required double angle,//旋转角度
this.origin,//起始位置
this.alignment = Alignment.center,
this.transformHitTests = true,
Widget child,
})
构造方法非常的简单,只需要计算出旋转的角度和起始位置的坐标即可,数学计算就不再具体讲了,由于这里没有计算文字的宽高所以可能会有一点的误差(原谅比较懒)
当然,这个计算肯定也是要根据你label显示的位置来算的。
所以,需要在LabelView的构造方法中再增加几个关于文字的属性,文字内容、文字风格
改动比较多,所以部分的代码都放了出来
class MyHomePage extends StatefulWidget {
final Size size = Size(600,200);
final String title;
final labelAlignment = LabelAlignment.leftTop;
MyHomePage({Key key, this.title}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ui.Image _image;
static final double PI = 3.1415926;
var textAngle;
var textAlignment;
var offset;
@override
void initState() {
super.initState();
/**
* 注意在初始化函数里面加加载图片
* build 函数中把图片传给TestPainter对象
*/
load("images/timg.jpg").then((i) {
setState(() {
_image = i;
});
});
}
@override
Widget build(BuildContext context) {
// TODO: implement build
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, -15);
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;
}
var paint = CustomPaint(
size: Size(600,200),
painter: LabelViewPainter(Colors.redAccent,LabelAlignment.leftTop,false),
child: Align( // 添加文字
alignment: textAlignment,
child: Transform.rotate(
angle: textAngle,
child: Text(
"TOP",
style: TextStyle(color: Colors.greenAccent),
),
origin: offset,
)),
);
return Scaffold(
appBar: AppBar(
title: Text( "Canvas的用法"),
),
body: Container(
width: widget.size.width,
height: widget.size.height,
child: paint,
color: Colors.grey,
),
);
}
}
包含其他child
LabView作为一个Widget只能显示不能组合其他Widget就不符合Flutter Widget设计的理念(组合大于继承)啊,所以我们的LabelView肯定也要支持组合其他的Widget的。
但是,看上面的代码可以发现CustomPaint的child已经被文字给占用了,但是现在还是需要在这个child里去放子Widget,怎么办?
肯定是要在这个child放置一个支持多child的Widget啊,想想也就那个几个
Widget | 说明 |
ROW | 横向显示 |
Colum | 纵向显示 |
ListView | 列表显示 |
Expanded | 子Widget按照比例分布 |
Table | 堆栈布局 |
IndexedStack | 可控制堆栈布局 |
这里使用Stack是最好的,位于栈定的Widget会覆盖位于栈底的Widget。
所以,把用于组合的子child放在栈底(第一个位置,优先入栈),把自定义的LabelView放在第二个位置,这样就实现了LabelView覆盖在子child上的效果。
return Scaffold(
appBar: AppBar(
title: Text( "Canvas的用法"),
),
body: Container(
width: widget.size.width,
height: widget.size.height,
child:Stack(
children: <Widget>[
Image.asset(
"images/timg.jpg",
height: 400,
width: 500,
fit: BoxFit.cover,
alignment: Alignment.topLeft,
),
paint,
],
),
),
);
本节就到这里 ,labelView实现的还是比较粗糙的,感兴趣的小伙伴可以继续去完善。