Stickers Import Improvements

This commit is contained in:
Ilya Laktyushin
2021-06-18 07:59:10 +03:00
parent 3d81bf7ad8
commit d8e36f149f
3 changed files with 227 additions and 65 deletions

View File

@@ -88,6 +88,7 @@ open class AlertController: ViewController, StandalonePresentableController {
private weak var existingAlertController: AlertController? private weak var existingAlertController: AlertController?
public var willDismiss: (() -> Void)?
public var dismissed: (() -> Void)? public var dismissed: (() -> Void)?
public init(theme: AlertControllerTheme, contentNode: AlertContentNode, existingAlertController: AlertController? = nil, allowInputInset: Bool = true) { public init(theme: AlertControllerTheme, contentNode: AlertContentNode, existingAlertController: AlertController? = nil, allowInputInset: Bool = true) {
@@ -115,6 +116,7 @@ open class AlertController: ViewController, StandalonePresentableController {
self.controllerNode.dismiss = { [weak self] in self.controllerNode.dismiss = { [weak self] in
if let strongSelf = self, strongSelf.contentNode.dismissOnOutsideTap { if let strongSelf = self, strongSelf.contentNode.dismissOnOutsideTap {
strongSelf.willDismiss?()
strongSelf.controllerNode.animateOut { strongSelf.controllerNode.animateOut {
self?.dismiss() self?.dismiss()
} }

View File

@@ -11,12 +11,179 @@ import AccountContext
import UrlEscaping import UrlEscaping
import ActivityIndicator import ActivityIndicator
private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { private class TextField: UITextField, UIScrollViewDelegate {
fileprivate func updatePrefixWidth(_ prefixWidth: CGFloat) {
let previousPrefixWidth = self.prefixWidth
self.prefixWidth = prefixWidth
let leftOffset = prefixWidth
if let scrollView = self.scrollView {
if scrollView.contentInset.left != leftOffset {
scrollView.contentInset = UIEdgeInsets(top: 0.0, left: leftOffset, bottom: 0.0, right: 0.0)
}
if leftOffset.isZero {
scrollView.contentOffset = CGPoint()
} else if self.prefixWidth != previousPrefixWidth {
scrollView.contentOffset = CGPoint(x: -leftOffset, y: 0.0)
}
self.updatePrefixPosition(transition: .immediate)
}
}
private var prefixWidth: CGFloat = 0.0
let prefixLabel: ImmediateTextNode
var prefixString: NSAttributedString? {
didSet {
self.prefixLabel.attributedText = self.prefixString
self.setNeedsLayout()
}
}
init() {
self.prefixLabel = ImmediateTextNode()
self.prefixLabel.isUserInteractionEnabled = false
self.prefixLabel.displaysAsynchronously = false
self.prefixLabel.maximumNumberOfLines = 1
self.prefixLabel.truncationMode = .byTruncatingTail
super.init(frame: CGRect())
self.addSubnode(self.prefixLabel)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func addSubview(_ view: UIView) {
super.addSubview(view)
if let scrollView = view as? UIScrollView {
scrollView.delegate = self
}
}
private weak var _scrollView: UIScrollView?
var scrollView: UIScrollView? {
if let scrollView = self._scrollView {
return scrollView
}
for view in self.subviews {
if let scrollView = view as? UIScrollView {
_scrollView = scrollView
return scrollView
}
}
return nil
}
override func deleteBackward() {
super.deleteBackward()
if let scrollView = self.scrollView {
if scrollView.contentSize.width <= scrollView.frame.width && scrollView.contentOffset.x > -scrollView.contentInset.left {
scrollView.contentOffset = CGPoint(x: max(scrollView.contentOffset.x - 5.0, -scrollView.contentInset.left), y: 0.0)
self.updatePrefixPosition()
}
}
}
var fixAutoScroll: CGPoint?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let fixAutoScroll = self.fixAutoScroll {
self.scrollView?.setContentOffset(fixAutoScroll, animated: true)
self.scrollView?.setContentOffset(fixAutoScroll, animated: false)
self.fixAutoScroll = nil
} else {
self.updatePrefixPosition()
}
}
override func becomeFirstResponder() -> Bool {
if let contentOffset = self.scrollView?.contentOffset {
self.fixAutoScroll = contentOffset
Queue.mainQueue().after(0.1) {
self.fixAutoScroll = nil
}
}
return super.becomeFirstResponder()
}
private func updatePrefixPosition(transition: ContainedViewLayoutTransition = .immediate) {
if let scrollView = self.scrollView {
transition.updateFrame(node: self.prefixLabel, frame: CGRect(origin: CGPoint(x: -scrollView.contentOffset.x - scrollView.contentInset.left, y: self.prefixLabel.frame.minY), size: self.prefixLabel.frame.size))
}
}
override var keyboardAppearance: UIKeyboardAppearance {
get {
return super.keyboardAppearance
}
set {
let resigning = self.isFirstResponder
if resigning {
self.resignFirstResponder()
}
super.keyboardAppearance = newValue
if resigning {
let _ = self.becomeFirstResponder()
}
}
}
override func textRect(forBounds bounds: CGRect) -> CGRect {
if bounds.size.width.isZero {
return CGRect(origin: CGPoint(), size: CGSize())
}
var rect = bounds.insetBy(dx: 0.0, dy: 4.0)
if #available(iOS 14.0, *) {
} else {
rect.origin.y += 1.0
}
if !self.prefixWidth.isZero && self.scrollView?.superview == nil {
var offset = self.prefixWidth
if let scrollView = self.scrollView {
offset = scrollView.contentOffset.x * -1.0
}
rect.origin.x += offset
rect.size.width -= offset
}
rect.size.width = max(rect.size.width, 10.0)
return rect
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return self.textRect(forBounds: bounds)
}
override func layoutSubviews() {
super.layoutSubviews()
let bounds = self.bounds
if bounds.size.width.isZero {
return
}
var placeholderOffset: CGFloat = 0.0
if #available(iOS 14.0, *) {
placeholderOffset = 1.0
} else {
}
let textRect = self.textRect(forBounds: bounds)
let prefixSize = self.prefixLabel.updateLayout(CGSize(width: floor(bounds.size.width * 0.7), height: bounds.size.height))
let prefixBounds = bounds.insetBy(dx: 4.0, dy: 4.0)
self.prefixLabel.frame = CGRect(origin: CGPoint(x: prefixBounds.minX, y: floorToScreenPixels((bounds.height - prefixSize.height) / 2.0)), size: prefixSize)
self.updatePrefixWidth(prefixSize.width)
}
}
private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, UITextFieldDelegate {
private var theme: PresentationTheme private var theme: PresentationTheme
private let backgroundNode: ASImageNode private let backgroundNode: ASImageNode
private let textInputNode: EditableTextNode // private let textInputNode: EditableTextNode
private let placeholderNode: ASTextNode private let textInputNode: TextField
private let prefixNode: ASTextNode
private let clearButton: HighlightableButtonNode private let clearButton: HighlightableButtonNode
var updateHeight: (() -> Void)? var updateHeight: (() -> Void)?
@@ -32,8 +199,7 @@ private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, ASEdita
} }
set { set {
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(14.0), textColor: self.theme.actionSheet.inputTextColor) self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(14.0), textColor: self.theme.actionSheet.inputTextColor)
self.placeholderNode.isHidden = !newValue.isEmpty if self.textInputNode.isFirstResponder {
if self.textInputNode.isFirstResponder() {
self.clearButton.isHidden = newValue.isEmpty self.clearButton.isHidden = newValue.isEmpty
} else { } else {
self.clearButton.isHidden = true self.clearButton.isHidden = true
@@ -41,15 +207,9 @@ private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, ASEdita
} }
} }
var placeholder: String = "" {
didSet {
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(14.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
}
}
var prefix: String = "" { var prefix: String = "" {
didSet { didSet {
self.prefixNode.attributedText = NSAttributedString(string: self.prefix, font: Font.regular(14.0), textColor: self.theme.actionSheet.inputTextColor) self.textInputNode.prefixString = NSAttributedString(string: self.prefix, font: Font.regular(14.0), textColor: self.theme.actionSheet.inputTextColor)
} }
} }
@@ -66,33 +226,22 @@ private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, ASEdita
self.maxLength = maxLength self.maxLength = maxLength
self.backgroundNode = ASImageNode() self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.textInputNode = EditableTextNode() self.textInputNode = TextField()
self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(14.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] self.textInputNode.font = Font.regular(14.0)
self.textInputNode.typingAttributes = [NSAttributedString.Key.font: Font.regular(14.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor]
self.textInputNode.clipsToBounds = true self.textInputNode.clipsToBounds = true
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) // self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0)
self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0)
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textInputNode.keyboardType = keyboardType self.textInputNode.keyboardType = keyboardType
self.textInputNode.autocapitalizationType = .sentences self.textInputNode.autocapitalizationType = .sentences
self.textInputNode.returnKeyType = returnKeyType self.textInputNode.returnKeyType = returnKeyType
self.textInputNode.autocorrectionType = .default self.textInputNode.autocorrectionType = .default
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
self.placeholderNode = ASTextNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.displaysAsynchronously = false
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(14.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
self.prefixNode = ASTextNode()
self.prefixNode.isUserInteractionEnabled = false
self.prefixNode.displaysAsynchronously = false
self.prefixNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(14.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
self.clearButton = HighlightableButtonNode() self.clearButton = HighlightableButtonNode()
self.clearButton.imageNode.displaysAsynchronously = false self.clearButton.imageNode.displaysAsynchronously = false
self.clearButton.imageNode.displayWithoutProcessing = true self.clearButton.imageNode.displayWithoutProcessing = true
@@ -101,24 +250,29 @@ private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, ASEdita
self.clearButton.isHidden = true self.clearButton.isHidden = true
super.init() super.init()
self.textInputNode.delegate = self
self.addSubnode(self.backgroundNode) self.addSubnode(self.backgroundNode)
self.addSubnode(self.textInputNode)
self.addSubnode(self.placeholderNode)
self.addSubnode(self.prefixNode)
self.addSubnode(self.clearButton) self.addSubnode(self.clearButton)
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
} }
override func didLoad() {
super.didLoad()
self.textInputNode.delegate = self
self.view.insertSubview(self.textInputNode, aboveSubview: self.backgroundNode.view)
}
func selectAll() {
self.textInputNode.selectAll(nil)
}
func updateTheme(_ theme: PresentationTheme) { func updateTheme(_ theme: PresentationTheme) {
self.theme = theme self.theme = theme
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor
self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: []) self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: [])
} }
@@ -133,13 +287,7 @@ private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, ASEdita
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) transition.updateFrame(view: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - 20.0, height: backgroundFrame.size.height)))
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
let prefixSize = self.prefixNode.measure(backgroundFrame.size)
transition.updateFrame(node: self.prefixNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - prefixSize.height) / 2.0)), size: prefixSize))
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left + prefixSize.width, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - 20.0, height: backgroundFrame.size.height)))
if let image = self.clearButton.image(for: []) { if let image = self.clearButton.image(for: []) {
transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size)) transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size))
@@ -149,51 +297,46 @@ private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, ASEdita
} }
func activateInput() { func activateInput() {
self.textInputNode.becomeFirstResponder() let _ = self.textInputNode.becomeFirstResponder()
} }
func deactivateInput() { func deactivateInput() {
self.textInputNode.resignFirstResponder() self.textInputNode.resignFirstResponder()
} }
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { func textFieldDidBeginEditing(_ textField: UITextField) {
self.updateTextNodeText(animated: true) self.clearButton.isHidden = (textField.text ?? "").isEmpty
self.textChanged?(editableTextNode.textView.text)
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
self.clearButton.isHidden = !self.placeholderNode.isHidden
} }
func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { func textFieldDidEndEditing(_ textField: UITextField) {
self.clearButton.isHidden = (editableTextNode.textView.text ?? "").isEmpty
}
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.clearButton.isHidden = true self.clearButton.isHidden = true
} }
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { func textFieldDidUpdateText(_ text: String) {
self.updateTextNodeText(animated: true)
self.textChanged?(text)
self.clearButton.isHidden = (text).isEmpty
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if self.disabled { if self.disabled {
return false return false
} }
let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) let updatedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
if updatedText.count > maxLength { if updatedText.count > maxLength {
self.textInputNode.layer.addShakeAnimation() self.textInputNode.layer.addShakeAnimation()
return false return false
} }
if text == "\n" { if string == "\n" {
self.complete?() self.complete?()
return false return false
} }
self.textFieldDidUpdateText(updatedText)
return true return true
} }
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
let backgroundInsets = self.backgroundInsets return 33.0
let inputInsets = self.inputInsets
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - 20.0, height: CGFloat.greatestFiniteMagnitude)).height))
return min(61.0, max(33.0, unboundTextFieldHeight))
} }
private func updateTextNodeText(animated: Bool) { private func updateTextNodeText(animated: Bool) {
@@ -208,11 +351,11 @@ private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, ASEdita
} }
@objc func clearPressed() { @objc func clearPressed() {
self.placeholderNode.isHidden = false
self.clearButton.isHidden = true self.clearButton.isHidden = true
self.textInputNode.attributedText = nil self.textInputNode.attributedText = nil
self.updateHeight?() self.updateHeight?()
self.textChanged?("")
} }
} }
@@ -552,6 +695,9 @@ func importStickerPackTitleController(context: AccountContext, title: String, te
contentNode.inputFieldNode.textChanged = { [weak contentNode] title in contentNode.inputFieldNode.textChanged = { [weak contentNode] title in
contentNode?.actionNodes.last?.actionEnabled = !title.trimmingTrailingSpaces().isEmpty contentNode?.actionNodes.last?.actionEnabled = !title.trimmingTrailingSpaces().isEmpty
} }
controller.willDismiss = { [weak contentNode] in
contentNode?.inputFieldNode.deactivateInput()
}
controller.dismissed = { controller.dismissed = {
presentationDataDisposable.dispose() presentationDataDisposable.dispose()
} }
@@ -603,6 +749,11 @@ func importStickerPackShortNameController(context: AccountContext, title: String
let checkDisposable = MetaDisposable() let checkDisposable = MetaDisposable()
var value = value ?? "" var value = value ?? ""
contentNode.actionNodes.last?.actionEnabled = !value.isEmpty contentNode.actionNodes.last?.actionEnabled = !value.isEmpty
if !value.isEmpty {
Queue.mainQueue().after(0.25) {
contentNode.inputFieldNode.selectAll()
}
}
contentNode.inputFieldNode.textChanged = { [weak contentNode] value in contentNode.inputFieldNode.textChanged = { [weak contentNode] value in
if value.isEmpty { if value.isEmpty {
checkDisposable.set(nil) checkDisposable.set(nil)
@@ -634,6 +785,9 @@ func importStickerPackShortNameController(context: AccountContext, title: String
})) }))
} }
} }
controller.willDismiss = { [weak contentNode] in
contentNode?.inputFieldNode.deactivateInput()
}
controller.dismissed = { controller.dismissed = {
presentationDataDisposable.dispose() presentationDataDisposable.dispose()
} }

View File

@@ -168,7 +168,13 @@ private func videoFirstFrameData(account: Account, resource: MediaResource, chun
private func fetchCachedStickerAJpegRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedStickerAJpegRepresentation) -> Signal<CachedMediaResourceRepresentationResult, NoError> { private func fetchCachedStickerAJpegRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedStickerAJpegRepresentation) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
return Signal({ subscriber in return Signal({ subscriber in
if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) {
if let image = WebP.convert(fromWebP: data) { var image: UIImage?
if let webpImage = WebP.convert(fromWebP: data) {
image = webpImage
} else if let pngImage = UIImage(data: data) {
image = pngImage
}
if let image = image {
let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))"
let url = URL(fileURLWithPath: path) let url = URL(fileURLWithPath: path)