likes
comments
collection
share

Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)

作者站长头像
站长
· 阅读数 68

[》跳过拾光记忆]

拾光记忆

简介: 该篇主要介绍15 篇文章含有功能的目录,可根据自己的需求选择对应的功能介绍查看。 推荐: ⭐️⭐️⭐️⭐️⭐️

简介: 该篇主要介绍 Flutter 之 IImage 库中如何实现图像反色功能以及实现原理的介绍。 推荐: ⭐️⭐️⭐️⭐️⭐️

简介: 该篇主要介绍 Flutter 之 图形(Canvas) 绘制路径 (Path)基础功能以及方法实现底层代码的解析。 推荐: ⭐️⭐️⭐️⭐️⭐️

[返回拾光记忆《]

一、简述

路径(Path)在图像绘制 (Canvas) 领域占有很大的地位。 你对各个开发语言的 Ptah 有多少的了解? 同时作为 Flutter 开发者,你又对 flutter 中的 Canvas-Path 又有多深的认知? 如果你感觉了解的还不是很多以及很深,这篇文章将带你进入 Path 深入了解之中。

二、Path 方法详解

1. void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)
  • 方法介绍 该方法是从当前点创建以 (x1,y1)(x2,y2) 为控制点的三阶贝塞尔曲线。该方法底层调用了 Path::cubicTo 方法,这个方法的代码实现如下所示:

    void CanvasPath::cubicTo(double x1,
                             double y1,
                             double x2,
                             double y2,
                             double x3,
                             double y3) {
      mutable_path().cubicTo(SafeNarrow(x1), SafeNarrow(y1), SafeNarrow(x2),
                             SafeNarrow(y2), SafeNarrow(x3), SafeNarrow(y3));
      resetVolatility();
    }
    

    在上述方法中又调用了 SkPathcubicTo 方法,该方法的代码实现如下所示:

    SkPath& SkPath::cubicTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2,
                            SkScalar x3, SkScalar y3) {
        SkDEBUGCODE(this->validate();)
        this->injectMoveToIfNeeded();
        SkPathRef::Editor ed(&fPathRef);
        SkPoint* pts = ed.growForVerb(kCubic_Verb);
        pts[0].set(x1, y1);
        pts[1].set(x2, y2);
        pts[2].set(x3, y3);
        return this->dirtyAfterEdit();
    }
    
  • 应用实例 在 Flutter 中该方法的使用实例如下:

    void cubicTo(Canvas canvas) {
      final Path path = Path();
      path.moveTo(100, 100);
      path.cubicTo(200, 20, 300, 180, 400, 100);
      canvas.drawPath(
        path,
        Paint()
          ..color = Colors.red
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2,
      );
    }
    

    上述代码的运行效果如下:

    Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)
2. void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3)
  • 方法介绍 该方法是:添加以当前点开始到偏移 (x3,y3) 点的三阶贝塞尔曲线,并以当前点偏移 (x1,y1)(x2,y2) 为控制点。 该方法底层调用了 Path::relativeCubicTo 方法,这个方法的代码实现如下:

    void CanvasPath::relativeCubicTo(double x1,
                                     double y1,
                                     double x2,
                                     double y2,
                                     double x3,
                                     double y3) {
      mutable_path().rCubicTo(SafeNarrow(x1), SafeNarrow(y1), SafeNarrow(x2),
                              SafeNarrow(y2), SafeNarrow(x3), SafeNarrow(y3));
      resetVolatility();
    }
    

    上面代码又调用了 SkPathSkPath& SkPath::rCubicTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2, SkScalar x3, SkScalar y3) 方法,这个方法的实现代码如下:

    SkPath& SkPath::rCubicTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2,
                             SkScalar x3, SkScalar y3) {
        this->injectMoveToIfNeeded();  // This can change the result of this->getLastPt().
        SkPoint pt;
        this->getLastPt(&pt);
        return this->cubicTo(pt.fX + x1, pt.fY + y1, pt.fX + x2, pt.fY + y2,
                             pt.fX + x3, pt.fY + y3);
    }
    

    从上边代码的第 6 行,可知这个方法最后也是调用 cubicTo 方法,然后传递的参数是当前点加上我们传入的参数。

  • 实例应用 在 Flutter 中该方法的调用实例如下:

    void relativeCubicTo(Canvas canvas) {
      final Path path = Path();
      path.moveTo(100, 100);
      path.relativeCubicTo(100, -80, 200, 80, 300, 0);
      canvas.drawPath(
        path,
        Paint()
          ..color = Colors.red
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2,
      );
    }
    

    上面代码运行效果如下所示:

    Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二) 从上图可知,该方法传入的参数是以当前点为新的坐标原点而设置的数据。

