mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Various improvements
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import TelegramPresentationData
|
||||
|
||||
final class VideoChatEncryptionKeyComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let emoji: [String]
|
||||
let isExpanded: Bool
|
||||
let tapAction: () -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
emoji: [String],
|
||||
isExpanded: Bool,
|
||||
tapAction: @escaping () -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.emoji = emoji
|
||||
self.isExpanded = isExpanded
|
||||
self.tapAction = tapAction
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatEncryptionKeyComponent, rhs: VideoChatEncryptionKeyComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.emoji != rhs.emoji {
|
||||
return false
|
||||
}
|
||||
if lhs.isExpanded != rhs.isExpanded {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let containerView: UIView
|
||||
private var emojiItems: [ComponentView<Empty>] = []
|
||||
private let background = ComponentView<Empty>()
|
||||
private let backgroundShadowLayer = SimpleLayer()
|
||||
private let collapsedText = ComponentView<Empty>()
|
||||
private let expandedText = ComponentView<Empty>()
|
||||
private let expandedSeparatorLayer = SimpleLayer()
|
||||
private let expandedButtonText = ComponentView<Empty>()
|
||||
|
||||
private var component: VideoChatEncryptionKeyComponent?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.containerView = UIView()
|
||||
self.containerView.clipsToBounds = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.containerView)
|
||||
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
self.addGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
if case .ended = recognizer.state {
|
||||
component.tapAction()
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.containerView.frame.contains(point) {
|
||||
return self
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: VideoChatEncryptionKeyComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
self.component = component
|
||||
|
||||
let alphaTransition: ComponentTransition
|
||||
if transition.animation.isImmediate {
|
||||
alphaTransition = .immediate
|
||||
} else {
|
||||
alphaTransition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
|
||||
let collapsedSideInset: CGFloat = 7.0
|
||||
let collapsedVerticalInset: CGFloat = 8.0
|
||||
let collapsedEmojiSpacing: CGFloat = 0.0
|
||||
let collapsedEmojiTextSpacing: CGFloat = 5.0
|
||||
|
||||
let expandedTopInset: CGFloat = 8.0
|
||||
let expandedSideInset: CGFloat = 14.0
|
||||
var expandedEmojiSpacing: CGFloat = 10.0
|
||||
let expandedEmojiTextSpacing: CGFloat = 10.0
|
||||
let expandedTextButtonSpacing: CGFloat = 10.0
|
||||
let expandedButtonTopInset: CGFloat = 12.0
|
||||
let expandedButtonBottomInset: CGFloat = 13.0
|
||||
|
||||
let emojiItemSizes = (0 ..< component.emoji.count).map { i -> CGSize in
|
||||
let emojiItem: ComponentView<Empty>
|
||||
if self.emojiItems.count > i {
|
||||
emojiItem = self.emojiItems[i]
|
||||
} else {
|
||||
emojiItem = ComponentView()
|
||||
self.emojiItems.append(emojiItem)
|
||||
}
|
||||
return emojiItem.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.emoji[i], font: Font.regular(40.0), textColor: .white))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: 200.0)
|
||||
)
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let collapsedTextSize = self.collapsedText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "End-to-end encrypted", font: Font.semibold(12.0), textColor: component.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 1000.0, height: 1000.0)
|
||||
)
|
||||
|
||||
let expandedTextSize = self.expandedText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: "These four emojis represent the call's encryption key. They must match for all participants and change when someone joins or leaves.", font: Font.regular(12.0), textColor: component.theme.list.itemPrimaryTextColor)),
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.3
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - expandedSideInset * 2.0, height: 1000.0)
|
||||
)
|
||||
|
||||
let expandedButtonTextSize = self.expandedButtonText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Close", font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - expandedSideInset * 2.0, height: 1000.0)
|
||||
)
|
||||
|
||||
let collapsedEmojiFactor: CGFloat = 20.0 / 40.0
|
||||
let collapsedEmojiSizes = emojiItemSizes.map { CGSize(width: floor($0.width * collapsedEmojiFactor), height: floor($0.height * collapsedEmojiFactor)) }
|
||||
|
||||
let collapsedSize = CGSize(width: collapsedTextSize.width + collapsedSideInset * 2.0 + collapsedEmojiTextSpacing * 2.0 + collapsedEmojiSpacing + collapsedEmojiSizes.reduce(into: 0.0, { $0 += $1.width }), height: collapsedTextSize.height + collapsedVerticalInset * 2.0)
|
||||
|
||||
var expandedEmojiWidth = expandedEmojiSpacing * CGFloat(self.emojiItems.count - 1) + emojiItemSizes.reduce(into: 0.0, { $0 += $1.width })
|
||||
|
||||
var expandedSize = CGSize(width: expandedSideInset * 2.0, height: 0.0)
|
||||
|
||||
expandedSize.width += max(expandedTextSize.width, expandedEmojiWidth)
|
||||
|
||||
expandedSize.height += expandedTopInset
|
||||
expandedSize.height += emojiItemSizes[0].height
|
||||
expandedSize.height += expandedEmojiTextSpacing
|
||||
expandedSize.height += expandedTextSize.height
|
||||
expandedSize.height += expandedTextButtonSpacing
|
||||
expandedSize.height += expandedButtonTopInset
|
||||
expandedSize.height += expandedButtonTextSize.height
|
||||
expandedSize.height += expandedButtonBottomInset
|
||||
|
||||
if expandedEmojiWidth < expandedSize.width - expandedSideInset * 2.0 {
|
||||
expandedEmojiWidth = expandedSize.width - expandedSideInset * 2.0
|
||||
|
||||
let cleanEmojiWidth = emojiItemSizes.reduce(into: 0.0, { $0 += $1.width })
|
||||
expandedEmojiSpacing = floorToScreenPixels((expandedEmojiWidth - cleanEmojiWidth) / CGFloat(self.emojiItems.count - 1))
|
||||
expandedEmojiSpacing = min(expandedEmojiSpacing, 24.0)
|
||||
expandedEmojiWidth = expandedEmojiSpacing * CGFloat(self.emojiItems.count - 1) + emojiItemSizes.reduce(into: 0.0, { $0 += $1.width })
|
||||
}
|
||||
|
||||
let backgroundSize = component.isExpanded ? expandedSize : collapsedSize
|
||||
let backgroundCornerRadius: CGFloat = component.isExpanded ? 10.0 : collapsedSize.height * 0.5
|
||||
|
||||
let _ = self.background.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(FilledRoundedRectangleComponent(
|
||||
color: component.theme.list.itemBlocksBackgroundColor,
|
||||
cornerRadius: .value(backgroundCornerRadius), smoothCorners: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: backgroundSize
|
||||
)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize)
|
||||
|
||||
if self.backgroundShadowLayer.superlayer == nil {
|
||||
self.backgroundShadowLayer.backgroundColor = UIColor.clear.cgColor
|
||||
self.containerView.layer.addSublayer(self.backgroundShadowLayer)
|
||||
}
|
||||
self.backgroundShadowLayer.shadowOpacity = 0.3
|
||||
self.backgroundShadowLayer.shadowColor = UIColor.black.cgColor
|
||||
self.backgroundShadowLayer.shadowRadius = 5.0
|
||||
self.backgroundShadowLayer.shadowOffset = CGSize(width: 0.0, height: 2.0)
|
||||
alphaTransition.setAlpha(layer: self.backgroundShadowLayer, alpha: component.isExpanded ? 1.0 : 0.0)
|
||||
|
||||
transition.setFrame(layer: self.backgroundShadowLayer, frame: backgroundFrame)
|
||||
transition.setCornerRadius(layer: self.backgroundShadowLayer, cornerRadius: backgroundCornerRadius)
|
||||
transition.setShadowPath(layer: self.backgroundShadowLayer, path: UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: backgroundFrame.size), cornerRadius: backgroundCornerRadius).cgPath)
|
||||
|
||||
if let backgroundView = self.background.view {
|
||||
if backgroundView.superview == nil {
|
||||
self.containerView.addSubview(backgroundView)
|
||||
}
|
||||
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||
}
|
||||
|
||||
var collapsedEmojiLeftOffset = collapsedSideInset
|
||||
var collapsedEmojiRightOffset = collapsedSize.width - collapsedSideInset
|
||||
|
||||
for i in 0 ..< self.emojiItems.count {
|
||||
let mappedIndex: Int
|
||||
if i < 2 {
|
||||
mappedIndex = i
|
||||
} else {
|
||||
mappedIndex = self.emojiItems.count - (i - 1)
|
||||
}
|
||||
|
||||
var collapsedItemFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((collapsedSize.height - collapsedEmojiSizes[mappedIndex].height) * 0.5)), size: collapsedEmojiSizes[mappedIndex])
|
||||
if i < 2 {
|
||||
if mappedIndex != 0 {
|
||||
collapsedEmojiLeftOffset += collapsedEmojiSpacing
|
||||
}
|
||||
collapsedItemFrame.origin.x = collapsedEmojiLeftOffset
|
||||
collapsedEmojiLeftOffset += collapsedEmojiSizes[mappedIndex].width
|
||||
} else {
|
||||
if mappedIndex != 0 {
|
||||
collapsedEmojiRightOffset -= collapsedEmojiSpacing
|
||||
}
|
||||
collapsedItemFrame.origin.x = collapsedEmojiRightOffset - collapsedEmojiSizes[mappedIndex].width
|
||||
collapsedEmojiRightOffset -= collapsedEmojiSizes[mappedIndex].width
|
||||
}
|
||||
|
||||
var expandedItemFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - expandedEmojiWidth) * 0.5), y: expandedTopInset), size: emojiItemSizes[mappedIndex])
|
||||
expandedItemFrame.origin.x += CGFloat(mappedIndex) * (emojiItemSizes[0].width + expandedEmojiSpacing)
|
||||
|
||||
let itemFrame: CGRect
|
||||
if component.isExpanded {
|
||||
itemFrame = expandedItemFrame
|
||||
} else {
|
||||
itemFrame = collapsedItemFrame
|
||||
}
|
||||
|
||||
if let itemView = self.emojiItems[mappedIndex].view {
|
||||
if itemView.superview == nil {
|
||||
self.containerView.addSubview(itemView)
|
||||
}
|
||||
transition.setPosition(view: itemView, position: itemFrame.center)
|
||||
itemView.bounds = CGRect(origin: CGPoint(), size: emojiItemSizes[mappedIndex])
|
||||
transition.setScale(view: itemView, scale: itemFrame.height / emojiItemSizes[mappedIndex].height)
|
||||
}
|
||||
}
|
||||
|
||||
var collapsedTextFrame = CGRect(origin: CGPoint(x: collapsedEmojiLeftOffset + collapsedEmojiTextSpacing, y: floor((collapsedSize.height - collapsedTextSize.height) * 0.5)), size: collapsedTextSize)
|
||||
var expandedTextFrame = CGRect(origin: CGPoint(x: floor((backgroundSize.width - expandedTextSize.width) * 0.5), y: expandedTopInset + emojiItemSizes[0].height + expandedEmojiTextSpacing), size: expandedTextSize)
|
||||
|
||||
if component.isExpanded {
|
||||
collapsedTextFrame.origin = expandedTextFrame.origin
|
||||
} else {
|
||||
expandedTextFrame.origin = collapsedTextFrame.origin
|
||||
}
|
||||
|
||||
if let collapsedTextView = self.collapsedText.view {
|
||||
if collapsedTextView.superview == nil {
|
||||
collapsedTextView.layer.anchorPoint = CGPoint()
|
||||
self.containerView.addSubview(collapsedTextView)
|
||||
} else {
|
||||
if collapsedTextView.alpha != (component.isExpanded ? 0.0 : 1.0) {
|
||||
if let blurFilter = CALayer.blur() {
|
||||
let maxBlur: CGFloat = 8.0
|
||||
if !component.isExpanded {
|
||||
blurFilter.setValue(0.0 as NSNumber, forKey: "inputRadius")
|
||||
collapsedTextView.layer.filters = [blurFilter]
|
||||
collapsedTextView.layer.animate(from: maxBlur as NSNumber, to: 0.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak collapsedTextView] flag in
|
||||
if flag, let collapsedTextView {
|
||||
collapsedTextView.layer.filters = nil
|
||||
}
|
||||
})
|
||||
} else {
|
||||
blurFilter.setValue(maxBlur as NSNumber, forKey: "inputRadius")
|
||||
collapsedTextView.layer.filters = [blurFilter]
|
||||
collapsedTextView.layer.animate(from: 0.0 as NSNumber, to: maxBlur as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, removeOnCompletion: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
transition.setPosition(view: collapsedTextView, position: collapsedTextFrame.origin)
|
||||
collapsedTextView.bounds = CGRect(origin: CGPoint(), size: collapsedTextFrame.size)
|
||||
alphaTransition.setAlpha(view: collapsedTextView, alpha: component.isExpanded ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
if let expandedTextView = self.expandedText.view {
|
||||
if expandedTextView.superview == nil {
|
||||
expandedTextView.layer.anchorPoint = CGPoint()
|
||||
self.containerView.addSubview(expandedTextView)
|
||||
} else {
|
||||
if expandedTextView.alpha != (component.isExpanded ? 1.0 : 0.0) {
|
||||
if let blurFilter = CALayer.blur() {
|
||||
let maxBlur: CGFloat = 8.0
|
||||
if component.isExpanded {
|
||||
blurFilter.setValue(0.0 as NSNumber, forKey: "inputRadius")
|
||||
expandedTextView.layer.filters = [blurFilter]
|
||||
expandedTextView.layer.animate(from: maxBlur as NSNumber, to: 0.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak expandedTextView] flag in
|
||||
if flag, let expandedTextView {
|
||||
expandedTextView.layer.filters = nil
|
||||
}
|
||||
})
|
||||
} else {
|
||||
blurFilter.setValue(maxBlur as NSNumber, forKey: "inputRadius")
|
||||
expandedTextView.layer.filters = [blurFilter]
|
||||
expandedTextView.layer.animate(from: 0.0 as NSNumber, to: maxBlur as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, removeOnCompletion: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
transition.setPosition(view: expandedTextView, position: expandedTextFrame.origin)
|
||||
expandedTextView.bounds = CGRect(origin: CGPoint(), size: expandedTextFrame.size)
|
||||
alphaTransition.setAlpha(view: expandedTextView, alpha: component.isExpanded ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
let expandedButtonOffset: CGFloat = component.isExpanded ? 0.0 : (expandedButtonBottomInset + expandedButtonTextSize.height + expandedButtonTopInset)
|
||||
|
||||
if self.expandedSeparatorLayer.superlayer == nil {
|
||||
self.containerView.layer.addSublayer(self.expandedSeparatorLayer)
|
||||
}
|
||||
self.expandedSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
|
||||
transition.setFrame(layer: self.expandedSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: backgroundSize.height - expandedButtonBottomInset - expandedButtonTextSize.height - expandedButtonTopInset + expandedButtonOffset), size: CGSize(width: backgroundSize.width, height: UIScreenPixel)))
|
||||
alphaTransition.setAlpha(layer: self.expandedSeparatorLayer, alpha: component.isExpanded ? 1.0 : 0.0)
|
||||
|
||||
if let expandedButtonTextView = self.expandedButtonText.view {
|
||||
if expandedButtonTextView.superview == nil {
|
||||
self.containerView.addSubview(expandedButtonTextView)
|
||||
}
|
||||
transition.setFrame(view: expandedButtonTextView, frame: CGRect(origin: CGPoint(x: floor((backgroundSize.width - expandedButtonTextSize.width) * 0.5), y: backgroundSize.height - expandedButtonBottomInset - expandedButtonTextSize.height + expandedButtonOffset), size: expandedButtonTextSize))
|
||||
alphaTransition.setAlpha(view: expandedButtonTextView, alpha: component.isExpanded ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backgroundSize))
|
||||
|
||||
return CGSize(width: backgroundSize.width, height: collapsedSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user