Effect improvements

This commit is contained in:
Isaac 2024-05-20 18:51:35 +04:00
parent 6e28b2a4e2
commit 7c0216b907
18 changed files with 2939 additions and 2826 deletions

View File

@ -27,7 +27,7 @@ public final class AvatarVideoNode: ASDisplayNode {
private var fileDisposable = MetaDisposable()
private var animationFile: TelegramMediaFile?
private var itemLayer: EmojiPagerContentComponent.View.ItemLayer?
private var itemLayer: EmojiKeyboardItemLayer?
private var useAnimationNode = false
private var animationNode: AnimatedStickerNode?
private let stickerFetchedDisposable = MetaDisposable()
@ -101,7 +101,7 @@ public final class AvatarVideoNode: ASDisplayNode {
let itemNativeFitSize = self.internalSize.width > 100.0 ? CGSize(width: 192.0, height: 192.0) : CGSize(width: 64.0, height: 64.0)
let animationData = EntityKeyboardAnimationData(file: animationFile)
let itemLayer = EmojiPagerContentComponent.View.ItemLayer(
let itemLayer = EmojiKeyboardItemLayer(
item: EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),

View File

@ -179,6 +179,9 @@ final class ChatSendMessageContextScreenComponent: Component {
private var animateOutToEmpty: Bool = false
private var initializationDisplayLink: SharedDisplayLinkDriver.Link?
private var updateSourcePositionsDisplayLink: SharedDisplayLinkDriver.Link?
private var stableSourceSendButtonFrame: CGRect?
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
@ -273,6 +276,21 @@ final class ChatSendMessageContextScreenComponent: Component {
let environment = environment[EnvironmentType.self].value
if let previousEnvironment = self.environment, previousEnvironment.inputHeight != 0.0, environment.inputHeight == 0.0 {
DispatchQueue.main.async { [weak self] in
guard let self, let component = self.component else {
return
}
let stableSourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
if self.stableSourceSendButtonFrame != stableSourceSendButtonFrame {
self.stableSourceSendButtonFrame = stableSourceSendButtonFrame
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.35))
}
}
}
}
var transition = transition
var transitionIsImmediate = transition.animation.isImmediate
@ -365,7 +383,19 @@ final class ChatSendMessageContextScreenComponent: Component {
self.addSubview(sendButton)
}
let sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
let sourceSendButtonFrame: CGRect
switch self.presentationAnimationState {
case .animatedOut:
sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
self.stableSourceSendButtonFrame = sourceSendButtonFrame
default:
if let stableSourceSendButtonFrame = self.stableSourceSendButtonFrame {
sourceSendButtonFrame = stableSourceSendButtonFrame
} else {
sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
self.stableSourceSendButtonFrame = sourceSendButtonFrame
}
}
let sendButtonScale: CGFloat
switch self.presentationAnimationState {
@ -843,6 +873,8 @@ final class ChatSendMessageContextScreenComponent: Component {
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
self.endEditing(true)
})
}))
}
@ -1014,6 +1046,13 @@ final class ChatSendMessageContextScreenComponent: Component {
}
}
if let standaloneReactionAnimation, let targetView = messageItemView.effectIconView {
let effectSize = CGSize(width: 380.0, height: 380.0)
var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self))
effectFrame.origin.x -= effectFrame.width * 0.3
transition.setFrame(view: standaloneReactionAnimation.view, frame: effectFrame)
}
if let reactionContextNode = self.reactionContextNode {
let reactionContextY = environment.statusBarHeight
let size = availableSize

View File

@ -122,8 +122,8 @@ final class StickerPackEmojisItemNode: GridItemNode {
private var boundsChangeTrackerLayer = SimpleLayer()
private var visibleItemLayers: [EmojiPagerContentComponent.View.ItemLayer.Key: EmojiPagerContentComponent.View.ItemLayer] = [:]
private var visibleItemPlaceholderViews: [EmojiPagerContentComponent.View.ItemLayer.Key: EmojiPagerContentComponent.View.ItemPlaceholderView] = [:]
private var visibleItemLayers: [EmojiKeyboardItemLayer.Key: EmojiKeyboardItemLayer] = [:]
private var visibleItemPlaceholderViews: [EmojiKeyboardItemLayer.Key: EmojiPagerContentComponent.View.ItemPlaceholderView] = [:]
private let containerNode: ASDisplayNode
private let titleNode: ImmediateTextNode
@ -195,7 +195,7 @@ final class StickerPackEmojisItemNode: GridItemNode {
func targetItem(at point: CGPoint) -> (TelegramMediaFile, CALayer)? {
if let (item, _) = self.item(atPoint: point), let file = item.itemFile {
let itemId = EmojiPagerContentComponent.View.ItemLayer.Key(
let itemId = EmojiKeyboardItemLayer.Key(
groupId: 0,
itemId: .animation(.file(file.fileId))
)
@ -237,7 +237,7 @@ final class StickerPackEmojisItemNode: GridItemNode {
private func item(atPoint point: CGPoint, extendedHitRange: Bool = false) -> (EmojiPagerContentComponent.Item, CGRect)? {
let localPoint = point
var closestItem: (key: EmojiPagerContentComponent.View.ItemLayer.Key, distance: CGFloat)?
var closestItem: (key: EmojiKeyboardItemLayer.Key, distance: CGFloat)?
for (key, itemLayer) in self.visibleItemLayers {
if extendedHitRange {
@ -308,7 +308,7 @@ final class StickerPackEmojisItemNode: GridItemNode {
let animationRenderer = item.animationRenderer
let theme = item.theme
let items = item.items
var validIds = Set<EmojiPagerContentComponent.View.ItemLayer.Key>()
var validIds = Set<EmojiKeyboardItemLayer.Key>()
let itemLayout: ItemLayout
if let current = self.itemLayout, current.width == self.size.width && current.itemsCount == items.count {
@ -322,7 +322,7 @@ final class StickerPackEmojisItemNode: GridItemNode {
for index in 0 ..< items.count {
let item = items[index]
let itemId = EmojiPagerContentComponent.View.ItemLayer.Key(
let itemId = EmojiKeyboardItemLayer.Key(
groupId: 0,
itemId: .animation(.file(item.file.fileId))
)
@ -334,7 +334,7 @@ final class StickerPackEmojisItemNode: GridItemNode {
var updateItemLayerPlaceholder = false
var itemTransition = transition
let itemLayer: EmojiPagerContentComponent.View.ItemLayer
let itemLayer: EmojiKeyboardItemLayer
if let current = self.visibleItemLayers[itemId] {
itemLayer = current
} else {
@ -342,7 +342,7 @@ final class StickerPackEmojisItemNode: GridItemNode {
itemTransition = .immediate
let animationData = EntityKeyboardAnimationData(file: item.file)
itemLayer = EmojiPagerContentComponent.View.ItemLayer(
itemLayer = EmojiKeyboardItemLayer(
item: EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),

View File

@ -638,6 +638,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private var replyRecognizer: ChatSwipeToReplyRecognizer?
private var currentSwipeAction: ChatControllerInteractionSwipeAction?
private var fetchEffectDisposable: Disposable?
//private let debugNode: ASDisplayNode
override public var visibility: ListViewItemNodeVisibility {
@ -839,6 +841,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchEffectDisposable?.dispose()
}
override public func cancelInsertionAnimations() {
self.shadowNode.layer.removeAllAnimations()
@ -5877,6 +5883,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private var additionalAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = []
private func playPremiumStickerAnimation(effect: AvailableMessageEffects.MessageEffect, force: Bool) {
guard let item = self.item else {
return
}
if self.playedPremiumStickerAnimation && !force {
return
}
@ -5884,10 +5893,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let effectAnimation = effect.effectAnimation {
self.playEffectAnimation(resource: effectAnimation.resource, isStickerEffect: true)
if self.fetchEffectDisposable == nil {
self.fetchEffectDisposable = freeMediaFileResourceInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: .standalone(media: effectAnimation), resource: effectAnimation.resource).startStrict()
}
} else {
let effectSticker = effect.effectSticker
if let effectFile = effectSticker.videoThumbnails.first {
self.playEffectAnimation(resource: effectFile.resource, isStickerEffect: true)
if self.fetchEffectDisposable == nil {
self.fetchEffectDisposable = freeMediaFileResourceInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: .standalone(media: effectSticker), resource: effectFile.resource).startStrict()
}
}
}
}

View File

@ -0,0 +1,477 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import MultiAnimationRenderer
import AnimationCache
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramPresentationData
import EmojiTextAttachmentView
import EmojiStatusComponent
final class EmojiKeyboardCloneItemLayer: SimpleLayer {
}
public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget {
public struct Key: Hashable {
var groupId: AnyHashable
var itemId: EmojiPagerContentComponent.ItemContent.Id
public init(
groupId: AnyHashable,
itemId: EmojiPagerContentComponent.ItemContent.Id
) {
self.groupId = groupId
self.itemId = itemId
}
}
enum Badge: Equatable {
case premium
case locked
case featured
case text(String)
case customFile(TelegramMediaFile)
}
public let item: EmojiPagerContentComponent.Item
private let context: AccountContext
private var content: EmojiPagerContentComponent.ItemContent
private var theme: PresentationTheme?
private let placeholderColor: UIColor
let pixelSize: CGSize
let pointSize: CGSize
private let size: CGSize
private var disposable: Disposable?
private var fetchDisposable: Disposable?
private var premiumBadgeView: PremiumBadgeView?
private var iconLayer: SimpleLayer?
private var tintIconLayer: SimpleLayer?
private(set) var tintContentLayer: SimpleLayer?
private var badge: Badge?
private var validSize: CGSize?
private var isInHierarchyValue: Bool = false
public var isVisibleForAnimations: Bool = false {
didSet {
if self.isVisibleForAnimations != oldValue {
self.updatePlayback()
}
}
}
public private(set) var displayPlaceholder: Bool = false
public let onUpdateDisplayPlaceholder: (Bool, Double) -> Void
weak var cloneLayer: EmojiKeyboardCloneItemLayer? {
didSet {
if let cloneLayer = self.cloneLayer {
cloneLayer.contents = self.contents
}
}
}
override public var contents: Any? {
didSet {
self.onContentsUpdate()
if let cloneLayer = self.cloneLayer {
cloneLayer.contents = self.contents
}
}
}
override public var position: CGPoint {
get {
return super.position
} set(value) {
if let mirrorLayer = self.tintContentLayer {
mirrorLayer.position = value
}
super.position = value
}
}
override public var bounds: CGRect {
get {
return super.bounds
} set(value) {
if let mirrorLayer = self.tintContentLayer {
mirrorLayer.bounds = value
}
super.bounds = value
}
}
override public func add(_ animation: CAAnimation, forKey key: String?) {
if let mirrorLayer = self.tintContentLayer {
mirrorLayer.add(animation, forKey: key)
}
super.add(animation, forKey: key)
}
override public func removeAllAnimations() {
if let mirrorLayer = self.tintContentLayer {
mirrorLayer.removeAllAnimations()
}
super.removeAllAnimations()
}
override public func removeAnimation(forKey: String) {
if let mirrorLayer = self.tintContentLayer {
mirrorLayer.removeAnimation(forKey: forKey)
}
super.removeAnimation(forKey: forKey)
}
public var onContentsUpdate: () -> Void = {}
public var onLoop: () -> Void = {}
public init(
item: EmojiPagerContentComponent.Item,
context: AccountContext,
attemptSynchronousLoad: Bool,
content: EmojiPagerContentComponent.ItemContent,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
placeholderColor: UIColor,
blurredBadgeColor: UIColor,
accentIconColor: UIColor,
pointSize: CGSize,
onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void
) {
self.item = item
self.context = context
self.content = content
self.placeholderColor = placeholderColor
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
let scale = min(2.0, UIScreenScale)
let pixelSize = CGSize(width: pointSize.width * scale, height: pointSize.height * scale)
self.pixelSize = pixelSize
self.pointSize = pointSize
self.size = CGSize(width: pixelSize.width / scale, height: pixelSize.height / scale)
super.init()
switch content {
case let .animation(animationData):
let loadAnimation: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, unique: false, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: pixelSize.width >= 120.0, customColor: animationData.isTemplate ? .white : nil))
}
if attemptSynchronousLoad {
if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize) {
self.updateDisplayPlaceholder(displayPlaceholder: true)
self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true, customColor: animationData.isTemplate ? .white : nil), completion: { [weak self] success, isFinal in
if !isFinal {
if !success {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
return
}
Queue.mainQueue().async {
loadAnimation()
if !success {
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
})
} else {
loadAnimation()
}
} else {
self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true, customColor: animationData.isTemplate ? .white : nil), completion: { [weak self] success, isFinal in
if !isFinal {
if !success {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
return
}
Queue.mainQueue().async {
loadAnimation()
if !success {
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
})
}
case let .staticEmoji(staticEmoji):
let image = generateImage(pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let preScaleFactor: CGFloat = 1.0
let scaledSize = CGSize(width: floor(size.width * preScaleFactor), height: floor(size.height * preScaleFactor))
let scaleFactor = scaledSize.width / size.width
context.scaleBy(x: 1.0 / scaleFactor, y: 1.0 / scaleFactor)
let string = NSAttributedString(string: staticEmoji, font: Font.regular(floor(32.0 * scaleFactor)), textColor: .black)
let boundingRect = string.boundingRect(with: scaledSize, options: .usesLineFragmentOrigin, context: nil)
UIGraphicsPushContext(context)
string.draw(at: CGPoint(x: floorToScreenPixels((scaledSize.width - boundingRect.width) / 2.0 + boundingRect.minX), y: floorToScreenPixels((scaledSize.height - boundingRect.height) / 2.0 + boundingRect.minY)))
UIGraphicsPopContext()
})
self.contents = image?.cgImage
case let .icon(icon):
let image = generateImage(pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
switch icon {
case .premiumStar:
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: accentIconColor) {
let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0))
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
}
case let .topic(title, color):
let colors = topicIconColors(for: color)
if let image = generateTopicIcon(backgroundColors: colors.0.map { UIColor(rgb: $0) }, strokeColors: colors.1.map { UIColor(rgb: $0) }, title: title) {
let imageSize = image.size//.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0))
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
}
case .stop:
if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/NoIcon"), color: .white) {
let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0))
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
}
case .add:
break
}
UIGraphicsPopContext()
})?.withRenderingMode(icon == .stop ? .alwaysTemplate : .alwaysOriginal)
self.contents = image?.cgImage
}
if case .icon(.add) = content {
let tintContentLayer = SimpleLayer()
self.tintContentLayer = tintContentLayer
let iconLayer = SimpleLayer()
self.iconLayer = iconLayer
self.addSublayer(iconLayer)
let tintIconLayer = SimpleLayer()
self.tintIconLayer = tintIconLayer
tintContentLayer.addSublayer(tintIconLayer)
}
}
override public init(layer: Any) {
guard let layer = layer as? EmojiKeyboardItemLayer else {
preconditionFailure()
}
self.context = layer.context
self.item = layer.item
self.content = layer.content
self.placeholderColor = layer.placeholderColor
self.size = layer.size
self.pixelSize = layer.pixelSize
self.pointSize = layer.pointSize
self.onUpdateDisplayPlaceholder = { _, _ in }
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable?.dispose()
self.fetchDisposable?.dispose()
}
public override func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchyValue = true
} else if event == kCAOnOrderOut {
self.isInHierarchyValue = false
}
self.updatePlayback()
return nullAction
}
func update(
content: EmojiPagerContentComponent.ItemContent,
theme: PresentationTheme
) {
var themeUpdated = false
if self.theme !== theme {
self.theme = theme
themeUpdated = true
}
var contentUpdated = false
if self.content != content {
self.content = content
contentUpdated = true
}
if themeUpdated || contentUpdated {
if case let .icon(icon) = content, case let .topic(title, color) = icon {
let image = generateImage(self.size, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
let colors = topicIconColors(for: color)
if let image = generateTopicIcon(backgroundColors: colors.0.map { UIColor(rgb: $0) }, strokeColors: colors.1.map { UIColor(rgb: $0) }, title: title) {
let imageSize = image.size
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
}
UIGraphicsPopContext()
})
self.contents = image?.cgImage
} else if case .icon(.add) = content {
guard let iconLayer = self.iconLayer, let tintIconLayer = self.tintIconLayer else {
return
}
func generateIcon(color: UIColor) -> UIImage? {
return generateImage(self.pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
context.setFillColor(color.withMultipliedAlpha(0.2).cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 8.0, dy: 8.0))
context.setFillColor(color.cgColor)
let plusSize = CGSize(width: 4.5, height: 31.5)
context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height), cornerRadius: plusSize.width / 2.0).cgPath)
context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width), cornerRadius: plusSize.width / 2.0).cgPath)
context.fillPath()
UIGraphicsPopContext()
})
}
let needsVibrancy = !theme.overallDarkAppearance
let color = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
iconLayer.contents = generateIcon(color: color)?.cgImage
tintIconLayer.contents = generateIcon(color: .white)?.cgImage
tintIconLayer.isHidden = !needsVibrancy
}
}
}
func update(
transition: Transition,
size: CGSize,
badge: Badge?,
blurredBadgeColor: UIColor,
blurredBadgeBackgroundColor: UIColor
) {
if self.badge != badge || self.validSize != size {
self.badge = badge
self.validSize = size
if let iconLayer = self.iconLayer, let tintIconLayer = self.tintIconLayer {
transition.setFrame(layer: iconLayer, frame: CGRect(origin: .zero, size: size))
transition.setFrame(layer: tintIconLayer, frame: CGRect(origin: .zero, size: size))
}
if let badge = badge {
var badgeTransition = transition
let premiumBadgeView: PremiumBadgeView
if let current = self.premiumBadgeView {
premiumBadgeView = current
} else {
badgeTransition = .immediate
premiumBadgeView = PremiumBadgeView(context: self.context)
self.premiumBadgeView = premiumBadgeView
self.addSublayer(premiumBadgeView.layer)
}
let badgeDiameter = min(16.0, floor(size.height * 0.5))
let badgeSize = CGSize(width: badgeDiameter, height: badgeDiameter)
badgeTransition.setFrame(view: premiumBadgeView, frame: CGRect(origin: CGPoint(x: size.width - badgeSize.width, y: size.height - badgeSize.height), size: badgeSize))
premiumBadgeView.update(transition: badgeTransition, badge: badge, backgroundColor: blurredBadgeColor, size: badgeSize)
self.blurredRepresentationBackgroundColor = blurredBadgeBackgroundColor
self.blurredRepresentationTarget = premiumBadgeView.contentLayer
} else {
if let premiumBadgeView = self.premiumBadgeView {
self.premiumBadgeView = nil
premiumBadgeView.removeFromSuperview()
self.blurredRepresentationBackgroundColor = nil
self.blurredRepresentationTarget = nil
}
}
}
}
private func updatePlayback() {
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations
self.shouldBeAnimating = shouldBePlaying
}
public override func updateDisplayPlaceholder(displayPlaceholder: Bool) {
if self.displayPlaceholder == displayPlaceholder {
return
}
self.displayPlaceholder = displayPlaceholder
self.onUpdateDisplayPlaceholder(displayPlaceholder, 0.0)
}
public override func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
self.contents = contents
if self.displayPlaceholder {
self.displayPlaceholder = false
self.onUpdateDisplayPlaceholder(false, 0.2)
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
}
if didLoop {
self.onLoop()
}
}
}

