都来学学这个Flutter的贝塞尔曲线吧

1,719 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

  • 看完本文后,你将知道什么是贝塞尔曲线、Flutter中如何实现贝塞尔曲线以及如何结合动画使用~

概念公式

Bézier curve(贝塞尔曲线)是应用于二维图形应用程序的数学曲线。 曲线定义:起始点、终止点(也称锚点)、控制点。通过调整控制点,贝塞尔曲线的形状会发生变化。 1962年,法国数学家Pierre Bézier第一个研究了这种矢量绘制曲线的方法,并给出了详细的计算公式,因此按照这样的公式绘制出来的曲线就用他的姓氏来命名,称为贝塞尔曲线。 N个点,即能确定一个N-1阶贝塞尔曲线

二阶贝塞尔

image.png 始终遵循 AD:DB = BE:EC = DF:FE = t,t为0-1

二阶 (P0起点,P1控制点,P2终点) 2021-10-26 17.19.09.gif

三阶贝塞尔

三阶 2021-10-26 17.22.44.gif

n阶贝塞尔

n阶

flutter中的贝塞尔示例(含注释)

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

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

class _BezierCurvePageState extends State<BezierCurvePage>
    with TickerProviderStateMixin {
  List<Offset> _points = <Offset>[];
  AnimationController? _animationController;
  Animation<double>? _animation;
  int n = 3;//n-1 = 2或3阶

  @override
  void initState() {
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(seconds: 3),
    );
    _animation = Tween(begin: 0.0, end: 1.0).animate(_animationController!)
      ..addListener(() {
        setState(() {});
      });
    super.initState();
  }

  @override
  void dispose() {
    _animationController?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("贝塞尔曲线"),
      ),
      body: Stack(
        children: [
          Listener(
            child: Container(
              alignment: Alignment.center,
              color: Colors.white,
              width: ScreenUtil().screenWidth,
              height: ScreenUtil().screenHeight,
            ),
            onPointerDown: (PointerDownEvent event) {
              setState(() {
                if (_points.length < n) {
                  _points.add(event.localPosition);
                  if(_points.length == n){
                    startAnim();
                  }
                }else{
                  _animationController?.reset();
                  _points.clear();
                  _points.add(event.localPosition);
                }
              });
            },
          ),
          CustomPaint(
            painter: DrawBezierPainter(_points, _animation?.value ?? 0.0,n),
          ),
        ],
      ),
    );
  }

  startAnim() {
    _animationController?.forward();
  }


}

class DrawBezierPainter extends CustomPainter {
  Paint dotPaint = new Paint();
  Paint linePaint = new Paint();
  Paint subLinePaint = new Paint()..color = Colors.grey;
  Paint bezierPaint = new Paint();

  double progress = 0.0;
  int n;

  List<Offset> points = [];

  DrawBezierPainter(this.points, this.progress,this.n);

  @override
  void paint(Canvas canvas, Size size) {
    dotPaint.color = Colors.blue;
    dotPaint.strokeCap = StrokeCap.square;
    dotPaint.strokeWidth = 5.0;
    for (int i = 0; i < (points.length); i++) {
      canvas.drawCircle(points[i], 5, dotPaint);
      if (i + 1 < points.length) {
        canvas.drawLine(points[i], points[i + 1], linePaint);
      }
    }
    if (progress > 0) {
      List<Offset> subLevelPoints =
          getAllPointsWithOriginPoints(points, progress);
      if (subLevelPoints.length == n) {//3   2阶
        canvas.drawLine(subLevelPoints[0], subLevelPoints[1], subLinePaint);
        canvas.drawCircle(subLevelPoints[2], 5, bezierPaint);
      }else if (subLevelPoints.length == 6){//6  3阶
        canvas.drawLine(subLevelPoints[0], subLevelPoints[1], subLinePaint);
        canvas.drawLine(subLevelPoints[1], subLevelPoints[2], subLinePaint);
        canvas.drawLine(subLevelPoints[3], subLevelPoints[4], subLinePaint
                                                                  ..color = Colors.blue
                                                                  ..strokeWidth = 1);
        canvas.drawCircle(subLevelPoints[5], 5, bezierPaint);
      }

      Path bezierPath = new Path();
      bezierPath.moveTo(points[0].dx, points[0].dy); //起点
      if(n == 3){
        bezierPath.quadraticBezierTo(
            points[1].dx,
            points[1].dy,
            points[2].dx,
            points[2].dy); //控制点,终点
      }else if (n == 4){
        bezierPath.cubicTo(points[1].dx, points[1].dy, points[2].dx, points[2].dy, points[3].dx, points[3].dy);
      }
      canvas.drawPath(
          bezierPath,
          bezierPaint
            ..strokeWidth = 1
            ..color = Colors.red
            ..style = PaintingStyle.stroke);
      // isAntiAlias(抗锯齿) color(颜色)          blendMode(混合模式)     style(画笔样式)
      // strokeWidth(线宽)   strokeCap(线帽类型)  strokeJoin(线接类型)    strokeMiterLimit(斜接限制)
      // maskFilter(遮罩滤镜) shader(着色器)      colorFilter(颜色滤镜)    imageFilter(图片滤镜)
      // invertColors(是否反色)                  filterQuality(滤镜质量)
    }
  }

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

