flutter 基础 —— CustomPaint 解析

基础介绍

实现自定义组件大致有三种方式,第一种是组合现有的组件;第二种是直接构建 RenderObject,比如 ColoredBox 组件;第三种就是下面介绍的,CustomPaint,它与第二种类似,都是通过 canvas 去绘制图形。

坐标

(注意Y轴正方向是向下,数学中是向上)

 

 

 CustomPaint

三层结构

CustomPaint(
  // 事件区域,如 GestureDetector 事件只能作用在 size 范围内
  size: Size.infinite,
  // 背景层
  painter: MyPainter(),
  // 中间层
  child: Text("hello"),
  // 前景层
  foregroundPainter: null,
)

核心方法

class MyPainter extends CustomPainter {

  /// [size] 为 [CustomPaint] 构造方法传入的 size
  @override
  void paint(Canvas canvas, Size size) {

  }

  @override
  bool shouldRepaint(MyPainter oldDelegate) => this != oldDelegate;
}

  

 

各类图形的绘制

声明引入的包

import 'dart:math';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

  

绘制点

 示例:

 

 代码

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
          appBar: AppBar(
            title: Text("Custom Paint"),
          ),
          body: SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Container(
                  height: 30,
                  color: Colors.green,
                ),
                CustomPaint(
                  // 事件区域,如 GestureDetector 事件只能作用在 size 范围内
                  size: Size(100, 70),
                  // 背景层
                  painter: BasePainter(context),
                ),
              ],
            ),
          )),
    );
  }
}

class BasePainter extends CustomPainter {
  BasePainter(this.context);

  final BuildContext context;

  /// [size] 为 [CustomPaint] 构造方法传入的 size
  @override
  void paint(Canvas canvas, Size size) {
    var points1 = [
      Offset(size.width / 2, size.height / 2),
      Offset(size.width, size.height),
      Offset(size.width * 2, size.height * 2),
    ];
    //画点
    canvas.drawPoints(
        ui.PointMode.points,
        points1,
        Paint()
          ..strokeWidth = 5
          ..color = Colors.red);

    //点连成线(只有前2个点会连,后面的点不连)
    canvas.drawPoints(
        ui.PointMode.lines,
        points1,
        Paint()
          ..strokeWidth = 5
          ..color = Colors.blue);

    //多点连成线
    canvas.drawPoints(ui.PointMode.polygon, points1, Paint()..strokeWidth = 2);

    //默认画笔
    Paint _paint = Paint()..strokeWidth = 1;

    //分割线
    var separate1 = [
      Offset(0, 150),
      Offset(MediaQuery.of(context).size.width, 150),
    ];
    _paint.color = Colors.black;
    canvas.drawPoints(ui.PointMode.lines, separate1, _paint);
  }

  @override
  bool shouldRepaint(BasePainter oldDelegate) => this != oldDelegate;
}

 

 

 

绘制形状

绘制圆与椭圆

示例:

 

代码

//圆
canvas.drawCircle(Offset(150, 200), 20, _paint..style = ui.PaintingStyle.fill);
//椭圆(左上与右下,2个点确定)
canvas.drawOval(Rect.fromLTRB(200, 170, 300, 220), _paint);

 

绘制矩形

示例:

 

代码

//确定矩形的几种方式:
//中心点 + 宽高
// Rect.fromCenter(center: Offset(60, 200), width: 80, height: 100);
//中心点+ 半径(正方形)
// Rect.fromCircle(center: Offset(60, 250), radius: 40);
//左上 + 右下
// Rect.fromLTRB(20, 200, 100, 300);
//左上 + 右下
// Rect.fromPoints(Offset(20, 200), Offset(100, 300));
//左上 + 宽高
// Rect.fromLTWH(20, 200, 80, 100);
//左上 + 宽高
Rect rect = Offset(20, 240) & Size(180, 100);
canvas.drawRect(
    rect,
    Paint()
      ..strokeWidth = 2
      ..color = Colors.orange
      ..style = PaintingStyle.stroke);

 

绘制圆角矩形

示例:

 

代码

