优步预订动画

今年 6 月,我在圣何塞加入 WWDC,那是我在使用 Uber 在库比蒂诺图书馆与朋友见面时看到的等待动画。

动画令人敬畏!

我很好奇他们是如何做到的,所以我做了一些探索。

Preview

好吧,折线动画仍然不够流畅。有谁有更好的主意吗?

一步步

旋转地图

由于以下两个原因,我选择 Map 作为要演示的默认地图:

  • WWDC19 之后,Simulator 在 MapKit 包括的所有 Apple 框架下的 Meta,Core 层之上构建,Metal 利用底层 Mac 系统的 GPU
  • Google Map 需要开发人员密钥才能访问,这意味着从 GitHub 分叉存储库后,您无法直接在模拟器上运行它

 可以通过嵌入在UIView.animate块中来调整地图交易

1
2
3
4
5
6
7
8
UIView.animate(withDuration: 1, delay: 0, options: .curveEaseInOut, animations: {  [weak self] in
self?.mapView.setCamera(MKMapCamera(lookingAtCenter: center, fromDistance: fromDistance, pitch: pitch, heading: 0), animated: true)
}) { b in
self.pinAnimation()
UIView.animate(withDuration: 180, delay: 0, options: [.curveLinear, .autoreverse], animations: { [weak self] in
self?.mapView.setCamera(MKMapCamera(lookingAtCenter: center, fromDistance: fromDistance, pitch: pitch, heading: heading), animated: true)
}, completion: nil)
}

Google Map 必须使用 CATransaction

1
2
3
4
CATransaction.begin()
CATransaction.setValue(NSNumber(float: 1.0), forKey: kCATransactionAnimationDuration)
// change the camera, set the zoom, whatever.
CATransaction.commit()

固定动画

我们需要两个CAShapeLayer来制作动画,一个用于圆圈,另一个用于图钉。
请注意,圆达到最大值后会消失,并且销钉具有 easyIn 曲线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//   Pin Animation
extension MainViewController {

func setUpLayers() {

circleLayer.lineWidth = 1.5
circleLayer.strokeColor = UIColor.white.cgColor
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.path = UIBezierPath(ovalIn: circleView.bounds).cgPath

circleLayer.shadowColor = UIColor.white.cgColor

pinLayer.fillColor = UIColor.white.cgColor
pinLayer.path = UIBezierPath(roundedRect: pinView.bounds, cornerRadius: 1).cgPath
pinLayer.opacity = 0.9
}

func pinAnimation() {
setUpLayers()

circleView.layer.addSublayer(circleLayer)

self.circleView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
self.circleView.alpha = 1

UIView.animate(withDuration: 2, delay: 0, options: [.repeat], animations: { [weak self] in
self?.circleView.transform = CGAffineTransform(scaleX: 1, y: 1)
self?.circleView.alpha = 0

}, completion: nil)


pinView.layer.addSublayer(pinLayer)
UIView.animate(withDuration: 1, delay: 0, options: [.curveEaseIn, .repeat, .autoreverse], animations: { [weak self] in

self?.pinView.transform = CGAffineTransform(translationX: 0, y: -4)
self?.pinView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)


}, completion: nil)
}
}

在地图上绘制折线

 地图使用MKMapViewDelegate处理绘图。

1
2
3
4
5
6
7
8
9
10
11

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let overlay = overlay as? MKPolyline {
let polyline = MKPolylineRenderer(overlay: overlay)
polyline.strokeColor = .white
polyline.lineWidth = 1.5
return polyline
}

return MKOverlayRenderer(overlay: overlay)
}

这是添加折线的方法

1
2
let currentSegment = MKPolyline(coordinates: subCoordinates, count: subCoordinates.count)
self.mapView.addOverlay(currentSegment)

动画折线

那是棘手的部分。

当我们看到 Uber 的动画足够接近时,折线会随着地图旋转,从而在地图上绘制。

我要做的是继续在地图上添加和删除折线,以假装它是动画。
同样,当出现在图钉的开头和结尾处时,该行具有标题和尾部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func animate(route: [CLLocationCoordinate2D], duration: TimeInterval, completion: (() -> Void)? = nil) {
guard route.count > 0 else { return }
var currentStep = 0
let delta = 25, opt = 3.0
let totalSteps = route.count + delta
let stepDrawDur = duration / TimeInterval(totalSteps) * opt
var prePolyline: MKPolyline?

drawingTimer = Timer.scheduledTimer(withTimeInterval: stepDrawDur, repeats: true) { [weak self] timer in
defer { completion?() }
guard let self = self else {
timer.invalidate()
return
}

if let previous = prePolyline {
self.mapView.removeOverlay(previous)
prePolyline = nil
}

if currentStep > totalSteps {
timer.invalidate()
return
}

let start = currentStep-delta < 0 ? 0 : currentStep-delta
let end = currentStep > route.count ? route.count : currentStep

let subCoordinates = Array(route[start..<end])
let currentSegment = MKPolyline(coordinates: subCoordinates, count: subCoordinates.count)
self.mapView.addOverlay(currentSegment)

prePolyline = currentSegment
currentStep += Int(opt)
}
}

这里是 repository, 如果您有更好的主意,请告诉我 :)

更新资料

好吧,由于屏幕刷新率为 60 fps,因此它应该使总步数/持续时间 ≈ 60