在代码生成的UIView上绘制UIBezierPath


68

我有一个 UIView在运行时添加了代码。

我想在其中画一个UIBezierPath,但这是否意味着我必须重写drawRectUIView?

还是有另一种在定制产品上绘图的方法 UIView

这是用于生成的代码UIView

UIView* shapeView = [[UIView alloc]initWithFrame:CGRectMake(xOrigin,yOrigin+(i*MENU_BLOCK_FRAME_HEIGHT), self.shapeScroll.frame.size.width, MENU_BLOCK_FRAME_HEIGHT)];
shapeView.clipsToBounds = YES;

这是创建和返回a的函数UIBezierPath

- (UIBezierPath*)createPath
{
    UIBezierPath* path = [[UIBezierPath alloc]init];
    [path moveToPoint:CGPointMake(100.0, 50.0)];
    [path addLineToPoint:CGPointMake(200.0,50.0)];
    [path addLineToPoint:CGPointMake(200.0, 200.0)];
    [path addLineToPoint:CGPointMake(100.0, 200.0)];
    [path closePath];
    return path;
}

Answers:


268

不久前,我什至不知道如何发音贝塞尔(Bézier),更不用说知道如何使用贝塞尔(Bézier)路径制作自定义形状了。以下是我所学到的。事实证明,它们并不像最初看起来的那样可怕。

如何在自定义视图中绘制贝塞尔路径

这些是主要步骤:

  1. 设计所需形状的轮廓。
  2. 将轮廓路径分为直线,圆弧和曲线段。
  3. 以编程方式构建该路径。
  4. drawRect使用或使用绘制路径CAShapeLayer

设计形状轮廓

您可以做任何事情,但是作为示例,我选择了以下形状。它可能是键盘上的弹出键。

在此处输入图片说明

将路径分为多个部分

回顾一下您的形状设计,并将其分解为线(对于直线),弧(对于圆和圆角)和曲线(对于其他任何东西)的简单元素。

这是我们的示例设计的样子:

在此处输入图片说明

  • 黑色是线段
  • 浅蓝色是弧线段
  • 红色是曲线
  • 橙色点是曲线的控制点
  • 绿点是路径段之间的点
  • 虚线显示边界矩形
  • 深蓝色数字是按程序段顺序添加的分段

以编程方式构建路径

我们将在左下角任意开始,然后顺时针方向工作。我将使用图像中的网格获取点的x和y值。我将在此处对所有内容进行硬编码,但是您当然不会在实际项目中这样做。

基本过程是:

  1. 创建一个新的 UIBezierPath
  2. 选择路径的起点 moveToPoint
  3. 将细分添加到路径
    • 线: addLineToPoint
    • 弧: addArcWithCenter
    • 曲线: addCurveToPoint
  4. 用以下方式封闭路径 closePath

这是在上图中创建路径的代码。

func createBezierPath() -> UIBezierPath {

    // create a new path
    let path = UIBezierPath()

    // starting point for the path (bottom left)
    path.move(to: CGPoint(x: 2, y: 26))

    // *********************
    // ***** Left side *****
    // *********************

    // segment 1: line
    path.addLine(to: CGPoint(x: 2, y: 15))

    // segment 2: curve
    path.addCurve(to: CGPoint(x: 0, y: 12), // ending point
        controlPoint1: CGPoint(x: 2, y: 14),
        controlPoint2: CGPoint(x: 0, y: 14))

    // segment 3: line
    path.addLine(to: CGPoint(x: 0, y: 2))

    // *********************
    // ****** Top side *****
    // *********************

    // segment 4: arc
    path.addArc(withCenter: CGPoint(x: 2, y: 2), // center point of circle
        radius: 2, // this will make it meet our path line
        startAngle: CGFloat(M_PI), // π radians = 180 degrees = straight left
        endAngle: CGFloat(3*M_PI_2), // 3π/2 radians = 270 degrees = straight up
        clockwise: true) // startAngle to endAngle goes in a clockwise direction

    // segment 5: line
    path.addLine(to: CGPoint(x: 8, y: 0))

    // segment 6: arc
    path.addArc(withCenter: CGPoint(x: 8, y: 2),
                          radius: 2,
                          startAngle: CGFloat(3*M_PI_2), // straight up
        endAngle: CGFloat(0), // 0 radians = straight right
        clockwise: true)

    // *********************
    // ***** Right side ****
    // *********************

    // segment 7: line
    path.addLine(to: CGPoint(x: 10, y: 12))

    // segment 8: curve
    path.addCurve(to: CGPoint(x: 8, y: 15), // ending point
        controlPoint1: CGPoint(x: 10, y: 14),
        controlPoint2: CGPoint(x: 8, y: 14))

    // segment 9: line
    path.addLine(to: CGPoint(x: 8, y: 26))

    // *********************
    // **** Bottom side ****
    // *********************

    // segment 10: line
    path.close() // draws the final line to close the path

    return path
}