3. void conicTo(double x1, double y1, double x2, double y2, double w)
  • 方法介绍 该方法是创建从当前点到 (x2,y2)点的贝塞尔曲线,并以 (x1,y1) 为控制点;w 为曲线的权重。

    权重:是控制点对曲线的影响程度。 w > 1 时,曲线是双曲线; w = 1 时,曲线是抛物线; w < 1 时,曲线是椭圆; w = 0 时,曲线是直线。

    该方法底层是调用了 Path::conicTo 方法,这个方法代码实现如下:

    void CanvasPath::conicTo(double x1, double y1, double x2, double y2, double w) {
      mutable_path().conicTo(SafeNarrow(x1), SafeNarrow(y1), SafeNarrow(x2),
                             SafeNarrow(y2), SafeNarrow(w));
      resetVolatility();
    }
    

    上述代码的第 2 行又调用了 SkPathSkPath& SkPath::conicTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2, SkScalar w) 方法,该方法代码实现如下:

    SkPath& SkPath::conicTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2,
                            SkScalar w) {
        // check for <= 0 or NaN with this test
        if (!(w > 0)) {
            this->lineTo(x2, y2);
        } else if (!SkScalarIsFinite(w)) {
            this->lineTo(x1, y1);
            this->lineTo(x2, y2);
        } else if (SK_Scalar1 == w) {
            this->quadTo(x1, y1, x2, y2);
        } else {
            SkDEBUGCODE(this->validate();)
            this->injectMoveToIfNeeded();
            SkPathRef::Editor ed(&fPathRef);
            SkPoint* pts = ed.growForVerb(kConic_Verb, w);
            pts[0].set(x1, y1);
            pts[1].set(x2, y2);
            (void)this->dirtyAfterEdit();
        }
        return *this;
    }
    

    从上述代码可知,我们 w 值的不同代码将执行不同的实现。

  • 实例应用 在 Flutter 中该方法的应用实例如下所示:

    void conicTo(Canvas canvas) {
      final Path path = Path();
      path.moveTo(100, 100);
      path.conicTo(250, 10, 400, 100, 6);
      canvas.drawPath(
        path,
        Paint()
          ..color = Colors.purple
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2,
      );
    
      final Path path1 = Path();
      path1.moveTo(100, 100);
      path1.conicTo(250, 10, 400, 100, 1);
      canvas.drawPath(
        path1,
        Paint()
          ..color = Colors.red
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2,
      );
    
      final Path path2 = Path();
      path2.moveTo(100, 100);
      path2.conicTo(250, 10, 400, 100, 0.5);
      canvas.drawPath(
        path2,
        Paint()
          ..color = Colors.green
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2,
      );
    
      final Path path3 = Path();
      path3.moveTo(100, 100);
      path3.conicTo(250, 10, 400, 100, 0);
      canvas.drawPath(
        path3,
        Paint()
          ..color = Colors.orange
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2,
      );
    
      canvas.drawPoints(
        PointMode.points,
        const [Offset(100, 100), Offset(250, 10), Offset(400, 100)],
        Paint()
          ..color = Colors.green
          ..strokeWidth = 6,
      );
    }
    

    上述代码的运行效果如下:

    Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)

    上面视图已经标出不同的 w 值,显示不同的曲线。

