6-闪耀的激光-CALayer 的应用

Posted 颐和园

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了6-闪耀的激光-CALayer 的应用相关的知识,希望对你有一定的参考价值。

##开始

最近项目中需要实现一个名片扫描的功能,这会用到一个让用户等待名片识别结果的界面。界面非常简单,让一条绿色的激光不停地在名片上上下移动,模拟扫描仪正在工作的样子。

这么简单的 UI,完全可以抛开美工和切图,一个人用 Core Grahics 来实现!而且这次要检验我们之前的学习成果!

绘制4个角

首先我们在窗口的 4 个角上绘制直角图形,用于表示镜头的取景框。首先绘制第一个角。只要绘制出第一个角,其它 3 个角无非是在它的基础上旋转一定角度而已。

首先声明变量:

    var borderColor: UIColor = .white // 4 个角的颜色
    var borderWidth: CGFloat = 3.5  // 4 个角的线宽
    lazy private var cornerLayer: CALayer = 
    	  // 1
        let cornerLayer = CALayer()
        // 2
        cornerLayer.frame = bounds
        // 3
        cornerLayer.position = center
        // 4
        cornerLayer.addSublayer(leftTopCornerLayer())
        // 5
        return cornerLayer
    ()

cornerLayer 用于绘制 4 个直角,它是一个懒加载属性,意味着当第一次访问这个属性时,使用代码块中的代码初始化它的值。这里我们不准备在 draw(_😃 函数中绘制了,因为这个图形是静止的,从头到尾都不会有任何变化,我们将它绘制在一个 CALayer 上,然后添加到视图中即可。

关于 CALayer,你可以简单地把它看成是 UIView,它具有和 UIView 视图树一样的树形结构,但是你不能在它上面添加任何 UIKit 组件,你只能添加其它 CALayer,比如 CAShapeLayer。

现在让我们来仔细分析这个懒加载到属性:

  1. 初始化一个 CALayer。
  2. 设置它的 frame 和承载它的视图一样大小。
  3. 让它对齐视图的中心。
  4. 添加第一个 subLayer。这个 subLayer 上绘制 4 个角中的第一个角。leftTopCornerLayer 方法负责创建该 subLayer。
  5. 返回这个 CALayer,作为 cornerLayer 的初始值。

然后来看 leftTopCornerLayer 函数:

    private func leftTopCornerLayer() -> CALayer 
        // 1
        let cornerLayer = CAShapeLayer()
        cornerLayer.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
        cornerLayer.lineWidth = borderWidth
        cornerLayer.strokeColor = borderColor.cgColor
        cornerLayer.fillColor = UIColor.clear.cgColor
        // 2
        var points: [CGPoint] = []
        points.append(CGPoint(x: 36, y: 4))
        points.append(CGPoint(x: 4, y: 4))
        points.append(CGPoint(x: 4, y: 36))
        // 3
        let cornerPath = LinePainter.polyLines(points)
        // 4
        cornerPath.lineCapStyle = .round
        cornerPath.lineJoinStyle = .round
        // 5
        cornerLayer.path = cornerPath.cgPath
        return cornerLayer
    
  1. 构建一个 CAShapeLayer,它是 CALayer 的子类,可以用贝塞尔曲线在 CAShapeLayer 上绘制各种几何图形。同时,设置它的线宽、stokeColor 等属性,这样绘制图形时可以用上这些属性。
  2. 因为要绘制线段,我们需要构造折线上的顶点。
  3. 利用前面课程中实现的 LinePainter 生成路径。
  4. lineCapStyle 属性和 lineJoinStyle 属性分别用于设置曲线末端和曲线弯曲处的样式。这里都设置为圆角会好看一点。
  5. 将路径赋值给 CAShapeLayer 的 path,完成图形的绘制。

绘制其余直角。只不过是在上面图形的基础上进行旋转而已,很容易理解,无需过多解释:

    private func rightTopCornerLayer() -> CALayer 
        let cornerLayer = leftTopCornerLayer()
        // 1
        cornerLayer.position = CGPoint(x: frame.maxX-20, y: 20)
        // 2
        cornerLayer.transform = CATransform3DRotate(CATransform3DIdentity, -CGFloat.pi/2, 0, 0, -1.0)
        return cornerLayer
    
    
    private func leftBottomoCornerLayer() -> CALayer 
        let cornerLayer = leftTopCornerLayer()
        cornerLayer.position = CGPoint(x: 20, y: frame.maxY-20)
        cornerLayer.transform = CATransform3DRotate(CATransform3DIdentity, CGFloat.pi/2, 0, 0, -1.0)
        return cornerLayer
    
    private func rightBottomoCornerLayer() -> CALayer 
        let cornerLayer = leftTopCornerLayer()
        cornerLayer.position = CGPoint(x: frame.maxX-20, y: frame.maxY-20)
        cornerLayer.transform = CATransform3DRotate(CATransform3DIdentity, CGFloat.pi, 0, 0, -1.0)
        return cornerLayer
    
  1. CALayer 的 position 和 anchor 其实都是指坐标的原点。只不过前者是 super layer 的坐标系,而 anchor 使用的是 layer 的坐标系( x, y 都取值 0-1 之间)。

  2. CATransform3DRotate 方法的第一个参数是原转换矩阵(也就是旋转之前的矩阵),这里我们当然是用 identity 矩阵了(这个矩阵是没有经过任何 3d 变换的原始矩阵)。第 2 个参数是旋转角度,记得是弧度哦。第 3、4、5 个参数分别指定要在哪个轴上进行旋转,我们做的是平面旋转(z 轴),所以 x, y 轴都是 0,z 轴设置为 1 表示顺时针旋转,-1 为反时针旋转。

然后在 cornerLayer 属性中调用它们:

    lazy private var cornerLayer: CALayer = 
        ... ...
        cornerLayer.addSublayer(rightTopCornerLayer())
        cornerLayer.addSublayer(leftBottomoCornerLayer())
        cornerLayer.addSublayer(rightBottomoCornerLayer())
        return cornerLayer
    ()

构造器:

    override init(frame: CGRect) 
        super.init(frame: frame)
        commonInit()
    
    required init?(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)
        commonInit()
    
    fileprivate func commonInit() 
        layer.addSublayer(cornerLayer)
    

在 commonInit 中,用 layer.addSublayer() 方法将 cornerLayer 添加到视图中。

绘制激光

首先绘制一条静止不动的激光。这个很简单:

    var laserWidth:CGFloat = 4 // 激光粗细
    var laserColor: UIColor = .green // 激光颜色
    var gap: CGFloat = 16 // 激光距离边框的距离
    var laserLayerHeight:CGFloat = 20 // laserLayer 的高度,要留出足够的空间给阴影
    // 阴影
    lazy var laserShadow: NSShadow! = 
        let shadow = NSShadow()
        shadow.shadowColor = UIColor(red: 0, green: 1, blue: 1, alpha: 1)
        shadow.shadowOffset = CGSize(width: 0, height: 0)
        shadow.shadowBlurRadius = 4
        return shadow
    ()
    
    lazy private var laserLayer: CAShapeLayer = 
        let lineLayer = CAShapeLayer()
        lineLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: laserLayerHeight)
        lineLayer.lineWidth = laserWidth
        lineLayer.strokeColor = laserColor.cgColor
        layer.addSublayer(lineLayer)
        let points = [CGPoint(x: gap, y: laserLayerHeight/2), CGPoint(x:frame.width-gap, y: laserLayerHeight/2)]
        let linePath = LinePainter.polyLines(points)
        lineLayer.path = linePath.cgPath
        lineLayer.shadowOffset = laserShadow.shadowOffset
        lineLayer.shadowColor = (laserShadow.shadowColor as! UIColor).cgColor
        lineLayer.shadowRadius = laserShadow.shadowBlurRadius
        lineLayer.shadowOpacity = 1
        return lineLayer
    ()