View File

@ -0,0 +1,607 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
import TelegramPresentationData
import AccountContext
import SwiftSignalKit
public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
private final class EmojiSearchTextField: UITextField {
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.integral
}
}
private struct Params: Equatable {
var context: AccountContext
var theme: PresentationTheme
var forceNeedsVibrancy: Bool
var strings: PresentationStrings
var text: String
var useOpaqueTheme: Bool
var isActive: Bool
var hasPresetSearch: Bool
var textInputState: EmojiSearchSearchBarComponent.TextInputState
var searchState: EmojiPagerContentComponent.SearchState
var size: CGSize
var canFocus: Bool
var searchCategories: EmojiSearchCategories?
static func ==(lhs: Params, rhs: Params) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.useOpaqueTheme != rhs.useOpaqueTheme {
return false
}
if lhs.isActive != rhs.isActive {
return false
}
if lhs.hasPresetSearch != rhs.hasPresetSearch {
return false
}
if lhs.textInputState != rhs.textInputState {
return false
}
if lhs.searchState != rhs.searchState {
return false
}
if lhs.size != rhs.size {
return false
}
if lhs.canFocus != rhs.canFocus {
return false
}
if lhs.searchCategories != rhs.searchCategories {
return false
}
return true
}
}
override public static var layerClass: AnyClass {
return PassthroughLayer.self
}
private let activated: (Bool) -> Void
private let deactivated: (Bool) -> Void
private let updateQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void
let tintContainerView: UIView
private let backgroundLayer: SimpleLayer
private let tintBackgroundLayer: SimpleLayer
private let statusIcon = ComponentView<Empty>()
private let clearIconView: UIImageView
private let clearIconTintView: UIImageView
private let clearIconButton: HighlightTrackingButton
private let cancelButtonTintTitle: ComponentView<Empty>
private let cancelButtonTitle: ComponentView<Empty>
private let cancelButton: HighlightTrackingButton
private var placeholderContent = ComponentView<Empty>()
private var textFrame: CGRect?
private var textField: EmojiSearchTextField?
private var tapRecognizer: UITapGestureRecognizer?
private(set) var currentPresetSearchTerm: EmojiSearchCategories.Group?
private var params: Params?
public var wantsDisplayBelowKeyboard: Bool {
return self.textField != nil
}
init(activated: @escaping (Bool) -> Void, deactivated: @escaping (Bool) -> Void, updateQuery: @escaping (EmojiPagerContentComponent.SearchQuery?) -> Void) {
self.activated = activated
self.deactivated = deactivated
self.updateQuery = updateQuery
self.tintContainerView = UIView()
self.backgroundLayer = SimpleLayer()
self.tintBackgroundLayer = SimpleLayer()
self.clearIconView = UIImageView()
self.clearIconTintView = UIImageView()
self.clearIconButton = HighlightableButton()
self.clearIconView.isHidden = true
self.clearIconTintView.isHidden = true
self.clearIconButton.isHidden = true
self.cancelButtonTintTitle = ComponentView()
self.cancelButtonTitle = ComponentView()
self.cancelButton = HighlightTrackingButton()
super.init(frame: CGRect())
self.layer.addSublayer(self.backgroundLayer)
self.tintContainerView.layer.addSublayer(self.tintBackgroundLayer)
self.addSubview(self.clearIconView)
self.tintContainerView.addSubview(self.clearIconTintView)
self.addSubview(self.clearIconButton)
self.addSubview(self.cancelButton)
self.clipsToBounds = true
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerView.layer
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.tapRecognizer = tapRecognizer
self.addGestureRecognizer(tapRecognizer)
self.cancelButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view {
cancelButtonTitleView.layer.removeAnimation(forKey: "opacity")
cancelButtonTitleView.alpha = 0.4
}
if let cancelButtonTintTitleView = strongSelf.cancelButtonTintTitle.view {
cancelButtonTintTitleView.layer.removeAnimation(forKey: "opacity")
cancelButtonTintTitleView.alpha = 0.4
}
} else {
if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view {
cancelButtonTitleView.alpha = 1.0
cancelButtonTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
if let cancelButtonTintTitleView = strongSelf.cancelButtonTintTitle.view {
cancelButtonTintTitleView.alpha = 1.0
cancelButtonTintTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), for: .touchUpInside)
self.clearIconButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.clearIconView.layer.removeAnimation(forKey: "opacity")
strongSelf.clearIconView.alpha = 0.4
strongSelf.clearIconTintView.layer.removeAnimation(forKey: "opacity")
strongSelf.clearIconTintView.alpha = 0.4
} else {
strongSelf.clearIconView.alpha = 1.0
strongSelf.clearIconView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.clearIconTintView.alpha = 1.0
strongSelf.clearIconTintView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.clearIconButton.addTarget(self, action: #selector(self.clearPressed), for: .touchUpInside)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let location = recognizer.location(in: self)
if let view = self.statusIcon.view, view.frame.contains(location), self.currentPresetSearchTerm != nil {
self.clearCategorySearch()
} else {
self.activateTextInput()
}
}
}
func clearCategorySearch() {
if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
placeholderContentView.clearSelection(dispatchEvent : true)
}
}
private func activateTextInput() {
guard let params = self.params else {
return
}
if self.textField == nil, let textFrame = self.textFrame, params.canFocus == true {
let backgroundFrame = self.backgroundLayer.frame
let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height))
let textField = EmojiSearchTextField(frame: textFieldFrame)
textField.keyboardAppearance = params.theme.rootController.keyboardColor.keyboardAppearance
textField.autocorrectionType = .no
textField.returnKeyType = .search
self.textField = textField
self.insertSubview(textField, belowSubview: self.clearIconView)
textField.delegate = self
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
}
if params.canFocus {
self.currentPresetSearchTerm = nil
if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
placeholderContentView.clearSelection(dispatchEvent: false)
}
}
self.activated(true)
self.textField?.becomeFirstResponder()
}
@objc private func cancelPressed() {
self.currentPresetSearchTerm = nil
self.updateQuery(nil)
self.clearIconView.isHidden = true
self.clearIconTintView.isHidden = true
self.clearIconButton.isHidden = true
let textField = self.textField
self.textField = nil
self.deactivated(textField?.isFirstResponder ?? false)
if let textField {
textField.resignFirstResponder()
textField.removeFromSuperview()
}
/*self.tintTextView.view?.isHidden = false
self.textView.view?.isHidden = false*/
}
@objc private func clearPressed() {
self.currentPresetSearchTerm = nil
self.updateQuery(nil)
self.textField?.text = ""
self.clearIconView.isHidden = true
self.clearIconTintView.isHidden = true
self.clearIconButton.isHidden = true
/*self.tintTextView.view?.isHidden = false
self.textView.view?.isHidden = false*/
}
var isActive: Bool {
return self.textField?.isFirstResponder ?? false
}
func deactivate() {
if let text = self.textField?.text, !text.isEmpty {
self.textField?.endEditing(true)
} else {
self.cancelPressed()
}
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
}
public func textFieldDidEndEditing(_ textField: UITextField) {
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.endEditing(true)
return false
}
@objc private func textFieldChanged(_ textField: UITextField) {
self.update(transition: .immediate)
let text = textField.text ?? ""
var inputLanguage = textField.textInputMode?.primaryLanguage ?? "en"
if let range = inputLanguage.range(of: "-") {
inputLanguage = String(inputLanguage[inputLanguage.startIndex ..< range.lowerBound])
}
if let range = inputLanguage.range(of: "_") {
inputLanguage = String(inputLanguage[inputLanguage.startIndex ..< range.lowerBound])
}
self.clearIconView.isHidden = text.isEmpty
self.clearIconTintView.isHidden = text.isEmpty
self.clearIconButton.isHidden = text.isEmpty
self.currentPresetSearchTerm = nil
self.updateQuery(.text(value: text, language: inputLanguage))
}
private func update(transition: Transition) {
guard let params = self.params else {
return
}
self.params = nil
self.update(context: params.context, theme: params.theme, forceNeedsVibrancy: params.forceNeedsVibrancy, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, searchCategories: params.searchCategories, searchState: params.searchState, transition: transition)
}
public func update(context: AccountContext, theme: PresentationTheme, forceNeedsVibrancy: Bool, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, searchState: EmojiPagerContentComponent.SearchState, transition: Transition) {
let textInputState: EmojiSearchSearchBarComponent.TextInputState
if let textField = self.textField {
textInputState = .active(hasText: !(textField.text ?? "").isEmpty)
} else {
textInputState = .inactive
}
let params = Params(
context: context,
theme: theme,
forceNeedsVibrancy: forceNeedsVibrancy,
strings: strings,
text: text,
useOpaqueTheme: useOpaqueTheme,
isActive: isActive,
hasPresetSearch: self.currentPresetSearchTerm == nil,
textInputState: textInputState,
searchState: searchState,
size: size,
canFocus: canFocus,
searchCategories: searchCategories
)
if self.params == params {
return
}
let isActiveWithText = isActive && self.currentPresetSearchTerm == nil
if self.params?.theme !== theme {
/*self.searchIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.searchIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
self.searchIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)
self.backIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.backIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
self.backIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white)*/
self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.clearIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
self.clearIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)
}
self.params = params
let sideInset: CGFloat = 12.0
let topInset: CGFloat = 8.0
let inputHeight: CGFloat = 36.0
let sideTextInset: CGFloat = sideInset + 4.0 + 24.0
if theme.overallDarkAppearance && forceNeedsVibrancy {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.withMultipliedAlpha(0.3).cgColor
self.tintBackgroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.2).cgColor
} else if useOpaqueTheme {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor
self.tintBackgroundLayer.backgroundColor = UIColor.white.cgColor
} else {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.cgColor
self.tintBackgroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.2).cgColor
}
self.backgroundLayer.cornerRadius = inputHeight * 0.5
self.tintBackgroundLayer.cornerRadius = inputHeight * 0.5
let cancelColor: UIColor
if theme.overallDarkAppearance && forceNeedsVibrancy {
cancelColor = theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3)
} else {
cancelColor = useOpaqueTheme ? theme.list.itemAccentColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
}
let cancelTextSize = self.cancelButtonTitle.update(
transition: .immediate,
component: AnyComponent(Text(
text: strings.Common_Cancel,
font: Font.regular(17.0),
color: cancelColor
)),
environment: {},
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
)
let _ = self.cancelButtonTintTitle.update(
transition: .immediate,
component: AnyComponent(Text(
text: strings.Common_Cancel,
font: Font.regular(17.0),
color: .white
)),
environment: {},
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
)
let cancelButtonSpacing: CGFloat = 8.0
var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight))
if isActiveWithText {
backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing
}
transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame)
transition.setFrame(layer: self.tintBackgroundLayer, frame: backgroundFrame)
transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height)))
let textX: CGFloat = backgroundFrame.minX + sideTextInset
let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height))
self.textFrame = textFrame
let statusContent: EmojiSearchStatusComponent.Content
switch searchState {
case .empty:
statusContent = .search
case .searching:
statusContent = .progress
case .active:
statusContent = .results
}
let statusSize = CGSize(width: 24.0, height: 24.0)
let _ = self.statusIcon.update(
transition: transition,
component: AnyComponent(EmojiSearchStatusComponent(
theme: theme,
forceNeedsVibrancy: forceNeedsVibrancy,
strings: strings,
useOpaqueTheme: useOpaqueTheme,
content: statusContent
)),
environment: {},
containerSize: statusSize
)
let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - statusSize.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - statusSize.height) / 2.0)), size: statusSize)
if let statusIconView = self.statusIcon.view as? EmojiSearchStatusComponent.View {
if statusIconView.superview == nil {
self.addSubview(statusIconView)
self.tintContainerView.addSubview(statusIconView.tintContainerView)
}
transition.setFrame(view: statusIconView, frame: iconFrame)
transition.setFrame(view: statusIconView.tintContainerView, frame: iconFrame)
}
let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX - 6.0, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - (textFrame.minX - 6.0), height: backgroundFrame.height))
let _ = self.placeholderContent.update(
transition: transition,
component: AnyComponent(EmojiSearchSearchBarComponent(
context: context,
theme: theme,
forceNeedsVibrancy: forceNeedsVibrancy,
strings: strings,
useOpaqueTheme: useOpaqueTheme,
textInputState: textInputState,
categories: searchCategories,
searchTermUpdated: { [weak self] term in
guard let self else {
return
}
var shouldChangeActivation = false
if (self.currentPresetSearchTerm == nil) != (term == nil) {
shouldChangeActivation = true
}
self.currentPresetSearchTerm = term
if shouldChangeActivation {
if let term {
self.update(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
self.updateQuery(.category(value: term))
self.activated(false)
} else {
self.deactivated(self.textField?.isFirstResponder ?? false)
self.updateQuery(nil)
}
} else {
if let term {
self.updateQuery(.category(value: term))
} else {
self.updateQuery(nil)
}
}
},
activateTextInput: { [weak self] in
guard let self else {
return
}
self.activateTextInput()
}
)),
environment: {},
containerSize: placeholderContentFrame.size
)
if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
if placeholderContentView.superview == nil {
self.addSubview(placeholderContentView)
self.tintContainerView.addSubview(placeholderContentView.tintContainerView)
}
transition.setFrame(view: placeholderContentView, frame: placeholderContentFrame)
transition.setFrame(view: placeholderContentView.tintContainerView, frame: placeholderContentFrame)
}
/*if let searchCategories {
let suggestedItemsView: ComponentView<Empty>
var suggestedItemsTransition = transition
if let current = self.suggestedItemsView {
suggestedItemsView = current
} else {
suggestedItemsTransition = .immediate
suggestedItemsView = ComponentView()
self.suggestedItemsView = suggestedItemsView
}
let itemsX: CGFloat = textFrame.maxX + 8.0
let suggestedItemsFrame = CGRect(origin: CGPoint(x: itemsX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - itemsX, height: backgroundFrame.height))
if let suggestedItemsComponentView = suggestedItemsView.view {
if suggestedItemsComponentView.superview == nil {
self.addSubview(suggestedItemsComponentView)
}
suggestedItemsTransition.setFrame(view: suggestedItemsComponentView, frame: suggestedItemsFrame)
suggestedItemsTransition.setAlpha(view: suggestedItemsComponentView, alpha: isActiveWithText ? 0.0 : 1.0)
}
} else {
if let suggestedItemsView = self.suggestedItemsView {
self.suggestedItemsView = nil
if let suggestedItemsComponentView = suggestedItemsView.view {
transition.setAlpha(view: suggestedItemsComponentView, alpha: 0.0, completion: { [weak suggestedItemsComponentView] _ in
suggestedItemsComponentView?.removeFromSuperview()
})
}
}
}*/
if let image = self.clearIconView.image {
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
transition.setFrame(view: self.clearIconView, frame: iconFrame)
transition.setFrame(view: self.clearIconTintView, frame: iconFrame)
transition.setFrame(view: self.clearIconButton, frame: iconFrame.insetBy(dx: -8.0, dy: -10.0))
}
if let cancelButtonTitleComponentView = self.cancelButtonTitle.view {
if cancelButtonTitleComponentView.superview == nil {
self.addSubview(cancelButtonTitleComponentView)
cancelButtonTitleComponentView.isUserInteractionEnabled = false
}
transition.setFrame(view: cancelButtonTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize))
transition.setAlpha(view: cancelButtonTitleComponentView, alpha: isActiveWithText ? 1.0 : 0.0)
}
if let cancelButtonTintTitleComponentView = self.cancelButtonTintTitle.view {
if cancelButtonTintTitleComponentView.superview == nil {
self.tintContainerView.addSubview(cancelButtonTintTitleComponentView)
cancelButtonTintTitleComponentView.isUserInteractionEnabled = false
}
transition.setFrame(view: cancelButtonTintTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize))
transition.setAlpha(view: cancelButtonTintTitleComponentView, alpha: isActiveWithText ? 1.0 : 0.0)
}
var hasText = false
if let textField = self.textField {
textField.textColor = theme.contextMenu.primaryColor
transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideTextInset, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width - sideTextInset - 32.0, height: backgroundFrame.height)))
if let text = textField.text, !text.isEmpty {
hasText = true
}
}
let _ = hasText
/*self.tintTextView.view?.isHidden = hasText
self.textView.view?.isHidden = hasText*/
}
}

