NSButton仿UIButton功能

2022-04-08  本文已影响0人  Jesscia_Liu

swift方法参考

import Cocoa

@IBDesignable
open class CustomButton: NSButton {
    private let titleLayer = CATextLayer()
    private var isMouseDown = false

    public static func circularButton(title: String, radius: Double, center: CGPoint) -> CustomButton {
        with(CustomButton()) {
            $0.title = title
            $0.frame = CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2)
            $0.cornerRadius = radius
            $0.font = .systemFont(ofSize: radius * 2 / 3)
        }
    }

    override open var wantsUpdateLayer: Bool { true }
    
    @IBInspectable public var isHandCursor: Bool = false
    
    @IBInspectable override public var title: String {
        didSet {
            setTitle()
        }
    }

    @IBInspectable public var textColor: NSColor = .labelColor {
        didSet {
            titleLayer.foregroundColor = textColor.cgColor
        }
    }

    @IBInspectable public var activeTextColor: NSColor = .labelColor {
        didSet {
            if state == .on {
                titleLayer.foregroundColor = textColor.cgColor
            }
        }
    }

    @IBInspectable public var cornerRadius: Double = 0 {
        didSet {
            layer?.cornerRadius = cornerRadius
        }
    }

    @IBInspectable public var hasContinuousCorners: Bool = true {
        didSet {
            if #available(macOS 10.15, *) {
                layer?.cornerCurve = hasContinuousCorners ? .continuous : .circular
            }
        }
    }

    @IBInspectable public var borderWidth: Double = 0 {
        didSet {
            layer?.borderWidth = borderWidth
        }
    }

    @IBInspectable public var borderColor: NSColor = .clear {
        didSet {
            layer?.borderColor = borderColor.cgColor
        }
    }

    @IBInspectable public var activeBorderColor: NSColor = .clear {
        didSet {
            if state == .on {
                layer?.borderColor = activeBorderColor.cgColor
            }
        }
    }

    @IBInspectable public var backgroundColor: NSColor = .clear {
        didSet {
            layer?.backgroundColor = backgroundColor.cgColor
        }
    }

    @IBInspectable public var activeBackgroundColor: NSColor = .clear {
        didSet {
            if state == .on {
                layer?.backgroundColor = activeBackgroundColor.cgColor
            }
        }
    }

    @IBInspectable public var shadowRadius: Double = 0 {
        didSet {
            layer?.shadowRadius = shadowRadius
        }
    }

    @IBInspectable public var activeShadowRadius: Double = -1 {
        didSet {
            if state == .on {
                layer?.shadowRadius = activeShadowRadius
            }
        }
    }

    @IBInspectable public var shadowOpacity: Double = 0 {
        didSet {
            layer?.shadowOpacity = Float(shadowOpacity)
        }
    }

    @IBInspectable public var activeShadowOpacity: Double = -1 {
        didSet {
            if state == .on {
                layer?.shadowOpacity = Float(activeShadowOpacity)
            }
        }
    }

    @IBInspectable public var shadowColor: NSColor = .clear {
        didSet {
            layer?.shadowColor = shadowColor.cgColor
        }
    }

    @IBInspectable public var activeShadowColor: NSColor? {
        didSet {
            if state == .on, let activeShadowColor = activeShadowColor {
                layer?.shadowColor = activeShadowColor.cgColor
            }
        }
    }

    override public var font: NSFont? {
        didSet {
            setTitle()
        }
    }

    override public var isEnabled: Bool {
        didSet {
            alphaValue = isEnabled ? 1 : 0.6
        }
    }

    public convenience init() {
        self.init(frame: .zero)
    }

    public required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    // Ensure the button doesn't draw its default contents.
    override open func draw(_ dirtyRect: CGRect) {}
    override open func drawFocusRingMask() {}

    override open func layout() {
        super.layout()
        positionTitle()
    }

    override open func viewDidChangeBackingProperties() {
        super.viewDidChangeBackingProperties()

        if let scale = window?.backingScaleFactor {
            layer?.contentsScale = scale
            titleLayer.contentsScale = scale
        }
    }

    private lazy var trackingArea = TrackingArea(
        for: self,
        options: [
            .mouseEnteredAndExited,
            .activeInActiveApp
        ]
    )

    override open func updateTrackingAreas() {
        super.updateTrackingAreas()
        trackingArea.update()
    }

    private func setup() {
        let isOn = state == .on

        wantsLayer = true

        layer?.masksToBounds = false

        layer?.cornerRadius = cornerRadius
        layer?.borderWidth = borderWidth
        layer?.shadowRadius = isOn && activeShadowRadius != -1 ? activeShadowRadius : shadowRadius
        layer?.shadowOpacity = Float(isOn && activeShadowOpacity != -1 ? activeShadowOpacity : shadowOpacity)
        layer?.backgroundColor = isOn ? activeBackgroundColor.cgColor : backgroundColor.cgColor
        layer?.borderColor = isOn ? activeBorderColor.cgColor : borderColor.cgColor
        layer?.shadowColor = isOn ? (activeShadowColor?.cgColor ?? shadowColor.cgColor) : shadowColor.cgColor

        if #available(macOS 10.15, *) {
            layer?.cornerCurve = hasContinuousCorners ? .continuous : .circular
        }

        titleLayer.alignmentMode = .center
        titleLayer.contentsScale = window?.backingScaleFactor ?? 2
        titleLayer.foregroundColor = isOn ? activeTextColor.cgColor : textColor.cgColor
        layer?.addSublayer(titleLayer)
        setTitle()

        needsDisplay = true
    }

    public typealias ColorGenerator = () -> NSColor

    private var colorGenerators = [KeyPath<CustomButton, NSColor>: ColorGenerator]()

    /**
    Gets or sets the color generation closure for the provided key path.

    - Parameter keyPath: The key path that specifies the color related property.
    */
    public subscript(colorGenerator keyPath: KeyPath<CustomButton, NSColor>) -> ColorGenerator? {
        get { colorGenerators[keyPath] }
        set {
            colorGenerators[keyPath] = newValue
        }
    }

    private func color(for keyPath: KeyPath<CustomButton, NSColor>) -> NSColor {
        colorGenerators[keyPath]?() ?? self[keyPath: keyPath]
    }

    override open func updateLayer() {
        animateColor()
    }

    private func setTitle() {
        titleLayer.string = title

        if let font = font {
            titleLayer.font = font
            titleLayer.fontSize = font.pointSize
        }

        needsLayout = true
    }

    private func positionTitle() {
        let titleSize = title.size(withAttributes: [.font: font as Any])
        titleLayer.frame = titleSize.centered(in: bounds).roundedOrigin()
    }

    private func animateColor() {
        let isOn = state == .on
        let duration = isOn ? 0.2 : 0.1
        let backgroundColor = isOn ? color(for: \.activeBackgroundColor) : color(for: \.backgroundColor)
        let textColor = isOn ? color(for: \.activeTextColor) : color(for: \.textColor)
        let borderColor = isOn ? color(for: \.activeBorderColor) : color(for: \.borderColor)
        let shadowColor = isOn ? (activeShadowColor ?? color(for: \.shadowColor)) : color(for: \.shadowColor)

        layer?.animate(\.backgroundColor, to: backgroundColor, duration: duration)
        layer?.animate(\.borderColor, to: borderColor, duration: duration)
        layer?.animate(\.shadowColor, to: shadowColor, duration: duration)
        titleLayer.animate(\.foregroundColor, to: textColor, duration: duration)
    }

    private func toggleState() {
        state = state == .off ? .on : .off
        animateColor()
    }

    override open func hitTest(_ point: CGPoint) -> NSView? {
        isEnabled ? super.hitTest(point) : nil
    }

    override open func mouseDown(with event: NSEvent) {
        isMouseDown = true
        toggleState()
    }

    override open func mouseEntered(with event: NSEvent) {
        if isHandCursor {
            NSCursor.pointingHand.set()
        }
        if isMouseDown {
            toggleState()
        }
    }

    override open func mouseExited(with event: NSEvent) {
        if isHandCursor {
            NSCursor.arrow.set()
        }
        if isMouseDown {
            toggleState()
            isMouseDown = false
        }
    }

    override open func mouseUp(with event: NSEvent) {
        if isMouseDown {
            isMouseDown = false
            toggleState()
            _ = target?.perform(action, with: self)
        }
    }
}

