美文网首页IOS个人开发v2panda的技术专题iOS进阶
放肆的使用UIBezierPath和CAShapeLayer画各

放肆的使用UIBezierPath和CAShapeLayer画各

作者: J0hnnny | 来源:发表于2015-11-23 20:11 被阅读57654次

    CAShapeLayer 是 CALayer 的子类,但是比 CALayer 更灵活,可以画出各种图形,当然,你也可以使用其他方式来画,随你。

    杂谈

    在 CAShapeLayer 中,也可以像 CALayer 一样指定它的 frame 来画,就像这样:

    let layer = CAShapeLayer()
    layer.frame = CGRectMake(110, 100, 150, 100)
    layer.backgroundColor = UIColor.blackColor().CGColor
    
    view.layer.addSublayer(layer)
    

    然后你就会得到如图这样的黑色矩形

    但是,CAShapeLayer 有一个神奇的属性 path 用这个属性配合上 UIBezierPath 这个类就可以达到超神的效果。

    UIBezierPath 顾名思义,这是用贝塞尔曲线的方式来构建一段弧线,你可以用任意条弧线来组成你想要的形状,比如,你想用它来和上面一样画一个矩形,那就可以这样子来做:

    let path = UIBezierPath(rect: CGRectMake(110, 100, 150, 100))
    let layer = CAShapeLayer()
    layer.path = path.CGPath
    layer.fillColor = UIColor.blackColor().CGColor
    view.layer.addSublayer(layer)
    

    要注意的是,这里就不要用backgroundColor 这个属性了,而要使用 fillColorstrokeColor ,前者代表设置这个 Layer 的填充色,后者代表设置它的边框色

    layer.fillColor = UIColor.clearColor().CGColor
    layer.strokeColor = UIColor.blackColor().CGColor
    

    可以试一下把上面代码设置颜色的部分改成这个样子,那么运行程序的时候就会是这种样子

    玩一下UIBezierPath

    在说回 UIBezierPath ,在 UIBezierPath 的 init 方法中,就有很多方便你画各种图形的方法,比如你可以画一个带圆角的图形

    let path = UIBezierPath(roundedRect: CGRectMake(110, 100, 150, 100), cornerRadius: 50)
    let layer = CAShapeLayer()
    layer.path = path.CGPath
    layer.fillColor = UIColor.clearColor().CGColor
    layer.strokeColor = UIColor.blackColor().CGColor
    

    还可以指定起始角和半径画圆

    let radius: CGFloat = 60.0
    let startAngle: CGFloat = 0.0
    let endAngle: CGFloat = CGFloat(M_PI * 2)
    let path = UIBezierPath(arcCenter: view.center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
    let layer = CAShapeLayer()
    layer.path = path.CGPath
    layer.fillColor = UIColor.clearColor().CGColor
    layer.strokeColor = UIColor.blackColor().CGColor
    

    在这里涉及到角度的问题,起始角和结束角,这里的角度使使用弧度制来表示,这里我收藏了一张图片,以方便参考

    怎么画曲线

    贝塞尔曲线的画法是由起点、终点、控制点三个参数来画的,为了解释清楚这个点,我写了几行代码来解释它

    let startPoint = CGPointMake(50, 300)
    let endPoint = CGPointMake(300, 300)
    let controlPoint = CGPointMake(170, 200)
    
    let layer1 = CALayer()
    layer1.frame = CGRectMake(startPoint.x, startPoint.y, 5, 5)
    layer1.backgroundColor = UIColor.redColor().CGColor
    
    let layer2 = CALayer()
    layer2.frame = CGRectMake(endPoint.x, endPoint.y, 5, 5)
    layer2.backgroundColor = UIColor.redColor().CGColor
    
    let layer3 = CALayer()
    layer3.frame = CGRectMake(controlPoint.x, controlPoint.y, 5, 5)
    layer3.backgroundColor = UIColor.redColor().CGColor
    
    let path = UIBezierPath()
    let layer = CAShapeLayer()
    
    path.moveToPoint(startPoint)
    path.addQuadCurveToPoint(endPoint, controlPoint: controlPoint)
    
    layer.path = path.CGPath
    layer.fillColor = UIColor.clearColor().CGColor
    layer.strokeColor = UIColor.blackColor().CGColor
    
    view.layer.addSublayer(layer)
    view.layer.addSublayer(layer1)
    view.layer.addSublayer(layer2)
    view.layer.addSublayer(layer3)
    

    我很随意的定义了三个点,为了清楚显示它们的位置,我放了三个矩形在上面以便观察,然后调用 path.moveToPoint(startPoint) 让它移动到起始点,然后调用path.addQuadCurveToPoint(endPoint, controlPoint: controlPoint) 这个方法告诉它结束点和控制点,这样它就能画出一条有弧度的线条了,如果把fillColor设置一个颜色,那么它就会变成一个很丑的形状了,示例图如下

    控制点决定了它的曲率,曲线的顶点不等于控制点的位置,具体可以看一下贝塞尔曲线的定义,你还可以使用两个控制点来画,两个控制点可以使用方法 path.addCurveToPoint(endPoint, controlPoint1: controlPoint, controlPoint2: controlPoint2)来搞定

    这样它会是这个样子

    再来说说 CAShapeLayer

    CAShapeLayer 是个神奇的东西,给它一个path它就能变成你想要的形状,它还有很多可以玩的地方。综合使用可以组合成不同的动画,比如下面这样

    demo1.gif demo2.gif demo3.gif

    这三个动画就是使用了 strokeEnd strokeStart lineWidth 三个属性,第一个动画用了strokeEnd这个属性的值范围是0-1,动画显示了从0到1之间每一个值对这条曲线的影响,strokeStart的方法则是相反的,如果把这两个值首先都设置成0.5然后慢慢改变成0和1的时候就会变成第二个动画,配合lineWidth则曲线会慢慢变粗,这里的很多属性都是支持动画的。

    示例代码

        private func animation1() {
            let animation = CABasicAnimation(keyPath: "strokeEnd")
            animation.fromValue = 0
            animation.toValue = 1
            animation.duration = 2
            layer.addAnimation(animation, forKey: "")
        }
        
        private func animation2() {
            layer.strokeStart = 0.5
            layer.strokeEnd = 0.5
            
            let animation = CABasicAnimation(keyPath: "strokeStart")
            animation.fromValue = 0.5
            animation.toValue = 0
            animation.duration = 2
            
            let animation2 = CABasicAnimation(keyPath: "strokeEnd")
            animation2.fromValue = 0.5
            animation2.toValue = 1
            animation2.duration = 2
            
            layer.addAnimation(animation, forKey: "")
            layer.addAnimation(animation2, forKey: "")
        }
        
        private func animation3() {
            let animation = CABasicAnimation(keyPath: "lineWidth")
            animation.fromValue = 1
            animation.toValue = 10
            animation.duration = 2
            layer.addAnimation(animation, forKey: "")
        }
    

    应用一下

    前一阵子在仿时光网这个APP,其中有一个Layer的形状很怪异,是这样的

    很明显它可以用 CAShapeLayer + UIBezierPath 来做,思路大概是这样,先移动到左上方的位置,然后向下划线,然后往右划线,然后往上划线,还剩一个盖子,这个盖子就用一个控制点控制曲率,非常简单,代码如下

    let finalSize = CGSizeMake(CGRectGetWidth(view.frame), 400)
    let layerHeight = finalSize.height * 0.2
    let layer = CAShapeLayer()
    let bezier = UIBezierPath()
    bezier.moveToPoint(CGPointMake(0, finalSize.height - layerHeight))
    bezier.addLineToPoint(CGPointMake(0, finalSize.height - 1))
    bezier.addLineToPoint(CGPointMake(finalSize.width, finalSize.height - 1))
    bezier.addLineToPoint(CGPointMake(finalSize.width, finalSize.height - layerHeight))
    bezier.addQuadCurveToPoint(CGPointMake(0,finalSize.height - layerHeight),
        controlPoint: CGPointMake(finalSize.width / 2, (finalSize.height - layerHeight) - 40))
    layer.path = bezier.CGPath
    layer.fillColor = UIColor.blackColor().CGColor
    view.layer.addSublayer(layer)
    

    就能画出这样的形状来

    再来一个复杂点的,微信下拉拍小视频的那只眼睛很有趣,来模仿一下那个效果吧,它是这样的

    首先你得画出这只眼睛,这是眼睛包括5个部分组成(这个是用OC写的)

    @property (strong, nonatomic) CAShapeLayer *eyeFirstLightLayer;
    @property (strong, nonatomic) CAShapeLayer *eyeSecondLightLayer;
    @property (strong, nonatomic) CAShapeLayer *eyeballLayer;
    @property (strong, nonatomic) CAShapeLayer *topEyesocketLayer;
    @property (strong, nonatomic) CAShapeLayer *bottomEyesocketLayer;
    

    然后,还是通过 UIBezierPath 和 CAShapeLayer 这样的老套路来画,代码较多

    - (CAShapeLayer *)eyeFirstLightLayer {
        if (!_eyeFirstLightLayer) {
            _eyeFirstLightLayer = [CAShapeLayer layer];
            CGPoint center = CGPointMake(CGRectGetWidth(self.frame) / 2, CGRectGetHeight(self.frame) / 2);
            UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center
                                                                radius:CGRectGetWidth(self.frame) * 0.2
                                                            startAngle:(230.f / 180.f) * M_PI
                                                              endAngle:(265.f / 180.f) * M_PI
                                                             clockwise:YES];
            _eyeFirstLightLayer.borderColor = [UIColor blackColor].CGColor;
            _eyeFirstLightLayer.lineWidth = 5.f;
            _eyeFirstLightLayer.path = path.CGPath;
            _eyeFirstLightLayer.fillColor = [UIColor clearColor].CGColor;
            _eyeFirstLightLayer.strokeColor = [UIColor whiteColor].CGColor;
        }
        return _eyeFirstLightLayer;
    }
    
    - (CAShapeLayer *)eyeSecondLightLayer {
        if (!_eyeSecondLightLayer) {
            _eyeSecondLightLayer = [CAShapeLayer layer];
            CGPoint center = CGPointMake(CGRectGetWidth(self.frame) / 2, CGRectGetHeight(self.frame) / 2);
            UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center
                                                                radius:CGRectGetWidth(self.frame) * 0.2
                                                            startAngle:(211.f / 180.f) * M_PI
                                                              endAngle:(220.f / 180.f) * M_PI
                                                             clockwise:YES];
            _eyeSecondLightLayer.borderColor = [UIColor blackColor].CGColor;
            _eyeSecondLightLayer.lineWidth = 5.f;
            _eyeSecondLightLayer.path = path.CGPath;
            _eyeSecondLightLayer.fillColor = [UIColor clearColor].CGColor;
            _eyeSecondLightLayer.strokeColor = [UIColor whiteColor].CGColor;
    
        }
        return _eyeSecondLightLayer;
    }
    
    - (CAShapeLayer *)eyeballLayer {
        if (!_eyeballLayer) {
            _eyeballLayer = [CAShapeLayer layer];
            CGPoint center = CGPointMake(CGRectGetWidth(self.frame) / 2, CGRectGetHeight(self.frame) / 2);
            UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center
                                                                radius:CGRectGetWidth(self.frame) * 0.3
                                                            startAngle:(0.f / 180.f) * M_PI
                                                              endAngle:(360.f / 180.f) * M_PI
                                                             clockwise:YES];
            _eyeballLayer.borderColor = [UIColor blackColor].CGColor;
            _eyeballLayer.lineWidth = 1.f;
            _eyeballLayer.path = path.CGPath;
            _eyeballLayer.fillColor = [UIColor clearColor].CGColor;
            _eyeballLayer.strokeColor = [UIColor whiteColor].CGColor;
            _eyeballLayer.anchorPoint = CGPointMake(0.5, 0.5);
            
        }
        return _eyeballLayer;
    }
    
    - (CAShapeLayer *)topEyesocketLayer {
        if (!_topEyesocketLayer) {
            _topEyesocketLayer = [CAShapeLayer layer];
            CGPoint center = CGPointMake(CGRectGetWidth(self.frame) / 2, CGRectGetHeight(self.frame) / 2);
            UIBezierPath *path = [UIBezierPath bezierPath];
            [path moveToPoint:CGPointMake(0, CGRectGetHeight(self.frame) / 2)];
            [path addQuadCurveToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetHeight(self.frame) / 2)
                         controlPoint:CGPointMake(CGRectGetWidth(self.frame) / 2, center.y - center.y - 20)];
            _topEyesocketLayer.borderColor = [UIColor blackColor].CGColor;
            _topEyesocketLayer.lineWidth = 1.f;
            _topEyesocketLayer.path = path.CGPath;
            _topEyesocketLayer.fillColor = [UIColor clearColor].CGColor;
            _topEyesocketLayer.strokeColor = [UIColor whiteColor].CGColor;
        }
        return _topEyesocketLayer;
    }
    
    - (CAShapeLayer *)bottomEyesocketLayer {
        if (!_bottomEyesocketLayer) {
            _bottomEyesocketLayer = [CAShapeLayer layer];
            CGPoint center = CGPointMake(CGRectGetWidth(self.frame) / 2, CGRectGetHeight(self.frame) / 2);
            UIBezierPath *path = [UIBezierPath bezierPath];
            [path moveToPoint:CGPointMake(0, CGRectGetHeight(self.frame) / 2)];
            [path addQuadCurveToPoint:CGPointMake(CGRectGetWidth(self.frame), CGRectGetHeight(self.frame) / 2)
                         controlPoint:CGPointMake(CGRectGetWidth(self.frame) / 2, center.y + center.y + 20)];
            _bottomEyesocketLayer.borderColor = [UIColor blackColor].CGColor;
            _bottomEyesocketLayer.lineWidth = 1.f;
            _bottomEyesocketLayer.path = path.CGPath;
            _bottomEyesocketLayer.fillColor = [UIColor clearColor].CGColor;
            _bottomEyesocketLayer.strokeColor = [UIColor whiteColor].CGColor;
    
        }
        return _bottomEyesocketLayer;
    }
    

    然后更改一下某些属性的值,方便稍后的动画

    - (void)setupAnimation {
        self.eyeFirstLightLayer.lineWidth = 0.f;
        self.eyeSecondLightLayer.lineWidth = 0.f;
        self.eyeballLayer.opacity = 0.f;
        _bottomEyesocketLayer.strokeStart = 0.5f;
        _bottomEyesocketLayer.strokeEnd = 0.5f;
        _topEyesocketLayer.strokeStart = 0.5f;
        _topEyesocketLayer.strokeEnd = 0.5f;
    }
    

    最后根据 UIScrollView 的 contentOffset 来控制各种属性,办法较笨,但管用。

    - (void)animationWith:(CGFloat)y {
        CGFloat flag = self.frame.origin.y * 2.f - 20.f;
        if (y < flag) {
            if (self.eyeFirstLightLayer.lineWidth < 5.f) {
                self.eyeFirstLightLayer.lineWidth += 1.f;
                self.eyeSecondLightLayer.lineWidth += 1.f;
            }
        }
        
        if(y < flag - 20) {
            if (self.eyeballLayer.opacity <= 1.0f) {
                self.eyeballLayer.opacity += 0.1f;
            }
            
        }
        
        if (y < flag - 40) {
            if (self.topEyesocketLayer.strokeEnd < 1.f && self.topEyesocketLayer.strokeStart > 0.f) {
                self.topEyesocketLayer.strokeEnd += 0.1f;
                self.topEyesocketLayer.strokeStart -= 0.1f;
                self.bottomEyesocketLayer.strokeEnd += 0.1f;
                self.bottomEyesocketLayer.strokeStart -= 0.1f;
            }
        }
        
        if (y > flag - 40) {
            if (self.topEyesocketLayer.strokeEnd > 0.5f && self.topEyesocketLayer.strokeStart < 0.5f) {
                self.topEyesocketLayer.strokeEnd -= 0.1f;
                self.topEyesocketLayer.strokeStart += 0.1f;
                self.bottomEyesocketLayer.strokeEnd -= 0.1f;
                self.bottomEyesocketLayer.strokeStart += 0.1f;
            }
        }
        
        if (y > flag - 20) {
            if (self.eyeballLayer.opacity >= 0.0f) {
                self.eyeballLayer.opacity -= 0.1f;
            }
        }
        
        if (y > flag) {
            if (self.eyeFirstLightLayer.lineWidth > 0.f) {
                self.eyeFirstLightLayer.lineWidth -= 1.f;
                self.eyeSecondLightLayer.lineWidth -= 1.f;
            }
        }
    }
    

    最后

    总之使用 UIbezierPath 和 CAShapeLayer 可以画出你想要的任何形状,没有它做不到,只有你想不到,搞定了它们你就可以轻松定制你想要的任何控件了。

    微信Demo

    代码

    https://github.com/J0HDev/WXVideo

    相关文章

      网友评论

      • 2bd7e62c5187:@FengxinLi -M_PI/2到M_PI*3/2是12点钟顺时针到12点钟,至于为什么不是0度到360度,是因为其表示3点钟顺时针到3点钟,起点和结束点都不一样,虽然它都是一个圆,
        FengxinLi:@好无语的名字 我需要做动画就是扇形进度条 但是起点是3点钟。 3点钟的扇形然后到3点钟结束
        2bd7e62c5187:@FengxinLi 如果你只是画圆就没什么区别,但要做动画就有区别了
        FengxinLi:请问一下怎么是起点和结束点都不一样呢?
      • FengxinLi:请问一下楼主 我看扇形很多开始值都是 -M_PI/2 ,结束的角度都是 M_PI *3 / 2。我理解为什么不是0度到360度呢?
        FengxinLi:@J0hnnny 扇形进度条 最后都是一个圆。这个起始的角度没影响吧?只是sin角度和cos角度对应的x轴和y轴的值计算对了就可以吗?以及初始化的时候 起始点和0度时候的点他们应该在一条线上?
        J0hnnny:@FengxinLi 0-360 不就是一个圆了吗?
      • PGOne爱吃饺子:楼主 你好,时光app的这个形状可不可以让美工给切出来呢
        J0hnnny:@4140d18ee6fc 任何你能代码画出来的 都可以切的
      • LD_左岸:bezier.addQuadCurveToPoint(CGPointMake(0,finalSize.height - layerHeight),
        controlPoint: CGPointMake(finalSize.width / 2, (finalSize.height - layerHeight) - 40))


        ToPoint(CGPointMake(0,finalSize.height - layerHeight)这个不应该是曲线的终点吗 但是我换成CGPointMake(finalSize.width, finalSize.height - layerHeight)就不对了
        宇文袥:因为起始点已经变成了曲线的终点
      • MacLeon:确实非常放肆啊:kissing_heart:
      • bbc7dabd6c48:666666666666
      • DarrenG:首先感谢作者愿意分享你的技术,另有一问题附上为什么在设置UIbezierPath 的lineCapStyle以及lineJoinStyle的两个属性后 CAShapeLayer 的path显示时没有两段的圆角呢?期待你的回答
      • 豆瓣菜:mark 动画
      • 韩大熊宝要姓张:感谢分享!
      • LikeSomeBody:楼主辛苦了!
      • violafa:不错,很棒
      • 6b69124ff056:老哥你你简直是我的再生XX,,,,:sob:
        春暖花已开:@0xDatou 我去,这就有点过了,你让jony情何以堪?
      • 大生活家:感谢分享
      • 浮动跳跃:原码不能直接粘贴运行。 还是我太白。。。
      • SpursGo:不错
      • BlusSunShine:我想请问一下是怎么做的抗锯齿的呢
      • xxttw:给力. 值得收藏
      • Rainqin:👌
      • 给你快乐:真给力,大大的赞!
      • GloryMan:感谢大神分享
      • 攻城狮小强:大赞啊,解决了我好多疑问!
      • ff4027bec949:很不错 收藏了
      • LaiYoung_:用oc重写了一次楼主的这些小案例,外加了一个画饼图 :blush: https://github.com/CYBoys/BezierPath
      • 白熊:讲解清晰代码到位,跟着敲一遍就入门了 :+1:
      • Lucifron:第一次认真看,很有收获
      • Zeroxhj:你好 最后一个微信的动画有demo吗,可以借鉴下吗
        J0hnnny:@hongjunxiao https://github.com/J0HDev/WXVideo
      • SundyLee:我也要写博客!
      • SANTOS_WU:喜欢
      • 213cd098169c:微信眼睛那个和我和做法一样 :smile:
      • ReidWang:您好,请问一下 我画的为什么有锯齿?就用 bezier 画 圆角矩形的时候
        ReidWang:@di3 哦哦 好了好了,解决了,是我把模拟器的原因 :smile:
      • _赖笔小新:在技术上是这样的,但在工程上,很多时候一张切图反而更方便
      • f94a386dc4c3:楼主 有没有遇到 shapelayer 闪出来的情况,闪一下,然后消失
        J0hnnny:@houae 找一些 core animation 的文章来看看你就知道为什么了
      • 暴走的码农:非常适合新手,太棒了!!! :kissing_heart:
      • 羊羊羊的洋:不错呀!不错
      • 深蓝吟风:请问你的self.frame是什么尺寸
      • 03d3d4eb5312:好文章
      • 我是C:能否发一下源码
        我是C:@love追风的孩子 粘代码自己运行下
        63031bfa603f:@J0hnnny 微信眼睛的源码都没有吗,这里只 写了一部分,看不大懂
        J0hnnny:@GreyLove sorry 示例代码都是现场写的,没有保存下来
      • zooleebee:学习了,感谢分享
      • Dyua:太棒了啊...赞.
      • Azen:放肆~
      • szmichael:值得收场
      • szmichael:很不错
      • WhoJun:mark。
      • 伽蓝香:学习了
      • 一一无痕:谢谢分享
      • 花前月下:挺不错的。感谢分享
      • 41badb1b8221:不错!
      • 方克己:666你真是我亲哥:scream::scream::scream:
      • StoneLeon:不错 适合入门看.
      • RanMeng:Beizerpath ,path ,layer 一般的动画差不多都可以解决,但是我还是觉得配合base animation更棒

      本文标题:放肆的使用UIBezierPath和CAShapeLayer画各

      本文链接:https://www.haomeiwen.com/subject/qderhttx.html