View File

@ -420,6 +420,15 @@ final class EmojiSearchSearchBarComponent: Component {
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let component = self.component else {
return nil
}
let _ = component
return super.hitTest(point, with: event)
}
private func updateScrolling(transition: Transition, fromScrolling: Bool) {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
@ -427,8 +436,12 @@ final class EmojiSearchSearchBarComponent: Component {
let itemAlpha: CGFloat
switch component.textInputState {
case .active:
case let .active(hasText):
if hasText {
itemAlpha = 0.0
} else {
itemAlpha = 1.0
}
case .inactive:
itemAlpha = 1.0
}
@ -674,7 +687,7 @@ final class EmojiSearchSearchBarComponent: Component {
if self.scrollView.bounds.size != availableSize {
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
if case .active = component.textInputState {
if case .active(true) = component.textInputState {
transition.setBoundsOrigin(view: self.scrollView, origin: CGPoint())
}
if self.scrollView.contentSize != itemLayout.contentSize {

View File

@ -0,0 +1,101 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AccountContext
import TelegramCore
import TelegramPresentationData
import EmojiStatusComponent
final class EmptySearchResultsView: UIView {
override public static var layerClass: AnyClass {
return PassthroughLayer.self
}
let tintContainerView: UIView
let titleLabel: ComponentView<Empty>
let titleTintLabel: ComponentView<Empty>
let icon: ComponentView<Empty>
override init(frame: CGRect) {
self.tintContainerView = UIView()
self.titleLabel = ComponentView()
self.titleTintLabel = ComponentView()
self.icon = ComponentView()
super.init(frame: frame)
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerView.layer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(context: AccountContext, theme: PresentationTheme, useOpaqueTheme: Bool, text: String, file: TelegramMediaFile?, size: CGSize, searchInitiallyHidden: Bool, transition: Transition) {
let titleColor: UIColor
if useOpaqueTheme {
titleColor = theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor
} else {
titleColor = theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
}
let iconSize: CGSize
if let file = file {
iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
content: .animation(content: .file(file: file), size: CGSize(width: 32.0, height: 32.0), placeholderColor: titleColor, themeColor: nil, loopMode: .forever),
isVisibleForAnimations: context.sharedContext.energyUsageSettings.loopEmoji,
action: nil
)),
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)
} else {
iconSize = CGSize()
}
let titleSize = self.titleLabel.update(
transition: .immediate,
component: AnyComponent(Text(text: text, font: Font.regular(15.0), color: titleColor)),
environment: {},
containerSize: CGSize(width: size.width, height: 100.0)
)
let _ = self.titleTintLabel.update(
transition: .immediate,
component: AnyComponent(Text(text: text, font: Font.regular(15.0), color: .white)),
environment: {},
containerSize: CGSize(width: size.width, height: 100.0)
)
let spacing: CGFloat = 4.0
let contentHeight = iconSize.height + spacing + titleSize.height
let contentOriginY = searchInitiallyHidden ? floor((size.height - contentHeight) / 2.0) : 10.0
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: contentOriginY), size: iconSize)
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: iconFrame.maxY + spacing), size: titleSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
}
if let titleLabelView = self.titleLabel.view {
if titleLabelView.superview == nil {
self.addSubview(titleLabelView)
}
transition.setFrame(view: titleLabelView, frame: titleFrame)
}
if let titleTintLabelView = self.titleTintLabel.view {
if titleTintLabelView.superview == nil {
self.tintContainerView.addSubview(titleTintLabelView)
}
transition.setFrame(view: titleTintLabelView, frame: titleFrame)
}
}
}