extension CustomButton: NSViewLayerContentScaleDelegate {
    public func layer(_ layer: CALayer, shouldInheritContentsScale newScale: CGFloat, from window: NSWindow) -> Bool { true }
}


import Cocoa

/**
Convenience function for initializing an object and modifying its properties.

\```
let label = with(NSTextField()) {
    $0.stringValue = "Foo"
    $0.textColor = .systemBlue
    view.addSubview($0)
}
\```
*/
@discardableResult
func with<T>(_ item: T, update: (inout T) throws -> Void) rethrows -> T {
    var this = item
    try update(&this)
    return this
}


/**
Convenience class for adding a tracking area to a view.

\```
final class HoverView: NSView {
    private lazy var trackingArea = TrackingArea(
        for: self,
        options: [
            .mouseEnteredAndExited,
            .activeInActiveApp
        ]
    )

    override func updateTrackingAreas() {
        super.updateTrackingAreas()
        trackingArea.update()
    }
}
\```
*/
final class TrackingArea {
    private weak var view: NSView?
    private let rect: CGRect
    private let options: NSTrackingArea.Options
    private weak var trackingArea: NSTrackingArea?

    /**
    - Parameters:
        - view: The view to add tracking to.
        - rect: The area inside the view to track. Defaults to the whole view (`view.bounds`).
    */
    init(
        for view: NSView,
        rect: CGRect? = nil,
        options: NSTrackingArea.Options = []
    ) {
        self.view = view
        self.rect = rect ?? view.bounds
        self.options = options
    }

