技术无止境,只怕不学习啊,Flutter 我们开始吧
有时候会遇到展示一些标签,最近项目中也遇到做一个标签标记,电商项目中多数都会用到,可能都是UI切的图,这里我们用自定义view 的方式来画一个标签
或
自定义LabelView
首先还是先建立类继承于CustomPainter
class LabelViewPainter extends CustomPainter {
@overridevoid paint(Canvas canvas, Size size) {
}@overridebool 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;}@overridevoid paint(Canvas canvas, Size size) {
Path path = new Path();path.lineTo(100, 0);path.lineTo(0, 100);path.close();canvas.drawPath(path, _paint);}@overridebool shouldRepaint(CustomPainter oldDelegate) {
return false;}}
可以看到使用Path构建了一个三角形的区域,并设置画笔的绘制风格为fill
当然,也可以改变这个label显示位置
右上角:
@overridevoid 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);}
左下角:
@overridevoid 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);}
右下角:
@overridevoid 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.@overrideWidget 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> {
@overrideWidget build(BuildContext context) {
// TODO: implement buildvar 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;}@overridevoid 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);}@overridebool 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;}@overridevoid 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);}@overridebool 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;@overridevoid initState() {
super.initState();/*** 注意在初始化函数里面加加载图片* build 函数中把图片传给TestPainter对象*/load("images/timg.jpg").then((i) {
setState(() {
_image = i;});});}@overrideWidget build(BuildContext context) {
// TODO: implement buildvar 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实现的还是比较粗糙的,感兴趣的小伙伴可以继续去完善。