MKMapView不断监视航向 [英] MKMapView constantly monitor heading

查看:65
本文介绍了MKMapView不断监视航向的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在MKMapView顶部的图层中渲染某些内容.除了旋转以外,整个过程都很好用.当用户旋转地图时,我需要能够旋转我在自己的图层中渲染的内容.

I'm rendering some content in a layer that sits on top of my MKMapView. The whole thing works great with the exception of rotation. When a user rotates the map I need to be able to rotate what I'm rendering in my own layer.

我找到的标准答案是:

NSLog(@"heading: %f", self.mapView.camera.heading");

此问题在于,标题变量的内容仅在捏合/旋转手势结束时更新,而不在手势期间更新.我需要更频繁的更新.

The issue with this is that the content of the heading variable only updates when the pinch/rotate gesture is ending, not during the gesture. I need much more frequent updates.

mapView 本身没有标题属性.

There is no heading property on the mapView itself.

我认为也许像这样使用 KVO :

I thought maybe using KVO like such:

    // Somewhere in setup
    [self.mapView.camera addObserver:self forKeyPath:@"heading" options:NSKeyValueObservingOptionNew context:NULL];


    // KVO Callback
    -(void)observeValueForKeyPath:(NSString *)keyPath
                         ofObject:(id)object
                           change:(NSDictionary *)change
                          context:(void *)context{

        if([keyPath isEqualToString:@"heading"]){
            // New value
        }
    }

但是 KVO 监听器从不触发,这不足为奇.

However the KVO listener never fires which isn't surprising.

有没有我可以忽略的方法?

Is there a method that I'm overlooking?

推荐答案

似乎确实没有办法跟踪在旋转地图时简单读取当前标题的情况.由于我刚刚实现了随地图旋转的罗盘视图,因此我想与您分享我的知识.

There seem to exist indeed no way to track the simply read the current heading while rotation the map. Since I just implemented a compass view that rotates with the map, I want to share my knowledge with you.

我明确邀请您完善此答案.由于我有最后期限,所以现在就很满意(在此之前,仅在地图停止旋转的那一刻才设置了指南针),但仍有改进和微调的空间.

I explicitly invite you to refine this answer. Since I have a deadline, I'm satisfied as it is now (before that, the compass was only set in the moment the map stopped to rotate) but there is room for improvement and finetuning.

我在此处上传了一个示例项目: MapRotation示例项目

I uploaded a sample project here: MapRotation Sample Project

好的,让我们开始吧.由于我假设您现在都使用情节提要,因此将一些手势识别器拖到地图上. (那些不确定地知道如何将这些步骤转换成书面形式的人.)

Okay, let's start. Since I assume you all use Storyboards nowadays, drag a few gesture recognizers to the map. (Those who don't surely knows how to convert these steps into written lines.)

要检测地图旋转,缩放和3D角度,我们需要旋转,平移和捏合手势识别器.

To detect map rotation, zoom and 3D angle we need a rotation, a pan and a pinch gesture recognizer.

禁用旋转手势识别器的延迟触摸结束" ...

Disable "Delays touches ended" for the Rotation Gesture Recognizer...

...,并将手势"识别器的触摸"增加到2.

... and increase "Touches" to 2 for the Pan Gesture Recognizer.

将这3个的委托设置为包含的视图控制器.

Set the delegate of these 3 to the containing view controller.

将所有3种手势识别器都拖动到参考视图集合"到MapView,然后选择"gestureRecognizers"

Drag for all 3 gesture recognizers the Referencing Outlet Collections to the MapView and select "gestureRecognizers"

现在按Ctrl键将旋转手势识别器作为Outlet拖到实现中,如下所示:

Now Ctrl-drag the rotation gesture recognizer to the implementation as Outlet like this:

@IBOutlet var rotationGestureRecognizer: UIRotationGestureRecognizer!

,所有3个识别器都称为IBAction:

and all 3 recognizers as IBAction:

@IBAction func handleRotation(sender: UIRotationGestureRecognizer) {
    ...
}

@IBAction func handleSwipe(sender: UIPanGestureRecognizer) {
    ...
}

@IBAction func pinchGestureRecognizer(sender: UIPinchGestureRecognizer) {
    ...
}

是的,我将平移手势命名为"handleSwype".解释如下. :)

Yes, I named the pan gesture "handleSwype". It's explained below. :)

下面列出了控制器的完整代码,当然,该代码也必须实现MKMapViewDelegate协议. 我试图在评论中非常详细.

Listed below the complete code for the controller that of course also has to implement the MKMapViewDelegate protocol. I tried to be very detailed in the comments.

