1. 程式人生 > >iOS 百度地圖路線繪製與小車平滑移動

iOS 百度地圖路線繪製與小車平滑移動

專案中用到了百度地圖,記錄一下,為以後開發提供幫助

紋理繪製和分段顏色繪製

我們在利用百度地圖計算出路線的點後可以在地圖上繪製出自己想要的紋理路線或者分段顏色顯示,通過自定義或者利用百度給出的類直接繪製。

我們在計算出路線之後就需要進行路線的繪製了,我們在路線的回撥方法中進行設定

#pragma mark - 計算車輛路線結果回撥
- (void)onGetDrivingRouteResult:(BMKRouteSearch *)searcher result:(BMKDrivingRouteResult *)result errorCode:(BMKSearchErrorCode
)
error

當獲取到路線的點時,根據百度地圖demo,我們可以在此方法中進行路線的自定義設定,可以是紋理圖片,可以是分段顏色繪製,也可以是單一顏色,都可以。

     // 通過points構建BMKPolyline
     //構建分段顏色索引陣列
     NSArray *colorIndexs = [NSArray arrayWithObjects:
     [NSNumber numberWithInt:0],nil];
     //構建BMKPolyline,使用分段顏色索引,其對應的BMKPolylineView必須設定colors屬性
     myPolyline = [BMKPolyline polylineWithPoints:temppoints count: planPointCounts textureIndex:colorIndexs];       
      [_mapView addOverlay:myPolyline];

路線分段顏色繪製

