CAEmitter层

465 阅读12分钟
原文链接: www.jianshu.com

从词源学,五彩纸屑来自于糖衣杏仁甜字意在庆祝抛出,这反过来,来自拉丁语得到他们的名字conficio:CON组(“与一起”) + 法西奥(“做什么,”) ; 在另一种意义上,“庆祝”

五彩纸屑被围了很多这些天扔,但几乎没有在20 世纪,其标志性的股票磁带游行下来纽约市的街道,像一个回家的阿波罗11号宇航员50年前。唉,数字技术的兴起使得废纸带包含这些眼镜的基板的股票代码过时了。结果,这种传统在今天变得越来越不常见。

事实上, “华盛顿邮报”今天刚刚报道,星期一的世界杯胜利庆祝美国女子足球队的自动收报机游戏是“纽约人第207次将办公室垃圾倾倒在他们的英雄身上

小编这里推荐一个群:691040931 里面有大量的书籍和面试资料哦有技术的来闲聊 没技术的来学习

让我们快速浏览一下视图和图层之间的区别:

视图和图层

在iOS上,每个视图都由一个图层支持......或者更准确地说,图层是视图前端的。

因为尽管他们作为UIKit的主力而享有声誉,但仍将其 UIView绝大部分功能委托给了CALayer。当然,视图处理触摸事件和自动调整,但除此之外,代码和屏幕上像素之间的几乎所有其他内容都是图层的责任。

在现有的CALayer石英核心/核心动画框架子类有通过显示大量的内容API的滚动 和瓷砖,也有做的API 先进的 转换,并且有让你在裸API的 金属。但我们今天的重点是一个特殊的课程 。CAEmitterLayer

粒子发射器

实际上,粒子系统经常用于产生火,烟,火花,烟火和爆炸。但它们也能够建模......不那么具有破坏性的现象,如雨,雪,沙,以及 - 最重要的 - 五彩纸屑。

CAEmitterLayer配置粒子发射位置和形状。至于那些粒子的具体行为和外观,这是由接种到发射层的物体决定的。CAEmitterCell

也就是说,粒子发射器层具有配置出生率,寿命,速度和旋转的特性; 这些作为构成发射器单元中设置的值的标量。

类推:

  • CAEmitterLayer 控制五彩纸屑大小的大小,位置和强度,
  • CAEmitterCell 控制装入料斗的每种类型五彩纸屑的大小,形状,颜色和移动。

如果你想用五彩纸屑黑色小胡子,橙色鸟和紫色土豚,那么你会加载一个与三个不同的对象,每个对象指定的,和其他行为。CAEmitterLayer CAEmitterCell color contents

粒子发射器细胞

高性能的秘诀在于它不会单独跟踪每个粒子。与视图甚至图层不同,发射的粒子一旦创建就不能改变。 (此外,它们不会彼此交互,这使得它们可以轻松地并行渲染)CAEmitterLayer

相反, 它具有巨大的API表面区域,可以在生成之前配置粒子外观和行为的每个方面,包括出生率,寿命,发射角度,速度,加速度,比例,放大滤镜,颜色 - 太多以覆盖任何深度这里。CAEmitterCell

通常,大多数发射器单元行为由单个属性或一组相关属性定义,这些属性指定基值 以及相应的范围和 / 或速度 。

范围属性指定可以随机添加或从基值中减去的最大金额。例如,scale属性确定每个粒子的大小,属性指定相对于该基值的可能大小的上限和下限; 一的和的 生成粒子0.8×和1.2之间尺寸×原始尺寸。scaleRange scale 1.0 scaleRange 0.2 contents

细胞发射器行为还可以具有相应的速度特性,其指定颗粒寿命期间的生长或衰减速率。例如,对于该属性,正值会导致粒子随时间增长,而负值会导致粒子缩小。scaleSpeed


我们已经充分了解了这些细节,现在是我们让这些知识在一连串代码中迸发出来的时候了!CAEmitterLayer

实现iOS的五彩纸屑视图

首先,让我们定义一个五彩纸屑的抽象,我们想从我们的五彩纸屑大炮中拍摄。枚举为我们的目的提供了约束和灵活性的完美平衡。

enum Content {
    enum Shape {
        case circle
        case triangle
        case square
        case custom(CGPath)
    }

    case shape(Shape, UIColor?)
    case image(UIImage, UIColor?)
    case emoji(Character)
}

