Last active
March 10, 2022 22:31
-
-
Save johnnewman/266f921682ce799aed24765e04a10a04 to your computer and use it in GitHub Desktop.
CircleLayer transition property pulse
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CircleTransitionPulse.swift | |
// | |
// Created by John Newman on 3/10/22. | |
// Copyright © 2022 Roadtrippers LLC. All rights reserved. | |
// | |
import Foundation | |
import MapboxMaps | |
import UIKit | |
class StyleLayerPulseRenderer { | |
private let pulseLayerIdentifier = "my-pulse-layer" | |
private let pulseSourceIdentifier = "my-pulse-source" | |
private var map: MapView | |
private var layerPosition: LayerPosition | |
init(map: MapView, layerPosition: LayerPosition) { | |
self.map = map | |
self.layerPosition = layerPosition | |
} | |
func startPulseAnimation(forAnnotation annotation: PointAnnotation) { | |
guard !map.mapboxMap.style.sourceExists(withId: pulseSourceIdentifier), | |
!map.mapboxMap.style.layerExists(withId: pulseLayerIdentifier) else { | |
print("Pulse layer/source already exist in style. Aborting.") | |
return | |
} | |
var pulseSource = GeoJSONSource() | |
pulseSource.data = .feature(.init(geometry: annotation.geometry)) | |
var pulseLayer = CircleLayer(id: pulseLayerIdentifier) | |
pulseLayer.source = pulseSourceIdentifier | |
// Set up starting values | |
pulseLayer.circleStrokeColor = .constant(StyleColor(UIColor.blue)) | |
pulseLayer.circleOpacity = .constant(0) | |
pulseLayer.circleRadius = .constant(0) | |
pulseLayer.circleStrokeWidth = .constant(0) | |
pulseLayer.circleStrokeOpacity = .constant(0) | |
// Avoid any Mapbox animations except the ones we want. | |
pulseLayer.circleBlurTransition = .init(duration: 0, delay: 0) | |
pulseLayer.circleColorTransition = .init(duration: 0, delay: 0) | |
pulseLayer.circleRadiusTransition = .init(duration: 0, delay: 0) | |
pulseLayer.circleOpacityTransition = .init(duration: 0, delay: 0) | |
pulseLayer.circleTranslateTransition = .init(duration: 0, delay: 0) | |
pulseLayer.circleStrokeColorTransition = .init(duration: 0, delay: 0) | |
pulseLayer.circleStrokeWidthTransition = .init(duration: 0, delay: 0) | |
pulseLayer.circleStrokeOpacityTransition = .init(duration: 0, delay: 0) | |
try! map.mapboxMap.style.addSource(pulseSource, id: pulseSourceIdentifier) | |
try! map.mapboxMap.style.addLayer(pulseLayer, layerPosition: layerPosition) | |
newPulseIteration(pulseLayerIdentifier) | |
} | |
private func newPulseIteration(_ identifier: String) { | |
// Animate the pulse. | |
map.mapboxMap.style.animateCircleLayer(identifier) { | |
// Wait for the entire animation to finish (3 seconds). | |
DispatchQueue.main.asyncAfter(deadline: .pulseRepeatInterval) { | |
// Reset the style layer back to its starting state. | |
self.map.mapboxMap.style.resetCircleLayer(identifier) { | |
// Once reset, run another pulse after a pause. | |
self.newPulseIteration(identifier) | |
} | |
} | |
} | |
} | |
} | |
struct PulseProperties { | |
static let strokeWidth = (start: 3.0, end: 5.0) | |
static let strokeOpacity = (start: 1.0, end: 0.0) | |
static let radius = (start: 0.0, end: 62.0) | |
} | |
extension TimeInterval { | |
static var pulseRepeatInterval: Self { 3 } | |
static var pulseDuration: Self { 1.5 } | |
static var pauseBeforePulsing: Self { pulseRepeatInterval - pulseDuration } | |
} | |
extension DispatchTime { | |
// The circle-xxx-transition properties cannot be updated alongside their | |
// circle-xxx properties. If you try that, the updates don't animate. We must | |
// first update the transition properties, wait for those updates to take hold, | |
// then run the circle layer updates. | |
static var mapboxStyleUpdateDelay: Self { | |
// TODO: Is there a Mapbox callback for when the transition properties are ready? | |
.now() + 0.05 | |
} | |
static var pauseBeforePulsing: Self { | |
.now() + TimeInterval.pauseBeforePulsing | |
} | |
static var pulseRepeatInterval: Self { | |
.now() + TimeInterval.pulseRepeatInterval | |
} | |
} | |
extension StyleTransition { | |
static var noDelay: StyleTransition { | |
.init(duration: 0, delay: 0) | |
} | |
static var pulse: StyleTransition { | |
.init(duration: .pulseDuration, delay: .pauseBeforePulsing) | |
} | |
} | |
extension Style { | |
func resetCircleLayer(_ identifier: String, completion: @escaping (() -> Void)) { | |
updateCircleLayerTransitions(identifier, transition: .noDelay) { | |
self.updateCircleLayerPulseProperties(identifier, atStart: true, completion: completion) | |
} | |
} | |
func animateCircleLayer(_ identifier: String, completion: @escaping (() -> Void)) { | |
updateCircleLayerTransitions(identifier, transition: .pulse) { | |
self.updateCircleLayerPulseProperties(identifier, atStart: false, completion: completion) | |
} | |
} | |
private func updateCircleLayerTransitions(_ identifier: String, transition: StyleTransition, completion: @escaping (() -> Void)) { | |
do { | |
try updateLayer(withId: identifier, type: CircleLayer.self) { layer in | |
layer.circleRadiusTransition = transition | |
layer.circleStrokeWidthTransition = transition | |
layer.circleStrokeOpacityTransition = transition | |
} | |
DispatchQueue.main.asyncAfter(deadline: .mapboxStyleUpdateDelay) { | |
completion() | |
} | |
} catch { | |
print("Error updating circle transition properties: \(error)") | |
} | |
} | |
private func updateCircleLayerPulseProperties(_ identifier: String, atStart: Bool, completion: @escaping (() -> Void)) { | |
do { | |
try self.updateLayer(withId: identifier, type: CircleLayer.self) { layer in | |
layer.circleRadius = .constant(atStart ? PulseProperties.radius.start : PulseProperties.radius.end) | |
layer.circleStrokeWidth = .constant(atStart ? PulseProperties.strokeWidth.start : PulseProperties.strokeWidth.end) | |
layer.circleStrokeOpacity = .constant(atStart ? PulseProperties.strokeOpacity.start : PulseProperties.strokeOpacity.end) | |
} | |
completion() | |
} catch { | |
print("Error updating circle pulse properties: \(error)") | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment