自定义NSSlider

2020-07-22

概述:
见名知意,本文介绍如何自定义NSSlider

NSSlider由2部分组成,外层的NSSlider以及内部的NSSliderCell。通常我们所说的自定义Slider说的就是自定义NSSliderCell

自定义NSSliderCell

以我自定定义的DNSliderCell举例

1. 定义/实现代理

NSSliderCell继承自NSActionCell,NSActionCell继承自NSCell,有关Tracking的方法都在NSCell中, 所以我们完全可以通过重写Tracking的相关方法来自定义Slider的点击、拖拽…

@objc protocol DNSliderCellDelegate {
    // 我这里定义的都是可选代理,代理者可根据需求去实现方法 
    // 开始拖动
    @objc optional func startTracking(doubleValue:Double ,sender:DNSliderCell)
    // 正在拖动
    @objc optional func continueTracking(doubleValue:Double ,sender:DNSliderCell)
    // 拖动结束 & 点击结束
    @objc optional func stopTracking(doubleValue:Double ,sender:DNSliderCell)
}

extension DNSliderCell {
    //MARK: 开始拖动
    override func startTracking(at startPoint: NSPoint, in controlView: NSView) -> Bool {
        delegate?.startTracking?(doubleValue: doubleValue, sender: self)
        return super.startTracking(at: startPoint, in: controlView)
    }
    //MARK: 拖动中
    override func continueTracking(last lastPoint: NSPoint, current currentPoint: NSPoint, in controlView: NSView) -> Bool {
        delegate?.continueTracking?(doubleValue: doubleValue, sender: self)
        return super.continueTracking(last: lastPoint, current: currentPoint, in: controlView)
    }
    //MARK: 结束拖动 & 点击结束
    override func stopTracking(last lastPoint: NSPoint, current stopPoint: NSPoint, in controlView: NSView, mouseIsUp flag: Bool) {
        delegate?.stopTracking?(doubleValue: doubleValue, sender: self)
        super.stopTracking(last: lastPoint, current: stopPoint, in: controlView, mouseIsUp: flag)
    }
}

2. 定义属性

可以根据需求自己定义常量或者变量,需要注意的是,Swift5、macOS Catalina 10.15.4上,xib创建的NSSlider会向两边各延伸2个px(其他版本是否如此不确定),例如我自己定义长度1000的slider,实际上长度为1004,所以定义了个Offset方便计算。

class DNSliderCell: NSSliderCell {
    let Offset:CGFloat = 2 // 进度条,实际上向(根据slider方向)左/右 或者 上/下 偏移2
    let progressColor = kRedHighlightColor // 进度条进度背景色
    var backgroundColor = NSColor.clear // 进度条整体背景色
    let knobColor = kRedHighlightColor // 按钮颜色
    let sliderThickness:CGFloat = 3.0 // 进度条厚度
    let sliderBarRadius:CGFloat = 0 // 进度条圆角
    let KnobWidth:CGFloat = 12.0 // 按钮宽度
    let KnobHeight:CGFloat = 12.0 // 按钮高度
    var progressRect = NSRect() // 进度的Rect
    var needControlKnobHidden = false // 是否需要控制旋钮显示隐藏
    
    weak var delegate:DNSliderCellDelegate? // 代理
    
    // slider是否是竖直
    lazy var isSliderVertical:Bool = {
        let isVertical = (controlView as? DNSlider)?.isVertical == true
        return isVertical
    }()
}

3. 重绘slider

要根据其Slider是横着还是竖着的来计算不同的rect

extension DNSliderCell {
    //MARK: - 重绘slider
    override func drawBar(inside rect: NSRect, flipped: Bool) {
        //MARK: 修改进度条'整体'背景色
        // 1. 获取 slider 的 rect
        var sliderRect = rect
        // 2. 更改 slider 厚度
        if isSliderVertical {
            sliderRect.size.width = sliderThickness
        } else {
            sliderRect.size.height = sliderThickness
        }
        // 3. 填充背景色
        let background = NSBezierPath(roundedRect: sliderRect, xRadius: sliderBarRadius, yRadius: sliderBarRadius)
        self.backgroundColor.setFill()
        background.fill()
        
        //MARK: 修改进度条'进度'背景色
        // 1. 计算当前进度的占有比例
        let value:CGFloat = CGFloat((doubleValue - minValue) / (maxValue - minValue))
        // 2. 根据比例计算进度应有的长度
        var viewLength:CGFloat
        if isSliderVertical {
            viewLength = (controlView?.frame.size.height)!
        } else {
            viewLength = (controlView?.frame.size.width)!
        }
 
        let finalLength:CGFloat = value * (viewLength - Offset * 2)
        // 3. 计算进度的Rect
        progressRect = sliderRect
        if isSliderVertical {
            progressRect.size.height = finalLength
            progressRect.origin.y = Offset
        } else {
            progressRect.size.width = finalLength
            progressRect.origin.x = Offset
        }
        
        // 4. 填充进度背景色
        let active = NSBezierPath(roundedRect: progressRect, xRadius: sliderBarRadius, yRadius: sliderBarRadius)
        self.progressColor.setFill()
        active.fill()
    }
}