同样使用 CAShapeLayer 绘制线段,不过这次只是一条直线,我们略微增加了一点阴影。

在 commonInit() 方法中,添加 laserLayer 图层。

layer.addSublayer(laserLayer)

使用定时器进行动画

首先需要定义一些变量:

    private var direction = 1 // 激光移动方向,1 向下,-1 向上
    private var currentY:CGFloat = 0 // 激光当前的 y 位置
    var speed:CGFloat = 75 // 移动速度,单位像素/秒
    var timeInterval: CGFloat = 0.03 // 时钟频率,单位秒
    private var isAnimating = false // 表示激光是否在移动中
    private var jiggleColor: UIColor! // 激光闪烁时的颜色

这次使用常规的定时器:

    lazy private var timer: Timer = 
        let timer = Timer(timeInterval: Double(timeInterval), target: self, selector: #selector(timerUpdate), userInfo: nil, repeats: true)
        
        return timer
    ()

注意我们没有使用 scheduledTimer 函数来构建定时器,这样它不会自动触发。

激光工作时会带有一个明暗交替的闪烁效果。我们可以通过定时切换激光颜色来模拟这个效果。

第一个颜色我们已经有了就是激光本来的绿色(即 laserColor)。第二个颜色用变量 jiggleColor 来存储。

为了简单起见,我们决定在第一个颜色的基础上降低它的亮度来作为第二个激光颜色。

在 commonInit 方法中,初始化 jiggleColor 颜色:

        // 1
        var h:CGFloat=0, s:CGFloat=0, b:CGFloat=0, a:CGFloat=0
        laserColor.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
        // 2
        jiggleColor = UIColor(hue: h, saturation: s, brightness: 0.7, alpha: a)
  1. 将 laserColor 的灰度、饱和度、亮度和透明度分别读出,存入相应变量。
  2. 修改 laserColor 的亮度为 70% 生成新的颜色,并赋给 jiggleColor。作为激光的第二个颜色。

然后是定时器的启动/停止:

    func startAnimation() 
        if isAnimating == false 
            isAnimating = true
            // 1
            RunLoop.current.add(timer, forMode: .common) 
        
    
    
    func stopAnimation() 
        if isAnimating == true 
            timer.invalidate()
            isAnimating = false
        
    

  1. 之前我们构造了一个常规的定时器(懒加载形式),但没有将它添加到 runloop。在这里我们进行这个动作。注意,timer.fire() 方法只会触发一次定时器调用,并不能反复多次运行。我们必须将定时器添加到当前 runloop 中它才能正常工作。

然后是定时器回调函数:

    @objc func timerUpdate() 
        // 1
        if direction == 1 && currentY > frame.height-laserLayerHeight 
            direction = -1
        
        // 2
        if direction == -1 && currentY < 0 
            direction = 1
        
        // 3
        moveLaser()
    

  1. 激光束的运动方向有两个:向上,或向下。通过定时器,我们让激光上下往复运动。我们用 direction 来表示激光束的运动方向。为 1 表示向下运动,-1 表示向上运动。当激光向下运动到视图底部(留有一定间距)时,改变方向,让它向上运动。
  2. 当激光向上运动到视图顶部(留有一个间距)时,改变方向,让它向下运动。
  3. 真正移动激光的实现方法在 moveLaser() 里。

来看 moveLaser() 方法:

    private func moveLaser() 
        // 1
        currentY = currentY+(speed*timeInterval)*CGFloat(direction)
        // 2
        let r = laserLayer.frame
        laserLayer.frame = CGRect(x: r.minX, y: currentY, width: r.width, height: r.height)
        // 3
        if round(currentY).truncatingRemainder(dividingBy: 2) > 0 
            laserLayer.strokeColor = jiggleColor.cgColor
            laserLayer.shadowOpacity = 0
        else 
            laserLayer.strokeColor = laserColor.cgColor
            laserLayer.shadowOpacity = 1
        
    

在 viewDidLoad 方法里:

        let view = LaserAnimationView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
        view.backgroundColor = .lightGray
        view.center = self.view.center
        view.startAnimation()

运行效果如下图:

以上是关于6-闪耀的激光-CALayer 的应用的主要内容,如果未能解决你的问题,请参考以下文章

BZOJ1218: [HNOI2003]激光炸弹

[激光原理与应用-28]:《激光原理与技术》-14- 激光产生技术 - 激光的主要参数与指标

[激光原理与应用-20]:《激光原理与技术》-6- 谐振腔的结构作用工作原理

[激光原理与应用-55]:《激光焊接质量实时监测系统研究》-6- Labview和LabWindows/CVI比较与选择

[激光器原理与应用-13]: 2022年中国激光行业产业链全景梳理

[激光器原理与应用-6]:Q开关元件与Q驱动电路板