Story weather display

This commit is contained in:
Ilya Laktyushin 2024-07-18 04:45:10 +04:00
parent a6c7e92d6f
commit f4cc9d1aad
8 changed files with 245 additions and 115 deletions

View File

@ -8,6 +8,7 @@ import AnimatedStickerNode
import TelegramAnimatedStickerNode
import StickerResources
import MediaEditor
import TelegramStringFormatting
private func generateIcon(style: DrawingWeatherEntity.Style) -> UIImage? {
guard let image = UIImage(bundleImageName: "Chat/Attach Menu/Location") else {
@ -50,7 +51,6 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
}
let backgroundView: UIView
let blurredBackgroundView: BlurredBackgroundView
let textView: DrawingTextView
let iconView: UIImageView
@ -61,13 +61,14 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
private let stickerFetchedDisposable = MetaDisposable()
private let cachedDisposable = MetaDisposable()
let temperature: String
init(context: AccountContext, entity: DrawingWeatherEntity) {
self.temperature = stringForTemperature(entity.temperature)
self.backgroundView = UIView()
self.backgroundView.clipsToBounds = true
self.blurredBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.25), enableBlur: true)
self.blurredBackgroundView.clipsToBounds = true
self.textView = DrawingTextView(frame: .zero)
self.textView.clipsToBounds = false
@ -95,7 +96,6 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
self.textView.delegate = self
self.addSubview(self.backgroundView)
self.addSubview(self.blurredBackgroundView)
self.addSubview(self.textView)
self.addSubview(self.iconView)
@ -138,7 +138,7 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
self.imageNode.frame = self.iconView.frame.offsetBy(dx: 0.0, dy: 2.0)
if let animationNode = self.animationNode {
animationNode.frame = self.iconView.frame.offsetBy(dx: 0.0, dy: 2.0)
animationNode.frame = self.iconView.frame
animationNode.updateLayout(size: self.iconView.frame.size)
}
@ -147,8 +147,6 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
self.textView.frame = CGRect(origin: CGPoint(x: self.bounds.width - self.textSize.width - 6.0, y: floorToScreenPixels((self.bounds.height - self.textSize.height) / 2.0)), size: self.textSize)
self.backgroundView.frame = self.bounds
self.blurredBackgroundView.frame = self.bounds
self.blurredBackgroundView.update(size: self.bounds.size, transition: .immediate)
}
override func selectedTapAction() -> Bool {
@ -161,16 +159,6 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
case .white:
updatedStyle = .black
case .black:
updatedStyle = .transparent
case .transparent:
if self.weatherEntity.hasCustomColor {
updatedStyle = .custom
} else {
updatedStyle = .white
}
case .custom:
updatedStyle = .white
case .blur:
updatedStyle = .white
}
self.weatherEntity.style = updatedStyle
@ -182,7 +170,7 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
private var displayFontSize: CGFloat {
var textFontSize: CGFloat = 0.07
let textLength = self.weatherEntity.temperature.count
let textLength = self.temperature.count
if textLength > 10 {
textFontSize = max(0.01, 0.07 - CGFloat(textLength - 10) / 100.0)
}
@ -194,7 +182,7 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
}
private func updateText() {
let text = NSMutableAttributedString(string: self.weatherEntity.temperature.uppercased())
let text = NSMutableAttributedString(string: self.temperature.uppercased())
let range = NSMakeRange(0, text.length)
let fontSize = self.displayFontSize
@ -213,15 +201,8 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
switch self.weatherEntity.style {
case .white:
textColor = .black
case .black, .transparent, .blur:
case .black:
textColor = .white
case .custom:
let color = self.weatherEntity.color.toUIColor()
if color.lightness > 0.705 {
textColor = .black
} else {
textColor = .white
}
}
text.addAttribute(.foregroundColor, value: textColor, range: range)
@ -241,34 +222,10 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
self.textView.textColor = .black
self.backgroundView.backgroundColor = .white
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .black:
self.textView.textColor = .white
self.backgroundView.backgroundColor = .black
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .transparent:
self.textView.textColor = .white
self.backgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .custom:
let color = self.weatherEntity.color.toUIColor()
let textColor: UIColor
if color.lightness > 0.705 {
textColor = .black
} else {
textColor = .white
}
self.textView.textColor = textColor
self.backgroundView.backgroundColor = color
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .blur:
self.textView.textColor = .white
self.backgroundView.isHidden = true
self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff)
self.blurredBackgroundView.isHidden = false
}
self.textView.textAlignment = .left
@ -282,10 +239,8 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega
}
self.backgroundView.layer.cornerRadius = self.textSize.height * 0.2
self.blurredBackgroundView.layer.cornerRadius = self.backgroundView.layer.cornerRadius
if #available(iOS 13.0, *) {
self.backgroundView.layer.cornerCurve = .continuous
self.blurredBackgroundView.layer.cornerCurve = .continuous
}
super.update(animated: animated)

