import Foundation import UIKit import Display import ComponentFlow import Camera final class CameraCodeFrameView: UIView { private var cornerLayers: [SimpleShapeLayer] = [] private let cornerRadius: CGFloat = 12.0 private let focusedCornerRadius: CGFloat = 6.0 private let cornerShort: CGFloat = 16.0 private var currentSize: CGSize? private var currentRect: CGRect? override init(frame: CGRect) { super.init(frame: frame) self.isUserInteractionEnabled = false for _ in 0..<4 { let layer = SimpleShapeLayer() layer.fillColor = UIColor.clear.cgColor layer.strokeColor = UIColor.white.cgColor layer.lineWidth = 2.0 layer.lineCap = .round layer.lineJoin = .round self.layer.addSublayer(layer) self.cornerLayers.append(layer) } } required init?(coder: NSCoder) { preconditionFailure() } func update(size: CGSize, code: CameraCode?) { let isFirstTime = self.currentSize == nil self.currentSize = size var duration: Double = 0.0 let bounds = CGRect(origin: .zero, size: size) let rect: CGRect if let code { let codeRect = code.boundingBox let side = max(codeRect.width * bounds.width, codeRect.height * bounds.height) * 0.7 let center = CGPoint(x: (1.0 - codeRect.center.y) * bounds.width, y: codeRect.center.x * bounds.height) rect = CGSize(width: side, height: side).centered(around: center) if !isFirstTime { if let currentRect = self.currentRect { if rect.center.distance(to: currentRect.center) > 40.0 || abs(rect.size.width - currentRect.size.width) > 40.0 { duration = 0.35 } else { duration = 0.2 } } else { duration = 0.4 } } self.currentRect = rect } else { rect = bounds.insetBy(dx: -2.0, dy: -2.0) if !isFirstTime { duration = 0.4 } self.currentRect = nil } let focused = code != nil self.applyPaths(to: self.cornerPaths(for: rect, focused: focused, rotation: 0.0), focused: focused, duration: duration) } private func cornerPaths(for rect: CGRect, focused: Bool, rotation: Double) -> [UIBezierPath] { let effectiveCornerRadius = focused ? self.focusedCornerRadius : self.cornerRadius let center = CGPoint(x: rect.midX, y: rect.midY) let transform = CGAffineTransform(translationX: center.x, y: center.y).rotated(by: rotation).translatedBy(x: -center.x, y: -center.y) let topLeftPath = UIBezierPath() topLeftPath.move(to: CGPoint(x: rect.minX, y: focused ? rect.minY + self.cornerShort : rect.midY)) topLeftPath.addLine(to: CGPoint(x: rect.minX, y: rect.minY + effectiveCornerRadius)) topLeftPath.addQuadCurve( to: CGPoint(x: rect.minX + effectiveCornerRadius, y: rect.minY), controlPoint: CGPoint(x: rect.minX, y: rect.minY) ) topLeftPath.addLine(to: CGPoint(x: focused ? rect.minX + self.cornerShort : rect.midX, y: rect.minY)) topLeftPath.apply(transform) let topRightPath = UIBezierPath() topRightPath.move(to: CGPoint(x: rect.maxX, y: focused ? rect.minY + self.cornerShort : rect.midY)) topRightPath.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + effectiveCornerRadius)) topRightPath.addQuadCurve( to: CGPoint(x: rect.maxX - effectiveCornerRadius, y: rect.minY), controlPoint: CGPoint(x: rect.maxX, y: rect.minY) ) topRightPath.addLine(to: CGPoint(x: focused ? rect.maxX - self.cornerShort : rect.midX, y: rect.minY)) topRightPath.apply(transform) let bottomRightPath = UIBezierPath() bottomRightPath.move(to: CGPoint(x: rect.maxX, y: focused ? rect.maxY - self.cornerShort : rect.midY)) bottomRightPath.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - effectiveCornerRadius)) bottomRightPath.addQuadCurve( to: CGPoint(x: rect.maxX - effectiveCornerRadius, y: rect.maxY), controlPoint: CGPoint(x: rect.maxX, y: rect.maxY) ) bottomRightPath.addLine(to: CGPoint(x: focused ? rect.maxX - self.cornerShort : rect.midX, y: rect.maxY)) bottomRightPath.apply(transform) let bottomLeftPath = UIBezierPath() bottomLeftPath.move(to: CGPoint(x: rect.minX, y: focused ? rect.maxY - self.cornerShort : rect.midY)) bottomLeftPath.addLine(to: CGPoint(x: rect.minX, y: rect.maxY - effectiveCornerRadius)) bottomLeftPath.addQuadCurve( to: CGPoint(x: rect.minX + effectiveCornerRadius, y: rect.maxY), controlPoint: CGPoint(x: rect.minX, y: rect.maxY) ) bottomLeftPath.addLine(to: CGPoint(x: focused ? rect.minX + self.cornerShort : rect.midX, y: rect.maxY)) bottomLeftPath.apply(transform) return [topLeftPath, topRightPath, bottomRightPath, bottomLeftPath] } private var animatingAppearance = false private func applyPaths(to paths: [UIBezierPath], focused: Bool, duration: Double) { let animatingAppearance = self.animatingAppearance for (index, path) in paths.enumerated() { let layer = self.cornerLayers[index] let previousPath = layer.path let previousAlpha = layer.opacity let previousColor = layer.strokeColor ?? UIColor.clear.cgColor let previousLineWidth = layer.lineWidth if duration > 0.0 && !focused { } else { layer.path = path.cgPath } layer.opacity = focused ? 1.0 : 0.0 layer.strokeColor = focused ? UIColor(rgb: 0xf8d74a).cgColor : UIColor.white.cgColor layer.lineWidth = focused ? 5.0 : 2.0 layer.shadowOffset = .zero layer.shadowRadius = 1.0 layer.shadowColor = UIColor.black.cgColor layer.shadowOpacity = 0.2 if duration > 0.0 && !animatingAppearance { if focused && previousAlpha.isZero && index == 0 { self.animatingAppearance = true } if focused { var currentPath = previousPath var duration = duration if let presentationPath = layer.presentation()?.path { currentPath = presentationPath duration *= 0.5 } layer.animate(from: currentPath, to: path.cgPath, keyPath: "path", timingFunction: duration > 0.35 ? kCAMediaTimingFunctionSpring : CAMediaTimingFunctionName.linear.rawValue, duration: duration, completion: { _ in if focused && index == 0 { self.animatingAppearance = false } }) } layer.animateAlpha(from: CGFloat(previousAlpha), to: CGFloat(layer.opacity), duration: focused ? 0.4 : 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, completion: !focused ? { finished in layer.path = path.cgPath } : nil) layer.animate(from: previousColor, to: layer.strokeColor ?? UIColor.white.cgColor, keyPath: "strokeColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3, delay: 0.15) layer.animate(from: previousLineWidth, to: layer.lineWidth, keyPath: "lineWidth", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3) } } } } private extension CGPoint { func distance(to point: CGPoint) -> CGFloat { return sqrt(pow((point.x - self.x), 2) + pow((point.y - self.y), 2)) } }