FlutterFlutter

Flutter 绘图部分详解

2020-07-22  本文已影响0人  HuyaRC

分享是每个优秀的程序员所必备的品质


Flutter绘图主要是CustomerPaint和Paint组合使用绘制出我们需要的图形。下面笔者会按基础逐渐深入,结合代码和注释,通俗易懂。

看完这篇,相信关于Flutter的绘图部分应该不是问题了!

CustomPainter:具体绘制的类,需要绘制的图形和自定义的画笔,一般会自定义一个类来继承。需要重写一下两个方法:

// 实现具体绘制的方法
@override
void paint(Canvas canvas, Size size) {
  // TODO: implement paint
}
// 控制自定义绘制是否需要重绘的,返回false代表在构建完成后不需要重绘。如果存在依赖于一个变量并且该变量发生了变化,那么你在这里返回true,就会重新绘制。
@override
bool shouldRepaint(CustomPainter oldDelegate) {
  // TODO: implement shouldRepaint
  return null;
}

CustomPaint:在绘制阶段提供一个 Canvas 画布
主要的参数:

child:子widget
painter : 绘制的对象,传入CustomPainter对象,且绘制是在child之前。若设置了child,则绘制的内容会被覆盖,即chind内容在绘制内容之上。
foregroundPainter: 绘制的对象,传入CustomPainter对象,它的绘制是在child之后,即chind内容在绘制内容之下。
size:绘制区域的大小,取决于child
      若child != null ,则绘制的区域为child的大小
      若child == null ,则绘制的区域为size设置的大小
      若child  !=  null  &  size != null,若child == null ,可以用SizeBox包裹一下CustomPaint;

Paint:画笔,设置颜色,线宽,填充等多种样式

Paint _paint = Paint()
    ..color = Colors.blueAccent //画笔颜色
    ..strokeWidth = 15.0 //画笔的宽度
    ..strokeCap = StrokeCap.round //画笔笔触类型(butt、round、square)
    ..isAntiAlias = true //是否启动抗锯齿
    ..blendMode = BlendMode.exclusion //绘制形状或合成层时应用的混合模式(如颜色混合模式,或在截取Image后合并Image用,枚举实在是太多了,笔者就不一一列举了,有需要的同学可以自己看下源码)
    ..style = PaintingStyle.fill //绘画风格,默认为填充
    ..colorFilter = ColorFilter.mode(Colors.blueAccent,BlendMode.exclusion) //颜色渲染模式,一般是矩阵效果来改变的,但是flutter中只能使用颜色混合模式
    ..maskFilter = MaskFilter.blur(BlurStyle.inner, 3.0) //模糊遮罩效果,flutter中只有这个
    ..filterQuality = FilterQuality.high //颜色渲染模式的质量

接下来就根据几个示例来应用

1 :绘制线:void drawLine(Offset p1, Offset p2, Paint paint)

class PaintDemo1 extends CustomPainter{
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawLine(Offset(50.0,50.0), Offset(150.0,50.0), _paint);
    canvas.drawLine(Offset(50.0, 100.0), Offset(250.0, 100.0), _paint);
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}
运行效果图: 1.png

2 :绘制点:void drawPoints(PointMode pointMode, List points, Paint paint)

