带有 MapKit ios 的渐变折线

Posted

技术标签:

【中文标题】带有 MapKit ios 的渐变折线【英文标题】:Gradient Polyline with MapKit ios 【发布时间】:2011-04-15 21:48:52 【问题描述】:

我正在尝试使用覆盖 (MKOverlay) 在 MKMapView 上跟踪路线。 但是,根据当前的速度,如果颜色正在变化(例如,如果用户从 65 英里/小时到 30 英里/小时的速度从绿色变为橙色),我想在跟踪路线时使用带有渐变的 Nike 应用程序。

这是我想要的截图:

所以每 20 米,我使用以下方法添加从旧坐标到新坐标的叠加层:

// Create a c array of points. 
MKMapPoint *pointsArray = malloc(sizeof(CLLocationCoordinate2D) * 2);

// Create 2 points.
MKMapPoint startPoint = MKMapPointForCoordinate(CLLocationCoordinate2DMake(oldLatitude, oldLongitude));
MKMapPoint endPoint = MKMapPointForCoordinate(CLLocationCoordinate2DMake(newLatitude, newLongitude));

// Fill the array.
pointsArray[0] = startPoint;
pointsArray[1] = endPoint;

// Erase polyline and polyline view if not nil.
if (self.routeLine != nil)
    self.routeLine = nil;

if (self.routeLineView != nil)
    self.routeLineView = nil;

// Create the polyline based on the array of points.
self.routeLine = [MKPolyline polylineWithPoints:pointsArray count:2];

// Add overlay to map.
[self.mapView addOverlay:self.routeLine];

// clear the memory allocated earlier for the points.
free(pointsArray);

// Save old coordinates.
oldLatitude = newLatitude;
oldLongitude = newLongitude;

基本上我会添加很多小叠加层。然后我想在这个小线条图上创建渐变,所以我试图在覆盖委托中这样做:

- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay 
    MKOverlayView* overlayView = nil;

    if(overlay == self.routeLine) 
        // If we have not yet created an overlay view for this overlay, create it now. 
        if(self.routeLineView == nil) 
            self.routeLineView = [[[MKPolylineView alloc] initWithPolyline:self.routeLine] autorelease];

            if (speedMPH < 25.0) 
                self.routeLineView.fillColor = [UIColor redColor];
                self.routeLineView.strokeColor = [UIColor redColor];
            
            else if (speedMPH >= 25.0 && speedMPH < 50.0) 
                self.routeLineView.fillColor = [UIColor orangeColor];
                self.routeLineView.strokeColor = [UIColor orangeColor];
            
            else 
                self.routeLineView.fillColor = [UIColor greenColor];
                self.routeLineView.strokeColor = [UIColor greenColor];
            

            // Size of the trace.
            self.routeLineView.lineWidth = routeLineWidth;

            // Add gradient if color changed.
            if (oldColor != self.routeLineView.fillColor) 
                CAGradientLayer *gradient = [CAGradientLayer layer];
                    gradient.frame = self.routeLineView.bounds;
                    gradient.colors = [NSArray arrayWithObjects:(id)[oldColor CGColor], (id)[self.routeLineView.fillColor CGColor], nil];
                    [self.routeLineView.layer insertSublayer:gradient atIndex:0];
            

            // Record old color for gradient.
            if (speedMPH < 25.0)
                oldColor = [UIColor redColor];
            else if (speedMPH >= 25.0 && speedMPH < 50.0)
                oldColor = [UIColor orangeColor];
            else
                oldColor = [UIColor greenColor];
        

        overlayView = self.routeLineView;
    

    return overlayView;

我正在尝试以这种方式添加渐变,但我想这不是这样做的方法,因为我无法使其工作。

每当用户位置(在位置对象的委托中)有更新时,或者每 20 米有一次更新时,我也可以跟踪路线。

你能帮我解决这个问题吗,给我一些建议! 谢谢!

【问题讨论】:

routeLine 和 routeLineView 是如何声明的?上面的代码在哪个类? “不能让它工作”——会发生什么? 我只是看不到要添加到叠加视图中的渐变。 MKPolyline *routeLine;MKPolylineView *routeLineView;,然后每 20 米创建一个新的并将其添加为叠加层。在上面的 MKMapView 委托函数中,我根据条件创建视图并尝试为其添加渐变。 【参考方案1】:

我想出的一个想法是创建一个 CGPath 并在每次调用 drawMapRect 方法时用渐变对其进行描边,因为 MKPolylineViewios7 中被 MKPlolylineRenderer 替换。

我试图通过子类化MKOverlayPathRenderer 来实现这一点,但我未能挑选出单独的CGPath,然后我找到了一个名为-(void) strokePath:(CGPathRef)path inContext:(CGContextRef)context 的神秘方法,这听起来像是我需要的,但如果你不这样做,它将不会被调用当您覆盖 drawMapRect 时,不要调用 super 方法。

这就是我现在正在做的事情。

我会继续尝试,所以如果我解决了一些问题,我会回来更新答案。

=========更新============================== ===================

