377 lines
12 KiB
Swift
377 lines
12 KiB
Swift
//
|
||
// ZLBaseStickerView.swift
|
||
// ZLPhotoBrowser
|
||
//
|
||
// Created by long on 2022/11/28.
|
||
//
|
||
|
||
import UIKit
|
||
|
||
protocol ZLStickerViewDelegate: NSObject {
|
||
// Called when scale or rotate or move.
|
||
func stickerBeginOperation(_ sticker: UIView)
|
||
|
||
// Called during scale or rotate or move.
|
||
func stickerOnOperation(_ sticker: UIView, panGes: UIPanGestureRecognizer)
|
||
|
||
// Called after scale or rotate or move.
|
||
func stickerEndOperation(_ sticker: UIView, panGes: UIPanGestureRecognizer)
|
||
|
||
// Called when tap sticker.
|
||
func stickerDidTap(_ sticker: UIView)
|
||
|
||
func sticker(_ textSticker: ZLTextStickerView, editText text: String)
|
||
}
|
||
|
||
protocol ZLStickerViewAdditional: NSObject {
|
||
var gesIsEnabled: Bool { get set }
|
||
|
||
func resetState()
|
||
|
||
func moveToAshbin()
|
||
|
||
func addScale(_ scale: CGFloat)
|
||
}
|
||
|
||
class ZLBaseStickerView<T>: UIView, UIGestureRecognizerDelegate {
|
||
private enum Direction: Int {
|
||
case up = 0
|
||
case right = 90
|
||
case bottom = 180
|
||
case left = 270
|
||
}
|
||
|
||
var borderWidth = 1 / UIScreen.main.scale
|
||
|
||
var firstLayout = true
|
||
|
||
let originScale: CGFloat
|
||
|
||
let originAngle: CGFloat
|
||
|
||
var maxGesScale: CGFloat
|
||
|
||
var originTransform: CGAffineTransform = .identity
|
||
|
||
var timer: Timer?
|
||
|
||
var totalTranslationPoint: CGPoint = .zero
|
||
|
||
var gesTranslationPoint: CGPoint = .zero
|
||
|
||
var gesRotation: CGFloat = 0
|
||
|
||
var gesScale: CGFloat = 1
|
||
|
||
var onOperation = false
|
||
|
||
var gesIsEnabled = true
|
||
|
||
var originFrame: CGRect
|
||
|
||
lazy var tapGes = UITapGestureRecognizer(target: self, action: #selector(tapAction(_:)))
|
||
|
||
lazy var pinchGes: UIPinchGestureRecognizer = {
|
||
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(pinchAction(_:)))
|
||
pinch.delegate = self
|
||
return pinch
|
||
}()
|
||
|
||
lazy var panGes: UIPanGestureRecognizer = {
|
||
let pan = UIPanGestureRecognizer(target: self, action: #selector(panAction(_:)))
|
||
pan.delegate = self
|
||
return pan
|
||
}()
|
||
|
||
var state: T {
|
||
fatalError()
|
||
}
|
||
|
||
var borderView: UIView {
|
||
return self
|
||
}
|
||
|
||
weak var delegate: ZLStickerViewDelegate?
|
||
|
||
deinit {
|
||
cleanTimer()
|
||
}
|
||
|
||
init(
|
||
originScale: CGFloat,
|
||
originAngle: CGFloat,
|
||
originFrame: CGRect,
|
||
gesScale: CGFloat = 1,
|
||
gesRotation: CGFloat = 0,
|
||
totalTranslationPoint: CGPoint = .zero,
|
||
showBorder: Bool = true
|
||
) {
|
||
self.originScale = originScale
|
||
self.originAngle = originAngle
|
||
self.originFrame = originFrame
|
||
self.maxGesScale = 4 / originScale
|
||
super.init(frame: .zero)
|
||
|
||
self.gesScale = gesScale
|
||
self.gesRotation = gesRotation
|
||
self.totalTranslationPoint = totalTranslationPoint
|
||
|
||
borderView.layer.borderWidth = borderWidth
|
||
hideBorder()
|
||
if showBorder {
|
||
startTimer()
|
||
}
|
||
|
||
addGestureRecognizer(tapGes)
|
||
addGestureRecognizer(pinchGes)
|
||
|
||
let rotationGes = UIRotationGestureRecognizer(target: self, action: #selector(rotationAction(_:)))
|
||
rotationGes.delegate = self
|
||
addGestureRecognizer(rotationGes)
|
||
|
||
addGestureRecognizer(panGes)
|
||
tapGes.require(toFail: panGes)
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
|
||
guard firstLayout else {
|
||
return
|
||
}
|
||
|
||
// Rotate must be first when first layout.
|
||
transform = transform.rotated(by: originAngle.zl.toPi)
|
||
|
||
if totalTranslationPoint != .zero {
|
||
let direction = direction(for: originAngle)
|
||
if direction == .right {
|
||
transform = transform.translatedBy(x: totalTranslationPoint.y, y: -totalTranslationPoint.x)
|
||
} else if direction == .bottom {
|
||
transform = transform.translatedBy(x: -totalTranslationPoint.x, y: -totalTranslationPoint.y)
|
||
} else if direction == .left {
|
||
transform = transform.translatedBy(x: -totalTranslationPoint.y, y: totalTranslationPoint.x)
|
||
} else {
|
||
transform = transform.translatedBy(x: totalTranslationPoint.x, y: totalTranslationPoint.y)
|
||
}
|
||
}
|
||
|
||
transform = transform.scaledBy(x: originScale, y: originScale)
|
||
|
||
originTransform = transform
|
||
|
||
if gesScale != 1 {
|
||
transform = transform.scaledBy(x: gesScale, y: gesScale)
|
||
}
|
||
if gesRotation != 0 {
|
||
transform = transform.rotated(by: gesRotation)
|
||
}
|
||
|
||
firstLayout = false
|
||
setupUIFrameWhenFirstLayout()
|
||
}
|
||
|
||
func setupUIFrameWhenFirstLayout() {}
|
||
|
||
private func direction(for angle: CGFloat) -> ZLBaseStickerView.Direction {
|
||
// 将角度转换为0~360,并对360取余
|
||
let angle = ((Int(angle) % 360) + 360) % 360
|
||
return ZLBaseStickerView.Direction(rawValue: angle) ?? .up
|
||
}
|
||
|
||
@objc func tapAction(_ ges: UITapGestureRecognizer) {
|
||
guard gesIsEnabled else { return }
|
||
|
||
superview?.bringSubviewToFront(self)
|
||
delegate?.stickerDidTap(self)
|
||
startTimer()
|
||
}
|
||
|
||
@objc func pinchAction(_ ges: UIPinchGestureRecognizer) {
|
||
guard gesIsEnabled else { return }
|
||
|
||
let scale = min(maxGesScale, gesScale * ges.scale)
|
||
ges.scale = 1
|
||
|
||
guard scale != gesScale else {
|
||
return
|
||
}
|
||
|
||
gesScale = scale
|
||
|
||
if ges.state == .began {
|
||
setOperation(true)
|
||
} else if ges.state == .changed {
|
||
updateTransform()
|
||
} else if ges.state == .ended || ges.state == .cancelled {
|
||
setOperation(false)
|
||
}
|
||
}
|
||
|
||
@objc func rotationAction(_ ges: UIRotationGestureRecognizer) {
|
||
guard gesIsEnabled else { return }
|
||
|
||
gesRotation += ges.rotation
|
||
ges.rotation = 0
|
||
|
||
if ges.state == .began {
|
||
setOperation(true)
|
||
} else if ges.state == .changed {
|
||
updateTransform()
|
||
} else if ges.state == .ended || ges.state == .cancelled {
|
||
setOperation(false)
|
||
}
|
||
}
|
||
|
||
@objc func panAction(_ ges: UIPanGestureRecognizer) {
|
||
guard gesIsEnabled else { return }
|
||
|
||
let point = ges.translation(in: superview)
|
||
gesTranslationPoint = CGPoint(x: point.x / originScale, y: point.y / originScale)
|
||
|
||
if ges.state == .began {
|
||
setOperation(true)
|
||
} else if ges.state == .changed {
|
||
updateTransform()
|
||
} else if ges.state == .ended || ges.state == .cancelled {
|
||
totalTranslationPoint.x += point.x
|
||
totalTranslationPoint.y += point.y
|
||
setOperation(false)
|
||
let direction = direction(for: originAngle)
|
||
if direction == .right {
|
||
originTransform = originTransform.translatedBy(x: gesTranslationPoint.y, y: -gesTranslationPoint.x)
|
||
} else if direction == .bottom {
|
||
originTransform = originTransform.translatedBy(x: -gesTranslationPoint.x, y: -gesTranslationPoint.y)
|
||
} else if direction == .left {
|
||
originTransform = originTransform.translatedBy(x: -gesTranslationPoint.y, y: gesTranslationPoint.x)
|
||
} else {
|
||
originTransform = originTransform.translatedBy(x: gesTranslationPoint.x, y: gesTranslationPoint.y)
|
||
}
|
||
gesTranslationPoint = .zero
|
||
}
|
||
}
|
||
|
||
func setOperation(_ isOn: Bool) {
|
||
if isOn, !onOperation {
|
||
onOperation = true
|
||
cleanTimer()
|
||
borderView.layer.borderColor = UIColor.white.cgColor
|
||
superview?.bringSubviewToFront(self)
|
||
delegate?.stickerBeginOperation(self)
|
||
} else if !isOn, onOperation {
|
||
onOperation = false
|
||
startTimer()
|
||
delegate?.stickerEndOperation(self, panGes: panGes)
|
||
}
|
||
}
|
||
|
||
func updateTransform() {
|
||
var transform = originTransform
|
||
|
||
let direction = direction(for: originAngle)
|
||
if direction == .right {
|
||
transform = transform.translatedBy(x: gesTranslationPoint.y, y: -gesTranslationPoint.x)
|
||
} else if direction == .bottom {
|
||
transform = transform.translatedBy(x: -gesTranslationPoint.x, y: -gesTranslationPoint.y)
|
||
} else if direction == .left {
|
||
transform = transform.translatedBy(x: -gesTranslationPoint.y, y: gesTranslationPoint.x)
|
||
} else {
|
||
transform = transform.translatedBy(x: gesTranslationPoint.x, y: gesTranslationPoint.y)
|
||
}
|
||
// Scale must after translate.
|
||
transform = transform.scaledBy(x: gesScale, y: gesScale)
|
||
// Rotate must after scale.
|
||
transform = transform.rotated(by: gesRotation)
|
||
self.transform = transform
|
||
|
||
delegate?.stickerOnOperation(self, panGes: panGes)
|
||
}
|
||
|
||
@objc private func hideBorder() {
|
||
borderView.layer.borderColor = UIColor.clear.cgColor
|
||
}
|
||
|
||
func startTimer() {
|
||
cleanTimer()
|
||
borderView.layer.borderColor = UIColor.white.cgColor
|
||
timer = Timer.scheduledTimer(timeInterval: 2, target: ZLWeakProxy(target: self), selector: #selector(hideBorder), userInfo: nil, repeats: false)
|
||
RunLoop.current.add(timer!, forMode: .common)
|
||
}
|
||
|
||
private func cleanTimer() {
|
||
timer?.invalidate()
|
||
timer = nil
|
||
}
|
||
|
||
// MARK: UIGestureRecognizerDelegate
|
||
|
||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||
return true
|
||
}
|
||
}
|
||
|
||
extension ZLBaseStickerView: ZLStickerViewAdditional {
|
||
func resetState() {
|
||
onOperation = false
|
||
cleanTimer()
|
||
hideBorder()
|
||
}
|
||
|
||
func moveToAshbin() {
|
||
cleanTimer()
|
||
removeFromSuperview()
|
||
}
|
||
|
||
func addScale(_ scale: CGFloat) {
|
||
// Revert zoom scale.
|
||
transform = transform.scaledBy(x: 1 / originScale, y: 1 / originScale)
|
||
// Revert ges scale.
|
||
transform = transform.scaledBy(x: 1 / gesScale, y: 1 / gesScale)
|
||
// Revert ges rotation.
|
||
transform = transform.rotated(by: -gesRotation)
|
||
|
||
var origin = frame.origin
|
||
origin.x *= scale
|
||
origin.y *= scale
|
||
|
||
let newSize = CGSize(width: frame.width * scale, height: frame.height * scale)
|
||
let newOrigin = CGPoint(x: frame.minX + (frame.width - newSize.width) / 2, y: frame.minY + (frame.height - newSize.height) / 2)
|
||
let diffX: CGFloat = (origin.x - newOrigin.x)
|
||
let diffY: CGFloat = (origin.y - newOrigin.y)
|
||
|
||
let direction = direction(for: originScale)
|
||
if direction == .right {
|
||
transform = transform.translatedBy(x: diffY, y: -diffX)
|
||
originTransform = originTransform.translatedBy(x: diffY / originScale, y: -diffX / originScale)
|
||
} else if direction == .bottom {
|
||
transform = transform.translatedBy(x: -diffX, y: -diffY)
|
||
originTransform = originTransform.translatedBy(x: -diffX / originScale, y: -diffY / originScale)
|
||
} else if direction == .left {
|
||
transform = transform.translatedBy(x: -diffY, y: diffX)
|
||
originTransform = originTransform.translatedBy(x: -diffY / originScale, y: diffX / originScale)
|
||
} else {
|
||
transform = transform.translatedBy(x: diffX, y: diffY)
|
||
originTransform = originTransform.translatedBy(x: diffX / originScale, y: diffY / originScale)
|
||
}
|
||
totalTranslationPoint.x += diffX
|
||
totalTranslationPoint.y += diffY
|
||
|
||
transform = transform.scaledBy(x: scale, y: scale)
|
||
|
||
// Readd zoom scale.
|
||
transform = transform.scaledBy(x: originScale, y: originScale)
|
||
// Readd ges scale.
|
||
transform = transform.scaledBy(x: gesScale, y: gesScale)
|
||
// Readd ges rotation.
|
||
transform = transform.rotated(by: gesRotation)
|
||
|
||
gesScale *= scale
|
||
maxGesScale *= scale
|
||
}
|
||
}
|