// 圆角矩形几种方式
// fromLTRBR/fromRectAndRadius:最后一个参数是圆角半径
// RRect rRect = RRect.fromLTRBR(120, 230, 160, 300, Radius.circular(10));
// RRect rRect = RRect.fromRectAndRadius(Rect.fromLTRB(120, 230, 160, 300), Radius.circular(10));
// fromLTRBAndCorners:可以分别设置四个角的半径
// RRect rRect = RRect.fromLTRBAndCorners(120, 230, 160, 300, topLeft: Radius.circular(10),);
// fromLTRBXY:最后两个参数XY确定的是椭圆弧度,不是半径相同的圆弧
// RRect rRect = RRect.fromLTRBXY(120, 230, 160, 300, 20, 10);
var rRect = RRect.fromRectAndRadius(rect, Radius.elliptical(20, 10));
canvas.drawRRect(
    rRect,
    Paint()
      ..strokeWidth = 2
      ..color = Colors.blue
      ..style = PaintingStyle.stroke);

  

绘制圆弧

 示例:

 

代码

// 绘制圆弧
// drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
// rect:跟椭圆一样,以矩形中心为原点画圆弧,其中 startAngle 开始角度,sweepAngle 为绘制多少角度,useCenter:是否和中心相连
canvas.drawArc(rect, pi / 2, pi / 2, false, Paint()..color = Colors.blue);
canvas.drawArc(rect, pi / 2, -pi / 2, false, Paint()..color = Colors.red);
canvas.drawArc(rect, -pi / 2, -pi / 2, false, Paint()..color = Colors.yellow);
canvas.drawArc(rect, -pi / 2, pi / 2, true, Paint()..color = Colors.green);

 

绘制图片

示例:

 

代码

class _HomePageState extends State<HomePage> {
  ui.Image? _image;

  @override
  void initState() {
    super.initState();
    //图片
    loadImage("images/test1.jpg").then((img) {
      setState(() {
        _image = img;
      });
    });
  }

  Future<ui.Image> loadImage(String path) async {
    ByteData data = await rootBundle.load(path);
    ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
    ui.FrameInfo fi = await codec.getNextFrame();
    return fi.image;
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
          appBar: AppBar(
            title: Text("Custom Paint"),
          ),
          body: SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Container(
                  height: 30,
                  color: Colors.green,
                ),
                CustomPaint(
                  // 事件区域,如 GestureDetector 事件只能作用在 size 范围内
                  size: Size(100, 70),
                  // 背景层
                  painter: BasePainter(context, _image),
                ),
              ],
            ),
          )),
    );
  }
}

//绘制图片
if (image != null) {
  canvas.drawImage(image!, Offset(220, 240), _paint);
}

 

 绘制路径

示例:

 

3阶贝赛尔曲线

 

代码

//绘制路径
var path = Path()
  ..moveTo(30, 350)
  ..lineTo(80, 400)
  ..lineTo(30, 400)
  ..close();
canvas.drawPath(
    path,
    Paint()
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke);

path.reset();

//二阶贝赛尔曲线 arcTo(跟圆弧类似)
//arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo)
//移动到点
path.moveTo(100, 380);
var rect1 = Rect.fromCircle(center: Offset(80, 450), radius: 60);
//由上点移动到圆弧,并连起来
path.arcTo(rect1, 0, pi, false);
var rect2 = Rect.fromCircle(center: Offset(80, 450), radius: 30);
//画一个圆,且不与上面的路径相连
path.arcTo(rect2, 0, 3.14*2, true);
canvas.drawPath(
    path,
    Paint()
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke);


path.reset();

//三阶贝塞尔曲线 cubicTo
//cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)
path.reset();
//确定桃心顶部中间点
path.moveTo(200, 400);
//画桃心左半边
path.cubicTo(150, 370, 150, 430, 200, 450);
//回到桃心顶部中间点
path.moveTo(200, 400);
//画桃心右半边
path.cubicTo(250, 370, 250, 430, 200, 450);
//上色填充
_paint.style = PaintingStyle.fill;
_paint.color = Colors.red;
canvas.drawPath(path, _paint);

 

完整版(包括动画层)

示例:

 

代码