#pragma mark - 路線分段顏色繪製
//根據overlay生成對應的View
- (BMKOverlayView *)mapView:(BMKMapView *)mapView viewForOverlay:(id <BMKOverlay>)overlay
{    
    if ([overlay isKindOfClass:[BMKPolyline class]])
    {
        BMKPolylineView* polylineView = [[BMKPolylineView alloc] initWithOverlay:overlay];
        if
(overlay == colorfulPolyline) { polylineView.lineWidth = 5; /// 使用分段顏色繪製時,必須設定(內容必須為UIColor) polylineView.colors = [NSArray arrayWithObjects: [[UIColor alloc] initWithRed:0 green:1 blue:0 alpha:1], [[UIColor alloc] initWithRed:1 green:0 blue:0 alpha:1], [[UIColor alloc] initWithRed:1 green:1 blue:0 alpha:0.5], nil]; } else { polylineView.strokeColor = [[UIColor blueColor] colorWithAlphaComponent:1]; polylineView.lineWidth = 20.0; [polylineView loadStrokeTextureImage:[UIImage imageNamed:@"arrowTexture"]]; } return polylineView; } return nil; }

路線紋理繪製

#pragma mark - 路線紋理繪製

//根據overlay生成對應的View
-(BMKOverlayView *)mapView:(BMKMapView *)mapView viewForOverlay:(id<BMKOverlay>)overlay{

    if ([overlay isKindOfClass:[BMKPolyline class]])
    {
        BMKPolylineView* polylineView = [[BMKPolylineView alloc] initWithOverlay:overlay];

        polylineView.lineWidth = 8;
        polylineView.isFocus = YES;
        polylineView.tileTexture = YES;

        // 載入分段紋理繪製 
        UIImage *image = [UIImage imageNamed:@"lv"];
        [polylineView loadStrokeTextureImages:@[image]];

        return polylineView;
    }

    return nil;
}

【紋理圖片】

  • (百度demo中獲取)—-> 圖片寬高必須為2的冪次方才可以正確的在專案中使用。

  • 這裡寫圖片描述

  • 這裡寫圖片描述

//////////////////////////////////////////////////////////////////////////////////

華麗的分割線

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

下面介紹一下小車的平滑移動,實現類似滴滴打車,小車移動的效果。

這裡寫圖片描述

步驟1
我們需要先自定義Annotation類,為小車的平滑移動儲存方向和Id資料,自定義大頭針,方便設定小車方向的旋轉 ,兩個自定義的類都是為了之後的平滑移動提供幫助。

// 自定義DBMKPointAnnotation用來儲存id和angle
@interface DBMKPointAnnotation : BMKPointAnnotation
/** ID  */
@property(strong,nonatomic)NSString *ID;
/** angle  */
@property(strong,nonatomic)NSString *Angle;
@end
// 自定義BMKAnnotationView,用於顯示小車
@interface SportAnnotationView1 : BMKAnnotationView

@property (nonatomic, strong) UIImageView *imageView;

@end

@implementation SportAnnotationView1

@synthesize imageView = _imageView;

- (id)initWithAnnotation:(id<BMKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
    if (self) {
        //設定小車顯示大小
        [self setBounds:CGRectMake(0.f, 0.f, 40.f, 40.f)];
        _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.f, 0.f, 40.f, 40.f)];
        _imageView.image = [UIImage imageNamed:@"小車圖片"];
        _imageView.contentMode = UIViewContentModeScaleAspectFit;
        _imageView.userInteractionEnabled = YES;
        [self addSubview:_imageView];
    }
    return self;
}

@end

步驟2
實時的獲取車輛的經緯度,方向,資料,進行展示.
百度給我們的方法非常簡單,就是利用UIView動畫,對大頭針進行一個經緯度及反向轉向的設定。下面是程式碼 —>

   [UIView animateWithDuration:0.5 animations:^{
        //轉向                                                                         sportAnnotationView.imageView.transform=CGAffineTransformMakeRotation(angle);
} completion:^(BOOL finished) {
    //平移
    [UIView animateWithDuration:5.5 animations:^{
        annotation.coordinate = CLLocationCoordinate2DMake([dic[@"Lat"] floatValue], [dic[@"Lng"] floatValue]);
    }];
}];

至此小車的平滑移動,我們就可以簡單的處理好了。但是,這樣處理的話,會有一個bug,就是對地圖進行縮放時,小車對跑出地圖外,這個問題是怎麼引起的呢?我們應該怎麼解決呢?

引起這個問題的原因是運用的UIView動畫,實際修改了屬性的真實值,進行動畫,UIView動畫是一個過程動畫,在縮放時會改變動畫過程,導致平滑移動出路線外bug。這個問題就涉及到了UIView動畫與核心動畫的區別問題,在這個問題的處理上,我們用KeyAnimation動畫進行處理(解決方案是根據百度官方人員回覆處理的)。

這裡建立一個顯示當前位置的圖層

#import <BaiduMapAPI_Map/BMKMapComponent.h>
#import <BaiduMapAPI_Utils/BMKUtilsComponent.h>

@interface CACoordLayer : CALayer

@property (nonatomic, assign) BMKMapView * mapView;

//定義一個BMKAnnotation物件
@property (nonatomic, strong) BMKPointAnnotation *annotation;

@property (nonatomic) double mapx;

@property (nonatomic) double mapy;

@property (nonatomic) CGPoint centerOffset;

@end
//
//  CACoordLayer.m
//

#import "CACoordLayer.h"

@implementation CACoordLayer

@dynamic mapx;
@dynamic mapy;

- (id)initWithLayer:(id)layer
{
    if ((self = [super initWithLayer:layer]))
    {
        if ([layer isKindOfClass:[CACoordLayer class]])
        {
            CACoordLayer * input = layer;
            self.mapx = input.mapx;
            self.mapy = input.mapy;
            [self setNeedsDisplay];
        }
    }
    return self;
}

+ (BOOL)needsDisplayForKey:(NSString *)key
{

    if ([@"mapx" isEqualToString:key])
    {
        return YES;
    }
    if ([@"mapy" isEqualToString:key])
    {
        return YES;
    }

    return [super needsDisplayForKey:key];
}

- (void)display
{

    CACoordLayer * layer = [self presentationLayer];
    BMKMapPoint mappoint = BMKMapPointMake(layer.mapx, layer.mapy);
    //根據得到的座標值,將其設定為annotation的經緯度
    self.annotation.coordinate = BMKCoordinateForMapPoint(mappoint);
    //設定layer的位置,顯示動畫
    CGPoint center = [self.mapView convertCoordinate:BMKCoordinateForMapPoint(mappoint) toPointToView:self.mapView];
    self.position = center;
}

@end

CACoordLayer類的關鍵程式碼needsDisplayForKey方法和圖層重繪方法display,就是當mapx,mapy改變時會呼叫圖層的重繪功能,為動畫做鋪墊。為大頭針提供動畫的能力。

根據建立好的顯示當前位置的圖層進行移動,下面主要說下關鍵程式碼,在自定義的大頭針中設定使用的圖層為我們剛才建立的圖層CACoordLayer,當我們獲取到車輛經緯度,方向等資訊傳給大頭針時,我們需要的操作為

- (void)addTrackingAnimationForPoints:(NSArray *)points duration:(CFTimeInterval)duration
{
    if (![points count])
    {
        return;
    }

    CACoordLayer * mylayer = ((CACoordLayer *)self.layer);

    //preparing
    NSUInteger num = 2*[points count] + 1;
    NSMutableArray * xvalues = [NSMutableArray arrayWithCapacity:num];
    NSMutableArray *yvalues = [NSMutableArray arrayWithCapacity:num];

    NSMutableArray * times = [NSMutableArray arrayWithCapacity:num];

    double sumOfDistance = 0.f;
    //dis儲存的為地址
    double * dis = malloc(([points count]) * sizeof(double));
//    NSLog(@"%p",dis);


    //the first point is set by the destination of last animation.
    BMKMapPoint preLoc;
    if (!([self.animationList count] > 0 || isAnimatingX || isAnimatingY))
    {
        lastDestination = BMKMapPointMake(mylayer.mapx, mylayer.mapy);
    }
    preLoc = lastDestination;

    [xvalues addObject:@(preLoc.x)];
    [yvalues addObject:@(preLoc.y)];
    [times addObject:@(0.f)];

    //set the animation points.
    for (int i = 0; i<[points count]; i++)
    {
        TracingPoint * tp = points[i];

        //position
        BMKMapPoint p = BMKMapPointForCoordinate(tp.coordinate);
        [xvalues addObjectsFromArray:@[@(p.x), @(p.x)]];//stop for turn
        [yvalues addObjectsFromArray:@[@(p.y), @(p.y)]];

        //distance
        dis[i] = BMKMetersBetweenMapPoints(p, preLoc);
        sumOfDistance = sumOfDistance + dis[i];
        dis[i] = sumOfDistance;

        //record pre
        preLoc = p;
    }

    //set the animation times.
    double preTime = 0.f;
    double turnDuration = TurnAnimationDuration/duration;
    for (int i = 0; i<[points count]; i++)
    {
        double turnEnd = dis[i]/sumOfDistance;
        double turnStart = (preTime > turnEnd - turnDuration) ? (turnEnd + preTime) * 0.5 : turnEnd - turnDuration;

        [times addObjectsFromArray:@[@(turnStart), @(turnEnd)]];

        preTime = turnEnd;
    }

    //record the destination.
    TracingPoint * last = [points lastObject];
    lastDestination = BMKMapPointForCoordinate(last.coordinate);

    free(dis);

    // add animation.
    CAKeyframeAnimation *xanimation = [CAKeyframeAnimation animationWithKeyPath:MapXAnimationKey];
    xanimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
    xanimation.values   = xvalues;
    xanimation.keyTimes = times;
    xanimation.duration = duration;
    xanimation.delegate = self;
    xanimation.fillMode = kCAFillModeForwards;

    CAKeyframeAnimation *yanimation = [CAKeyframeAnimation animationWithKeyPath:MapYAnimationKey];
    yanimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
    yanimation.values   = yvalues;
    yanimation.keyTimes = times;
    yanimation.duration = duration;
    yanimation.delegate = self;
    yanimation.fillMode = kCAFillModeForwards;

    [self pushBackAnimation:xanimation];
    [self pushBackAnimation:yanimation];

    mylayer.mapView = [self mapView];
}
#pragma mark - Animation Delegate

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    if ([anim isKindOfClass:[CAKeyframeAnimation class]])
    {
        CAKeyframeAnimation * keyAnim = ((CAKeyframeAnimation *)anim);
        if ([keyAnim.keyPath isEqualToString:MapXAnimationKey])
        {
            isAnimatingX = NO;

            CACoordLayer * mylayer = ((CACoordLayer *)self.layer);
            mylayer.mapx = ((NSNumber *)[keyAnim.values lastObject]).doubleValue;
            currDestination.x = mylayer.mapx;

            [self updateAnnotationCoordinate];
            [self popFrontAnimationForKey:MapXAnimationKey];
        }
        else if ([keyAnim.keyPath isEqualToString:MapYAnimationKey])
        {
            isAnimatingY = NO;

            CACoordLayer * mylayer = ((CACoordLayer *)self.layer);
            mylayer.mapy = ((NSNumber *)[keyAnim.values lastObject]).doubleValue;
            currDestination.y = mylayer.mapy;

            [self updateAnnotationCoordinate];
            [self popFrontAnimationForKey:MapYAnimationKey];
        }
        animateCompleteTimes++;
        if (animateCompleteTimes % 2 == 0) {
            if (_animateDelegate && [_animateDelegate respondsToSelector:@selector(movingAnnotationViewAnimationFinished)]) {
                [_animateDelegate movingAnnotationViewAnimationFinished];
            }
        }
    }
}