// compassView is the container View,
// arrowImageView is the arrow which will be rotated
@IBOutlet weak var compassView: UIView!
var arrowImageView = UIImageView(image: UIImage(named: "Compass")!)

override func viewDidLoad() {
    super.viewDidLoad()
    compassView.addSubview(arrowImageView)
}

// ******************************************************************************************
//                                                                                          *
// Helper: Detect when the MapView changes                                                  *

private func mapViewRegionDidChangeFromUserInteraction() -> Bool {
    let view = mapView!.subviews[0]
    // Look through gesture recognizers to determine whether this region
    // change is from user interaction
    if let gestureRecognizers = view.gestureRecognizers {
        for recognizer in gestureRecognizers {
            if( recognizer.state == UIGestureRecognizerState.Began ||
                recognizer.state == UIGestureRecognizerState.Ended ) {
                return true
            }
        }
    }
    return false
}
//                                                                                          *
// ******************************************************************************************



// ******************************************************************************************
//                                                                                          *
// Helper: Needed to be allowed to recognize gestures simultaneously to the MapView ones.   *

func gestureRecognizer(_: UIGestureRecognizer,
    shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
        return true
}
//                                                                                          *
// ******************************************************************************************



// ******************************************************************************************
//                                                                                          *
// Helper: Use CADisplayLink to fire a selector at screen refreshes to sync with each       *
// frame of MapKit's animation

private var displayLink : CADisplayLink!

func setUpDisplayLink() {
    displayLink = CADisplayLink(target: self, selector: "refreshCompassHeading:")
    displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
}
//                                                                                          *
// ******************************************************************************************





// ******************************************************************************************
//                                                                                          *
// Detect if the user starts to interact with the map...                                    *

private var mapChangedFromUserInteraction = false

func mapView(mapView: MKMapView, regionWillChangeAnimated animated: Bool) {

    mapChangedFromUserInteraction = mapViewRegionDidChangeFromUserInteraction()

    if (mapChangedFromUserInteraction) {

        // Map interaction. Set up a CADisplayLink.
        setUpDisplayLink()
    }
}
//                                                                                          *
// ******************************************************************************************
//                                                                                          *
// ... and when he stops.                                                                   *

func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {

    if mapChangedFromUserInteraction {

        // Final transform.
        // If all calculations would be correct, then this shouldn't be needed do nothing.
        // However, if something went wrong, with this final transformation the compass
        // always points to the right direction after the interaction is finished.
        // Making it a 500 ms animation provides elasticity und prevents hard transitions.

        UIView.animateWithDuration(0.5, animations: {
            self.arrowImageView.transform =
                CGAffineTransformMakeRotation(CGFloat(M_PI * -mapView.camera.heading) / 180.0)
        })



        // You may want this here to work on a better rotate out equation. :)

        let stoptime = NSDate.timeIntervalSinceReferenceDate()
        print("Needed time to rotate out:", stoptime - startRotateOut, "with velocity",
            remainingVelocityAfterUserInteractionEnded, ".")
        print("Velocity decrease per sec:", (Double(remainingVelocityAfterUserInteractionEnded)
            / (stoptime - startRotateOut)))



        // Clean up for the next rotation.

        remainingVelocityAfterUserInteractionEnded = 0
        initialMapGestureModeIsRotation = nil
        if let _ = displayLink {
            displayLink.invalidate()
        }
    }
}
//                                                                                          *
// ******************************************************************************************





// ******************************************************************************************
//                                                                                          *
// This is our main function. The display link calls it once every display frame.           *

// The moment the user let go of the map.
var startRotateOut = NSTimeInterval(0)

// After that, if there is still momentum left, the velocity is > 0.
// The velocity of the rotation gesture in radians per second.
private var remainingVelocityAfterUserInteractionEnded = CGFloat(0)

// We need some values from the last frame
private var prevHeading = CLLocationDirection()
private var prevRotationInRadian = CGFloat(0)
private var prevTime = NSTimeInterval(0)

// The momentum gets slower ower time
private var currentlyRemainingVelocity = CGFloat(0)