注意:可以通过在单个命令中添加一条线和一条圆弧来减少上面的某些代码(因为圆弧有一个隐含的起点)。有关更多详细信息,请参见此处

画出道路

我们可以在图层或图层中绘制路径drawRect

方法1:在图层中绘制路径

我们的自定义类如下所示。CAShapeLayer初始化视图后,我们将Bezier路径添加到新路径。

import UIKit
class MyCustomView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    func setup() {

        // Create a CAShapeLayer
        let shapeLayer = CAShapeLayer()

        // The Bezier path that we made needs to be converted to 
        // a CGPath before it can be used on a layer.
        shapeLayer.path = createBezierPath().cgPath

        // apply other properties related to the path
        shapeLayer.strokeColor = UIColor.blue.cgColor
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 1.0
        shapeLayer.position = CGPoint(x: 10, y: 10)

        // add the new layer to our custom view
        self.layer.addSublayer(shapeLayer)
    }

    func createBezierPath() -> UIBezierPath {

        // see previous code for creating the Bezier path
    }
}

然后像这样在视图控制器中创建视图

override func viewDidLoad() {
    super.viewDidLoad()

    // create a new UIView and add it to the view controller
    let myView = MyCustomView()
    myView.frame = CGRect(x: 100, y: 100, width: 50, height: 50)
    myView.backgroundColor = UIColor.yellow
    view.addSubview(myView)

}

我们得到...

在此处输入图片说明

嗯,这有点小,因为我将所有数字都硬编码了。不过,我可以像下面这样放大路径大小:

let path = createBezierPath()
let scale = CGAffineTransform(scaleX: 2, y: 2)
path.apply(scale)
shapeLayer.path = path.cgPath

在此处输入图片说明

方法2:在中绘制路径 draw

使用draw的速度比绘制图层的速度慢,因此如果不需要,则不建议使用此方法。

这是我们的自定义视图的修改后的代码:

import UIKit
class MyCustomView: UIView {

    override func draw(_ rect: CGRect) {

        // create path (see previous code)
        let path = createBezierPath()

        // fill
        let fillColor = UIColor.white
        fillColor.setFill()

        // stroke
        path.lineWidth = 1.0
        let strokeColor = UIColor.blue
        strokeColor.setStroke()

        // Move the path to a new location
        path.apply(CGAffineTransform(translationX: 10, y: 10))

        // fill and stroke the path (always do these last)
        path.fill()
        path.stroke()

    }

    func createBezierPath() -> UIBezierPath {

        // see previous code for creating the Bezier path
    }
}

这给了我们相同的结果...

在此处输入图片说明

进一步研究

真的建议您查看以下材料。它们终于使贝塞尔(Bézier)道路对我而言可以理解。(并教我如何发音:/ ˈbɛzieɪ/。)


如果frame视图改变了怎么办?当方向发生变化时,我们如何调整形状的大小?
ozgur's

3
@ozgur,至少有两个选择。如我上面的示例所示,一种方法是进行缩放(并可能转换)。另一个选择是根据新框架重新计算贝塞尔曲线路径。在上面的示例中,我将所有数字硬编码到Bezier路径中。但是,当我在实际项目中使用Bezier路径时,我会根据帧大小确定Bezier值。当框架(或更可能是边界)发生变化时,我将重新计算贝塞尔曲线路径。
苏拉奇