  /**
      获取根据touch点衍生出来所有的点

      @param points 手动点击的点
      @param progress 贝塞尔曲线当前进度
      @return pointArray
   */
  List<Offset> getAllPointsWithOriginPoints(
      List<Offset> points, double progress) {
    List<Offset> resultArray = [];
    int level = points.length;

    List<Offset> tempArray = points;
    for (int i = 0; i < level; i++) {
      tempArray = getsubLevelPointsWithSuperPoints(tempArray, progress);
      // 中间阶级的点
      resultArray.addAll(tempArray);
      if (tempArray.length == 1) {
        // 最终贝塞尔曲线的点
        // [_bezierPathPoints addObjectsFromArray: tempArray];
        break;
      }
    }
    return resultArray;
  }

  /**
      根据上一级的点获取下一级的点
   */
  List<Offset> getsubLevelPointsWithSuperPoints(
      List<Offset> points, double progress) {
    List<Offset> tempArr = [];
    for (int i = 0; i < points.length - 1; i++) {
      Offset prePoint = points[i];

      Offset lastPoint = points[i + 1];
      double diffX = lastPoint.dx - prePoint.dx;
      double diffY = lastPoint.dy - prePoint.dy;

      Offset currentPoint = Offset(
          prePoint.dx + diffX * progress, prePoint.dy + diffY * progress);
      tempArr.add(currentPoint);
    }
    return tempArr;
  }
}

Flutter中使用贝塞尔+动画(含注释)

在日常开发中,我们可以使用贝塞尔曲线结合动画完成一些需求.

2021-10-27 13.40.52.gif


import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

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

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

class _RotatePageState extends State<RotatePage> with TickerProviderStateMixin {
  AnimationController? _leftController,
      _rightController,
      _propellerController; // 动画 controller
  Animation<double>? _leftAnimation, _rightAnimation, _propellerAnimation; // 动画
  double? leftViewToLeft; // 小圆点的left(动态计算)
  double? leftViewToTop; // 小远点的right(动态计算)
  double? rightViewToLeft; // 小圆点的left(动态计算)
  double? rightViewToTop; // 小远点的right(动态计算)
  double angle = 0;
  double shipSize = 50;
  double heartWidth = ScreenUtil().screenWidth;
  double heartHeight = ScreenUtil().screenWidth + 200;

  @override
  void initState() {
    super.initState();
    addLeftAnimation();
    addRightAnimation();
    addPropellerAnimation();
  }

  addLeftAnimation() {
    _leftController =
        AnimationController(duration: Duration(seconds: 5), vsync: this);
    _leftAnimation = Tween(begin: 1.0, end: 0.0).animate(_leftController!);

    var x0Left = heartWidth / 2;
    var y0Left = heartHeight / 4;

    var x1Left = heartWidth / 7;
    var y1Left = heartHeight / 9;

    var x2Left = heartWidth / 21;
    var y2Left = (heartHeight * 2) / 5;

    var x3Left = heartWidth / 2;
    var y3Left = heartHeight * 7 / 12;

    _leftAnimation!.addListener(() {
      if (_leftAnimation!.isCompleted) {
        _leftController!.reverse();
      }
      // t 动态变化的值
      var t = _leftAnimation!.value;
      if (mounted)
        setState(() {
// B3(t) = (1-t)^3*P0+3t(1-t)^2*P1+3t^2(1-t)*P2+t^3*P3,  t属于 [0,1]
          rightViewToLeft = pow(1 - t, 3) * x0Left +
              3 * t * pow(1 - t, 2) * x1Left +
              3 * pow(t, 2) * (1 - t) * x2Left +
              pow(t, 3) * x3Left;

          rightViewToTop = pow(1 - t, 3) * y0Left +
              3 * t * pow(1 - t, 2) * y1Left +
              3 * pow(t, 2) * (1 - t) * y2Left +
              pow(t, 3) * y3Left;
        });
    });

    // 初始化小圆点的位置
    rightViewToLeft = x0Left;
    rightViewToTop = y0Left;

    //显示小圆点的时候动画就开始
    _leftController!.forward();
  }