func refreshCompassHeading(sender: AnyObject) {

    // If the gesture mode is not determinated or user is adjusting pitch
    // we do obviously nothing here. :)
    if initialMapGestureModeIsRotation == nil || !initialMapGestureModeIsRotation! {
        return
    }


    let rotationInRadian : CGFloat

    if remainingVelocityAfterUserInteractionEnded == 0 {

        // This is the normal case, when the map is beeing rotated.
        rotationInRadian = rotationGestureRecognizer.rotation

    } else {

        // velocity is > 0 or < 0.
        // This is the case when the user ended the gesture and there is
        // still some momentum left.

        let currentTime = NSDate.timeIntervalSinceReferenceDate()
        let deltaTime = currentTime - prevTime

        // Calculate new remaining velocity here.
        // This is only very empiric and leaves room for improvement.
        // For instance I noticed that in the middle of the translation
        // the needle rotates a bid faster than the map.
        let SLOW_DOWN_FACTOR : CGFloat = 1.87
        let elapsedTime = currentTime - startRotateOut

        // Mathematicians, the next line is for you to play.
        currentlyRemainingVelocity -=
            currentlyRemainingVelocity * CGFloat(elapsedTime)/SLOW_DOWN_FACTOR


        let rotationInRadianSinceLastFrame =
        currentlyRemainingVelocity * CGFloat(deltaTime)
        rotationInRadian = prevRotationInRadian + rotationInRadianSinceLastFrame

        // Remember for the next frame.
        prevRotationInRadian = rotationInRadian
        prevTime = currentTime
    }

    // Convert radian to degree and get our long-desired new heading.
    let rotationInDegrees = Double(rotationInRadian * (180 / CGFloat(M_PI)))
    let newHeading = -mapView!.camera.heading + rotationInDegrees

    // No real difference? No expensive transform then.
    let difference = abs(newHeading - prevHeading)
    if difference < 0.001 {
        return
    }

    // Finally rotate the compass.
    arrowImageView.transform =
        CGAffineTransformMakeRotation(CGFloat(M_PI * newHeading) / 180.0)

    // Remember for the next frame.
    prevHeading = newHeading
}
//                                                                                          *
// ******************************************************************************************



// As soon as this optional is set the initial mode is determined.
// If it's true than the map is in rotation mode,
// if false, the map is in 3D position adjust mode.

private var initialMapGestureModeIsRotation : Bool?



// ******************************************************************************************
//                                                                                          *
// UIRotationGestureRecognizer                                                              *

@IBAction func handleRotation(sender: UIRotationGestureRecognizer) {

    if (initialMapGestureModeIsRotation == nil) {
        initialMapGestureModeIsRotation = true
    } else if !initialMapGestureModeIsRotation! {
        // User is not in rotation mode.
        return
    }


    if sender.state == .Ended {
        if sender.velocity != 0 {

            // Velocity left after ending rotation gesture. Decelerate from remaining
            // momentum. This block is only called once.
            remainingVelocityAfterUserInteractionEnded = sender.velocity
            currentlyRemainingVelocity = remainingVelocityAfterUserInteractionEnded
            startRotateOut = NSDate.timeIntervalSinceReferenceDate()
            prevTime = startRotateOut
            prevRotationInRadian = rotationGestureRecognizer.rotation
        }
    }
}
//                                                                                          *
// ******************************************************************************************
//                                                                                          *
// Yes, there is also a SwypeGestureRecognizer, but the length for being recognized as      *
// is far too long. Recognizing a 2 finger swype up or down with a PanGestureRecognizer
// yields better results.

@IBAction func handleSwipe(sender: UIPanGestureRecognizer) {

    // After a certain altitude is reached, there is no pitch possible.
    // In this case the 3D perspective change does not work and the rotation is initialized.
    // Play with this one.
    let MAX_PITCH_ALTITUDE : Double = 100000

    // Play with this one for best results detecting a swype. The 3D perspective change is
    // recognized quite quickly, thats the reason a swype recognizer here is of no use.
    let SWYPE_SENSITIVITY : CGFloat = 0.5 // play with this one

    if let _ = initialMapGestureModeIsRotation {
        // Gesture mode is already determined.
        // Swypes don't care us anymore.
        return
    }

    if mapView?.camera.altitude > MAX_PITCH_ALTITUDE {
        // Altitude is too high to adjust pitch.
        return
    }


    let panned = sender.translationInView(mapView)

    if fabs(panned.y) > SWYPE_SENSITIVITY {
        // Initial swype up or down.
        // Map gesture is most likely a 3D perspective correction.
        initialMapGestureModeIsRotation = false
    }
}
//                                                                                          *
// ******************************************************************************************
//                                                                                          *

@IBAction func pinchGestureRecognizer(sender: UIPinchGestureRecognizer) {
    // pinch is zoom. this always enables rotation mode.
    if (initialMapGestureModeIsRotation == nil) {
        initialMapGestureModeIsRotation = true
        // Initial pinch detected. This is normally a zoom
        // which goes in hand with a rotation.
    }
}
//                                                                                          *
// ******************************************************************************************

这篇关于MKMapView不断监视航向的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