4. void relativeConicTo(double x1, double y1, double x2, double y2, double w)
  • 方法介绍 该方法的从当前点开始偏移 (x1,y1)点为控制点,并以 w 为权重,生成到当前点偏移 (x2,y2) 点的曲线。

    注意: 当前点的偏移点也可以理解为以当前点为新的原点的点。

    上述方法底层调用了 Path::relativeConicTo方法,这个方法的实现代码如下:

    void CanvasPath::relativeConicTo(double x1,
                                     double y1,
                                     double x2,
                                     double y2,
                                     double w) {
      mutable_path().rConicTo(SafeNarrow(x1), SafeNarrow(y1), SafeNarrow(x2),
                              SafeNarrow(y2), SafeNarrow(w));
      resetVolatility();
    }
    

    上述方法第 6 行又调用了 SkPathSkPath& SkPath::rConicTo(SkScalar dx1, SkScalar dy1, SkScalar dx2, SkScalar dy2, SkScalar w) 方法,这个方法的代码实现如下:

    SkPath& SkPath::rConicTo(SkScalar dx1, SkScalar dy1, SkScalar dx2, SkScalar dy2,
                             SkScalar w) {
        this->injectMoveToIfNeeded();  // This can change the result of this->getLastPt().
        SkPoint pt;
        this->getLastPt(&pt);
        return this->conicTo(pt.fX + dx1, pt.fY + dy1, pt.fX + dx2, pt.fY + dy2, w);
    }
    

    从上述代码的第 6 行可以看出,该方法是调用了 conicTo 方法,参数是当前点加上我们传入的参数。

  • 实例应用 该方法在 Flutter 中的应用实例如下:

    void relativeConicTo(Canvas canvas) {
      final Path path = Path();
      path.moveTo(100, 200);
      path.relativeConicTo(100, -200, 200, 0, 2);
      canvas.drawPath(
        path,
        Paint()
          ..color = Colors.purple
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2,
      );
    }
    

    上述代码运行的效果如下:

    Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)

    上边只演示了 w=2 的情况,w 的其他情况和 conicTo 的一样。

