# 给 UIView 来点烟花

3,211

## 烟花的细节

• 每一次点击都会产生两处烟花
• 每一处烟花会产生 8 个火花
• 每个火花都遵循着自己的轨迹
• 轨迹看起来相似，但其实不完全一样。从爆炸开始的位置来看，有部分朝，有部分朝，剩余的朝

## 实现

``````protocol SparkTrajectory {

/// 存储着定义轨迹所需要的所有的点
var points: [CGPoint] { get set }

/// 用 path 来表现轨迹
var path: UIBezierPath { get }
}
``````

``````/// 拥有两个控制点的贝塞尔曲线
struct CubicBezierTrajectory: SparkTrajectory {

var points = [CGPoint]()

init(_ x0: CGFloat, _ y0: CGFloat,
_ x1: CGFloat, _ y1: CGFloat,
_ x2: CGFloat, _ y2: CGFloat,
_ x3: CGFloat, _ y3: CGFloat) {
self.points.append(CGPoint(x: x0, y: y0))
self.points.append(CGPoint(x: x1, y: y1))
self.points.append(CGPoint(x: x2, y: y2))
self.points.append(CGPoint(x: x3, y: y3))
}

var path: UIBezierPath {
guard self.points.count == 4 else { fatalError("4 points required") }

let path = UIBezierPath()
path.move(to: self.points[0])
path.addCurve(to: self.points[3], controlPoint1: self.points[1], controlPoint2: self.points[2])
return path
}
}
``````

``````protocol SparkTrajectoryFactory {}

protocol ClassicSparkTrajectoryFactoryProtocol: SparkTrajectoryFactory {

func randomTopRight() -> SparkTrajectory
func randomBottomRight() -> SparkTrajectory
}

final class ClassicSparkTrajectoryFactory: ClassicSparkTrajectoryFactoryProtocol {

private lazy var topRight: [SparkTrajectory] = {
return [
CubicBezierTrajectory(0.00, 0.00, 0.31, -0.46, 0.74, -0.29, 0.99, 0.12),
CubicBezierTrajectory(0.00, 0.00, 0.31, -0.46, 0.62, -0.49, 0.88, -0.19),
CubicBezierTrajectory(0.00, 0.00, 0.10, -0.54, 0.44, -0.53, 0.66, -0.30),
CubicBezierTrajectory(0.00, 0.00, 0.19, -0.46, 0.41, -0.53, 0.65, -0.45),
]
}()

private lazy var bottomRight: [SparkTrajectory] = {
return [
CubicBezierTrajectory(0.00, 0.00, 0.42, -0.01, 0.68, 0.11, 0.87, 0.44),
CubicBezierTrajectory(0.00, 0.00, 0.35, 0.00, 0.55, 0.12, 0.62, 0.45),
CubicBezierTrajectory(0.00, 0.00, 0.21, 0.05, 0.31, 0.19, 0.32, 0.45),
CubicBezierTrajectory(0.00, 0.00, 0.18, 0.00, 0.31, 0.11, 0.35, 0.25),
]
}()

func randomTopRight() -> SparkTrajectory {
return self.topRight[Int(arc4random_uniform(UInt32(self.topRight.count)))]
}

func randomBottomRight() -> SparkTrajectory {
return self.bottomRight[Int(arc4random_uniform(UInt32(self.bottomRight.count)))]
}
}
``````

``````class SparkView: UIView {}

final class CircleColorSparkView: SparkView {

init(color: UIColor, size: CGSize) {
super.init(frame: CGRect(origin: .zero, size: size))
self.backgroundColor = color
}

}
}

extension UIColor {

static var sparkColorSet1: [UIColor] = {
return [
UIColor(red:0.89, green:0.58, blue:0.70, alpha:1.00),
UIColor(red:0.96, green:0.87, blue:0.62, alpha:1.00),
UIColor(red:0.67, green:0.82, blue:0.94, alpha:1.00),
UIColor(red:0.54, green:0.56, blue:0.94, alpha:1.00),
]
}()
}
``````

``````protocol SparkViewFactoryData {

var size: CGSize { get }
var index: Int { get }
}

protocol SparkViewFactory {

func create(with data: SparkViewFactoryData) -> SparkView
}

class CircleColorSparkViewFactory: SparkViewFactory {

var colors: [UIColor] {
return UIColor.sparkColorSet1
}

func create(with data: SparkViewFactoryData) -> SparkView {
let color = self.colors[data.index % self.colors.count]
return CircleColorSparkView(color: color, size: data.size)
}
}
``````

``````typealias FireworkSpark = (sparkView: SparkView, trajectory: SparkTrajectory)