这就是我这几天的工作,我几乎实现了上面提到的基本思想,但是是的,我仍然无法根据特定的 mapRect 挑选出单独的 PATH,所以我只是用渐变绘制所有路径所有路径的boundingBox与当前mapRect相交的时间。拙劣的伎俩,但现在工作。

在渲染类的-(void) drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context 方法中,我这样做:

CGMutablePathRef fullPath = CGPathCreateMutable();
BOOL pathIsEmpty = YES;

//merging all the points as entire path
for (int i=0;i< polyline.pointCount;i++)
    CGPoint point = [self pointForMapPoint:polyline.points[i]];
    if (pathIsEmpty)
        CGPathMoveToPoint(fullPath, nil, point.x, point.y);
        pathIsEmpty = NO;
     else 
        CGPathAddLineToPoint(fullPath, nil, point.x, point.y);
    


//get bounding box out of entire path.
CGRect pointsRect = CGPathGetBoundingBox(fullPath);
CGRect mapRectCG = [self rectForMapRect:mapRect];
//stop any drawing logic, cuz there is no path in current rect.
if (!CGRectIntersectsRect(pointsRect, mapRectCG))return;

然后我逐点分割整个路径以单独绘制其渐变。 请注意,hues 数组包含映射每个位置速度的色调值。

for (int i=0;i< polyline.pointCount;i++)
    CGMutablePathRef path = CGPathCreateMutable();
    CGPoint point = [self pointForMapPoint:polyline.points[i]];
    ccolor = [UIColor colorWithHue:hues[i] saturation:1.0f brightness:1.0f alpha:1.0f];
    if (i==0)
        CGPathMoveToPoint(path, nil, point.x, point.y);
     else 
        CGPoint prevPoint = [self pointForMapPoint:polyline.points[i-1]];
        CGPathMoveToPoint(path, nil, prevPoint.x, prevPoint.y);
        CGPathAddLineToPoint(path, nil, point.x, point.y);
        CGFloat pc_r,pc_g,pc_b,pc_a,
        cc_r,cc_g,cc_b,cc_a;
        [pcolor getRed:&pc_r green:&pc_g blue:&pc_b alpha:&pc_a];
        [ccolor getRed:&cc_r green:&cc_g blue:&cc_b alpha:&cc_a];
        CGFloat gradientColors[8] = pc_r,pc_g,pc_b,pc_a,
                                    cc_r,cc_g,cc_b,cc_a;

        CGFloat gradientLocation[2] = 0,1;
        CGContextSaveGState(context);
        CGFloat lineWidth = CGContextConvertSizeToUserSpace(context, (CGSize)self.lineWidth,self.lineWidth).width;
        CGPathRef pathToFill = CGPathCreateCopyByStrokingPath(path, NULL, lineWidth, self.lineCap, self.lineJoin, self.miterLimit);
        CGContextAddPath(context, pathToFill);
        CGContextClip(context);//<--clip your context after you SAVE it, important!
        CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
        CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradientColors, gradientLocation, 2);
        CGColorSpaceRelease(colorSpace);
        CGPoint gradientStart = prevPoint;
        CGPoint gradientEnd = point;
        CGContextDrawLinearGradient(context, gradient, gradientStart, gradientEnd, kCGGradientDrawsAfterEndLocation);
        CGGradientRelease(gradient);
        CGContextRestoreGState(context);//<--Don't forget to restore your context.
    
    pcolor = [UIColor colorWithCGColor:ccolor.CGColor];

这就是所有的核心绘图方法,当然您需要在覆盖类中使用pointsvelocity 并使用 CLLocationManager 提供它们。

最后一点是如何从速度中得到hue的值,好吧,我发现如果从0.03~0.3范围内的色调正好代表从红色到绿色,所以我对色调和速度做了一些比例映射。

最后一个,这里是这个演示的完整源代码:https://github.com/wdanxna/GradientPolyline

如果看不到你画的线不要惊慌,我只是把地图区域放在我的位置上:)

【讨论】:

刚刚看到你的答案。我认为这正是我在这个问题中需要实现的目标:***.com/questions/20247313/…。基本上,如果我理解正确,您可以手动逐段重新创建路径,并通过每次剪切将渐变仅应用于当前添加的路径。这是最优/足够快吗? 一开始,我尝试使用一个渐变对象绘制整个路径,该对象在gradientColors 中具有我需要的所有颜色值,并且每个颜色值映射我的gradientLocation 中的一个位置值。我认为这应该可行,但不是(奇怪)。并且文件说我们不应该从当前的 MapRect 中绘制任何东西,它似乎一次绘制整个路径不是一种选择?所以我认为将它分成段是最简单的方法......谈论最佳,它在我的 iPhone 上运行正常 :) 但仍有一些需要改进的地方,比如:只绘制与当前 MapRect 相交的线。 我根据您的代码和 github 示例实现了我的东西,效果非常好。我建议的唯一更改是直接在 init 中准备路径并仅在 drawRect 中使用或仅使用 CGRect pointsR = [self rectForMapRect:polyline.boundingMapRect]; 并检查 CGRectContains,而不仅仅是交叉口。我的问题仍然没有答案,并且有赏金。您可以在上面发布您的答案,我会很高兴地接受它,它对我有很大帮助:)【参考方案2】:

