前言:
这里写这篇文章是为了记录一下动画旅行路线地图从0到1的整个实现过程,心态、技术上都有一些想要分享的点,目前基本上没有文章有讲到这些,如果你正在实现类似的功能 (参照TravelBoast或者恋爱记的旅行地图模块),那么这将会帮到你
动画旅行路线技术功能点
- 绘制弧线
- 拖拽弧线生成新的途径点
- 预览视频地图进入的动画
- 车辆沿弧线进行移动
- 生成视频
绘制弧线
先去看高德的开发者文档,看有没有对应生成弧线的API,然而高德只支持折线,这里曾经都打算放弃了,后边就想着,高德不支持,那就自己通过贝塞尔来绘制弧线, 但是贝塞尔曲线封装的API最多只有三次贝塞尔曲线,那也就是四个点,问产品,最多支持两个途径点行不行,产品的回答你懂的,那只有继续查资料,功夫不负有心人,最终发现了Centripetal Catmull–Rom spline,可以通过一组点生成平滑的曲线,哈哈,弧线这就可以实现了,成功的迈出了第一步,这里贴上生成曲线的代码
/// 通过一组坐标生成曲线
/// @param pointsArray 坐标数组
/// @param granularity 点的数量
- (UIBezierPath *)smoothedPathWithPoints:(NSArray *) pointsArray andGranularity:(NSInteger)granularity {
NSMutableArray *points = [pointsArray mutableCopy];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetAllowsAntialiasing(context, YES);
CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor);
CGContextSetLineWidth(context, 0.6);
UIBezierPath *smoothedPath = [UIBezierPath bezierPath];
// Add control points to make the math make sense
[points insertObject:[points objectAtIndex:0] atIndex:0];
[points addObject:[points lastObject]];
[smoothedPath moveToPoint:POINT(0)];
for (NSUInteger index = 1; index < points.count - 2; index++) {
CGPoint p0 = POINT(index - 1);
CGPoint p1 = POINT(index);
CGPoint p2 = POINT(index + 1);
CGPoint p3 = POINT(index + 2);
// now add n points starting at p1 + dx/dy up until p2 using Catmull-Rom splines
for (int i = 1; i < granularity; i++) {
float t = (float) i * (1.0f / (float) granularity);
float tt = t * t;
float ttt = tt * t;
CGPoint pi; // intermediate point
pi.x = 0.5 * (2*p1.x+(p2.x-p0.x)*t + (2*p0.x-5*p1.x+4*p2.x-p3.x)*tt + (3*p1.x-p0.x-3*p2.x+p3.x)*ttt);
pi.y = 0.5 * (2*p1.y+(p2.y-p0.y)*t + (2*p0.y-5*p1.y+4*p2.y-p3.y)*tt + (3*p1.y-p0.y-3*p2.y+p3.y)*ttt);
[smoothedPath addLineToPoint:pi];
}
// Now add p2
[smoothedPath addLineToPoint:p2];
}
// finish by adding the last point
[smoothedPath addLineToPoint:POINT(points.count - 1)];
return smoothedPath;
}
拖拽弧线生成新的途径点
这里可以拆分为弧线拖拽以及点的插入,首先是弧线拖拽,我们可以先给地图添加移动手势,手势开始移动,判断手势的point是否在点上,取出对应的小标,进行点的位置更新,不在点上再判断是否在p0 和 p1两点构成的线段上,取出对应的小标插入新的点到数组中,生成新的途径点,如果都不在,就不做处理,这样拖拽弧线和点的生成、位置更新都可以实现了,判断在不在点上比较简单,就不赘述,这里贴上判断点point是否在p0 和 p1两点构成的线段上的代码
/**
*判断点point是否在p0 和 p1两点构成的线段上
*/
- (BOOL)xw_point:(CGPoint)point isInLineByTwoPoint:(CGPoint)p0 p1:(CGPoint)p1 {
// 先设置一个所允许的最大值,点到线段的最短距离小于该值说明点在线段上
CGFloat maxAllowOffsetLength = 40;
// 通过直线方程的两点式计算出一般式的ABC参数,具体可以自己拿起笔换算一下,很容易
CGFloat A = p1.y - p0.y;
CGFloat B = p0.x - p1.x;
CGFloat C = p1.x * p0.y - p0.x * p1.y;
// 带入点到直线的距离公式求出点到直线的距离dis
CGFloat dis = fabs((A * point.x + B * point.y + C) / sqrt(pow(A, 2) + pow(B, 2)));
// 如果该距离大于允许值说明则不在线段上
if (dis > maxAllowOffsetLength || isnan(dis)) {
NSLog(@"===================inout");
return NO;
} else {
// 否则我们要进一步判断,投影点是否在线段上,根据公式求出投影点的X坐标jiaoX
CGFloat D = (A * point.y - B * point.x);
CGFloat jiaoX = -(A * C + B *D) / (pow(B, 2) + pow(A, 2));
//判断jiaoX是否在线段上,t如果在0~1之间说明在线段上,大于1则说明不在线段且靠近端点p1,小于0则不在线段上且靠近端点p0,这里用了插值的思想
CGFloat t = (jiaoX - p0.x) / (p1.x - p0.x);
if (t > 1 || isnan(t)) {
//最小距离为到p1点的距离
dis = XWLengthOfTwoPoint(p1, point);
} else if (t < 0) {
//最小距离为到p2点的距离
dis = XWLengthOfTwoPoint(p0, point);
}
//再次判断真正的最小距离是否小于允许值,小于则该点在直线上,反之则不在
if (dis <= maxAllowOffsetLength) {
NSLog(@"===================inside");
return YES;
} else {
NSLog(@"===================inout");
return NO;
}
}
}
//这里是求两点距离公式
static inline CGFloat XWLengthOfTwoPoint(CGPoint point1, CGPoint point2) {
return sqrt(pow(point1.x - point2.x, 2) + pow(point1.y - point2.y, 2));
}
预览视频地图进入的动画
刚开始看travelBoast的地图进入动画,感觉好炫酷,这个动画要咋写,非常疑惑,再去翻了翻高德的开发文档,说不定就有收获,哈哈,意外发现,高德提供了地图的一系列动画,于是慢慢的调整参数,两行代码就搞定了,最后进入的动画效果还不错
MAMapStatus *status = [MAMapStatus statusWithCenterCoordinate:startAnnotation.coordinate zoomLevel:self.mapView.zoomLevel + 0.5 rotationDegree:0 cameraDegree:90 screenAnchor:CGPointMake(0.5, 0.5)];
[self.mapView setMapStatus:status animated:YES duration:1.0];
车辆沿弧线进行移动
这里弧线也参照Centripetal Catmull–Rom spline生成一组点进行创建,不同的是,这里需要用高德的折线进行绘制,因为预览时,用户可以进行地图的旋转、放大、缩小,所以需要绘制到地图上,所以通过高德的API生成一条线段,这里注意的是,实线会有一点点不那么曲,设置成虚线可以解决这个小问题,弧线这里画好了, 就需要开始移动车辆了,因为地图的中心点需要和车辆一起移动,所以这里就添加了一个定时器,持续的设置地图的setCenterCoordinate为下一个点的经纬度和设置车辆的经纬度就可以实现地图和车辆移动的效果了,最后我们还需要设置车辆的方向和弧线的方向保持一致,思路就是先获取下一个点和当前点的角度差,将车辆进行旋转就可以了
- (void)linkClick {
if (self.index == self.coordinates.count - 1) {
[self showMoveEndingAnimationView];
return;
}
CLLocationCoordinate2D coordinate = [self.coordinates[self.index] MACoordinateValue];
[self.annotations enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
LNAnnotation *annotation = (LNAnnotation *)obj;
/// 这里是获取用户创建的途径点,进行拖尾、载具等更新
if (fabs(annotation.coordinate.latitude - coordinate.latitude) < 0.0001 && fabs(annotation.coordinate.longitude - coordinate.longitude) < 0.0001) {
self.startAnnotationView.animatedAnnotation = annotation;
*stop = YES;
}
}];
CGPoint currentPoint = [self.mapView convertCoordinate:coordinate toPointToView:self.mapView];
CGPoint nextPoint = [self.mapView convertCoordinate:[self.coordinates[self.index + 1] MACoordinateValue] toPointToView:self.mapView];
CGFloat angle = [self angleForStartPoint:currentPoint EndPoint:nextPoint];
self.startAnnotationView.transform = CGAffineTransformMakeRotation(M_PI_2 + angle);
[self.mapView setCenterCoordinate:coordinate animated:NO];
[self.startAnnotation setCoordinate:coordinate];
self.index += 1;
}
- (CGFloat)angleForStartPoint:(CGPoint)startPoint EndPoint:(CGPoint)endPoint{
CGPoint Xpoint = CGPointMake(startPoint.x + 100, startPoint.y);
CGFloat a = endPoint.x - startPoint.x;
CGFloat b = endPoint.y - startPoint.y;
CGFloat c = Xpoint.x - startPoint.x;
CGFloat d = Xpoint.y - startPoint.y;
CGFloat rads = acos(((a*c) + (b*d)) / ((sqrt(a*a + b*b)) * (sqrt(c*c + d*d))));
if (startPoint.y>endPoint.y) {
rads = -rads;
}
return rads;
}
生成视频
最开始的方案是想要进行MapView的录屏,从而生成视频进行保存,查了好久资料都没有找到对应的方案,如果有同学知道,可以留言告诉我一下,最后没办法,选择了开启定时器,每秒截屏30帧,然后通过AVAssetWriter进行写成视频,最后保存到相册,这里不想自己写,可以参考Glimpse,封装了一下,可以拿来直接用,但是内存消耗会特别大,10s视频,会到1G往上走,低端机型就直接崩溃了,解决方案也很简单,再截图的时候,对图片进行压缩,内存消耗会减少很多,代码如下, 那么到这里视频的生成就可以实现了
- (UIImage *)imageFromView:(UIView *)view {
UIGraphicsBeginImageContextWithOptions(view.frame.size , YES , 0 );
if ([view respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {
[view drawViewHierarchyInRect:view.bounds afterScreenUpdates:NO];
} else {
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
}
UIImage *rasterizedView = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return [UIImage imageWithData:UIImageJPEGRepresentation(rasterizedView, 0.8)];
}
上面就是动画旅行路线地图主体的实现思路,当然,还有一些小细节实现没有讲到,但是相信大家都可以解决,回顾从开始准备放弃,到后来一步一步的往下走,最后实现功能,还是比较有成就感的,其实这次让我学习到,遇到困难,不要害怕,可以将困难进行拆分,一步一步的去克服它,就没那么难了,再就是不要轻言放弃,老话说的好,世上无难事,只怕有心人