很久没有更新文章了, 最近完成一项类似KEEP的运动软件.简单总结一下思想和实现方式.(因为原项目是swift,所以就用swift简单演示一下)
1. 架构.
- MVC-State数据驱动
-- Manager 业务管理器(数据管理,语音管理,运动轨迹管理)
-- Model 业务模型(数据didSet发出通知)
-- Module 业务模块(展示图表,运动主页,地图模块,排行模块,分享模块,设置模块,上传模块等等)
-- Router 业务流转路由
-- Lib 库文件
1. 实现.
SportTrackingManager.swift运动管理器通过高德定位AMapLocationManager回调方法绘制轨迹,判断GPS信号,计算运动数据.通过CMPedometer计步器校准步数,通过CMAltimeter气压计校准海拔数据,通过CMMotionActivityManager活动对象判断用户行为.
/// 定位管理
public lazy var locationManager = AMapLocationManager()
/// 计步类
private var pedometer: CMPedometer?
/// 气压计
private var altimeter: CMAltimeter?
/// 活动器对象
public var motionActivityManager: CMMotionActivityManager?
public var motionActivitystatus: CMMotionActivity?
GPS 信号策略
1.定位管理返回的精度策略
i. 如果水平精度或垂直精度均小于0 或者 水平精度或垂直精度均大于等于100 --> 认为已经失去了GPS信号
ii. 如果水平精度和垂直精度均小于等于10 --> 认为GPS信号非常的好
iii. 如果水平精度小于60 --> 认为GPS信号一般
iiii.其他情况下 --> 认为GPS信号很差
2.定位返回位置的间隔时间判断
如果当前位置点相对等待了 超过了5s --> 认为GPS信号很差
如果当前位置点相对等待了 超过了10s --> 认为GPS信号失去
@objc private func gpsBad() {
if self.gpsStatus != .Bad {
self.gpsStatus = .Bad
}
}
@objc private func gpsDisconnected() {
if self.gpsStatus != .Disconnected {
self.gpsStatus = .Disconnected
}
}
@objc private func gpsDistanceFilter() {
self.locationManager.distanceFilter = kCLDistanceFilterNone
}
// MARK:- GPS信号强弱时间判断
private func gpsPerformRequests () {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(gpsBad), object: nil)
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(gpsDisconnected), object: nil)
self.perform(#selector(gpsBad), with: nil, afterDelay: 5)
self.perform(#selector(gpsDisconnected), with: nil, afterDelay: 10)
}
// MARK:- GPS信号强弱精度判断
public func gpsStatusWithCLLocation(location: CLLocation) -> SportTrackingGPSStatus {
if location.horizontalAccuracy < 0 || location.verticalAccuracy < 0 || location.horizontalAccuracy >= SportConfig.disconnectLimitAccuracy || location.verticalAccuracy >= SportConfig.disconnectLimitAccuracy {
return .Disconnected
} else if location.horizontalAccuracy <= SportConfig.bestLimitAccuracy {
return .Good
} else if location.horizontalAccuracy < SportConfig.normalLimitAccuracy {
return .Normal
} else {
return .Bad
}
}
实时定位打点轨迹策略
- 定位点时间戳与当前时间戳差值 > 1s 认为不是当前的点位 去除
- GPS信号丢失 去除
- 上一次获取点位时间戳与当前点位时间戳差值 <= 0.1s 认为在原地不动 去除
- 上一次获取点位位置与当前点位位置差值 <= 0.1m 认为原地不动 去除
- GPS获取当前点位置的速度 = 0 去除
- 自动检测当前运动类型是驾车和静止 去除
- 根据运动类型判断速度策略
行走 --最小速度 minLimitSpeed = 0.3 m/s
--最大速度 max(当前计算速度, gps返回的当前速度) > 5.5 m/s -> // 估算人类竞走最快纪录 5.5 m/s
跑步 --最小速度 minLimitSpeed = 0.5 m/s
--最大速度 max(当前计算速度, gps返回的当前速度) > 100 / 9.58 m/s -> // 目前最新百米赛跑的世界纪录是9.58 秒
骑行 --最小速度 minLimitSpeed = 0.8 m/s
--最大速度 max(当前计算速度, gps返回的当前速度) > 56 m/s -> // 公路自行车世界纪录 202km/h = 56 m/s- 当gps信号很差的时候过滤:
max(当前计算速度, gps返回的当前速度) > 300米后计算出的平均速度 * 2 如果当前计算出来的的速度跳变为当前平均速度的2倍 去除- 手动和自动暂停 去除
- 经过卡尔曼滤波处理 优化轨迹平滑度
- 三角滤波去噪抽稀
- 道格拉斯算法压缩轨迹
extension SportTrackingManager : AMapLocationManagerDelegate{
func amapLocationManager(_ manager: AMapLocationManager!, didFailWithError error: Error!) {
//kCLErrorDenied
manager.startUpdatingLocation()
}
// MARK:- 连续定位方法
func amapLocationManager(_ manager: AMapLocationManager!, didUpdate location: CLLocation!, reGeocode: AMapLocationReGeocode!) {
if self.status == .Finish || self.status == .Cancel {return}
guard let location = location else { return }
guard let currentSportTracking = self.currentSportTracking else {return}
if self.locationManager.distanceFilter == kCLDistanceFilterNone {
self.locationManager.distanceFilter = 5
}
if currentSportTracking.subType == SportTrackingSubType.indoorRun.rawValue {return}
if currentSportTracking.subType != SportTrackingSubType.ride.rawValue && CMPedometer.isStepCountingAvailable() && currentSportTracking.coreMotionDistance == 0 {
return
}
// GPS 信号 - 时间判断
if currentSportTracking.subType != SportTrackingSubType.ride.rawValue {
self.gpsPerformRequests()
}
self.gpsStatus = gpsStatusWithCLLocation(location: location)
let howRecent = location.timestamp.timeIntervalSinceNow
if fabs(howRecent) >= 1 {return}
var mapLocation = location
// 去噪抽稀
if currentSportTracking.lineArray.count >= 2 {
var pre = currentSportTracking.lineArray[currentSportTracking.lineArray.count - 2]
var cur = currentSportTracking.lineArray[currentSportTracking.lineArray.count - 1]
let prev = MAMapPointForCoordinate(CLLocationCoordinate2DMake(pre.coordinate.latitude, pre.coordinate.longitude))
let curP = MAMapPointForCoordinate(CLLocationCoordinate2DMake(cur.coordinate.latitude,cur.coordinate.longitude))
let nextP = MAMapPointForCoordinate(CLLocationCoordinate2DMake(location.coordinate.latitude,location.coordinate.longitude))
let threshHold = calculateDistance(pt: curP, begin: prev, end: nextP)
if threshHold > 20 { // 垂直距离超过一定阈值认为是噪点去除
#if DEBUG
self.showCentralToast("过滤漂移点")
#endif
return
}
if threshHold > SportConfig.threshHold { // 垂直距离在某些范围内,利用三角重心重新拉回点
let curLatitude = (pre.coordinate.latitude + cur.coordinate.latitude + location.coordinate.latitude) / 3
let curLongitude = (pre.coordinate.longitude + cur.coordinate.longitude + location.coordinate.longitude) / 3
cur = CLLocation(latitude: curLatitude, longitude: curLongitude)
currentSportTracking.lineArray[currentSportTracking.lineArray.count - 1] = cur
}
mapLocation = cur
currentSportTracking.lineArray.append(location)
} else {
currentSportTracking.lineArray.append(location)
return
}
// 上一次位置
self.lastLocation = self.curLocation
// 用户最新位置
self.curLocation = mapLocation
// 上一次没有位置
guard self.lastLocation != nil else {return}
guard self.curLocation != nil else {return}
if #available(iOS 9.0, *) {
// 卡尔曼滤波处理
self.curLocation = hcKalmanFilter(location: self.curLocation!)
}
// GPS 信号信号丢失丢弃定位点
if location.horizontalAccuracy >= SportConfig.disconnectLimitAccuracy {return}
// 2个时间差
var delta = self.curLocation!.timestamp.timeIntervalSince(self.lastLocation!.timestamp)
// 2个距离差
var distance = self.curLocation!.distance(from: self.lastLocation!)
if (distance.isNaN || distance <= 0.1) {
return
}
if (delta.isNaN || delta <= 0.1) {
return
}
if self.curLocation!.speed == 0.0 {return}
if self.curLocation!.horizontalAccuracy > SportConfig.bestLimitAccuracy * 2 {
// 处理线段添加前速度策略
if handleTrack(isHandle: false, curSpeed: distance / delta) == false {return}
// 运动状态控制
if self.status == .Sporting {
if currentSportTracking.locationsArray.count > 0 {
if lastFlag == false {
self.lastLocation = currentSportTracking.locationsArray.last?.endLocation ?? CLLocation(coordinate: CLLocationCoordinate2D(latitude: currentSportTracking.locationsArray.last?.latitude ?? 0, longitude: currentSportTracking.locationsArray.last?.longitude ?? 0), altitude: currentSportTracking.locationsArray.last?.altitude ?? 0, horizontalAccuracy: currentSportTracking.locationsArray.last?.accuracy ?? 10, verticalAccuracy: currentSportTracking.locationsArray.last?.verticalAccuracy ?? 10, timestamp: Date(timeIntervalSince1970: TimeInterval(currentSportTracking.locationsArray.last?.timestamp ?? 0)))
} else {
lastFlag = false // 线段接续上一次的点位
}
}
} else {
lastFlag = true // 线段中断
return
}
}
// 总距离计算
distanceTmp = distanceTmp + self.curLocation!.distance(from: self.lastLocation!)
delta = self.curLocation!.timestamp.timeIntervalSince(self.lastLocation!.timestamp)
distance = self.curLocation!.distance(from: self.lastLocation!)
let speed = (distance / delta).roundTo(places: 2) > minLimitSpeed ? (distance / delta).roundTo(places: 2) : minLimitSpeed
if self.status == .Paused {return}
currentSportTracking.totalDistance = (distanceTmp / 1000.0).roundTo(places: 2)
// 将点位添加入数组
let line = SportTrackingLine()
line.sportId = currentSportTracking.id
line.beginLocation = self.lastLocation
line.endLocation = self.curLocation
line.distance = currentSportTracking.totalDistance * 1000
line.relativetime = currentSportTracking.totalTime
line.speed = currentSportTracking.avgSpeed_m_s > minLimitSpeed ? currentSportTracking.avgSpeed_m_s : minLimitSpeed
line.mileage = Int(distanceTmp)
if CMPedometer.isStepCountingAvailable() {
line.relativeAltitude = self.relativeAltitude
line.pressure = self.pressure
}
// 当前速度计算
currentSportTracking.curSpeed_m_s = speed > minLimitSpeed ? speed : line.speed
currentSportTracking.curSpeed = String.stringifyAvgPaceFromDist(meters: currentSportTracking.curSpeed_m_s, seconds: 1)
// 判断是否本次是否异常
if currentSportTracking.locationsArray.count > 10 && self.curLocation!.horizontalAccuracy <= SportConfig.bestLimitAccuracy && self.lastLocation!.horizontalAccuracy <= SportConfig.bestLimitAccuracy {
if CMMotionActivityManager.isActivityAvailable() {
if (self.motionActivitystatus?.automotive ?? false) {
handleTrack(isHandle: true,curSpeed: speed)
}
} else {
handleTrack(isHandle: true,curSpeed: speed)
}
}
// 发出产生线段通知
let notiName = SportNotification.Notification.SportTrackingLineBeGenerated.rawValue
NotificationCenter.default.post(name: NSNotification.Name(rawValue: notiName), object: self, userInfo: [notiName:self.curLocation!])
let realm = SportDataManager.realm()
try? realm.write {
currentSportTracking.locationsArray.append(line)
}
}
}
// MARK:- 数据处理
extension SportTrackingManager {
// MARK:- 卡尔曼滤波
private func hcKalmanFilter(location: CLLocation) -> (CLLocation) {
if hcKalmanFilter == nil {
self.hcKalmanFilter = HCKalmanAlgorithm(initialLocation: location)
} else {
if let hcKalmanFilter = self.hcKalmanFilter {
if resetKalmanFilter == true {
hcKalmanFilter.resetKalman(newStartLocation: location)
resetKalmanFilter = false
} else {
return hcKalmanFilter.processState(currentLocation: location)
}
}
}
return location
}
// MARK:- 计算当前点到线的垂线距离
private func calculateDistance(pt: MAMapPoint,begin: MAMapPoint, end: MAMapPoint) -> Double {
var mappedPoint: MAMapPoint = MAMapPoint(x: 0, y: 0)
var dx = begin.x - end.x
var dy = begin.y - end.y
if(fabs(dx) < 0.00000001 && fabs(dy) < 0.00000001 ) {
mappedPoint = begin
} else {
var u = (pt.x - begin.x)*(begin.x - end.x) + (pt.y - begin.y)*(begin.y - end.y)
u = u/((dx*dx)+(dy*dy))
mappedPoint.x = begin.x + u*dx
mappedPoint.y = begin.y + u*dy
}
return MAMetersBetweenMapPoints(pt, mappedPoint)
}
}
语音播报使用AVFoundation的AVQueuePlayer先读取本地语音文件
/// 初始化
private override init(){
super.init()
guard let voicePath = SportAssets.hostBundle.url(forResource: "voice.bundle/voice.json", withExtension: nil),
let data = try? Data(contentsOf: voicePath) else{
fatalError("`JSON File Fetch Failed`")
}
// JSON序列化
guard let json = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers),
let dict = json as? [String: Any] else{
fatalError("`JSON Data Serialize Failed`")
}
try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: .duckOthers)
try? AVAudioSession.sharedInstance().setActive(true)
voiceDict = dict
}
/// 语音播放
@objc private func speakText(text: String) {
let manager = SportTrackingManager.shareInstance
if canSpeak() { return } //控制是否静音
guard let player = player else {return}
let itemStringArray = text.components(separatedBy: ",")
guard let folderPath = SportAssets.hostBundle.path(forResource: "voice.bundle", ofType: nil) else {return}
for itemString in itemStringArray {
guard voiceDict.keys.contains(itemString) else {return}
let itemPath = "\(folderPath)/\(voiceDict[itemString]!)"
if voiceDict[itemString] == nil || FileManager.default.fileExists(atPath: itemPath) == false {
break
}
let itemURL = URL(fileURLWithPath: itemPath)
let item = AVPlayerItem(url: itemURL)
if player.items().count == 0 {
player.replaceCurrentItem(with: item)
} else {
if player.canInsert(item, after: player.items().last) {
player.insert(item, after: player.items().last)
}
}
}
if isCanSpeak || (manager.status == .Finish ) || (isAutoComplete) {
player.play()
}
}
数据存储使用Realm
override static func primaryKey() -> String? {
return "primaryKey"
}
@objc dynamic var primaryKey: String?
/// 运动id
@objc dynamic var id:Int64 = -1
/// 用户id
@objc dynamic var userId: Int64 = 0
/// 状态(运动开始与结束)
@objc dynamic var status = SportTrackingSaveStatus.Start.rawValue
/// 状态(上传成功与失败: 0: 失败,1: 成功)
@objc dynamic var upstatus = 0
/// 轨迹是否异常(0: 异常,1: 正常)
@objc dynamic var trackStatus = 1
/// 进程杀死后是否需要恢复运动(0: 不需要,1: 需要)
@objc dynamic var killStatus = 0
/// 是否是目标跑(0: 不是,1: 是)
@objc dynamic var targetRun = 0
/// 目标类型(0: 距离, 1: 时间, 2: 卡路里, 3:速度 )
@objc dynamic var targetRunType = 0
/// 线段集合
var locationsArray = List<SportTrackingLine>()
/// 全程的平均速度
var avgSpeed_m_s_array = List<Double>()
/// 1公里数组
var SportUnitSpeedArray = List<SportUnitSpeedRecord>()
/// 步频数组
var sportStepRecords = List<SportCoreMotionModel>()
/// 线段开始时间,1970开始
@objc dynamic var startTime: Int64 = 0 {
didSet{
primaryKey = "\(startTime)"
}
}
/// 线段截止时间
@objc dynamic var endTime: Int64 = 0
/// 运动类型
@objc dynamic var type: String = SportTrackingType.Walk.rawValue
/// 运动子类型
@objc dynamic var subType: String = SportTrackingSubType.outdoorRun.rawValue
/// 轨迹总距离 单位(公里)
@objc dynamic var totalDistance: Double = 0.00 {
didSet{
if totalDistance < oldValue {
totalDistance = oldValue
}
if totalDistance > oldValue {
calculate(totalDistance: totalDistance)
}
}
}
/// 最高海拔
@objc dynamic var altitudeMax = 0
/// 最低海拔
@objc dynamic var altitudeMin = 0
/// 卡路里
@objc dynamic var calorie = 0 {
didSet{
if calorie < oldValue {
calorie = oldValue
}
if calorie != oldValue {
//时间发生变化时发出通知
let notiName = SportNotification.Notification.SportTrackingCalorieChanged.rawValue
NotificationCenter.default.post(name: NSNotification.Name(rawValue: notiName), object: self, userInfo: [notiName:calorie])
}
}
}
/// 轨迹的key
@objc dynamic var trace: String?
/// 步频的key
@objc dynamic var stepKey: String?
/// 轨迹图片的key
@objc dynamic var traceImg: String?
/// 步数
@objc dynamic var numberOfSteps = 0
/// 步速
@objc dynamic var currentPace = 0.0
/// 上楼
@objc dynamic var floorsAscended = 0.0
/// 下楼
@objc dynamic var floorsDescended = 0.0
/// 传感器的距离
@objc dynamic var coreMotionDistance = 0.0
/// 步频限制参数
@objc dynamic var currentCadenceLimit = 0.0
/// 总共时长 s/秒
@objc dynamic var totalTime: Double = 0.0
/// 总共时长字符串
@objc dynamic var totalTimeString: String {
if self.totalTime == 0.0 {
return "00:00:00"
}
return String.formatTime(time: self.totalTime)
}
/// 平均速度(字符串)分/公里
@objc dynamic var avgSpeed: String = "0'00\""
/// 当前格式化配速
@objc dynamic var curSpeed: String = "0'00\""
/// 平均速度 m/s
@objc dynamic var avgSpeed_m_s: Double = 0.0
/// 当前速度 m/s
@objc dynamic var curSpeed_m_s: Double = 0.0
@objc dynamic var maxSpeed: Double = 0.0
@objc dynamic var minSpeed: Double = 0.0
/// 1公里内最快速度
@objc dynamic var maxSpeed_1: String {
if SportUnitSpeedArray.count == 0 {return "0'00\""}
let str = String.stringifyAvgPaceFromDist(meters:SportUnitSpeedArray.map{$0.speed}.max() ?? 0.0,seconds:1)
return str
}
/// 1公里内最慢速度
@objc dynamic var minSpeed_1: String {
if SportUnitSpeedArray.count == 0 {return "0'00\""}
let str = String.stringifyAvgPaceFromDist(meters:SportUnitSpeedArray.filter{ $0.speed != 0.0}.map{$0.speed}.min() ?? 0.0,seconds:1)
return str
}
///目标:跑量
@objc dynamic var targetMileage: Int = 0
///目标:用时
@objc dynamic var targetCastTime: Int64 = 0
///目标:热量
@objc dynamic var targetCalorie: Int = 0
///目标:速度
@objc dynamic var targetSpeed: Double = 0.0
///五公里时间
@objc dynamic var fiveKmTime: Int64 = 0
///十公里时间
@objc dynamic var tenKmTime: Int64 = 0
///半马时间
@objc dynamic var halfMarathonTime: Int64 = 0 //21KM
///全马时间
@objc dynamic var marathonTime: Int64 = 0 //42KM
///目标:数值字符串
@objc dynamic var targetValueStr: String = ""
地图MAMapView: 自定义脉冲圈, 根据速度渐变色绘制线段.
public lazy var mapView: MAMapView = {
let mapView = MAMapView(frame:self.bounds)
AMapServices.shared().enableHTTPS = true
mapView.showsUserLocation = true
mapView.showsCompass = false
mapView.delegate = self
mapView.setZoomLevel(16, animated: false)
mapView.maxZoomLevel = 19
mapView.userTrackingMode = .follow
mapView.customizeUserLocationAccuracyCircleRepresentation = true
mapView.showsScale = false
mapView.isRotateEnabled = false
mapView.isRotateCameraEnabled = false
// 将高德地图标记去除
for subview in mapView.subviews {
if subview is UIImageView {
subview.layer.contents = UIImage().cgImage
}
}
return mapView
} ()
public func addSportTrackLines(currentSportTracking: SportTracking?) {
guard let curSportTracking = currentSportTracking else { return }
if curSportTracking.locationsArray.count == 0 {return}
for line in curSportTracking.locationsArray {
addPolyline(line: line,currentSportTracking: curSportTracking)
}
if isPlayBack {
showOverlays()
let pointAnnotationfirst = MAPointAnnotation()
pointAnnotationfirst.title = "起点"
let endCoordinatefirst = CLLocationCoordinate2D(latitude: curSportTracking.locationsArray.first!.preLatitude, longitude: curSportTracking.locationsArray.first!.preLongitude)
pointAnnotationfirst.coordinate = endCoordinatefirst
self.mapView.addAnnotation(pointAnnotationfirst)
if curSportTracking.locationsArray.count > 1 {
let pointAnnotationlast = MAPointAnnotation()
pointAnnotationlast.title = "终点"
let endCoordinatelast = CLLocationCoordinate2D(latitude: curSportTracking.locationsArray.last!.latitude, longitude: curSportTracking.locationsArray.last!.longitude)
pointAnnotationlast.coordinate = endCoordinatelast
self.mapView.addAnnotation(pointAnnotationlast)
}
if endCoordinates.count > 0 {
pointArray = endCoordinates.map { (endCoordinate) -> CGPoint in
let point = mapView.convert(endCoordinate, toPointTo: self)
return point
}
}
} else {
let endCoordinatelast = CLLocationCoordinate2D(latitude: curSportTracking.locationsArray.last!.latitude, longitude: curSportTracking.locationsArray.last!.longitude)
self.mapView.showOverlays(self.mapView.overlays, edgePadding: UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100), animated: true)
}
}
// MARK:- 添加线段
public func addPolyline(line: SportTrackingLine,currentSportTracking: SportTracking) {
if currentSportTracking.locationsArray.count == 0 {return}
let beginCoordinate = CLLocationCoordinate2D(latitude: line.preLatitude, longitude: line.preLongitude)
let endCoordinate = CLLocationCoordinate2D(latitude: line.latitude, longitude: line.longitude)
if beginCoordinate.latitude == 0.0 || beginCoordinate.longitude == 0.0 {return}
if endCoordinate.latitude == 0.0 || endCoordinate.longitude == 0.0 {return}
if isBeAddedOrigin == false {
let pointAnnotation = MAPointAnnotation()
pointAnnotation.title = "起点"
pointAnnotation.coordinate = beginCoordinate
self.mapView.addAnnotation(pointAnnotation)
isBeAddedOrigin = true
}
var polylineCoords: [CLLocationCoordinate2D] = [beginCoordinate,endCoordinate]
let polyline = SportColorPolyline(coordinates: &polylineCoords, count: 2, drawStyleIndexes: [0,1])
//如果回放
if isPlayBack {
polyline?.color = colorPolyline(curSpeed: line.speed,
midSpeed: currentSportTracking.avgSpeed_m_s,
maxSpeed: currentSportTracking.maxSpeed,
minSpeed: currentSportTracking.minSpeed)
lastColor = polyline?.color?.last ?? SportColor.normalColor
polyline?.color = [SportColor.normalColor]
endCoordinates.append(endCoordinate)
}
else {
polyline?.color = [SportColor.normalColor]
}
self.mapView.add(polyline)
}
public func showOverlays(){
self.mapView.showOverlays(self.mapView.overlays, edgePadding: UIEdgeInsets(top: 80, left: 50, bottom: self.frame.height * 0.5, right: 50), animated: false)
}
private func stringTimeFor(date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter.string(from: date)
}
// MARK:- 渐变色
private func colorPolyline(curSpeed: Double,midSpeed: Double, maxSpeed: Double, minSpeed: Double)-> [UIColor] {
// 慢的
let s_red = 243/255.0
let s_green = 82/255.0
let s_blue = 82/255.0
// 不快不慢
let m_red = 36/255.0
let m_green = 199/255.0
let m_blue = 137/255.0
// 快的
let f_red = 44/255.0
let f_green = 226/255.0
let f_blue = 255/255.0
if curSpeed == 0.0 || midSpeed == 0.0 || maxSpeed == 0.0 {
return [lastColor,SportColor.normalColor]
}
if curSpeed <= minSpeed {
if lastColor == SportColor.fastColor {
return [lastColor,SportColor.normalColor]
}
return [lastColor,SportColor.slowColor]
}
if curSpeed >= maxSpeed {
if lastColor == SportColor.slowColor {
return [lastColor,SportColor.normalColor]
}
return [lastColor,SportColor.fastColor]
}
if curSpeed < midSpeed {
if midSpeed == minSpeed {return [lastColor,SportColor.normalColor]}
let ratio = (curSpeed - minSpeed) / (midSpeed - minSpeed)
let red = s_red + ratio * (m_red - s_red);
let green = s_green + ratio * (m_green - s_green);
let blue = s_blue + ratio * (m_blue - s_blue);
let curColor = UIColor(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: 1.0)
if lastColor == SportColor.fastColor {
return [lastColor,SportColor.normalColor]
}
return [lastColor,curColor]
} else {
if maxSpeed == midSpeed {return [lastColor,SportColor.normalColor]}
let ratio = (curSpeed - midSpeed) / (maxSpeed - midSpeed);
let red = m_red + ratio * (f_red - m_red);
let green = m_green + ratio * (f_green - m_green);
let blue = m_blue + ratio * (f_blue - m_blue);
let curColor = UIColor(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: 1.0)
if lastColor == SportColor.slowColor {
return [lastColor,SportColor.normalColor]
}
return [lastColor,curColor]
}
}
}
extension SportMapView: MAMapViewDelegate {
func mapView(_ mapView: MAMapView!, didUpdate userLocation: MAUserLocation!, updatingLocation: Bool) {
if !updatingLocation && userAnnotationView != nil && !isPlayBack {
UIView.animate(withDuration: 0.1, animations: {
self.userAnnotationView?.rotateDegree = CGFloat(userLocation.heading.trueHeading) - mapView.rotationDegree
})
}
}
func mapView(_ mapView: MAMapView!, rendererFor overlay: MAOverlay!) -> MAOverlayRenderer! {
if overlay.isKind(of: SportColorPolyline.self) {
let renderer: MAMultiColoredPolylineRenderer = MAMultiColoredPolylineRenderer(overlay: overlay)
let polyline = overlay as! SportColorPolyline
renderer.strokeColors = polyline.color ?? [SportColor.normalColor]
renderer.lineJoinType = kMALineJoinRound
renderer.lineCapType = kMALineCapRound
renderer.isGradient = true
renderer.lineWidth = 7.0
return renderer
}
return nil
}
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
if annotation.isKind(of: MAUserLocation.self) {
var userAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: SportMapView.userpointReuseIndetifier) as? RadialCircleAnnotationView
if userAnnotationView == nil {
userAnnotationView = RadialCircleAnnotationView(annotation: annotation, reuseIdentifier: SportMapView.userpointReuseIndetifier)
}
userAnnotationView?.canShowCallout = true
//脉冲圈个数
userAnnotationView?.pulseCount = 1
//单个脉冲圈动画时长
userAnnotationView?.animationDuration = 2.0
//单个脉冲圈缩放比例
userAnnotationView?.scale = 2.0
//单个脉冲圈fillColor
userAnnotationView?.fillColor = UIColor.white
//单个脉冲圈strokeColor
userAnnotationView?.strokeColor = UIColor.white
//更改设置后重新开始动画
userAnnotationView?.startPulseAnimation()
self.userAnnotationView = userAnnotationView
return userAnnotationView
}
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: SportMapView.startpointReuseIndetifier) as? MAPinAnnotationView
if annotationView == nil {
annotationView = MAPinAnnotationView(annotation: annotation, reuseIdentifier: SportMapView.startpointReuseIndetifier)
}
//如果回放
if isPlayBack {
if annotation.title == "起点"{
annotationView?.image = SportAssets.bundleImage(named: "map_annotation_start_playback")
} else if annotation.title == "终点" {
annotationView?.image = SportAssets.bundleImage(named: "map_annotation_end_playback")
}
} else {
annotationView?.image = SportAssets.bundleImage(named: "map_annotation_start")
}
annotationView?.canShowCallout = true
annotationView?.animatesDrop = true
// 设置图片偏移量
annotationView?.centerOffset = CGPoint(x: 0, y: -(annotationView?.image.size.height ?? 0) * 0.5 + 5)
return annotationView
}
}
总结: 时间紧迫,源码没有上传. 如果有问题,请联系我,欢迎指正.