import 'dart:math';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  ui.Image? _image;

  late AnimationController _controller;

  late Offset _center;

  late double _radius;

  bool _inner = false;

  @override
  void initState() {
    //圆心
    _center = Offset(100, 250);
    //半径
    _radius = 40;
    //动画
    _controller = AnimationController(vsync: this, duration: Duration(seconds: 1));
    _controller.repeat();
    //图片
    loadImage("images/test1.jpg").then((img) {
      setState(() {
        _image = img;
      });
    });
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  Future<ui.Image> loadImage(String path) async {
    ByteData data = await rootBundle.load(path);
    ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
    ui.FrameInfo fi = await codec.getNextFrame();
    return fi.image;
  }

  @override
  Widget build(BuildContext context) {
    print("${MediaQuery.of(context).size}");

    return SafeArea(
      child: Scaffold(
          appBar: AppBar(
            title: Text("Custom Paint"),
          ),
          body: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Container(
                height: 30,
                color: Colors.green,
              ),
              Expanded(
                  child: AnimatedBuilder(
                      animation: _controller,
                      builder: (context, child) {
                        return Stack(
                          children: [
                            //由于 CustomPaint 的事件作用域只限于 Size 框选的区域,所以不能把 GestureDetector 直接包在 CustomPaint 上
                            //而是通过 Stack 进行包装
                            GestureDetector(
                              behavior: HitTestBehavior.translucent,
                              onPanDown: (DragDownDetails details) {
                                //判断手指点击位置是否在圆内,在圆内即可拖动
                                var range =
                                    sqrt(pow(details.localPosition.dx - _center.dx, 2) + pow(details.localPosition.dy - _center.dy, 2));
                                if (range <= _radius) {
                                  _inner = true;
                                }
                              },
                              onPanUpdate: (DragUpdateDetails details) {
                                if (_inner) {
                                  setState(() {
                                    _center = details.localPosition;
                                  });
                                }
                              },
                              onPanEnd: (DragEndDetails details) {
                                _inner = false;
                              },
                            ),
                            CustomPaint(
                              // 事件区域,如 GestureDetector 事件只能作用在 size 范围内
                              size: Size(100, 70),
                              // 背景层
                              painter: BasePainter(context, _image),
                              foregroundPainter: CirclePainter(_controller.value, _center, _radius),
                              child: Container(
                                width: 200,
                                height: 200,
                                decoration: BoxDecoration(
                                  border: Border.all(width: 2, color: Colors.blue),
                                  // shape: BoxShape.circle,
                                  borderRadius: BorderRadius.circular(200),
                                  color: Colors.transparent,
                                ),
                              ),
                            )
                          ],
                        );
                      })),
            ],
          )),
    );
  }
}

class BasePainter extends CustomPainter {
  BasePainter(this.context, this.image);

  final BuildContext context;

  final ui.Image? image;