5. void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo)
  • 方法介绍 该方法是在给定的矩形中创建一段内切椭圆的一段圆弧,如果参数 forceMoveTotrue新起一段路径添加一段圆弧;如果为false,则是直接添加一条线段和一段圆弧。该方法的底层实现如下:

    void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) {
      assert(_rectIsValid(rect));
      _arcTo(rect.left, rect.top, rect.right, rect.bottom, startAngle, sweepAngle, forceMoveTo);
    }
    

    上述代码中的第 2 行,是判断传入的矩形是否为 null 以及 矩形是否可以表示出来。 上述代码第 3 行调用了 void _arcTo( double left, double top, double right, double bottom, double startAngle, double sweepAngle, bool forceMoveTo) 方法,该方法又调用了 Path::arcTo 方法,这个方法的底层实现如下:

    void CanvasPath::arcTo(double left,
                           double top,
                           double right,
                           double bottom,
                           double startAngle,
                           double sweepAngle,
                           bool forceMoveTo) {
      mutable_path().arcTo(
          SkRect::MakeLTRB(SafeNarrow(left), SafeNarrow(top), SafeNarrow(right),
                           SafeNarrow(bottom)),
          SafeNarrow(startAngle) * 180.0f / static_cast<float>(M_PI),
          SafeNarrow(sweepAngle) * 180.0f / static_cast<float>(M_PI), forceMoveTo);
      resetVolatility();
    }
    

    上述方法的第 8 行,调用了 SkPathSkPath& SkPath::arcTo(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle, bool forceMoveTo) ,该方法的底层实现如下:

    SkPath& SkPath::arcTo(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle,
                          bool forceMoveTo) {
        if (oval.width() < 0 || oval.height() < 0) {
            return *this;
        }
    
        startAngle = SkScalarMod(startAngle, 360.0f);
    
        if (fPathRef->countVerbs() == 0) {
            forceMoveTo = true;
        }
    
        SkPoint lonePt;
        if (arc_is_lone_point(oval, startAngle, sweepAngle, &lonePt)) {
            return forceMoveTo ? this->moveTo(lonePt) : this->lineTo(lonePt);
        }
    
        SkVector startV, stopV;
        SkRotationDirection dir;
        angles_to_unit_vectors(startAngle, sweepAngle, &startV, &stopV, &dir);
    
        SkPoint singlePt;
    
        // Adds a move-to to 'pt' if forceMoveTo is true. Otherwise a lineTo unless we're sufficiently
        // close to 'pt' currently. This prevents spurious lineTos when adding a series of contiguous
        // arcs from the same oval.
        auto addPt = [&forceMoveTo, this](const SkPoint& pt) {
            SkPoint lastPt;
            if (forceMoveTo) {
                this->moveTo(pt);
            } else if (!this->getLastPt(&lastPt) ||
                       !SkScalarNearlyEqual(lastPt.fX, pt.fX) ||
                       !SkScalarNearlyEqual(lastPt.fY, pt.fY)) {
                this->lineTo(pt);
            }
        };
    
        // At this point, we know that the arc is not a lone point, but startV == stopV
        // indicates that the sweepAngle is too small such that angles_to_unit_vectors
        // cannot handle it.
        if (startV == stopV) {
            SkScalar endAngle = SkDegreesToRadians(startAngle + sweepAngle);
            SkScalar radiusX = oval.width() / 2;
            SkScalar radiusY = oval.height() / 2;
            // We do not use SkScalar[Sin|Cos]SnapToZero here. When sin(startAngle) is 0 and sweepAngle
            // is very small and radius is huge, the expected behavior here is to draw a line. But
            // calling SkScalarSinSnapToZero will make sin(endAngle) be 0 which will then draw a dot.
            singlePt.set(oval.centerX() + radiusX * SkScalarCos(endAngle),
                         oval.centerY() + radiusY * SkScalarSin(endAngle));
            addPt(singlePt);
            return *this;
        }
    
        SkConic conics[SkConic::kMaxConicsForArc];
        int count = build_arc_conics(oval, startV, stopV, dir, conics, &singlePt);
        if (count) {
            this->incReserve(count * 2 + 1);
            const SkPoint& pt = conics[0].fPts[0];
            addPt(pt);
            for (int i = 0; i < count; ++i) {
                this->conicTo(conics[i].fPts[1], conics[i].fPts[2], conics[i].fW);
            }
        } else {
            addPt(singlePt);
        }
        return *this;
    }
    
  • 实例应用 在 Flutter 中该方法的应用实例如下:

    void arcTo(Canvas canvas) {
      final Path path = Path();
      path.moveTo(100, 100);
      path.arcTo(Rect.fromCenter(center: const Offset(400, 200), width: 300, height: 200), 0, pi, false);
      canvas.drawPath(
        path,
        Paint()
          ..color = Colors.purple
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2,
      );
    }
    

    上述代码在参数forceMoveTotrue 或者 false 运行的视图结果如下:

    • forceMoveTo = true

      Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)
    • forceMoveTo = false

      Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)

    从上述可知,控制参数的不同可以获取不同的效果。 需要注意的是:

    1. 如果绘制没有设置绘制的起点,则 forceMoveTo 设置为 false 时,底层实现也是按 true 获取显示效果的。测试代码如下:

      void arcTo(Canvas canvas) {
        final Path path = Path();
        // path.moveTo(100, 100);
        path.arcTo(Rect.fromCenter(center: const Offset(400, 200), width: 300, height: 200), 0, pi, false);
        canvas.drawPath(
          path,
          Paint()
            ..color = Colors.purple
            ..style = PaintingStyle.stroke
            ..strokeWidth = 2,
        );
      }
      

      上面代码的运行视图效果如下:

      Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)

      看着这个图,是否很熟悉,这不是和有绘制起点同时 forceMoveTo 设置为 true 的结果一样吗? 是的你没有看错。这是 SkPath& SkPath::arcTo(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle, bool forceMoveTo) 方法底层做了相关的处理,处理代码如下:

      if (fPathRef->countVerbs() == 0) {
         forceMoveTo = true;
      }
      
    2. 如果 sweepAngle = startAngle + 2k*pi 则绘制没有圆弧,测试代码如下:

      void arcTo(Canvas canvas) {
        final Path path = Path();
        path.moveTo(100, 100);
        path.arcTo(Rect.fromCenter(center: const Offset(400, 200), width: 300, height: 200), 0, pi * 2, false);
        canvas.drawPath(
          path,
          Paint()
            ..color = Colors.purple
            ..style = PaintingStyle.stroke
            ..strokeWidth = 2,
        );
      }
      

      上述代码运行效果如下:

      Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)
