Channel recommendation improvements

This commit is contained in:
Ilya Laktyushin 2023-11-16 18:03:14 +04:00
parent c688b5ad5f
commit 17a203d6b5
3 changed files with 151 additions and 40 deletions

View File

@ -29,6 +29,9 @@ public final class AvatarBadgeView: UIImageView {
private struct Parameters: Equatable {
var size: CGSize
var text: String
var hasTimeoutIcon: Bool
var useSolidColor: Bool
var strokeColor: UIColor?
}
private var originalContent: OriginalContent?
@ -50,8 +53,8 @@ public final class AvatarBadgeView: UIImageView {
}
}
public func update(size: CGSize, text: String) {
let parameters = Parameters(size: size, text: text)
public func update(size: CGSize, text: String, hasTimeoutIcon: Bool = true, useSolidColor: Bool = false, strokeColor: UIColor? = nil) {
let parameters = Parameters(size: size, text: text, hasTimeoutIcon: hasTimeoutIcon, useSolidColor: useSolidColor, strokeColor: strokeColor)
if self.parameters != parameters || !self.hasContent {
self.parameters = parameters
self.update()
@ -192,24 +195,89 @@ public final class AvatarBadgeView: UIImageView {
return
}
self.image = generateImage(parameters.size, rotatedContext: { size, context in
var solidColor: UIColor?
if parameters.useSolidColor {
let context = DrawingContext(size: CGSize(width: 1.0, height: 1.0), scale: 1.0, clear: false)!
context.withFlippedContext({ context in
if let cgImage = blurredImage.cgImage {
context.draw(cgImage, in: CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0))
}
})
solidColor = context.colorAt(.zero)
}
var badgeSize = parameters.size
let strokeWidth: CGFloat = 1.0 + UIScreenPixel
var size = parameters.size
var offset: CGPoint = .zero
if parameters.strokeColor != nil {
offset = CGPoint(x: strokeWidth / 2.0, y: strokeWidth / 2.0)
badgeSize.width += strokeWidth
badgeSize.height += strokeWidth
size.width += strokeWidth * 2.0
size.height += strokeWidth * 2.0
}
self.image = generateImage(size, rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor.black.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
blurredImage.draw(in: CGRect(origin: CGPoint(), size: size), blendMode: .sourceIn, alpha: 1.0)
context.setBlendMode(.normal)
let textColor: UIColor
if isLightImage {
textColor = UIColor(white: 0.7, alpha: 1.0)
} else {
if parameters.useSolidColor {
textColor = .white
} else {
if isLightImage {
textColor = UIColor(white: 0.7, alpha: 1.0)
} else {
textColor = .white
}
}
if var solidColor {
func adjustedBackgroundColor(backgroundColor: UIColor, textColor: UIColor) -> UIColor {
let minContrastRatio: CGFloat = 4.5
if backgroundColor.contrastRatio(with: textColor) < minContrastRatio {
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
backgroundColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
return UIColor(hue: hue, saturation: saturation, brightness: brightness * 0.9, alpha: alpha)
} else {
return backgroundColor
}
}
solidColor = adjustedBackgroundColor(backgroundColor: solidColor, textColor: textColor)
if let strokeColor = parameters.strokeColor {
context.setStrokeColor(strokeColor.cgColor)
context.setLineWidth(strokeWidth)
}
context.setFillColor(solidColor.cgColor)
} else {
context.setBlendMode(.copy)
context.setFillColor(UIColor.black.cgColor)
}
if badgeSize.width != badgeSize.height {
let path = UIBezierPath(roundedRect: CGRect(origin: offset, size: badgeSize), cornerRadius: badgeSize.height / 2.0)
context.addPath(path.cgPath)
if let _ = parameters.strokeColor {
context.drawPath(using: .fillStroke)
} else {
context.fillPath()
}
} else {
context.fillEllipse(in: CGRect(origin: offset, size: badgeSize))
}
if let _ = solidColor {
} else {
blurredImage.draw(in: CGRect(origin: CGPoint(), size: badgeSize), blendMode: .sourceIn, alpha: 1.0)
context.setBlendMode(.normal)
}
var fontSize: CGFloat = floor(parameters.size.height * 0.48)
@ -225,28 +293,30 @@ public final class AvatarBadgeView: UIImageView {
}
}
let lineWidth: CGFloat = 1.5
let lineInset: CGFloat = 2.0
let lineRadius: CGFloat = size.width * 0.5 - lineInset - lineWidth * 0.5
context.setLineWidth(lineWidth)
context.setStrokeColor(textColor.cgColor)
context.setLineCap(.round)
context.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: lineRadius, startAngle: CGFloat.pi * 0.5, endAngle: -CGFloat.pi * 0.5, clockwise: false)
context.strokePath()
let sectionAngle: CGFloat = CGFloat.pi / 11.0
for i in 0 ..< 10 {
if i % 2 == 0 {
continue
}
if parameters.hasTimeoutIcon {
let lineWidth: CGFloat = 1.5
let lineInset: CGFloat = 2.0
let lineRadius: CGFloat = size.width * 0.5 - lineInset - lineWidth * 0.5
context.setLineWidth(lineWidth)
context.setStrokeColor(textColor.cgColor)
context.setLineCap(.round)
let startAngle = CGFloat.pi * 0.5 - CGFloat(i) * sectionAngle - sectionAngle * 0.15
let endAngle = startAngle - sectionAngle * 0.75
context.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: lineRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
context.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: lineRadius, startAngle: CGFloat.pi * 0.5, endAngle: -CGFloat.pi * 0.5, clockwise: false)
context.strokePath()
let sectionAngle: CGFloat = CGFloat.pi / 11.0
for i in 0 ..< 10 {
if i % 2 == 0 {
continue
}
let startAngle = CGFloat.pi * 0.5 - CGFloat(i) * sectionAngle - sectionAngle * 0.15
let endAngle = startAngle - sectionAngle * 0.75
context.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: lineRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
context.strokePath()
}
}
/*if isLightImage {

View File

@ -172,6 +172,12 @@ public extension UIColor {
}
}
func contrastRatio(with other: UIColor) -> CGFloat {
let l1 = self.lightness
let l2 = other.lightness
return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05)
}
var brightness: CGFloat {
var hue: CGFloat = 0.0
var saturation: CGFloat = 0.0

View File

@ -22,6 +22,7 @@ import ChatMessageItemCommon
import RoundedRectWithTailPath
import AvatarNode
import MultilineTextComponent
import BundleIconComponent
import ChatMessageBackground
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? {
@ -545,6 +546,8 @@ private final class ChannelItemComponent: Component {
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let avatarNode: AvatarNode
private let avatarBadge: AvatarBadgeView
private let subtitleIcon = ComponentView<Empty>()
private var component: ChannelItemComponent?
private weak var state: EmptyComponentState?
@ -553,12 +556,17 @@ private final class ChannelItemComponent: Component {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.avatarNode.isUserInteractionEnabled = false
self.avatarBadge = AvatarBadgeView(frame: CGRect())
self.containerButton = HighlightTrackingButton()
super.init(frame: frame)
self.addSubview(self.containerButton)
self.addSubnode(self.avatarNode)
self.avatarNode.view.addSubview(self.avatarBadge)
self.avatarNode.badgeView = self.avatarBadge
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
@ -581,25 +589,38 @@ private final class ChannelItemComponent: Component {
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.regular(11.0), textColor: component.theme.chat.message.incoming.primaryTextColor))
text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.regular(11.0), textColor: component.theme.chat.message.incoming.primaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 2
)),
environment: {},
containerSize: CGSize(width: itemSize.width - 20.0, height: 100.0)
containerSize: CGSize(width: itemSize.width - 16.0, height: 100.0)
)
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.subtitle, font: Font.regular(10.0), textColor: component.theme.chat.message.incoming.secondaryTextColor))
text: .plain(NSAttributedString(string: component.subtitle, font: Font.with(size: 9.0, design: .round, weight: .bold), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: itemSize.width - 6.0, height: 100.0)
)
let subtitleIconSize = self.subtitleIcon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(name: "Chat/Message/Subscriber", tintColor: .white)),
environment: {},
containerSize: CGSize(width: itemSize.width - 6.0, height: 100.0)
)
let avatarSize = CGSize(width: 60.0, height: 60.0)
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((itemSize.width - avatarSize.width) / 2.0), y: 0.0), size: avatarSize)
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((itemSize.width - titleSize.width) / 2.0), y: avatarFrame.maxY + 4.0), size: titleSize)
let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((itemSize.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize)
let subtitleSpacing: CGFloat = 1.0 + UIScreenPixel
let subtitleTotalWidth = subtitleIconSize.width + subtitleSize.width + subtitleSpacing
let subtitleIconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((itemSize.width - subtitleTotalWidth) / 2.0) + 1.0 - UIScreenPixel, y: avatarFrame.maxY - subtitleSize.height + 1.0 - UIScreenPixel), size: subtitleIconSize)
let subtitleFrame = CGRect(origin: CGPoint(x: subtitleIconFrame.maxX + subtitleSpacing, y: avatarFrame.maxY - subtitleSize.height - UIScreenPixel), size: subtitleSize)
self.avatarNode.frame = avatarFrame
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer)
@ -607,18 +628,32 @@ private final class ChannelItemComponent: Component {
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerButton.addSubview(titleView)
self.addSubview(titleView)
}
titleView.frame = titleFrame
}
if let subtitleView = self.subtitle.view {
if subtitleView.superview == nil {
subtitleView.isUserInteractionEnabled = false
self.containerButton.addSubview(subtitleView)
self.addSubview(subtitleView)
}
subtitleView.frame = subtitleFrame
}
if let subtitleIconView = self.subtitleIcon.view {
if subtitleIconView.superview == nil {
subtitleIconView.isUserInteractionEnabled = false
self.addSubview(subtitleIconView)
}
subtitleIconView.frame = subtitleIconFrame
}
let strokeWidth: CGFloat = 1.0 + UIScreenPixel
let avatarBadgeSize = CGSize(width: subtitleSize.width + 4.0 + 4.0 + 6.0, height: 15.0)
self.avatarBadge.update(size: avatarBadgeSize, text: "", hasTimeoutIcon: false, useSolidColor: true, strokeColor: component.theme.chat.message.incoming.bubble.withoutWallpaper.fill.first!)
let avatarBadgeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((avatarFrame.width - avatarBadgeSize.width) / 2.0), y: avatarFrame.height - avatarBadgeSize.height + 2.0), size: avatarBadgeSize).insetBy(dx: -strokeWidth, dy: -strokeWidth)
self.avatarBadge.frame = avatarBadgeFrame
self.containerButton.frame = CGRect(origin: .zero, size: itemSize)
return itemSize