macOS控件的鼠标悬停(Hover)操作实现

2022-01-20  本文已影响0人  jifu

查看苹果官方开发文档会发现NSView有两个方法鼠标进入mouseEntered(with:)和鼠标退出mouseExited(with:)两个方法;
虽然文档上说:子类覆写这两个方法可以接受到对应的事件;但实际上还需要override updateTrackingAreas方法并且更新对应的trackingAreas

具体实现的代码如下

class MouseTrackingView: NSView {
    var isMouseInside: Bool = false {
        didSet {
            print("isMouseInside:\(isMouseInside)")
        }
    }
    
    override func mouseExited(with event: NSEvent) {
        isMouseInside = false
    }
    
    override func mouseEntered(with event: NSEvent) {
        isMouseInside = true
    }
    
    override func updateTrackingAreas() {
        trackingAreas.forEach(removeTrackingArea(_:))
        addTrackingArea(.init(rect: .zero,
                              options: [.activeAlways, .inVisibleRect, .mouseEnteredAndExited],
                              owner: self,
                              userInfo: nil))
    }
}

实现悬停的逻辑是鼠标进入时改变按钮样式或者展示气泡;鼠标退出时恢复样式或者气泡消失;
为了实现视图跟业务逻辑代码分离需要定义一个鼠标移动事件;
定义一个MouseMoveEvent的枚举类型;并且增加subscribeMouseMoveEvent的事件订阅方法便可将业务代码和UI视图的代码分离开来;具体实现如下

class MouseTrackingView: NSView {
    enum MouseMoveEvent {
        case exited, entered
    }
    
    typealias MouseMoveEventObserver = (MouseMoveEvent) -> Void
    
    private var observer: MouseMoveEventObserver?
    
    var mouseMoveEvent: MouseMoveEvent = .exited {
        didSet {
            observer?(mouseMoveEvent)
        }
    }
    
    override func mouseExited(with event: NSEvent) {
        mouseMoveEvent = .exited
    }
    
    override func mouseEntered(with event: NSEvent) {
        mouseMoveEvent = .entered
    }
    
    override func updateTrackingAreas() {
        trackingAreas.forEach(removeTrackingArea(_:))
        addTrackingArea(.init(rect: .zero,
                              options: [.activeAlways, .inVisibleRect, .mouseEnteredAndExited],
                              owner: self,
                              userInfo: nil))
    }
    
    func subscribeMouseMoveEvent(_ observer: MouseMoveEventObserver?) {
        self.observer = observer
    }
}

使用方法:

        @IBOutlet weak var mouseView: MouseTrackingView!

        mouseView.subscribeMouseMoveEvent { event in
            print("mouse event: \(event)")
            switch event {
            case .exited: break
            case .entered: break
            }
        }

到目前为止鼠标悬停的目的已经达到了;但是有一个小问题:假设项目中有需求对NSTextFieldNSViewNSButton等等控件都需要实现鼠标悬停的功能;那按照上面的方法就需要子类化所有控件、MouseMovableTextFiledMouseMovableViewMouseMovableButton; 显然这样做会出现大量重复的模版代码;

使用协议实现鼠标悬停接口

如果可以定义一个协议MouseTrackable来实现鼠标悬停的接口;那么只要实现此协议的控件就能获得鼠标悬停的能力extension NSView: MouseTrackable {} ;代码实现如下

import AppKit

enum MouseMoveEvent {
    case exited, entered
}

// MARK: -
protocol MouseTrackable {
    func subscribeMouseMoveEvent(_ observer: @escaping (MouseMoveEvent) -> Void)
}

// MARK: -
protocol MouseTrackCompatible {
    var mouseTracker: MouseTrackable { get }
}

// MARK: -
class MouseTracker: MouseTrackable {
    typealias MouseMoveEventObserver = (MouseMoveEvent) -> Void
    private var observer: MouseMoveEventObserver?
    
    private var event: MouseMoveEvent = .exited {
        didSet {
            observer?(event)
        }
    }
    
    func subscribeMouseMoveEvent(_ observer: @escaping MouseMoveEventObserver) {
        self.observer = observer
    }
    
    func updateMouseMoveEvent(_ event: MouseMoveEvent) {
        self.event = event
    }
}

// MARK: -
extension NSView: MouseTrackCompatible  {
    static var _mouseTracker: Int = 0
    var mouseTracker: MouseTrackable {
        guard let tracker = objc_getAssociatedObject(self, &Self._mouseTracker) as? MouseTracker else {
            let tracker = MouseTracker()
            objc_setAssociatedObject(self, &Self._mouseTracker, tracker, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            exchangeUpdateTrackingAreasImplementation()
            return tracker
        }
        return tracker
    }
    
    open override func mouseExited(with event: NSEvent) {
        (mouseTracker as? MouseTracker)?.updateMouseMoveEvent(.exited)
    }
    
    open override func mouseEntered(with event: NSEvent) {
        (mouseTracker as? MouseTracker)?.updateMouseMoveEvent(.entered)
    }
   
    private func exchangeUpdateTrackingAreasImplementation() {
        let classObj = Self.self
        let origin = class_getInstanceMethod(classObj, #selector(updateTrackingAreas))!
        let new = class_getInstanceMethod(classObj, #selector(swizzle_updateTrackingAreas))!
        method_exchangeImplementations(origin, new)
    }
    
    @objc
    private func swizzle_updateTrackingAreas() {
        swizzle_updateTrackingAreas()
        trackingAreas.forEach(removeTrackingArea(_:))
        addTrackingArea(.init(rect: .zero,
                              options: [.activeAlways, .inVisibleRect, .mouseEnteredAndExited],
                              owner: self,
                              userInfo: nil))
    }
}


使用方法:

      self.label
         .mouseTracker
        .subscribeMouseMoveEvent { event in
            print("mouse event: \(event)")
            switch event {
            case .exited: break
            case .entered: break
            }
        }

使用协议实现鼠标悬停接口的好处就是不用对目标控件进行子类化;通过对NSView拓展出mouseTracker对象后便可以简单的实现鼠标移动事件的订阅;当然NSButton、NSImageView、NSTextField也会自动继承这个属性方法;

源代码:
https://github.com/jifucao/ViewHover

上一篇下一篇

猜你喜欢

热点阅读