How to implement NSTrackingArea's mouseEntered/Exited with animation?
Asked Answered
C

2

10

I want to implement a feature that when an user hovers over the specific area, the new view appears with drawer-like animation. And also, when the user leaves the specific area, the drawer should go away with animation. This is exactly what you see when you hover over the bottom of the screen in OS X, where the Dock appears and disappears with animation.

However, if I implement the feature with animation, it does not work properly when you re-enter the specific area before the animation in the mouseExited: is completed. Here's my code:

let trackingArea = NSTrackingArea(rect: CGRectMake(0, 0, 120, 300), options: NSTrackingAreaOptions.ActiveAlways | NSTrackingAreaOptions.MouseEnteredAndExited, owner: self, userInfo: nil)
underView.addTrackingArea(trackingArea) // underView is the dummy view just to respond to the mouse tracking, since the drawerView's frame is changed during the animation; not sure if this is the clean way...

override func mouseEntered(theEvent: NSEvent) {
    let frameAfterVisible = CGRectMake(0, 0, 120, 300)
    NSAnimationContext.runAnimationGroup({
        (context: NSAnimationContext!) in
            context.duration = 0.6
            self.drawerView.animator().frame = frameAfterVisible
        }, completionHandler: { () -> Void in
    })
}

override func mouseExited(theEvent: NSEvent) {
    let frameAfterInvisible = CGRectMake(-120, 0, 120, 300)
    NSAnimationContext.runAnimationGroup({
        (context: NSAnimationContext!) in
            context.duration = 0.6
            self.drawerView.animator().frame = frameAfterInvisible
        }, completionHandler: { () -> Void in
    })
}

// drawerView's frame upon launch is (-120, 0, 120, 300), since it is not visible at first

In this code, I animate the drawerView by altering its x position. However, as I stated, when you enter the tracking area and then leave the tracking area, the drawer works correctly. But that is not the case if you re-enter the tracking area before the leave-off animation is fully completed.

Of course if I set the animation duration shorter, such as 0.1, this would rarely occur. But I want to move the view with animation.

What I want to do is make the drawerView start to appear again even if the view has not completed disappearing. Is there any practice to do it?

Colorless answered 28/2, 2015 at 3:11 Comment(1)
Is your mouseEntered() function being called when you re-enter while the animation is in progress? That is, is the problem that it's not being called or is it that the setting of the new frame is not effective? Put some logging in both functions, before the animation group, at the end of the animation group, and in the completion handler.Valence
F
13

I have a solution that is very similar to your code. What I do different is is that I install the NSTrackingArea not on the view that contains the drawer view, but on the drawer view itself.

This obviously means that the drawer needs to 'stick out' a little bit. In my case the drawer is a small bit visible when it is down because I put an image view in it. If you don't want that then I suggest you just leave the visible area of the drawer empty and translucent.

Here is my implementation:

private enum DrawerPosition {
    case Up, Down
}

private let DrawerHeightWhenDown: CGFloat = 16
private let DrawerAnimationDuration: NSTimeInterval = 0.75

class ViewController: NSViewController {

    @IBOutlet weak var drawerView: NSImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Remove the auto-constraints for the image view otherwise we are not able to change its position
        view.removeConstraints(view.constraints)
        drawerView.frame = frameForDrawerAtPosition(.Down)

        let trackingArea = NSTrackingArea(rect: drawerView.bounds,
            options: NSTrackingAreaOptions.ActiveInKeyWindow|NSTrackingAreaOptions.MouseEnteredAndExited,
                owner: self, userInfo: nil)
        drawerView.addTrackingArea(trackingArea)
    }

    private func frameForDrawerAtPosition(position: DrawerPosition) -> NSRect {
        var frame = drawerView.frame
        switch position {
        case .Up:
            frame.origin.y = 0
            break
        case .Down:
            frame.origin.y = (-frame.size.height) + DrawerHeightWhenDown
            break
        }
        return frame
    }

    override func mouseEntered(event: NSEvent) {
        NSAnimationContext.runAnimationGroup({ (context: NSAnimationContext!) in
            context.duration = DrawerAnimationDuration
            self.drawerView.animator().frame = self.frameForDrawerAtPosition(.Up)
        }, completionHandler: nil)
    }

    override func mouseExited(theEvent: NSEvent) {
        NSAnimationContext.runAnimationGroup({ (context: NSAnimationContext!) in
            context.duration = DrawerAnimationDuration
            self.drawerView.animator().frame = self.frameForDrawerAtPosition(.Down)
        }, completionHandler: nil)
    }
}

Full project at https://github.com/st3fan/StackOverflow-28777670-TrackingArea

Let me know if this was useful. Happy to make changes.

Flagging answered 9/3, 2015 at 19:14 Comment(1)
Be aware, as from Swift 2.0 you have to follow another syntax for the tracking area options. let trackingArea = NSTrackingArea(rect: self.bounds, options: [NSTrackingAreaOptions.MouseEnteredAndExited, NSTrackingAreaOptions.ActiveAlways], owner: self, userInfo: nil)Herren
S
10

Starting Swift 3. You need to do it like this:

let trackingArea = NSTrackingArea(rect: CGRectMake(0, 0, 120, 300), options: [NSTrackingAreaOptions.ActiveAlways ,NSTrackingAreaOptions.MouseEnteredAndExited], owner: self, userInfo: nil)
view.addTrackingArea(trackingArea)

Credits @marc above!

Saddler answered 1/7, 2016 at 3:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.