75. Контрастный текст поверх картинки
На днях пришла задача - сделать текст контрастным поверх картинки, чтобы его было видно хорошо независимо от яркости картинки под ним. Расскажу как я это сделал по шагам, а для решения обратимся к UIImage и UIColor 😁
Этапы решения
- Определить средний цвет картинки
- Определить яркость (
luminance) получившегося цвета и настроитьcolorSchemeдля текста
Весь код как обычно можно будет посмотреть по ссылке в конце статьи, а ниже будет верхнеуровневое описание решения.
Определяем средний цвет картинки
extension UIImage {
/// Средний цвет для картинки или `nil`, если цвет не определяется
public var averageColor: UIColor? {
// много кода ...
return color
}
}
Поскольку у нас SwiftUI, в котором для работы с картинками мы чаще всего используем Image, нужно преобразовать Image в UIImage. Сделать это можно двумя способами в зависимости от версии iOS, которую нужно поддерживать.
extension View {
/// Делает `UIImage` из `SwiftUI`-вьюхи
@MainActor
public var asUIImage: UIImage? {
if #available(iOS 16.0, *) { // в iOS 16 это очень легко
return ImageRenderer(content: self).uiImage
} else {
// код ...
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
}
Определяем яркость цвета и настраиваем текст
extension Text {
public func makeContrastText(for backgroundColor: Color) -> some View {
var r, g, b, a: CGFloat
(r, g, b, a) = (0, 0, 0, 0)
UIColor(backgroundColor).getRed(&r, green: &g, blue: &b, alpha: &a)
let luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
return luminance < 0.4
? environment(\.colorScheme, .dark)
: environment(\.colorScheme, .light)
}
}
На этот раз кода немного, но для многих в нем может быть достаточно непонятных вещей, поэтому в двух словах описываю что там происходит:
- применяем к тексту метод, принимающий на вход средний цвет картинки, который мы вычислили в предыдущем шаге
- вычисляем яркость цвета и если она меньше 0.4, то применяем к тексту темную тему, иначе - светлую (значение 0.4 взято "на глаз")
Итоговый вариант
Сделаем экран, на котором расположим вертикально три картинки с первого скриншота, и сразу поверх них будет текст:
struct ContrastTextExample: View {
private let imageResources: [ImageResource] = [.swift, .swiftBird, .swiftDark]
var body: some View {
ScrollView {
VStack(spacing: 24) {
ForEach(Array(zip(imageResources.indices, imageResources)), id: \.0) { _, resource in
let image = Image(resource) // <- сделали картинку из ресурса
let color = Color(image.asUIImage?.averageColor ?? .clear) // <- нашли backgroundColor для текста
image
.resizable()
.scaledToFit()
.frame(width: 250, height: 250) // пояснение под скриншотом
.background(color) // пояснение под скриншотом
.overlay(alignment: .bottomTrailing) {
Text("Контрастный текст")
.font(.title2.bold())
.makeContrastText(for: color)
.padding([.trailing, .bottom], 4)
}
}
}
}
}
}
Фрейм и background для картинки я добавил для визуального достижения одинаковых габаритов всех картинок, и заодно можно посмотреть как отличается наш средний цвет в зависимости от наличия разных цветов на картинке: у второй картинки отчетливо видна разница между средним цветом и небом за птицей (по бокам средний цвет).
Замечания
В зависимости от места расположения текста на картинке он может лучше/хуже читаться - это нормально для нашей задачи, но при желании можно постараться и вычислить средний цвет для картинки непосредственно под текстом.
Также можно менять не colorScheme для текста в зависимости от яркости, а другие свойства, в том числе сразу задавать нужный цвет.
Заключение
Вот так нас снова выручает UIKit, и работает все неплохо. За наводку на решение спасибо моему начальнику Леониду 🙂
Код для этой статьи можно посмотреть тут, другие статьи по разработке - тут, а про инвестиции - тут.