class PaintDemo2 extends CustomPainter{
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawPoints(
      ///PointMode的枚举类型有三个,points(点),lines(线,隔点连接),polygon(线,相邻连接)
        PointMode.points,
        [
          Offset(200.0, 50.0),
          Offset(300.0, 120.0),
          Offset(300.0, 250.0),
          Offset(200.0, 320.0),
          Offset(100.0, 250.0),
          Offset(100.0, 120.0),
          Offset(200.0, 50.0),
        ],
        _paint);
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

points类型运行效果:

2.1.png
lines类型运行效果:
2.2.png
polygon类型运行效果:
2..png

3、绘制圆rawCircle void drawOval(Rect rect, Paint paint)

// 圆形是否填充或描边(或两者)由Paint.style控制(fill填充)。
class PaintDemo3 extends CustomPainter{
  @override
  void paint(Canvas canvas, Size size) {
    //绘制圆 参数(圆心,半径,画笔)
    canvas.drawCircle(
        Offset(200.0, 120.0),
        100.0,
        _paint..color = Colors.redAccent..style = PaintingStyle.stroke //绘画风格改为stroke
    );
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

运行效果:


3.png

4、绘制椭圆rawCircle void drawCircle(Offset c, double radius, Paint paint)

class PaintDemo4 extends CustomPainter{
  // 使用左上角点和右下角点坐标来确定矩形的大小和位置,椭圆是在这个矩形之中内切的,正方形中就是个圆了
  @override
  void paint(Canvas canvas, Size size) {
    Rect rect1 = Rect.fromPoints(Offset(100.0, 100.0), Offset(300.0, 200.0));
    canvas.drawOval(rect1, _paint);
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}
运行效果: 4.png

5、绘制圆弧void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)

class PaintDemo5 extends CustomPainter{
  @override
  // Rect来确认圆弧的位置,还需要开始的弧度、结束的弧度、是否使用中心点绘制(圆弧两段是否连接圆心)、以及paint弧度
  void paint(Canvas canvas, Size size) {
    Rect rect2 = Rect.fromCircle(center: Offset(200.0, 50.0), radius: 80.0);
    canvas.drawArc(rect2, 0.0, pi, false, _paint);
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

运行效果:
useCenter = false

5.1.png
useCenter = true
5.2.png

6、绘制矩形、圆角矩形

class PaintDemo6 extends CustomPainter{
  @override
  // Rect来确认圆弧的位置,还需要开始的弧度、结束的弧度、是否使用中心点绘制、以及paint弧度
  void paint(Canvas canvas, Size size) {

    // 1、用Rect构建一个边长50,中心点坐标为100,100的矩形
    Rect rect = Rect.fromCircle(center: Offset(60.0, 100.0), radius: 50.0);
    canvas.drawRect(rect, _paint);

    // 2、圆角矩形
    Rect rect0 = Rect.fromCircle(center: Offset(180.0, 100.0), radius: 50.0);
    RRect rrect = RRect.fromRectAndRadius(rect0, Radius.circular(10.0));
    canvas.drawRRect(rrect, _paint);

    // 3、分别绘制外部圆角矩形和内部的圆角矩形
    Rect rect1 = Rect.fromCircle(center: Offset(320.0, 100.0), radius: 60.0);
    Rect rect2 = Rect.fromCircle(center: Offset(320.0, 100.0), radius: 40.0);

    RRect outer = RRect.fromRectAndRadius(rect1, Radius.circular(10.0));
    RRect inner = RRect.fromRectAndRadius(rect2, Radius.circular(10.0));
    canvas.drawDRRect(outer, inner, _paint);
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}
运行效果: 6.png

7、绘制路径drawPathvoid drawPath(Path path, Paint paint)

class PaintDemo7 extends CustomPainter{
  @override
  void paint(Canvas canvas, Size size) {
    /* Path的常用方法:
    *  moveTo :将路径起始点移动到指定的位置
    *  relativeMoveTo : 相对于当前位置移动到
    *  lineTo :从当前位置连接指定点
    *  relativeLineTo : 相对当前位置连接到
    *  arcTo : 曲线
    *  conicTo :  贝塞尔曲线
    *  add** : 添加其他图形,如addArc,在路径是添加圆弧
    *  contains : 路径上是否包括某点
    *  transfor : 给路径做matrix4变换
    *  combine : 结合两个路径
    *  close : 关闭路径,连接路径的起始点
    *  reset : 重置路径,恢复到默认状态
    */
    Path path = Path();
    path.moveTo(50.0, 50.0);
    path.lineTo(100, 100.0);
    path.lineTo(50.0, 150.0);
    path.lineTo(100.0, 200.0);
    canvas.drawPath(path, _paint);
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

运行效果:


7.png

8、绘制阴影

//  路径、阴影的颜色、阴影扩散的范围、
void drawShadow(path, color, elevation, transparentOccluder) 
class PaintDemo8 extends CustomPainter{
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawShadow(Path()
      ..moveTo(50.0, 50.0)
      ..lineTo(150.0, 50.0)
      ..lineTo(150.0, 150.0)
      ..lineTo(50.0, 150.0)
      ..close(), Colors.red, 3, false);

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

运行效果:


8.png

9、二阶贝塞尔曲线绘制弧线void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo)

我们用贝塞尔曲线来绘制一个大写字母C和字母G。

方法注解:
/*
* rect 矩形,startAngle是开始的弧度,sweepAngle是结束的弧度
* 如果“forceMoveTo”参数为false,则添加一条直线段和一条弧段。
* 如果“forceMoveTo”参数为true,则启动一个新的子路径,其中包含一个弧段。
*/

引申一下,对于Rect,有多种构建方式

// Rect构建方式
  fromPoints(Offset a, Offset b)
  使用左上和右下角坐标来确定矩形的大小和位置

  fromCircle({ Offset center, double radius })
  使用圆的圆心点坐标和半径和确定外切矩形的大小和位置

  fromLTRB(double left, double top, double right, double bottom)
  使用矩形左边的X坐标、矩形顶部的Y坐标、矩形右边的X坐标、矩形底部的Y坐标来确定矩形的大小和位置

  fromLTWH(double left, double top, double width, double height)
  使用矩形左边的X坐标、矩形顶部的Y坐标矩形的宽高来确定矩形的大小和位置
  

代码:

class PaintDemo8 extends CustomPainter{
  @override
  void paint(Canvas canvas, Size size) {
    Path path =  Path()..moveTo(100.0, 100.0);
    Rect rect = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 60.0);
    path.arcTo(rect, pi*0.2, pi*1.5, true); // 字母C
    //path.arcTo(rect, pi*0.0, pi*1.6, false); // 字母G
    canvas.drawPath(path, _paint);
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }[图片上传失败...(image-ecbd30-1595388326757)]

运行结果:
C

9.1.png
G
9.2.png
9.1使用二阶贝塞尔曲线直接画一个圆
class PaintDemo9 extends CustomPainter{
  @override
  void paint(Canvas canvas, Size size) {
    Path path = new Path()..moveTo(100.0, 100.0);
    Rect rect = Rect.fromCircle(center: Offset(200.0, 200.0), radius: 60.0);
    path.arcTo(rect, 0.0, 3.14*2, true);
    canvas.drawPath(path, _paint);
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

10、三阶贝塞尔曲线绘制void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)

相比于二阶的,三阶贝塞尔曲线就是说两个点之间的线 有两个控制点

10.1.png

如上
要画出如图的曲线,A、B、C是要经过的路径点,其中
AB段的控制点为点p1、p2
BC段的控制点为点p3、p4
控制点需自行设置,不同的控制点画出来的圆弧是不一样的!
则代码:

class PaintDemo10 extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    _paint..style = PaintingStyle.stroke;
    Path path = Path();
    // A点
    path.moveTo(0, 50);
    // B点
    path.cubicTo(0, 25, 40, 0, 80, 0);
    // C点
    path.cubicTo(120, 0, 160, 25, 160, 50);
    canvas.drawPath(path, _paint);
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

运行效果:


10.2.png

下面笔者用cubicTo绘制一个心形送给最爱的慧同学!

class PaintDemo11 extends CustomPainter{
  @override
  void paint(Canvas canvas, Size size) {
    double width = 200;
    double height = 300;
    _paint..style = PaintingStyle.fill..color = Colors.redAccent;

    // 右边一半
    Path path = Path();
    path.moveTo(width / 2, height / 4);
    path.cubicTo((width * 6) / 7, height / 9, width, (height * 2) / 5, width / 2, (height * 7) / 12);

    // 左边一半
    path.moveTo(width / 2, height / 4);
    path.cubicTo(width / 7, height / 9, width / 21, (height * 2) / 5, width / 2, (height * 7) / 12);

    canvas.drawPath(path,_paint);
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

运行效果:


送给慧同学.png

11、绘制文字

文字(以及图片)的绘制,需要Dart中的相关方法,
记得要导入import 'dart:ui' as ui;

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

    ParagraphBuilder pb = ParagraphBuilder(
        ParagraphStyle(
            textAlign: TextAlign.center,
            fontSize: 20,
            fontWeight: FontWeight.w600,
            fontStyle: FontStyle.normal, // 正常 or 斜体
            maxLines: 1
        )
    );
    pb.pushStyle(
        ui.TextStyle(color: Colors.redAccent)
    );
    pb.addText("RC LOVE TMH");
    // 绘制的宽度
    ParagraphConstraints pc = ParagraphConstraints(width: 350.0);
    Paragraph paragraph = pb.build()..layout(pc);
    canvas.drawParagraph(paragraph, Offset(30, 300));
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

运行效果:


11.png

12、绘制图片

drawImage
drawImageRect
drawImageNine
注意:图片一定是已经加载完成的
可以通过canvas.clipRect(Rect)来裁剪画布

这玩意有点绕,需要ui/painting文件的Image,这个Image不是Widget而是一个Uint8List 格式的图片流,还要通过instantiateImageCodec获取图片编解码器,它是返回一个Future的方法,而且传入一个Uint8List。

图片转换成Uint8List的方法:

// 本地图片转换成Uint8List的方法
  Future<ui.Image> getImage(String asset) async {
    ByteData data = await rootBundle.load(asset);
    Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
    FrameInfo frameInfo = await codec.getNextFrame();
    return frameInfo.image;
  }

  // 网络图片和本地图片换成Uint8List的方法
  Future <ui.Image> loadImage(var path, bool networkImage) async {
    ImageStream stream;
    if (networkImage) {
      stream = NetworkImage(path).resolve(ImageConfiguration.empty);
    } else {
      stream = AssetImage(path, bundle: rootBundle)
          .resolve(ImageConfiguration.empty);
    }
    Completer <ui.Image> completer = Completer<ui.Image>();
    void listener(ImageInfo frame, bool synchronousCall) {
      final ui.Image image = frame.image;
      completer.complete(image);
      stream.removeListener(ImageStreamListener(listener));
    }
    stream.addListener(ImageStreamListener(listener));
    return completer.future;
  }

示例:点击按钮生成图片并且预览

class DrawDemo extends StatefulWidget {
  @override
  _DrawDemoState createState() => _DrawDemoState();
}

class _DrawDemoState extends State<DrawDemo> {

  Uint8List imageMemory = null;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: <Widget>[
          SizedBox(height: 30,),
          imageMemory == null ? Container():SingleChildScrollView(child: Image.memory(imageMemory,width: 300,fit: BoxFit.fitWidth,),),
          imageMemory == null ?
          InkWell(
            child: Container(width: 160,height: 80,color: Colors.redAccent,),
            onTap: ()async {
              var pictureRecorder = ui.PictureRecorder(); // 图片记录仪
              var canvas =  Canvas(pictureRecorder); //canvas接受一个图片记录仪
              var images = await getImage('assets/rt.JPG'); // 使用方法获取Unit8List格式的图片
              // 绘制图片
              canvas.drawImage(images, Offset(0, 0), Paint());

              // 绘制文字
              ui.ParagraphBuilder pb = ui.ParagraphBuilder(ui.ParagraphStyle(
                textAlign: TextAlign.center, 
                fontWeight: FontWeight.bold,
                fontStyle: FontStyle.normal,
                fontSize: 50.0));
              pb.pushStyle(ui.TextStyle(color: Colors.black));
              pb.addText("贤合庄真的是太辣了🌶🌶🌶🌶🌶");
              ParagraphConstraints pc = ui.ParagraphConstraints(width: images.width.toDouble());
              ui.Paragraph paragraph = pb.build()..layout(pc);
              canvas.drawParagraph(paragraph, Offset(0, 200));

              //生成图片
              var picture = await pictureRecorder.endRecording().toImage(750, 1334);//设置生成图片的宽和高
              var pngImageBytes = await picture.toByteData(format: ui.ImageByteFormat.png);
              // var imgBytes = Uint8List.view(pngImageBytes.buffer);
              Uint8List pngBytes = pngImageBytes.buffer.asUint8List();
              setState(() {
                imageMemory = pngBytes;
              });
            },
          ):Container()
        ],
      ),
    );
  }

  // 本地图片转换成Uint8List的方法
  Future<ui.Image> getImage(String asset) async {
    ByteData data = await rootBundle.load(asset);
    Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
    FrameInfo frameInfo = await codec.getNextFrame();
    return frameInfo.image;
  }
}

点击按钮后,运行效果(存在延时):


是真的辣.png

13、雷达图

边长、位置、颜色等可高度自定义

class RadarDemo extends CustomPainter {

  int sideNumber = 6; // 多边形边数
  int layerNumber = 4; // 维度分几层
  double c_X;// view 的中心点
  double c_Y;
  double maxRadius; // 半径,最大的半径
  Paint linePaint; // 划线的画笔
  Path path; // 路径
  Paint maskPaint; // 遮罩的画笔

  RadarDemo(int sideNumber) {
    this.sideNumber = sideNumber;
    linePaint = Paint()
    ..color = randomRGB()
    ..isAntiAlias = true
    ..style = PaintingStyle.stroke;
    path = Path();

    maskPaint = Paint()
    ..color = randomARGB()
    ..isAntiAlias = true
    ..style = PaintingStyle.fill;
  }

  @override
  void paint(Canvas canvas, Size size) {
    c_X = size.width / 2;
    c_Y = size.height / 2;
    if (c_X > c_Y) {
      maxRadius = c_Y;
    } else {
      maxRadius = c_X;
    }
    canvas.save();
    drawPolygon(canvas);
    drawMaskLayer(canvas);
    canvas.restore();
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    return oldDelegate != this;
  }

  double eachRadius;
  double eachAngle;

  // 绘制多边形边框
  void drawPolygon(Canvas canvas) {
    ///每个角的度数
    eachAngle = 360 / sideNumber;

    ///找好所有的顶点,连接起来即可
    for (int i = 0; i < layerNumber; i++) {
      path.reset();
      eachRadius = maxRadius / layerNumber * (i + 1);

      for (int j = 0; j < sideNumber + 1; j++) {
        if (j == 0) {
          path.moveTo(c_X + eachRadius, c_Y);
        } else {
          double x = c_X + eachRadius * cos(degToRad(eachAngle * j));
          double y = c_Y + eachRadius * sin(degToRad(eachAngle * j));
          path.lineTo(x, y);
        }
      }
      path.close();
      canvas.drawPath(path, linePaint);
    }
    drawLineLinkPoint(canvas, eachAngle, eachRadius);
  }

  // 连接多边形顶点和中心点
  void drawLineLinkPoint(Canvas canvas, double eachAngle, double eachRadius) {
    path.reset();
    for (int i = 0; i < sideNumber; i++) {
      path.moveTo(c_X, c_Y);
      double x = c_X + eachRadius * cos(degToRad(eachAngle * i));
      double y = c_Y + eachRadius * sin(degToRad(eachAngle * i));
      path.lineTo(x, y);
      path.close();
      canvas.drawPath(path, linePaint);
    }
  }

  // 绘制遮罩
  void drawMaskLayer(Canvas canvas) {
    path.reset();
    for (int i = 0; i < sideNumber; i++) {
      double mRandomInt = randomInt();
      double x =
          c_X + maxRadius * cos(degToRad(eachAngle * i)) * mRandomInt;
      double y =
          c_Y + maxRadius * sin(degToRad(eachAngle * i)) * mRandomInt;
      if (i == 0) {
        path.moveTo(x, c_Y);
      } else {
        path.lineTo(x, y);
      }
    }
    path.close();
    canvas.drawPath(path, maskPaint);
  }

  num degToRad(num deg) => deg * (pi / 180.0);

  num radToDeg(num rad) => rad * (180.0 / pi);

  Color randomRGB() {
    Random random = new Random();
    return Color.fromARGB(
        255, random.nextInt(255), random.nextInt(255), random.nextInt(255));
  }
  Color randomARGB() {
    Random random = Random();
    return Color.fromARGB(random.nextInt(90), random.nextInt(255),
        random.nextInt(255), random.nextInt(255));
  }
  double randomInt() {
    Random random = new Random();
    return (random.nextInt(10) + 1) / 10;
  }
}

运行效果图:


13.png

简单demo : RCFlutterDrawDemo

上一篇 下一篇

猜你喜欢

热点阅读