View File

@ -249,7 +249,7 @@ public enum MediaArea: Codable, Equatable {
try container.encode(coordinates, forKey: .coordinates)
try container.encode(url, forKey: .value)
case let .weather(coordinates, emoji, temperature, flags):
try container.encode(MediaAreaType.link.rawValue, forKey: .type)
try container.encode(MediaAreaType.weather.rawValue, forKey: .type)
try container.encode(coordinates, forKey: .coordinates)
try container.encode(emoji, forKey: .value)
try container.encode(temperature, forKey: .temperature)

View File

@ -120,7 +120,7 @@ public enum CodableDrawingEntity: Equatable {
rotation = entity.rotation
scale = entity.scale
if let size {
cornerRadius = 10.0 / (size.width * entity.scale)
cornerRadius = (size.height * 0.17) / size.width
}
default:
return nil
@ -191,6 +191,17 @@ public enum CodableDrawingEntity: Equatable {
coordinates: coordinates,
url: url
)
case let .weather(entity):
var flags: MediaArea.WeatherFlags = []
if entity.style == .black {
flags.insert(.isDark)
}
return .weather(
coordinates: coordinates,
emoji: entity.emoji,
temperature: entity.temperature,
flags: flags
)
default:
return nil
}

View File

@ -11,7 +11,7 @@ public final class DrawingWeatherEntity: DrawingEntity, Codable {
case uuid
case style
case color
case hasCustomColor
case emoji
case temperature
case icon
case referenceDrawingSize
@ -25,9 +25,6 @@ public final class DrawingWeatherEntity: DrawingEntity, Codable {
public enum Style: Codable, Equatable {
case white
case black
case transparent
case custom
case blur
}
public var uuid: UUID
@ -37,20 +34,11 @@ public final class DrawingWeatherEntity: DrawingEntity, Codable {
public var style: Style
public var temperature: String
public var icon: TelegramMediaFile?
public var color: DrawingColor = DrawingColor(color: .white) {
didSet {
if self.color.toUIColor().argb == UIColor.white.argb {
self.style = .white
self.hasCustomColor = false
} else {
self.style = .custom
self.hasCustomColor = true
}
}
}
public var hasCustomColor = false
public var emoji: String
public var temperature: Double
public var color: DrawingColor = DrawingColor.clear
public var lineWidth: CGFloat = 0.0
public var referenceDrawingSize: CGSize
@ -74,13 +62,14 @@ public final class DrawingWeatherEntity: DrawingEntity, Codable {
return false
}
public init(temperature: String, style: Style, icon: TelegramMediaFile?) {
public init(emoji: String, emojiFile: TelegramMediaFile?, temperature: Double, style: Style) {
self.uuid = UUID()
self.emoji = emoji
self.icon = emojiFile
self.temperature = temperature
self.style = style
self.icon = icon
self.referenceDrawingSize = .zero
self.position = .zero
self.width = 100.0
@ -91,10 +80,9 @@ public final class DrawingWeatherEntity: DrawingEntity, Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uuid = try container.decode(UUID.self, forKey: .uuid)
self.temperature = try container.decode(String.self, forKey: .temperature)
self.emoji = try container.decode(String.self, forKey: .emoji)
self.temperature = try container.decode(Double.self, forKey: .temperature)
self.style = try container.decode(Style.self, forKey: .style)
self.color = try container.decodeIfPresent(DrawingColor.self, forKey: .color) ?? DrawingColor(color: .white)
self.hasCustomColor = try container.decodeIfPresent(Bool.self, forKey: .hasCustomColor) ?? false
if let iconData = try container.decodeIfPresent(Data.self, forKey: .icon) {
self.icon = PostboxDecoder(buffer: MemoryBuffer(data: iconData)).decodeRootObject() as? TelegramMediaFile
@ -113,10 +101,9 @@ public final class DrawingWeatherEntity: DrawingEntity, Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.uuid, forKey: .uuid)
try container.encode(self.emoji, forKey: .emoji)
try container.encode(self.temperature, forKey: .temperature)
try container.encode(self.style, forKey: .style)
try container.encode(self.color, forKey: .color)
try container.encode(self.hasCustomColor, forKey: .hasCustomColor)
var encoder = PostboxEncoder()
if let icon = self.icon {
@ -137,7 +124,7 @@ public final class DrawingWeatherEntity: DrawingEntity, Codable {
}
public func duplicate(copy: Bool) -> DrawingEntity {
let newEntity = DrawingWeatherEntity(temperature: self.temperature, style: self.style, icon: self.icon)
let newEntity = DrawingWeatherEntity(emoji: self.emoji, emojiFile: self.icon, temperature: self.temperature, style: self.style)
if copy {
newEntity.uuid = self.uuid
}
@ -156,6 +143,9 @@ public final class DrawingWeatherEntity: DrawingEntity, Codable {
if self.uuid != other.uuid {
return false
}
if self.emoji != other.emoji {
return false
}
if self.temperature != other.temperature {
return false
}

View File

@ -70,7 +70,9 @@ private func prerenderTextTransformations(entity: DrawingEntity, image: UIImage,
}
func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, entity: DrawingEntity, colorSpace: CGColorSpace, tintColor: UIColor? = nil) -> [MediaEditorComposerEntity] {
if let entity = entity as? DrawingStickerEntity {
if entity is DrawingWeatherEntity {
return []
} else if let entity = entity as? DrawingStickerEntity {
if case let .file(_, type) = entity.content, case .reaction = type {
return []
} else {
@ -126,10 +128,10 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti
return entities
} else if let entity = entity as? DrawingLocationEntity {
return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)]
} else if let entity = entity as? DrawingWeatherEntity {
return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)]
} else if let entity = entity as? DrawingLinkEntity {
return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)]
} else if let entity = entity as? DrawingWeatherEntity {
return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)]
}
}
return []

View File

@ -4583,11 +4583,24 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather) {
let maxWeatherCount = 3
var currentWeatherCount = 0
self.entitiesView.eachView { entityView in
if entityView.entity is DrawingWeatherEntity {
currentWeatherCount += 1
}
}
if currentWeatherCount >= maxWeatherCount {
self.controller?.hapticFeedback.error()
return
}
self.interaction?.insertEntity(
DrawingWeatherEntity(
temperature: stringForTemperature(weather.temperature),
style: .white,
icon: weather.emojiFile
emoji: weather.emoji,
emojiFile: weather.emojiFile,
temperature: weather.temperature,
style: .white
),
scale: nil,
position: nil
@ -6061,7 +6074,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
})
self.present(controller, in: .window(.root))
}
func maybePresentDiscardAlert() {
self.hapticFeedback.impact(.light)
if !self.isEligibleForDraft() {

View File

@ -58,18 +58,14 @@ func getWeather(context: AccountContext) -> Signal<StickerPickerScreen.Weather,
return getWeatherData(context: context, location: location)
|> mapToSignal { weather in
if let weather {
return context.animatedEmojiStickers
|> take(1)
|> mapToSignal { result in
if let match = result[weather.emoji.strippedEmoji]?.first {
return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather(
emoji: weather.emoji.strippedEmoji,
emojiFile: match.file,
temperature: weather.temperature
)))
} else {
return .single(.none)
}
if let match = context.animatedEmojiStickersValue[weather.emoji.strippedEmoji]?.first {
return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather(
emoji: weather.emoji.strippedEmoji,
emojiFile: match.file,
temperature: weather.temperature
)))
} else {
return .single(.none)
}
} else {
return .single(.none)

View File

@ -20,6 +20,7 @@ import LottieComponent
import LottieComponentResourceContent
import StickerResources
import AnimationCache
import TelegramStringFormatting
private let shadowImage: UIImage = {
return UIImage(bundleImageName: "Stories/ReactionShadow")!
@ -224,12 +225,16 @@ public func storyPreviewWithAddedReactions(
}
}
private protocol ItemView: UIView {
}
final class StoryItemOverlaysView: UIView {
static let counterFont: UIFont = {
return Font.with(size: 17.0, design: .camera, weight: .semibold, traits: .monospacedNumbers)
}()
private final class ItemView: HighlightTrackingButton {
private final class ReactionView: HighlightTrackingButton, ItemView {
private let shadowView: UIImageView
private let coverView: UIImageView
@ -524,6 +529,120 @@ final class StoryItemOverlaysView: UIView {
}
}
private final class WeatherView: UIView, ItemView {
private let backgroundView = UIView()
private let directStickerView = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private var file: TelegramMediaFile?
private var textFont: UIFont?
private var customEmojiLoadDisposable: Disposable?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundView.clipsToBounds = true
if #available(iOS 13.0, *) {
self.backgroundView.layer.cornerCurve = .continuous
}
self.addSubview(self.backgroundView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.customEmojiLoadDisposable?.dispose()
}
func update(
context: AccountContext,
emoji: String,
emojiFile: TelegramMediaFile?,
temperature: Double,
flags: MediaArea.WeatherFlags,
synchronous: Bool,
size: CGSize,
cornerRadius: CGFloat,
isActive: Bool
) {
self.backgroundView.backgroundColor = flags.contains(.isDark) ? UIColor(rgb: 0x000000) : UIColor(rgb: 0xffffff)
self.backgroundView.frame = CGRect(origin: .zero, size: size)
self.backgroundView.layer.cornerRadius = cornerRadius
let itemSize = CGSize(width: floor(size.height * 0.71), height: floor(size.height * 0.71))
if self.file?.fileId != emojiFile?.fileId, let file = emojiFile {
self.file = file
self.customEmojiLoadDisposable?.dispose()
self.customEmojiLoadDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: file.resource)).start()
let placeholderColor = flags.contains(.isDark) ? UIColor(white: 1.0, alpha: 0.1) : UIColor(white: 0.0, alpha: 0.1)
let _ = self.directStickerView.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.ResourceContent(context: context, file: file, attemptSynchronously: synchronous, providesPlaceholder: true),
placeholderColor: placeholderColor,
renderingScale: 2.0,
loop: true
)),
environment: {},
containerSize: itemSize
)
}
let textFont: UIFont
if let current = self.textFont {
textFont = current
} else {
textFont = Font.with(size: floorToScreenPixels(size.height * 0.69), design: .camera, weight: .semibold, traits: .monospacedNumbers)
self.textFont = textFont
}
let string = NSMutableAttributedString(
string: stringForTemperature(temperature),
font: textFont,
textColor: flags.contains(.isDark) ? UIColor(rgb: 0xffffff) : UIColor(rgb: 0x000000)
)
string.addAttribute(.kern, value: -(size.height / 38.0) as NSNumber, range: NSMakeRange(0, string.length))
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(string))
),
environment: {},
containerSize: size
)
if let view = self.text.view {
if view.superview == nil {
self.addSubview(view)
}
let textFrame = CGRect(origin: CGPoint(x: size.width - textSize.width - size.height * 0.2, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
let textTransition = ComponentTransition.immediate
textTransition.setFrame(view: view, frame: textFrame)
}
if let directStickerView = self.directStickerView.view as? LottieComponent.View {
if directStickerView.superview == nil {
self.addSubview(directStickerView)
}
let stickerFrame = itemSize.centered(around: CGPoint(x: size.height * 0.5 + size.height * 0.058, y: size.height * 0.5))
let stickerTransition = ComponentTransition.immediate
stickerTransition.setPosition(view: directStickerView, position: stickerFrame.center)
stickerTransition.setBounds(view: directStickerView, bounds: CGRect(origin: CGPoint(), size: stickerFrame.size))
directStickerView.externalShouldPlay = isActive
}
}
}
private var itemViews: [Int: ItemView] = [:]
var activate: ((UIView, MessageReaction.Reaction) -> Void)?
var requestUpdate: (() -> Void)?
@ -561,25 +680,37 @@ final class StoryItemOverlaysView: UIView {
isActive: Bool,
transition: ComponentTransition
) {
func getFrameAndRotation(coordinates: MediaArea.Coordinates, scale: CGFloat = 1.0) -> (frame: CGRect, rotation: CGFloat, cornerRadius: CGFloat)? {
let referenceSize = size
var areaSize = CGSize(width: coordinates.width / 100.0 * referenceSize.width, height: coordinates.height / 100.0 * referenceSize.height)
areaSize.width *= scale
areaSize.height *= scale
let targetFrame = CGRect(x: coordinates.x / 100.0 * referenceSize.width - areaSize.width * 0.5, y: coordinates.y / 100.0 * referenceSize.height - areaSize.height * 0.5, width: areaSize.width, height: areaSize.height)
if targetFrame.width < 5.0 || targetFrame.height < 5.0 {
return nil
}
var cornerRadius: CGFloat = 0.0
if let radius = coordinates.cornerRadius {
cornerRadius = radius / 100.0 * areaSize.width
}
return (targetFrame, coordinates.rotation * (CGFloat.pi / 180.0), cornerRadius)
}
var nextId = 0
for mediaArea in story.mediaAreas {
switch mediaArea {
case let .reaction(coordinates, reaction, flags):
let referenceSize = size
var areaSize = CGSize(width: coordinates.width / 100.0 * referenceSize.width, height: coordinates.height / 100.0 * referenceSize.height)
areaSize.width *= 0.97
areaSize.height *= 0.97
let targetFrame = CGRect(x: coordinates.x / 100.0 * referenceSize.width - areaSize.width * 0.5, y: coordinates.y / 100.0 * referenceSize.height - areaSize.height * 0.5, width: areaSize.width, height: areaSize.height)
if targetFrame.width < 5.0 || targetFrame.height < 5.0 {
guard let (itemFrame, itemRotation, _) = getFrameAndRotation(coordinates: coordinates, scale: 0.97) else {
continue
}
let itemView: ItemView
let itemView: ReactionView
let itemId = nextId
if let current = self.itemViews[itemId] {
if let current = self.itemViews[itemId] as? ReactionView {
itemView = current
} else {
itemView = ItemView(frame: CGRect())
itemView = ReactionView(frame: CGRect())
itemView.activate = { [weak self] view, reaction in
self?.activate?(view, reaction)
}
@ -590,9 +721,9 @@ final class StoryItemOverlaysView: UIView {
self.addSubview(itemView)
}
transition.setPosition(view: itemView, position: targetFrame.center)
transition.setBounds(view: itemView, bounds: CGRect(origin: CGPoint(), size: targetFrame.size))
transition.setTransform(view: itemView, transform: CATransform3DMakeRotation(coordinates.rotation * (CGFloat.pi / 180.0), 0.0, 0.0, 1.0))
transition.setPosition(view: itemView, position: itemFrame.center)
transition.setBounds(view: itemView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
transition.setTransform(view: itemView, transform: CATransform3DMakeRotation(itemRotation, 0.0, 0.0, 1.0))
var counter = 0
if let reactionData = story.views?.reactions.first(where: { $0.value == reaction }) {
@ -607,7 +738,39 @@ final class StoryItemOverlaysView: UIView {
availableReactions: availableReactions,
entityFiles: entityFiles,
synchronous: attemptSynchronous,
size: targetFrame.size,
size: itemFrame.size,
isActive: isActive
)
nextId += 1
case let .weather(coordinates, emoji, temperature, flags):
guard let (itemFrame, itemRotation, cornerRadius) = getFrameAndRotation(coordinates: coordinates) else {
continue
}
let itemView: WeatherView
let itemId = nextId
if let current = self.itemViews[itemId] as? WeatherView {
itemView = current
} else {
itemView = WeatherView(frame: CGRect())
self.itemViews[itemId] = itemView
self.addSubview(itemView)
}
transition.setPosition(view: itemView, position: itemFrame.center)
transition.setBounds(view: itemView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
transition.setTransform(view: itemView, transform: CATransform3DMakeRotation(itemRotation, 0.0, 0.0, 1.0))
itemView.update(
context: context,
emoji: emoji,
emojiFile: context.animatedEmojiStickersValue[emoji]?.first?.file,
temperature: temperature,
flags: flags,
synchronous: attemptSynchronous,
size: itemFrame.size,
cornerRadius: cornerRadius,
isActive: isActive
)