View File

@ -85,7 +85,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
}
final class View: UIView {
var itemLayer: EmojiPagerContentComponent.View.ItemLayer?
var itemLayer: EmojiKeyboardItemLayer?
var placeholderView: EmojiPagerContentComponent.View.ItemPlaceholderView?
var component: EntityKeyboardAnimationTopPanelComponent?
var titleView: ComponentView<Empty>?
@ -116,7 +116,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
if self.itemLayer == nil {
let tintColor: EmojiPagerContentComponent.Item.TintMode = component.customTintColor.flatMap { .custom($0) } ?? .primary
let itemLayer = EmojiPagerContentComponent.View.ItemLayer(
let itemLayer = EmojiKeyboardItemLayer(
item: EmojiPagerContentComponent.Item(
animationData: component.item,
content: .animation(component.item),
@ -157,7 +157,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
transition.setPosition(layer: itemLayer, position: CGPoint(x: iconFrame.midX, y: iconFrame.midY))
transition.setBounds(layer: itemLayer, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
var badge: EmojiPagerContentComponent.View.ItemLayer.Badge?
var badge: EmojiKeyboardItemLayer.Badge?
if component.isPremiumLocked {
badge = .locked
} else if component.isFeatured {

View File

@ -0,0 +1,209 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AccountContext
import TelegramPresentationData
import AnimationCache
import MultiAnimationRenderer
import PagerComponent
final class GroupEmbeddedView: UIScrollView, UIScrollViewDelegate, PagerExpandableScrollView {
private struct ItemLayout {
var itemSize: CGFloat
var itemSpacing: CGFloat
var sideInset: CGFloat
var itemCount: Int
var contentSize: CGSize
init(height: CGFloat, sideInset: CGFloat, itemCount: Int) {
self.itemSize = 30.0
self.itemSpacing = 20.0
self.sideInset = sideInset
self.itemCount = itemCount
self.contentSize = CGSize(width: self.sideInset * 2.0 + CGFloat(self.itemCount) * self.itemSize + CGFloat(self.itemCount - 1) * self.itemSpacing, height: height)
}
func frame(at index: Int) -> CGRect {
return CGRect(origin: CGPoint(x: sideInset + CGFloat(index) * (self.itemSize + self.itemSpacing), y: floor((self.contentSize.height - self.itemSize) / 2.0)), size: CGSize(width: self.itemSize, height: self.itemSize))
}
func visibleItems(for rect: CGRect) -> Range<Int>? {
let offsetRect = rect.offsetBy(dx: -self.sideInset, dy: 0.0)
var minVisibleIndex = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize + self.itemSpacing)))
minVisibleIndex = max(0, minVisibleIndex)
var maxVisibleIndex = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize + self.itemSpacing)))
maxVisibleIndex = min(maxVisibleIndex, self.itemCount - 1)
if minVisibleIndex <= maxVisibleIndex {
return minVisibleIndex ..< (maxVisibleIndex + 1)
} else {
return nil
}
}
}
private let performItemAction: (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void
private var visibleItemLayers: [EmojiKeyboardItemLayer.Key: EmojiKeyboardItemLayer] = [:]
private var ignoreScrolling: Bool = false
private var context: AccountContext?
private var theme: PresentationTheme?
private var cache: AnimationCache?
private var renderer: MultiAnimationRenderer?
private var currentInsets: UIEdgeInsets?
private var currentSize: CGSize?
private var items: [EmojiPagerContentComponent.Item]?
private var isStickers: Bool = false
private var itemLayout: ItemLayout?
init(performItemAction: @escaping (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void) {
self.performItemAction = performItemAction
super.init(frame: CGRect())
self.delaysContentTouches = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.automaticallyAdjustsScrollIndicatorInsets = false
}
self.showsVerticalScrollIndicator = true
self.showsHorizontalScrollIndicator = false
self.delegate = self
self.clipsToBounds = true
self.scrollsToTop = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func tapGesture(point: CGPoint) -> Bool {
guard let itemLayout = self.itemLayout else {
return false
}
for (_, itemLayer) in self.visibleItemLayers {
if itemLayer.frame.inset(by: UIEdgeInsets(top: -6.0, left: -itemLayout.itemSpacing, bottom: -6.0, right: -itemLayout.itemSpacing)).contains(point) {
self.performItemAction(itemLayer.item, self, itemLayer.frame, itemLayer)
return true
}
}
return false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateVisibleItems(transition: .immediate, attemptSynchronousLoad: false)
}
}
private func updateVisibleItems(transition: Transition, attemptSynchronousLoad: Bool) {
guard let context = self.context, let theme = self.theme, let itemLayout = self.itemLayout, let items = self.items, let cache = self.cache, let renderer = self.renderer else {
return
}
var validIds = Set<EmojiKeyboardItemLayer.Key>()
if let itemRange = itemLayout.visibleItems(for: self.bounds) {
for index in itemRange.lowerBound ..< itemRange.upperBound {
let item = items[index]
let itemId = EmojiKeyboardItemLayer.Key(
groupId: AnyHashable(0),
itemId: item.content.id
)
validIds.insert(itemId)
let itemLayer: EmojiKeyboardItemLayer
if let current = self.visibleItemLayers[itemId] {
itemLayer = current
} else {
itemLayer = EmojiKeyboardItemLayer(
item: item,
context: context,
attemptSynchronousLoad: attemptSynchronousLoad,
content: item.content,
cache: cache,
renderer: renderer,
placeholderColor: .clear,
blurredBadgeColor: .clear,
accentIconColor: theme.list.itemAccentColor,
pointSize: CGSize(width: 32.0, height: 32.0),
onUpdateDisplayPlaceholder: { _, _ in
}
)
self.visibleItemLayers[itemId] = itemLayer
self.layer.addSublayer(itemLayer)
}
switch item.tintMode {
case let .custom(color):
itemLayer.layerTintColor = color.cgColor
case .accent:
itemLayer.layerTintColor = theme.list.itemAccentColor.cgColor
case .primary:
itemLayer.layerTintColor = theme.list.itemPrimaryTextColor.cgColor
case .none:
itemLayer.layerTintColor = nil
}
let itemFrame = itemLayout.frame(at: index)
itemLayer.frame = itemFrame
itemLayer.isVisibleForAnimations = self.isStickers ? context.sharedContext.energyUsageSettings.loopStickers : context.sharedContext.energyUsageSettings.loopEmoji
}
}
var removedIds: [EmojiKeyboardItemLayer.Key] = []
for (id, itemLayer) in self.visibleItemLayers {
if !validIds.contains(id) {
removedIds.append(id)
itemLayer.removeFromSuperlayer()
}
}
for id in removedIds {
self.visibleItemLayers.removeValue(forKey: id)
}
}
func update(
context: AccountContext,
theme: PresentationTheme,
insets: UIEdgeInsets,
size: CGSize,
items: [EmojiPagerContentComponent.Item],
isStickers: Bool,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
attemptSynchronousLoad: Bool
) {
if self.theme === theme && self.currentInsets == insets && self.currentSize == size && self.items == items {
return
}
self.context = context
self.theme = theme
self.currentInsets = insets
self.currentSize = size
self.items = items
self.isStickers = isStickers
self.cache = cache
self.renderer = renderer
let itemLayout = ItemLayout(height: size.height, sideInset: insets.left, itemCount: items.count)
self.itemLayout = itemLayout
self.ignoreScrolling = true
if itemLayout.contentSize != self.contentSize {
self.contentSize = itemLayout.contentSize
}
self.ignoreScrolling = false
self.updateVisibleItems(transition: .immediate, attemptSynchronousLoad: attemptSynchronousLoad)
}
}

View File

@ -0,0 +1,128 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
final class GroupExpandActionButton: UIButton {
override static var layerClass: AnyClass {
return PassthroughLayer.self
}
let tintContainerLayer: SimpleLayer
private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
private let backgroundLayer: SimpleLayer
private let tintBackgroundLayer: SimpleLayer
private let textLayer: SimpleLayer
private let pressed: () -> Void
init(pressed: @escaping () -> Void) {
self.pressed = pressed
self.tintContainerLayer = SimpleLayer()
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.masksToBounds = true
self.tintBackgroundLayer = SimpleLayer()
self.tintBackgroundLayer.masksToBounds = true
self.textLayer = SimpleLayer()
super.init(frame: CGRect())
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerLayer
self.layer.addSublayer(self.backgroundLayer)
self.layer.addSublayer(self.textLayer)
self.addTarget(self, action: #selector(self.onPressed), for: .touchUpInside)
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func onPressed() {
self.pressed()
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.alpha = 0.6
return super.beginTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.endTracking(touch, with: event)
}
override func cancelTracking(with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.cancelTracking(with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.touchesCancelled(touches, with: event)
}
func update(theme: PresentationTheme, title: String, useOpaqueTheme: Bool) -> CGSize {
let textConstrainedWidth: CGFloat = 100.0
let color = theme.list.itemCheckColors.foregroundColor
if useOpaqueTheme {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueOverlayColor.cgColor
} else {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantOverlayColor.cgColor
}
self.tintContainerLayer.backgroundColor = UIColor.white.cgColor
let textSize: CGSize
if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == textConstrainedWidth {
textSize = currentTextLayout.size
} else {
let font: UIFont = Font.semibold(13.0)
let string = NSAttributedString(string: title.uppercased(), font: font, textColor: color)
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(in: stringBounds)
UIGraphicsPopContext()
})?.cgImage
self.currentTextLayout = (title, color, textConstrainedWidth, textSize)
}
var sideInset: CGFloat = 10.0
if textSize.width > 24.0 {
sideInset = 6.0
}
let size = CGSize(width: textSize.width + sideInset * 2.0, height: 28.0)
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((size.height - textSize.height) / 2.0)), size: textSize)
self.textLayer.frame = textFrame
self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
self.tintBackgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
self.backgroundLayer.cornerRadius = min(size.width, size.height) / 2.0
self.tintContainerLayer.cornerRadius = min(size.width, size.height) / 2.0
return size
}
}

View File

@ -0,0 +1,149 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
final class GroupHeaderActionButton: UIButton {
override static var layerClass: AnyClass {
return PassthroughLayer.self
}
let tintContainerLayer: SimpleLayer
private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
private let backgroundLayer: SimpleLayer
private let tintBackgroundLayer: SimpleLayer
private let textLayer: SimpleLayer
private let tintTextLayer: SimpleLayer
private let pressed: () -> Void
init(pressed: @escaping () -> Void) {
self.pressed = pressed
self.tintContainerLayer = SimpleLayer()
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.masksToBounds = true
self.tintBackgroundLayer = SimpleLayer()
self.tintBackgroundLayer.masksToBounds = true
self.textLayer = SimpleLayer()
self.tintTextLayer = SimpleLayer()
super.init(frame: CGRect())
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerLayer
self.layer.addSublayer(self.backgroundLayer)
self.layer.addSublayer(self.textLayer)
self.addTarget(self, action: #selector(self.onPressed), for: .touchUpInside)
self.tintContainerLayer.addSublayer(self.tintBackgroundLayer)
self.tintContainerLayer.addSublayer(self.tintTextLayer)
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func onPressed() {
self.pressed()
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.alpha = 0.6
return super.beginTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.endTracking(touch, with: event)
}
override func cancelTracking(with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.cancelTracking(with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.touchesCancelled(touches, with: event)
}
func update(theme: PresentationTheme, title: String, compact: Bool) -> CGSize {
let textConstrainedWidth: CGFloat = 100.0
let needsVibrancy = !theme.overallDarkAppearance && compact
let foregroundColor: UIColor
let backgroundColor: UIColor
if compact {
foregroundColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
backgroundColor = foregroundColor.withMultipliedAlpha(0.2)
} else {
foregroundColor = theme.list.itemCheckColors.foregroundColor
backgroundColor = theme.list.itemCheckColors.fillColor
}
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
self.tintBackgroundLayer.backgroundColor = UIColor.white.withAlphaComponent(0.2).cgColor
self.tintContainerLayer.isHidden = !needsVibrancy
let textSize: CGSize
if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == foregroundColor, currentTextLayout.constrainedWidth == textConstrainedWidth {
textSize = currentTextLayout.size
} else {
let font: UIFont = compact ? Font.medium(11.0) : Font.semibold(15.0)
let string = NSAttributedString(string: title.uppercased(), font: font, textColor: foregroundColor)
let tintString = NSAttributedString(string: title.uppercased(), font: font, textColor: .white)
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(in: stringBounds)
UIGraphicsPopContext()
})?.cgImage
self.tintTextLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
tintString.draw(in: stringBounds)
UIGraphicsPopContext()
})?.cgImage
self.currentTextLayout = (title, foregroundColor, textConstrainedWidth, textSize)
}
let size = CGSize(width: textSize.width + (compact ? 6.0 : 16.0) * 2.0, height: compact ? 16.0 : 28.0)
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
self.textLayer.frame = textFrame
self.tintTextLayer.frame = textFrame
self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
self.backgroundLayer.cornerRadius = min(size.width, size.height) / 2.0
self.tintBackgroundLayer.frame = self.backgroundLayer.frame
self.tintBackgroundLayer.cornerRadius = self.backgroundLayer.cornerRadius
return size
}
}

