Add blur and tint mode implementation.

Add blur and tint mode implementation
This commit is contained in:
morevsavva
2022-11-14 22:16:48 +03:00
parent 42dd278b52
commit 62ead6a8ee
56 changed files with 498 additions and 141 deletions
@@ -1,3 +1,5 @@
// Copyright 2022 Yandex LLC. All rights reserved.
protocol FilterProtocol {
var name: String { get }
var parameters: [String: Any] { get }
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2022 Yandex LLC. All rights reserved.
import CoreImage
public enum ImageBlurType: FilterProtocol {
case gaussian(radius: CGFloat)
public var name: String {
switch self {
case .gaussian:
return "CIGaussianBlur"
}
}
public var parameters: [String: Any] {
switch self {
case let .gaussian(radius):
return [kCIInputRadiusKey: radius]
}
}
public var imageFilter: ImageFilter {
{ image in
let parameters: [String: Any] = [
kCIInputImageKey: image,
]
let filter = CIFilter(name: name, parameters: self.parameters.merging(parameters) { $1 })
return filter?.outputImage
}
}
}
@@ -1,18 +1,35 @@
// Copyright 2022 Yandex LLC. All rights reserved.
import CoreImage
public enum ImageComposerType: FilterProtocol {
case sourceAtop
case sourceIn
case darken
case lighten
case multiply
case screen
public var name: String {
switch self {
case .sourceAtop:
return "CISourceAtopCompositing"
case .sourceIn:
return "CISourceInCompositing"
case .darken:
return "CIDarkenBlendMode"
case .lighten:
return "CILightenBlendMode"
case .multiply:
return "CIMultiplyCompositing"
case .screen:
return "CIScreenBlendMode"
}
}
public var parameters: [String: Any] {
switch self {
case .sourceAtop:
case .sourceAtop, .sourceIn, .darken, .lighten, .multiply, .screen:
return [:]
}
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2022 Yandex LLC. All rights reserved.
import CoreImage
public enum ImageCropType: FilterProtocol {
case crop(rect: CIVector)
public var name: String {
switch self {
case .crop:
return "CICrop"
}
}
public var parameters: [String: Any] {
switch self {
case let .crop(rect):
return ["inputRectangle": rect]
}
}
public var imageFilter: ImageFilter {
{ image in
let parameters: [String: Any] = [
kCIInputImageKey: image,
]
let filter = CIFilter(name: name, parameters: self.parameters.merging(parameters) { $1 })
return filter?.outputImage
}
}
}
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2021 Yandex LLC. All rights reserved.
import CoreGraphics
import BaseTiny
public enum ImageEffect: Equatable {
case blur(radius: CGFloat)
case tint(color: RGBAColor, mode: TintMode?)
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2022 Yandex LLC. All rights reserved.
import CoreImage
public typealias ImageFilter = (CIImage) -> CIImage?
public typealias ImageGenerator = () -> CIImage?
public typealias ImageComposer = (CIImage) -> ImageFilter
public func combine(
_ f1: @escaping ImageFilter,
_ f2: @escaping ImageFilter
) -> ImageFilter {
{ image in f1(image).flatMap { f2($0) } }
}
public func combine(
_ f1: @escaping ImageFilter,
_ f2: @escaping ImageFilter,
_ f3: @escaping ImageFilter
) -> ImageFilter {
combine(combine(f1, f2), f3)
}
@@ -1,3 +1,5 @@
// Copyright 2022 Yandex LLC. All rights reserved.
import CoreImage
public enum ImageGeneratorType: FilterProtocol {
+7 -4
View File
@@ -9,7 +9,7 @@ public final class Lazy<T> {
private enum State {
case willLoad(getter: Getter, deferredActions: [Action])
case loading
case loading(deferredActions: [Action])
case loaded(value: T)
var value: T? {
@@ -74,8 +74,8 @@ public final class Lazy<T> {
switch state {
case let .willLoad(getter, deferredActions):
state = .willLoad(getter: getter, deferredActions: deferredActions + [action])
case .loading:
assertionFailure()
case let .loading(deferredActions):
state = .loading(deferredActions: deferredActions + [action])
case let .loaded(value):
action(value)
}
@@ -85,8 +85,11 @@ public final class Lazy<T> {
ensureIsValidThread()
switch state {
case let .willLoad(getter, deferredActions):
state = .loading
state = .loading(deferredActions: deferredActions)
let value = getter()
guard case let .loading(deferredActions) = state else {
return assertionFailure()
}
state = .loaded(value: value)
deferredActions.forEach { $0(value) }
case .loading, .loaded:
+1
View File
@@ -1,4 +1,5 @@
// Copyright 2018 Yandex LLC. All rights reserved.
public struct Observer<T> {
public let action: (T) -> Void
+15
View File
@@ -201,3 +201,18 @@ extension Property: ExpressibleByBooleanLiteral where T == Bool {
self.init(initialValue: value)
}
}
extension Property: ExpressibleByIntegerLiteral where T == Int {
public init(integerLiteral value: Int) {
self.init(initialValue: value)
}
}
extension Property: ExpressibleByUnicodeScalarLiteral where T == String {}
extension Property: ExpressibleByExtendedGraphemeClusterLiteral where T == String {}
extension Property: ExpressibleByStringLiteral where T == String {
public init(stringLiteral value: String) {
self.init(initialValue: value)
}
}
+17 -10
View File
@@ -37,7 +37,6 @@ public final class RadialGradientView: UIView {
+ [gradient.outerColor.cgColor]
gradientLayer.locations =
([0] + gradient.intermediatePoints.map { $0.location } + [1]) as [NSNumber]
}
public override func layoutSubviews() {
@@ -79,12 +78,20 @@ public final class RadialGradientView: UIView {
}
radius = min(startPoint.x.nearestDistance / kx, startPoint.y.nearestDistance / ky)
case .nearestCorner:
if startPoint.x.isOnBorder && startPoint.y.isOnBorder {
if startPoint.x.isOnBorder, startPoint.y.isOnBorder {
return CGPoint(x: startPoint.x + .ulpOfOne, y: startPoint.y + .ulpOfOne)
}
radius = sqrt(pow(startPoint.x.nearestDistance / kx, 2) + pow(startPoint.y.nearestDistance / ky, 2))
radius =
sqrt(
pow(startPoint.x.nearestDistance / kx, 2) +
pow(startPoint.y.nearestDistance / ky, 2)
)
case .farthestCorner:
radius = sqrt(pow(startPoint.x.farthestDistance / kx, 2) + pow(startPoint.y.farthestDistance / ky, 2))
radius =
sqrt(
pow(startPoint.x.farthestDistance / kx, 2) +
pow(startPoint.y.farthestDistance / ky, 2)
)
case .farthestSide:
radius = max(startPoint.x.farthestDistance / kx, startPoint.y.farthestDistance / ky)
}
@@ -104,24 +111,24 @@ public final class RadialGradientView: UIView {
}
}
fileprivate extension CGFloat {
var nearest: CGFloat {
extension CGFloat {
fileprivate var nearest: CGFloat {
self < 0.5 ? 0 : 1
}
var farthest: CGFloat {
fileprivate var farthest: CGFloat {
self > 0.5 ? 0 : 1
}
var nearestDistance: CGFloat {
fileprivate var nearestDistance: CGFloat {
abs(self.nearest - self)
}
var farthestDistance: CGFloat {
fileprivate var farthestDistance: CGFloat {
abs(self.farthest - self)
}
var isOnBorder: Bool {
fileprivate var isOnBorder: Bool {
self == 0 || self == 1
}
}
+18 -17
View File
@@ -76,25 +76,26 @@ extension SettingProperty where T: NSCoding {
extension SettingProperty where T: Codable {
public static func codableStorage(_ storage: KeyValueStorage, key: String) -> Property<T?> {
Property(getter: {
guard let object = storage.object(forKey: key) else { return nil }
guard let data = object as? Data else {
assertionFailure()
SettingProperty<Data>.storage(storage, key: key).decoded(T.self)
}
}
extension Property where T == Data? {
public func decoded<U: Codable>(_: U.Type) -> Property<U?> {
bimap(
get: { $0.flatMap { try? SingleValueDecoder().decode(U.self, from: $0) } },
set: {
if let newValue = $0 {
do {
return try SingleValueEncoder().encode(newValue)
} catch {
assertionFailure(error.localizedDescription)
}
}
return nil
}
return try? SingleValueDecoder().decode(T.self, from: data)
}, setter: {
if let newValue = $0 {
do {
let encodedValue = try SingleValueEncoder().encode(newValue)
storage.set(encodedValue, forKey: key)
} catch {
assertionFailure(error.localizedDescription)
}
} else {
storage.removeObject(forKey: key)
}
})
)
}
}
+1
View File
@@ -1,4 +1,5 @@
// Copyright 2018 Yandex LLC. All rights reserved.
import Foundation
public struct Signal<T> {
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2022 Yandex LLC. All rights reserved.
public enum TintMode {
case sourceIn
case sourceAtop
case darken
case lighten
case multiply
case screen
}
@@ -42,6 +42,10 @@ public struct WeakCollection<T> {
public func contains(_ object: T) -> Bool {
array.contains(where: { $0.value === (object as AnyObject) })
}
public mutating func removeAll() {
array.removeAll()
}
}
extension WeakCollection: CustomStringConvertible {
@@ -15,6 +15,10 @@ public enum ArrayBuilder<Element> {
element.map { [$0] } ?? []
}
public static func buildExpression(_ component: Component) -> Component {
component
}
public static func buildOptional(_ component: Component?) -> Component {
component ?? []
}
+5 -2
View File
@@ -42,7 +42,10 @@ extension Comparable {
}
extension Comparable where Self: Strideable, Self.Stride: SignedInteger {
public func clamp(_ range: Range<Self>) -> Self {
clamp(ClosedRange(range))
public func clamp(_ range: Range<Self>) -> Self? {
guard !range.isEmpty else {
return nil
}
return clamp(ClosedRange(range))
}
}
+19
View File
@@ -345,6 +345,25 @@ extension Dictionary where Key == String, Value == JSONObject {
}
}
extension Dictionary where Key == String, Value == JSONObject {
public func value(atPath path: JSONObject.Path) throws -> JSONObject {
try JSONObject.object(self).value(atPath: path)
}
}
extension Dictionary where Key == String, Value == JSONObject {
public func flatten() -> JSONDictionary {
reduce(into: [:]) { result, entry in
let (key, value) = entry
if case let .object(dict) = value {
dict.flatten().forEach { result[key + "." + $0] = $1 }
} else {
result[key] = value
}
}
}
}
extension JSONObject {
public struct Path: Codable, CustomDebugStringConvertible, ExpressibleByStringLiteral, Hashable {
public typealias Component = JSONPathComponent
@@ -102,9 +102,15 @@ extension AccessibilityElement {
public static func button(
strings: Strings,
enabled: Bool = true,
startsMediaSession: Bool = false
) -> AccessibilityElement {
AccessibilityElement(traits: .button, strings: strings, startsMediaSession: startsMediaSession)
AccessibilityElement(
traits: .button,
strings: strings,
enabled: enabled,
startsMediaSession: startsMediaSession
)
}
public static func header(label: String?) -> AccessibilityElement? {
@@ -1,8 +1,11 @@
// Copyright 2021 Yandex LLC. All rights reserved.
import CoreGraphics
import CoreImage
import BaseTiny
@_implementationOnly import CoreImage
extension CGColor {
public var rgba: RGBAColor {
let color = CIColor(cgColor: self)
-12
View File
@@ -1,12 +0,0 @@
import CoreImage
public typealias ImageFilter = (CIImage) -> CIImage?
public typealias ImageGenerator = () -> CIImage?
public typealias ImageComposer = (CIImage) -> ImageFilter
public func combine(
_ lhs: @escaping ImageFilter,
_ rhs: @escaping ImageFilter
) -> ImageFilter {
{ image in lhs(image).flatMap { rhs($0) } }
}
@@ -31,6 +31,8 @@ extension UIDevice {
public static let iPhone12mini = "iPhone13,1"
public static let iPhone12Pro = "iPhone13,3"
public static let iPhone12ProMax = "iPhone13,4"
public static let iPadAir_5gen = "iPad13,17"
}
public var systemModelName: String {
@@ -0,0 +1,24 @@
// Copyright 2022 Yandex LLC. All rights reserved.
import BaseUI
public struct ImageRedrawingStyle: Equatable {
public static func ==(lhs: ImageRedrawingStyle, rhs: ImageRedrawingStyle) -> Bool {
lhs.tintColor == rhs.tintColor &&
lhs.tintMode == rhs.tintMode
}
let tintColor: Color?
let tintMode: TintMode?
let effects: [ImageEffect]
public init(
tintColor: Color?,
tintMode: TintMode? = nil,
effects: [ImageEffect]
) {
self.tintColor = tintColor
self.tintMode = tintMode
self.effects = effects
}
}
@@ -1,3 +1,5 @@
// Copyright 2022 Yandex LLC. All rights reserved.
import UIKit
import Base
@@ -29,22 +31,3 @@ public struct ImageViewAnimation {
self.options = options
}
}
public struct ImageRedrawingStyle: Equatable {
let tintColor: Color
let tintMode: TintMode?
public init(tintColor: Color, tintMode: TintMode? = nil) {
self.tintColor = tintColor
self.tintMode = tintMode
}
public enum TintMode {
case sourceIn
case sourceAtop
case darken
case lighten
case multiply
case screen
}
}
+80 -57
View File
@@ -1,3 +1,5 @@
// Copyright 2022 Yandex LLC. All rights reserved.
import MetalKit
import UIKit
@@ -18,8 +20,6 @@ public final class MetalImageView: UIView, RemoteImageViewContentProtocol {
return mtkView
}()
public var lastTexture: MTLTexture?
public override init(frame: CGRect) {
super.init(frame: frame)
}
@@ -32,12 +32,12 @@ public final class MetalImageView: UIView, RemoteImageViewContentProtocol {
public override func layoutSubviews() {
super.layoutSubviews()
metalView.frame = bounds
image = redrawingImage(uiImage, bounds: bounds, imageRedrawingStyle: imageRedrawingStyle)
}
public func setImage(_ image: UIImage?, animated: Bool?) {
self.image = redrawingImage(image, imageRedrawingStyle: imageRedrawingStyle)
uiImage = image
metalView.setNeedsDisplay()
setNeedsLayout()
if let appearanceAnimation = appearanceAnimation, animated == true {
self.alpha = appearanceAnimation.startAlpha
UIView.animate(
@@ -60,8 +60,8 @@ public final class MetalImageView: UIView, RemoteImageViewContentProtocol {
public var imageRedrawingStyle: ImageRedrawingStyle? {
didSet {
if oldValue != imageRedrawingStyle, let uiImage {
image = redrawingImage(uiImage, imageRedrawingStyle: imageRedrawingStyle)
if oldValue != imageRedrawingStyle {
setNeedsLayout()
}
}
}
@@ -75,56 +75,74 @@ public final class MetalImageView: UIView, RemoteImageViewContentProtocol {
}
}
extension UIImage.Orientation {
fileprivate var layerTransform: CGAffineTransform {
switch self {
case .up:
return .identity
case .upMirrored:
return CGAffineTransform(scaleX: -1, y: 1)
case .down:
return CGAffineTransform(rotationAngle: .pi)
case .downMirrored:
return CGAffineTransform(rotationAngle: .pi).scaledBy(x: -1, y: 1)
case .left:
return CGAffineTransform(rotationAngle: -.pi / 2)
case .leftMirrored:
return CGAffineTransform(rotationAngle: -.pi / 2).scaledBy(x: -1, y: 1)
case .right:
return CGAffineTransform(rotationAngle: .pi / 2)
case .rightMirrored:
return CGAffineTransform(rotationAngle: .pi / 2).scaledBy(x: -1, y: 1)
@unknown default:
return .identity
}
}
}
extension CALayer {
fileprivate func setContents(_ source: Image?) {
contents = source?.cgImage
contentsScale = source?.scale ?? 1
setAffineTransform(source?.imageOrientation.layerTransform ?? .identity)
}
}
fileprivate func redrawingImage(
_ image: UIImage?,
bounds: CGRect,
imageRedrawingStyle: ImageRedrawingStyle?
) -> CIImage? {
guard let image,
let cgImage = image.cgImage else {
return nil
}
let baseImage = CIImage(cgImage: cgImage)
let ciImage = CIImage(cgImage: cgImage)
let extent = ciImage.extent
guard let tintColor = imageRedrawingStyle?.tintColor.ciColor,
let coloredImage = ImageGeneratorType.constantColor(color: tintColor).imageGenerator(),
let ciImage = ImageComposerType.sourceAtop.imageComposer(baseImage)(coloredImage) else {
return baseImage
let identityFilter: ImageFilter = { $0 }
let tintModeFilter: ImageFilter
if let tintColor = imageRedrawingStyle?.tintColor,
let coloredImage = ImageGeneratorType.constantColor(color: tintColor.ciColor)
.imageGenerator() {
let mode = imageRedrawingStyle?.tintMode ?? .sourceAtop
tintModeFilter = { mode.composerType.imageComposer($0)(coloredImage) }
} else {
tintModeFilter = identityFilter
}
return ciImage.cropped(to: baseImage.extent)
let filter = imageRedrawingStyle?.effects.compactMap {
switch $0 {
case let .blur(radius: radius):
let scaleX = bounds.width / extent.width
let scaleY = bounds.height / extent.height
let scale = max(scaleX, scaleY) * 2
return ImageBlurType.gaussian(radius: radius / scale).imageFilter
case let .tint(color: color, mode: mode):
guard let mode,
let coloredImage = ImageGeneratorType.constantColor(color: color.ciColor)
.imageGenerator()
else { return nil }
return { mode.composerType.imageComposer($0)(coloredImage) }
}
}.reduce(identityFilter, combine) ?? identityFilter
let contentRect = CIVector(
x: 0,
y: 0,
z: extent.width,
w: extent.height
)
return combine(tintModeFilter, filter, ImageCropType.crop(rect: contentRect).imageFilter)(ciImage)
}
extension TintMode {
fileprivate var composerType: ImageComposerType {
switch self {
case .sourceIn:
return .sourceIn
case .sourceAtop:
return .sourceAtop
case .darken:
return .darken
case .lighten:
return .lighten
case .multiply:
return .multiply
case .screen:
return .screen
}
}
}
extension MetalImageView: MTKViewDelegate {
@@ -139,26 +157,18 @@ extension MetalImageView: MTKViewDelegate {
let buffer = commandQueue.makeCommandBuffer()
let drawableSize = view.drawableSize
let boundsSize: CGSize
if imageContentMode.scale == .noScale {
boundsSize = drawableSize
} else {
boundsSize = view.bounds.size
}
let layout = layout(
contentMode: imageContentMode,
contentSize: image.extent.size,
boundsSize: boundsSize
boundsSize: view.bounds.size
)
let ciContext = CIContext()
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bounds = CGRect(origin: CGPoint.zero, size: drawableSize)
let screenFactorX = drawableSize.width / boundsSize.width
let screenFactorY = drawableSize.height / boundsSize.height
let screenFactorX = drawableSize.width / view.bounds.width
let screenFactorY = drawableSize.height / view.bounds.height
let scaleX = layout.width / image.extent.width * screenFactorX
let scaleY = layout.height / image.extent.height * screenFactorY
@@ -174,7 +184,15 @@ extension MetalImageView: MTKViewDelegate {
y: layout.origin.y * screenFactorY
))
ciContext.render(
let ciContext: CIContext?
#if DEBUG
ciContext = isSnapshotTest ? CIContext() : self.ciContext
#else
ciContext = self.ciContext
#endif
ciContext?.render(
scaledImage,
to: drawable.texture,
commandBuffer: buffer,
@@ -242,3 +260,8 @@ private func makeOrigin(
}
return CGPoint(x: x, y: y)
}
#if DEBUG
private let isSnapshotTest = ProcessInfo.processInfo
.environment["XCTestConfigurationFilePath"] != nil
#endif
@@ -1,3 +1,5 @@
// Copyright 2022 Yandex LLC. All rights reserved.
import UIKit
import Base
@@ -43,6 +45,7 @@ public final class RemoteImageView: UIView, RemoteImageViewContentProtocol {
}
}
setNeedsLayout()
layoutIfNeeded()
}
public func setImage(_ image: UIImage?, animated: Bool?) {
@@ -60,6 +63,8 @@ public final class RemoteImageView: UIView, RemoteImageViewContentProtocol {
} else {
updateContent()
}
setNeedsLayout()
layoutIfNeeded()
}
private var image: UIImage?
@@ -1,4 +1,4 @@
// Copyright 2015 Yandex LLC. All rights reserved.
// Copyright 2022 Yandex LLC. All rights reserved.
import UIKit
@@ -4,5 +4,5 @@ import Foundation
import UIKit
public protocol RemoteImageViewContentProtocol: UIView, ImageViewProtocol {
func setImage(_ image: UIImage?, animated: Bool?)
func setImage(_ image: UIImage?, animated: Bool?)
}
@@ -67,3 +67,47 @@ extension HTTPHeaders {
extension HTTPHeaders {
public static let yandexUID = "X-Yandex-RandomUID"
}
extension HTTPHeaders {
/**
Creates a new instance, combining current headers with newHeaders.
Conflicting keys get a value from newHeaders.
"Cookies" field is merged separately, all cookies are stored,
conflicting cookie keys get a value from newHeaders' cookies.
**/
public func merged(with newHeaders: HTTPHeaders) -> HTTPHeaders {
let cookie = mergeCookies(self["Cookie"], newHeaders["Cookie"])
return self + newHeaders +
(cookie != "" ? HTTPHeaders(headersDictionary: ["Cookie": cookie]) : HTTPHeaders.empty)
}
private func mergeCookies(_ cookies: String?, _ newCookies: String?) -> String {
var result = parseCookies(cookies ?? "")
parseCookies(newCookies ?? "").forEach { newCookie in
if let index = result.firstIndex(where: { $0.name == newCookie.name }) {
result[index] = newCookie
} else {
result.append(newCookie)
}
}
return result.map { "\($0.name)=\($0.value)" }.joined(separator: "; ")
}
private func parseCookies(_ cookies: String) -> [(name: String, value: String)] {
cookies
.components(separatedBy: "; ")
.filter { $0 != "" }
.map {
let keyVal = $0.components(separatedByFirst: "=")
return (name: keyVal[0], value: keyVal.count > 1 ? keyVal[1] : "")
}
}
}
extension String {
fileprivate func components(separatedByFirst delimiter: String) -> [String] {
let comps = components(separatedBy: delimiter)
guard comps.count > 1 else { return comps }
return [comps[0], comps.suffix(from: 1).joined(separator: delimiter)]
}
}
@@ -118,6 +118,6 @@ extension RemoteImageHolder: CustomDebugStringConvertible {
extension RemoteImageHolder {
public func reused(with placeholder: ImagePlaceholder?, remoteImageURL: URL?) -> ImageHolder? {
(placeholder === placeholder && url == remoteImageURL) ? self : nil
(self.placeholder === placeholder && url == remoteImageURL) ? self : nil
}
}
+13
View File
@@ -53,6 +53,14 @@ cp $1/src/base/ios/yandex/storage/KeyValueDirectStoringSupporting.swift Base
cp $1/src/base/ios/yandex/storage/KeyValueStorage.swift Base
cp $1/src/base/ios/yandex/storage/SettingProperty.swift Base
cp $1/src/base/ios/yandex/storage/SingleValueCoder.swift Base
cp $1/src/base/ios/yandex/thick_ui/filters/FilterProtocol.swift Base
cp $1/src/base/ios/yandex/thick_ui/filters/ImageComposer.swift Base
cp $1/src/base/ios/yandex/thick_ui/filters/ImageFilters.swift Base
cp $1/src/base/ios/yandex/thick_ui/filters/ImageGenerator.swift Base
cp $1/src/base/ios/yandex/thick_ui/filters/TintMode.swift Base
cp $1/src/base/ios/yandex/thick_ui/filters/ImageBlur.swift Base
cp $1/src/base/ios/yandex/thick_ui/filters/ImageCrop.swift Base
cp $1/src/base/ios/yandex/thick_ui/filters/ImageEffect.swift Base
cp $1/src/base/ios/yandex/thick_ui/graphics/InternalImageDescriptor.swift Base
cp $1/src/base/ios/yandex/thick_ui/types/color/ColorExtensions.swift Base
cp $1/src/base/ios/yandex/thick_ui/types/color/RGBAColorExtensions.swift Base
@@ -126,6 +134,11 @@ cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/ImageContaining.swift
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/ImageLayerLayout.swift CommonCore
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/ObjectsReusability.swift CommonCore
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/RemoteImageView.swift CommonCore
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/RemoteImageViewContainer.swift CommonCore
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/RemoteImageViewContentProtocol.swift CommonCore
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/MetalImageView.swift CommonCore
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/ImageViewProtocol.swift CommonCore
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/ImageRedrawingStyle.swift CommonCore
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/Resetting.swift CommonCore
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/Theme.swift CommonCore
cp $1/src/yandex/ios/search_app/CommonCore/CommonCore/Base/UIStyles.swift CommonCore
@@ -9,7 +9,8 @@ import Networking
extension DivBackground {
func makeBlockBackground(
with imageHolderFactory: ImageHolderFactory,
expressionResolver: ExpressionResolver
expressionResolver: ExpressionResolver,
metalImageRenderingEnabled: Bool
) -> LayoutKit.Background? {
switch self {
case let .divLinearGradient(gradient):
@@ -38,7 +39,9 @@ extension DivBackground {
imageBackground.resolveImageUrl(expressionResolver)
),
contentMode: imageBackground.resolveContentMode(expressionResolver),
alpha: imageBackground.resolveAlpha(expressionResolver)
alpha: imageBackground.resolveAlpha(expressionResolver),
effects: imageBackground.makeEffects(with: expressionResolver),
metalImageRenderingEnabled: metalImageRenderingEnabled
)
return .image(image)
case let .divSolidBackground(solidBackground):
@@ -56,3 +59,9 @@ extension DivBackground {
}
extension DivImageBackground: DivImageContentMode {}
extension DivImageBackground {
fileprivate func makeEffects(with resolver: ExpressionResolver) -> [ImageEffect] {
filters?.compactMap { $0.makeImageEffect(with: resolver)} ?? []
}
}
@@ -62,7 +62,8 @@ extension DivBase {
background,
to: block,
imageHolderFactory: context.imageHolderFactory,
expressionResolver: expressionResolver
expressionResolver: expressionResolver,
metalImageRenderingEnabled: context.flagsInfo.metalImageRenderingEnabled
)
.addingDecorations(
boundary: border.makeBoundaryTrait(with: expressionResolver),
@@ -222,7 +223,8 @@ extension DivBase {
_ backgrounds: [DivBackground]?,
to block: Block,
imageHolderFactory: ImageHolderFactory,
expressionResolver: ExpressionResolver
expressionResolver: ExpressionResolver,
metalImageRenderingEnabled: Bool
) -> Block {
guard let backgrounds = backgrounds else {
return block
@@ -232,7 +234,8 @@ extension DivBase {
if backgrounds.count == 1 {
guard let background = backgrounds[0].makeBlockBackground(
with: imageHolderFactory,
expressionResolver: expressionResolver
expressionResolver: expressionResolver,
metalImageRenderingEnabled: metalImageRenderingEnabled
) else {
return block
}
@@ -245,7 +248,8 @@ extension DivBase {
let blockBackgrounds = backgrounds.compactMap {
$0.makeBlockBackground(
with: imageHolderFactory,
expressionResolver: expressionResolver
expressionResolver: expressionResolver,
metalImageRenderingEnabled: metalImageRenderingEnabled
)
}
guard let background = blockBackgrounds.composite() else {
@@ -0,0 +1,11 @@
import Base
import CoreGraphics
extension DivFilter {
func makeImageEffect(with resolver: ExpressionResolver) -> ImageEffect {
switch self {
case .divBlur(let blur):
return .blur(radius: CGFloat(blur.resolveRadius(resolver) ?? 0))
}
}
}
@@ -1,5 +1,6 @@
import Foundation
import BaseUI
import CommonCore
import Networking
import LayoutKit
@@ -39,8 +40,35 @@ extension DivImage: DivBlockModeling, DivImageProtocol {
height: resolveHeight(context),
contentMode: resolveContentMode(expressionResolver),
tintColor: resolveTintColor(expressionResolver),
tintMode: resolveTintMode(expressionResolver).tintMode,
effects: makeEffects(with: expressionResolver),
metalImageRenderingEnabled: context.flagsInfo.metalImageRenderingEnabled,
appearanceAnimation: appearanceAnimation?.makeAppearanceAnimation(with: expressionResolver)
)
}
}
extension DivBlendMode {
fileprivate var tintMode: TintMode {
switch self {
case .sourceIn:
return .sourceIn
case .sourceAtop:
return .sourceAtop
case .darken:
return .darken
case .lighten:
return .lighten
case .multiply:
return .multiply
case .screen:
return .screen
}
}
}
extension DivImage {
fileprivate func makeEffects(with resolver: ExpressionResolver) -> [ImageEffect] {
filters?.compactMap { $0.makeImageEffect(with: resolver)} ?? []
}
}
@@ -31,7 +31,9 @@ public final class ImageExtensionHandler: DivExtensionHandler {
widthTrait: block.widthTrait,
height: block.height,
contentMode: block.contentMode,
tintColor: block.tintColor
tintColor: block.tintColor,
tintMode: block.tintMode,
effects: block.effects
)
}
}
@@ -8,6 +8,8 @@ extension ImageBlock {
height: height,
contentMode: contentMode,
tintColor: tintColor,
tintMode: tintMode,
effects: effects,
metalImageRenderingEnabled: metalImageRenderingEnabled,
accessibilityElement: accessibilityElement
)
@@ -10,6 +10,8 @@ public final class ImageBlock: ImageBaseBlock {
public let height: ImageBlockHeight
public let contentMode: ImageContentMode
public let tintColor: Color?
public let tintMode: TintMode?
public let effects: [ImageEffect]
public let accessibilityElement: AccessibilityElement?
public let appearanceAnimation: TransitioningAnimation?
public let metalImageRenderingEnabled: Bool
@@ -20,6 +22,8 @@ public final class ImageBlock: ImageBaseBlock {
height: ImageBlockHeight,
contentMode: ImageContentMode,
tintColor: Color?,
tintMode: TintMode?,
effects: [ImageEffect],
metalImageRenderingEnabled: Bool = false,
accessibilityElement: AccessibilityElement? = nil,
appearanceAnimation: TransitioningAnimation? = nil
@@ -29,6 +33,8 @@ public final class ImageBlock: ImageBaseBlock {
self.height = height
self.contentMode = contentMode
self.tintColor = tintColor
self.tintMode = tintMode
self.effects = effects
self.metalImageRenderingEnabled = metalImageRenderingEnabled
self.accessibilityElement = accessibilityElement
self.appearanceAnimation = appearanceAnimation
@@ -40,6 +46,8 @@ public final class ImageBlock: ImageBaseBlock {
heightTrait: LayoutTrait = .intrinsic,
contentMode: ImageContentMode = .default,
tintColor: Color? = nil,
tintMode: TintMode? = nil,
effects: [ImageEffect] = [],
metalImageRenderingEnabled: Bool = false,
accessibilityElement: AccessibilityElement? = nil,
appearanceAnimation: TransitioningAnimation? = nil
@@ -50,6 +58,8 @@ public final class ImageBlock: ImageBaseBlock {
height: .trait(heightTrait),
contentMode: contentMode,
tintColor: tintColor,
tintMode: tintMode,
effects: effects,
metalImageRenderingEnabled: metalImageRenderingEnabled,
accessibilityElement: accessibilityElement,
appearanceAnimation: appearanceAnimation
@@ -61,6 +71,8 @@ public final class ImageBlock: ImageBaseBlock {
size: CGSize,
contentMode: ImageContentMode = .default,
tintColor: Color? = nil,
tintMode: TintMode? = nil,
effects: [ImageEffect] = [],
metalImageRenderingEnabled: Bool = false,
accessibilityElement: AccessibilityElement? = nil,
appearanceAnimation: TransitioningAnimation? = nil
@@ -71,6 +83,8 @@ public final class ImageBlock: ImageBaseBlock {
heightTrait: .fixed(size.height),
contentMode: contentMode,
tintColor: tintColor,
tintMode: tintMode,
effects: effects,
metalImageRenderingEnabled: metalImageRenderingEnabled,
accessibilityElement: accessibilityElement,
appearanceAnimation: appearanceAnimation
@@ -21,9 +21,9 @@ extension ImageBlock {
remoteImageViewContainer.imageHolder = imageHolder
}
remoteImageViewContainer.contentView.imageContentMode = contentMode
if let tintColor = tintColor {
remoteImageViewContainer.contentView.imageRedrawingStyle = .init(tintColor: tintColor)
}
remoteImageViewContainer.contentView.imageRedrawingStyle = ImageRedrawingStyle(tintColor: tintColor,
tintMode: tintMode,
effects: effects)
remoteImageViewContainer.contentView.isUserInteractionEnabled = false
remoteImageViewContainer.isUserInteractionEnabled = false
remoteImageViewContainer.applyAccessibility(accessibilityElement)
@@ -88,8 +88,12 @@ extension BackgroundView {
case .solidColor, .tiledImage:
view = ColoredView()
case .image:
view = RemoteImageViewContainer(contentView: RemoteImageView())
case .image(let backgroundImage):
if backgroundImage.metalImageRenderingEnabled {
view = RemoteImageViewContainer(contentView: MetalImageView())
} else {
view = RemoteImageViewContainer(contentView: RemoteImageView())
}
case .ninePatchImage:
view = RemoteImageViewContainer(contentView: NinePatchImageView())
@@ -148,6 +152,7 @@ extension BackgroundView {
imageViewContainer.contentView.clipsToBounds = true
imageViewContainer.contentView.alpha = CGFloat(image.alpha)
imageViewContainer.contentView.imageContentMode = image.contentMode
imageViewContainer.contentView.imageRedrawingStyle = ImageRedrawingStyle(tintColor: nil, effects: image.effects)
if imageViewContainer.imageHolder != image.imageHolder {
imageViewContainer.imageHolder = image.imageHolder
}
@@ -1,18 +1,25 @@
import BaseUI
import CommonCore
public struct BackgroundImage {
let imageHolder: ImageHolder
let contentMode: ImageContentMode
let alpha: Double
let effects: [ImageEffect]
let metalImageRenderingEnabled: Bool
public init(
imageHolder: ImageHolder,
contentMode: ImageContentMode = .default,
alpha: Double = 1.0
alpha: Double = 1.0,
effects: [ImageEffect],
metalImageRenderingEnabled: Bool
) {
self.imageHolder = imageHolder
self.contentMode = contentMode
self.alpha = alpha
self.effects = effects
self.metalImageRenderingEnabled = metalImageRenderingEnabled
}
}
@@ -20,7 +27,8 @@ extension BackgroundImage: Equatable {
public static func ==(lhs: BackgroundImage, rhs: BackgroundImage) -> Bool {
lhs.imageHolder == rhs.imageHolder &&
lhs.contentMode == rhs.contentMode &&
lhs.alpha == rhs.alpha
lhs.alpha == rhs.alpha &&
lhs.effects == rhs.effects
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 25 KiB