initial
This commit is contained in:
199
Pods/ZLPhotoBrowser/Sources/Edit/ZLAdjustSlider.swift
generated
Normal file
199
Pods/ZLPhotoBrowser/Sources/Edit/ZLAdjustSlider.swift
generated
Normal file
@@ -0,0 +1,199 @@
|
||||
//
|
||||
// ZLAdjustSlider.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2021/12/17.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLAdjustSlider: UIView {
|
||||
static let maximumValue: Float = 1
|
||||
|
||||
static let minimumValue: Float = -1
|
||||
|
||||
let sliderWidth: CGFloat = 5
|
||||
|
||||
lazy var valueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFont.systemFont(ofSize: 12)
|
||||
label.layer.shadowColor = UIColor.black.withAlphaComponent(0.6).cgColor
|
||||
label.layer.shadowOffset = .zero
|
||||
label.layer.shadowOpacity = 1
|
||||
label.textColor = .white
|
||||
label.textAlignment = ZLPhotoUIConfiguration.default().adjustSliderType == .vertical ? .right : .center
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.6
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var separator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .zl.rgba(230, 230, 230)
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var shadowView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .zl.adjustSliderNormalColor
|
||||
view.layer.cornerRadius = sliderWidth / 2
|
||||
view.layer.shadowColor = UIColor.black.withAlphaComponent(0.4).cgColor
|
||||
view.layer.shadowOffset = .zero
|
||||
view.layer.shadowOpacity = 1
|
||||
view.layer.shadowRadius = 3
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var whiteView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .zl.adjustSliderNormalColor
|
||||
view.layer.cornerRadius = sliderWidth / 2
|
||||
view.layer.masksToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var tintView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .zl.adjustSliderTintColor
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var pan = UIPanGestureRecognizer(target: self, action: #selector(panAction(_:)))
|
||||
|
||||
private var impactFeedback: UIImpactFeedbackGenerator?
|
||||
|
||||
private var valueForPanBegan: Float = 0
|
||||
|
||||
var value: Float = 0 {
|
||||
didSet {
|
||||
valueLabel.text = String(Int(roundf(value * 100)))
|
||||
tintView.frame = calculateTintFrame()
|
||||
}
|
||||
}
|
||||
|
||||
private var isVertical = ZLPhotoUIConfiguration.default().adjustSliderType == .vertical
|
||||
|
||||
var beginAdjust: (() -> Void)?
|
||||
|
||||
var valueChanged: ((Float) -> Void)?
|
||||
|
||||
var endAdjust: (() -> Void)?
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLAdjustSlider deinit")
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupUI()
|
||||
|
||||
let editConfig = ZLPhotoConfiguration.default().editImageConfiguration
|
||||
if editConfig.impactFeedbackWhenAdjustSliderValueIsZero {
|
||||
impactFeedback = UIImpactFeedbackGenerator(style: editConfig.impactFeedbackStyle)
|
||||
}
|
||||
|
||||
addGestureRecognizer(pan)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
if isVertical {
|
||||
shadowView.frame = CGRect(x: 40, y: 0, width: sliderWidth, height: bounds.height)
|
||||
whiteView.frame = shadowView.frame
|
||||
tintView.frame = calculateTintFrame()
|
||||
let separatorH: CGFloat = 1
|
||||
separator.frame = CGRect(x: 0, y: (bounds.height - separatorH) / 2, width: sliderWidth, height: separatorH)
|
||||
valueLabel.frame = CGRect(x: 0, y: bounds.height / 2 - 10, width: 38, height: 20)
|
||||
} else {
|
||||
valueLabel.frame = CGRect(x: 0, y: 0, width: zl.width, height: 38)
|
||||
shadowView.frame = CGRect(x: 0, y: valueLabel.zl.bottom + 2, width: zl.width, height: sliderWidth)
|
||||
whiteView.frame = shadowView.frame
|
||||
tintView.frame = calculateTintFrame()
|
||||
let separatorW: CGFloat = 1
|
||||
separator.frame = CGRect(x: (zl.width - separatorW) / 2, y: 0, width: separatorW, height: sliderWidth)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(shadowView)
|
||||
addSubview(whiteView)
|
||||
whiteView.addSubview(tintView)
|
||||
whiteView.addSubview(separator)
|
||||
addSubview(valueLabel)
|
||||
}
|
||||
|
||||
private func calculateTintFrame() -> CGRect {
|
||||
if isVertical {
|
||||
let totalH = zl.height / 2
|
||||
let tintH = totalH * abs(CGFloat(value)) / CGFloat(ZLAdjustSlider.maximumValue)
|
||||
if value > 0 {
|
||||
return CGRect(x: 0, y: totalH - tintH, width: sliderWidth, height: tintH)
|
||||
} else {
|
||||
return CGRect(x: 0, y: totalH, width: sliderWidth, height: tintH)
|
||||
}
|
||||
} else {
|
||||
let totalW = zl.width / 2
|
||||
let tintW = totalW * abs(CGFloat(value)) / CGFloat(ZLAdjustSlider.maximumValue)
|
||||
if value > 0 {
|
||||
return CGRect(x: totalW, y: 0, width: tintW, height: sliderWidth)
|
||||
} else {
|
||||
return CGRect(x: totalW - tintW, y: 0, width: tintW, height: sliderWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func panAction(_ pan: UIPanGestureRecognizer) {
|
||||
let translation = pan.translation(in: self)
|
||||
|
||||
if pan.state == .began {
|
||||
valueForPanBegan = value
|
||||
beginAdjust?()
|
||||
impactFeedback?.prepare()
|
||||
} else if pan.state == .changed {
|
||||
let transValue = isVertical ? -translation.y : translation.x
|
||||
let totalLength = isVertical ? zl.height / 2 : zl.width / 2
|
||||
var temp = valueForPanBegan + Float(transValue / totalLength)
|
||||
temp = max(ZLAdjustSlider.minimumValue, min(ZLAdjustSlider.maximumValue, temp))
|
||||
|
||||
if (-0.0049..<0.005) ~= temp {
|
||||
temp = 0
|
||||
}
|
||||
|
||||
guard value != temp else { return }
|
||||
|
||||
value = temp
|
||||
valueChanged?(value)
|
||||
|
||||
guard #available(iOS 10.0, *) else { return }
|
||||
if value == 0 {
|
||||
impactFeedback?.impactOccurred()
|
||||
}
|
||||
} else {
|
||||
valueForPanBegan = value
|
||||
endAdjust?()
|
||||
}
|
||||
}
|
||||
}
|
||||
376
Pods/ZLPhotoBrowser/Sources/Edit/ZLBaseStickerView.swift
generated
Normal file
376
Pods/ZLPhotoBrowser/Sources/Edit/ZLBaseStickerView.swift
generated
Normal file
@@ -0,0 +1,376 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
1351
Pods/ZLPhotoBrowser/Sources/Edit/ZLClipImageViewController.swift
generated
Normal file
1351
Pods/ZLPhotoBrowser/Sources/Edit/ZLClipImageViewController.swift
generated
Normal file
File diff suppressed because it is too large
Load Diff
1818
Pods/ZLPhotoBrowser/Sources/Edit/ZLEditImageViewController.swift
generated
Normal file
1818
Pods/ZLPhotoBrowser/Sources/Edit/ZLEditImageViewController.swift
generated
Normal file
File diff suppressed because it is too large
Load Diff
223
Pods/ZLPhotoBrowser/Sources/Edit/ZLEditToolCells.swift
generated
Normal file
223
Pods/ZLPhotoBrowser/Sources/Edit/ZLEditToolCells.swift
generated
Normal file
@@ -0,0 +1,223 @@
|
||||
//
|
||||
// ZLEditToolCells.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2021/12/16.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
// MARK: Edit tool cell
|
||||
|
||||
class ZLEditToolCell: UICollectionViewCell {
|
||||
var toolType: ZLEditImageConfiguration.EditTool = .draw {
|
||||
didSet {
|
||||
switch toolType {
|
||||
case .draw:
|
||||
icon.image = .zl.getImage("zl_drawLine")
|
||||
icon.highlightedImage = .zl.getImage("zl_drawLine_selected")
|
||||
case .clip:
|
||||
icon.image = .zl.getImage("zl_clip")
|
||||
icon.highlightedImage = .zl.getImage("zl_clip")
|
||||
case .imageSticker:
|
||||
icon.image = .zl.getImage("zl_imageSticker")
|
||||
icon.highlightedImage = .zl.getImage("zl_imageSticker")
|
||||
case .textSticker:
|
||||
icon.image = .zl.getImage("zl_textSticker")
|
||||
icon.highlightedImage = .zl.getImage("zl_textSticker")
|
||||
case .mosaic:
|
||||
icon.image = .zl.getImage("zl_mosaic")
|
||||
icon.highlightedImage = .zl.getImage("zl_mosaic_selected")
|
||||
case .filter:
|
||||
icon.image = .zl.getImage("zl_filter")
|
||||
icon.highlightedImage = .zl.getImage("zl_filter_selected")
|
||||
case .adjust:
|
||||
icon.image = .zl.getImage("zl_adjust")
|
||||
icon.highlightedImage = .zl.getImage("zl_adjust_selected")
|
||||
}
|
||||
if let color = UIColor.zl.imageEditorToolIconTintColor {
|
||||
icon.highlightedImage = icon.highlightedImage?
|
||||
.zl.fillColor(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy var icon = UIImageView(frame: contentView.bounds)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
contentView.addSubview(icon)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: draw color cell
|
||||
|
||||
class ZLDrawColorCell: UICollectionViewCell {
|
||||
lazy var colorView: UIView = {
|
||||
let view = UIView()
|
||||
view.layer.cornerRadius = 10
|
||||
view.layer.masksToBounds = true
|
||||
view.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var bgWhiteView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white
|
||||
view.layer.cornerRadius = 12
|
||||
view.layer.masksToBounds = true
|
||||
view.frame = CGRect(x: 0, y: 0, width: 24, height: 24)
|
||||
return view
|
||||
}()
|
||||
|
||||
var color: UIColor = .clear {
|
||||
didSet {
|
||||
colorView.backgroundColor = color
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
contentView.addSubview(bgWhiteView)
|
||||
contentView.addSubview(colorView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
colorView.center = contentView.center
|
||||
bgWhiteView.center = contentView.center
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: filter cell
|
||||
|
||||
class ZLFilterImageCell: UICollectionViewCell {
|
||||
lazy var nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.frame = CGRect(x: 0, y: bounds.height - 20, width: bounds.width, height: 20)
|
||||
label.font = .zl.font(ofSize: 12)
|
||||
label.textColor = .white
|
||||
label.textAlignment = .center
|
||||
label.layer.shadowColor = UIColor.black.withAlphaComponent(0.3).cgColor
|
||||
label.layer.shadowOffset = .zero
|
||||
label.layer.shadowOpacity = 1
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.5
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.width)
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
contentView.addSubview(nameLabel)
|
||||
contentView.addSubview(imageView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: adjust tool cell
|
||||
|
||||
class ZLAdjustToolCell: UICollectionViewCell {
|
||||
lazy var nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.frame = CGRect(x: 0, y: bounds.height - 30, width: bounds.width, height: 30)
|
||||
label.font = .zl.font(ofSize: 12)
|
||||
label.textColor = .white
|
||||
label.textAlignment = .center
|
||||
label.numberOfLines = 2
|
||||
label.lineBreakMode = .byCharWrapping
|
||||
label.layer.shadowColor = UIColor.black.withAlphaComponent(0.3).cgColor
|
||||
label.layer.shadowOffset = .zero
|
||||
label.layer.shadowOpacity = 1
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.5
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.frame = CGRect(x: (bounds.width - 30) / 2, y: 0, width: 30, height: 30)
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
var adjustTool: ZLEditImageConfiguration.AdjustTool = .brightness {
|
||||
didSet {
|
||||
switch adjustTool {
|
||||
case .brightness:
|
||||
imageView.image = .zl.getImage("zl_brightness")
|
||||
imageView.highlightedImage = .zl.getImage("zl_brightness_selected")
|
||||
nameLabel.text = localLanguageTextValue(.brightness)
|
||||
case .contrast:
|
||||
imageView.image = .zl.getImage("zl_contrast")
|
||||
imageView.highlightedImage = .zl.getImage("zl_contrast_selected")
|
||||
nameLabel.text = localLanguageTextValue(.contrast)
|
||||
case .saturation:
|
||||
imageView.image = .zl.getImage("zl_saturation")
|
||||
imageView.highlightedImage = .zl.getImage("zl_saturation_selected")
|
||||
nameLabel.text = localLanguageTextValue(.saturation)
|
||||
}
|
||||
if let color = UIColor.zl.imageEditorToolIconTintColor {
|
||||
imageView.highlightedImage = imageView.highlightedImage?
|
||||
.zl.fillColor(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
contentView.addSubview(nameLabel)
|
||||
contentView.addSubview(imageView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
679
Pods/ZLPhotoBrowser/Sources/Edit/ZLEditVideoViewController.swift
generated
Normal file
679
Pods/ZLPhotoBrowser/Sources/Edit/ZLEditVideoViewController.swift
generated
Normal file
@@ -0,0 +1,679 @@
|
||||
//
|
||||
// ZLEditVideoViewController.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/8/30.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
public class ZLEditVideoViewController: UIViewController {
|
||||
private static let frameImageSize = CGSize(width: CGFloat(round(50.0 * 2.0 / 3.0)), height: 50.0)
|
||||
|
||||
private let avAsset: AVAsset
|
||||
|
||||
private let animateDismiss: Bool
|
||||
|
||||
private lazy var cancelBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle(localLanguageTextValue(.cancel), for: .normal)
|
||||
btn.setTitleColor(.zl.bottomToolViewBtnNormalTitleColor, for: .normal)
|
||||
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
|
||||
btn.addTarget(self, action: #selector(cancelBtnClick), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
private lazy var doneBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle(localLanguageTextValue(.done), for: .normal)
|
||||
btn.setTitleColor(.zl.bottomToolViewBtnNormalTitleColor, for: .normal)
|
||||
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
|
||||
btn.addTarget(self, action: #selector(doneBtnClick), for: .touchUpInside)
|
||||
btn.backgroundColor = .zl.bottomToolViewBtnNormalBgColor
|
||||
btn.layer.masksToBounds = true
|
||||
btn.layer.cornerRadius = ZLLayout.bottomToolBtnCornerRadius
|
||||
return btn
|
||||
}()
|
||||
|
||||
private var timer: Timer?
|
||||
|
||||
private lazy var playerLayer: AVPlayerLayer = {
|
||||
let layer = AVPlayerLayer()
|
||||
layer.videoGravity = .resizeAspect
|
||||
return layer
|
||||
}()
|
||||
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
let layout = ZLCollectionViewFlowLayout()
|
||||
layout.itemSize = ZLEditVideoViewController.frameImageSize
|
||||
layout.minimumLineSpacing = 0
|
||||
layout.minimumInteritemSpacing = 0
|
||||
layout.scrollDirection = .horizontal
|
||||
|
||||
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
view.backgroundColor = .clear
|
||||
view.delegate = self
|
||||
view.dataSource = self
|
||||
view.showsHorizontalScrollIndicator = false
|
||||
ZLEditVideoFrameImageCell.zl.register(view)
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var frameImageBorderView: ZLEditVideoFrameImageBorderView = {
|
||||
let view = ZLEditVideoFrameImageBorderView()
|
||||
view.isUserInteractionEnabled = false
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var leftSideView: UIImageView = {
|
||||
let view = UIImageView(image: .zl.getImage("zl_ic_left"))
|
||||
view.isUserInteractionEnabled = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var rightSideView: UIImageView = {
|
||||
let view = UIImageView(image: .zl.getImage("zl_ic_right"))
|
||||
view.isUserInteractionEnabled = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var leftSidePan: UIPanGestureRecognizer = {
|
||||
let pan = UIPanGestureRecognizer(target: self, action: #selector(leftSidePanAction(_:)))
|
||||
pan.delegate = self
|
||||
return pan
|
||||
}()
|
||||
|
||||
private lazy var rightSidePan: UIPanGestureRecognizer = {
|
||||
let pan = UIPanGestureRecognizer(target: self, action: #selector(rightSidePanAction(_:)))
|
||||
pan.delegate = self
|
||||
return pan
|
||||
}()
|
||||
|
||||
private lazy var indicator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.white.withAlphaComponent(0.7)
|
||||
return view
|
||||
}()
|
||||
|
||||
private var measureCount = 0
|
||||
|
||||
private lazy var interval: TimeInterval = {
|
||||
let assetDuration = round(self.avAsset.duration.seconds)
|
||||
return min(assetDuration, TimeInterval(ZLPhotoConfiguration.default().maxEditVideoTime)) / 10
|
||||
}()
|
||||
|
||||
private lazy var requestFrameImageQueue: OperationQueue = {
|
||||
let queue = OperationQueue()
|
||||
queue.maxConcurrentOperationCount = 10
|
||||
return queue
|
||||
}()
|
||||
|
||||
private lazy var avAssetRequestID = PHInvalidImageRequestID
|
||||
|
||||
private lazy var videoRequestID = PHInvalidImageRequestID
|
||||
|
||||
private var frameImageCache: [Int: UIImage] = [:]
|
||||
|
||||
private var requestFailedFrameImageIndex: [Int] = []
|
||||
|
||||
private var shouldLayout = true
|
||||
|
||||
private lazy var generator: AVAssetImageGenerator = {
|
||||
let g = AVAssetImageGenerator(asset: self.avAsset)
|
||||
g.maximumSize = CGSize(width: ZLEditVideoViewController.frameImageSize.width * 3, height: ZLEditVideoViewController.frameImageSize.height * 3)
|
||||
g.appliesPreferredTrackTransform = true
|
||||
g.requestedTimeToleranceBefore = .zero
|
||||
g.requestedTimeToleranceAfter = .zero
|
||||
g.apertureMode = .productionAperture
|
||||
return g
|
||||
}()
|
||||
|
||||
@objc public var editFinishBlock: ((URL?) -> Void)?
|
||||
|
||||
override public var prefersStatusBarHidden: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLEditVideoViewController deinit")
|
||||
cleanTimer()
|
||||
requestFrameImageQueue.cancelAllOperations()
|
||||
if avAssetRequestID > PHInvalidImageRequestID {
|
||||
PHImageManager.default().cancelImageRequest(avAssetRequestID)
|
||||
}
|
||||
if videoRequestID > PHInvalidImageRequestID {
|
||||
PHImageManager.default().cancelImageRequest(videoRequestID)
|
||||
}
|
||||
}
|
||||
|
||||
/// initialize
|
||||
/// - Parameters:
|
||||
/// - avAsset: AVAsset对象,需要传入本地视频,网络视频不支持
|
||||
/// - animateDismiss: 退出界面时是否显示dismiss动画
|
||||
@objc public init(avAsset: AVAsset, animateDismiss: Bool = false) {
|
||||
self.avAsset = avAsset
|
||||
self.animateDismiss = animateDismiss
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupUI()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
analysisAssetImages()
|
||||
}
|
||||
|
||||
override public func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
guard shouldLayout else {
|
||||
return
|
||||
}
|
||||
shouldLayout = false
|
||||
|
||||
zl_debugPrint("edit video layout subviews")
|
||||
var insets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
|
||||
if #available(iOS 11.0, *) {
|
||||
insets = self.view.safeAreaInsets
|
||||
}
|
||||
|
||||
let btnH = ZLLayout.bottomToolBtnH
|
||||
let bottomBtnAndColSpacing: CGFloat = 20
|
||||
let playerLayerY = insets.top + 20
|
||||
let diffBottom = btnH + ZLEditVideoViewController.frameImageSize.height + bottomBtnAndColSpacing + insets.bottom + 30
|
||||
|
||||
playerLayer.frame = CGRect(x: 15, y: insets.top + 20, width: view.bounds.width - 30, height: view.bounds.height - playerLayerY - diffBottom)
|
||||
|
||||
let cancelBtnW = localLanguageTextValue(.cancel).zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: btnH)).width
|
||||
cancelBtn.frame = CGRect(x: 20, y: view.bounds.height - insets.bottom - btnH, width: cancelBtnW, height: btnH)
|
||||
let doneBtnW = localLanguageTextValue(.done).zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: btnH)).width + 20
|
||||
doneBtn.frame = CGRect(x: view.bounds.width - doneBtnW - 20, y: view.bounds.height - insets.bottom - btnH, width: doneBtnW, height: btnH)
|
||||
|
||||
collectionView.frame = CGRect(x: 0, y: doneBtn.frame.minY - bottomBtnAndColSpacing - ZLEditVideoViewController.frameImageSize.height, width: view.bounds.width, height: ZLEditVideoViewController.frameImageSize.height)
|
||||
|
||||
let frameViewW = ZLEditVideoViewController.frameImageSize.width * 10
|
||||
frameImageBorderView.frame = CGRect(x: (view.bounds.width - frameViewW) / 2, y: collectionView.frame.minY, width: frameViewW, height: ZLEditVideoViewController.frameImageSize.height)
|
||||
// 左右拖动view
|
||||
let leftRightSideViewW = ZLEditVideoViewController.frameImageSize.width / 2
|
||||
leftSideView.frame = CGRect(x: frameImageBorderView.frame.minX, y: collectionView.frame.minY, width: leftRightSideViewW, height: ZLEditVideoViewController.frameImageSize.height)
|
||||
let rightSideViewX = view.bounds.width - frameImageBorderView.frame.minX - leftRightSideViewW
|
||||
rightSideView.frame = CGRect(x: rightSideViewX, y: collectionView.frame.minY, width: leftRightSideViewW, height: ZLEditVideoViewController.frameImageSize.height)
|
||||
|
||||
frameImageBorderView.validRect = frameImageBorderView.convert(clipRect(), from: view)
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
view.backgroundColor = .black
|
||||
|
||||
view.layer.addSublayer(playerLayer)
|
||||
view.addSubview(collectionView)
|
||||
view.addSubview(frameImageBorderView)
|
||||
view.addSubview(indicator)
|
||||
view.addSubview(leftSideView)
|
||||
view.addSubview(rightSideView)
|
||||
|
||||
view.addGestureRecognizer(leftSidePan)
|
||||
view.addGestureRecognizer(rightSidePan)
|
||||
|
||||
collectionView.panGestureRecognizer.require(toFail: leftSidePan)
|
||||
collectionView.panGestureRecognizer.require(toFail: rightSidePan)
|
||||
rightSidePan.require(toFail: leftSidePan)
|
||||
|
||||
view.addSubview(cancelBtn)
|
||||
view.addSubview(doneBtn)
|
||||
}
|
||||
|
||||
@objc private func cancelBtnClick() {
|
||||
dismiss(animated: animateDismiss, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func doneBtnClick() {
|
||||
cleanTimer()
|
||||
|
||||
let d = CGFloat(interval) * clipRect().width / ZLEditVideoViewController.frameImageSize.width
|
||||
if ZLPhotoConfiguration.Second(round(d)) < ZLPhotoConfiguration.default().minSelectVideoDuration {
|
||||
let message = String(format: localLanguageTextValue(.shorterThanMinVideoDuration), ZLPhotoConfiguration.default().minSelectVideoDuration)
|
||||
showAlertView(message, self)
|
||||
return
|
||||
}
|
||||
if ZLPhotoConfiguration.Second(round(d)) > ZLPhotoConfiguration.default().maxSelectVideoDuration {
|
||||
let message = String(format: localLanguageTextValue(.longerThanMaxVideoDuration), ZLPhotoConfiguration.default().maxSelectVideoDuration)
|
||||
showAlertView(message, self)
|
||||
return
|
||||
}
|
||||
|
||||
// Max deviation is 0.01
|
||||
if abs(d - round(CGFloat(avAsset.duration.seconds))) <= 0.01 {
|
||||
dismiss(animated: animateDismiss) {
|
||||
self.editFinishBlock?(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let hud = ZLProgressHUD.show()
|
||||
|
||||
ZLVideoManager.exportEditVideo(for: avAsset, range: getTimeRange()) { [weak self] url, error in
|
||||
hud.hide()
|
||||
if let er = error {
|
||||
showAlertView(er.localizedDescription, self)
|
||||
} else if url != nil {
|
||||
self?.dismiss(animated: self?.animateDismiss ?? false) {
|
||||
self?.editFinishBlock?(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func leftSidePanAction(_ pan: UIPanGestureRecognizer) {
|
||||
let point = pan.location(in: view)
|
||||
|
||||
if pan.state == .began {
|
||||
frameImageBorderView.layer.borderColor = UIColor(white: 1, alpha: 0.4).cgColor
|
||||
cleanTimer()
|
||||
} else if pan.state == .changed {
|
||||
let minX = frameImageBorderView.frame.minX
|
||||
let maxX = rightSideView.frame.minX - leftSideView.frame.width
|
||||
|
||||
var frame = leftSideView.frame
|
||||
frame.origin.x = min(maxX, max(minX, point.x))
|
||||
leftSideView.frame = frame
|
||||
frameImageBorderView.validRect = frameImageBorderView.convert(clipRect(), from: view)
|
||||
|
||||
playerLayer.player?.seek(to: getStartTime(), toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
} else if pan.state == .ended || pan.state == .cancelled {
|
||||
frameImageBorderView.layer.borderColor = UIColor.clear.cgColor
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func rightSidePanAction(_ pan: UIPanGestureRecognizer) {
|
||||
let point = pan.location(in: view)
|
||||
|
||||
if pan.state == .began {
|
||||
frameImageBorderView.layer.borderColor = UIColor(white: 1, alpha: 0.4).cgColor
|
||||
cleanTimer()
|
||||
} else if pan.state == .changed {
|
||||
let minX = leftSideView.frame.maxX
|
||||
let maxX = frameImageBorderView.frame.maxX - rightSideView.frame.width
|
||||
|
||||
var frame = rightSideView.frame
|
||||
frame.origin.x = min(maxX, max(minX, point.x))
|
||||
rightSideView.frame = frame
|
||||
frameImageBorderView.validRect = frameImageBorderView.convert(clipRect(), from: view)
|
||||
|
||||
playerLayer.player?.seek(to: getStartTime(), toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
} else if pan.state == .ended || pan.state == .cancelled {
|
||||
frameImageBorderView.layer.borderColor = UIColor.clear.cgColor
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func appWillResignActive() {
|
||||
cleanTimer()
|
||||
indicator.layer.removeAllAnimations()
|
||||
}
|
||||
|
||||
@objc private func appDidBecomeActive() {
|
||||
startTimer()
|
||||
}
|
||||
|
||||
private func analysisAssetImages() {
|
||||
let duration = round(avAsset.duration.seconds)
|
||||
guard duration > 0 else {
|
||||
showFetchFailedAlert()
|
||||
return
|
||||
}
|
||||
let item = AVPlayerItem(asset: avAsset)
|
||||
let player = AVPlayer(playerItem: item)
|
||||
playerLayer.player = player
|
||||
startTimer()
|
||||
|
||||
measureCount = Int(duration / interval)
|
||||
collectionView.reloadData()
|
||||
requestVideoMeasureFrameImage()
|
||||
}
|
||||
|
||||
private func requestVideoMeasureFrameImage() {
|
||||
for i in 0..<measureCount {
|
||||
let mes = TimeInterval(i) * interval
|
||||
let time = CMTimeMakeWithSeconds(Float64(mes), preferredTimescale: avAsset.duration.timescale)
|
||||
|
||||
let operation = ZLEditVideoFetchFrameImageOperation(generator: generator, time: time) { [weak self] image, _ in
|
||||
self?.frameImageCache[Int(i)] = image
|
||||
let cell = self?.collectionView.cellForItem(at: IndexPath(row: Int(i), section: 0)) as? ZLEditVideoFrameImageCell
|
||||
cell?.imageView.image = image
|
||||
if image == nil {
|
||||
self?.requestFailedFrameImageIndex.append(i)
|
||||
}
|
||||
}
|
||||
requestFrameImageQueue.addOperation(operation)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func playPartVideo() {
|
||||
playerLayer.player?.seek(to: getStartTime(), toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
if (playerLayer.player?.rate ?? 0) == 0 {
|
||||
playerLayer.player?.play()
|
||||
}
|
||||
}
|
||||
|
||||
private func startTimer() {
|
||||
cleanTimer()
|
||||
let duration = interval * TimeInterval(clipRect().width / ZLEditVideoViewController.frameImageSize.width)
|
||||
|
||||
timer = Timer.scheduledTimer(timeInterval: duration, target: ZLWeakProxy(target: self), selector: #selector(playPartVideo), userInfo: nil, repeats: true)
|
||||
timer?.fire()
|
||||
RunLoop.main.add(timer!, forMode: .common)
|
||||
|
||||
indicator.isHidden = false
|
||||
|
||||
|
||||
let indicatorW: CGFloat = 2
|
||||
let indicatorH = leftSideView.zl.height
|
||||
let indicatorY = leftSideView.zl.top
|
||||
var indicatorFromX = leftSideView.zl.left
|
||||
var indicatorToX = rightSideView.zl.right - indicatorW
|
||||
|
||||
if isRTL() {
|
||||
swap(&indicatorFromX, &indicatorToX)
|
||||
}
|
||||
|
||||
let fromFrame = CGRect(x: indicatorFromX, y: indicatorY, width: indicatorW, height: indicatorH)
|
||||
indicator.frame = fromFrame
|
||||
|
||||
var toFrame = fromFrame
|
||||
toFrame.origin.x = indicatorToX
|
||||
|
||||
indicator.layer.removeAllAnimations()
|
||||
UIView.animate(withDuration: duration, delay: 0, options: [.allowUserInteraction, .curveLinear, .repeat], animations: {
|
||||
self.indicator.frame = toFrame
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
private func cleanTimer() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
indicator.layer.removeAllAnimations()
|
||||
indicator.isHidden = true
|
||||
playerLayer.player?.pause()
|
||||
}
|
||||
|
||||
private func getStartTime() -> CMTime {
|
||||
var rect = collectionView.convert(clipRect(), from: view)
|
||||
rect.origin.x -= frameImageBorderView.frame.minX
|
||||
let second = max(0, CGFloat(interval) * rect.minX / ZLEditVideoViewController.frameImageSize.width)
|
||||
return CMTimeMakeWithSeconds(Float64(second), preferredTimescale: avAsset.duration.timescale)
|
||||
}
|
||||
|
||||
private func getTimeRange() -> CMTimeRange {
|
||||
let start = getStartTime()
|
||||
let d = CGFloat(interval) * clipRect().width / ZLEditVideoViewController.frameImageSize.width
|
||||
let duration = CMTimeMakeWithSeconds(Float64(d), preferredTimescale: avAsset.duration.timescale)
|
||||
return CMTimeRangeMake(start: start, duration: duration)
|
||||
}
|
||||
|
||||
private func clipRect() -> CGRect {
|
||||
var frame = CGRect.zero
|
||||
frame.origin.x = leftSideView.frame.minX
|
||||
frame.origin.y = leftSideView.frame.minY
|
||||
frame.size.width = rightSideView.frame.maxX - frame.minX
|
||||
frame.size.height = leftSideView.frame.height
|
||||
return frame
|
||||
}
|
||||
|
||||
private func showFetchFailedAlert() {
|
||||
let action = ZLCustomAlertAction(title: localLanguageTextValue(.ok), style: .default) { [weak self] _ in
|
||||
self?.dismiss(animated: false)
|
||||
}
|
||||
showAlertController(title: nil, message: localLanguageTextValue(.iCloudVideoLoadFaild), style: .alert, actions: [action], sender: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLEditVideoViewController: UIGestureRecognizerDelegate {
|
||||
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer == leftSidePan {
|
||||
let point = gestureRecognizer.location(in: view)
|
||||
let frame = leftSideView.frame
|
||||
let outerFrame = frame.inset(by: UIEdgeInsets(top: -20, left: -40, bottom: -20, right: -20))
|
||||
return outerFrame.contains(point)
|
||||
} else if gestureRecognizer == rightSidePan {
|
||||
let point = gestureRecognizer.location(in: view)
|
||||
let frame = rightSideView.frame
|
||||
let outerFrame = frame.inset(by: UIEdgeInsets(top: -20, left: -20, bottom: -20, right: -40))
|
||||
return outerFrame.contains(point)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLEditVideoViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
cleanTimer()
|
||||
playerLayer.player?.seek(to: getStartTime(), toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
if !decelerate {
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
startTimer()
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||
let w = ZLEditVideoViewController.frameImageSize.width * 10
|
||||
let leftRight = (collectionView.frame.width - w) / 2
|
||||
return UIEdgeInsets(top: 0, left: leftRight, bottom: 0, right: leftRight)
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return measureCount
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLEditVideoFrameImageCell.zl.identifier, for: indexPath) as! ZLEditVideoFrameImageCell
|
||||
|
||||
if let image = frameImageCache[indexPath.row] {
|
||||
cell.imageView.image = image
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
if requestFailedFrameImageIndex.contains(indexPath.row) {
|
||||
let mes = TimeInterval(indexPath.row) * interval
|
||||
let time = CMTimeMakeWithSeconds(Float64(mes), preferredTimescale: avAsset.duration.timescale)
|
||||
|
||||
let operation = ZLEditVideoFetchFrameImageOperation(generator: generator, time: time) { [weak self] image, _ in
|
||||
self?.frameImageCache[indexPath.row] = image
|
||||
let cell = self?.collectionView.cellForItem(at: IndexPath(row: indexPath.row, section: 0)) as? ZLEditVideoFrameImageCell
|
||||
cell?.imageView.image = image
|
||||
if image != nil {
|
||||
self?.requestFailedFrameImageIndex.removeAll { $0 == indexPath.row }
|
||||
}
|
||||
}
|
||||
requestFrameImageQueue.addOperation(operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZLEditVideoFrameImageBorderView: UIView {
|
||||
var validRect: CGRect = .zero {
|
||||
didSet {
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
layer.borderWidth = 2
|
||||
layer.borderColor = UIColor.clear.cgColor
|
||||
backgroundColor = .clear
|
||||
isOpaque = false
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
context?.setStrokeColor(UIColor.white.cgColor)
|
||||
context?.setLineWidth(4)
|
||||
|
||||
context?.move(to: CGPoint(x: validRect.minX, y: 0))
|
||||
context?.addLine(to: CGPoint(x: validRect.minX + validRect.width, y: 0))
|
||||
|
||||
context?.move(to: CGPoint(x: validRect.minX, y: rect.height))
|
||||
context?.addLine(to: CGPoint(x: validRect.minX + validRect.width, y: rect.height))
|
||||
|
||||
context?.strokePath()
|
||||
}
|
||||
}
|
||||
|
||||
class ZLEditVideoFrameImageCell: UICollectionViewCell {
|
||||
lazy var imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
contentView.addSubview(imageView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
imageView.frame = bounds
|
||||
}
|
||||
}
|
||||
|
||||
class ZLEditVideoFetchFrameImageOperation: Operation {
|
||||
private let generator: AVAssetImageGenerator
|
||||
|
||||
private let time: CMTime
|
||||
|
||||
let completion: (UIImage?, CMTime) -> Void
|
||||
|
||||
var pri_isExecuting = false {
|
||||
willSet {
|
||||
self.willChangeValue(forKey: "isExecuting")
|
||||
}
|
||||
didSet {
|
||||
self.didChangeValue(forKey: "isExecuting")
|
||||
}
|
||||
}
|
||||
|
||||
override var isExecuting: Bool {
|
||||
return pri_isExecuting
|
||||
}
|
||||
|
||||
var pri_isFinished = false {
|
||||
willSet {
|
||||
self.willChangeValue(forKey: "isFinished")
|
||||
}
|
||||
didSet {
|
||||
self.didChangeValue(forKey: "isFinished")
|
||||
}
|
||||
}
|
||||
|
||||
override var isFinished: Bool {
|
||||
return pri_isFinished
|
||||
}
|
||||
|
||||
var pri_isCancelled = false {
|
||||
willSet {
|
||||
self.willChangeValue(forKey: "isCancelled")
|
||||
}
|
||||
didSet {
|
||||
self.didChangeValue(forKey: "isCancelled")
|
||||
}
|
||||
}
|
||||
|
||||
override var isCancelled: Bool {
|
||||
return pri_isCancelled
|
||||
}
|
||||
|
||||
init(generator: AVAssetImageGenerator, time: CMTime, completion: @escaping ((UIImage?, CMTime) -> Void)) {
|
||||
self.generator = generator
|
||||
self.time = time
|
||||
self.completion = completion
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func start() {
|
||||
if isCancelled {
|
||||
fetchFinish()
|
||||
return
|
||||
}
|
||||
pri_isExecuting = true
|
||||
generator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) { _, cgImage, _, result, _ in
|
||||
if result == .succeeded, let cg = cgImage {
|
||||
let image = UIImage(cgImage: cg)
|
||||
ZLMainAsync {
|
||||
self.completion(image, self.time)
|
||||
}
|
||||
self.fetchFinish()
|
||||
} else {
|
||||
self.fetchFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
super.cancel()
|
||||
pri_isCancelled = true
|
||||
}
|
||||
|
||||
private func fetchFinish() {
|
||||
pri_isExecuting = false
|
||||
pri_isFinished = true
|
||||
}
|
||||
}
|
||||
281
Pods/ZLPhotoBrowser/Sources/Edit/ZLFilter.swift
generated
Normal file
281
Pods/ZLPhotoBrowser/Sources/Edit/ZLFilter.swift
generated
Normal file
@@ -0,0 +1,281 @@
|
||||
//
|
||||
// ZLFilter.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/10/9.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Filter code reference from https://github.com/Yummypets/YPImagePicker
|
||||
|
||||
public typealias ZLFilterApplierType = (_ image: UIImage) -> UIImage
|
||||
|
||||
@objc public enum ZLFilterType: Int {
|
||||
case normal
|
||||
case chrome
|
||||
case fade
|
||||
case instant
|
||||
case process
|
||||
case transfer
|
||||
case tone
|
||||
case linear
|
||||
case sepia
|
||||
case mono
|
||||
case noir
|
||||
case tonal
|
||||
|
||||
var coreImageFilterName: String {
|
||||
switch self {
|
||||
case .normal:
|
||||
return ""
|
||||
case .chrome:
|
||||
return "CIPhotoEffectChrome"
|
||||
case .fade:
|
||||
return "CIPhotoEffectFade"
|
||||
case .instant:
|
||||
return "CIPhotoEffectInstant"
|
||||
case .process:
|
||||
return "CIPhotoEffectProcess"
|
||||
case .transfer:
|
||||
return "CIPhotoEffectTransfer"
|
||||
case .tone:
|
||||
return "CILinearToSRGBToneCurve"
|
||||
case .linear:
|
||||
return "CISRGBToneCurveToLinear"
|
||||
case .sepia:
|
||||
return "CISepiaTone"
|
||||
case .mono:
|
||||
return "CIPhotoEffectMono"
|
||||
case .noir:
|
||||
return "CIPhotoEffectNoir"
|
||||
case .tonal:
|
||||
return "CIPhotoEffectTonal"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ZLFilter: NSObject {
|
||||
public var name: String
|
||||
|
||||
let applier: ZLFilterApplierType?
|
||||
|
||||
@objc public init(name: String, filterType: ZLFilterType) {
|
||||
self.name = name
|
||||
|
||||
if filterType != .normal {
|
||||
applier = { image -> UIImage in
|
||||
guard let ciImage = image.zl.toCIImage() else {
|
||||
return image
|
||||
}
|
||||
|
||||
let filter = CIFilter(name: filterType.coreImageFilterName)
|
||||
filter?.setValue(ciImage, forKey: kCIInputImageKey)
|
||||
guard let outputImage = filter?.outputImage?.zl.toUIImage() else {
|
||||
return image
|
||||
}
|
||||
return outputImage
|
||||
}
|
||||
} else {
|
||||
applier = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 可传入 applier 自定义滤镜
|
||||
@objc public init(name: String, applier: ZLFilterApplierType?) {
|
||||
self.name = name
|
||||
self.applier = applier
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLFilter {
|
||||
class func clarendonFilter(image: UIImage) -> UIImage {
|
||||
guard let ciImage = image.zl.toCIImage() else {
|
||||
return image
|
||||
}
|
||||
|
||||
let backgroundImage = getColorImage(red: 127, green: 187, blue: 227, alpha: Int(255 * 0.2), rect: ciImage.extent)
|
||||
let outputCIImage = ciImage.applyingFilter("CIOverlayBlendMode", parameters: [
|
||||
"inputBackgroundImage": backgroundImage,
|
||||
])
|
||||
.applyingFilter("CIColorControls", parameters: [
|
||||
"inputSaturation": 1.35,
|
||||
"inputBrightness": 0.05,
|
||||
"inputContrast": 1.1,
|
||||
])
|
||||
guard let outputImage = outputCIImage.zl.toUIImage() else {
|
||||
return image
|
||||
}
|
||||
return outputImage
|
||||
}
|
||||
|
||||
class func nashvilleFilter(image: UIImage) -> UIImage {
|
||||
guard let ciImage = image.zl.toCIImage() else {
|
||||
return image
|
||||
}
|
||||
|
||||
let backgroundImage = getColorImage(red: 247, green: 176, blue: 153, alpha: Int(255 * 0.56), rect: ciImage.extent)
|
||||
let backgroundImage2 = getColorImage(red: 0, green: 70, blue: 150, alpha: Int(255 * 0.4), rect: ciImage.extent)
|
||||
let outputCIImage = ciImage
|
||||
.applyingFilter("CIDarkenBlendMode", parameters: [
|
||||
"inputBackgroundImage": backgroundImage,
|
||||
])
|
||||
.applyingFilter("CISepiaTone", parameters: [
|
||||
"inputIntensity": 0.2,
|
||||
])
|
||||
.applyingFilter("CIColorControls", parameters: [
|
||||
"inputSaturation": 1.2,
|
||||
"inputBrightness": 0.05,
|
||||
"inputContrast": 1.1,
|
||||
])
|
||||
.applyingFilter("CILightenBlendMode", parameters: [
|
||||
"inputBackgroundImage": backgroundImage2,
|
||||
])
|
||||
|
||||
guard let outputImage = outputCIImage.zl.toUIImage() else {
|
||||
return image
|
||||
}
|
||||
return outputImage
|
||||
}
|
||||
|
||||
class func apply1977Filter(image: UIImage) -> UIImage {
|
||||
guard let ciImage = image.zl.toCIImage() else {
|
||||
return image
|
||||
}
|
||||
|
||||
let filterImage = getColorImage(red: 243, green: 106, blue: 188, alpha: Int(255 * 0.1), rect: ciImage.extent)
|
||||
let backgroundImage = ciImage
|
||||
.applyingFilter("CIColorControls", parameters: [
|
||||
"inputSaturation": 1.3,
|
||||
"inputBrightness": 0.1,
|
||||
"inputContrast": 1.05,
|
||||
])
|
||||
.applyingFilter("CIHueAdjust", parameters: [
|
||||
"inputAngle": 0.3,
|
||||
])
|
||||
|
||||
let outputCIImage = filterImage
|
||||
.applyingFilter("CIScreenBlendMode", parameters: [
|
||||
"inputBackgroundImage": backgroundImage,
|
||||
])
|
||||
.applyingFilter("CIToneCurve", parameters: [
|
||||
"inputPoint0": CIVector(x: 0, y: 0),
|
||||
"inputPoint1": CIVector(x: 0.25, y: 0.20),
|
||||
"inputPoint2": CIVector(x: 0.5, y: 0.5),
|
||||
"inputPoint3": CIVector(x: 0.75, y: 0.80),
|
||||
"inputPoint4": CIVector(x: 1, y: 1),
|
||||
])
|
||||
|
||||
guard let outputImage = outputCIImage.zl.toUIImage() else {
|
||||
return image
|
||||
}
|
||||
return outputImage
|
||||
}
|
||||
|
||||
class func toasterFilter(image: UIImage) -> UIImage {
|
||||
guard let ciImage = image.zl.toCIImage() else {
|
||||
return image
|
||||
}
|
||||
|
||||
let width = ciImage.extent.width
|
||||
let height = ciImage.extent.height
|
||||
let centerWidth = width / 2.0
|
||||
let centerHeight = height / 2.0
|
||||
let radius0 = min(width / 4.0, height / 4.0)
|
||||
let radius1 = min(width / 1.5, height / 1.5)
|
||||
|
||||
let color0 = getColor(red: 128, green: 78, blue: 15, alpha: 255)
|
||||
let color1 = getColor(red: 79, green: 0, blue: 79, alpha: 255)
|
||||
let circle = CIFilter(name: "CIRadialGradient", parameters: [
|
||||
"inputCenter": CIVector(x: centerWidth, y: centerHeight),
|
||||
"inputRadius0": radius0,
|
||||
"inputRadius1": radius1,
|
||||
"inputColor0": color0,
|
||||
"inputColor1": color1,
|
||||
])?.outputImage?.cropped(to: ciImage.extent)
|
||||
|
||||
let outputCIImage = ciImage
|
||||
.applyingFilter("CIColorControls", parameters: [
|
||||
"inputSaturation": 1.0,
|
||||
"inputBrightness": 0.01,
|
||||
"inputContrast": 1.1,
|
||||
])
|
||||
.applyingFilter("CIScreenBlendMode", parameters: [
|
||||
"inputBackgroundImage": circle!,
|
||||
])
|
||||
|
||||
guard let outputImage = outputCIImage.zl.toUIImage() else {
|
||||
return image
|
||||
}
|
||||
return outputImage
|
||||
}
|
||||
|
||||
class func getColor(red: Int, green: Int, blue: Int, alpha: Int = 255) -> CIColor {
|
||||
return CIColor(
|
||||
red: CGFloat(Double(red) / 255.0),
|
||||
green: CGFloat(Double(green) / 255.0),
|
||||
blue: CGFloat(Double(blue) / 255.0),
|
||||
alpha: CGFloat(Double(alpha) / 255.0)
|
||||
)
|
||||
}
|
||||
|
||||
class func getColorImage(red: Int, green: Int, blue: Int, alpha: Int = 255, rect: CGRect) -> CIImage {
|
||||
let color = getColor(red: red, green: green, blue: blue, alpha: alpha)
|
||||
return CIImage(color: color).cropped(to: rect)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ZLFilter {
|
||||
@objc static let all: [ZLFilter] = [.normal, .clarendon, .nashville, .apply1977, .toaster, .chrome, .fade, .instant, .process, .transfer, .tone, .linear, .sepia, .mono, .noir, .tonal]
|
||||
|
||||
@objc static let normal = ZLFilter(name: "Normal", filterType: .normal)
|
||||
|
||||
@objc static let clarendon = ZLFilter(name: "Clarendon", applier: ZLFilter.clarendonFilter)
|
||||
|
||||
@objc static let nashville = ZLFilter(name: "Nashville", applier: ZLFilter.nashvilleFilter)
|
||||
|
||||
@objc static let apply1977 = ZLFilter(name: "1977", applier: ZLFilter.apply1977Filter)
|
||||
|
||||
@objc static let toaster = ZLFilter(name: "Toaster", applier: ZLFilter.toasterFilter)
|
||||
|
||||
@objc static let chrome = ZLFilter(name: "Chrome", filterType: .chrome)
|
||||
|
||||
@objc static let fade = ZLFilter(name: "Fade", filterType: .fade)
|
||||
|
||||
@objc static let instant = ZLFilter(name: "Instant", filterType: .instant)
|
||||
|
||||
@objc static let process = ZLFilter(name: "Process", filterType: .process)
|
||||
|
||||
@objc static let transfer = ZLFilter(name: "Transfer", filterType: .transfer)
|
||||
|
||||
@objc static let tone = ZLFilter(name: "Tone", filterType: .tone)
|
||||
|
||||
@objc static let linear = ZLFilter(name: "Linear", filterType: .linear)
|
||||
|
||||
@objc static let sepia = ZLFilter(name: "Sepia", filterType: .sepia)
|
||||
|
||||
@objc static let mono = ZLFilter(name: "Mono", filterType: .mono)
|
||||
|
||||
@objc static let noir = ZLFilter(name: "Noir", filterType: .noir)
|
||||
|
||||
@objc static let tonal = ZLFilter(name: "Tonal", filterType: .tonal)
|
||||
}
|
||||
143
Pods/ZLPhotoBrowser/Sources/Edit/ZLImageStickerView.swift
generated
Normal file
143
Pods/ZLPhotoBrowser/Sources/Edit/ZLImageStickerView.swift
generated
Normal file
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// ZLImageStickerView.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/11/20.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLImageStickerView: ZLBaseStickerView<ZLImageStickerState> {
|
||||
private let image: UIImage
|
||||
|
||||
private static let edgeInset: CGFloat = 20
|
||||
|
||||
private lazy var imageView: UIImageView = {
|
||||
let view = UIImageView(image: image)
|
||||
view.contentMode = .scaleAspectFit
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
// Convert all states to model.
|
||||
override var state: ZLImageStickerState {
|
||||
return ZLImageStickerState(
|
||||
image: image,
|
||||
originScale: originScale,
|
||||
originAngle: originAngle,
|
||||
originFrame: originFrame,
|
||||
gesScale: gesScale,
|
||||
gesRotation: gesRotation,
|
||||
totalTranslationPoint: totalTranslationPoint
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLImageStickerView deinit")
|
||||
}
|
||||
|
||||
convenience init(state: ZLImageStickerState) {
|
||||
self.init(
|
||||
image: state.image,
|
||||
originScale: state.originScale,
|
||||
originAngle: state.originAngle,
|
||||
originFrame: state.originFrame,
|
||||
gesScale: state.gesScale,
|
||||
gesRotation: state.gesRotation,
|
||||
totalTranslationPoint: state.totalTranslationPoint,
|
||||
showBorder: false
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
image: UIImage,
|
||||
originScale: CGFloat,
|
||||
originAngle: CGFloat,
|
||||
originFrame: CGRect,
|
||||
gesScale: CGFloat = 1,
|
||||
gesRotation: CGFloat = 0,
|
||||
totalTranslationPoint: CGPoint = .zero,
|
||||
showBorder: Bool = true
|
||||
) {
|
||||
self.image = image
|
||||
super.init(originScale: originScale, originAngle: originAngle, originFrame: originFrame, gesScale: gesScale, gesRotation: gesRotation, totalTranslationPoint: totalTranslationPoint, showBorder: showBorder)
|
||||
|
||||
borderView.addSubview(imageView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupUIFrameWhenFirstLayout() {
|
||||
imageView.frame = bounds.insetBy(dx: Self.edgeInset, dy: Self.edgeInset)
|
||||
}
|
||||
|
||||
class func calculateSize(image: UIImage, width: CGFloat) -> CGSize {
|
||||
let maxSide = width / 2
|
||||
let minSide: CGFloat = 100
|
||||
let whRatio = image.size.width / image.size.height
|
||||
var size: CGSize = .zero
|
||||
if whRatio >= 1 {
|
||||
let w = min(maxSide, max(minSide, image.size.width))
|
||||
let h = w / whRatio
|
||||
size = CGSize(width: w, height: h)
|
||||
} else {
|
||||
let h = min(maxSide, max(minSide, image.size.width))
|
||||
let w = h * whRatio
|
||||
size = CGSize(width: w, height: h)
|
||||
}
|
||||
size.width += Self.edgeInset * 2
|
||||
size.height += Self.edgeInset * 2
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public class ZLImageStickerState: NSObject {
|
||||
let image: UIImage
|
||||
let originScale: CGFloat
|
||||
let originAngle: CGFloat
|
||||
let originFrame: CGRect
|
||||
let gesScale: CGFloat
|
||||
let gesRotation: CGFloat
|
||||
let totalTranslationPoint: CGPoint
|
||||
|
||||
init(
|
||||
image: UIImage,
|
||||
originScale: CGFloat,
|
||||
originAngle: CGFloat,
|
||||
originFrame: CGRect,
|
||||
gesScale: CGFloat,
|
||||
gesRotation: CGFloat,
|
||||
totalTranslationPoint: CGPoint
|
||||
) {
|
||||
self.image = image
|
||||
self.originScale = originScale
|
||||
self.originAngle = originAngle
|
||||
self.originFrame = originFrame
|
||||
self.gesScale = gesScale
|
||||
self.gesRotation = gesRotation
|
||||
self.totalTranslationPoint = totalTranslationPoint
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
517
Pods/ZLPhotoBrowser/Sources/Edit/ZLInputTextViewController.swift
generated
Normal file
517
Pods/ZLPhotoBrowser/Sources/Edit/ZLInputTextViewController.swift
generated
Normal file
@@ -0,0 +1,517 @@
|
||||
//
|
||||
// ZLInputTextViewController.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/10/30.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLInputTextViewController: UIViewController {
|
||||
private static let toolViewHeight: CGFloat = 70
|
||||
|
||||
private let image: UIImage?
|
||||
|
||||
private var text: String
|
||||
|
||||
private var currentColor: UIColor {
|
||||
didSet {
|
||||
refreshTextViewUI()
|
||||
}
|
||||
}
|
||||
|
||||
private var textStyle: ZLInputTextStyle
|
||||
|
||||
private lazy var cancelBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle(localLanguageTextValue(.cancel), for: .normal)
|
||||
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
|
||||
btn.addTarget(self, action: #selector(cancelBtnClick), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
private lazy var doneBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle(localLanguageTextValue(.done), for: .normal)
|
||||
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
|
||||
btn.setTitleColor(.zl.bottomToolViewDoneBtnNormalTitleColor, for: .normal)
|
||||
btn.backgroundColor = .zl.bottomToolViewBtnNormalBgColor
|
||||
btn.addTarget(self, action: #selector(doneBtnClick), for: .touchUpInside)
|
||||
btn.layer.masksToBounds = true
|
||||
btn.layer.cornerRadius = ZLLayout.bottomToolBtnCornerRadius
|
||||
return btn
|
||||
}()
|
||||
|
||||
private lazy var textView: UITextView = {
|
||||
let y = max(deviceSafeAreaInsets().top, 20) + 20 + ZLLayout.bottomToolBtnH + 12
|
||||
let textView = UITextView(frame: CGRect(x: 10, y: y, width: view.zl.width - 20, height: 200))
|
||||
textView.keyboardAppearance = .dark
|
||||
textView.returnKeyType = .done
|
||||
textView.delegate = self
|
||||
textView.backgroundColor = .clear
|
||||
textView.tintColor = .zl.bottomToolViewBtnNormalBgColor
|
||||
textView.textColor = currentColor
|
||||
textView.text = text
|
||||
textView.font = .boldSystemFont(ofSize: ZLTextStickerView.fontSize)
|
||||
textView.textContainerInset = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 10)
|
||||
textView.textContainer.lineFragmentPadding = 0
|
||||
textView.layoutManager.delegate = self
|
||||
return textView
|
||||
}()
|
||||
|
||||
private lazy var toolView = UIView(frame: CGRect(
|
||||
x: 0,
|
||||
y: view.zl.height - Self.toolViewHeight,
|
||||
width: view.zl.width,
|
||||
height: Self.toolViewHeight
|
||||
))
|
||||
|
||||
private lazy var textStyleBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.addTarget(self, action: #selector(textStyleBtnClick), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
let layout = ZLCollectionViewFlowLayout()
|
||||
layout.itemSize = CGSize(width: 36, height: 36)
|
||||
layout.minimumLineSpacing = 0
|
||||
layout.minimumInteritemSpacing = 0
|
||||
layout.scrollDirection = .horizontal
|
||||
let inset = (Self.toolViewHeight - layout.itemSize.height) / 2
|
||||
layout.sectionInset = UIEdgeInsets(top: inset, left: 0, bottom: inset, right: 0)
|
||||
|
||||
let collectionView = UICollectionView(
|
||||
frame: .zero,
|
||||
collectionViewLayout: layout
|
||||
)
|
||||
collectionView.backgroundColor = .clear
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
ZLDrawColorCell.zl.register(collectionView)
|
||||
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
private lazy var textLayer = CAShapeLayer()
|
||||
|
||||
private let textLayerRadius: CGFloat = 10
|
||||
|
||||
private let maxTextCount = 100
|
||||
|
||||
/// text, textColor, image, style
|
||||
var endInput: ((String, UIColor, UIImage?, ZLInputTextStyle) -> Void)?
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLInputTextViewController deinit")
|
||||
}
|
||||
|
||||
init(image: UIImage?, text: String? = nil, textColor: UIColor? = nil, style: ZLInputTextStyle = .normal) {
|
||||
self.image = image
|
||||
self.text = text ?? ""
|
||||
if let textColor = textColor {
|
||||
currentColor = textColor
|
||||
} else {
|
||||
let editConfig = ZLPhotoConfiguration.default().editImageConfiguration
|
||||
if !editConfig.textStickerTextColors.contains(editConfig.textStickerDefaultTextColor) {
|
||||
currentColor = editConfig.textStickerTextColors.first!
|
||||
} else {
|
||||
currentColor = editConfig.textStickerDefaultTextColor
|
||||
}
|
||||
}
|
||||
self.textStyle = style
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupUI()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIApplication.keyboardWillShowNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIApplication.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
textView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
let btnY = max(deviceSafeAreaInsets().top, 20) + 20
|
||||
let cancelBtnW = localLanguageTextValue(.cancel).zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: .greatestFiniteMagnitude, height: ZLLayout.bottomToolBtnH)).width + 20
|
||||
cancelBtn.frame = CGRect(x: 15, y: btnY, width: cancelBtnW, height: ZLLayout.bottomToolBtnH)
|
||||
|
||||
let doneBtnW = localLanguageTextValue(.done).zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: .greatestFiniteMagnitude, height: ZLLayout.bottomToolBtnH)).width + 20
|
||||
doneBtn.frame = CGRect(x: view.zl.width - 20 - doneBtnW, y: btnY, width: doneBtnW, height: ZLLayout.bottomToolBtnH)
|
||||
|
||||
textStyleBtn.frame = CGRect(
|
||||
x: 12,
|
||||
y: 0,
|
||||
width: 50,
|
||||
height: Self.toolViewHeight
|
||||
)
|
||||
collectionView.frame = CGRect(
|
||||
x: textStyleBtn.zl.right + 5,
|
||||
y: 0,
|
||||
width: view.zl.width - textStyleBtn.zl.right - 5 - 24,
|
||||
height: Self.toolViewHeight
|
||||
)
|
||||
|
||||
if let index = ZLPhotoConfiguration.default().editImageConfiguration.textStickerTextColors.firstIndex(where: { $0 == self.currentColor }) {
|
||||
collectionView.scrollToItem(at: IndexPath(row: index, section: 0), at: .centeredHorizontally, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
view.backgroundColor = .black
|
||||
|
||||
let bgImageView = UIImageView(image: image?.zl.blurImage(level: 4))
|
||||
bgImageView.frame = view.bounds
|
||||
bgImageView.contentMode = .scaleAspectFit
|
||||
view.addSubview(bgImageView)
|
||||
|
||||
let coverView = UIView(frame: bgImageView.bounds)
|
||||
coverView.backgroundColor = .black
|
||||
coverView.alpha = 0.4
|
||||
bgImageView.addSubview(coverView)
|
||||
|
||||
view.addSubview(cancelBtn)
|
||||
view.addSubview(doneBtn)
|
||||
view.addSubview(textView)
|
||||
view.addSubview(toolView)
|
||||
toolView.addSubview(textStyleBtn)
|
||||
toolView.addSubview(collectionView)
|
||||
|
||||
refreshTextViewUI()
|
||||
}
|
||||
|
||||
private func refreshTextViewUI() {
|
||||
textStyleBtn.setImage(textStyle.btnImage, for: .normal)
|
||||
textStyleBtn.setImage(textStyle.btnImage, for: .highlighted)
|
||||
|
||||
drawTextBackground()
|
||||
|
||||
guard textStyle == .bg else {
|
||||
textView.textColor = currentColor
|
||||
return
|
||||
}
|
||||
|
||||
if currentColor == .white {
|
||||
textView.textColor = .black
|
||||
} else if currentColor == .black {
|
||||
textView.textColor = .white
|
||||
} else {
|
||||
textView.textColor = .white
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func textStyleBtnClick() {
|
||||
if textStyle == .normal {
|
||||
textStyle = .bg
|
||||
} else {
|
||||
textStyle = .normal
|
||||
}
|
||||
|
||||
refreshTextViewUI()
|
||||
}
|
||||
|
||||
@objc private func cancelBtnClick() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func doneBtnClick() {
|
||||
textView.tintColor = .clear
|
||||
textView.endEditing(true)
|
||||
|
||||
var image: UIImage?
|
||||
|
||||
if !textView.text.isEmpty {
|
||||
for subview in textView.subviews {
|
||||
if NSStringFromClass(subview.classForCoder) == "_UITextContainerView" {
|
||||
let size = textView.sizeThatFits(subview.frame.size)
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
if textStyle == .bg {
|
||||
textLayer.render(in: context)
|
||||
}
|
||||
|
||||
subview.layer.render(in: context)
|
||||
image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endInput?(textView.text, currentColor, image, textStyle)
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func keyboardWillShow(_ notify: Notification) {
|
||||
let rect = notify.userInfo?[UIApplication.keyboardFrameEndUserInfoKey] as? CGRect
|
||||
let keyboardH = rect?.height ?? 366
|
||||
let duration: TimeInterval = notify.userInfo?[UIApplication.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0.25
|
||||
|
||||
let toolViewFrame = CGRect(
|
||||
x: 0,
|
||||
y: view.zl.height - keyboardH - Self.toolViewHeight,
|
||||
width: view.zl.width,
|
||||
height: Self.toolViewHeight
|
||||
)
|
||||
|
||||
var textViewFrame = textView.frame
|
||||
textViewFrame.size.height = toolViewFrame.minY - textViewFrame.minY - 20
|
||||
|
||||
UIView.animate(withDuration: max(duration, 0.25)) {
|
||||
self.toolView.frame = toolViewFrame
|
||||
self.textView.frame = textViewFrame
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func keyboardWillHide(_ notify: Notification) {
|
||||
let duration: TimeInterval = notify.userInfo?[UIApplication.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0.25
|
||||
|
||||
let toolViewFrame = CGRect(
|
||||
x: 0,
|
||||
y: view.zl.height - deviceSafeAreaInsets().bottom - Self.toolViewHeight,
|
||||
width: view.zl.width,
|
||||
height: Self.toolViewHeight
|
||||
)
|
||||
|
||||
var textViewFrame = textView.frame
|
||||
textViewFrame.size.height = toolViewFrame.minY - textViewFrame.minY - 20
|
||||
|
||||
UIView.animate(withDuration: max(duration, 0.25)) {
|
||||
self.toolView.frame = toolViewFrame
|
||||
self.textView.frame = textViewFrame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLInputTextViewController: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return ZLPhotoConfiguration.default().editImageConfiguration.textStickerTextColors.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLDrawColorCell.zl.identifier, for: indexPath) as! ZLDrawColorCell
|
||||
|
||||
let c = ZLPhotoConfiguration.default().editImageConfiguration.textStickerTextColors[indexPath.row]
|
||||
cell.color = c
|
||||
if c == currentColor {
|
||||
cell.bgWhiteView.layer.transform = CATransform3DMakeScale(1.33, 1.33, 1)
|
||||
cell.colorView.layer.transform = CATransform3DMakeScale(1.2, 1.2, 1)
|
||||
} else {
|
||||
cell.bgWhiteView.layer.transform = CATransform3DIdentity
|
||||
cell.colorView.layer.transform = CATransform3DIdentity
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
currentColor = ZLPhotoConfiguration.default().editImageConfiguration.textStickerTextColors[indexPath.row]
|
||||
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
|
||||
collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Draw text layer
|
||||
|
||||
extension ZLInputTextViewController {
|
||||
private func drawTextBackground() {
|
||||
guard textStyle == .bg, !textView.text.isEmpty else {
|
||||
textLayer.removeFromSuperlayer()
|
||||
return
|
||||
}
|
||||
|
||||
let rects = calculateTextRects()
|
||||
|
||||
let path = UIBezierPath()
|
||||
for (index, rect) in rects.enumerated() {
|
||||
if index == 0 {
|
||||
path.move(to: CGPoint(x: rect.minX, y: rect.minY + textLayerRadius))
|
||||
path.addArc(withCenter: CGPoint(x: rect.minX + textLayerRadius, y: rect.minY + textLayerRadius), radius: textLayerRadius, startAngle: .pi, endAngle: .pi * 1.5, clockwise: true)
|
||||
path.addLine(to: CGPoint(x: rect.maxX - textLayerRadius, y: rect.minY))
|
||||
path.addArc(withCenter: CGPoint(x: rect.maxX - textLayerRadius, y: rect.minY + textLayerRadius), radius: textLayerRadius, startAngle: .pi * 1.5, endAngle: .pi * 2, clockwise: true)
|
||||
} else {
|
||||
let preRect = rects[index - 1]
|
||||
if rect.maxX > preRect.maxX {
|
||||
path.addLine(to: CGPoint(x: preRect.maxX, y: rect.minY - textLayerRadius))
|
||||
path.addArc(withCenter: CGPoint(x: preRect.maxX + textLayerRadius, y: rect.minY - textLayerRadius), radius: textLayerRadius, startAngle: -.pi, endAngle: -.pi * 1.5, clockwise: false)
|
||||
path.addLine(to: CGPoint(x: rect.maxX - textLayerRadius, y: rect.minY))
|
||||
path.addArc(withCenter: CGPoint(x: rect.maxX - textLayerRadius, y: rect.minY + textLayerRadius), radius: textLayerRadius, startAngle: .pi * 1.5, endAngle: .pi * 2, clockwise: true)
|
||||
} else if rect.maxX < preRect.maxX {
|
||||
path.addLine(to: CGPoint(x: preRect.maxX, y: preRect.maxY - textLayerRadius))
|
||||
path.addArc(withCenter: CGPoint(x: preRect.maxX - textLayerRadius, y: preRect.maxY - textLayerRadius), radius: textLayerRadius, startAngle: 0, endAngle: .pi / 2, clockwise: true)
|
||||
path.addLine(to: CGPoint(x: rect.maxX + textLayerRadius, y: preRect.maxY))
|
||||
path.addArc(withCenter: CGPoint(x: rect.maxX + textLayerRadius, y: preRect.maxY + textLayerRadius), radius: textLayerRadius, startAngle: -.pi / 2, endAngle: -.pi, clockwise: false)
|
||||
} else {
|
||||
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + textLayerRadius))
|
||||
}
|
||||
}
|
||||
|
||||
if index == rects.count - 1 {
|
||||
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - textLayerRadius))
|
||||
path.addArc(withCenter: CGPoint(x: rect.maxX - textLayerRadius, y: rect.maxY - textLayerRadius), radius: textLayerRadius, startAngle: 0, endAngle: .pi / 2, clockwise: true)
|
||||
path.addLine(to: CGPoint(x: rect.minX + textLayerRadius, y: rect.maxY))
|
||||
path.addArc(withCenter: CGPoint(x: rect.minX + textLayerRadius, y: rect.maxY - textLayerRadius), radius: textLayerRadius, startAngle: .pi / 2, endAngle: .pi, clockwise: true)
|
||||
|
||||
let firstRect = rects[0]
|
||||
path.addLine(to: CGPoint(x: firstRect.minX, y: firstRect.minY + textLayerRadius))
|
||||
path.close()
|
||||
}
|
||||
}
|
||||
|
||||
textLayer.path = path.cgPath
|
||||
textLayer.fillColor = currentColor.cgColor
|
||||
if textLayer.superlayer == nil {
|
||||
textView.layer.insertSublayer(textLayer, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
private func calculateTextRects() -> [CGRect] {
|
||||
let layoutManager = textView.layoutManager
|
||||
|
||||
// 这里必须用utf16.count 或者 (text as NSString).length,因为用count的话不准,一个emoji表情的count为2
|
||||
let range = layoutManager.glyphRange(forCharacterRange: NSMakeRange(0, textView.text.utf16.count), actualCharacterRange: nil)
|
||||
let glyphRange = layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil)
|
||||
|
||||
var rects: [CGRect] = []
|
||||
|
||||
let insetLeft = textView.textContainerInset.left
|
||||
let insetTop = textView.textContainerInset.top
|
||||
layoutManager.enumerateLineFragments(forGlyphRange: glyphRange) { _, usedRect, _, _, _ in
|
||||
rects.append(CGRect(x: usedRect.minX - 10 + insetLeft, y: usedRect.minY - 8 + insetTop, width: usedRect.width + 20, height: usedRect.height + 16))
|
||||
}
|
||||
|
||||
guard rects.count > 1 else {
|
||||
return rects
|
||||
}
|
||||
|
||||
for i in 1..<rects.count {
|
||||
processRects(&rects, index: i, maxIndex: i)
|
||||
}
|
||||
|
||||
return rects
|
||||
}
|
||||
|
||||
private func processRects(_ rects: inout [CGRect], index: Int, maxIndex: Int) {
|
||||
guard rects.count > 1, index > 0, index <= maxIndex else {
|
||||
return
|
||||
}
|
||||
|
||||
var preRect = rects[index - 1]
|
||||
var currRect = rects[index]
|
||||
|
||||
var preChanged = false
|
||||
var currChanged = false
|
||||
|
||||
// 当前rect宽度大于上方的rect,但差值小于2倍圆角
|
||||
if currRect.width > preRect.width, currRect.width - preRect.width < 2 * textLayerRadius {
|
||||
var size = preRect.size
|
||||
size.width = currRect.width
|
||||
preRect = CGRect(origin: preRect.origin, size: size)
|
||||
preChanged = true
|
||||
}
|
||||
|
||||
if currRect.width < preRect.width, preRect.width - currRect.width < 2 * textLayerRadius {
|
||||
var size = currRect.size
|
||||
size.width = preRect.width
|
||||
currRect = CGRect(origin: currRect.origin, size: size)
|
||||
currChanged = true
|
||||
}
|
||||
|
||||
if preChanged {
|
||||
rects[index - 1] = preRect
|
||||
processRects(&rects, index: index - 1, maxIndex: maxIndex)
|
||||
}
|
||||
|
||||
if currChanged {
|
||||
rects[index] = currRect
|
||||
processRects(&rects, index: index + 1, maxIndex: maxIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLInputTextViewController: UITextViewDelegate {
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
let markedTextRange = textView.markedTextRange
|
||||
guard markedTextRange == nil || (markedTextRange?.isEmpty ?? true) else {
|
||||
return
|
||||
}
|
||||
|
||||
let text = textView.text ?? ""
|
||||
if text.count > maxTextCount {
|
||||
let endIndex = text.index(text.startIndex, offsetBy: maxTextCount)
|
||||
textView.text = String(text[..<endIndex])
|
||||
}
|
||||
}
|
||||
|
||||
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
if text == "\n" {
|
||||
doneBtnClick()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension ZLInputTextViewController: NSLayoutManagerDelegate {
|
||||
func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) {
|
||||
guard layoutFinishedFlag else {
|
||||
return
|
||||
}
|
||||
|
||||
drawTextBackground()
|
||||
}
|
||||
}
|
||||
|
||||
public enum ZLInputTextStyle {
|
||||
case normal
|
||||
case bg
|
||||
|
||||
fileprivate var btnImage: UIImage? {
|
||||
switch self {
|
||||
case .normal:
|
||||
return .zl.getImage("zl_input_font")
|
||||
case .bg:
|
||||
return .zl.getImage("zl_input_font_bg")
|
||||
}
|
||||
}
|
||||
}
|
||||
209
Pods/ZLPhotoBrowser/Sources/Edit/ZLTextStickerView.swift
generated
Normal file
209
Pods/ZLPhotoBrowser/Sources/Edit/ZLTextStickerView.swift
generated
Normal file
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// ZLTextStickerView.swift
|
||||
// ZLPhotoBrowser
|
||||
//
|
||||
// Created by long on 2020/10/30.
|
||||
//
|
||||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ZLTextStickerView: ZLBaseStickerView<ZLTextStickerState> {
|
||||
static let fontSize: CGFloat = 32
|
||||
|
||||
private static let edgeInset: CGFloat = 10
|
||||
|
||||
private lazy var imageView: UIImageView = {
|
||||
let view = UIImageView(image: image)
|
||||
view.contentMode = .scaleAspectFit
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
var text: String
|
||||
|
||||
var textColor: UIColor
|
||||
|
||||
var style: ZLInputTextStyle
|
||||
|
||||
var image: UIImage {
|
||||
didSet {
|
||||
imageView.image = image
|
||||
}
|
||||
}
|
||||
|
||||
// Convert all states to model.
|
||||
override var state: ZLTextStickerState {
|
||||
return ZLTextStickerState(
|
||||
text: text,
|
||||
textColor: textColor,
|
||||
style: style,
|
||||
image: image,
|
||||
originScale: originScale,
|
||||
originAngle: originAngle,
|
||||
originFrame: originFrame,
|
||||
gesScale: gesScale,
|
||||
gesRotation: gesRotation,
|
||||
totalTranslationPoint: totalTranslationPoint
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
zl_debugPrint("ZLTextStickerView deinit")
|
||||
}
|
||||
|
||||
convenience init(state: ZLTextStickerState) {
|
||||
self.init(
|
||||
text: state.text,
|
||||
textColor: state.textColor,
|
||||
style: state.style,
|
||||
image: state.image,
|
||||
originScale: state.originScale,
|
||||
originAngle: state.originAngle,
|
||||
originFrame: state.originFrame,
|
||||
gesScale: state.gesScale,
|
||||
gesRotation: state.gesRotation,
|
||||
totalTranslationPoint: state.totalTranslationPoint,
|
||||
showBorder: false
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
text: String,
|
||||
textColor: UIColor,
|
||||
style: ZLInputTextStyle,
|
||||
image: UIImage,
|
||||
originScale: CGFloat,
|
||||
originAngle: CGFloat,
|
||||
originFrame: CGRect,
|
||||
gesScale: CGFloat = 1,
|
||||
gesRotation: CGFloat = 0,
|
||||
totalTranslationPoint: CGPoint = .zero,
|
||||
showBorder: Bool = true
|
||||
) {
|
||||
self.text = text
|
||||
self.textColor = textColor
|
||||
self.style = style
|
||||
self.image = image
|
||||
super.init(originScale: originScale, originAngle: originAngle, originFrame: originFrame, gesScale: gesScale, gesRotation: gesRotation, totalTranslationPoint: totalTranslationPoint, showBorder: showBorder)
|
||||
|
||||
borderView.addSubview(imageView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupUIFrameWhenFirstLayout() {
|
||||
imageView.frame = borderView.bounds.insetBy(dx: Self.edgeInset, dy: Self.edgeInset)
|
||||
}
|
||||
|
||||
override func tapAction(_ ges: UITapGestureRecognizer) {
|
||||
guard gesIsEnabled else { return }
|
||||
|
||||
if let timer = timer, timer.isValid {
|
||||
delegate?.sticker(self, editText: text)
|
||||
} else {
|
||||
super.tapAction(ges)
|
||||
}
|
||||
}
|
||||
|
||||
func changeSize(to newSize: CGSize) {
|
||||
// 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)
|
||||
transform = transform.rotated(by: -originAngle.zl.toPi)
|
||||
|
||||
// Recalculate current frame.
|
||||
let center = CGPoint(x: self.frame.midX, y: self.frame.midY)
|
||||
var frame = self.frame
|
||||
frame.origin.x = center.x - newSize.width / 2
|
||||
frame.origin.y = center.y - newSize.height / 2
|
||||
frame.size = newSize
|
||||
self.frame = frame
|
||||
|
||||
let oc = CGPoint(x: originFrame.midX, y: originFrame.midY)
|
||||
var of = originFrame
|
||||
of.origin.x = oc.x - newSize.width / 2
|
||||
of.origin.y = oc.y - newSize.height / 2
|
||||
of.size = newSize
|
||||
originFrame = of
|
||||
|
||||
imageView.frame = borderView.bounds.insetBy(dx: Self.edgeInset, dy: Self.edgeInset)
|
||||
|
||||
// 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)
|
||||
transform = transform.rotated(by: originAngle.zl.toPi)
|
||||
}
|
||||
|
||||
class func calculateSize(image: UIImage) -> CGSize {
|
||||
var size = image.size
|
||||
size.width += Self.edgeInset * 2
|
||||
size.height += Self.edgeInset * 2
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public class ZLTextStickerState: NSObject {
|
||||
let text: String
|
||||
let textColor: UIColor
|
||||
let style: ZLInputTextStyle
|
||||
let image: UIImage
|
||||
let originScale: CGFloat
|
||||
let originAngle: CGFloat
|
||||
let originFrame: CGRect
|
||||
let gesScale: CGFloat
|
||||
let gesRotation: CGFloat
|
||||
let totalTranslationPoint: CGPoint
|
||||
|
||||
init(
|
||||
text: String,
|
||||
textColor: UIColor,
|
||||
style: ZLInputTextStyle,
|
||||
image: UIImage,
|
||||
originScale: CGFloat,
|
||||
originAngle: CGFloat,
|
||||
originFrame: CGRect,
|
||||
gesScale: CGFloat,
|
||||
gesRotation: CGFloat,
|
||||
totalTranslationPoint: CGPoint
|
||||
) {
|
||||
self.text = text
|
||||
self.textColor = textColor
|
||||
self.style = style
|
||||
self.image = image
|
||||
self.originScale = originScale
|
||||
self.originAngle = originAngle
|
||||
self.originFrame = originFrame
|
||||
self.gesScale = gesScale
|
||||
self.gesRotation = gesRotation
|
||||
self.totalTranslationPoint = totalTranslationPoint
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user