View File

@ -0,0 +1,525 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AccountContext
import TelegramCore
import TelegramPresentationData
import AnimationCache
import MultiAnimationRenderer
final class GroupHeaderLayer: UIView {
override static var layerClass: AnyClass {
return PassthroughLayer.self
}
private let actionPressed: () -> Void
private let performItemAction: (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void
private let textLayer: SimpleLayer
private let tintTextLayer: SimpleLayer
private var subtitleLayer: SimpleLayer?
private var tintSubtitleLayer: SimpleLayer?
private var lockIconLayer: SimpleLayer?
private var tintLockIconLayer: SimpleLayer?
private var badgeLayer: SimpleLayer?
private var tintBadgeLayer: SimpleLayer?
private(set) var clearIconLayer: SimpleLayer?
private var tintClearIconLayer: SimpleLayer?
private var separatorLayer: SimpleLayer?
private var tintSeparatorLayer: SimpleLayer?
private var actionButton: GroupHeaderActionButton?
private var groupEmbeddedView: GroupEmbeddedView?
private var theme: PresentationTheme?
private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
private var currentSubtitleLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
let tintContentLayer: SimpleLayer
init(actionPressed: @escaping () -> Void, performItemAction: @escaping (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void) {
self.actionPressed = actionPressed
self.performItemAction = performItemAction
self.textLayer = SimpleLayer()
self.tintTextLayer = SimpleLayer()
self.tintContentLayer = SimpleLayer()
super.init(frame: CGRect())
self.layer.addSublayer(self.textLayer)
self.tintContentLayer.addSublayer(self.tintTextLayer)
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContentLayer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(
context: AccountContext,
theme: PresentationTheme,
forceNeedsVibrancy: Bool,
layoutType: EmojiPagerContentComponent.ItemLayoutType,
hasTopSeparator: Bool,
actionButtonTitle: String?,
actionButtonIsCompact: Bool,
title: String,
subtitle: String?,
badge: String?,
isPremiumLocked: Bool,
hasClear: Bool,
embeddedItems: [EmojiPagerContentComponent.Item]?,
isStickers: Bool,
constrainedSize: CGSize,
insets: UIEdgeInsets,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
attemptSynchronousLoad: Bool
) -> (size: CGSize, centralContentWidth: CGFloat) {
var themeUpdated = false
if self.theme !== theme {
self.theme = theme
themeUpdated = true
}
let needsVibrancy = !theme.overallDarkAppearance || forceNeedsVibrancy
let textOffsetY: CGFloat
if hasTopSeparator {
textOffsetY = 9.0
} else {
textOffsetY = 0.0
}
let subtitleColor: UIColor
if theme.overallDarkAppearance && forceNeedsVibrancy {
subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.withMultipliedAlpha(0.2)
} else {
subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
}
let color: UIColor
let needsTintText: Bool
if subtitle != nil {
color = theme.chat.inputPanel.primaryTextColor
needsTintText = false
} else {
color = subtitleColor
needsTintText = true
}
let titleHorizontalOffset: CGFloat
if isPremiumLocked {
titleHorizontalOffset = 10.0 + 2.0
} else {
titleHorizontalOffset = 0.0
}
var actionButtonSize: CGSize?
if let actionButtonTitle = actionButtonTitle {
let actionButton: GroupHeaderActionButton
if let current = self.actionButton {
actionButton = current
} else {
actionButton = GroupHeaderActionButton(pressed: self.actionPressed)
self.actionButton = actionButton
self.addSubview(actionButton)
self.tintContentLayer.addSublayer(actionButton.tintContainerLayer)
}
actionButtonSize = actionButton.update(theme: theme, title: actionButtonTitle, compact: actionButtonIsCompact)
} else {
if let actionButton = self.actionButton {
self.actionButton = nil
actionButton.removeFromSuperview()
}
}
var clearSize: CGSize = .zero
var clearWidth: CGFloat = 0.0
if hasClear {
var updateImage = themeUpdated
let clearIconLayer: SimpleLayer
if let current = self.clearIconLayer {
clearIconLayer = current
} else {
updateImage = true
clearIconLayer = SimpleLayer()
self.clearIconLayer = clearIconLayer
self.layer.addSublayer(clearIconLayer)
}
let tintClearIconLayer: SimpleLayer
if let current = self.tintClearIconLayer {
tintClearIconLayer = current
} else {
updateImage = true
tintClearIconLayer = SimpleLayer()
self.tintClearIconLayer = tintClearIconLayer
self.tintContentLayer.addSublayer(tintClearIconLayer)
}
tintClearIconLayer.isHidden = !needsVibrancy
clearSize = clearIconLayer.bounds.size
if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: subtitleColor) {
clearSize = image.size
clearIconLayer.contents = image.cgImage
}
if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: .white) {
tintClearIconLayer.contents = image.cgImage
}
tintClearIconLayer.frame = clearIconLayer.frame
clearWidth = 4.0 + clearSize.width
} else {
if let clearIconLayer = self.clearIconLayer {
self.clearIconLayer = nil
clearIconLayer.removeFromSuperlayer()
}
if let tintClearIconLayer = self.tintClearIconLayer {
self.tintClearIconLayer = nil
tintClearIconLayer.removeFromSuperlayer()
}
}
var textConstrainedWidth = constrainedSize.width - titleHorizontalOffset - 10.0
if let actionButtonSize = actionButtonSize {
if actionButtonIsCompact {
textConstrainedWidth -= actionButtonSize.width * 2.0 + 10.0
} else {
textConstrainedWidth -= actionButtonSize.width + 10.0
}
}
if clearWidth > 0.0 {
textConstrainedWidth -= clearWidth + 8.0
}
let textSize: CGSize
if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == textConstrainedWidth {
textSize = currentTextLayout.size
} else {
let font: UIFont
let stringValue: String
if subtitle == nil {
font = Font.medium(13.0)
stringValue = title.uppercased()
} else {
font = Font.semibold(16.0)
stringValue = title
}
let string = NSAttributedString(string: stringValue, font: font, textColor: color)
let whiteString = NSAttributedString(string: stringValue, font: font, textColor: .white)
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 18.0), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
//string.draw(in: stringBounds)
string.draw(with: stringBounds, options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
UIGraphicsPopContext()
})?.cgImage
self.tintTextLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
//whiteString.draw(in: stringBounds)
whiteString.draw(with: stringBounds, options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
UIGraphicsPopContext()
})?.cgImage
self.tintTextLayer.isHidden = !needsVibrancy
self.currentTextLayout = (title, color, textConstrainedWidth, textSize)
}
var badgeSize: CGSize = .zero
if let badge {
func generateBadgeImage(color: UIColor) -> UIImage? {
let string = NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white)
let stringBounds = string.boundingRect(with: CGSize(width: 120, height: 18.0), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
let badgeSize = CGSize(width: stringBounds.width + 8.0, height: 16.0)
return generateImage(badgeSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: badgeSize), cornerRadius: badgeSize.height / 2.0).cgPath)
context.fillPath()
context.setBlendMode(.clear)
UIGraphicsPushContext(context)
string.draw(with: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeSize.width - stringBounds.size.width) / 2.0), y: floorToScreenPixels((badgeSize.height - stringBounds.size.height) / 2.0)), size: stringBounds.size), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
UIGraphicsPopContext()
})
}
let badgeLayer: SimpleLayer
if let current = self.badgeLayer {
badgeLayer = current
} else {
badgeLayer = SimpleLayer()
self.badgeLayer = badgeLayer
self.layer.addSublayer(badgeLayer)
if let image = generateBadgeImage(color: color.withMultipliedAlpha(0.66)) {
badgeLayer.contents = image.cgImage
badgeLayer.bounds = CGRect(origin: .zero, size: image.size)
}
}
badgeSize = badgeLayer.bounds.size
let tintBadgeLayer: SimpleLayer
if let current = self.tintBadgeLayer {
tintBadgeLayer = current
} else {
tintBadgeLayer = SimpleLayer()
self.tintBadgeLayer = tintBadgeLayer
self.tintContentLayer.addSublayer(tintBadgeLayer)
if let image = generateBadgeImage(color: .white) {
tintBadgeLayer.contents = image.cgImage
}
}
} else {
if let badgeLayer = self.badgeLayer {
self.badgeLayer = nil
badgeLayer.removeFromSuperlayer()
}
if let tintBadgeLayer = self.tintBadgeLayer {
self.tintBadgeLayer = nil
tintBadgeLayer.removeFromSuperlayer()
}
}
let textFrame: CGRect
if subtitle == nil {
textFrame = CGRect(origin: CGPoint(x: titleHorizontalOffset + floor((constrainedSize.width - titleHorizontalOffset - (textSize.width + badgeSize.width)) / 2.0), y: textOffsetY), size: textSize)
} else {
textFrame = CGRect(origin: CGPoint(x: titleHorizontalOffset, y: textOffsetY), size: textSize)
}
self.textLayer.frame = textFrame
self.tintTextLayer.frame = textFrame
self.tintTextLayer.isHidden = !needsTintText
if let badgeLayer = self.badgeLayer, let tintBadgeLayer = self.tintBadgeLayer {
badgeLayer.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 4.0, y: 0.0), size: badgeLayer.frame.size)
tintBadgeLayer.frame = badgeLayer.frame
}
if isPremiumLocked {
let lockIconLayer: SimpleLayer
if let current = self.lockIconLayer {
lockIconLayer = current
} else {
lockIconLayer = SimpleLayer()
self.lockIconLayer = lockIconLayer
self.layer.addSublayer(lockIconLayer)
}
if let image = PresentationResourcesChat.chatEntityKeyboardLock(theme, color: color) {
let imageSize = image.size
lockIconLayer.contents = image.cgImage
lockIconLayer.frame = CGRect(origin: CGPoint(x: textFrame.minX - imageSize.width - 3.0, y: 2.0 + UIScreenPixel), size: imageSize)
} else {
lockIconLayer.contents = nil
}
let tintLockIconLayer: SimpleLayer
if let current = self.tintLockIconLayer {
tintLockIconLayer = current
} else {
tintLockIconLayer = SimpleLayer()
self.tintLockIconLayer = tintLockIconLayer
self.tintContentLayer.addSublayer(tintLockIconLayer)
}
if let image = PresentationResourcesChat.chatEntityKeyboardLock(theme, color: .white) {
tintLockIconLayer.contents = image.cgImage
tintLockIconLayer.frame = lockIconLayer.frame
tintLockIconLayer.isHidden = !needsVibrancy
} else {
tintLockIconLayer.contents = nil
}
} else {
if let lockIconLayer = self.lockIconLayer {
self.lockIconLayer = nil
lockIconLayer.removeFromSuperlayer()
}
if let tintLockIconLayer = self.tintLockIconLayer {
self.tintLockIconLayer = nil
tintLockIconLayer.removeFromSuperlayer()
}
}
let subtitleSize: CGSize
if let subtitle = subtitle {
var updateSubtitleContents: UIImage?
var updateTintSubtitleContents: UIImage?
if let currentSubtitleLayout = self.currentSubtitleLayout, currentSubtitleLayout.string == subtitle, currentSubtitleLayout.color == subtitleColor, currentSubtitleLayout.constrainedWidth == textConstrainedWidth {
subtitleSize = currentSubtitleLayout.size
} else {
let string = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: subtitleColor)
let whiteString = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: .white)
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
subtitleSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
updateSubtitleContents = generateImage(subtitleSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(in: stringBounds)
UIGraphicsPopContext()
})
updateTintSubtitleContents = generateImage(subtitleSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
whiteString.draw(in: stringBounds)
UIGraphicsPopContext()
})
self.currentSubtitleLayout = (subtitle, subtitleColor, textConstrainedWidth, subtitleSize)
}
let subtitleLayer: SimpleLayer
if let current = self.subtitleLayer {
subtitleLayer = current
} else {
subtitleLayer = SimpleLayer()
self.subtitleLayer = subtitleLayer
self.layer.addSublayer(subtitleLayer)
}
if let updateSubtitleContents = updateSubtitleContents {
subtitleLayer.contents = updateSubtitleContents.cgImage
}
let tintSubtitleLayer: SimpleLayer
if let current = self.tintSubtitleLayer {
tintSubtitleLayer = current
} else {
tintSubtitleLayer = SimpleLayer()
self.tintSubtitleLayer = tintSubtitleLayer
self.tintContentLayer.addSublayer(tintSubtitleLayer)
}
tintSubtitleLayer.isHidden = !needsVibrancy
if let updateTintSubtitleContents = updateTintSubtitleContents {
tintSubtitleLayer.contents = updateTintSubtitleContents.cgImage
}
let subtitleFrame = CGRect(origin: CGPoint(x: 0.0, y: textFrame.maxY + 1.0), size: subtitleSize)
subtitleLayer.frame = subtitleFrame
tintSubtitleLayer.frame = subtitleFrame
} else {
subtitleSize = CGSize()
if let subtitleLayer = self.subtitleLayer {
self.subtitleLayer = nil
subtitleLayer.removeFromSuperlayer()
}
if let tintSubtitleLayer = self.tintSubtitleLayer {
self.tintSubtitleLayer = nil
tintSubtitleLayer.removeFromSuperlayer()
}
}
self.clearIconLayer?.frame = CGRect(origin: CGPoint(x: constrainedSize.width - clearSize.width, y: floorToScreenPixels((textSize.height - clearSize.height) / 2.0)), size: clearSize)
var size: CGSize
size = CGSize(width: constrainedSize.width, height: constrainedSize.height)
if let embeddedItems = embeddedItems {
let groupEmbeddedView: GroupEmbeddedView
if let current = self.groupEmbeddedView {
groupEmbeddedView = current
} else {
groupEmbeddedView = GroupEmbeddedView(performItemAction: self.performItemAction)
self.groupEmbeddedView = groupEmbeddedView
self.addSubview(groupEmbeddedView)
}
let groupEmbeddedViewSize = CGSize(width: constrainedSize.width + insets.left + insets.right, height: 36.0)
groupEmbeddedView.frame = CGRect(origin: CGPoint(x: -insets.left, y: size.height - groupEmbeddedViewSize.height), size: groupEmbeddedViewSize)
groupEmbeddedView.update(
context: context,
theme: theme,
insets: insets,
size: groupEmbeddedViewSize,
items: embeddedItems,
isStickers: isStickers,
cache: cache,
renderer: renderer,
attemptSynchronousLoad: attemptSynchronousLoad
)
} else {
if let groupEmbeddedView = self.groupEmbeddedView {
self.groupEmbeddedView = nil
groupEmbeddedView.removeFromSuperview()
}
}
if let actionButtonSize = actionButtonSize, let actionButton = self.actionButton {
let actionButtonFrame = CGRect(origin: CGPoint(x: size.width - actionButtonSize.width, y: textFrame.minY + (actionButtonIsCompact ? 0.0 : 3.0)), size: actionButtonSize)
actionButton.bounds = CGRect(origin: CGPoint(), size: actionButtonFrame.size)
actionButton.center = actionButtonFrame.center
}
if hasTopSeparator {
let separatorLayer: SimpleLayer
if let current = self.separatorLayer {
separatorLayer = current
} else {
separatorLayer = SimpleLayer()
self.separatorLayer = separatorLayer
self.layer.addSublayer(separatorLayer)
}
separatorLayer.backgroundColor = subtitleColor.cgColor
separatorLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))
let tintSeparatorLayer: SimpleLayer
if let current = self.tintSeparatorLayer {
tintSeparatorLayer = current
} else {
tintSeparatorLayer = SimpleLayer()
self.tintSeparatorLayer = tintSeparatorLayer
self.tintContentLayer.addSublayer(tintSeparatorLayer)
}
tintSeparatorLayer.backgroundColor = UIColor.white.cgColor
tintSeparatorLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))
tintSeparatorLayer.isHidden = !needsVibrancy
} else {
if let separatorLayer = self.separatorLayer {
self.separatorLayer = separatorLayer
separatorLayer.removeFromSuperlayer()
}
if let tintSeparatorLayer = self.tintSeparatorLayer {
self.tintSeparatorLayer = tintSeparatorLayer
tintSeparatorLayer.removeFromSuperlayer()
}
}
return (size, titleHorizontalOffset + textSize.width + clearWidth)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
func tapGesture(point: CGPoint) -> Bool {
if let groupEmbeddedView = self.groupEmbeddedView {
return groupEmbeddedView.tapGesture(point: self.convert(point, to: groupEmbeddedView))
} else {
return false
}
}
}