    /**
    Updates the tracking area.
    - Note: This should be called in your `NSView#updateTrackingAreas()` method.
    */
    func update() {
        if let oldTrackingArea = trackingArea {
            view?.removeTrackingArea(oldTrackingArea)
        }

        let newTrackingArea = NSTrackingArea(
            rect: rect,
            options: [
                .mouseEnteredAndExited,
                .activeInActiveApp
            ],
            owner: view,
            userInfo: nil
        )

        view?.addTrackingArea(newTrackingArea)
        trackingArea = newTrackingArea
    }
}


final class AnimationDelegate: NSObject, CAAnimationDelegate {
    var didStopHandler: ((Bool) -> Void)?

    func animationDidStop(_ animation: CAAnimation, finished flag: Bool) {
        didStopHandler?(flag)
    }
}


protocol LayerColorAnimation: AnyObject {}
extension CALayer: LayerColorAnimation {}

extension LayerColorAnimation where Self: CALayer {
    /**
    Animate colors.
    */
    func animate(_ keyPath: ReferenceWritableKeyPath<Self, CGColor?>, to color: CGColor, duration: Double) {
        let animation = CABasicAnimation(keyPath: keyPath.toString)
        animation.fromValue = self[keyPath: keyPath]
        animation.toValue = color
        animation.duration = duration
        animation.fillMode = .forwards
        animation.isRemovedOnCompletion = false

        add(animation, forKeyPath: keyPath) { [weak self] _ in
            self?[keyPath: keyPath] = color
        }
    }

    /**
    Animate colors.
    */
    func animate(_ keyPath: ReferenceWritableKeyPath<Self, CGColor?>, to color: NSColor, duration: Double) {
        animate(keyPath, to: color.cgColor, duration: duration)
    }

    /**
    Add color animation.
    */
    func add(_ animation: CAAnimation, forKeyPath keyPath: ReferenceWritableKeyPath<Self, CGColor?>, completion: @escaping ((Bool) -> Void)) {
        let animationDelegate = AnimationDelegate()
        animationDelegate.didStopHandler = completion
        animation.delegate = animationDelegate
        add(animation, forKey: keyPath.toString)
    }
}


extension CGPoint {
    func rounded(_ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Self {
        Self(x: x.rounded(rule), y: y.rounded(rule))
    }
}


extension CGRect {
    func roundedOrigin(_ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Self {
        var rect = self
        rect.origin = rect.origin.rounded(rule)
        return rect
    }
}


extension CGSize {
    /**
    Returns a CGRect with `self` centered in it.
    */
    func centered(in rect: CGRect) -> CGRect {
        CGRect(
            x: (rect.width - width) / 2,
            y: (rect.height - height) / 2,
            width: width,
            height: height
        )
    }
}


extension KeyPath where Root: NSObject {
    /**
    Get the string version of the key path when the root is an `NSObject`.
    */
    var toString: String {
        NSExpression(forKeyPath: self).keyPath
    }
}

OC方法参考

《macOS开发》自定义控件之NSButton

上一篇下一篇

猜你喜欢

热点阅读