這個方法所做的就是將經緯度座標轉換為投影后的直角地理座標,然後進行CAKeyframeAnimation動畫,移動的屬性是自定義CACoordLayer中的mapx,mapy,而不是UIView動畫中的經緯度,我們都知道核心動畫都是假象,並不是真正的移動,所以在移動完之後,重新為mapx賦值,更新大頭針位置,避免出現小車移動的起始位置錯亂的問題,簡易的流程應該是這樣的

Created with Raphaël 2.1.0KeyFrame動畫對mapx屬性動畫needsDisplayForKey中設定了當前指定的屬性key改變是否需要重新繪製,所以產生了動畫當動畫結束後賦值改變mapx,mapy,的值,保證下次動畫起始位置的正確性yes

//////////////////////////////////////////////////////////////////////////////////

華麗的分割線

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

內邊距的設定

我們的專案還要實現,當路線繪製出來後,需要顯示這個路線,並且需要有內邊距,當我呼叫這個介面時

   [_mapView setVisibleMapRect:polyLine.boundingMapRect edgePadding:UIEdgeInsetsMake(20, 20, 280, 20) animated:YES];

地圖縮放的顯示範圍並不是我設定的內邊距,有誤差,類似於這種
這裡寫圖片描述

通過與百度地圖技術人員的詢問,得知在調取polyLine.boundingMapRect 不能夠獲取到數值,技術人員給出了另一個方法獲取到polyline的MapRect,然後設定了內邊距

//根據polyline設定地圖範圍
- (void)mapViewFitPolyLine:(BMKPolyline *) polyLine {
    CGFloat ltX, ltY, rbX, rbY;
    if (polyLine.pointCount < 1) {
        return;
    }
    BMKMapPoint pt = polyLine.points[0];
    ltX = pt.x, ltY = pt.y;
    rbX = pt.x, rbY = pt.y;
    for (int i = 1; i < polyLine.pointCount; i++) {
        BMKMapPoint pt = polyLine.points[i];
        if (pt.x < ltX) {
            ltX = pt.x;
        }
        if (pt.x > rbX) {
            rbX = pt.x;
        }
        if (pt.y < ltY) {
            ltY = pt.y;
        }
        if (pt.y > rbY) {
            rbY = pt.y;
        }
    }
    BMKMapRect rect;
    rect.origin = BMKMapPointMake(ltX , ltY);
    rect.size = BMKMapSizeMake(rbX - ltX, rbY - ltY);
    [_mapView setVisibleMapRect:rect edgePadding:UIEdgeInsetsMake(0, 0, 100, 0) animated:YES];
    _mapView.zoomLevel = _mapView.zoomLevel - 0.3;
}

而且根據百度技術人員的說法,這個方法也會有誤差,是因為SDK設定了邊距的級別,目前暫不能根據邊距的大小進行精確設定

不過要按照專案要求實現,如果有更好的方法,可以告訴我,謝謝。

如有錯誤的地方❌,請指正✅。。。

第一次寫部落格,玻璃心求輕噴,希望自己能堅持下去,當做一種知識總結也能分享給一些新手朋友們.