4. 重绘旋钮

要根据其Slider是横着还是竖着的来计算不同的rect

//MARK: - 重绘旋钮
    override func drawKnob() {
        // 根据比例计算进度应有的OriginX
        let value:CGFloat = CGFloat((doubleValue - minValue) / (maxValue - minValue))
        var viewLength:CGFloat
        if isSliderVertical{
            viewLength = (controlView?.frame.size.height)!
        } else {
            viewLength = (controlView?.frame.size.width)!
        }
        
        var customKnobRect:NSRect
        
        if isSliderVertical {
            //                   Offset +  进度  * ((       实际宽度     ) -  knob宽度 )
            let finalStart:CGFloat = Offset + value * (viewLength - Offset * 2 - KnobHeight)
            // 计算Knob的Rect
            customKnobRect = NSRect(x: progressRect.origin.x + progressRect.size.width / 2 - KnobWidth / 2, y: finalStart, width: KnobWidth , height: KnobHeight)
        } else {
            //                   Offset +  进度  * ((       实际宽度      ) -  knob宽度 )
            let finalStart:CGFloat = Offset + value * (viewLength - Offset * 2 - KnobWidth)
            // 计算Knob的Rect
            customKnobRect = NSRect(x: finalStart, y: progressRect.origin.y + progressRect.size.height / 2 - KnobHeight / 2, width: KnobWidth , height: KnobHeight)
        }
        
        
        // 填充Knob的颜色
        let background = NSBezierPath(roundedRect: customKnobRect, xRadius: KnobWidth / 2, yRadius: KnobHeight / 2)
        if needControlKnobHidden == true {
            if (controlView as? DNSlider)?.shouKnob == true {
                self.knobColor.setFill()
            } else {
                NSColor.clear.setFill()
            }
        } else {
            self.knobColor.setFill()
        }

        background.fill()
    }

自定义NSSlider

自定义NSSlider,可以处理鼠标滑过时鼠标指针的状态、控制knob的显示隐藏等; 不废话直接上代码!

class DNSlider: NSSlider {
    private var trackingArea:NSTrackingArea?
    // 是否显示knob
    var shouKnob:Bool = false    
}

extension DNSlider {
    override func updateTrackingAreas() {
        super.updateTrackingAreas()
        if trackingArea != nil {
            self.removeTrackingArea(trackingArea!)
        }
        
        // 将设置追踪区域为控件大小
        // 设置鼠标追踪区域,如果不设置追踪区域,mouseEntered和mouseExited会无效
        trackingArea = NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeAlways], owner: self, userInfo: nil)
        self.addTrackingArea(trackingArea!)
    }
    
    override func mouseEntered(with event: NSEvent) {
        super.mouseEntered(with: event)
        NSCursor.pointingHand.set()
        /*
            通过设置 isHighlighted 取反来触发 cell 的 drawKnob 方法;
            通过 shouKnob 来控制是否显示(因为isHighlighted受其他状态影响,无法精准控制)
         */
        shouKnob = true
        isHighlighted = !isHighlighted
    }
    
    override func mouseExited(with event: NSEvent) {
        super.mouseExited(with: event)
        NSCursor.arrow.set()
        shouKnob = false
        
        // 不直接设置true/false是因为如果在mouseExited前isHighlighted已经是true/false,那就无法触发cell重新渲染了
        isHighlighted = !isHighlighted
    }
    // 点击后,状态会刷新,此时如不做更改会默认改回指针状态
    override func cursorUpdate(with event: NSEvent) {
        super.cursorUpdate(with: event)
        NSCursor.pointingHand.set()
    }
}