View File

@ -0,0 +1,346 @@
import Foundation
import UIKit
import Display
import ComponentFlow
public class PassthroughLayer: CALayer {
public var mirrorLayer: CALayer?
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public var position: CGPoint {
get {
return super.position
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.position = value
}
super.position = value
}
}
override public var bounds: CGRect {
get {
return super.bounds
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.bounds = value
}
super.bounds = value
}
}
override public var opacity: Float {
get {
return super.opacity
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.opacity = value
}
super.opacity = value
}
}
override public var sublayerTransform: CATransform3D {
get {
return super.sublayerTransform
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.sublayerTransform = value
}
super.sublayerTransform = value
}
}
override public var transform: CATransform3D {
get {
return super.transform
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.transform = value
}
super.transform = value
}
}
override public func add(_ animation: CAAnimation, forKey key: String?) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.add(animation, forKey: key)
}
super.add(animation, forKey: key)
}
override public func removeAllAnimations() {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.removeAllAnimations()
}
super.removeAllAnimations()
}
override public func removeAnimation(forKey: String) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.removeAnimation(forKey: forKey)
}
super.removeAnimation(forKey: forKey)
}
}
open class PassthroughView: UIView {
override public static var layerClass: AnyClass {
return PassthroughLayer.self
}
public let passthroughView: UIView
override public init(frame: CGRect) {
self.passthroughView = UIView()
super.init(frame: frame)
(self.layer as? PassthroughLayer)?.mirrorLayer = self.passthroughView.layer
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class PassthroughShapeLayer: CAShapeLayer {
var mirrorLayer: CAShapeLayer?
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var position: CGPoint {
get {
return super.position
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.position = value
}
super.position = value
}
}
override var bounds: CGRect {
get {
return super.bounds
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.bounds = value
}
super.bounds = value
}
}
override var opacity: Float {
get {
return super.opacity
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.opacity = value
}
super.opacity = value
}
}
override var sublayerTransform: CATransform3D {
get {
return super.sublayerTransform
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.sublayerTransform = value
}
super.sublayerTransform = value
}
}
override var transform: CATransform3D {
get {
return super.transform
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.transform = value
}
super.transform = value
}
}
override var path: CGPath? {
get {
return super.path
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.path = value
}
super.path = value
}
}
override var fillColor: CGColor? {
get {
return super.fillColor
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.fillColor = value
}
super.fillColor = value
}
}
override var fillRule: CAShapeLayerFillRule {
get {
return super.fillRule
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.fillRule = value
}
super.fillRule = value
}
}
override var strokeColor: CGColor? {
get {
return super.strokeColor
} set(value) {
/*if let mirrorLayer = self.mirrorLayer {
mirrorLayer.strokeColor = value
}*/
super.strokeColor = value
}
}
override var strokeStart: CGFloat {
get {
return super.strokeStart
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.strokeStart = value
}
super.strokeStart = value
}
}
override var strokeEnd: CGFloat {
get {
return super.strokeEnd
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.strokeEnd = value
}
super.strokeEnd = value
}
}
override var lineWidth: CGFloat {
get {
return super.lineWidth
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineWidth = value
}
super.lineWidth = value
}
}
override var miterLimit: CGFloat {
get {
return super.miterLimit
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.miterLimit = value
}
super.miterLimit = value
}
}
override var lineCap: CAShapeLayerLineCap {
get {
return super.lineCap
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineCap = value
}
super.lineCap = value
}
}
override var lineJoin: CAShapeLayerLineJoin {
get {
return super.lineJoin
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineJoin = value
}
super.lineJoin = value
}
}
override var lineDashPhase: CGFloat {
get {
return super.lineDashPhase
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineDashPhase = value
}
super.lineDashPhase = value
}
}
override var lineDashPattern: [NSNumber]? {
get {
return super.lineDashPattern
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineDashPattern = value
}
super.lineDashPattern = value
}
}
override func add(_ animation: CAAnimation, forKey key: String?) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.add(animation, forKey: key)
}
super.add(animation, forKey: key)
}
override func removeAllAnimations() {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.removeAllAnimations()
}
super.removeAllAnimations()
}
override func removeAnimation(forKey: String) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.removeAnimation(forKey: forKey)
}
super.removeAnimation(forKey: forKey)
}
}