6. void arcToPoint(Offset arcEnd, {Radius radius = Radius.zero,double rotation = 0.0,bool largeArc = false,bool clockwise = true,})
  • 方法介绍 该方法是从路径的最后一个点到给定点绘制曲线,同时,它还有 radiusrotationlargeArcclockwise 四个参数可以自定义,以改变曲线的样式。该方法的底层代码如下:

      void arcToPoint(
        Offset arcEnd, {
        Radius radius = Radius.zero,
        double rotation = 0.0,
        bool largeArc = false,
        bool clockwise = true,
      }) {
        assert(_offsetIsValid(arcEnd));
        assert(_radiusIsValid(radius));
        _arcToPoint(arcEnd.dx, arcEnd.dy, radius.x, radius.y, rotation, largeArc, clockwise);
      }
    

    上述方法的第 11 行,有调用了 Path::arcToPoint 方法,该方法的底层代码如下:

    void CanvasPath::arcToPoint(double arcEndX,
                                double arcEndY,
                                double radiusX,
                                double radiusY,
                                double xAxisRotation,
                                bool isLargeArc,
                                bool isClockwiseDirection) {
      const auto arcSize = isLargeArc ? SkPath::ArcSize::kLarge_ArcSize
                                      : SkPath::ArcSize::kSmall_ArcSize;
      const auto direction =
          isClockwiseDirection ? SkPathDirection::kCW : SkPathDirection::kCCW;
    
      mutable_path().arcTo(SafeNarrow(radiusX), SafeNarrow(radiusY),
                           SafeNarrow(xAxisRotation), arcSize, direction,
                           SafeNarrow(arcEndX), SafeNarrow(arcEndY));
      resetVolatility();
    }
    

    上述方法的第 13 行又调用了 SkPath& SkPath::arcTo(SkScalar rx, SkScalar ry, SkScalar angle, SkPath::ArcSize arcLarge, SkPathDirection arcSweep, SkScalar x, SkScalar y) 方法,这个方法的底层实现如下:

    SkPath& SkPath::arcTo(SkScalar rx, SkScalar ry, SkScalar angle, SkPath::ArcSize arcLarge,
                          SkPathDirection arcSweep, SkScalar x, SkScalar y) {
        this->injectMoveToIfNeeded();
        SkPoint srcPts[2];
        this->getLastPt(&srcPts[0]);
        // If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a "lineto")
        // joining the endpoints.
        // http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
        if (!rx || !ry) {
            return this->lineTo(x, y);
        }
        // If the current point and target point for the arc are identical, it should be treated as a
        // zero length path. This ensures continuity in animations.
        srcPts[1].set(x, y);
        if (srcPts[0] == srcPts[1]) {
            return this->lineTo(x, y);
        }
        rx = SkScalarAbs(rx);
        ry = SkScalarAbs(ry);
        SkVector midPointDistance = srcPts[0] - srcPts[1];
        midPointDistance *= 0.5f;
    
        SkMatrix pointTransform;
        pointTransform.setRotate(-angle);
    
        SkPoint transformedMidPoint;
        pointTransform.mapPoints(&transformedMidPoint, &midPointDistance, 1);
        SkScalar squareRx = rx * rx;
        SkScalar squareRy = ry * ry;
        SkScalar squareX = transformedMidPoint.fX * transformedMidPoint.fX;
        SkScalar squareY = transformedMidPoint.fY * transformedMidPoint.fY;
    
        // Check if the radii are big enough to draw the arc, scale radii if not.
        // http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
        SkScalar radiiScale = squareX / squareRx + squareY / squareRy;
        if (radiiScale > 1) {
            radiiScale = SkScalarSqrt(radiiScale);
            rx *= radiiScale;
            ry *= radiiScale;
        }
    
        pointTransform.setScale(1 / rx, 1 / ry);
        pointTransform.preRotate(-angle);
    
        SkPoint unitPts[2];
        pointTransform.mapPoints(unitPts, srcPts, (int) std::size(unitPts));
        SkVector delta = unitPts[1] - unitPts[0];
    
        SkScalar d = delta.fX * delta.fX + delta.fY * delta.fY;
        SkScalar scaleFactorSquared = std::max(1 / d - 0.25f, 0.f);
    
        SkScalar scaleFactor = SkScalarSqrt(scaleFactorSquared);
        if ((arcSweep == SkPathDirection::kCCW) != SkToBool(arcLarge)) {  // flipped from the original implementation
            scaleFactor = -scaleFactor;
        }
        delta.scale(scaleFactor);
        SkPoint centerPoint = unitPts[0] + unitPts[1];
        centerPoint *= 0.5f;
        centerPoint.offset(-delta.fY, delta.fX);
        unitPts[0] -= centerPoint;
        unitPts[1] -= centerPoint;
        SkScalar theta1 = SkScalarATan2(unitPts[0].fY, unitPts[0].fX);
        SkScalar theta2 = SkScalarATan2(unitPts[1].fY, unitPts[1].fX);
        SkScalar thetaArc = theta2 - theta1;
        if (thetaArc < 0 && (arcSweep == SkPathDirection::kCW)) {  // arcSweep flipped from the original implementation
            thetaArc += SK_ScalarPI * 2;
        } else if (thetaArc > 0 && (arcSweep != SkPathDirection::kCW)) {  // arcSweep flipped from the original implementation
            thetaArc -= SK_ScalarPI * 2;
        }
    
        // Very tiny angles cause our subsequent math to go wonky (skbug.com/9272)
        // so we do a quick check here. The precise tolerance amount is just made up.
        // PI/million happens to fix the bug in 9272, but a larger value is probably
        // ok too.
        if (SkScalarAbs(thetaArc) < (SK_ScalarPI / (1000 * 1000))) {
            return this->lineTo(x, y);
        }
    
        pointTransform.setRotate(angle);
        pointTransform.preScale(rx, ry);
    
        // the arc may be slightly bigger than 1/4 circle, so allow up to 1/3rd
        int segments = SkScalarCeilToInt(SkScalarAbs(thetaArc / (2 * SK_ScalarPI / 3)));
        SkScalar thetaWidth = thetaArc / segments;
        SkScalar t = SkScalarTan(0.5f * thetaWidth);
        if (!SkScalarIsFinite(t)) {
            return *this;
        }
        SkScalar startTheta = theta1;
        SkScalar w = SkScalarSqrt(SK_ScalarHalf + SkScalarCos(thetaWidth) * SK_ScalarHalf);
        auto scalar_is_integer = [](SkScalar scalar) -> bool {
            return scalar == SkScalarFloorToScalar(scalar);
        };
        bool expectIntegers = SkScalarNearlyZero(SK_ScalarPI/2 - SkScalarAbs(thetaWidth)) &&
            scalar_is_integer(rx) && scalar_is_integer(ry) &&
            scalar_is_integer(x) && scalar_is_integer(y);
    
        for (int i = 0; i < segments; ++i) {
            SkScalar endTheta    = startTheta + thetaWidth,
                     sinEndTheta = SkScalarSinSnapToZero(endTheta),
                     cosEndTheta = SkScalarCosSnapToZero(endTheta);
    
            unitPts[1].set(cosEndTheta, sinEndTheta);
            unitPts[1] += centerPoint;
            unitPts[0] = unitPts[1];
            unitPts[0].offset(t * sinEndTheta, -t * cosEndTheta);
            SkPoint mapped[2];
            pointTransform.mapPoints(mapped, unitPts, (int) std::size(unitPts));
            /*
            Computing the arc width introduces rounding errors that cause arcs to start
            outside their marks. A round rect may lose convexity as a result. If the input
            values are on integers, place the conic on integers as well.
             */
            if (expectIntegers) {
                for (SkPoint& point : mapped) {
                    point.fX = SkScalarRoundToScalar(point.fX);
                    point.fY = SkScalarRoundToScalar(point.fY);
                }
            }
            this->conicTo(mapped[0], mapped[1], w);
            startTheta = endTheta;
        }
    
        // The final point should match the input point (by definition); replace it to
        // ensure that rounding errors in the above math don't cause any problems.
        this->setLastPt(x, y);
        return *this;
    }
    

    上面代码长度很长,我们可能对 c++ 了解不多,所有去分析上面代码可能也收获不多。我们在 实例应用 中采用图形结合的方式给大家介绍该方法用法和表现。

  • 实例应用 该方法不同的参数有不同的表现形式,我们下面一一探究。首先我们先设定:

    1. d 为路径最后一个点到 arcEnd 点连线距离的一半;
    2. c 为路径最后一个点到 arcEnd 点连线的中心;
    3. r 为设置的 radius 参数。

    下面我们介绍各个参数不同导致绘制的视图效果不同。

    1. 如果该方法只传递一个 arcEnd 参数,并且 arcEnd 不等于路径的最后一个点,该方法将会展示一条从路径的最后一个点到 arcEnd点的直线。测试代码如下:

      void arcToPoint(Canvas canvas) {
        final Path path = Path();
        path.moveTo(100, 200);
        path.arcToPoint(const Offset(200, 200));
        canvas.drawPath(
          path,
          Paint()
            ..color = Colors.purple
            ..style = PaintingStyle.stroke
            ..strokeWidth = 2,
        );
      }
      

      上述代码运行结果视图如下:

      Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)

      注意

      1. arcEnd 等于路径的最后一个点,该方法的视图表现是没有任何绘制。
      2. 该方法再不传递 Radius radius 参数时,该方法的视图表现是一条直线。
  1. arcEnd 不等于路径的最后一个点时,同时设置 radius 参数时,该方的视图表现受 rd 的大小关系而影响。假如,
    1. r <= d 则方法的视图表现为以 c 为圆点,以 d 为半径,绘制的半圆。实例代码如下:

      void arcToPoint(Canvas canvas) {
        final Path path = Path();
        path.moveTo(100, 200);
        path.arcToPoint(const Offset(200, 100), radius: const Radius.circular(10));
        canvas.drawPath(
          path,
          Paint()
            ..color = Colors.purple
            ..style = PaintingStyle.stroke
            ..strokeWidth = 2,
        );
      }
      

      上述代码运行视图结果如下:

      Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)
    2. r > d 时 (假设:最后一个点为 (x1,y1)), 该函数表现的视图效果是以 (x1 + d, r2−d2\sqrt{r^2-d^2}r2d2) 点为圆点, r 为半径的圆的圆弧的一段。 测试代码如下:

      void arcToPoint(Canvas canvas) {
        final Path path = Path();
        path.moveTo(100, 200);
        path.arcToPoint(const Offset(200, 200), radius: const Radius.circular(100));
        canvas.drawPath(
          path,
          Paint()
            ..color = Colors.purple
            ..style = PaintingStyle.stroke
            ..strokeWidth = 2,
        );
      }
      

      上面代码运行视图如下:

      Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)

      上面图片紫色为函数绘制的曲线,红色是为了辅助了解添加的。

    3. 在方法中 largeArc 参数是设置视图绘制的是大弧还是小弧。该参数的视图效果,又受 rd 的大小关系影响。

      1. 如果 largeArcfalse 时, 当 r <= d 时,绘制的是半圆;当 r > d 时绘制的是一小段圆弧。测试代码如下:

        void arcToPoint(Canvas canvas) {
          final Path path = Path();
          path.moveTo(100, 200);
          path.arcToPoint(const Offset(200, 200), radius: const Radius.circular(40), largeArc: false);
          canvas.drawPath(
            path,
            Paint()
              ..color = Colors.purple
              ..style = PaintingStyle.stroke
              ..strokeWidth = 2,
          );
        
          final Path path1 = Path();
          path1.moveTo(400, 200);
          path1.arcToPoint(const Offset(500, 200), radius: const Radius.circular(60), largeArc: false);
          canvas.drawPath(
            path1,
            Paint()
              ..color = Colors.purple
              ..style = PaintingStyle.stroke
              ..strokeWidth = 2,
          );
        }
        

        上面代码运行视图效果如下:

        Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)
      2. 如果 largeArctrue 时, 当 r <= d 时,绘制的是半圆;当 r > d 时绘制的是一大段圆弧。测试代码如下:

        void arcToPoint(Canvas canvas) {
          final Path path = Path();
          path.moveTo(100, 200);
          path.arcToPoint(const Offset(200, 200), radius: const Radius.circular(40), largeArc: true);
          canvas.drawPath(
            path,
            Paint()
              ..color = Colors.purple
              ..style = PaintingStyle.stroke
              ..strokeWidth = 2,
          );
        
          final Path path1 = Path();
          path1.moveTo(400, 200);
          path1.arcToPoint(const Offset(500, 200), radius: const Radius.circular(60), largeArc: true);
          canvas.drawPath(
            path1,
            Paint()
              ..color = Colors.purple
              ..style = PaintingStyle.stroke
              ..strokeWidth = 2,
          );
        }
        

        上述代码运行的效果如下:

        Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)

      总结:该参数主要影响的是当r > d 时, 如果 largeArc = true 时, 绘制的是大弧,否则绘制的是小弧。

    4. 参数 clockwise 是设置绘制弧度的方向是顺时针还是逆时针。其实它很好理解,可以理解为 clockwise 设置为 true 或者 false 它们的图案是沿着 路径最后一个点到 arcEnd 点的连线对称即可。 测试代码如下:

      void arcToPoint(Canvas canvas) {
        final Path path = Path();
        path.moveTo(100, 200);
        path.arcToPoint(const Offset(200, 200), radius: const Radius.circular(40), largeArc: true, clockwise: true);
        canvas.drawPath(
          path,
          Paint()
            ..color = Colors.purple
            ..style = PaintingStyle.stroke
            ..strokeWidth = 2,
        );
      
        final Path path1 = Path();
        path1.moveTo(400, 200);
        path1.arcToPoint(const Offset(500, 200), radius: const Radius.circular(60), largeArc: true, clockwise: true);
        canvas.drawPath(
          path1,
          Paint()
            ..color = Colors.purple
            ..style = PaintingStyle.stroke
            ..strokeWidth = 2,
        );
      
        final Path path2 = Path();
        path2.moveTo(100, 200);
        path2.arcToPoint(const Offset(200, 200), radius: const Radius.circular(40), largeArc: true, clockwise: false);
        canvas.drawPath(
          path2,
          Paint()
            ..color = Colors.purple
            ..style = PaintingStyle.stroke
            ..strokeWidth = 2,
        );
      
        final Path path3 = Path();
        path3.moveTo(400, 200);
        path3.arcToPoint(const Offset(500, 200), radius: const Radius.circular(60), largeArc: true, clockwise: false);
        canvas.drawPath(
          path3,
          Paint()
            ..color = Colors.purple
            ..style = PaintingStyle.stroke
            ..strokeWidth = 2,
        );
      }
      

      上边代码运行视图效果如下:

      Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)
      1. rotation 参数,该方法的文档注释是绘制曲线旋转。经过测试发现无论怎么设置该参数都没有效果。经过底层代码查看,发现该参数在底层生成的变量没有使用,如下图所示:

        Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二) Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)

        从上图黄色标记出看,该参数在底层代码只是生成一个 SkMatrix 变量,一直在设置起参数,最后就是没有使用,所以该参数无论怎么设置都没有效果。