  addRightAnimation() {
    _rightController =
        AnimationController(duration: Duration(seconds: 5), vsync: this);
    _rightAnimation = Tween(begin: 1.0, end: 0.0).animate(_rightController!);
    var x0Right = heartWidth / 2;
    var y0Right = heartHeight / 4;

    var x1Right = (heartWidth * 6) / 7;
    var y1Right = heartHeight / 9;

    var x2Right = (heartWidth * 13) / 13;
    var y2Right = (heartHeight * 2) / 5;

    var x3Right = heartWidth / 2;
    var y3Right = heartHeight * 7 / 12;

    _rightAnimation!.addListener(() {
      if (_rightAnimation!.isCompleted) {
        _rightController!.reverse();
      }
      // t 动态变化的值
      var t = _rightAnimation!.value;
      if (mounted)
        setState(() {
// B3(t) = (1-t)^3*P0+3t(1-t)^2*P1+3t^2(1-t)*P2+t^3*P3,  t属于 [0,1]
          leftViewToLeft = pow(1 - t, 3) * x0Right +
              3 * t * pow(1 - t, 2) * x1Right +
              3 * pow(t, 2) * (1 - t) * x2Right +
              pow(t, 3) * x3Right;

          leftViewToTop = pow(1 - t, 3) * y0Right +
              3 * t * pow(1 - t, 2) * y1Right +
              3 * pow(t, 2) * (1 - t) * y2Right +
              pow(t, 3) * y3Right;
        });
    });

    // 初始化小圆点的位置
    leftViewToLeft = x0Right;
    leftViewToTop = y0Right;

    //显示小圆点的时候动画就开始
    _rightController!.forward();
  }

  addPropellerAnimation() {
    _propellerController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 800));
    _propellerAnimation =
        Tween(begin: 0.0, end: pi).animate(_propellerController!);
    _propellerController?.repeat(); // 循环播放动画
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        _childWidget(leftViewToLeft!, leftViewToTop!),
        _childWidget(rightViewToLeft!, rightViewToTop!),
        CustomPaint(
          painter: HeartShapedPainter(heartWidth, heartHeight),
          size: Size(heartWidth, heartHeight),
        )
      ],
    );
  }

  Widget _childWidget(double toLeft, double toTop) {
    return Positioned(
      left: toLeft - shipSize / 2,
      top: toTop - shipSize / 2,
      child: Stack(children: [
        Image.network(
          "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic38.nipic.com%2F20140226%2F18047467_102622574149_2.jpg&refer=http%3A%2F%2Fpic38.nipic.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1634376105&t=277f3070e1c9b37b0971c8ac79086c81",
          width: shipSize,
          height: shipSize,
        ),
        Positioned(
          left: shipSize / 2,
          top: shipSize / 2,
          child: AnimatedBuilder(
              animation: _leftAnimation!,
              builder: (context, _) => Transform.rotate(
                    alignment: Alignment.topLeft,
                    angle: _propellerAnimation!.value,
                    child: Container(
                      color: Colors.yellow,
                      width: 200,
                      height: 10,
                    ),
                  )),
        ),
      ]),
    );
  }

  @override
  void dispose() {
    _leftController?.dispose();
    _rightController?.dispose();
    _propellerController?.dispose();
    super.dispose();
  }
}

class HeartShapedPainter extends CustomPainter {
  double width;
  double height;

  HeartShapedPainter(this.width, this.height);

  Paint _paint = Paint()..strokeWidth = 2.0;

  @override
  void paint(Canvas canvas, Size size) {
    /// 三阶贝塞尔曲线

    ///p0 = width / 2, height / 4
    ///p1 = (width * 6) / 7, height / 9)
    ///p2 = (width * 13) / 13, (height * 2) / 5)
    ///p3 = width / 2, (height * 7) / 12

    /// 右边
    Path path1 = new Path();
    path1.moveTo(width / 2, height / 4);
    path1.cubicTo((width * 6) / 7, height / 9, (width * 13) / 13,
        (height * 2) / 5, width / 2, (height * 7) / 12);
    canvas.drawPath(
        path1,
        _paint
          ..color = Colors.redAccent
          ..style = PaintingStyle.stroke);

    ///左边
    Path path2 = new Path();
    path2.moveTo(width / 2, height / 4);
    path2.cubicTo(width / 7, height / 9, width / 21, (height * 2) / 5,
        width / 2, (height * 7) / 12);
    canvas.drawPath(
        path2,
        _paint
          ..color = Colors.redAccent
          ..style = PaintingStyle.stroke);

  }

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