自定义NSButton实现hover、highlighted效果
NSButton并没有UIButton可以设置state的接口,虽然系统提供了很多button的样式,但是自定义程度不够高,比如hover或highlighted的效果。没关系,既然没有,那就自定义好了。
其实,自定义NSButton非常简单,基本思路就是要在响应不同的鼠标事件时让button呈现不同的状态(通过字体或背景颜色区分,也可以是边框颜色或宽度)。
先上效果:
左边的button是通过各种颜色的变化来表示hover、highlighted和selected状态的,右边的button是通过图片切换来表示,原理相同。
为了响应不同鼠标事件,可以定义四种状态,分别对应正常、鼠标移入、鼠标按下、鼠标释放。
typedef NS_ENUM(NSUInteger, SWSTAnswerButtonState) {
SWSTAnswerButtonNormalState = 0,
SWSTAnswerButtonHoverState = 1,
SWSTAnswerButtonHighlightState = 2,
SWSTAnswerButtonSelectedState = 3
};
NSButton继承了NSResponder的键鼠方法,我们要实现这四种状态需要重写mouseEntered:、mouseExited:、mouseDown:以及mouseUp:四个方法。
- (void)mouseEntered:(NSEvent *)theEvent {
self.hover = YES;
if (!self.selected) {
self.buttonState = SWSTAnswerButtonHoverState;
}
}
- (void)mouseExited:(NSEvent *)theEvent {
self.hover = NO;
if (!self.selected) {
[self setButtonState:SWSTAnswerButtonNormalState];
}
}
- (void)mouseDown:(NSEvent *)event {
self.mouseUp = NO;
if (self.enabled && !self.selected) {
self.buttonState = SWSTAnswerButtonHighlightState;
}
}
- (void)mouseUp:(NSEvent *)event {
self.mouseUp = YES;
if (self.enabled) {
if (self.canSelected && self.hover) {
self.selected = !self.selected;
self.buttonState = self.selected ? SWSTAnswerButtonSelectedState : SWSTAnswerButtonNormalState;
} else {
if (!self.selected) {
self.buttonState = SWSTAnswerButtonNormalState;
}
}
}
}
通过mouseEntered:和mouseExited:来标记hover状态,而mouseDown:来标记highlighted状态,最后mouseUp:标记selected状态。
在buttonState改变的时候更新UI:
- (void)updateButtonApperaceWithState:(SWSTAnswerButtonState)state {
CGFloat cornerRadius = 0.f;
CGFloat borderWidth = 0.f;
NSColor *borderColor = nil;
NSColor *themeColor = nil;
NSColor *backgroundColor = nil;
switch (state) {
case SWSTAnswerButtonNormalState: {
cornerRadius = self.cornerNormalRadius;
borderWidth = self.borderNormalWidth;
borderColor = self.borderNormalColor;
themeColor = self.normalColor;
backgroundColor = self.backgroundNormalColor;
if (self.normalImage != nil) {
self.defaultImage = self.normalImage;
}
break;
}
case SWSTAnswerButtonHoverState: {
cornerRadius = self.cornerHoverRadius;
borderWidth = self.borderHoverWidth;
borderColor = self.borderHoverColor;
themeColor = self.hoverColor;
backgroundColor = self.backgroundHoverColor;
if (self.hoverImage != nil) {
self.defaultImage = self.hoverImage;
}
}
break;
case SWSTAnswerButtonHighlightState: {
cornerRadius = self.cornerHighlightRadius;
borderWidth = self.borderHighlightWidth;
borderColor = self.borderHighlightColor;
themeColor = self.highlightColor;
backgroundColor = self.backgroundHighlightColor;
if (self.highlightImage != nil) {
self.defaultImage = self.highlightImage;
}
}
break;
case SWSTAnswerButtonSelectedState: {
cornerRadius = self.cornerSelectedRadius;
borderWidth = self.borderSelectedWidth;
borderColor = self.borderSelectedColor;
themeColor = self.selectedColor;
backgroundColor = self.backgroundSelectedColor;
if (self.selectedImage != nil) {
self.defaultImage = self.selectedImage;
}
}
break;
}
if (self.defaultImage != nil) {
self.image = self.defaultImage;
}
[self setFontColor:themeColor];
if (self.hasBorder) {
self.layer.cornerRadius = cornerRadius;
self.layer.borderWidth = borderWidth;
self.layer.borderColor = borderColor.CGColor;
} else {
self.layer.cornerRadius = 0.f;
self.layer.borderWidth = 0.f;
self.layer.borderColor = [NSColor clearColor].CGColor;
}
self.layer.backgroundColor = backgroundColor.CGColor;
}
这样就可以简单的实现你想要的几乎任何NSButton支持的样式,只要扩展对应的属性就好了。
不过,在实现样式的时候,发现了一个严重的问题。在重写mouseDown:和mouseUp:方法后,NSButton的action不执行了。这里调用super方法也不管用,看来系统会在这种情况下忽略action方法。
既然系统不执行,我在mouseUp:方法里手动执行一下:
- (void)mouseUp:(NSEvent *)event {
self.mouseUp = YES;
if (self.enabled) {
if (self.canSelected && self.hover) {
self.selected = !self.selected;
self.buttonState = self.selected ? SWSTAnswerButtonSelectedState : SWSTAnswerButtonNormalState;
} else {
if (!self.selected) {
self.buttonState = SWSTAnswerButtonNormalState;
}
}
if (self.hover && self.enabled) {
NSString *selString = NSStringFromSelector(self.action);
if ([selString hasSuffix:@":"]) {
[self.target performSelector:self.action withObject:self afterDelay:0.f];
} else {
[self.target performSelector:self.action withObject:nil afterDelay:0.f];
}
}
}
}
修改后的mouseUp:方法在hover(确保鼠标在button的frame内)及enabled的状态下会通过performSelector:withObject:afterDelay:方法让button的target执行button的action。看起来有点奇怪,但确实可以这么干。
NSButton的初始化可以在代码里给所有属性赋值,但我相信你会觉得这些代码看着很恶心。其实完全不用放在代码里,这些初始化可以全丢给storyboard。
在storyboard里选中NSButton,选择右边第三个标签(Identity),看到有一行是User Defined Runtime Attributes。这里可以在运行时初始化一些自定义属性,支持基本类型外,居然还支持NSPoint、NSSize、NSRect、NScolor,甚至是NSImage(传图片名就好了)。

最后,在updateTrackingAreas方法中,添加了自己的NSTrackingArea,以保证窗口不在最前时移动鼠标到button也可以实现hover效果。
- (void)updateTrackingAreas {
[super updateTrackingAreas];
if (self.trackingArea != nil) {
[self removeTrackingArea:self.trackingArea];
self.trackingArea = nil;
}
NSTrackingAreaOptions options = NSTrackingInVisibleRect|NSTrackingMouseEnteredAndExited|NSTrackingActiveAlways;
self.trackingArea = [[NSTrackingArea alloc] initWithRect:CGRectZero options:options owner:self userInfo:nil];
[self addTrackingArea:self.trackingArea];
}
完整代码见gitlab:https://gitlab.com/Maulyn/CustomButtonDemo
欢迎随时交流~