7. void relativeArcToPoint(Offset arcEndDelta, {Radius radius = Radius.zero,double rotation = 0.0,bool largeArc = false,bool clockwise = true,})
  • 方法介绍 该方法和 arcToPoint 方法基本一样,具体的细节和视图展示效果请查看 arcToPoint 方法的介绍。 该方法的和 arcToPoint 的唯一区别是该方法是以路径的最后一个点为新的原点,而 arcToPoint 是以绘制 (0,0) 点为原点。其实就是选择的参考点不一样而已。

  • 实例应用 该方法在 Flutter 中的调用如下所示:

    void relativeArcToPoint(Canvas canvas) {
      final Path path = Path();
      path.moveTo(100, 200);
      path.lineTo(200, 200);
      path.relativeArcToPoint(const Offset(100, 100), radius: const Radius.circular(50));
      canvas.drawPath(
        path,
        Paint()
          ..color = Colors.purple
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2,
      );
    }
    

    上述代码运行的视图效果如下:

    Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~ (二)

三、 鼓励和建议

这是 Flutter 绘制路径 Path 的全部方法介绍 的第二篇文章,由于文章篇幅越来越长,影响阅读效果,所以到此终止,我们将将剩余的方法放置到第三篇文章中,大家请继续关注。如果感觉写的还可以,请留下你的爱心或者评论。本篇文章使用的测试 Demo 地址 Path Demo, 欢迎下载。

转载自:https://juejin.cn/post/7267723121037017088
评论
请登录