美文网首页
macOS控件的鼠标悬停(Hover)操作实现

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

作者: jifu | 来源:发表于2022-01-20 20:54 被阅读0次

    查看苹果官方开发文档会发现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

    相关文章

      网友评论

          本文标题:macOS控件的鼠标悬停(Hover)操作实现

          本文链接:https://www.haomeiwen.com/subject/jdxshrtx.html