技术无止境,只怕不学习啊,Flutter 我们开始吧

有时候会遇到展示一些标签,最近项目中也遇到做一个标签标记,电商项目中多数都会用到,可能都是UI切的图,这里我们用自定义view 的方式来画一个标签

flutter android 签名 flutter 标签_flutter



flutter android 签名 flutter 标签_flutter android 签名_02


自定义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

flutter android 签名 flutter 标签_flutter android 签名_03

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显示位置

右上角:

flutter android 签名 flutter 标签_java_04

@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);
  }

左下角:

flutter android 签名 flutter 标签_ios_05

@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);
  }

右下角:

flutter android 签名 flutter 标签_flutter_06

@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;
}

flutter android 签名 flutter 标签_java_07


可以看到,调用的地方传入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;
  }
}

效果:

flutter android 签名 flutter 标签_ios_08


给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);

位移和旋转的角度,可以自行计算,这里是随便进行了绘制

flutter android 签名 flutter 标签_flutter android 签名_09


也可以使用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,
      ),
    );
  }
}

flutter android 签名 flutter 标签_ios_10


包含其他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,
            ],
        ),
      ),
    );

flutter android 签名 flutter 标签_java_11

本节就到这里 ,labelView实现的还是比较粗糙的,感兴趣的小伙伴可以继续去完善。