我实现了一个受上述@wdanxna 解决方案启发的Swift 4 版本。有些事情发生了变化,路径已经在超类中创建。

我没有将色调存储在渲染器中,而是创建了一个 MKPolyline 的子类,它在构造函数中计算色调。然后我用渲染器中的值抓取折线。我将它映射到速度,但我想你可以将渐变映射到任何你想要的。

渐变折线

class GradientPolyline: MKPolyline 
    var hues: [CGFloat]?
    public func getHue(from index: Int) -> CGColor 
        return UIColor(hue: (hues?[index])!, saturation: 1, brightness: 1, alpha: 1).cgColor
    


extension GradientPolyline 
    convenience init(locations: [CLLocation]) 
        let coordinates = locations.map(  $0.coordinate  )
        self.init(coordinates: coordinates, count: coordinates.count)

        let V_MAX: Double = 5.0, V_MIN = 2.0, H_MAX = 0.3, H_MIN = 0.03

        hues = locations.map(
            let velocity: Double = $0.speed

            if velocity > V_MAX 
                return CGFloat(H_MAX)
            

            if V_MIN <= velocity || velocity <= V_MAX 
                return CGFloat((H_MAX + ((velocity - V_MIN) * (H_MAX - H_MIN)) / (V_MAX - V_MIN)))
            

            if velocity < V_MIN 
                return CGFloat(H_MIN)
            

            return CGFloat(velocity)
        )
    

渐变多线渲染器

class GradidentPolylineRenderer: MKPolylineRenderer 

    override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) 
        let boundingBox = self.path.boundingBox
        let mapRectCG = rect(for: mapRect)

        if(!mapRectCG.intersects(boundingBox))  return 


        var prevColor: CGColor?
        var currentColor: CGColor?

        guard let polyLine = self.polyline as? GradientPolyline else  return 

        for index in 0...self.polyline.pointCount - 1
            let point = self.point(for: self.polyline.points()[index])
            let path = CGMutablePath()


            currentColor = polyLine.getHue(from: index)

            if index == 0 
               path.move(to: point)
             else 
                let prevPoint = self.point(for: self.polyline.points()[index - 1])
                path.move(to: prevPoint)
                path.addLine(to: point)

                let colors = [prevColor!, currentColor!] as CFArray
                let baseWidth = self.lineWidth / zoomScale

                context.saveGState()
                context.addPath(path)

                let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: [0, 1])

                context.setLineWidth(baseWidth)
                context.replacePathWithStrokedPath()
                context.clip()
                context.drawLinearGradient(gradient!, start: prevPoint, end: point, options: [])
                context.restoreGState()
            
            prevColor = currentColor
        
    

如何使用

CLLocations 数组中创建一行

let runRoute = GradientPolyline(locations: locations)
self.mapView.addOverlay(runRoute)

在委托中传递 GradientPolylineRenderer

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer 
    if overlay is GradientPolyline 
        let polyLineRender = GradientMKPolylineRenderer(overlay: overlay)
        polyLineRender.lineWidth = 7
        return polyLineRender
    

结果

【讨论】:

【参考方案3】:

听起来你在画线视图中的 drawRect 缺少渐变的设置。绘图可能发生在覆盖的不同线程上。请发布代码。

【讨论】:

请看我帖子上面的代码,我更新了。我有一个函数可以每隔 20 米从 A 点添加到 B 点的叠加层,然后在地图视图委托中创建叠加视图时尝试添加渐变层。 你的逻辑好像有问题。您不能像这样向叠加层添加渐变层。相反,您必须有一种方法来构建和绘制具有不同阴影的各个部分的整个路径。 WWDC 2010 示例代码(“面包屑”)演示了这一点。您当前的方法行不通,因为您只是在第一次添加路径段作为新的子层。但这会导致层树随着时间的推移变得非常大。您需要能够在一层中绘制整个路径。 好的,谢谢@Cocoanetics,我会看看它并让你更新! 找不到示例代码,我在苹果官网等上找过,也因为没买票也登陆不了WWDC。你有什么办法可以给我这个资源?谢谢! 通过developer.apple.com/itunes/… 访问 iTunes 中的 WWDC 2010 视频站点 - 然后在 LINKS 下的右下角有一个所有示例源代码的下载链接。【参考方案4】:

gugge 的代码非常有用。 不过最好改成这样。

return CGFloat((H_MIN + ((velocity - V_MIN) * (H_MAX - H_MIN)) / (V_MAX - V_MIN)))

【讨论】:

以上是关于带有 MapKit ios 的渐变折线的主要内容,如果未能解决你的问题,请参考以下文章

iOS MapKit 通过道路连接地图上的多个点

如何向 MKPolyline 和 MKPolygon 添加描述?

将可拖动标记与折线谷歌地图绑定

在图例折线图中找不到带有标签的句柄

带有折线的文本在 Rails 中不起作用

如何在 iOS 上从 Mapbox 解码“编码折线”