View File

@ -0,0 +1,140 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AccountContext
import TelegramCore
private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white)
private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white)
private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white)
private let itemBadgeTextFont: UIFont = {
return Font.regular(10.0)
}()
final class PremiumBadgeView: UIView {
private let context: AccountContext
private var badge: EmojiKeyboardItemLayer.Badge?
let contentLayer: SimpleLayer
private let overlayColorLayer: SimpleLayer
private let iconLayer: SimpleLayer
private var customFileLayer: InlineFileIconLayer?
init(context: AccountContext) {
self.context = context
self.contentLayer = SimpleLayer()
self.contentLayer.contentsGravity = .resize
self.contentLayer.masksToBounds = true
self.overlayColorLayer = SimpleLayer()
self.overlayColorLayer.masksToBounds = true
self.iconLayer = SimpleLayer()
super.init(frame: CGRect())
self.layer.addSublayer(self.contentLayer)
self.layer.addSublayer(self.overlayColorLayer)
self.layer.addSublayer(self.iconLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(transition: Transition, badge: EmojiKeyboardItemLayer.Badge, backgroundColor: UIColor, size: CGSize) {
if self.badge != badge {
self.badge = badge
switch badge {
case .premium:
self.iconLayer.contents = premiumBadgeIcon?.cgImage
case .featured:
self.iconLayer.contents = featuredBadgeIcon?.cgImage
case .locked:
self.iconLayer.contents = lockedBadgeIcon?.cgImage
case let .text(text):
let string = NSAttributedString(string: text, font: itemBadgeTextFont)
let size = CGSize(width: 12.0, height: 12.0)
let stringBounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(at: CGPoint(x: floor((size.width - stringBounds.width) * 0.5), y: floor((size.height - stringBounds.height) * 0.5)))
UIGraphicsPopContext()
})
self.iconLayer.contents = image?.cgImage
case .customFile:
self.iconLayer.contents = nil
}
if case let .customFile(customFile) = badge {
let customFileLayer: InlineFileIconLayer
if let current = self.customFileLayer {
customFileLayer = current
} else {
customFileLayer = InlineFileIconLayer(
context: self.context,
userLocation: .other,
attemptSynchronousLoad: false,
file: customFile,
cache: self.context.animationCache,
renderer: self.context.animationRenderer,
unique: false,
placeholderColor: .clear,
pointSize: CGSize(width: 18.0, height: 18.0),
dynamicColor: nil
)
self.customFileLayer = customFileLayer
self.layer.addSublayer(customFileLayer)
}
let _ = customFileLayer
} else {
if let customFileLayer = self.customFileLayer {
self.customFileLayer = nil
customFileLayer.removeFromSuperlayer()
}
}
}
let iconInset: CGFloat
switch badge {
case .premium:
iconInset = 2.0
case .featured:
iconInset = 0.0
case .locked:
iconInset = 0.0
case .text, .customFile:
iconInset = 0.0
}
switch badge {
case .text, .customFile:
self.contentLayer.isHidden = true
self.overlayColorLayer.isHidden = true
default:
self.contentLayer.isHidden = false
self.overlayColorLayer.isHidden = false
}
self.overlayColorLayer.backgroundColor = backgroundColor.cgColor
transition.setFrame(layer: self.contentLayer, frame: CGRect(origin: CGPoint(), size: size))
transition.setCornerRadius(layer: self.contentLayer, cornerRadius: min(size.width / 2.0, size.height / 2.0))
transition.setFrame(layer: self.overlayColorLayer, frame: CGRect(origin: CGPoint(), size: size))
transition.setCornerRadius(layer: self.overlayColorLayer, cornerRadius: min(size.width / 2.0, size.height / 2.0))
transition.setFrame(layer: self.iconLayer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: iconInset, dy: iconInset))
if let customFileLayer = self.customFileLayer {
let iconSize = CGSize(width: 18.0, height: 18.0)
transition.setFrame(layer: customFileLayer, frame: CGRect(origin: CGPoint(), size: iconSize))
}
}
}