以下是我们如何配置我们的五彩纸屑大炮来拍摄各种各样的形状和图像:

let contents: [Content] = [
    .shape(.circle, .purple),
    .shape(.triangle, .lightGray),
    .image(UIImage(named: "swift")!, .orange),
    .emoji("👨🏻"),
    .emoji("📱")
]

为简洁起见,我们正在跳过我们如何构建形状路径或将表情符号字符渲染为图像,但也有一些好东西。UIGraphicsImageRenderer

展开实施细节

fileprivate extension Content {
    var color: UIColor? {
        switch self {
        case let .image(_, color?),
             let .shape(_, color?):
            return color
        default:
            return nil
        }
    }

    var image: UIImage {
        switch self {
        case let .image(image, _):
            return image
        case let .shape(shape, color):
            return shape.image(with: color ?? .white)
        case let .emoji(character):
            return "\(character)".image()
        }
    }
}

fileprivate extension Content.Shape {
    func path(in rect: CGRect) -> CGPath {
        switch self {
        case .circle:
            return CGPath(ellipseIn: rect, transform: nil)
        case .triangle:
            let path = CGMutablePath()
            path.addLines(between: [
                CGPoint(x: rect.midX, y: 0),
                CGPoint(x: rect.maxX, y: rect.maxY),
                CGPoint(x: rect.minX, y: rect.maxY),
                CGPoint(x: rect.midX, y: 0)
            ])
            return path
        case .square:
            return CGPath(rect: rect, transform: nil)
        case .custom(let path):
            return path
        }
    }

    func image(with color: UIColor) -> UIImage {
        let rect = CGRect(origin: .zero, size: CGSize(width: 12.0, height: 12.0))
        return UIGraphicsImageRenderer(size: rect.size).image { context in
            context.cgContext.setFillColor(color.cgColor)
            context.cgContext.addPath(path(in: rect))
            context.cgContext.fillPath()
        }
    }
}

fileprivate extension String {
    func image(with font: UIFont = UIFont.systemFont(ofSize: 16.0)) -> UIImage {
        let string = NSString(string: "\(self)")
        let attributes: [NSAttributedString.Key: Any] = [
            .font: font
        ]
        let size = string.size(withAttributes: attributes)

        return UIGraphicsImageRenderer(size: size).image { _ in
            string.draw(at: .zero, withAttributes: attributes)
        }
    }
}

创建CAEmitterLayer子类

下一步是实现发射器层本身。

主要责任 是配置其单元格。五彩纸屑从上面下来,只是在尺寸,速度和旋转方面有足够的变化,使其变得有趣。我们使用传递的值数组来设置单元格(a )和填充颜色(a )。CAEmitterLayer Content contents CGImage CGColor

private final class Layer: CAEmitterLayer {
    func configure(with contents: [Content]) {
        emitterCells = contents.map { content in
            let cell = CAEmitterCell()

            cell.birthRate = 50.0
            cell.lifetime = 10.0
            cell.velocity = CGFloat(cell.birthRate * cell.lifetime)
            cell.velocityRange = cell.velocity / 2
            cell.emissionLongitude = .pi
            cell.emissionRange = .pi / 4
            cell.spinRange = .pi * 6
            cell.scaleRange = 0.25
            cell.scale = 1.0 - cell.scaleRange
            cell.contents = content.image.cgImage
            if let color = content.color {
                cell.color = color.cgColor
            }

            return cell
        }
    }

    ...
}

我们configure(with:)将从我们的五彩纸屑视图中调用此方法,这将是我们的下一步也是最后一步。

尽管不是绝对必要,但是子类化允许我们覆盖 并自动调整发射器的大小和位置,只要它发生变化就会沿着图层框架的顶部边缘定位(否则你不能这么做)。CAEmitterLayer layoutSublayers()

...

    // MARK: CALayer

    override func layoutSublayers() {
        super.layoutSublayers()

        emitterShape = .line
        emitterSize = CGSize(width: frame.size.width, height: 1.0)
        emitterPosition = CGPoint(x: frame.size.width / 2.0, y: 0)
    }
}

实现ConfettiView

我们希望我们的五彩纸屑视图在一定时间内发出五彩纸屑,然后停止。然而,实现这一点非常困难,正如Stack Overflow上的问题所证明的那样 。

核心问题在于核心动画在其自己的时间轴上运行,这并不总是符合我们对时间的理解。