protocol Firework {

/// 烟花的初始位置
var origin: CGPoint { get set }

/// 定义了轨迹的大小. 轨迹都是统一大小
/// 所以需要在展示到屏幕上前将其放大
var scale: CGFloat { get set }

/// 火花的大小
var sparkSize: CGSize { get set }

/// 获取轨迹
var trajectoryFactory: SparkTrajectoryFactory { get }

/// 获取火花视图
var sparkViewFactory: SparkViewFactory { get }

func sparkViewFactoryData(at index: Int) -> SparkViewFactoryData
func sparkView(at index: Int) -> SparkView
func trajectory(at index: Int) -> SparkTrajectory
}

extension Firework {

/// 帮助方法，用于返回火花视图及对应的轨迹
func spark(at index: Int) -> FireworkSpark {
return FireworkSpark(self.sparkView(at: index), self.trajectory(at: index))
}
}
``````

• origin
• scale
• sparkSize
• trajectoryFactory
• sparkViewFactory

``````extension SparkTrajectory {

/// 缩放轨迹使其符合各种 UI 的要求
/// 在各种形变和 shift: 之前使用
func scale(by value: CGFloat) -> SparkTrajectory {
var copy = self
(0..<self.points.count).forEach { copy.points[\$0].multiply(by: value) }
return copy
}

/// 水平翻转轨迹
func flip() -> SparkTrajectory {
var copy = self
(0..<self.points.count).forEach { copy.points[\$0].x *= -1 }
return copy
}

/// 偏移轨迹，在每个点上生效
/// 在各种形变和 scale: 和之后使用
func shift(to point: CGPoint) -> SparkTrajectory {
var copy = self
let vector = CGVector(dx: point.x, dy: point.y)
return copy
}
}
``````

``````class ClassicFirework: Firework {

/**
x     |     x
x  |   x
|
---------------
x |  x
x   |
|     x
**/

private struct FlipOptions: OptionSet {

let rawValue: Int

static let horizontally = FlipOptions(rawValue: 1 << 0)
static let vertically = FlipOptions(rawValue: 1 << 1)
}

private enum Quarter {

case topRight
case bottomRight
case bottomLeft
case topLeft
}

var origin: CGPoint
var scale: CGFloat
var sparkSize: CGSize

var maxChangeValue: Int {
return 10
}

var trajectoryFactory: SparkTrajectoryFactory {
return ClassicSparkTrajectoryFactory()
}

var classicTrajectoryFactory: ClassicSparkTrajectoryFactoryProtocol {
return self.trajectoryFactory as! ClassicSparkTrajectoryFactoryProtocol
}

var sparkViewFactory: SparkViewFactory {
return CircleColorSparkViewFactory()
}

private var quarters = [Quarter]()

init(origin: CGPoint, sparkSize: CGSize, scale: CGFloat) {
self.origin = origin
self.scale = scale
self.sparkSize = sparkSize
self.quarters = self.shuffledQuarters()
}

func sparkViewFactoryData(at index: Int) -> SparkViewFactoryData {
return DefaultSparkViewFactoryData(size: self.sparkSize, index: index)
}

func sparkView(at index: Int) -> SparkView {
return self.sparkViewFactory.create(with: self.sparkViewFactoryData(at: index))
}

func trajectory(at index: Int) -> SparkTrajectory {
let quarter = self.quarters[index]
let flipOptions = self.flipOptions(for: quarter)
let changeVector = self.randomChangeVector(flipOptions: flipOptions, maxValue: self.maxChangeValue)
return self.randomTrajectory(flipOptions: flipOptions).scale(by: self.scale).shift(to: sparkOrigin)
}

private func flipOptions(`for` quarter: Quarter) -> FlipOptions {
var flipOptions: FlipOptions = []
if quarter == .bottomLeft || quarter == .topLeft {
flipOptions.insert(.horizontally)
}

if quarter == .bottomLeft || quarter == .bottomRight {
flipOptions.insert(.vertically)
}

return flipOptions
}

private func shuffledQuarters() -> [Quarter] {
var quarters: [Quarter] = [
.topRight, .topRight,
.bottomRight, .bottomRight,
.bottomLeft, .bottomLeft,
.topLeft, .topLeft
]

var shuffled = [Quarter]()
for _ in 0..<quarters.count {
let idx = Int(arc4random_uniform(UInt32(quarters.count)))
shuffled.append(quarters[idx])
quarters.remove(at: idx)
}

return shuffled
}

private func randomTrajectory(flipOptions: FlipOptions) -> SparkTrajectory {
var trajectory: SparkTrajectory

if flipOptions.contains(.vertically) {
trajectory = self.classicTrajectoryFactory.randomBottomRight()
} else {
trajectory = self.classicTrajectoryFactory.randomTopRight()
}

return flipOptions.contains(.horizontally) ? trajectory.flip() : trajectory
}

private func randomChangeVector(flipOptions: FlipOptions, maxValue: Int) -> CGVector {
let values = (self.randomChange(maxValue), self.randomChange(maxValue))
let changeX = flipOptions.contains(.horizontally) ? -values.0 : values.0
let changeY = flipOptions.contains(.vertically) ? values.1 : -values.0
return CGVector(dx: changeX, dy: changeY)
}

private func randomChange(_ maxValue: Int) -> CGFloat {
return CGFloat(arc4random_uniform(UInt32(maxValue)))
}
}
``````