  /// [size] 为 [CustomPaint] 构造方法传入的 size
  @override
  void paint(Canvas canvas, Size size) {
    var points1 = [
      Offset(size.width / 2, size.height / 2),
      Offset(size.width, size.height),
      Offset(size.width * 2, size.height * 2),
    ];
    //画点
    canvas.drawPoints(
        ui.PointMode.points,
        points1,
        Paint()
          ..strokeWidth = 5
          ..color = Colors.red);

    //点连成线(只有前2个点会连,后面的点不连)
    canvas.drawPoints(
        ui.PointMode.lines,
        points1,
        Paint()
          ..strokeWidth = 5
          ..color = Colors.blue);

    //多点连成线
    canvas.drawPoints(ui.PointMode.polygon, points1, Paint()..strokeWidth = 2);

    //默认画笔
    Paint _paint = Paint()..strokeWidth = 1;

    //分割线
    var separate1 = [
      Offset(0, 150),
      Offset(MediaQuery.of(context).size.width, 150),
    ];
    _paint.color = Colors.black;
    canvas.drawPoints(ui.PointMode.lines, separate1, _paint);

    //圆
    canvas.drawCircle(Offset(150, 200), 20, _paint..style = ui.PaintingStyle.fill);
    //椭圆(左上与右下,2个点确定)
    canvas.drawOval(Rect.fromLTRB(200, 170, 300, 220), _paint);

    //确定矩形的几种方式:
    //中心点 + 宽高
    // Rect.fromCenter(center: Offset(60, 200), width: 80, height: 100);
    //中心点+ 半径(正方形)
    // Rect.fromCircle(center: Offset(60, 250), radius: 40);
    //左上 + 右下
    // Rect.fromLTRB(20, 200, 100, 300);
    //左上 + 右下
    // Rect.fromPoints(Offset(20, 200), Offset(100, 300));
    //左上 + 宽高
    // Rect.fromLTWH(20, 200, 80, 100);
    //左上 + 宽高
    Rect rect = Offset(20, 240) & Size(180, 100);
    canvas.drawRect(
        rect,
        Paint()
          ..strokeWidth = 2
          ..color = Colors.orange
          ..style = PaintingStyle.stroke);

    // 圆角矩形几种方式
    // fromLTRBR/fromRectAndRadius:最后一个参数是圆角半径
    // RRect rRect = RRect.fromLTRBR(120, 230, 160, 300, Radius.circular(10));
    // RRect rRect = RRect.fromRectAndRadius(Rect.fromLTRB(120, 230, 160, 300), Radius.circular(10));
    // fromLTRBAndCorners:可以分别设置四个角的半径
    // RRect rRect = RRect.fromLTRBAndCorners(120, 230, 160, 300, topLeft: Radius.circular(10),);
    // fromLTRBXY:最后两个参数XY确定的是椭圆弧度,不是半径相同的圆弧
    // RRect rRect = RRect.fromLTRBXY(120, 230, 160, 300, 20, 10);
    var rRect = RRect.fromRectAndRadius(rect, Radius.elliptical(20, 10));
    canvas.drawRRect(
        rRect,
        Paint()
          ..strokeWidth = 2
          ..color = Colors.blue
          ..style = PaintingStyle.stroke);

    // 绘制圆弧
    // drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
    // rect:跟椭圆一样,以矩形中心为原点画圆弧,其中 startAngle 开始角度,sweepAngle 为绘制多少角度,useCenter:是否和中心相连
    canvas.drawArc(rect, pi / 2, pi / 2, false, Paint()..color = Colors.blue);
    canvas.drawArc(rect, pi / 2, -pi / 2, false, Paint()..color = Colors.red);
    canvas.drawArc(rect, -pi / 2, -pi / 2, false, Paint()..color = Colors.yellow);
    canvas.drawArc(rect, -pi / 2, pi / 2, true, Paint()..color = Colors.green);

    //绘制图片
    if (image != null) {
      canvas.drawImage(image!, Offset(220, 240), _paint);
    }

    //绘制路径
    var path = Path()
      ..moveTo(30, 350)
      ..lineTo(80, 400)
      ..lineTo(30, 400)
      ..close();
    canvas.drawPath(
        path,
        Paint()
          ..strokeWidth = 2
          ..style = PaintingStyle.stroke);

    path.reset();

    //二阶贝赛尔曲线 arcTo(跟圆弧类似)
    //arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo)
    //移动到点
    path.moveTo(100, 380);
    var rect1 = Rect.fromCircle(center: Offset(80, 450), radius: 60);
    //由上点移动到圆弧,并连起来
    path.arcTo(rect1, 0, pi, false);
    var rect2 = Rect.fromCircle(center: Offset(80, 450), radius: 30);
    //画一个圆,且不与上面的路径相连
    path.arcTo(rect2, 0, 3.14 * 2, true);
    canvas.drawPath(
        path,
        Paint()
          ..strokeWidth = 2
          ..style = PaintingStyle.stroke);

    path.reset();

    //三阶贝塞尔曲线 cubicTo
    //cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)
    path.reset();
    //确定桃心顶部中间点
    path.moveTo(200, 400);
    //画桃心左半边
    path.cubicTo(150, 370, 150, 430, 200, 450);
    //回到桃心顶部中间点
    path.moveTo(200, 400);
    //画桃心右半边
    path.cubicTo(250, 370, 250, 430, 200, 450);
    //上色填充
    _paint.style = PaintingStyle.fill;
    _paint.color = Colors.red;
    canvas.drawPath(path, _paint);
  }

  @override
  bool shouldRepaint(BasePainter oldDelegate) => this != oldDelegate;
}

class CirclePainter extends CustomPainter {
  CirclePainter(this._controller, this._center, this._radius);

  final double _controller;

  final Offset _center;

  final double _radius;

  Paint _paint = Paint()
    ..color = Colors.red
    ..strokeWidth = 3
    ..style = PaintingStyle.stroke;

  @override
  void paint(Canvas canvas, Size size) {
    var rect = Rect.fromCircle(center: _center, radius: _radius);
    canvas.drawArc(rect, .0, _controller * 2 * pi, false, _paint);
  }

  @override
  bool shouldRepaint(CirclePainter oldDelegate) => this != oldDelegate;
}

 

233

 参考文章


https://blog.csdn.net/Gold_brick/article/details/117370461

posted on 2022-01-12 10:52  Lemo_wd  阅读(548)  评论(0编辑  收藏  举报

导航