View File

@ -0,0 +1,138 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
final class WarpView: UIView {
private final class WarpPartView: UIView {
let cloneView: PortalView
init?(contentView: PortalSourceView) {
guard let cloneView = PortalView(matchPosition: false) else {
return nil
}
self.cloneView = cloneView
super.init(frame: CGRect())
self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
self.clipsToBounds = true
self.addSubview(cloneView.view)
contentView.addPortal(view: cloneView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(containerSize: CGSize, rect: CGRect, transition: Transition) {
transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: CGSize(width: containerSize.width, height: containerSize.height)))
}
}
let contentView: PortalSourceView
private let clippingView: UIView
private var warpViews: [WarpPartView] = []
private let warpMaskContainer: UIView
private let warpMaskGradientLayer: SimpleGradientLayer
override init(frame: CGRect) {
self.contentView = PortalSourceView()
self.clippingView = UIView()
self.warpMaskContainer = UIView()
self.warpMaskGradientLayer = SimpleGradientLayer()
self.warpMaskContainer.layer.mask = self.warpMaskGradientLayer
super.init(frame: frame)
self.clippingView.addSubview(self.contentView)
self.clippingView.clipsToBounds = true
self.addSubview(self.clippingView)
self.addSubview(self.warpMaskContainer)
for _ in 0 ..< 8 {
if let warpView = WarpPartView(contentView: self.contentView) {
self.warpViews.append(warpView)
self.warpMaskContainer.addSubview(warpView)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, topInset: CGFloat, warpHeight: CGFloat, theme: PresentationTheme, transition: Transition) {
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size))
let allItemsHeight = warpHeight * 0.5
for i in 0 ..< self.warpViews.count {
let itemHeight = warpHeight / CGFloat(self.warpViews.count)
let itemFraction = CGFloat(i + 1) / CGFloat(self.warpViews.count)
let _ = itemHeight
let da = CGFloat.pi * 0.5 / CGFloat(self.warpViews.count)
let alpha = CGFloat.pi * 0.5 - itemFraction * CGFloat.pi * 0.5
let endPoint = CGPoint(x: cos(alpha), y: sin(alpha))
let prevAngle = alpha + da
let prevPt = CGPoint(x: cos(prevAngle), y: sin(prevAngle))
var angle: CGFloat
angle = -atan2(endPoint.y - prevPt.y, endPoint.x - prevPt.x)
let itemLengthVector = CGPoint(x: endPoint.x - prevPt.x, y: endPoint.y - prevPt.y)
let itemLength = sqrt(itemLengthVector.x * itemLengthVector.x + itemLengthVector.y * itemLengthVector.y) * warpHeight * 0.5
let _ = itemLength
var transform: CATransform3D
transform = CATransform3DIdentity
transform.m34 = 1.0 / 240.0
transform = CATransform3DTranslate(transform, 0.0, prevPt.x * allItemsHeight, (1.0 - prevPt.y) * allItemsHeight)
transform = CATransform3DRotate(transform, angle, 1.0, 0.0, 0.0)
let positionY = size.height - allItemsHeight + 4.0 + CGFloat(i) * itemLength
let rect = CGRect(origin: CGPoint(x: 0.0, y: positionY), size: CGSize(width: size.width, height: itemLength))
transition.setPosition(view: self.warpViews[i], position: CGPoint(x: rect.midX, y: 4.0))
transition.setBounds(view: self.warpViews[i], bounds: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: itemLength)))
transition.setTransform(view: self.warpViews[i], transform: transform)
self.warpViews[i].update(containerSize: size, rect: rect, transition: transition)
}
let clippingTopInset: CGFloat = topInset
let frame = CGRect(origin: CGPoint(x: 0.0, y: clippingTopInset), size: CGSize(width: size.width, height: -clippingTopInset + size.height - 21.0))
transition.setPosition(view: self.clippingView, position: frame.center)
transition.setBounds(view: self.clippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: clippingTopInset), size: frame.size))
self.clippingView.clipsToBounds = true
transition.setFrame(view: self.warpMaskContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - allItemsHeight), size: CGSize(width: size.width, height: allItemsHeight)))
var locations: [NSNumber] = []
var colors: [CGColor] = []
let numStops = 6
for i in 0 ..< numStops {
let step = CGFloat(i) / CGFloat(numStops - 1)
locations.append(step as NSNumber)
colors.append(UIColor.black.withAlphaComponent(1.0 - step * step).cgColor)
}
let gradientHeight: CGFloat = 6.0
self.warpMaskGradientLayer.startPoint = CGPoint(x: 0.0, y: (allItemsHeight - gradientHeight) / allItemsHeight)
self.warpMaskGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
self.warpMaskGradientLayer.locations = locations
self.warpMaskGradientLayer.colors = colors
self.warpMaskGradientLayer.type = .axial
transition.setFrame(layer: self.warpMaskGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: allItemsHeight)))
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self.contentView.hitTest(point, with: event)
}
}

View File

@ -105,7 +105,7 @@ final class EmojiListInputComponent: Component {
private var component: EmojiListInputComponent?
private weak var state: EmptyComponentState?
private var itemLayers: [Int64: EmojiPagerContentComponent.View.ItemLayer] = [:]
private var itemLayers: [Int64: EmojiKeyboardItemLayer] = [:]
private let trailingPlaceholder = ComponentView<Empty>()
private let caretIndicator: CaretIndicatorView
@ -239,7 +239,7 @@ final class EmojiListInputComponent: Component {
var itemTransition = transition
var animateIn = false
let itemLayer: EmojiPagerContentComponent.View.ItemLayer
let itemLayer: EmojiKeyboardItemLayer
if let current = self.itemLayers[itemKey] {
itemLayer = current
} else {
@ -249,7 +249,7 @@ final class EmojiListInputComponent: Component {
let animationData = EntityKeyboardAnimationData(
file: item.file
)
itemLayer = EmojiPagerContentComponent.View.ItemLayer(
itemLayer = EmojiKeyboardItemLayer(
item: EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),