``````protocol SparkViewAnimator {

func animate(spark: FireworkSpark, duration: TimeInterval)
}
``````

``````struct ClassicFireworkAnimator: SparkViewAnimator {

func animate(spark: FireworkSpark, duration: TimeInterval) {
spark.sparkView.isHidden = false // show previously hidden spark view

CATransaction.begin()

// 火花的位置
let positionAnim = CAKeyframeAnimation(keyPath: "position")
positionAnim.path = spark.trajectory.path.cgPath
positionAnim.calculationMode = kCAAnimationLinear
positionAnim.rotationMode = kCAAnimationRotateAuto
positionAnim.duration = duration

// 火花的缩放
let randomMaxScale = 1.0 + CGFloat(arc4random_uniform(7)) / 10.0
let randomMinScale = 0.5 + CGFloat(arc4random_uniform(3)) / 10.0

let fromTransform = CATransform3DIdentity
let byTransform = CATransform3DScale(fromTransform, randomMaxScale, randomMaxScale, randomMaxScale)
let toTransform = CATransform3DScale(CATransform3DIdentity, randomMinScale, randomMinScale, randomMinScale)
let transformAnim = CAKeyframeAnimation(keyPath: "transform")

transformAnim.values = [
NSValue(caTransform3D: fromTransform),
NSValue(caTransform3D: byTransform),
NSValue(caTransform3D: toTransform)
]

transformAnim.duration = duration
transformAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
spark.sparkView.layer.transform = toTransform

// 火花的不透明度
let opacityAnim = CAKeyframeAnimation(keyPath: "opacity")
opacityAnim.values = [1.0, 0.0]
opacityAnim.keyTimes = [0.95, 0.98]
opacityAnim.duration = duration
spark.sparkView.layer.opacity = 0.0

// 组合动画
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [positionAnim, transformAnim, opacityAnim]
groupAnimation.duration = duration

CATransaction.setCompletionBlock({
spark.sparkView.removeFromSuperview()
})

CATransaction.commit()
}
}
``````

``````class ClassicFireworkController {

var sparkAnimator: SparkViewAnimator {
return ClassicFireworkAnimator()
}

func createFirework(at origin: CGPoint, sparkSize: CGSize, scale: CGFloat) -> Firework {
return ClassicFirework(origin: origin, sparkSize: sparkSize, scale: scale)
}

/// 让烟花在其源视图的角落附近爆开
func addFireworks(count fireworksCount: Int = 1,
sparks sparksCount: Int,
around sourceView: UIView,
sparkSize: CGSize = CGSize(width: 7, height: 7),
scale: CGFloat = 45.0,
maxVectorChange: CGFloat = 15.0,
animationDuration: TimeInterval = 0.4,
canChangeZIndex: Bool = true) {
guard let superview = sourceView.superview else { fatalError() }

let origins = [
CGPoint(x: sourceView.frame.minX, y: sourceView.frame.minY),
CGPoint(x: sourceView.frame.maxX, y: sourceView.frame.minY),
CGPoint(x: sourceView.frame.minX, y: sourceView.frame.maxY),
CGPoint(x: sourceView.frame.maxX, y: sourceView.frame.maxY),
]

for _ in 0..<fireworksCount {
let idx = Int(arc4random_uniform(UInt32(origins.count)))
let origin = origins[idx].adding(vector: self.randomChangeVector(max: maxVectorChange))

let firework = self.createFirework(at: origin, sparkSize: sparkSize, scale: scale)

for sparkIndex in 0..<sparksCount {
let spark = firework.spark(at: sparkIndex)
spark.sparkView.isHidden = true

if canChangeZIndex {
let zIndexChange: CGFloat = arc4random_uniform(2) == 0 ? -1 : +1
spark.sparkView.layer.zPosition = sourceView.layer.zPosition + zIndexChange
} else {
spark.sparkView.layer.zPosition = sourceView.layer.zPosition
}

self.sparkAnimator.animate(spark: spark, duration: animationDuration)
}
}
}

private func randomChangeVector(max: CGFloat) -> CGVector {
return CGVector(dx: self.randomChange(max: max), dy: self.randomChange(max: max))
}

private func randomChange(max: CGFloat) -> CGFloat {
return CGFloat(arc4random_uniform(UInt32(max))) - (max / 2.0)
}
}
``````

``````self.fireworkController.addFireworks(count: 2, sparks: 8, around: button)
``````