This commit is contained in:
DDIsFriend
2023-08-18 17:28:57 +08:00
commit f0e8a1709d
4282 changed files with 192396 additions and 0 deletions

View 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?()
}
}
}

View 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~360360
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
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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")
}
}

View 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
}
}

View 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)
}

View 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()
}
}

View 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).lengthcountemojicount2
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
// rectrect2
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")
}
}
}

View 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()
}
}