Swiftgram/submodules/LocationUI/Sources/LocationMapHeaderNode.swift
2021-05-18 19:30:41 +04:00

255 lines
16 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AppBundle
import CoreLocation
private let panelInset: CGFloat = 4.0
private let panelButtonSize = CGSize(width: 46.0, height: 46.0)
private func generateBackgroundImage(theme: PresentationTheme) -> UIImage? {
let cornerRadius: CGFloat = 9.0
return generateImage(CGSize(width: (cornerRadius + panelInset) * 2.0, height: (cornerRadius + panelInset) * 2.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.2).cgColor)
context.setFillColor(theme.rootController.navigationBar.opaqueBackgroundColor.cgColor)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: panelInset, y: panelInset), size: CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)), cornerRadius: cornerRadius)
context.addPath(path.cgPath)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius + panelInset), topCapHeight: Int(cornerRadius + panelInset))
}
private func generateShadowImage(theme: PresentationTheme, highlighted: Bool) -> UIImage? {
return generateImage(CGSize(width: 26.0, height: 14.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.2).cgColor)
context.setFillColor(highlighted ? theme.list.itemHighlightedBackgroundColor.cgColor : theme.list.plainBackgroundColor.cgColor)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 0.0, y: 4.0), size: CGSize(width: 26.0, height: 20.0)), cornerRadius: 9.0)
context.addPath(path.cgPath)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 13, topCapHeight: 0)
}
final class LocationMapHeaderNode: ASDisplayNode {
private var presentationData: PresentationData
private let toggleMapModeSelection: () -> Void
private let goToUserLocation: () -> Void
private let showPlacesInThisArea: () -> Void
private let setupProximityNotification: (Bool) -> Void
private var displayingPlacesButton = false
private var proximityNotification: Bool?
let mapNode: LocationMapNode
var trackingMode: LocationTrackingMode = .none
private let optionsBackgroundNode: ASImageNode
private let optionsSeparatorNode: ASDisplayNode
private let optionsSecondSeparatorNode: ASDisplayNode
private let infoButtonNode: HighlightableButtonNode
private let locationButtonNode: HighlightableButtonNode
private let notificationButtonNode: HighlightableButtonNode
private let placesBackgroundNode: ASImageNode
private let placesButtonNode: HighlightableButtonNode
private let shadowNode: ASImageNode
private var validLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat, CGSize)?
init(presentationData: PresentationData, toggleMapModeSelection: @escaping () -> Void, goToUserLocation: @escaping () -> Void, setupProximityNotification: @escaping (Bool) -> Void = { _ in }, showPlacesInThisArea: @escaping () -> Void = {}) {
self.presentationData = presentationData
self.toggleMapModeSelection = toggleMapModeSelection
self.goToUserLocation = goToUserLocation
self.setupProximityNotification = setupProximityNotification
self.showPlacesInThisArea = showPlacesInThisArea
self.mapNode = LocationMapNode()
self.optionsBackgroundNode = ASImageNode()
self.optionsBackgroundNode.contentMode = .scaleToFill
self.optionsBackgroundNode.displaysAsynchronously = false
self.optionsBackgroundNode.displayWithoutProcessing = true
self.optionsBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme)
self.optionsBackgroundNode.isUserInteractionEnabled = true
self.optionsSeparatorNode = ASDisplayNode()
self.optionsSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor
self.optionsSecondSeparatorNode = ASDisplayNode()
self.optionsSecondSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor
self.infoButtonNode = HighlightableButtonNode()
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected)
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted])
self.locationButtonNode = HighlightableButtonNode()
self.locationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/TrackIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
self.notificationButtonNode = HighlightableButtonNode()
self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/NotificationIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/MuteIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected)
self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/MuteIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted])
self.placesBackgroundNode = ASImageNode()
self.placesBackgroundNode.contentMode = .scaleToFill
self.placesBackgroundNode.displaysAsynchronously = false
self.placesBackgroundNode.displayWithoutProcessing = true
self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme)
self.placesBackgroundNode.isUserInteractionEnabled = true
self.placesButtonNode = HighlightableButtonNode()
self.placesButtonNode.setTitle(presentationData.strings.Map_PlacesInThisArea, with: Font.regular(17.0), with: presentationData.theme.rootController.navigationBar.buttonColor, for: .normal)
self.shadowNode = ASImageNode()
self.shadowNode.contentMode = .scaleToFill
self.shadowNode.displaysAsynchronously = false
self.shadowNode.displayWithoutProcessing = true
self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false)
super.init()
self.clipsToBounds = true
self.addSubnode(self.mapNode)
self.addSubnode(self.optionsBackgroundNode)
self.optionsBackgroundNode.addSubnode(self.optionsSeparatorNode)
self.optionsBackgroundNode.addSubnode(self.optionsSecondSeparatorNode)
self.optionsBackgroundNode.addSubnode(self.infoButtonNode)
self.optionsBackgroundNode.addSubnode(self.locationButtonNode)
self.optionsBackgroundNode.addSubnode(self.notificationButtonNode)
self.addSubnode(self.placesBackgroundNode)
self.placesBackgroundNode.addSubnode(self.placesButtonNode)
self.addSubnode(self.shadowNode)
self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside)
self.locationButtonNode.addTarget(self, action: #selector(self.locationPressed), forControlEvents: .touchUpInside)
self.notificationButtonNode.addTarget(self, action: #selector(self.notificationPressed), forControlEvents: .touchUpInside)
self.placesButtonNode.addTarget(self, action: #selector(self.placesPressed), forControlEvents: .touchUpInside)
}
func updateState(mapMode: LocationMapMode, trackingMode: LocationTrackingMode, displayingMapModeOptions: Bool, displayingPlacesButton: Bool, proximityNotification: Bool?, animated: Bool) {
self.mapNode.mapMode = mapMode
self.trackingMode = trackingMode
self.infoButtonNode.isSelected = displayingMapModeOptions
self.notificationButtonNode.isSelected = proximityNotification ?? false
self.locationButtonNode.setImage(generateTintedImage(image: self.iconForTracking(), color: self.presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
let updateLayout = self.displayingPlacesButton != displayingPlacesButton || self.proximityNotification != proximityNotification
self.displayingPlacesButton = displayingPlacesButton
self.proximityNotification = proximityNotification
if updateLayout, let (layout, navigationBarHeight, topPadding, offset, size) = self.validLayout {
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .spring) : .immediate
self.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: topPadding, offset: offset, size: size, transition: transition)
}
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.optionsBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme)
self.optionsSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor
self.optionsSecondSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected)
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted])
self.locationButtonNode.setImage(generateTintedImage(image: self.iconForTracking(), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/NotificationIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/MuteIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected)
self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/MuteIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted])
self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme)
self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false)
}
private func iconForTracking() -> UIImage? {
switch self.trackingMode {
case .none:
return UIImage(bundleImageName: "Location/TrackIcon")
case .follow:
return UIImage(bundleImageName: "Location/TrackActiveIcon")
case .followWithHeading:
return UIImage(bundleImageName: "Location/TrackHeadingIcon")
}
}
func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, topPadding: CGFloat, offset: CGFloat, size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight, topPadding, offset, size)
let mapHeight: CGFloat = floor(layout.size.height * 1.3)
let mapFrame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - mapHeight + navigationBarHeight) / 2.0) + offset, width: size.width, height: mapHeight)
transition.updateFrame(node: self.mapNode, frame: mapFrame)
self.mapNode.updateLayout(size: mapFrame.size)
let inset: CGFloat = 6.0
let placesButtonSize = CGSize(width: 180.0 + panelInset * 2.0, height: 45.0 + panelInset * 2.0)
let placesButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - placesButtonSize.width) / 2.0), y: self.displayingPlacesButton ? navigationBarHeight + topPadding + inset : 0.0), size: placesButtonSize)
transition.updateFrame(node: self.placesBackgroundNode, frame: placesButtonFrame)
transition.updateFrame(node: self.placesButtonNode, frame: CGRect(origin: CGPoint(), size: placesButtonSize))
transition.updateFrame(node: self.shadowNode, frame: CGRect(x: 0.0, y: size.height - 14.0, width: size.width, height: 14.0))
transition.updateFrame(node: self.infoButtonNode, frame: CGRect(x: panelInset, y: panelInset, width: panelButtonSize.width, height: panelButtonSize.height))
transition.updateFrame(node: self.locationButtonNode, frame: CGRect(x: panelInset, y: panelInset + panelButtonSize.height, width: panelButtonSize.width, height: panelButtonSize.height))
transition.updateFrame(node: self.notificationButtonNode, frame: CGRect(x: panelInset, y: panelInset + panelButtonSize.height * 2.0, width: panelButtonSize.width, height: panelButtonSize.height))
transition.updateFrame(node: self.optionsSeparatorNode, frame: CGRect(x: panelInset, y: panelInset + panelButtonSize.height, width: panelButtonSize.width, height: UIScreenPixel))
transition.updateFrame(node: self.optionsSecondSeparatorNode, frame: CGRect(x: panelInset, y: panelInset + panelButtonSize.height * 2.0, width: panelButtonSize.width, height: UIScreenPixel))
var panelHeight: CGFloat = panelButtonSize.height * 2.0
if self.proximityNotification != nil {
panelHeight += panelButtonSize.height
}
transition.updateAlpha(node: self.notificationButtonNode, alpha: self.proximityNotification != nil ? 1.0 : 0.0)
transition.updateAlpha(node: self.optionsSecondSeparatorNode, alpha: self.proximityNotification != nil ? 1.0 : 0.0)
transition.updateFrame(node: self.optionsBackgroundNode, frame: CGRect(x: size.width - inset - panelButtonSize.width - panelInset * 2.0, y: navigationBarHeight + topPadding + inset, width: panelButtonSize.width + panelInset * 2.0, height: panelHeight + panelInset * 2.0))
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
let optionsAlpha: CGFloat = size.height > 160.0 + navigationBarHeight && !self.forceIsHidden ? 1.0 : 0.0
alphaTransition.updateAlpha(node: self.optionsBackgroundNode, alpha: optionsAlpha)
}
var forceIsHidden: Bool = false {
didSet {
if let (layout, navigationBarHeight, topPadding, offset, size) = self.validLayout {
self.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: topPadding, offset: offset, size: size, transition: .immediate)
}
}
}
func updateHighlight(_ highlighted: Bool) {
self.shadowNode.image = generateShadowImage(theme: self.presentationData.theme, highlighted: highlighted)
}
func proximityButtonFrame() -> CGRect? {
if self.notificationButtonNode.alpha > 0.0 {
return self.optionsBackgroundNode.view.convert(self.notificationButtonNode.frame, to: self.view)
} else {
return nil
}
}
@objc private func infoPressed() {
self.toggleMapModeSelection()
}
@objc private func locationPressed() {
self.goToUserLocation()
}
@objc private func notificationPressed() {
if let proximityNotification = self.proximityNotification {
self.setupProximityNotification(proximityNotification)
}
}
@objc private func placesPressed() {
self.showPlacesInThisArea()
}
}