例如,如果您在显示之前忽略了初始化发射器层的权利,那么它将使用错误的时间空间进行渲染。beginTime CACurrentMediaTime()

就停止而言:您可以通过将其属性设置为来告诉图层停止发射粒子。但是如果你通过重置该属性再次启动它,你会得到一大堆粒子填满屏幕而不是第一次发射时的漂亮的初始爆发。birthRate 0 1

可以说,有许多不同的方法使这种行为符合预期。但这是我们发现的处理启动和停止以及同时具有多个发射器的最佳解决方案:


回到我们原来的解释片刻,UIView (或其子类之一)的每个实例都由一个(或其子类之一)的单个相应实例支持CALayer 。视图还可以托管一个或多个附加层,作为后代层的兄弟姐妹或子级。

利用这一事实,我们可以在每次点燃五彩纸屑大炮时创建一个新的发射器层。我们可以将该图层添加为视图的托管子图层,并让视图以一种漂亮,自包含的方式处理每个图层的动画和处理。

private let kAnimationLayerKey = "com.nshipster.animationLayer"

final class ConfettiView: UIView {
    func emit(with contents: [Content],
              for duration: TimeInterval = 3.0)
    {
    ❶ let layer = Layer()
        layer.configure(with: contents)
        layer.frame = self.bounds
        layer.needsDisplayOnBoundsChange = true
        self.layer.addSublayer(layer)

        guard duration.isFinite else { return }

    ❷ let animation = CAKeyframeAnimation(keyPath: #keyPath(CAEmitterLayer.birthRate))
        animation.duration = duration
        animation.timingFunction = CAMediaTimingFunction(name: .easeIn)
        animation.values = [1, 0, 0]
        animation.keyTimes = [0, 0.5, 1]
        animation.fillMode = .forwards
        animation.isRemovedOnCompletion = false

        layer.beginTime = CACurrentMediaTime()
        layer.birthRate = 1.0

    ❸ CATransaction.begin()
        CATransaction.setCompletionBlock {
            let transition = CATransition()
            transition.delegate = self
            transition.type = .fade
            transition.duration = 1
            transition.timingFunction = CAMediaTimingFunction(name: .easeOut)
            transition.setValue(layer, forKey: kAnimationLayerKey) ❹
            transition.isRemovedOnCompletion = false

            layer.add(transition, forKey: nil)

            layer.opacity = 0
        }
        layer.add(animation, forKey: nil)
        CATransaction.commit()
    }

    ...

这里要解包很多代码,所以让我们关注它的不同部分:

首先,我们创建一个自定义子类的实例(在这里创造性命名,因为它是一个私有的嵌套类型)。它使用我们之前的方法设置并添加为子图层。 无论出于何种原因,该属性都默认为; 将它设置为允许我们更好地处理设备特征变化(如旋转或移动到新窗口)。CAEmitterLayer Layer configure(with:) needsDisplayOnBoundsChange false true

接下来,我们创建一个关键帧动画,以将属性逐渐缩小到指定的持续时间。birthRate 0

然后我们在事务中添加该动画,并使用完成块设置淡出过渡。(我们将视图设置为转换的动画委托,如下一个会话中所述)

最后,我们使用键值编码在发射器层和转换之间创建一个引用,以便以后可以引用和清除它。

我们总是希望五彩纸屑视图填充其超级视图的范围,我们可以通过覆盖来确保这种行为。这样做的另一个好处是它允许使用简单的,不合格的初始化程序。willMove(toSuperview:) ConfettiView

final class ConfettiView: UIView {
    init() {
        super.init(frame: .zero)
    }

    ...

    // MARK: UIView

    override func willMove(toSuperview newSuperview: UIView?) {
        guard let superview = newSuperview else { return }
        frame = superview.bounds
    }
}

采用CAAnimationDelegate协议

为了扩展我们的总体比喻, 是 Rocky和Bullwinkle的小卡通看门人 在自动收报机游行结束时用推扫帚。CAAnimationDelegate

该委托方法被调用时,我们完成。然后,我们获取对调用层的引用,以便删除所有动画并将其从超级层中删除。animationDidStop(_:) CATransition

// MARK: - CAAnimationDelegate

extension ConfettiView: CAAnimationDelegate {
    func animationDidStop(_ animation: CAAnimation, finished flag: Bool) {
        if let layer = animation.value(forKey: kAnimationLayerKey) as? CALayer {
            layer.removeAllAnimations()
            layer.removeFromSuperlayer()
        }
    }
}

最终的结果是:好像NSHipster.com在纽约市的街道上游行(或者更确切地说,布鲁克林,如果你真的想要进入时髦的东西)


相反,我们将以奖励回合结束,详细说明您可以实施五彩纸屑的其他七种方式:


✨BONUS✨五彩纸屑的替代方法

SpriteKit粒子系统

SpriteKit是UIKit更酷,更年轻的堂兄,为游戏提供节点而不是应用程序的视图。在他们的表面上,他们看起来彼此不同。然而,两者都有共同的,深层的依赖层,这使得熟悉的低级API成为可能。

这两个框架之间的比较更深入,如果您打开文件>新建,向下滚动到“资源”并创建一个新的SpriteKit粒子系统文件。打开它,Xcode提供了一个让人想起Interface Builder的专用编辑器。

通过名称调用您的设计或重新实现代码 (如果您是手动滚动所有s的类型) 以获得定制的五彩纸屑体验。SKEmitterNodeUIView

SceneKit粒子系统

再次使用这些比喻,SceneKit将3D的SpriteKit变为2D。

在Xcode 11中,打开文件>新建,在“资源”标题下选择SceneKit场景文件,您将在Xcode窗口中找到整个3D场景编辑器。

添加一个动态的物理体和湍流效果,你可以立刻掀起一个令人惊讶的功能模拟(虽然如果你像我一样,你可能会发现自己花了几个小时只玩一切)

Xcode 10包含一个“SceneKit粒子系统文件”模板 - 带有用于生成五彩纸屑的预设!不幸的是,我们无法让编辑器工作。使用Xcode 11,此模板和.scnp文件类型已被删除。

UIKit动力学

在SpriteKit进入现场的同时,你可能会想象UIKit开始对自己所推出的“仅限商业”的氛围有所了解。

因此,在iOS 7中,UIKit一直试图向其“同胞们”证明自己很酷,并将其添加为一组称为“UIKit Dynamics”的API。UIDynamicAnimator

HEVC视频与Alpha通道

好消息!今年晚些时候,AVFoundation在HEVC视频中增加了对alpha通道的支持!因此,如果您已经拥有一个时髦的After Effects组合的五彩纸屑,您可以使用透明背景导出它并将其直接合成到您的应用或网络上。

动画PNG

当然,明年的这个时候,尽管存在各种缺点,我们仍然会向对方发送动画GIF。

动画GIF对于透明度尤其糟糕。如果没有合适的Alpha通道,GIF将限制为单一的透明遮罩颜色,这会导致边缘周围出现难看的瑕疵。

我们都知道在网络上使用PNG来获取透明图像,但我们中只有一小部分人甚至意识到 APNG)甚至是一件事。

更不用说我们知道iOS 13增加了对APNG的原生支持(以及动画GIF - 最后!)。不仅今年WWDC上没有公告,而且这个新功能存在的唯一线索是Xcode 11 Beta 2和3之间的API差异。这是该功能的文档存根, CGAnimateImageAtURLWithBlock

如果你认为(不幸的话)这些天的课程相当,它会变得更糟。因为无论出于何种原因,相关的头文件()在Swift中是不可访问的!ImageIO/CGImageAnimation.h

现在,我们真的不知道这应该如何工作,但这是我们最好的猜测:

// ⚠️ Expected Usage
let imageView = UIImageView()

let imageURL = URL(fileURLWithPath: "path/to/animated.png")
let options: [String: Any] = [kCGImageAnimationLoopCount: 42]
CGAnimateImageAtURLWithBlock(imageURL, options) { (index, cgimage, stop) in
    imageView.image = UIImage(cgImage: cg)
}

同时,Safari已经支持动画PNG很长时间了,而且API更简单:

<img src="animated.png" />

WebGL的

说到网络,我们来谈谈一个名为WebGL的闪亮的,新的( - )网络标准 。

只需几百行JavaScript和GL着色器语言,您也可以为您自己的博客渲染五彩纸屑 Web开发Objective-C,Swift和Cocoa

表情符号

但实际上,我们可以取消所有这些五彩纸屑并更简单地表达自己:

let mood = "🥳"

扫码进交流群 有技术的来闲聊 没技术的来学习

691040931

原文转载地址:nshipster.com/caemitterla…