July 12, 2024

75. Контрастный текст поверх картинки

На днях пришла задача - сделать текст контрастным поверх картинки, чтобы его было видно хорошо независимо от яркости картинки под ним. Расскажу как я это сделал по шагам, а для решения обратимся к UIImage и UIColor 😁

Промежуточное демо

Этапы решения

  1. Определить средний цвет картинки
  2. Определить яркость (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, и работает все неплохо. За наводку на решение спасибо моему начальнику Леониду 🙂

Код для этой статьи можно посмотреть тут, другие статьи по разработке - тут, а про инвестиции - тут.