Files
OrderScheduling/Pods/ZLPhotoBrowser/Sources/Edit/ZLInputTextViewController.swift
DDIsFriend f0e8a1709d initial
2023-08-18 17:28:57 +08:00

518 lines
21 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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