Various improvements

This commit is contained in:
Ilya Laktyushin
2020-10-19 02:01:27 +04:00
parent fe9610eee9
commit e45a3c0ee9
23 changed files with 4544 additions and 4288 deletions

View File

@@ -68,21 +68,26 @@ private class LocationMapView: MKMapView, UIGestureRecognizerDelegate {
}
}
private let arrowImageSize = CGSize(width: 90.0, height: 90.0)
func generateHeadingArrowImage() -> UIImage? {
return generateImage(CGSize(width: 88.0, height: 88.0), contextGenerator: { size, context in
return generateImage(arrowImageSize, contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.move(to: CGPoint(x: 44.0, y: 44.0))
context.addArc(center: CGPoint(x: 44.0, y: 44.0), radius: 44.0, startAngle: CGFloat.pi / 2.0 + CGFloat.pi / 6.0, endAngle: CGFloat.pi / 2.0 - CGFloat.pi / 6.0, clockwise: true)
let center = CGPoint(x: arrowImageSize.width / 2.0, y: arrowImageSize.height / 2.0)
context.move(to: center)
context.addArc(center: center, radius: arrowImageSize.width / 2.0, startAngle: CGFloat.pi / 2.0 + CGFloat.pi / 8.0, endAngle: CGFloat.pi / 2.0 - CGFloat.pi / 8.0, clockwise: true)
context.clip()
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [UIColor(rgb: 0x007ee5, alpha: 0.0).cgColor, UIColor(rgb: 0x007ee5, alpha: 0.75).cgColor, UIColor(rgb: 0x007ee5, alpha: 0.0).cgColor]
var locations: [CGFloat] = [0.0, 0.4, 1.0]
let colors: [CGColor] = [UIColor(rgb: 0x007ee5, alpha: 0.5).cgColor, UIColor(rgb: 0x007ee5, alpha: 0.3).cgColor, UIColor(rgb: 0x007ee5, alpha: 0.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions())
context.drawRadialGradient(gradient, startCenter: center, startRadius: 11.0, endCenter: center, endRadius: arrowImageSize.width / 2.0, options: .drawsAfterEndLocation)
context.setBlendMode(.clear)
context.fillEllipse(in: CGRect(x: (arrowImageSize.width - 22.0) / 2.0, y: (arrowImageSize.height - 22.0) / 2.0, width: 22.0, height: 22.0))
})
}
@@ -98,6 +103,86 @@ private func generateProximityDim(size: CGSize, rect: CGRect) -> UIImage {
}
final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
class ProximityCircleRenderer: MKCircleRenderer {
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
super.draw(mapRect, zoomScale: zoomScale, in: context)
context.saveGState()
let mapBoundingRect = self.circle.boundingMapRect
let mapPoint = MKMapPoint(x: mapBoundingRect.midX, y: mapBoundingRect.maxY)
let drawingPoint = point(for: mapPoint)
if let image = generateTintedImage(image: UIImage(bundleImageName: "Location/ProximityIcon"), color: self.strokeColor ?? .black) {
let imageSize = CGSize(width: floor(image.size.width / zoomScale * self.contentScaleFactor * 0.75), height: floor(image.size.height / zoomScale * self.contentScaleFactor * 0.75))
context.translateBy(x: drawingPoint.x, y: drawingPoint.y)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -drawingPoint.x, y: -drawingPoint.y)
let imageRect = CGRect(x: floor((drawingPoint.x - imageSize.width / 2.0)), y: floor((drawingPoint.y - imageSize.height / 2.0)), width: imageSize.width, height: imageSize.height)
context.clear(imageRect)
context.draw(image.cgImage!, in: imageRect)
}
context.restoreGState()
}
}
class InvertedProximityCircle: NSObject, MKOverlay {
var coordinate: CLLocationCoordinate2D
var radius: Double
var alpha: CGFloat {
didSet {
self.alphaTransition = (oldValue, CACurrentMediaTime(), 0.3)
}
}
var alphaTransition: (from: CGFloat, startTimestamp: Double, duration: Double)?
var boundingMapRect: MKMapRect {
return MKMapRect.world
}
init(center coord: CLLocationCoordinate2D, radius: Double, alpha: CGFloat = 0.0) {
self.coordinate = coord
self.radius = radius
self.alpha = alpha
}
}
class InvertedProximityCircleRenderer: MKOverlayRenderer {
var radius: Double = 0.0
var fillColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.4)
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
guard let overlay = self.overlay as? InvertedProximityCircle else {
return
}
var alpha: CGFloat = overlay.alpha
if let transition = overlay.alphaTransition {
var t = (CACurrentMediaTime() - transition.startTimestamp) / transition.duration
t = min(1.0, max(0.0, t))
alpha = transition.from + (alpha - transition.from) * CGFloat(t)
}
context.setAlpha(alpha)
let path = UIBezierPath(rect: CGRect(x: mapRect.origin.x, y: mapRect.origin.y, width: mapRect.size.width, height: mapRect.size.height))
let radiusInMap = overlay.radius * MKMapPointsPerMeterAtLatitude(overlay.coordinate.latitude)
let mapSize: MKMapSize = MKMapSize(width: radiusInMap, height: radiusInMap)
let regionOrigin = MKMapPoint(overlay.coordinate)
var regionRect: MKMapRect = MKMapRect(origin: regionOrigin, size: mapSize)
regionRect = regionRect.offsetBy(dx: -radiusInMap / 2.0, dy: -radiusInMap / 2.0);
regionRect = regionRect.intersection(MKMapRect.world);
let excludePath: UIBezierPath = UIBezierPath(roundedRect: CGRect(x: regionRect.origin.x, y: regionRect.origin.y, width: regionRect.size.width, height: regionRect.size.height), cornerRadius: CGFloat(regionRect.size.width) / 2.0)
path.append(excludePath)
context.setFillColor(fillColor.cgColor);
context.addPath(path.cgPath);
context.fillPath(using: .evenOdd)
}
}
private weak var currentInvertedCircleRenderer: InvertedProximityCircleRenderer?
private let locationPromise = Promise<CLLocation?>(nil)
private let pickerAnnotationContainerView: PickerAnnotationContainerView
@@ -119,37 +204,96 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
var annotationSelected: ((LocationPinAnnotation?) -> Void)?
var userLocationAnnotationSelected: (() -> Void)?
var proximityDimView = UIImageView()
var proximityRadius: Double? {
var indicatorOverlay: InvertedProximityCircle?
var proximityIndicatorRadius: Double? {
didSet {
if let radius = self.proximityRadius, let mapView = self.mapView {
let region = MKCoordinateRegion(center: mapView.userLocation.coordinate, latitudinalMeters: radius * 2.0, longitudinalMeters: radius * 2.0)
let rect = mapView.convert(region, toRectTo: mapView)
if proximityDimView.image == nil {
proximityDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
if oldValue == 0 {
UIView.transition(with: proximityDimView, duration: 0.2, options: .transitionCrossDissolve) {
self.proximityDimView.image = generateProximityDim(size: mapView.bounds.size, rect: rect)
} completion: { _ in
if let activeProximityRadius = self.proximityIndicatorRadius {
if let location = self.currentUserLocation, activeProximityRadius != oldValue {
let indicatorOverlay: InvertedProximityCircle
if let current = self.indicatorOverlay {
indicatorOverlay = current
indicatorOverlay.radius = activeProximityRadius
self.mapView?.removeOverlay(indicatorOverlay)
self.mapView?.addOverlay(indicatorOverlay)
} else {
indicatorOverlay = InvertedProximityCircle(center: location.coordinate, radius: activeProximityRadius)
self.mapView?.addOverlay(indicatorOverlay)
indicatorOverlay.alpha = 1.0
self.updateAnimations()
}
} else {
proximityDimView.image = generateProximityDim(size: mapView.bounds.size, rect: rect)
self.indicatorOverlay = indicatorOverlay
}
} else {
if proximityDimView.image != nil {
UIView.transition(with: proximityDimView, duration: 0.2, options: .transitionCrossDissolve) {
self.proximityDimView.image = nil
} completion: { _ in
}
if let indicatorOverlay = self.indicatorOverlay {
indicatorOverlay.alpha = 0.0
self.updateAnimations()
}
}
}
}
private var animator: ConstantDisplayLinkAnimator?
private func updateAnimations() {
guard let mapView = self.mapView else {
return
}
var animate = false
let timestamp = CACurrentMediaTime()
if let indicatorOverlay = self.indicatorOverlay, let transition = indicatorOverlay.alphaTransition {
if transition.startTimestamp + transition.duration < timestamp {
indicatorOverlay.alphaTransition = nil
if indicatorOverlay.alpha.isZero {
self.indicatorOverlay = nil
mapView.removeOverlay(indicatorOverlay)
}
} else {
animate = true
}
}
if animate {
let animator: ConstantDisplayLinkAnimator
if let current = self.animator {
animator = current
} else {
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.updateAnimations()
})
animator.frameInterval = 2
self.animator = animator
}
animator.isPaused = false
} else {
self.animator?.isPaused = true
}
self.currentInvertedCircleRenderer?.setNeedsDisplay()
}
private var circleOverlay: MKCircle?
var activeProximityRadius: Double? {
didSet {
if let activeProximityRadius = self.activeProximityRadius {
if let circleOverlay = self.circleOverlay {
self.circleOverlay = nil
self.mapView?.removeOverlay(circleOverlay)
}
if let location = self.currentUserLocation {
let overlay = MKCircle(center: location.coordinate, radius: activeProximityRadius)
self.circleOverlay = overlay
self.mapView?.addOverlay(overlay)
}
} else {
if let circleOverlay = self.circleOverlay {
self.circleOverlay = nil
self.mapView?.removeOverlay(circleOverlay)
}
}
}
}
override init() {
self.pickerAnnotationContainerView = PickerAnnotationContainerView()
self.pickerAnnotationContainerView.isHidden = true
@@ -192,8 +336,6 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
return false
}
self.mapView?.addSubview(self.proximityDimView)
self.view.addSubview(self.pickerAnnotationContainerView)
}
@@ -345,47 +487,55 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
}
}
}
var circleOverlay: MKCircle?
var activeProximityRadius: Double? {
didSet {
if let activeProximityRadius = self.activeProximityRadius {
if let circleOverlay = self.circleOverlay {
self.circleOverlay = nil
self.mapView?.removeOverlay(circleOverlay)
}
if let location = self.currentUserLocation {
let overlay = MKCircle(center: location.coordinate, radius: activeProximityRadius)
self.circleOverlay = overlay
self.mapView?.addOverlay(overlay)
}
} else {
if let circleOverlay = self.circleOverlay {
self.circleOverlay = nil
self.mapView?.removeOverlay(circleOverlay)
}
}
}
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let circle = overlay as? MKCircle {
let renderer = MKCircleRenderer(circle: circle)
if let invertedCircle = overlay as? InvertedProximityCircle {
let renderer = InvertedProximityCircleRenderer(overlay: invertedCircle)
self.currentInvertedCircleRenderer = renderer
return renderer
} else if let circle = overlay as? MKCircle {
let renderer = ProximityCircleRenderer(circle: circle)
renderer.fillColor = .clear
renderer.strokeColor = UIColor(rgb: 0xc3baaf)
renderer.lineWidth = 1.0
renderer.lineDashPattern = [5, 3]
renderer.lineWidth = 0.75
renderer.lineDashPattern = [5, 4]
return renderer
} else {
return MKOverlayRenderer()
}
}
var distance: Double? {
if let annotation = self.annotations.first, let location = self.currentUserLocation {
return location.distance(from: CLLocation(latitude: annotation.coordinate.latitude, longitude: annotation.coordinate.longitude))
func mapView(_ mapView: MKMapView, didAdd renderers: [MKOverlayRenderer]) {
for renderer in renderers {
if let renderer = renderer as? InvertedProximityCircleRenderer {
renderer.alpha = 0.0
UIView.animate(withDuration: 0.3) {
renderer.alpha = 1.0
}
}
}
}
var distancesToAllAnnotations: Signal<[Double], NoError> {
let poll = Signal<[LocationPinAnnotation], NoError> { [weak self] subscriber in
if let strongSelf = self {
subscriber.putNext(strongSelf.annotations)
}
subscriber.putCompletion()
return EmptyDisposable
}
let annotationsPoll = (poll |> then(.complete() |> delay(3.0, queue: Queue.concurrentDefaultQueue()))) |> restart
return combineLatest(self.userLocation, annotationsPoll)
|> map { userLocation, annotations -> [Double] in
var distances: [Double] = []
if let userLocation = userLocation {
for annotation in annotations {
distances.append(userLocation.distance(from: CLLocation(latitude: annotation.coordinate.latitude, longitude: annotation.coordinate.longitude)))
}
}
return distances.sorted()
}
return nil
}
var currentUserLocation: CLLocation? {
@@ -567,7 +717,6 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
}
func updateLayout(size: CGSize) {
self.proximityDimView.frame = CGRect(origin: CGPoint(), size: size)
self.pickerAnnotationContainerView.frame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - size.width) / 2.0), width: size.width, height: size.width)
if let pickerAnnotationView = self.pickerAnnotationView {
pickerAnnotationView.center = CGPoint(x: self.pickerAnnotationContainerView.frame.width / 2.0, y: self.pickerAnnotationContainerView.frame.height / 2.0)