mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 14:20:20 +00:00
Various improvements
This commit is contained in:
@@ -43,6 +43,8 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/DynamicCornerRadiusView",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/WallpaperResources",
|
||||
"//submodules/MediaPickerUI",
|
||||
"//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Photos
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
@@ -29,6 +30,8 @@ import EmojiStatusComponent
|
||||
import DynamicCornerRadiusView
|
||||
import ComponentDisplayAdapters
|
||||
import WallpaperResources
|
||||
import MediaPickerUI
|
||||
import WallpaperGalleryScreen
|
||||
|
||||
private final class EmojiActionIconComponent: Component {
|
||||
let context: AccountContext
|
||||
@@ -590,6 +593,58 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
self.environment?.controller()?.push(statsController)
|
||||
}
|
||||
|
||||
private func openCustomWallpaperSetup() {
|
||||
guard let _ = self.component, let contentsData = self.contentsData else {
|
||||
return
|
||||
}
|
||||
// let dismissControllers = { [weak self] in
|
||||
// if let self, let navigationController = self.controller?.navigationController as? NavigationController {
|
||||
// let controllers = navigationController.viewControllers.filter({ controller in
|
||||
// if controller is WallpaperGalleryController || controller is AttachmentController || controller is PeerInfoScreenImpl {
|
||||
// return false
|
||||
// }
|
||||
// return true
|
||||
// })
|
||||
// navigationController.setViewControllers(controllers, animated: true)
|
||||
// }
|
||||
// }
|
||||
// var openWallpaperPickerImpl: ((Bool) -> Void)?
|
||||
let openWallpaperPicker: (Bool) -> Void = { [weak self] animateAppearance in
|
||||
guard let self, let component = self.component, let peer = contentsData.peer else {
|
||||
return
|
||||
}
|
||||
let controller = wallpaperMediaPickerController(
|
||||
context: component.context,
|
||||
updatedPresentationData: nil,
|
||||
peer: peer,
|
||||
animateAppearance: true,
|
||||
completion: { [weak self] _, result in
|
||||
guard let self, let component = self.component, let asset = result as? PHAsset else {
|
||||
return
|
||||
}
|
||||
let controller = WallpaperGalleryController(context: component.context, source: .asset(asset), mode: .peer(peer, false))
|
||||
controller.navigationPresentation = .modal
|
||||
controller.apply = { wallpaper, options, editedImage, cropRect, brightness, forBoth in
|
||||
// if let strongSelf = self {
|
||||
// uploadCustomPeerWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: peer.id, forBoth: forBoth, completion: {
|
||||
// Queue.mainQueue().after(0.3, {
|
||||
// dismissControllers()
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
}
|
||||
self.environment?.controller()?.push(controller)
|
||||
},
|
||||
openColors: {
|
||||
}
|
||||
)
|
||||
controller.navigationPresentation = .flatModal
|
||||
self.environment?.controller()?.push(controller)
|
||||
}
|
||||
// openWallpaperPickerImpl = openWallpaperPicker
|
||||
openWallpaperPicker(true)
|
||||
}
|
||||
|
||||
private enum EmojiSetupSubject {
|
||||
case reply
|
||||
case profile
|
||||
@@ -1055,7 +1110,7 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
self.openCustomWallpaperSetup()
|
||||
}
|
||||
)))
|
||||
]
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SettingsThemeWallpaperNode",
|
||||
module_name = "SettingsThemeWallpaperNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/GradientBackground",
|
||||
"//submodules/WallpaperResources",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/RadialStatusNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,308 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import RadialStatusNode
|
||||
import WallpaperResources
|
||||
import GradientBackground
|
||||
|
||||
private func whiteColorImage(theme: PresentationTheme, color: UIColor) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
return .single({ arguments in
|
||||
guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
context.withFlippedContext { c in
|
||||
c.setFillColor(color.cgColor)
|
||||
c.fill(CGRect(origin: CGPoint(), size: arguments.drawingSize))
|
||||
|
||||
let lineWidth: CGFloat = 1.0
|
||||
c.setLineWidth(lineWidth)
|
||||
c.setStrokeColor(theme.list.controlSecondaryColor.cgColor)
|
||||
c.stroke(CGRect(origin: CGPoint(), size: arguments.drawingSize).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
|
||||
}
|
||||
|
||||
return context
|
||||
})
|
||||
}
|
||||
|
||||
private let blackColorImage: UIImage? = {
|
||||
guard let context = DrawingContext(size: CGSize(width: 1.0, height: 1.0), scale: 1.0, opaque: true, clear: false) else {
|
||||
return nil
|
||||
}
|
||||
context.withContext { c in
|
||||
c.setFillColor(UIColor.black.cgColor)
|
||||
c.fill(CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)))
|
||||
}
|
||||
return context.generateImage()
|
||||
}()
|
||||
|
||||
public final class SettingsThemeWallpaperNode: ASDisplayNode {
|
||||
public var wallpaper: TelegramWallpaper?
|
||||
private var arguments: PatternWallpaperArguments?
|
||||
|
||||
public let buttonNode = HighlightTrackingButtonNode()
|
||||
public let backgroundNode = ASImageNode()
|
||||
public let imageNode = TransformImageNode()
|
||||
private var gradientNode: GradientBackgroundNode?
|
||||
private let statusNode: RadialStatusNode
|
||||
|
||||
public var pressed: (() -> Void)?
|
||||
|
||||
private let displayLoading: Bool
|
||||
private var isSelected: Bool = false
|
||||
private var isLoaded: Bool = false
|
||||
|
||||
private let isLoadedDisposable = MetaDisposable()
|
||||
|
||||
public init(displayLoading: Bool = false, overlayBackgroundColor: UIColor = UIColor(white: 0.0, alpha: 0.3)) {
|
||||
self.displayLoading = displayLoading
|
||||
self.imageNode.contentAnimations = [.subsequentUpdates]
|
||||
|
||||
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.2), enableBlur: true)
|
||||
let progressDiameter: CGFloat = 50.0
|
||||
self.statusNode.frame = CGRect(x: 0.0, y: 0.0, width: progressDiameter, height: progressDiameter)
|
||||
self.statusNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
self.addSubnode(self.statusNode)
|
||||
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.isLoadedDisposable.dispose()
|
||||
}
|
||||
|
||||
public func setSelected(_ selected: Bool, animated: Bool = false) {
|
||||
if self.isSelected != selected {
|
||||
self.isSelected = selected
|
||||
|
||||
self.updateStatus(animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateIsLoaded(isLoaded: Bool, animated: Bool) {
|
||||
if self.isLoaded != isLoaded {
|
||||
self.isLoaded = isLoaded
|
||||
self.updateStatus(animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStatus(animated: Bool) {
|
||||
if self.isSelected {
|
||||
if self.isLoaded || !displayLoading {
|
||||
self.statusNode.transitionToState(.check(.white), animated: animated, completion: {})
|
||||
} else {
|
||||
self.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: animated, completion: {})
|
||||
}
|
||||
} else {
|
||||
self.statusNode.transitionToState(.none, animated: animated, completion: {})
|
||||
}
|
||||
}
|
||||
|
||||
public func setOverlayBackgroundColor(_ color: UIColor) {
|
||||
self.statusNode.backgroundNodeColor = color
|
||||
}
|
||||
|
||||
public func setWallpaper(context: AccountContext, wallpaper: TelegramWallpaper, selected: Bool, size: CGSize, cornerRadius: CGFloat = 0.0, synchronousLoad: Bool = false) {
|
||||
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
var colors: [UInt32] = []
|
||||
var intensity: CGFloat = 0.5
|
||||
if case let .gradient(gradient) = wallpaper {
|
||||
colors = gradient.colors
|
||||
} else if case let .file(file) = wallpaper {
|
||||
colors = file.settings.colors
|
||||
intensity = CGFloat(file.settings.intensity ?? 50) / 100.0
|
||||
} else if case let .color(color) = wallpaper {
|
||||
colors = [color]
|
||||
}
|
||||
let isBlack = UIColor.average(of: colors.map(UIColor.init(rgb:))).hsb.b <= 0.01
|
||||
if colors.count >= 3 {
|
||||
if let gradientNode = self.gradientNode {
|
||||
gradientNode.updateColors(colors: colors.map { UIColor(rgb: $0) })
|
||||
} else {
|
||||
let gradientNode = createGradientBackgroundNode()
|
||||
gradientNode.isUserInteractionEnabled = false
|
||||
self.gradientNode = gradientNode
|
||||
gradientNode.updateColors(colors: colors.map { UIColor(rgb: $0) })
|
||||
self.insertSubnode(gradientNode, belowSubnode: self.imageNode)
|
||||
}
|
||||
|
||||
if intensity < 0.0 {
|
||||
self.imageNode.layer.compositingFilter = nil
|
||||
} else {
|
||||
if isBlack {
|
||||
self.imageNode.layer.compositingFilter = nil
|
||||
} else {
|
||||
self.imageNode.layer.compositingFilter = "softLightBlendMode"
|
||||
}
|
||||
}
|
||||
self.backgroundNode.image = nil
|
||||
} else {
|
||||
if let gradientNode = self.gradientNode {
|
||||
self.gradientNode = nil
|
||||
gradientNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if intensity < 0.0 {
|
||||
self.imageNode.layer.compositingFilter = nil
|
||||
} else {
|
||||
if isBlack {
|
||||
self.imageNode.layer.compositingFilter = nil
|
||||
} else {
|
||||
self.imageNode.layer.compositingFilter = "softLightBlendMode"
|
||||
}
|
||||
}
|
||||
|
||||
if colors.count >= 2 {
|
||||
self.backgroundNode.image = generateGradientImage(size: CGSize(width: 80.0, height: 80.0), colors: colors.map(UIColor.init(rgb:)), locations: [0.0, 1.0], direction: .vertical)
|
||||
self.backgroundNode.backgroundColor = nil
|
||||
} else if colors.count >= 1 {
|
||||
self.backgroundNode.image = nil
|
||||
self.backgroundNode.backgroundColor = UIColor(rgb: colors[0])
|
||||
}
|
||||
}
|
||||
|
||||
if let gradientNode = self.gradientNode {
|
||||
gradientNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
gradientNode.updateLayout(size: size, transition: .immediate, extendAnimation: false, backwards: false, completion: {})
|
||||
}
|
||||
|
||||
let progressDiameter: CGFloat = 50.0
|
||||
self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - progressDiameter) / 2.0), y: floorToScreenPixels((size.height - progressDiameter) / 2.0), width: progressDiameter, height: progressDiameter)
|
||||
|
||||
let corners = ImageCorners(radius: cornerRadius)
|
||||
|
||||
if self.wallpaper != wallpaper {
|
||||
self.wallpaper = wallpaper
|
||||
switch wallpaper {
|
||||
case .builtin:
|
||||
self.imageNode.alpha = 1.0
|
||||
self.imageNode.setSignal(settingsBuiltinWallpaperImage(account: context.account, thumbnail: true))
|
||||
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
|
||||
apply()
|
||||
self.isLoadedDisposable.set(nil)
|
||||
self.updateIsLoaded(isLoaded: true, animated: false)
|
||||
case let .image(representations, _):
|
||||
let convertedRepresentations: [ImageRepresentationWithReference] = representations.map({ ImageRepresentationWithReference(representation: $0, reference: .wallpaper(wallpaper: nil, resource: $0.resource)) })
|
||||
self.imageNode.alpha = 1.0
|
||||
self.imageNode.setSignal(wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, thumbnail: true, autoFetchFullSize: true, synchronousLoad: synchronousLoad))
|
||||
|
||||
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: largestImageRepresentation(representations)!.dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
|
||||
apply()
|
||||
self.isLoadedDisposable.set(nil)
|
||||
self.updateIsLoaded(isLoaded: true, animated: false)
|
||||
case let .file(file):
|
||||
let convertedRepresentations : [ImageRepresentationWithReference] = file.file.previewRepresentations.map {
|
||||
ImageRepresentationWithReference(representation: $0, reference: .wallpaper(wallpaper: .slug(file.slug), resource: $0.resource))
|
||||
}
|
||||
|
||||
let fullDimensions = file.file.dimensions ?? PixelDimensions(width: 2000, height: 4000)
|
||||
let convertedFullRepresentations = [ImageRepresentationWithReference(representation: .init(dimensions: fullDimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))]
|
||||
|
||||
let imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>
|
||||
if wallpaper.isPattern {
|
||||
var patternIntensity: CGFloat = 0.5
|
||||
if !file.settings.colors.isEmpty {
|
||||
if let intensity = file.settings.intensity {
|
||||
patternIntensity = CGFloat(intensity) / 100.0
|
||||
}
|
||||
}
|
||||
|
||||
if patternIntensity < 0.0 {
|
||||
self.imageNode.alpha = 1.0
|
||||
self.arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: UIColor(white: 0.0, alpha: 1.0 + patternIntensity))
|
||||
} else {
|
||||
self.imageNode.alpha = CGFloat(file.settings.intensity ?? 50) / 100.0
|
||||
let isLight = UIColor.average(of: file.settings.colors.map(UIColor.init(rgb:))).hsb.b > 0.3
|
||||
self.arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: isLight ? .black : .white)
|
||||
}
|
||||
imageSignal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: .thumbnail, autoFetchFullSize: true)
|
||||
|> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in
|
||||
if let value = value {
|
||||
return .single(value)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|
||||
let anyStatus = combineLatest(queue: .mainQueue(),
|
||||
context.account.postbox.mediaBox.resourceStatus(convertedFullRepresentations[0].reference.resource, approximateSynchronousValue: true),
|
||||
context.sharedContext.accountManager.mediaBox.resourceStatus(convertedFullRepresentations[0].reference.resource, approximateSynchronousValue: true)
|
||||
)
|
||||
|> map { a, b -> Bool in
|
||||
switch a {
|
||||
case .Local:
|
||||
return true
|
||||
default:
|
||||
break
|
||||
}
|
||||
switch b {
|
||||
case .Local:
|
||||
return true
|
||||
default:
|
||||
break
|
||||
}
|
||||
return false
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
self.updateIsLoaded(isLoaded: false, animated: false)
|
||||
self.isLoadedDisposable.set((anyStatus
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateIsLoaded(isLoaded: value, animated: true)
|
||||
}))
|
||||
} else {
|
||||
self.imageNode.alpha = 1.0
|
||||
|
||||
imageSignal = wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, thumbnail: true, autoFetchFullSize: true, blurred: file.settings.blur, synchronousLoad: synchronousLoad)
|
||||
|
||||
self.updateIsLoaded(isLoaded: true, animated: false)
|
||||
self.isLoadedDisposable.set(nil)
|
||||
}
|
||||
self.imageNode.setSignal(imageSignal, attemptSynchronously: synchronousLoad)
|
||||
|
||||
let dimensions = file.file.dimensions ?? PixelDimensions(width: 100, height: 100)
|
||||
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets(), custom: self.arguments))
|
||||
apply()
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else if let wallpaper = self.wallpaper {
|
||||
switch wallpaper {
|
||||
case .builtin, .color, .gradient:
|
||||
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
|
||||
apply()
|
||||
case let .image(representations, _):
|
||||
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: largestImageRepresentation(representations)!.dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
|
||||
apply()
|
||||
case let .file(file):
|
||||
let dimensions = file.file.dimensions ?? PixelDimensions(width: 100, height: 100)
|
||||
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets(), custom: self.arguments))
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
self.setSelected(selected, animated: false)
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
self.pressed?()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "WallpaperGalleryScreen",
|
||||
module_name = "WallpaperGalleryScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/SolidRoundedButtonNode",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/PremiumUI",
|
||||
"//submodules/WallpaperResources",
|
||||
"//submodules/HexColor",
|
||||
"//submodules/MergeLists",
|
||||
"//submodules/ShareController",
|
||||
"//submodules/GalleryUI",
|
||||
"//submodules/CounterContollerTitleView",
|
||||
"//submodules/LegacyMediaPickerUI",
|
||||
"//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,134 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Accelerate
|
||||
import ImageBlur
|
||||
|
||||
private class BlurLayer: CALayer {
|
||||
private static let blurRadiusKey = "blurRadius"
|
||||
@NSManaged var blurRadius: CGFloat
|
||||
|
||||
private var fromBlurRadius: CGFloat?
|
||||
var presentationRadius: CGFloat {
|
||||
if let radius = self.fromBlurRadius {
|
||||
if let layer = presentation() {
|
||||
return layer.blurRadius
|
||||
} else {
|
||||
return radius
|
||||
}
|
||||
} else {
|
||||
return self.blurRadius
|
||||
}
|
||||
}
|
||||
|
||||
override class func needsDisplay(forKey key: String) -> Bool {
|
||||
if key == blurRadiusKey {
|
||||
return true
|
||||
}
|
||||
return super.needsDisplay(forKey: key)
|
||||
}
|
||||
|
||||
open override func action(forKey event: String) -> CAAction? {
|
||||
if event == BlurLayer.blurRadiusKey {
|
||||
self.fromBlurRadius = nil
|
||||
|
||||
if let action = super.action(forKey: "opacity") as? CABasicAnimation {
|
||||
self.fromBlurRadius = (presentation() ?? self).blurRadius
|
||||
|
||||
action.keyPath = event
|
||||
action.fromValue = self.fromBlurRadius
|
||||
return action
|
||||
}
|
||||
}
|
||||
|
||||
return super.action(forKey: event)
|
||||
}
|
||||
|
||||
func draw(_ image: UIImage) {
|
||||
self.contents = image.cgImage
|
||||
self.contentsScale = image.scale
|
||||
self.contentsGravity = .resizeAspectFill
|
||||
}
|
||||
|
||||
func render(in context: CGContext, for layer: CALayer) {
|
||||
layer.render(in: context)
|
||||
}
|
||||
}
|
||||
|
||||
public class BlurView: UIView {
|
||||
public override class var layerClass : AnyClass {
|
||||
return BlurLayer.self
|
||||
}
|
||||
|
||||
private var blurLayer: BlurLayer {
|
||||
return self.layer as! BlurLayer
|
||||
}
|
||||
|
||||
var image: UIImage?
|
||||
|
||||
private let queue: Queue = {
|
||||
return Queue(name: nil, qos: .userInteractive)
|
||||
}()
|
||||
|
||||
public var blurRadius: CGFloat {
|
||||
set { self.blurLayer.blurRadius = newValue }
|
||||
get { return self.blurLayer.blurRadius }
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
self.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
private func async(on queue: DispatchQueue, actions: @escaping () -> Void) {
|
||||
queue.async(execute: actions)
|
||||
}
|
||||
|
||||
private func sync(on queue: DispatchQueue, actions: () -> Void) {
|
||||
queue.sync(execute: actions)
|
||||
}
|
||||
|
||||
private func draw(_ image: UIImage, blurRadius: CGFloat) {
|
||||
self.queue.async { [weak self] in
|
||||
if let strongSelf = self, let blurredImage = blurredImage(image, radius: blurRadius) {
|
||||
Queue.mainQueue().sync {
|
||||
strongSelf.blurLayer.draw(blurredImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override func display(_ layer: CALayer) {
|
||||
let blurRadius = self.blurLayer.presentationRadius
|
||||
if let image = self.image {
|
||||
self.draw(image, blurRadius: blurRadius)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class BlurredImageNode: ASDisplayNode {
|
||||
public var image: UIImage? {
|
||||
didSet {
|
||||
self.blurView.image = self.image
|
||||
self.blurView.layer.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
public var blurView: BlurView {
|
||||
return (self.view as? BlurView)!
|
||||
}
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return BlurView()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,744 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import HexColor
|
||||
|
||||
private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, UIImage)?
|
||||
private func textInputBackgroundImage(fieldColor: UIColor, strokeColor: UIColor, diameter: CGFloat) -> UIImage? {
|
||||
if let current = currentTextInputBackgroundImage {
|
||||
if current.0.isEqual(fieldColor) && current.1.isEqual(strokeColor) && current.2.isEqual(to: diameter) {
|
||||
return current.3
|
||||
}
|
||||
}
|
||||
|
||||
let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
|
||||
context.clear(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter))
|
||||
context.setFillColor(fieldColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter))
|
||||
context.setStrokeColor(strokeColor.cgColor)
|
||||
let strokeWidth: CGFloat = 1.0
|
||||
context.setLineWidth(strokeWidth)
|
||||
context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth))
|
||||
})?.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2)
|
||||
if let image = image {
|
||||
currentTextInputBackgroundImage = (fieldColor, strokeColor, diameter, image)
|
||||
return image
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func generateSwatchBorderImage(theme: PresentationTheme) -> UIImage? {
|
||||
return nil
|
||||
}
|
||||
|
||||
private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate {
|
||||
private var theme: PresentationTheme
|
||||
|
||||
private let swatchNode: ASDisplayNode
|
||||
private let borderNode: ASImageNode
|
||||
private let removeButton: HighlightableButtonNode
|
||||
private let textBackgroundNode: ASImageNode
|
||||
private let selectionNode: ASDisplayNode
|
||||
let textFieldNode: TextFieldNode
|
||||
private let measureNode: ImmediateTextNode
|
||||
private let prefixNode: ASTextNode
|
||||
|
||||
private var gestureRecognizer: UITapGestureRecognizer?
|
||||
|
||||
var colorChanged: ((UIColor, Bool) -> Void)?
|
||||
var colorRemoved: (() -> Void)?
|
||||
var colorSelected: (() -> Void)?
|
||||
|
||||
private var color: UIColor?
|
||||
|
||||
private var isDefault = false {
|
||||
didSet {
|
||||
self.updateSelectionVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
var isRemovable: Bool = false {
|
||||
didSet {
|
||||
self.removeButton.isUserInteractionEnabled = self.isRemovable
|
||||
}
|
||||
}
|
||||
|
||||
private var previousIsDefault: Bool?
|
||||
private var previousColor: UIColor?
|
||||
private var validLayout: (CGSize, Bool)?
|
||||
|
||||
private var skipEndEditing = false
|
||||
|
||||
private let displaySwatch: Bool
|
||||
|
||||
init(theme: PresentationTheme, displaySwatch: Bool = true) {
|
||||
self.theme = theme
|
||||
|
||||
self.displaySwatch = displaySwatch
|
||||
|
||||
self.textBackgroundNode = ASImageNode()
|
||||
self.textBackgroundNode.image = textInputBackgroundImage(fieldColor: theme.chat.inputPanel.inputBackgroundColor, strokeColor: theme.chat.inputPanel.inputStrokeColor, diameter: 33.0)
|
||||
self.textBackgroundNode.displayWithoutProcessing = true
|
||||
self.textBackgroundNode.displaysAsynchronously = false
|
||||
|
||||
self.selectionNode = ASDisplayNode()
|
||||
self.selectionNode.backgroundColor = theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.2)
|
||||
self.selectionNode.cornerRadius = 3.0
|
||||
self.selectionNode.isUserInteractionEnabled = false
|
||||
|
||||
self.textFieldNode = TextFieldNode()
|
||||
self.measureNode = ImmediateTextNode()
|
||||
|
||||
self.prefixNode = ASTextNode()
|
||||
self.prefixNode.attributedText = NSAttributedString(string: "#", font: Font.regular(17.0), textColor: self.theme.chat.inputPanel.inputTextColor)
|
||||
|
||||
self.swatchNode = ASDisplayNode()
|
||||
self.swatchNode.cornerRadius = 10.5
|
||||
|
||||
self.borderNode = ASImageNode()
|
||||
self.borderNode.displaysAsynchronously = false
|
||||
self.borderNode.displayWithoutProcessing = true
|
||||
self.borderNode.image = generateSwatchBorderImage(theme: theme)
|
||||
|
||||
self.removeButton = HighlightableButtonNode()
|
||||
self.removeButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRemoveIcon"), color: theme.chat.inputPanel.inputControlColor), for: .normal)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.textBackgroundNode)
|
||||
self.addSubnode(self.selectionNode)
|
||||
self.addSubnode(self.textFieldNode)
|
||||
self.addSubnode(self.prefixNode)
|
||||
self.addSubnode(self.swatchNode)
|
||||
self.addSubnode(self.borderNode)
|
||||
self.addSubnode(self.removeButton)
|
||||
|
||||
self.removeButton.addTarget(self, action: #selector(self.removePressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.textFieldNode.textField.font = Font.regular(17.0)
|
||||
self.textFieldNode.textField.textColor = self.theme.chat.inputPanel.inputTextColor
|
||||
self.textFieldNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.textFieldNode.textField.autocorrectionType = .no
|
||||
self.textFieldNode.textField.autocapitalizationType = .allCharacters
|
||||
self.textFieldNode.textField.keyboardType = .asciiCapable
|
||||
self.textFieldNode.textField.returnKeyType = .done
|
||||
self.textFieldNode.textField.delegate = self
|
||||
self.textFieldNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged)
|
||||
self.textFieldNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
self.textFieldNode.textField.tintColor = self.theme.list.itemAccentColor
|
||||
|
||||
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapped(_:)))
|
||||
self.view.addGestureRecognizer(gestureRecognizer)
|
||||
self.gestureRecognizer = gestureRecognizer
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.textBackgroundNode.image = textInputBackgroundImage(fieldColor: self.theme.chat.inputPanel.inputBackgroundColor, strokeColor: self.theme.chat.inputPanel.inputStrokeColor, diameter: 33.0)
|
||||
|
||||
self.textFieldNode.textField.textColor = self.isDefault ? self.theme.chat.inputPanel.inputPlaceholderColor : self.theme.chat.inputPanel.inputTextColor
|
||||
self.textFieldNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.textFieldNode.textField.tintColor = self.theme.list.itemAccentColor
|
||||
|
||||
self.selectionNode.backgroundColor = theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.2)
|
||||
self.borderNode.image = generateSwatchBorderImage(theme: theme)
|
||||
self.updateBorderVisibility()
|
||||
}
|
||||
|
||||
func setColor(_ color: UIColor, isDefault: Bool = false, update: Bool = true, ended: Bool = true) {
|
||||
self.color = color
|
||||
self.isDefault = isDefault
|
||||
let text = color.hexString.uppercased()
|
||||
self.textFieldNode.textField.text = text
|
||||
self.textFieldNode.textField.textColor = isDefault ? self.theme.chat.inputPanel.inputPlaceholderColor : self.theme.chat.inputPanel.inputTextColor
|
||||
if let (size, _) = self.validLayout {
|
||||
self.updateSelectionLayout(size: size, transition: .immediate)
|
||||
}
|
||||
if update {
|
||||
self.colorChanged?(color, ended)
|
||||
}
|
||||
self.swatchNode.backgroundColor = color
|
||||
self.updateBorderVisibility()
|
||||
}
|
||||
|
||||
private func updateBorderVisibility() {
|
||||
guard let color = self.swatchNode.backgroundColor else {
|
||||
return
|
||||
}
|
||||
let inputBackgroundColor = self.theme.chat.inputPanel.inputBackgroundColor
|
||||
if color.distance(to: inputBackgroundColor) < 200 {
|
||||
self.borderNode.alpha = 1.0
|
||||
} else {
|
||||
self.borderNode.alpha = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func removePressed() {
|
||||
if self.textFieldNode.textField.isFirstResponder {
|
||||
self.skipEndEditing = true
|
||||
}
|
||||
|
||||
self.colorRemoved?()
|
||||
}
|
||||
|
||||
@objc private func tapped(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.colorSelected?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc internal func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
var updated = textField.text ?? ""
|
||||
updated.replaceSubrange(updated.index(updated.startIndex, offsetBy: range.lowerBound) ..< updated.index(updated.startIndex, offsetBy: range.upperBound), with: string)
|
||||
if updated.hasPrefix("#") {
|
||||
updated.removeFirst()
|
||||
}
|
||||
if updated.count <= 6 && updated.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil {
|
||||
textField.text = updated.uppercased()
|
||||
textField.textColor = self.theme.chat.inputPanel.inputTextColor
|
||||
|
||||
if updated.count == 6, let color = UIColor(hexString: updated) {
|
||||
self.setColor(color)
|
||||
}
|
||||
|
||||
if let (size, _) = self.validLayout {
|
||||
self.updateSelectionLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@objc func textFieldTextChanged(_ sender: UITextField) {
|
||||
if let color = self.colorFromCurrentText() {
|
||||
self.setColor(color)
|
||||
}
|
||||
|
||||
if let (size, _) = self.validLayout {
|
||||
self.updateSelectionLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
self.skipEndEditing = true
|
||||
if let color = self.colorFromCurrentText() {
|
||||
self.setColor(color)
|
||||
} else {
|
||||
self.setColor(self.previousColor ?? .black, isDefault: self.previousIsDefault ?? false)
|
||||
}
|
||||
self.textFieldNode.textField.resignFirstResponder()
|
||||
return false
|
||||
}
|
||||
|
||||
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
||||
self.skipEndEditing = false
|
||||
self.previousColor = self.color
|
||||
self.previousIsDefault = self.isDefault
|
||||
|
||||
textField.textColor = self.theme.chat.inputPanel.inputTextColor
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@objc func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
if !self.skipEndEditing {
|
||||
if let color = self.colorFromCurrentText() {
|
||||
self.setColor(color)
|
||||
} else {
|
||||
self.setColor(self.previousColor ?? .black, isDefault: self.previousIsDefault ?? false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setSkipEndEditingIfNeeded() {
|
||||
if self.textFieldNode.textField.isFirstResponder && self.colorFromCurrentText() != nil {
|
||||
self.skipEndEditing = true
|
||||
}
|
||||
}
|
||||
|
||||
private func colorFromCurrentText() -> UIColor? {
|
||||
if let text = self.textFieldNode.textField.text, text.count == 6, let color = UIColor(hexString: text) {
|
||||
return color
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSelectionLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
self.measureNode.attributedText = NSAttributedString(string: self.textFieldNode.textField.text ?? "", font: self.textFieldNode.textField.font)
|
||||
let size = self.measureNode.updateLayout(size)
|
||||
transition.updateFrame(node: self.selectionNode, frame: CGRect(x: self.textFieldNode.frame.minX, y: 6.0, width: max(0.0, size.width), height: 20.0))
|
||||
}
|
||||
|
||||
private func updateSelectionVisibility() {
|
||||
self.selectionNode.isHidden = true
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, condensed: Bool, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (size, condensed)
|
||||
|
||||
let swatchFrame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 21.0, height: 21.0))
|
||||
transition.updateFrame(node: self.swatchNode, frame: swatchFrame)
|
||||
transition.updateFrame(node: self.borderNode, frame: swatchFrame)
|
||||
|
||||
self.swatchNode.isHidden = !self.displaySwatch
|
||||
|
||||
let textPadding: CGFloat
|
||||
if self.displaySwatch {
|
||||
textPadding = condensed ? 31.0 : 37.0
|
||||
} else {
|
||||
textPadding = 12.0
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.textBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
|
||||
transition.updateFrame(node: self.textFieldNode, frame: CGRect(x: textPadding + 10.0, y: 1.0, width: size.width - (21.0 + textPadding), height: size.height - 2.0))
|
||||
|
||||
self.updateSelectionLayout(size: size, transition: transition)
|
||||
|
||||
let prefixSize = self.prefixNode.measure(size)
|
||||
transition.updateFrame(node: self.prefixNode, frame: CGRect(origin: CGPoint(x: textPadding - UIScreenPixel, y: 6.0), size: prefixSize))
|
||||
|
||||
let removeSize = CGSize(width: 33.0, height: 33.0)
|
||||
let removeOffset: CGFloat = condensed ? 3.0 : 0.0
|
||||
transition.updateFrame(node: self.removeButton, frame: CGRect(origin: CGPoint(x: size.width - removeSize.width + removeOffset, y: 0.0), size: removeSize))
|
||||
self.removeButton.alpha = self.isRemovable ? 1.0 : 0.0
|
||||
}
|
||||
}
|
||||
|
||||
public struct WallpaperColorPanelNodeState: Equatable {
|
||||
public var selection: Int?
|
||||
public var colors: [HSBColor]
|
||||
public var maximumNumberOfColors: Int
|
||||
public var rotateAvailable: Bool
|
||||
public var rotation: Int32
|
||||
public var preview: Bool
|
||||
public var simpleGradientGeneration: Bool
|
||||
public var suggestedNewColor: HSBColor?
|
||||
|
||||
public init(selection: Int? = nil, colors: [HSBColor], maximumNumberOfColors: Int, rotateAvailable: Bool, rotation: Int32, preview: Bool, simpleGradientGeneration: Bool, suggestedNewColor: HSBColor? = nil) {
|
||||
self.selection = selection
|
||||
self.colors = colors
|
||||
self.maximumNumberOfColors = maximumNumberOfColors
|
||||
self.rotateAvailable = rotateAvailable
|
||||
self.rotation = rotation
|
||||
self.preview = preview
|
||||
self.simpleGradientGeneration = simpleGradientGeneration
|
||||
self.suggestedNewColor = suggestedNewColor
|
||||
}
|
||||
}
|
||||
|
||||
private final class ColorSampleItemNode: ASImageNode {
|
||||
private struct State: Equatable {
|
||||
var color: UInt32
|
||||
var size: CGSize
|
||||
var isSelected: Bool
|
||||
}
|
||||
|
||||
private var action: () -> Void
|
||||
private var validState: State?
|
||||
|
||||
init(action: @escaping () -> Void) {
|
||||
self.action = action
|
||||
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = true
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize, color: UIColor, isSelected: Bool) {
|
||||
let state = State(color: color.rgb, size: size, isSelected: isSelected)
|
||||
if self.validState != state {
|
||||
self.validState = state
|
||||
|
||||
self.image = generateImage(CGSize(width: size.width, height: size.height), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setBlendMode(.softLight)
|
||||
context.setStrokeColor(UIColor(white: 0.0, alpha: 0.3).cgColor)
|
||||
context.setLineWidth(UIScreenPixel)
|
||||
context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: UIScreenPixel, dy: UIScreenPixel))
|
||||
|
||||
if isSelected {
|
||||
context.setBlendMode(.copy)
|
||||
context.setStrokeColor(UIColor.clear.cgColor)
|
||||
let lineWidth: CGFloat = 2.0
|
||||
context.setLineWidth(lineWidth)
|
||||
let inset: CGFloat = 2.0 + lineWidth / 2.0
|
||||
context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: inset, dy: inset))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class WallpaperColorPanelNode: ASDisplayNode {
|
||||
private var theme: PresentationTheme
|
||||
|
||||
private var state: WallpaperColorPanelNodeState
|
||||
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
private let topSeparatorNode: ASDisplayNode
|
||||
private let bottomSeparatorNode: ASDisplayNode
|
||||
private let rotateButton: HighlightableButtonNode
|
||||
private let swapButton: HighlightableButtonNode
|
||||
private let addButton: HighlightableButtonNode
|
||||
private let doneButton: HighlightableButtonNode
|
||||
private let colorPickerNode: WallpaperColorPickerNode
|
||||
|
||||
private var sampleItemNodes: [ColorSampleItemNode] = []
|
||||
private let multiColorFieldNode: ColorInputFieldNode
|
||||
|
||||
public var colorsChanged: (([HSBColor], Int, Bool) -> Void)?
|
||||
public var colorSelected: (() -> Void)?
|
||||
public var rotate: (() -> Void)?
|
||||
|
||||
public var colorAdded: (() -> Void)?
|
||||
public var colorRemoved: (() -> Void)?
|
||||
|
||||
private var validLayout: (CGSize, CGFloat)?
|
||||
|
||||
public init(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundNode = NavigationBackgroundNode(color: theme.chat.inputPanel.panelBackgroundColor)
|
||||
|
||||
self.topSeparatorNode = ASDisplayNode()
|
||||
self.topSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor
|
||||
self.bottomSeparatorNode = ASDisplayNode()
|
||||
self.bottomSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor
|
||||
|
||||
self.doneButton = HighlightableButtonNode()
|
||||
self.doneButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(theme), for: .normal)
|
||||
|
||||
self.colorPickerNode = WallpaperColorPickerNode(strings: strings)
|
||||
|
||||
self.rotateButton = HighlightableButtonNode()
|
||||
self.rotateButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRotateIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal)
|
||||
self.swapButton = HighlightableButtonNode()
|
||||
self.swapButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorSwapIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal)
|
||||
self.addButton = HighlightableButtonNode()
|
||||
self.addButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorAddIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal)
|
||||
|
||||
self.multiColorFieldNode = ColorInputFieldNode(theme: theme, displaySwatch: false)
|
||||
|
||||
self.state = WallpaperColorPanelNodeState(
|
||||
selection: 0,
|
||||
colors: [],
|
||||
maximumNumberOfColors: 1,
|
||||
rotateAvailable: false,
|
||||
rotation: 0,
|
||||
preview: false,
|
||||
simpleGradientGeneration: false
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = .white
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.topSeparatorNode)
|
||||
self.addSubnode(self.bottomSeparatorNode)
|
||||
self.addSubnode(self.multiColorFieldNode)
|
||||
self.addSubnode(self.doneButton)
|
||||
self.addSubnode(self.colorPickerNode)
|
||||
|
||||
self.addSubnode(self.rotateButton)
|
||||
self.addSubnode(self.swapButton)
|
||||
self.addSubnode(self.addButton)
|
||||
|
||||
self.rotateButton.addTarget(self, action: #selector(self.rotatePressed), forControlEvents: .touchUpInside)
|
||||
self.swapButton.addTarget(self, action: #selector(self.swapPressed), forControlEvents: .touchUpInside)
|
||||
self.addButton.addTarget(self, action: #selector(self.addPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.multiColorFieldNode.colorChanged = { [weak self] color, ended in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateState({ current in
|
||||
var updated = current
|
||||
updated.preview = !ended
|
||||
if let index = strongSelf.state.selection {
|
||||
updated.colors[index] = HSBColor(color: color)
|
||||
}
|
||||
return updated
|
||||
})
|
||||
}
|
||||
}
|
||||
self.multiColorFieldNode.colorRemoved = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.colorRemoved?()
|
||||
strongSelf.updateState({ current in
|
||||
var updated = current
|
||||
if let index = strongSelf.state.selection {
|
||||
updated.colors.remove(at: index)
|
||||
if updated.colors.isEmpty {
|
||||
updated.selection = nil
|
||||
} else {
|
||||
updated.selection = max(0, min(index - 1, updated.colors.count - 1))
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}, animated: strongSelf.state.colors.count >= 2)
|
||||
}
|
||||
}
|
||||
|
||||
self.colorPickerNode.colorChanged = { [weak self] color in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateState({ current in
|
||||
var updated = current
|
||||
updated.preview = true
|
||||
if let index = strongSelf.state.selection {
|
||||
updated.colors[index] = color
|
||||
}
|
||||
return updated
|
||||
}, updateLayout: false)
|
||||
}
|
||||
}
|
||||
self.colorPickerNode.colorChangeEnded = { [weak self] color in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateState({ current in
|
||||
var updated = current
|
||||
updated.preview = false
|
||||
if let index = strongSelf.state.selection {
|
||||
updated.colors[index] = color
|
||||
}
|
||||
return updated
|
||||
}, updateLayout: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateTheme(_ theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
self.backgroundNode.updateColor(color: self.theme.chat.inputPanel.panelBackgroundColor, transition: .immediate)
|
||||
self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor
|
||||
self.bottomSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor
|
||||
self.multiColorFieldNode.updateTheme(theme)
|
||||
}
|
||||
|
||||
public func updateState(_ f: (WallpaperColorPanelNodeState) -> WallpaperColorPanelNodeState, updateLayout: Bool = true, animated: Bool = true) {
|
||||
var updateLayout = updateLayout
|
||||
let previousColors = self.state.colors
|
||||
let previousPreview = self.state.preview
|
||||
let previousSelection = self.state.selection
|
||||
self.state = f(self.state)
|
||||
|
||||
let colorWasRemovable = self.multiColorFieldNode.isRemovable
|
||||
self.multiColorFieldNode.isRemovable = self.state.colors.count > 1
|
||||
if colorWasRemovable != self.multiColorFieldNode.isRemovable {
|
||||
updateLayout = true
|
||||
}
|
||||
|
||||
if let index = self.state.selection {
|
||||
if self.state.colors.count > index {
|
||||
self.colorPickerNode.color = self.state.colors[index]
|
||||
}
|
||||
}
|
||||
|
||||
if updateLayout, let (size, bottomInset) = self.validLayout {
|
||||
self.updateLayout(size: size, bottomInset: bottomInset, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
|
||||
}
|
||||
|
||||
if let index = self.state.selection {
|
||||
if self.state.colors.count > index {
|
||||
self.multiColorFieldNode.setColor(self.state.colors[index].color, update: false)
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0 ..< self.state.colors.count {
|
||||
if i < self.sampleItemNodes.count {
|
||||
self.sampleItemNodes[i].update(size: self.sampleItemNodes[i].bounds.size, color: self.state.colors[i].color, isSelected: state.selection == i)
|
||||
}
|
||||
}
|
||||
|
||||
if self.state.colors != previousColors || self.state.preview != previousPreview || self.state.selection != previousSelection {
|
||||
self.colorsChanged?(self.state.colors, self.state.selection ?? 0, !self.state.preview)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (size, bottomInset)
|
||||
|
||||
let condensedLayout = size.width < 375.0
|
||||
let separatorHeight = UIScreenPixel
|
||||
let topPanelHeight: CGFloat = 47.0
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight))
|
||||
self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition)
|
||||
transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: separatorHeight))
|
||||
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(x: 0.0, y: topPanelHeight, width: size.width, height: separatorHeight))
|
||||
|
||||
let fieldHeight: CGFloat = 33.0
|
||||
let leftInset: CGFloat
|
||||
let rightInset: CGFloat
|
||||
if condensedLayout {
|
||||
leftInset = 6.0
|
||||
rightInset = 6.0
|
||||
} else {
|
||||
leftInset = 15.0
|
||||
rightInset = 15.0
|
||||
}
|
||||
|
||||
let buttonSize = CGSize(width: 26.0, height: 26.0)
|
||||
let canAddColors = self.state.colors.count < self.state.maximumNumberOfColors
|
||||
|
||||
transition.updateFrame(node: self.addButton, frame: CGRect(origin: CGPoint(x: size.width - rightInset - buttonSize.width, y: floor((topPanelHeight - buttonSize.height) / 2.0)), size: buttonSize))
|
||||
transition.updateAlpha(node: self.addButton, alpha: canAddColors ? 1.0 : 0.0)
|
||||
transition.updateSublayerTransformScale(node: self.addButton, scale: canAddColors ? 1.0 : 0.1)
|
||||
|
||||
func degreesToRadians(_ degrees: CGFloat) -> CGFloat {
|
||||
var degrees = degrees
|
||||
if degrees >= 270.0 {
|
||||
degrees = degrees - 360.0
|
||||
}
|
||||
return degrees * CGFloat.pi / 180.0
|
||||
}
|
||||
|
||||
transition.updateTransformRotation(node: self.rotateButton, angle: degreesToRadians(CGFloat(self.state.rotation)), beginWithCurrentState: true, completion: nil)
|
||||
|
||||
self.rotateButton.isHidden = true
|
||||
self.swapButton.isHidden = true
|
||||
self.multiColorFieldNode.isHidden = false
|
||||
|
||||
let sampleItemSize: CGFloat = 32.0
|
||||
let sampleItemSpacing: CGFloat = 15.0
|
||||
|
||||
var nextSampleX = leftInset
|
||||
|
||||
for i in 0 ..< self.state.colors.count {
|
||||
var animateIn = false
|
||||
let itemNode: ColorSampleItemNode
|
||||
if self.sampleItemNodes.count > i {
|
||||
itemNode = self.sampleItemNodes[i]
|
||||
} else {
|
||||
itemNode = ColorSampleItemNode(action: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let index = i
|
||||
strongSelf.updateState({ state in
|
||||
var state = state
|
||||
state.selection = index
|
||||
return state
|
||||
})
|
||||
})
|
||||
self.sampleItemNodes.append(itemNode)
|
||||
self.insertSubnode(itemNode, aboveSubnode: self.multiColorFieldNode)
|
||||
animateIn = true
|
||||
}
|
||||
|
||||
if i != 0 {
|
||||
nextSampleX += sampleItemSpacing
|
||||
}
|
||||
itemNode.frame = CGRect(origin: CGPoint(x: nextSampleX, y: (topPanelHeight - sampleItemSize) / 2.0), size: CGSize(width: sampleItemSize, height: sampleItemSize))
|
||||
nextSampleX += sampleItemSize
|
||||
itemNode.update(size: itemNode.bounds.size, color: self.state.colors[i].color, isSelected: self.state.selection == i)
|
||||
|
||||
if animateIn {
|
||||
transition.animateTransformScale(node: itemNode, from: 0.1)
|
||||
itemNode.alpha = 0.0
|
||||
transition.updateAlpha(node: itemNode, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
if self.sampleItemNodes.count > self.state.colors.count {
|
||||
for i in self.state.colors.count ..< self.sampleItemNodes.count {
|
||||
let itemNode = self.sampleItemNodes[i]
|
||||
transition.updateTransformScale(node: itemNode, scale: 0.1)
|
||||
transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in
|
||||
itemNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
self.sampleItemNodes.removeSubrange(self.state.colors.count ..< self.sampleItemNodes.count)
|
||||
}
|
||||
|
||||
let fieldX = nextSampleX + sampleItemSpacing
|
||||
|
||||
let fieldFrame = CGRect(x: fieldX, y: (topPanelHeight - fieldHeight) / 2.0, width: size.width - fieldX - leftInset - (canAddColors ? (buttonSize.width + sampleItemSpacing) : 0.0), height: fieldHeight)
|
||||
transition.updateFrame(node: self.multiColorFieldNode, frame: fieldFrame)
|
||||
self.multiColorFieldNode.updateLayout(size: fieldFrame.size, condensed: false, transition: transition)
|
||||
|
||||
let colorPickerSize = CGSize(width: size.width, height: size.height - topPanelHeight - separatorHeight)
|
||||
transition.updateFrame(node: self.colorPickerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight + separatorHeight), size: colorPickerSize))
|
||||
self.colorPickerNode.updateLayout(size: colorPickerSize, transition: transition)
|
||||
}
|
||||
|
||||
@objc private func rotatePressed() {
|
||||
self.rotate?()
|
||||
self.updateState({ current in
|
||||
var updated = current
|
||||
var newRotation = updated.rotation + 45
|
||||
if newRotation >= 360 {
|
||||
newRotation = 0
|
||||
}
|
||||
updated.rotation = newRotation
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
@objc private func swapPressed() {
|
||||
/*self.updateState({ current in
|
||||
var updated = current
|
||||
if let secondColor = current.secondColor {
|
||||
updated.firstColor = secondColor
|
||||
updated.secondColor = current.firstColor
|
||||
}
|
||||
return updated
|
||||
})*/
|
||||
}
|
||||
|
||||
@objc private func addPressed() {
|
||||
self.colorSelected?()
|
||||
self.colorAdded?()
|
||||
|
||||
self.multiColorFieldNode.setSkipEndEditingIfNeeded()
|
||||
|
||||
self.updateState({ current in
|
||||
var current = current
|
||||
if current.colors.count < current.maximumNumberOfColors {
|
||||
if current.colors.isEmpty {
|
||||
current.colors.append(HSBColor(rgb: 0xffffff))
|
||||
} else if current.simpleGradientGeneration {
|
||||
var hsb = current.colors[0].values
|
||||
if hsb.1 > 0.5 {
|
||||
hsb.1 -= 0.15
|
||||
} else {
|
||||
hsb.1 += 0.15
|
||||
}
|
||||
if hsb.0 > 0.5 {
|
||||
hsb.0 -= 0.05
|
||||
} else {
|
||||
hsb.0 += 0.05
|
||||
}
|
||||
current.colors.append(HSBColor(values: hsb))
|
||||
} else if let suggestedNewColor = current.suggestedNewColor {
|
||||
current.colors.append(suggestedNewColor)
|
||||
} else {
|
||||
current.colors.append(current.colors[current.colors.count - 1])
|
||||
}
|
||||
current.selection = current.colors.count - 1
|
||||
}
|
||||
return current
|
||||
})
|
||||
}
|
||||
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let result = super.hitTest(point, with: event) {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
|
||||
private let knobBackgroundImage: UIImage? = {
|
||||
return generateImage(CGSize(width: 45.0, height: 45.0), contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: -1.5), blur: 4.5, color: UIColor(rgb: 0x000000, alpha: 0.4).cgColor)
|
||||
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor)
|
||||
context.fillEllipse(in: bounds.insetBy(dx: 3.0 + UIScreenPixel, dy: 3.0 + UIScreenPixel))
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: bounds.insetBy(dx: 3.0, dy: 3.0))
|
||||
}, opaque: false, scale: nil)
|
||||
}()
|
||||
|
||||
private let pointerImage: UIImage? = {
|
||||
return generateImage(CGSize(width: 12.0, height: 55.0), opaque: false, scale: nil, rotatedContext: { size, context in
|
||||
context.setBlendMode(.clear)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
context.setBlendMode(.normal)
|
||||
|
||||
let lineWidth: CGFloat = 1.0
|
||||
context.setFillColor(UIColor.black.cgColor)
|
||||
context.setStrokeColor(UIColor.white.cgColor)
|
||||
context.setLineWidth(lineWidth)
|
||||
context.setLineCap(.round)
|
||||
context.setLineJoin(.round)
|
||||
|
||||
let pointerHeight: CGFloat = 7.0
|
||||
context.move(to: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0))
|
||||
context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: lineWidth / 2.0))
|
||||
context.addLine(to: CGPoint(x: size.width / 2.0, y: lineWidth / 2.0 + pointerHeight))
|
||||
context.closePath()
|
||||
context.drawPath(using: .fillStroke)
|
||||
|
||||
context.move(to: CGPoint(x: lineWidth / 2.0, y: size.height - lineWidth / 2.0))
|
||||
context.addLine(to: CGPoint(x: size.width / 2.0, y: size.height - lineWidth / 2.0 - pointerHeight))
|
||||
context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: size.height - lineWidth / 2.0))
|
||||
context.closePath()
|
||||
context.drawPath(using: .fillStroke)
|
||||
})
|
||||
}()
|
||||
|
||||
private let brightnessMaskImage: UIImage? = {
|
||||
return generateImage(CGSize(width: 36.0, height: 36.0), opaque: false, scale: nil, rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fill(bounds)
|
||||
|
||||
context.setBlendMode(.clear)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: bounds)
|
||||
})?.stretchableImage(withLeftCapWidth: 18, topCapHeight: 18)
|
||||
}()
|
||||
|
||||
private let brightnessGradientImage: UIImage? = {
|
||||
return generateImage(CGSize(width: 160.0, height: 1.0), opaque: false, scale: nil, rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let gradientColors = [UIColor.black.withAlphaComponent(0.0), UIColor.black].map { $0.cgColor } as CFArray
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
|
||||
})
|
||||
}()
|
||||
|
||||
private final class HSBParameter: NSObject {
|
||||
let hue: CGFloat
|
||||
let saturation: CGFloat
|
||||
let value: CGFloat
|
||||
|
||||
init(hue: CGFloat, saturation: CGFloat, value: CGFloat) {
|
||||
self.hue = hue
|
||||
self.saturation = saturation
|
||||
self.value = value
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
private final class WallpaperColorKnobNode: ASDisplayNode {
|
||||
var color: HSBColor = HSBColor(hue: 0.0, saturation: 0.0, brightness: 1.0) {
|
||||
didSet {
|
||||
if self.color != oldValue {
|
||||
self.colorNode.backgroundColor = self.color.color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let backgroundNode: ASImageNode
|
||||
private let colorNode: ASDisplayNode
|
||||
|
||||
override init() {
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.image = knobBackgroundImage
|
||||
|
||||
self.colorNode = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.colorNode)
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.backgroundNode.frame = self.bounds
|
||||
self.colorNode.frame = self.bounds.insetBy(dx: 7.0 - UIScreenPixel, dy: 7.0 - UIScreenPixel)
|
||||
self.colorNode.cornerRadius = self.colorNode.frame.width / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
private final class WallpaperColorHueSaturationNode: ASDisplayNode {
|
||||
var value: CGFloat = 1.0 {
|
||||
didSet {
|
||||
if self.value != oldValue {
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.isOpaque = true
|
||||
self.displaysAsynchronously = false
|
||||
}
|
||||
|
||||
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
return HSBParameter(hue: 1.0, saturation: 1.0, value: 1.0)
|
||||
}
|
||||
|
||||
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
||||
guard let parameters = parameters as? HSBParameter else {
|
||||
return
|
||||
}
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
|
||||
let colors = [UIColor(rgb: 0xff0000).cgColor, UIColor(rgb: 0xffff00).cgColor, UIColor(rgb: 0x00ff00).cgColor, UIColor(rgb: 0x00ffff).cgColor, UIColor(rgb: 0x0000ff).cgColor, UIColor(rgb: 0xff00ff).cgColor, UIColor(rgb: 0xff0000).cgColor]
|
||||
var locations: [CGFloat] = [0.0, 0.16667, 0.33333, 0.5, 0.66667, 0.83334, 1.0]
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: bounds.width, y: 0.0), options: CGGradientDrawingOptions())
|
||||
|
||||
let overlayColors = [UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff).cgColor]
|
||||
var overlayLocations: [CGFloat] = [0.0, 1.0]
|
||||
let overlayGradient = CGGradient(colorsSpace: colorSpace, colors: overlayColors as CFArray, locations: &overlayLocations)!
|
||||
context.drawLinearGradient(overlayGradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.height), options: CGGradientDrawingOptions())
|
||||
|
||||
context.setFillColor(UIColor(rgb: 0x000000, alpha: 1.0 - parameters.value).cgColor)
|
||||
context.fill(bounds)
|
||||
}
|
||||
|
||||
var tap: ((CGPoint) -> Void)?
|
||||
var panBegan: ((CGPoint) -> Void)?
|
||||
var panChanged: ((CGPoint, Bool) -> Void)?
|
||||
|
||||
var initialTouchLocation: CGPoint?
|
||||
var touchMoved = false
|
||||
var previousTouchLocation: CGPoint?
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if let touchLocation = touches.first?.location(in: self.view) {
|
||||
self.touchMoved = false
|
||||
self.initialTouchLocation = touchLocation
|
||||
self.previousTouchLocation = nil
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
if let touchLocation = touches.first?.location(in: self.view), let initialLocation = self.initialTouchLocation {
|
||||
let dX = touchLocation.x - initialLocation.x
|
||||
let dY = touchLocation.y - initialLocation.y
|
||||
if !self.touchMoved && dX * dX + dY * dY > 3.0 {
|
||||
self.touchMoved = true
|
||||
self.panBegan?(touchLocation)
|
||||
self.previousTouchLocation = touchLocation
|
||||
} else if let previousTouchLocation = self.previousTouchLocation {
|
||||
let dX = touchLocation.x - previousTouchLocation.x
|
||||
let dY = touchLocation.y - previousTouchLocation.y
|
||||
let translation = CGPoint(x: dX, y: dY)
|
||||
|
||||
self.panChanged?(translation, false)
|
||||
self.previousTouchLocation = touchLocation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
if self.touchMoved {
|
||||
if let touchLocation = touches.first?.location(in: self.view), let previousTouchLocation = self.previousTouchLocation {
|
||||
let dX = touchLocation.x - previousTouchLocation.x
|
||||
let dY = touchLocation.y - previousTouchLocation.y
|
||||
let translation = CGPoint(x: dX, y: dY)
|
||||
|
||||
self.panChanged?(translation, true)
|
||||
}
|
||||
} else if let touchLocation = self.initialTouchLocation {
|
||||
self.tap?(touchLocation)
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
private final class WallpaperColorBrightnessNode: ASDisplayNode {
|
||||
private let gradientNode: ASImageNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
var hsb: (CGFloat, CGFloat, CGFloat) = (0.0, 1.0, 1.0) {
|
||||
didSet {
|
||||
if self.hsb.0 != oldValue.0 || self.hsb.1 != oldValue.1 {
|
||||
let color = UIColor(hue: hsb.0, saturation: hsb.1, brightness: 1.0, alpha: 1.0)
|
||||
self.backgroundColor = color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
self.gradientNode = ASImageNode()
|
||||
self.gradientNode.displaysAsynchronously = false
|
||||
self.gradientNode.displayWithoutProcessing = true
|
||||
self.gradientNode.image = brightnessGradientImage
|
||||
self.gradientNode.contentMode = .scaleToFill
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
self.maskNode.displaysAsynchronously = false
|
||||
self.maskNode.displayWithoutProcessing = true
|
||||
self.maskNode.image = brightnessMaskImage
|
||||
self.maskNode.contentMode = .scaleToFill
|
||||
|
||||
super.init()
|
||||
|
||||
self.isOpaque = true
|
||||
self.addSubnode(self.gradientNode)
|
||||
self.addSubnode(self.maskNode)
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.gradientNode.frame = self.bounds
|
||||
self.maskNode.frame = self.bounds
|
||||
}
|
||||
}
|
||||
|
||||
public struct HSBColor: Equatable {
|
||||
public static func == (lhs: HSBColor, rhs: HSBColor) -> Bool {
|
||||
return lhs.values.h == rhs.values.h && lhs.values.s == rhs.values.s && lhs.values.b == rhs.values.b
|
||||
}
|
||||
|
||||
public let values: (h: CGFloat, s: CGFloat, b: CGFloat)
|
||||
public let backingColor: UIColor
|
||||
|
||||
public var hue: CGFloat {
|
||||
return self.values.h
|
||||
}
|
||||
|
||||
public var saturation: CGFloat {
|
||||
return self.values.s
|
||||
}
|
||||
|
||||
public var brightness: CGFloat {
|
||||
return self.values.b
|
||||
}
|
||||
|
||||
public var rgb: UInt32 {
|
||||
return self.color.argb
|
||||
}
|
||||
|
||||
public init(values: (h: CGFloat, s: CGFloat, b: CGFloat)) {
|
||||
self.values = values
|
||||
self.backingColor = UIColor(hue: values.h, saturation: values.s, brightness: values.b, alpha: 1.0)
|
||||
}
|
||||
|
||||
public init(hue: CGFloat, saturation: CGFloat, brightness: CGFloat) {
|
||||
self.values = (h: hue, s: saturation, b: brightness)
|
||||
self.backingColor = UIColor(hue: self.values.h, saturation: self.values.s, brightness: self.values.b, alpha: 1.0)
|
||||
}
|
||||
|
||||
public init(color: UIColor) {
|
||||
self.values = color.hsb
|
||||
self.backingColor = color
|
||||
}
|
||||
|
||||
public init(rgb: UInt32) {
|
||||
self.init(color: UIColor(rgb: rgb))
|
||||
}
|
||||
|
||||
public var color: UIColor {
|
||||
return self.backingColor
|
||||
}
|
||||
}
|
||||
|
||||
final class WallpaperColorPickerNode: ASDisplayNode {
|
||||
private let brightnessNode: WallpaperColorBrightnessNode
|
||||
private let brightnessKnobNode: ASImageNode
|
||||
private let colorNode: WallpaperColorHueSaturationNode
|
||||
private let colorKnobNode: WallpaperColorKnobNode
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
var color: HSBColor = HSBColor(hue: 0.0, saturation: 1.0, brightness: 1.0) {
|
||||
didSet {
|
||||
if self.color != oldValue {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var colorChanged: ((HSBColor) -> Void)?
|
||||
var colorChangeEnded: ((HSBColor) -> Void)?
|
||||
|
||||
init(strings: PresentationStrings) {
|
||||
self.brightnessNode = WallpaperColorBrightnessNode()
|
||||
self.brightnessNode.hitTestSlop = UIEdgeInsets(top: -16.0, left: -16.0, bottom: -16.0, right: -16.0)
|
||||
self.brightnessKnobNode = ASImageNode()
|
||||
self.brightnessKnobNode.image = pointerImage
|
||||
self.brightnessKnobNode.isUserInteractionEnabled = false
|
||||
self.colorNode = WallpaperColorHueSaturationNode()
|
||||
self.colorNode.hitTestSlop = UIEdgeInsets(top: -16.0, left: -16.0, bottom: -16.0, right: -16.0)
|
||||
self.colorKnobNode = WallpaperColorKnobNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = .white
|
||||
|
||||
self.addSubnode(self.brightnessNode)
|
||||
self.addSubnode(self.brightnessKnobNode)
|
||||
self.addSubnode(self.colorNode)
|
||||
self.addSubnode(self.colorKnobNode)
|
||||
|
||||
self.update()
|
||||
|
||||
self.colorNode.tap = { [weak self] location in
|
||||
guard let strongSelf = self, let size = strongSelf.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let colorHeight = size.height - 66.0
|
||||
|
||||
let newHue = max(0.0, min(1.0, location.x / size.width))
|
||||
let newSaturation = max(0.0, min(1.0, (1.0 - location.y / colorHeight)))
|
||||
strongSelf.color = HSBColor(hue: newHue, saturation: newSaturation, brightness: strongSelf.color.brightness)
|
||||
|
||||
strongSelf.updateKnobLayout(size: size, panningColor: false, transition: .immediate)
|
||||
|
||||
strongSelf.update()
|
||||
strongSelf.colorChangeEnded?(strongSelf.color)
|
||||
}
|
||||
|
||||
self.colorNode.panBegan = { [weak self] location in
|
||||
guard let strongSelf = self, let size = strongSelf.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let previousColor = strongSelf.color
|
||||
|
||||
let colorHeight = size.height - 66.0
|
||||
|
||||
let newHue = max(0.0, min(1.0, location.x / size.width))
|
||||
let newSaturation = max(0.0, min(1.0, (1.0 - location.y / colorHeight)))
|
||||
strongSelf.color = HSBColor(hue: newHue, saturation: newSaturation, brightness: strongSelf.color.brightness)
|
||||
|
||||
strongSelf.updateKnobLayout(size: size, panningColor: true, transition: .immediate)
|
||||
|
||||
if strongSelf.color != previousColor {
|
||||
strongSelf.colorChanged?(strongSelf.color)
|
||||
}
|
||||
}
|
||||
|
||||
self.colorNode.panChanged = { [weak self] translation, ended in
|
||||
guard let strongSelf = self, let size = strongSelf.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let previousColor = strongSelf.color
|
||||
|
||||
let colorHeight = size.height - 66.0
|
||||
|
||||
let newHue = max(0.0, min(1.0, strongSelf.color.hue + translation.x / size.width))
|
||||
let newSaturation = max(0.0, min(1.0, strongSelf.color.saturation - translation.y / colorHeight))
|
||||
strongSelf.color = HSBColor(hue: newHue, saturation: newSaturation, brightness: strongSelf.color.brightness)
|
||||
|
||||
if ended {
|
||||
strongSelf.updateKnobLayout(size: size, panningColor: false, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
} else {
|
||||
strongSelf.updateKnobLayout(size: size, panningColor: true, transition: .immediate)
|
||||
}
|
||||
|
||||
if strongSelf.color != previousColor || ended {
|
||||
strongSelf.update()
|
||||
if ended {
|
||||
strongSelf.colorChangeEnded?(strongSelf.color)
|
||||
} else {
|
||||
strongSelf.colorChanged?(strongSelf.color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
self.view.disablesInteractiveModalDismiss = true
|
||||
|
||||
let brightnessPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(WallpaperColorPickerNode.brightnessPan))
|
||||
self.brightnessNode.view.addGestureRecognizer(brightnessPanRecognizer)
|
||||
}
|
||||
|
||||
private func update() {
|
||||
self.backgroundColor = .white
|
||||
self.colorNode.value = self.color.brightness
|
||||
self.brightnessNode.hsb = self.color.values
|
||||
self.colorKnobNode.color = self.color
|
||||
}
|
||||
|
||||
private func updateKnobLayout(size: CGSize, panningColor: Bool, transition: ContainedViewLayoutTransition) {
|
||||
let knobSize = CGSize(width: 45.0, height: 45.0)
|
||||
|
||||
let colorHeight = size.height - 66.0
|
||||
var colorKnobFrame = CGRect(x: floorToScreenPixels(-knobSize.width / 2.0 + size.width * self.color.hue), y: floorToScreenPixels(-knobSize.height / 2.0 + (colorHeight * (1.0 - self.color.saturation))), width: knobSize.width, height: knobSize.height)
|
||||
var origin = colorKnobFrame.origin
|
||||
if !panningColor {
|
||||
origin = CGPoint(x: max(0.0, min(origin.x, size.width - knobSize.width)), y: max(0.0, min(origin.y, colorHeight - knobSize.height)))
|
||||
} else {
|
||||
origin = origin.offsetBy(dx: 0.0, dy: -32.0)
|
||||
}
|
||||
colorKnobFrame.origin = origin
|
||||
transition.updateFrame(node: self.colorKnobNode, frame: colorKnobFrame)
|
||||
|
||||
let inset: CGFloat = 15.0
|
||||
let brightnessKnobSize = CGSize(width: 12.0, height: 55.0)
|
||||
let brightnessKnobFrame = CGRect(x: inset - brightnessKnobSize.width / 2.0 + (size.width - inset * 2.0) * (1.0 - self.color.brightness), y: size.height - 65.0, width: brightnessKnobSize.width, height: brightnessKnobSize.height)
|
||||
transition.updateFrame(node: self.brightnessKnobNode, frame: brightnessKnobFrame)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = size
|
||||
|
||||
let colorHeight = size.height - 66.0
|
||||
transition.updateFrame(node: self.colorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: colorHeight))
|
||||
|
||||
let inset: CGFloat = 15.0
|
||||
transition.updateFrame(node: self.brightnessNode, frame: CGRect(x: inset, y: size.height - 55.0, width: size.width - inset * 2.0, height: 35.0))
|
||||
|
||||
self.updateKnobLayout(size: size, panningColor: false, transition: .immediate)
|
||||
}
|
||||
|
||||
@objc private func brightnessPan(_ recognizer: UIPanGestureRecognizer) {
|
||||
guard let size = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let previousColor = self.color
|
||||
|
||||
let transition = recognizer.translation(in: recognizer.view)
|
||||
let brightnessWidth: CGFloat = size.width - 42.0 * 2.0
|
||||
let newValue = max(0.0, min(1.0, self.color.brightness - transition.x / brightnessWidth))
|
||||
self.color = HSBColor(hue: self.color.hue, saturation: self.color.saturation, brightness: newValue)
|
||||
|
||||
var ended = false
|
||||
switch recognizer.state {
|
||||
case .changed:
|
||||
self.updateKnobLayout(size: size, panningColor: false, transition: .immediate)
|
||||
recognizer.setTranslation(CGPoint(), in: recognizer.view)
|
||||
case .ended:
|
||||
self.updateKnobLayout(size: size, panningColor: false, transition: .immediate)
|
||||
ended = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if self.color != previousColor || ended {
|
||||
self.update()
|
||||
if ended {
|
||||
self.colorChangeEnded?(self.color)
|
||||
} else {
|
||||
self.colorChanged?(self.color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class WallpaperCropNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
let scrollNode: ASScrollNode
|
||||
|
||||
private var ignoreZoom = false
|
||||
private var ignoreZoomTransition: ContainedViewLayoutTransition?
|
||||
|
||||
private var containerLayout: ContainerViewLayout?
|
||||
|
||||
var zoomableContent: (CGSize, ASDisplayNode)? {
|
||||
didSet {
|
||||
if oldValue?.1 !== self.zoomableContent?.1 {
|
||||
if let node = oldValue?.1 {
|
||||
node.view.removeFromSuperview()
|
||||
}
|
||||
if let node = self.zoomableContent?.1 {
|
||||
self.scrollNode.addSubnode(node)
|
||||
}
|
||||
}
|
||||
self.resetScrollViewContents(transition: .immediate)
|
||||
self.centerScrollViewContents(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
self.scrollNode = ASScrollNode()
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.scrollNode.view.delegate = self
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.clipsToBounds = false
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.decelerationRate = UIScrollView.DecelerationRate.fast
|
||||
|
||||
self.addSubnode(self.scrollNode)
|
||||
}
|
||||
|
||||
@objc func contentTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
if recognizer.state == .ended {
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .doubleTap:
|
||||
if let contentView = self.zoomableContent?.1.view, self.scrollNode.view.zoomScale.isLessThanOrEqualTo(self.scrollNode.view.minimumZoomScale) {
|
||||
let pointInView = self.scrollNode.view.convert(location, to: contentView)
|
||||
|
||||
let newZoomScale = self.scrollNode.view.maximumZoomScale
|
||||
let scrollViewSize = self.scrollNode.view.bounds.size
|
||||
|
||||
let w = scrollViewSize.width / newZoomScale
|
||||
let h = scrollViewSize.height / newZoomScale
|
||||
let x = pointInView.x - (w / 2.0)
|
||||
let y = pointInView.y - (h / 2.0)
|
||||
|
||||
let rectToZoomTo = CGRect(x: x, y: y, width: w, height: h)
|
||||
|
||||
self.scrollNode.view.zoom(to: rectToZoomTo, animated: true)
|
||||
} else {
|
||||
self.scrollNode.view.setZoomScale(self.scrollNode.view.minimumZoomScale, animated: true)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
var shouldResetContents = false
|
||||
if let containerLayout = self.containerLayout {
|
||||
shouldResetContents = !containerLayout.size.equalTo(layout.size)
|
||||
} else {
|
||||
shouldResetContents = true
|
||||
}
|
||||
self.containerLayout = layout
|
||||
|
||||
if shouldResetContents {
|
||||
var previousFrame: CGRect?
|
||||
var previousScale: CGFloat?
|
||||
if let (_, contentNode) = self.zoomableContent {
|
||||
previousFrame = contentNode.view.frame
|
||||
let t = contentNode.layer.transform
|
||||
previousScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
self.resetScrollViewContents(transition: .immediate)
|
||||
|
||||
if let (_, contentNode) = self.zoomableContent, let previousFrame = previousFrame, let previousScale = previousScale {
|
||||
transition.animatePosition(node: contentNode, from: CGPoint(x: previousFrame.midX, y: previousFrame.midY))
|
||||
switch transition {
|
||||
case .immediate:
|
||||
break
|
||||
case let .animated(duration, curve):
|
||||
let t = contentNode.layer.transform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
|
||||
contentNode.layer.animateScale(from: previousScale, to: currentScale, duration: duration, timingFunction: curve.timingFunction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func resetScrollViewContents(transition: ContainedViewLayoutTransition) {
|
||||
guard let (contentSize, contentNode) = self.zoomableContent else {
|
||||
return
|
||||
}
|
||||
|
||||
self.ignoreZoom = true
|
||||
self.ignoreZoomTransition = transition
|
||||
self.scrollNode.view.minimumZoomScale = 1.0
|
||||
self.scrollNode.view.maximumZoomScale = 1.0
|
||||
self.scrollNode.view.zoomScale = 1.0
|
||||
self.scrollNode.view.contentSize = contentSize
|
||||
|
||||
contentNode.transform = CATransform3DIdentity
|
||||
contentNode.frame = CGRect(origin: CGPoint(), size: contentSize)
|
||||
|
||||
self.centerScrollViewContents(transition: transition)
|
||||
self.ignoreZoom = false
|
||||
|
||||
self.scrollNode.view.zoomScale = self.scrollNode.view.minimumZoomScale
|
||||
self.ignoreZoomTransition = nil
|
||||
}
|
||||
|
||||
private func centerScrollViewContents(transition: ContainedViewLayoutTransition) {
|
||||
guard let (contentSize, contentNode) = self.zoomableContent else {
|
||||
return
|
||||
}
|
||||
|
||||
let boundsSize = self.scrollNode.view.bounds.size
|
||||
if contentSize.width.isLessThanOrEqualTo(0.0) || contentSize.height.isLessThanOrEqualTo(0.0) || boundsSize.width.isLessThanOrEqualTo(0.0) || boundsSize.height.isLessThanOrEqualTo(0.0) {
|
||||
return
|
||||
}
|
||||
|
||||
let scaleWidth = boundsSize.width / contentSize.width
|
||||
let scaleHeight = boundsSize.height / contentSize.height
|
||||
let minScale = max(scaleWidth, scaleHeight)
|
||||
let maxScale = minScale * 3.0
|
||||
|
||||
if !self.scrollNode.view.minimumZoomScale.isEqual(to: minScale) {
|
||||
self.scrollNode.view.minimumZoomScale = minScale
|
||||
}
|
||||
|
||||
if !self.scrollNode.view.maximumZoomScale.isEqual(to: maxScale) {
|
||||
self.scrollNode.view.maximumZoomScale = maxScale
|
||||
}
|
||||
|
||||
var contentFrame = contentNode.view.frame
|
||||
if boundsSize.width > contentFrame.size.width {
|
||||
contentFrame.origin.x = (boundsSize.width - contentFrame.size.width) / 2.0
|
||||
} else {
|
||||
contentFrame.origin.x = 0.0
|
||||
}
|
||||
|
||||
if boundsSize.height >= contentFrame.size.height {
|
||||
contentFrame.origin.y = (boundsSize.height - contentFrame.size.height) / 2.0
|
||||
} else {
|
||||
contentFrame.origin.y = 0.0
|
||||
}
|
||||
|
||||
if !self.ignoreZoom {
|
||||
transition.updateFrame(view: contentNode.view, frame: contentFrame)
|
||||
}
|
||||
}
|
||||
|
||||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
return self.zoomableContent?.1.view
|
||||
}
|
||||
|
||||
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreZoom {
|
||||
self.centerScrollViewContents(transition: self.ignoreZoomTransition ?? .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
var cropRect: CGRect {
|
||||
let scrollView = self.scrollNode.view
|
||||
return scrollView.convert(scrollView.bounds, to: self.zoomableContent?.1.view)
|
||||
}
|
||||
|
||||
func zoom(to rect: CGRect) {
|
||||
self.scrollNode.view.zoom(to: rect, animated: false)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,461 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import ManagedAnimationNode
|
||||
|
||||
public enum WallpaperGalleryToolbarCancelButtonType {
|
||||
case cancel
|
||||
case discard
|
||||
}
|
||||
|
||||
public enum WallpaperGalleryToolbarDoneButtonType {
|
||||
case set
|
||||
case setPeer(String, Bool)
|
||||
case setChannel
|
||||
case proceed
|
||||
case apply
|
||||
case none
|
||||
}
|
||||
|
||||
public protocol WallpaperGalleryToolbar: ASDisplayNode {
|
||||
var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { get set }
|
||||
var doneButtonType: WallpaperGalleryToolbarDoneButtonType { get set }
|
||||
|
||||
var cancel: (() -> Void)? { get set }
|
||||
var done: ((Bool) -> Void)? { get set }
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings)
|
||||
|
||||
func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition)
|
||||
}
|
||||
|
||||
public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar {
|
||||
class ButtonNode: ASDisplayNode {
|
||||
private let doneButton = HighlightTrackingButtonNode()
|
||||
private var doneButtonBackgroundNode: ASDisplayNode
|
||||
private let doneButtonTitleNode: ImmediateTextNode
|
||||
private let doneButtonSolidBackgroundNode: ASDisplayNode
|
||||
private let doneButtonSolidTitleNode: ImmediateTextNode
|
||||
|
||||
private let animationNode: SimpleAnimationNode
|
||||
|
||||
var action: () -> Void = {}
|
||||
|
||||
var isLocked: Bool = false {
|
||||
didSet {
|
||||
self.animationNode.isHidden = !self.isLocked
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
self.doneButtonBackgroundNode = WallpaperLightButtonBackgroundNode()
|
||||
self.doneButtonBackgroundNode.cornerRadius = 14.0
|
||||
|
||||
self.doneButtonTitleNode = ImmediateTextNode()
|
||||
self.doneButtonTitleNode.displaysAsynchronously = false
|
||||
self.doneButtonTitleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.doneButtonSolidBackgroundNode = ASDisplayNode()
|
||||
self.doneButtonSolidBackgroundNode.alpha = 0.0
|
||||
self.doneButtonSolidBackgroundNode.clipsToBounds = true
|
||||
self.doneButtonSolidBackgroundNode.layer.cornerRadius = 14.0
|
||||
if #available(iOS 13.0, *) {
|
||||
self.doneButtonSolidBackgroundNode.layer.cornerCurve = .continuous
|
||||
}
|
||||
self.doneButtonSolidBackgroundNode.isUserInteractionEnabled = false
|
||||
|
||||
self.doneButtonSolidTitleNode = ImmediateTextNode()
|
||||
self.doneButtonSolidTitleNode.alpha = 0.0
|
||||
self.doneButtonSolidTitleNode.displaysAsynchronously = false
|
||||
self.doneButtonSolidTitleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.animationNode = SimpleAnimationNode(animationName: "premium_unlock", size: CGSize(width: 30.0, height: 30.0))
|
||||
self.animationNode.customColor = .white
|
||||
self.animationNode.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.doneButton.isExclusiveTouch = true
|
||||
|
||||
self.addSubnode(self.doneButtonBackgroundNode)
|
||||
self.addSubnode(self.doneButtonTitleNode)
|
||||
|
||||
self.addSubnode(self.doneButtonSolidBackgroundNode)
|
||||
self.addSubnode(self.doneButtonSolidTitleNode)
|
||||
|
||||
self.addSubnode(self.animationNode)
|
||||
|
||||
self.addSubnode(self.doneButton)
|
||||
|
||||
self.doneButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
if strongSelf.isSolid {
|
||||
strongSelf.doneButtonSolidBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.doneButtonSolidBackgroundNode.alpha = 0.55
|
||||
strongSelf.doneButtonSolidTitleNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.doneButtonSolidTitleNode.alpha = 0.55
|
||||
} else {
|
||||
strongSelf.doneButtonBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.doneButtonBackgroundNode.alpha = 0.55
|
||||
strongSelf.doneButtonTitleNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.doneButtonTitleNode.alpha = 0.55
|
||||
}
|
||||
} else {
|
||||
if strongSelf.isSolid {
|
||||
strongSelf.doneButtonSolidBackgroundNode.alpha = 1.0
|
||||
strongSelf.doneButtonSolidBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
||||
strongSelf.doneButtonSolidTitleNode.alpha = 1.0
|
||||
strongSelf.doneButtonSolidTitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
||||
} else {
|
||||
strongSelf.doneButtonBackgroundNode.alpha = 1.0
|
||||
strongSelf.doneButtonBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
||||
strongSelf.doneButtonTitleNode.alpha = 1.0
|
||||
strongSelf.doneButtonTitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.doneButton.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func setEnabled(_ enabled: Bool) {
|
||||
self.doneButton.alpha = enabled ? 1.0 : 0.4
|
||||
self.doneButton.isUserInteractionEnabled = enabled
|
||||
}
|
||||
|
||||
private var isSolid = false
|
||||
func setIsSolid(_ isSolid: Bool, transition: ContainedViewLayoutTransition) {
|
||||
guard self.isSolid != isSolid else {
|
||||
return
|
||||
}
|
||||
self.isSolid = isSolid
|
||||
|
||||
transition.updateAlpha(node: self.doneButtonBackgroundNode, alpha: isSolid ? 0.0 : 1.0)
|
||||
transition.updateAlpha(node: self.doneButtonSolidBackgroundNode, alpha: isSolid ? 1.0 : 0.0)
|
||||
transition.updateAlpha(node: self.doneButtonTitleNode, alpha: isSolid ? 0.0 : 1.0)
|
||||
transition.updateAlpha(node: self.doneButtonSolidTitleNode, alpha: isSolid ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
func updateTitle(_ title: String, theme: PresentationTheme) {
|
||||
self.doneButtonTitleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: .white)
|
||||
|
||||
self.doneButtonSolidBackgroundNode.backgroundColor = theme.list.itemCheckColors.fillColor
|
||||
self.doneButtonSolidTitleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor)
|
||||
}
|
||||
|
||||
func updateSize(_ size: CGSize) {
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
self.doneButtonBackgroundNode.frame = bounds
|
||||
if let backgroundNode = self.doneButtonBackgroundNode as? WallpaperOptionBackgroundNode {
|
||||
backgroundNode.updateLayout(size: size)
|
||||
} else if let backgroundNode = self.doneButtonBackgroundNode as? WallpaperLightButtonBackgroundNode {
|
||||
backgroundNode.updateLayout(size: size)
|
||||
}
|
||||
self.doneButtonSolidBackgroundNode.frame = bounds
|
||||
|
||||
let constrainedSize = CGSize(width: size.width - 44.0, height: size.height)
|
||||
let iconSize = CGSize(width: 30.0, height: 30.0)
|
||||
let doneTitleSize = self.doneButtonTitleNode.updateLayout(constrainedSize)
|
||||
|
||||
var totalWidth = doneTitleSize.width
|
||||
if self.isLocked {
|
||||
totalWidth += iconSize.width + 1.0
|
||||
}
|
||||
let titleOriginX = floorToScreenPixels((bounds.width - totalWidth) / 2.0)
|
||||
|
||||
self.animationNode.frame = CGRect(origin: CGPoint(x: titleOriginX, y: floorToScreenPixels((bounds.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
self.doneButtonTitleNode.frame = CGRect(origin: CGPoint(x: titleOriginX + totalWidth - doneTitleSize.width, y: floorToScreenPixels((bounds.height - doneTitleSize.height) / 2.0)), size: doneTitleSize).offsetBy(dx: bounds.minX, dy: bounds.minY)
|
||||
|
||||
let _ = self.doneButtonSolidTitleNode.updateLayout(constrainedSize)
|
||||
self.doneButtonSolidTitleNode.frame = self.doneButtonTitleNode.frame
|
||||
|
||||
self.doneButton.frame = bounds
|
||||
}
|
||||
|
||||
var dark: Bool = false {
|
||||
didSet {
|
||||
if self.dark != oldValue {
|
||||
self.doneButtonBackgroundNode.removeFromSupernode()
|
||||
if self.dark {
|
||||
self.doneButtonBackgroundNode = WallpaperOptionBackgroundNode(enableSaturation: true)
|
||||
} else {
|
||||
self.doneButtonBackgroundNode = WallpaperLightButtonBackgroundNode()
|
||||
}
|
||||
self.doneButtonBackgroundNode.cornerRadius = 14.0
|
||||
self.insertSubnode(self.doneButtonBackgroundNode, at: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var previousActionTime: Double?
|
||||
@objc func pressed() {
|
||||
let currentTime = CACurrentMediaTime()
|
||||
if let previousActionTime = self.previousActionTime, currentTime < previousActionTime + 1.0 {
|
||||
return
|
||||
}
|
||||
self.previousActionTime = currentTime
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
||||
private var theme: PresentationTheme
|
||||
private let strings: PresentationStrings
|
||||
|
||||
public var cancelButtonType: WallpaperGalleryToolbarCancelButtonType {
|
||||
didSet {
|
||||
self.updateThemeAndStrings(theme: self.theme, strings: self.strings)
|
||||
}
|
||||
}
|
||||
public var doneButtonType: WallpaperGalleryToolbarDoneButtonType {
|
||||
didSet {
|
||||
self.updateThemeAndStrings(theme: self.theme, strings: self.strings)
|
||||
}
|
||||
}
|
||||
|
||||
public var dark: Bool = false {
|
||||
didSet {
|
||||
self.applyButton.dark = self.dark
|
||||
self.applyForBothButton.dark = self.dark
|
||||
}
|
||||
}
|
||||
|
||||
private let applyButton = ButtonNode()
|
||||
private let applyForBothButton = ButtonNode()
|
||||
|
||||
public var cancel: (() -> Void)?
|
||||
public var done: ((Bool) -> Void)?
|
||||
|
||||
public init(theme: PresentationTheme, strings: PresentationStrings, cancelButtonType: WallpaperGalleryToolbarCancelButtonType = .cancel, doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.cancelButtonType = cancelButtonType
|
||||
self.doneButtonType = doneButtonType
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.applyButton)
|
||||
if case .setPeer = doneButtonType {
|
||||
self.addSubnode(self.applyForBothButton)
|
||||
}
|
||||
|
||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
|
||||
self.applyButton.action = { [weak self] in
|
||||
if let self {
|
||||
self.done?(false)
|
||||
}
|
||||
}
|
||||
self.applyForBothButton.action = { [weak self] in
|
||||
if let self {
|
||||
self.done?(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func setDoneEnabled(_ enabled: Bool) {
|
||||
self.applyButton.setEnabled(enabled)
|
||||
self.applyForBothButton.setEnabled(enabled)
|
||||
}
|
||||
|
||||
private var isSolid = false
|
||||
public func setDoneIsSolid(_ isSolid: Bool, transition: ContainedViewLayoutTransition) {
|
||||
guard self.isSolid != isSolid else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isSolid = isSolid
|
||||
self.applyButton.setIsSolid(isSolid, transition: transition)
|
||||
self.applyForBothButton.setIsSolid(isSolid, transition: transition)
|
||||
}
|
||||
|
||||
public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
|
||||
let applyTitle: String
|
||||
var applyForBothTitle: String? = nil
|
||||
var applyForBothLocked = false
|
||||
switch self.doneButtonType {
|
||||
case .set:
|
||||
applyTitle = strings.Wallpaper_ApplyForAll
|
||||
case let .setPeer(name, isPremium):
|
||||
applyTitle = strings.Wallpaper_ApplyForMe
|
||||
applyForBothTitle = strings.Wallpaper_ApplyForBoth(name).string
|
||||
applyForBothLocked = !isPremium
|
||||
case .setChannel:
|
||||
applyTitle = strings.Wallpaper_ApplyForChannel
|
||||
case .proceed:
|
||||
applyTitle = strings.Theme_Colors_Proceed
|
||||
case .apply:
|
||||
applyTitle = strings.WallpaperPreview_PatternPaternApply
|
||||
case .none:
|
||||
applyTitle = ""
|
||||
self.applyButton.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
self.applyButton.updateTitle(applyTitle, theme: theme)
|
||||
if let applyForBothTitle {
|
||||
self.applyForBothButton.updateTitle(applyForBothTitle, theme: theme)
|
||||
}
|
||||
self.applyForBothButton.isLocked = applyForBothLocked
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
let inset: CGFloat = 16.0
|
||||
let buttonHeight: CGFloat = 50.0
|
||||
|
||||
let spacing: CGFloat = 8.0
|
||||
|
||||
let applyFrame = CGRect(origin: CGPoint(x: inset, y: 2.0), size: CGSize(width: size.width - inset * 2.0, height: buttonHeight))
|
||||
let applyForBothFrame = CGRect(origin: CGPoint(x: inset, y: applyFrame.maxY + spacing), size: CGSize(width: size.width - inset * 2.0, height: buttonHeight))
|
||||
|
||||
var showApplyForBothButton = false
|
||||
if case .setPeer = self.doneButtonType {
|
||||
showApplyForBothButton = true
|
||||
}
|
||||
transition.updateAlpha(node: self.applyForBothButton, alpha: showApplyForBothButton ? 1.0 : 0.0)
|
||||
|
||||
self.applyButton.frame = applyFrame
|
||||
self.applyButton.updateSize(applyFrame.size)
|
||||
self.applyForBothButton.frame = applyForBothFrame
|
||||
self.applyForBothButton.updateSize(applyForBothFrame.size)
|
||||
}
|
||||
|
||||
@objc func cancelPressed() {
|
||||
self.cancel?()
|
||||
}
|
||||
}
|
||||
|
||||
public final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalleryToolbar {
|
||||
private var theme: PresentationTheme
|
||||
private let strings: PresentationStrings
|
||||
|
||||
public var cancelButtonType: WallpaperGalleryToolbarCancelButtonType {
|
||||
didSet {
|
||||
self.updateThemeAndStrings(theme: self.theme, strings: self.strings)
|
||||
}
|
||||
}
|
||||
public var doneButtonType: WallpaperGalleryToolbarDoneButtonType {
|
||||
didSet {
|
||||
self.updateThemeAndStrings(theme: self.theme, strings: self.strings)
|
||||
}
|
||||
}
|
||||
|
||||
private let cancelButton = HighlightTrackingButtonNode()
|
||||
private let cancelHighlightBackgroundNode = ASDisplayNode()
|
||||
private let doneButton = HighlightTrackingButtonNode()
|
||||
private let doneHighlightBackgroundNode = ASDisplayNode()
|
||||
private let backgroundNode = NavigationBackgroundNode(color: .clear)
|
||||
private let separatorNode = ASDisplayNode()
|
||||
private let topSeparatorNode = ASDisplayNode()
|
||||
|
||||
public var cancel: (() -> Void)?
|
||||
public var done: ((Bool) -> Void)?
|
||||
|
||||
public init(theme: PresentationTheme, strings: PresentationStrings, cancelButtonType: WallpaperGalleryToolbarCancelButtonType = .cancel, doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.cancelButtonType = cancelButtonType
|
||||
self.doneButtonType = doneButtonType
|
||||
|
||||
self.cancelHighlightBackgroundNode.alpha = 0.0
|
||||
self.doneHighlightBackgroundNode.alpha = 0.0
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.cancelHighlightBackgroundNode)
|
||||
self.addSubnode(self.cancelButton)
|
||||
self.addSubnode(self.doneHighlightBackgroundNode)
|
||||
self.addSubnode(self.doneButton)
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.topSeparatorNode)
|
||||
|
||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
|
||||
self.cancelButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.cancelHighlightBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.cancelHighlightBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
strongSelf.cancelHighlightBackgroundNode.alpha = 0.0
|
||||
strongSelf.cancelHighlightBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.doneButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.doneHighlightBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.doneHighlightBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
strongSelf.doneHighlightBackgroundNode.alpha = 0.0
|
||||
strongSelf.doneHighlightBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
|
||||
self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
public func setDoneEnabled(_ enabled: Bool) {
|
||||
self.doneButton.alpha = enabled ? 1.0 : 0.4
|
||||
self.doneButton.isUserInteractionEnabled = enabled
|
||||
}
|
||||
|
||||
public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
self.backgroundNode.updateColor(color: theme.rootController.tabBar.backgroundColor, transition: .immediate)
|
||||
self.separatorNode.backgroundColor = theme.rootController.tabBar.separatorColor
|
||||
self.topSeparatorNode.backgroundColor = theme.rootController.tabBar.separatorColor
|
||||
self.cancelHighlightBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor
|
||||
self.doneHighlightBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor
|
||||
|
||||
let cancelTitle: String
|
||||
switch self.cancelButtonType {
|
||||
case .cancel:
|
||||
cancelTitle = strings.Common_Cancel
|
||||
case .discard:
|
||||
cancelTitle = strings.WallpaperPreview_PatternPaternDiscard
|
||||
}
|
||||
let doneTitle: String
|
||||
switch self.doneButtonType {
|
||||
case .set, .setPeer, .setChannel:
|
||||
doneTitle = strings.Wallpaper_Set
|
||||
case .proceed:
|
||||
doneTitle = strings.Theme_Colors_Proceed
|
||||
case .apply:
|
||||
doneTitle = strings.WallpaperPreview_PatternPaternApply
|
||||
case .none:
|
||||
doneTitle = ""
|
||||
self.doneButton.isUserInteractionEnabled = false
|
||||
}
|
||||
self.cancelButton.setTitle(cancelTitle, with: Font.regular(17.0), with: theme.list.itemPrimaryTextColor, for: [])
|
||||
self.doneButton.setTitle(doneTitle, with: Font.regular(17.0), with: theme.list.itemPrimaryTextColor, for: [])
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.cancelButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: floor(size.width / 2.0), height: size.height))
|
||||
self.cancelHighlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: floor(size.width / 2.0), height: size.height))
|
||||
self.doneButton.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: size.width - floor(size.width / 2.0), height: size.height))
|
||||
self.doneHighlightBackgroundNode.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: size.width - floor(size.width / 2.0), height: size.height))
|
||||
self.separatorNode.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: UIScreenPixel, height: size.height + layout.intrinsicInsets.bottom))
|
||||
self.topSeparatorNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: UIScreenPixel))
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.backgroundNode.update(size: CGSize(width: size.width, height: size.height + layout.intrinsicInsets.bottom), transition: .immediate)
|
||||
}
|
||||
|
||||
@objc func cancelPressed() {
|
||||
self.cancel?()
|
||||
}
|
||||
|
||||
@objc func donePressed() {
|
||||
self.done?(false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,635 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import CheckNode
|
||||
import AnimationUI
|
||||
|
||||
public enum WallpaperOptionButtonValue {
|
||||
case check(Bool)
|
||||
case color(Bool, UIColor)
|
||||
case colors(Bool, [UIColor])
|
||||
}
|
||||
|
||||
private func generateColorsImage(diameter: CGFloat, colors: [UIColor]) -> UIImage? {
|
||||
return generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if !colors.isEmpty {
|
||||
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
var startAngle = -CGFloat.pi * 0.5
|
||||
for i in 0 ..< colors.count {
|
||||
context.setFillColor(colors[i].cgColor)
|
||||
|
||||
let endAngle = startAngle + 2.0 * CGFloat.pi * (1.0 / CGFloat(colors.count))
|
||||
|
||||
context.move(to: center)
|
||||
context.addArc(center: center, radius: size.width / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: false)
|
||||
context.fillPath()
|
||||
|
||||
startAngle = endAngle
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
final class WallpaperLightButtonBackgroundNode: ASDisplayNode {
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
private let overlayNode: ASDisplayNode
|
||||
private let lightNode: ASDisplayNode
|
||||
|
||||
override init() {
|
||||
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x333333, alpha: 0.35), enableBlur: true, enableSaturation: false)
|
||||
self.overlayNode = ASDisplayNode()
|
||||
self.overlayNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.75)
|
||||
self.overlayNode.layer.compositingFilter = "overlayBlendMode"
|
||||
|
||||
self.lightNode = ASDisplayNode()
|
||||
self.lightNode.backgroundColor = UIColor(rgb: 0xf2f2f2, alpha: 0.2)
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.overlayNode)
|
||||
self.addSubnode(self.lightNode)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize) {
|
||||
let frame = CGRect(origin: .zero, size: size)
|
||||
self.backgroundNode.frame = frame
|
||||
self.overlayNode.frame = frame
|
||||
self.lightNode.frame = frame
|
||||
|
||||
self.backgroundNode.update(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
final class WallpaperOptionBackgroundNode: ASDisplayNode {
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
|
||||
var enableSaturation: Bool {
|
||||
didSet {
|
||||
self.backgroundNode.updateColor(color: UIColor(rgb: 0x333333, alpha: 0.35), enableBlur: true, enableSaturation: self.enableSaturation, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
init(enableSaturation: Bool = false) {
|
||||
self.enableSaturation = enableSaturation
|
||||
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x333333, alpha: 0.35), enableBlur: true, enableSaturation: enableSaturation)
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize) {
|
||||
let frame = CGRect(origin: .zero, size: size)
|
||||
self.backgroundNode.frame = frame
|
||||
|
||||
self.backgroundNode.update(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
final class WallpaperNavigationButtonNode: HighlightTrackingButtonNode {
|
||||
enum Content {
|
||||
case icon(image: UIImage?, size: CGSize)
|
||||
case text(String)
|
||||
case dayNight(isNight: Bool)
|
||||
}
|
||||
|
||||
var enableSaturation: Bool = false {
|
||||
didSet {
|
||||
if let backgroundNode = self.backgroundNode as? WallpaperOptionBackgroundNode {
|
||||
backgroundNode.enableSaturation = self.enableSaturation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let content: Content
|
||||
var dark: Bool {
|
||||
didSet {
|
||||
if self.dark != oldValue {
|
||||
self.backgroundNode.removeFromSupernode()
|
||||
if self.dark {
|
||||
self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation)
|
||||
} else {
|
||||
self.backgroundNode = WallpaperLightButtonBackgroundNode()
|
||||
}
|
||||
self.insertSubnode(self.backgroundNode, at: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var backgroundNode: ASDisplayNode
|
||||
private let iconNode: ASImageNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private var animationNode: AnimationNode?
|
||||
|
||||
func setIcon(_ image: UIImage?) {
|
||||
self.iconNode.image = generateTintedImage(image: image, color: .white)
|
||||
}
|
||||
|
||||
init(content: Content, dark: Bool) {
|
||||
self.content = content
|
||||
self.dark = dark
|
||||
|
||||
if dark {
|
||||
self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation)
|
||||
} else {
|
||||
self.backgroundNode = WallpaperLightButtonBackgroundNode()
|
||||
}
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.contentMode = .center
|
||||
|
||||
var title: String
|
||||
switch content {
|
||||
case let .text(text):
|
||||
title = text
|
||||
case let .icon(icon, _):
|
||||
title = ""
|
||||
self.iconNode.image = generateTintedImage(image: icon, color: .white)
|
||||
case let .dayNight(isNight):
|
||||
title = ""
|
||||
let animationNode = AnimationNode(animation: isNight ? "anim_sun_reverse" : "anim_sun", colors: [:], scale: 1.0)
|
||||
animationNode.speed = 1.66
|
||||
animationNode.isUserInteractionEnabled = false
|
||||
self.animationNode = animationNode
|
||||
}
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: .white)
|
||||
|
||||
super.init()
|
||||
|
||||
self.isExclusiveTouch = true
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
if let animationNode = self.animationNode {
|
||||
self.addSubnode(animationNode)
|
||||
}
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.backgroundNode.alpha = 0.4
|
||||
|
||||
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.iconNode.alpha = 0.4
|
||||
|
||||
strongSelf.textNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.textNode.alpha = 0.4
|
||||
|
||||
// if let animationNode = strongSelf.animationNode {
|
||||
// animationNode.layer.removeAnimation(forKey: "opacity")
|
||||
// animationNode.alpha = 0.4
|
||||
// }
|
||||
} else {
|
||||
strongSelf.backgroundNode.alpha = 1.0
|
||||
strongSelf.backgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
|
||||
strongSelf.iconNode.alpha = 1.0
|
||||
strongSelf.iconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
|
||||
strongSelf.textNode.alpha = 1.0
|
||||
strongSelf.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
|
||||
// if let animationNode = strongSelf.animationNode {
|
||||
// animationNode.alpha = 1.0
|
||||
// animationNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setIsNight(_ isNight: Bool) {
|
||||
self.animationNode?.setAnimation(name: !isNight ? "anim_sun_reverse" : "anim_sun", colors: [:])
|
||||
self.animationNode?.speed = 1.66
|
||||
self.animationNode?.playOnce()
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
Queue.mainQueue().after(0.4) {
|
||||
self.isUserInteractionEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
var buttonColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.3) {
|
||||
didSet {
|
||||
}
|
||||
}
|
||||
|
||||
private var textSize: CGSize?
|
||||
override func measure(_ constrainedSize: CGSize) -> CGSize {
|
||||
switch self.content {
|
||||
case .text:
|
||||
let size = self.textNode.updateLayout(constrainedSize)
|
||||
self.textSize = size
|
||||
return CGSize(width: ceil(size.width) + 16.0, height: 28.0)
|
||||
case let .icon(_, size):
|
||||
return size
|
||||
case .dayNight:
|
||||
return CGSize(width: 28.0, height: 28.0)
|
||||
}
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
self.backgroundNode.frame = self.bounds
|
||||
if let backgroundNode = self.backgroundNode as? WallpaperOptionBackgroundNode {
|
||||
backgroundNode.updateLayout(size: self.backgroundNode.bounds.size)
|
||||
} else if let backgroundNode = self.backgroundNode as? WallpaperLightButtonBackgroundNode {
|
||||
backgroundNode.updateLayout(size: self.backgroundNode.bounds.size)
|
||||
}
|
||||
self.backgroundNode.cornerRadius = size.height / 2.0
|
||||
|
||||
self.iconNode.frame = self.bounds
|
||||
|
||||
if let textSize = self.textSize {
|
||||
self.textNode.frame = CGRect(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0), width: textSize.width, height: textSize.height)
|
||||
}
|
||||
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.bounds = CGRect(origin: .zero, size: CGSize(width: 24.0, height: 24.0))
|
||||
animationNode.position = CGPoint(x: 14.0, y: 14.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public final class WallpaperOptionButtonNode: HighlightTrackingButtonNode {
|
||||
let backgroundNode: WallpaperOptionBackgroundNode
|
||||
|
||||
private let checkNode: CheckNode
|
||||
private let colorNode: ASImageNode
|
||||
|
||||
private let textNode: ImmediateTextNode
|
||||
|
||||
private var textSize: CGSize?
|
||||
|
||||
private var _value: WallpaperOptionButtonValue
|
||||
public override var isSelected: Bool {
|
||||
get {
|
||||
switch self._value {
|
||||
case let .check(selected), let .color(selected, _), let .colors(selected, _):
|
||||
return selected
|
||||
}
|
||||
}
|
||||
set {
|
||||
switch self._value {
|
||||
case .check:
|
||||
self._value = .check(newValue)
|
||||
case let .color(_, color):
|
||||
self._value = .color(newValue, color)
|
||||
case let .colors(_, colors):
|
||||
self._value = .colors(newValue, colors)
|
||||
}
|
||||
self.checkNode.setSelected(newValue, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
public var title: String {
|
||||
didSet {
|
||||
self.textNode.attributedText = NSAttributedString(string: title, font: Font.medium(13), textColor: .white)
|
||||
}
|
||||
}
|
||||
|
||||
public init(title: String, value: WallpaperOptionButtonValue) {
|
||||
self._value = value
|
||||
self.title = title
|
||||
|
||||
self.backgroundNode = WallpaperOptionBackgroundNode()
|
||||
|
||||
self.checkNode = CheckNode(theme: CheckNodeTheme(backgroundColor: .white, strokeColor: .clear, borderColor: .white, overlayBorder: false, hasInset: false, hasShadow: false, borderWidth: 1.5))
|
||||
self.checkNode.isUserInteractionEnabled = false
|
||||
|
||||
self.colorNode = ASImageNode()
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.attributedText = NSAttributedString(string: title, font: Font.medium(13), textColor: .white)
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.cornerRadius = 14.0
|
||||
self.isExclusiveTouch = true
|
||||
|
||||
switch value {
|
||||
case let .check(selected):
|
||||
self.checkNode.isHidden = false
|
||||
self.colorNode.isHidden = true
|
||||
self.checkNode.selected = selected
|
||||
case let .color(_, color):
|
||||
self.checkNode.isHidden = true
|
||||
self.colorNode.isHidden = false
|
||||
self.colorNode.image = generateFilledCircleImage(diameter: 18.0, color: color)
|
||||
case let .colors(_, colors):
|
||||
self.checkNode.isHidden = true
|
||||
self.colorNode.isHidden = false
|
||||
self.colorNode.image = generateColorsImage(diameter: 18.0, colors: colors)
|
||||
}
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
|
||||
self.addSubnode(self.checkNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.colorNode)
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.backgroundNode.alpha = 0.4
|
||||
|
||||
strongSelf.colorNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.colorNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.backgroundNode.alpha = 1.0
|
||||
strongSelf.backgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
|
||||
strongSelf.colorNode.alpha = 1.0
|
||||
strongSelf.colorNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var buttonColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.3) {
|
||||
didSet {
|
||||
}
|
||||
}
|
||||
|
||||
public var color: UIColor? {
|
||||
get {
|
||||
switch self._value {
|
||||
case let .color(_, color):
|
||||
return color
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
set {
|
||||
if let color = newValue {
|
||||
switch self._value {
|
||||
case let .color(selected, _):
|
||||
self._value = .color(selected, color)
|
||||
self.colorNode.image = generateFilledCircleImage(diameter: 18.0, color: color)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var colors: [UIColor]? {
|
||||
get {
|
||||
switch self._value {
|
||||
case let .colors(_, colors):
|
||||
return colors
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
set {
|
||||
if let colors = newValue {
|
||||
switch self._value {
|
||||
case let .colors(selected, current):
|
||||
if current.count == colors.count {
|
||||
var updated = false
|
||||
for i in 0 ..< current.count {
|
||||
if !current[i].isEqual(colors[i]) {
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
return
|
||||
}
|
||||
}
|
||||
self._value = .colors(selected, colors)
|
||||
self.colorNode.image = generateColorsImage(diameter: 18.0, colors: colors)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func setSelected(_ selected: Bool, animated: Bool = false) {
|
||||
switch self._value {
|
||||
case .check:
|
||||
self._value = .check(selected)
|
||||
case let .color(_, color):
|
||||
self._value = .color(selected, color)
|
||||
case let .colors(_, colors):
|
||||
self._value = .colors(selected, colors)
|
||||
}
|
||||
self.checkNode.setSelected(selected, animated: animated)
|
||||
}
|
||||
|
||||
public func setEnabled(_ enabled: Bool) {
|
||||
let alpha: CGFloat = enabled ? 1.0 : 0.4
|
||||
self.checkNode.alpha = alpha
|
||||
self.colorNode.alpha = alpha
|
||||
self.textNode.alpha = alpha
|
||||
self.isUserInteractionEnabled = enabled
|
||||
}
|
||||
|
||||
public override func measure(_ constrainedSize: CGSize) -> CGSize {
|
||||
let size = self.textNode.updateLayout(constrainedSize)
|
||||
self.textSize = size
|
||||
return CGSize(width: ceil(size.width) + 48.0, height: 30.0)
|
||||
}
|
||||
|
||||
public override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.backgroundNode.frame = self.bounds
|
||||
self.backgroundNode.updateLayout(size: self.backgroundNode.bounds.size)
|
||||
|
||||
guard let _ = self.textSize else {
|
||||
return
|
||||
}
|
||||
|
||||
let padding: CGFloat = 6.0
|
||||
let spacing: CGFloat = 9.0
|
||||
let checkSize = CGSize(width: 18.0, height: 18.0)
|
||||
let checkFrame = CGRect(origin: CGPoint(x: padding, y: padding), size: checkSize)
|
||||
self.checkNode.frame = checkFrame
|
||||
self.colorNode.frame = checkFrame
|
||||
|
||||
if let textSize = self.textSize {
|
||||
self.textNode.frame = CGRect(x: max(padding + checkSize.width + spacing, padding + checkSize.width + floor((self.bounds.width - padding - checkSize.width - textSize.width) / 2.0) - 2.0), y: floorToScreenPixels((self.bounds.height - textSize.height) / 2.0), width: textSize.width, height: textSize.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class WallpaperSliderNode: ASDisplayNode {
|
||||
let minValue: CGFloat
|
||||
let maxValue: CGFloat
|
||||
var value: CGFloat = 1.0 {
|
||||
didSet {
|
||||
if let size = self.validLayout {
|
||||
self.updateLayout(size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
|
||||
private let foregroundNode: ASDisplayNode
|
||||
private let foregroundLightNode: ASDisplayNode
|
||||
private let leftIconNode: ASImageNode
|
||||
private let rightIconNode: ASImageNode
|
||||
|
||||
private let valueChanged: (CGFloat, Bool) -> Void
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
init(minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) {
|
||||
self.minValue = minValue
|
||||
self.maxValue = maxValue
|
||||
self.value = value
|
||||
self.valueChanged = valueChanged
|
||||
|
||||
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x333333, alpha: 0.35), enableBlur: true, enableSaturation: false)
|
||||
|
||||
self.foregroundNode = ASDisplayNode()
|
||||
self.foregroundNode.clipsToBounds = true
|
||||
self.foregroundNode.cornerRadius = 3.0
|
||||
self.foregroundNode.isAccessibilityElement = false
|
||||
self.foregroundNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.75)
|
||||
self.foregroundNode.layer.compositingFilter = "overlayBlendMode"
|
||||
self.foregroundNode.isUserInteractionEnabled = false
|
||||
|
||||
self.foregroundLightNode = ASDisplayNode()
|
||||
self.foregroundLightNode.clipsToBounds = true
|
||||
self.foregroundLightNode.cornerRadius = 3.0
|
||||
self.foregroundLightNode.backgroundColor = UIColor(rgb: 0xf2f2f2, alpha: 0.2)
|
||||
|
||||
self.leftIconNode = ASImageNode()
|
||||
self.leftIconNode.displaysAsynchronously = false
|
||||
self.leftIconNode.image = UIImage(bundleImageName: "Settings/WallpaperBrightnessMin")
|
||||
self.leftIconNode.contentMode = .center
|
||||
|
||||
self.rightIconNode = ASImageNode()
|
||||
self.rightIconNode.displaysAsynchronously = false
|
||||
self.rightIconNode.image = UIImage(bundleImageName: "Settings/WallpaperBrightnessMax")
|
||||
self.rightIconNode.contentMode = .center
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.cornerRadius = 15.0
|
||||
self.isUserInteractionEnabled = true
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
|
||||
self.addSubnode(self.foregroundNode)
|
||||
self.addSubnode(self.foregroundLightNode)
|
||||
|
||||
self.addSubnode(self.leftIconNode)
|
||||
self.addSubnode(self.rightIconNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||
self.view.addGestureRecognizer(panGestureRecognizer)
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
self.view.addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
|
||||
var ignoreUpdates = false
|
||||
func animateValue(from: CGFloat, to: CGFloat, transition: ContainedViewLayoutTransition = .immediate) {
|
||||
guard let size = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.internalUpdateLayout(size: size, value: from)
|
||||
self.internalUpdateLayout(size: size, value: to, transition: transition)
|
||||
}
|
||||
|
||||
func internalUpdateLayout(size: CGSize, value: CGFloat, transition: ContainedViewLayoutTransition = .immediate) {
|
||||
self.validLayout = size
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: size))
|
||||
self.backgroundNode.update(size: size, transition: transition)
|
||||
|
||||
if let icon = self.leftIconNode.image {
|
||||
transition.updateFrame(node: self.leftIconNode, frame: CGRect(origin: CGPoint(x: 7.0, y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size))
|
||||
}
|
||||
|
||||
if let icon = self.rightIconNode.image {
|
||||
transition.updateFrame(node: self.rightIconNode, frame: CGRect(origin: CGPoint(x: size.width - icon.size.width - 6.0, y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size))
|
||||
}
|
||||
|
||||
let range = self.maxValue - self.minValue
|
||||
let value = (value - self.minValue) / range
|
||||
let foregroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: value * size.width, height: size.height))
|
||||
transition.updateFrame(node: self.foregroundNode, frame: foregroundFrame)
|
||||
transition.updateFrame(node: self.foregroundLightNode, frame: foregroundFrame)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition = .immediate) {
|
||||
guard !self.ignoreUpdates else {
|
||||
return
|
||||
}
|
||||
self.internalUpdateLayout(size: size, value: self.value, transition: transition)
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
let range = self.maxValue - self.minValue
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
break
|
||||
case .changed:
|
||||
let previousValue = self.value
|
||||
|
||||
let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x
|
||||
let delta = translation / self.bounds.width * range
|
||||
self.value = max(self.minValue, min(self.maxValue, self.value + delta))
|
||||
gestureRecognizer.setTranslation(CGPoint(), in: gestureRecognizer.view)
|
||||
|
||||
if self.value == 0.0 && previousValue != 0.0 {
|
||||
self.hapticFeedback.impact(.soft)
|
||||
} else if self.value == 1.0 && previousValue != 1.0 {
|
||||
self.hapticFeedback.impact(.soft)
|
||||
}
|
||||
if abs(previousValue - self.value) >= 0.001 {
|
||||
self.valueChanged(self.value, false)
|
||||
}
|
||||
case .ended:
|
||||
let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x
|
||||
let delta = translation / self.bounds.width * range
|
||||
self.value = max(self.minValue, min(self.maxValue, self.value + delta))
|
||||
self.valueChanged(self.value, true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
let range = self.maxValue - self.minValue
|
||||
let location = gestureRecognizer.location(in: gestureRecognizer.view)
|
||||
self.value = max(self.minValue, min(self.maxValue, self.minValue + location.x / self.bounds.width * range))
|
||||
self.valueChanged(self.value, true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import LegacyComponents
|
||||
import AccountContext
|
||||
import MergeLists
|
||||
import Postbox
|
||||
import SettingsThemeWallpaperNode
|
||||
|
||||
private let itemSize = CGSize(width: 88.0, height: 88.0)
|
||||
private let inset: CGFloat = 12.0
|
||||
|
||||
private func intensityToSliderValue(_ value: Int32, allowDark: Bool) -> CGFloat {
|
||||
if allowDark {
|
||||
if value < 0 {
|
||||
return max(0.0, min(100.0, CGFloat(abs(value))))
|
||||
} else {
|
||||
return 100.0 + max(0.0, min(100.0, CGFloat(value)))
|
||||
}
|
||||
} else {
|
||||
return CGFloat(max(value, 0)) * 2.0
|
||||
}
|
||||
}
|
||||
|
||||
private func sliderValueToIntensity(_ value: CGFloat, allowDark: Bool) -> Int32 {
|
||||
if allowDark {
|
||||
if value < 100.0 {
|
||||
return -Int32(max(1.0, value))
|
||||
} else {
|
||||
return Int32(value - 100.0)
|
||||
}
|
||||
} else {
|
||||
return Int32(value / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
private struct WallpaperPatternEntry: Comparable, Identifiable {
|
||||
let index: Int
|
||||
let wallpaper: TelegramWallpaper
|
||||
let selected: Bool
|
||||
|
||||
var stableId: Int64 {
|
||||
if case let .file(file) = self.wallpaper {
|
||||
return file.id
|
||||
} else {
|
||||
return Int64(self.index)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: WallpaperPatternEntry, rhs: WallpaperPatternEntry) -> Bool {
|
||||
if lhs.index != rhs.index {
|
||||
return false
|
||||
}
|
||||
if lhs.wallpaper != rhs.wallpaper {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static func <(lhs: WallpaperPatternEntry, rhs: WallpaperPatternEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(context: AccountContext, action: @escaping (TelegramWallpaper) -> Void) -> ListViewItem {
|
||||
return WallpaperPatternItem(context: context, wallpaper: self.wallpaper, selected: self.selected, action: action)
|
||||
}
|
||||
}
|
||||
|
||||
private class WallpaperPatternItem: ListViewItem {
|
||||
let context: AccountContext
|
||||
let wallpaper: TelegramWallpaper
|
||||
let selected: Bool
|
||||
let action: (TelegramWallpaper) -> Void
|
||||
|
||||
public init(context: AccountContext, wallpaper: TelegramWallpaper, selected: Bool, action: @escaping (TelegramWallpaper) -> Void) {
|
||||
self.context = context
|
||||
self.wallpaper = wallpaper
|
||||
self.selected = selected
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = WallpaperPatternItemNode()
|
||||
let (nodeLayout, apply) = node.asyncLayout()(self, params)
|
||||
node.insets = nodeLayout.insets
|
||||
node.contentSize = nodeLayout.contentSize
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in
|
||||
apply(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
assert(node() is WallpaperPatternItemNode)
|
||||
if let nodeValue = node() as? WallpaperPatternItemNode {
|
||||
let layout = nodeValue.asyncLayout()
|
||||
async {
|
||||
let (nodeLayout, apply) = layout(self, params)
|
||||
Queue.mainQueue().async {
|
||||
completion(nodeLayout, { _ in
|
||||
apply(animation.isAnimated)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var selectable = true
|
||||
public func selected(listView: ListView) {
|
||||
self.action(self.wallpaper)
|
||||
}
|
||||
}
|
||||
|
||||
private final class WallpaperPatternItemNode : ListViewItemNode {
|
||||
private let wallpaperNode: SettingsThemeWallpaperNode
|
||||
|
||||
var item: WallpaperPatternItem?
|
||||
|
||||
init() {
|
||||
self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: true)
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.addSubnode(self.wallpaperNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (WallpaperPatternItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
return { [weak self] item, params in
|
||||
let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 112.0, height: 112.0), insets: UIEdgeInsets())
|
||||
return (itemLayout, { animated in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.wallpaperNode.frame = CGRect(x: 0.0, y: 12.0, width: itemSize.width, height: itemSize.height)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration, short: short)
|
||||
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateRemoved(currentTimestamp, duration: duration)
|
||||
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateAdded(currentTimestamp, duration: duration)
|
||||
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
public final class WallpaperPatternPanelNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private var theme: PresentationTheme
|
||||
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
private let topSeparatorNode: ASDisplayNode
|
||||
|
||||
public let scrollNode: ASScrollNode
|
||||
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let labelNode: ImmediateTextNode
|
||||
private var sliderView: TGPhotoEditorSliderView?
|
||||
|
||||
private var disposable: Disposable?
|
||||
public var wallpapers: [TelegramWallpaper] = []
|
||||
private var currentWallpaper: TelegramWallpaper?
|
||||
|
||||
public var serviceBackgroundColor: UIColor = UIColor(rgb: 0x748698) {
|
||||
didSet {
|
||||
guard let nodes = self.scrollNode.subnodes else {
|
||||
return
|
||||
}
|
||||
for case let node as SettingsThemeWallpaperNode in nodes {
|
||||
node.setOverlayBackgroundColor(self.serviceBackgroundColor.withAlphaComponent(0.4))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var backgroundColors: ([HSBColor], Int32?, Int32?)? = nil {
|
||||
didSet {
|
||||
var updated = false
|
||||
if oldValue?.0 != self.backgroundColors?.0 || oldValue?.1 != self.backgroundColors?.1 {
|
||||
updated = true
|
||||
} else if oldValue?.2 != self.backgroundColors?.2 {
|
||||
if let oldIntensity = oldValue?.2, let newIntensity = self.backgroundColors?.2 {
|
||||
if (oldIntensity < 0) != (newIntensity < 0) {
|
||||
updated = true
|
||||
}
|
||||
} else if (oldValue?.2 != nil) != (self.backgroundColors?.2 != nil) {
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
if updated {
|
||||
self.updateWallpapers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var validLayout: (CGSize, CGFloat)?
|
||||
|
||||
public var patternChanged: ((TelegramWallpaper?, Int32?, Bool) -> Void)?
|
||||
|
||||
private let allowDark: Bool
|
||||
|
||||
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.allowDark = theme.overallDarkAppearance
|
||||
|
||||
self.backgroundNode = NavigationBackgroundNode(color: theme.chat.inputPanel.panelBackgroundColor)
|
||||
|
||||
self.topSeparatorNode = ASDisplayNode()
|
||||
self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.attributedText = NSAttributedString(string: strings.WallpaperPreview_PatternTitle, font: Font.bold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
|
||||
|
||||
self.labelNode = ImmediateTextNode()
|
||||
self.labelNode.attributedText = NSAttributedString(string: strings.WallpaperPreview_PatternIntensity, font: Font.regular(14.0), textColor: theme.rootController.navigationBar.primaryTextColor)
|
||||
|
||||
super.init()
|
||||
|
||||
self.allowsGroupOpacity = true
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.topSeparatorNode)
|
||||
self.addSubnode(self.scrollNode)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
self.disposable = ((telegramWallpapers(postbox: context.account.postbox, network: context.account.network)
|
||||
|> map { wallpapers -> [TelegramWallpaper] in
|
||||
var existingIds = Set<MediaId>()
|
||||
|
||||
return wallpapers.filter { wallpaper in
|
||||
if case let .file(file) = wallpaper, wallpaper.isPattern, file.file.mimeType != "image/webp" {
|
||||
if file.id == 0 {
|
||||
return true
|
||||
}
|
||||
if existingIds.contains(file.file.fileId) {
|
||||
return false
|
||||
} else {
|
||||
existingIds.insert(file.file.fileId)
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] wallpapers in
|
||||
if let strongSelf = self {
|
||||
strongSelf.wallpapers = wallpapers
|
||||
strongSelf.updateWallpapers()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
}
|
||||
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.alwaysBounceHorizontal = true
|
||||
|
||||
let sliderView = TGPhotoEditorSliderView()
|
||||
sliderView.disableSnapToPositions = true
|
||||
sliderView.trackCornerRadius = 2.0
|
||||
sliderView.lineSize = 4.0
|
||||
sliderView.startValue = 0.0
|
||||
sliderView.minimumValue = 0.0
|
||||
sliderView.maximumValue = 200.0
|
||||
if self.allowDark {
|
||||
sliderView.positionsCount = 3
|
||||
}
|
||||
sliderView.useLinesForPositions = true
|
||||
sliderView.value = intensityToSliderValue(50, allowDark: self.allowDark)
|
||||
sliderView.disablesInteractiveTransitionGestureRecognizer = true
|
||||
sliderView.backgroundColor = .clear
|
||||
sliderView.backColor = self.theme.list.itemSwitchColors.frameColor
|
||||
if self.allowDark {
|
||||
sliderView.trackColor = self.theme.list.disclosureArrowColor
|
||||
} else {
|
||||
sliderView.trackColor = self.theme.list.itemAccentColor
|
||||
}
|
||||
|
||||
self.view.addSubview(sliderView)
|
||||
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
|
||||
self.sliderView = sliderView
|
||||
}
|
||||
|
||||
public func updateWallpapers() {
|
||||
guard let subnodes = self.scrollNode.subnodes else {
|
||||
return
|
||||
}
|
||||
|
||||
for node in subnodes {
|
||||
node.removeFromSupernode()
|
||||
}
|
||||
|
||||
let backgroundColors = self.backgroundColors.flatMap { ($0.0.map({ $0.rgb }), $0.1, $0.2) } ?? ([0xd6e2ee], nil, nil)
|
||||
let intensity: Int32 = backgroundColors.2.flatMap { value in
|
||||
if value < 0 {
|
||||
return -80
|
||||
} else {
|
||||
return 80
|
||||
}
|
||||
} ?? 80
|
||||
|
||||
var selectedFileId: Int64?
|
||||
var selectedSlug: String?
|
||||
if let currentWallpaper = self.currentWallpaper, case let .file(file) = currentWallpaper {
|
||||
selectedFileId = file.id
|
||||
selectedSlug = file.slug
|
||||
}
|
||||
|
||||
for wallpaper in self.wallpapers {
|
||||
let node = SettingsThemeWallpaperNode(displayLoading: true, overlayBackgroundColor: self.serviceBackgroundColor.withAlphaComponent(0.4))
|
||||
node.clipsToBounds = true
|
||||
node.cornerRadius = 5.0
|
||||
|
||||
var updatedWallpaper = wallpaper
|
||||
if case let .file(file) = updatedWallpaper {
|
||||
let settings = WallpaperSettings(colors: backgroundColors.0, intensity: intensity, rotation: backgroundColors.1)
|
||||
updatedWallpaper = .file(TelegramWallpaper.File(id: file.id, accessHash: file.accessHash, isCreator: file.isCreator, isDefault: file.isDefault, isPattern: updatedWallpaper.isPattern, isDark: file.isDark, slug: file.slug, file: file.file, settings: settings))
|
||||
}
|
||||
|
||||
var selected = false
|
||||
if case let .file(file) = wallpaper, (file.id == selectedFileId || file.slug == selectedSlug) {
|
||||
selected = true
|
||||
}
|
||||
|
||||
node.setWallpaper(context: self.context, wallpaper: updatedWallpaper, selected: selected, size: itemSize)
|
||||
node.pressed = { [weak self, weak node] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.currentWallpaper = updatedWallpaper
|
||||
if let sliderView = strongSelf.sliderView {
|
||||
strongSelf.patternChanged?(updatedWallpaper, sliderValueToIntensity(sliderView.value, allowDark: strongSelf.allowDark), false)
|
||||
}
|
||||
if let subnodes = strongSelf.scrollNode.subnodes {
|
||||
for case let subnode as SettingsThemeWallpaperNode in subnodes {
|
||||
let selected = node === subnode
|
||||
subnode.setSelected(selected, animated: true)
|
||||
if selected {
|
||||
strongSelf.scrollToNode(subnode, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.scrollNode.addSubnode(node)
|
||||
}
|
||||
|
||||
self.scrollNode.view.contentSize = CGSize(width: (itemSize.width + inset) * CGFloat(wallpapers.count) + inset, height: 112.0)
|
||||
self.layoutItemNodes(transition: .immediate)
|
||||
}
|
||||
|
||||
public func updateTheme(_ theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundNode.updateColor(color: self.theme.chat.inputPanel.panelBackgroundColor, transition: .immediate)
|
||||
self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor
|
||||
|
||||
self.sliderView?.backColor = self.theme.list.itemSwitchColors.frameColor
|
||||
if self.allowDark {
|
||||
self.sliderView?.trackColor = self.theme.list.disclosureArrowColor
|
||||
} else {
|
||||
self.sliderView?.trackColor = self.theme.list.itemAccentColor
|
||||
}
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.labelNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||
self.labelNode.attributedText = NSAttributedString(string: self.labelNode.attributedText?.string ?? "", font: Font.regular(14.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
||||
|
||||
if let (size, bottomInset) = self.validLayout {
|
||||
self.updateLayout(size: size, bottomInset: bottomInset, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func sliderValueChanged() {
|
||||
guard let sliderView = self.sliderView else {
|
||||
return
|
||||
}
|
||||
|
||||
if let wallpaper = self.currentWallpaper {
|
||||
self.patternChanged?(wallpaper, sliderValueToIntensity(sliderView.value, allowDark: self.allowDark), sliderView.isTracking)
|
||||
}
|
||||
}
|
||||
|
||||
public func didAppear(initialWallpaper: TelegramWallpaper? = nil, intensity: Int32? = nil) {
|
||||
let wallpaper: TelegramWallpaper?
|
||||
|
||||
if self.wallpapers.isEmpty {
|
||||
wallpaper = nil
|
||||
} else {
|
||||
switch initialWallpaper {
|
||||
case var .file(file):
|
||||
file.settings = self.wallpapers[0].settings ?? WallpaperSettings()
|
||||
wallpaper = .file(file)
|
||||
default:
|
||||
wallpaper = self.wallpapers.first
|
||||
}
|
||||
}
|
||||
|
||||
if let wallpaper = wallpaper {
|
||||
var selectedFileId: Int64?
|
||||
if case let .file(file) = wallpaper {
|
||||
selectedFileId = file.id
|
||||
}
|
||||
|
||||
self.currentWallpaper = wallpaper
|
||||
self.sliderView?.value = intensity.flatMap { intensityToSliderValue($0, allowDark: self.allowDark) } ?? intensityToSliderValue(50, allowDark: self.allowDark)
|
||||
|
||||
self.scrollNode.view.contentOffset = CGPoint()
|
||||
|
||||
var selectedNode: SettingsThemeWallpaperNode?
|
||||
if let subnodes = self.scrollNode.subnodes {
|
||||
for case let subnode as SettingsThemeWallpaperNode in subnodes {
|
||||
var selected = false
|
||||
if case let .file(file) = subnode.wallpaper, file.id == selectedFileId {
|
||||
selected = true
|
||||
selectedNode = subnode
|
||||
}
|
||||
subnode.setSelected(selected, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
if let wallpaper = self.currentWallpaper, let sliderView = self.sliderView {
|
||||
self.patternChanged?(wallpaper, sliderValueToIntensity(sliderView.value, allowDark: self.allowDark), false)
|
||||
}
|
||||
|
||||
if let selectedNode = selectedNode {
|
||||
self.scrollToNode(selectedNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func scrollToNode(_ node: SettingsThemeWallpaperNode, animated: Bool = false) {
|
||||
let bounds = self.scrollNode.view.bounds
|
||||
let frame = node.frame.insetBy(dx: -48.0, dy: 0.0)
|
||||
|
||||
if frame.minX < bounds.minX || frame.maxX > bounds.maxX {
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate
|
||||
|
||||
var origin = CGPoint()
|
||||
if frame.minX < bounds.minX {
|
||||
origin.x = max(0.0, frame.minX)
|
||||
} else if frame.maxX > bounds.maxX {
|
||||
origin.x = min(self.scrollNode.view.contentSize.width - bounds.width, frame.maxX - bounds.width)
|
||||
}
|
||||
|
||||
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: origin, size: self.scrollNode.frame.size))
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (size, bottomInset)
|
||||
|
||||
let backgroundFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height + bottomInset)
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
self.backgroundNode.update(size: backgroundFrame.size, transition: transition)
|
||||
transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: UIScreenPixel))
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(self.bounds.size)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((self.bounds.width - titleSize.width) / 2.0), y: 19.0), size: titleSize))
|
||||
|
||||
let scrollViewFrame = CGRect(x: 0.0, y: 52.0, width: size.width, height: 114.0)
|
||||
transition.updateFrame(node: self.scrollNode, frame: scrollViewFrame)
|
||||
|
||||
let labelSize = self.labelNode.updateLayout(self.bounds.size)
|
||||
let combinedHeight = labelSize.height + 34.0
|
||||
|
||||
let originY: CGFloat = scrollViewFrame.maxY + floor((size.height - scrollViewFrame.maxY - combinedHeight) / 2.0)
|
||||
transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: 14.0, y: originY), size: labelSize))
|
||||
|
||||
self.sliderView?.frame = CGRect(origin: CGPoint(x: 15.0, y: originY + 8.0), size: CGSize(width: size.width - 15.0 * 2.0, height: 44.0))
|
||||
|
||||
self.layoutItemNodes(transition: transition)
|
||||
}
|
||||
|
||||
private func layoutItemNodes(transition: ContainedViewLayoutTransition) {
|
||||
var offset: CGFloat = 12.0
|
||||
if let subnodes = self.scrollNode.subnodes {
|
||||
for node in subnodes {
|
||||
transition.updateFrame(node: node, frame: CGRect(x: offset, y: 12.0, width: itemSize.width, height: itemSize.height))
|
||||
offset += inset + itemSize.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user