1
@ozgur,layoutSubviews听起来绝对是正确的选择。我会说这是否有效,然后保留在那里。这是我使用贝塞尔曲线路径的示例。(滚动到底部。)由于某种原因,我没有将其放入,layoutSubviews但我不记得现在为什么。我可能应该将此提交代码审查。我不是专家。我刚刚在上面做了一个答案,以了解自己如何做Bezier路径。
Suragch '16

1
@BohdanSavych,该draw方法已经属于视图,因此您无需添加它。这是UIView出于图纸目的定义的标准方法。我们只是在这里覆盖它,以便我们可以在视图上绘制自己的图纸。
Suragch

1
@Suragch我一直在虔诚地遵循与您相同的方法(主要是由于我在开始绘制时找到了答案),但是我很好奇为什么“使用绘制比绘制图层要慢,因此不建议这样做不需要的方法。” 我知道draw每次覆盖范围更改时,它的覆盖都会重绘所有内容,但是更改路径layoutSubviews是否一样?
Rikh '18

54

如果您使用这样的CAShapeLayer,会更容易:

CAShapeLayer *shapeView = [[CAShapeLayer alloc] init];

并设置它path

[shapeView setPath:[self createPath].CGPath];

最后添加它:

[[self.view layer] addSublayer:shapeView];

16

您可以使用CAShapeLayer来执行此操作。

像这样...

CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [self createPath].CGPath;
shapeLayer.strokeColor = [UIColor redColor].CGColor; //etc...
shapeLayer.lineWidth = 2.0; //etc...
shapeLayer.position = CGPointMake(100, 100); //etc...
[self.layer addSublayer:shapeLayer];

然后,这将添加并绘制路径,而不必覆盖drawRect


5

有多种方法可以实现您的期望。我最常看到的是:覆盖drawRect,将形状绘制为CAShapeLayer,然后将其作为子层添加到视图中,或者将路径绘制到另一个上下文中,另存为图像,然后将其添加到您的视图中视图。

所有这些都是合理的选择,哪个是最佳选择取决于其他许多因素,例如您是否要不断添加形状,调用频率等。


2

正如其他海报指出的那样,使用形状图层是一种不错的方法。

形状图层a可能比覆盖drawRect更好的性能。

如果要自己绘制路径,则可以,您需要为自定义视图类重写drawRect。


1

是的,如果要绘制任何东西,都必须重写drawrect。创建UIBezierPath可以在任何地方完成,但是要绘制一些东西,您必须在drawrect方法内部进行

setNeedsDisplay如果您在UIView的子类中重写drawRect,则应该调用该方法,该子类基本上是在屏幕上绘制某些东西的自定义视图,如lines,image,矩形。


setNeedsDisplay在哪里调用?
seyha

0

在代码生成的UIView上绘制UIBezierPath时,可以如下使用UIView触摸事件。为Touch起点和Touch终点创建全局变量,如下所示:

CGPoint startingPoint;
CGPoint endingPoint;

然后使用UIView Touchevents绘制UIBezierPath,如下所示:

  /*Touch Start*/
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  {
      UITouch *touch = [[event allTouches] anyObject];
      startingPoint = [touch locationInView:self];
 }
/*Touch End*/
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
  {

 }
/*Touch Move*/
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
     UITouch *touch = [touches anyObject];
      endingPoint = [touch locationInView:self];
      [self makeLineLayer:self.layer lineFromPointA:startingPoint 
      toPointB:endingPoint];
   }
/*Draw UIBezierPath*/
-(void)makeLineLayer:(CALayer *)layer lineFromPointA:(CGPoint)pointA 
      toPointB:(CGPoint)pointB
 {
      CAShapeLayer *line = [CAShapeLayer layer];
      UIBezierPath *linePath=[UIBezierPath bezierPath];
      [linePath moveToPoint: pointA];// Start Point
      [linePath addLineToPoint:pointB];//End Point
       line.path=linePath.CGPath;
       line.fillColor = nil;
       line.opacity = 2.0;
       line.lineWidth = 4.0;
       line.strokeColor = [UIColor redColor].CGColor;
       [layer addSublayer:line];
 }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.