自定義NSButton實(shí)現(xiàn)hover梁厉、highlighted效果

NSButton并沒(méi)有UIButton可以設(shè)置state的接口辜羊,雖然系統(tǒng)提供了很多button的樣式,但是自定義程度不夠高词顾,比如hover或highlighted的效果八秃。沒(méi)關(guān)系,既然沒(méi)有肉盹,那就自定義好了昔驱。

其實(shí),自定義NSButton非常簡(jiǎn)單上忍,基本思路就是要在響應(yīng)不同的鼠標(biāo)事件時(shí)讓button呈現(xiàn)不同的狀態(tài)(通過(guò)字體或背景顏色區(qū)分骤肛,也可以是邊框顏色或?qū)挾龋?/p>

先上效果:
ButtonEffect.gif

左邊的button是通過(guò)各種顏色的變化來(lái)表示hover、highlighted和selected狀態(tài)的窍蓝,右邊的button是通過(guò)圖片切換來(lái)表示腋颠,原理相同。

為了響應(yīng)不同鼠標(biāo)事件吓笙,可以定義四種狀態(tài)淑玫,分別對(duì)應(yīng)正常、鼠標(biāo)移入、鼠標(biāo)按下絮蒿、鼠標(biāo)釋放尊搬。

typedef NS_ENUM(NSUInteger, SWSTAnswerButtonState) {
    SWSTAnswerButtonNormalState      = 0,
    SWSTAnswerButtonHoverState       = 1,
    SWSTAnswerButtonHighlightState   = 2,
    SWSTAnswerButtonSelectedState    = 3
};

NSButton繼承了NSResponder的鍵鼠方法,我們要實(shí)現(xiàn)這四種狀態(tài)需要重寫(xiě)mouseEntered:土涝、mouseExited:毁嗦、mouseDown:以及mouseUp:四個(gè)方法。

- (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;
            }
        }
    }
}

通過(guò)mouseEntered:和mouseExited:來(lái)標(biāo)記hover狀態(tài)回铛,而mouseDown:來(lái)標(biāo)記highlighted狀態(tài),最后mouseUp:標(biāo)記selected狀態(tài)克锣。

在buttonState改變的時(shí)候更新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;
}

這樣就可以簡(jiǎn)單的實(shí)現(xiàn)你想要的幾乎任何NSButton支持的樣式茵肃,只要擴(kuò)展對(duì)應(yīng)的屬性就好了。

不過(guò)袭祟,在實(shí)現(xiàn)樣式的時(shí)候验残,發(fā)現(xiàn)了一個(gè)嚴(yán)重的問(wèn)題。在重寫(xiě)mouseDown:和mouseUp:方法后巾乳,NSButton的action不執(zhí)行了您没。這里調(diào)用super方法也不管用,看來(lái)系統(tǒng)會(huì)在這種情況下忽略action方法胆绊。

既然系統(tǒng)不執(zhí)行氨鹏,我在mouseUp:方法里手動(dòng)執(zhí)行一下:

- (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(確保鼠標(biāo)在button的frame內(nèi))及enabled的狀態(tài)下會(huì)通過(guò)performSelector:withObject:afterDelay:方法讓button的target執(zhí)行button的action⊙棺矗看起來(lái)有點(diǎn)奇怪仆抵,但確實(shí)可以這么干。

NSButton的初始化可以在代碼里給所有屬性賦值种冬,但我相信你會(huì)覺(jué)得這些代碼看著很惡心镣丑。其實(shí)完全不用放在代碼里,這些初始化可以全丟給storyboard娱两。

在storyboard里選中NSButton莺匠,選擇右邊第三個(gè)標(biāo)簽(Identity),看到有一行是User Defined Runtime Attributes十兢。這里可以在運(yùn)行時(shí)初始化一些自定義屬性趣竣,支持基本類(lèi)型外,居然還支持NSPoint纪挎、NSSize期贫、NSRect、NScolor异袄,甚至是NSImage(傳圖片名就好了)通砍。

UserDefined.png

最后,在updateTrackingAreas方法中,添加了自己的NSTrackingArea封孙,以保證窗口不在最前時(shí)移動(dòng)鼠標(biāo)到button也可以實(shí)現(xiàn)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];
}

完整代碼見(jiàn)gitlab:https://gitlab.com/Maulyn/CustomButtonDemo

歡迎隨時(shí)交流~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市虎忌,隨后出現(xiàn)的幾起案子泡徙,更是在濱河造成了極大的恐慌,老刑警劉巖膜蠢,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堪藐,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡挑围,警方通過(guò)查閱死者的電腦和手機(jī)礁竞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)杉辙,“玉大人模捂,你說(shuō)我怎么就攤上這事≈┦福” “怎么了狂男?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)品腹。 經(jīng)常有香客問(wèn)我岖食,道長(zhǎng),這世上最難降的妖魔是什么珍昨? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任县耽,我火速辦了婚禮,結(jié)果婚禮上镣典,老公的妹妹穿的比我還像新娘兔毙。我一直安慰自己,他們只是感情好兄春,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布澎剥。 她就那樣靜靜地躺著,像睡著了一般赶舆。 火紅的嫁衣襯著肌膚如雪哑姚。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天芜茵,我揣著相機(jī)與錄音叙量,去河邊找鬼。 笑死九串,一個(gè)胖子當(dāng)著我的面吹牛绞佩,可吹牛的內(nèi)容都是我干的寺鸥。 我是一名探鬼主播刨疼,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼懒鉴,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼路幸!你這毒婦竟也來(lái)了姨涡?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤炬搭,失蹤者是張志新(化名)和其女友劉穎饲常,沒(méi)想到半個(gè)月后鳍征,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體涯呻,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡凉驻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了复罐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沿侈。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖市栗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情咳短,我是刑警寧澤填帽,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站咙好,受9級(jí)特大地震影響篡腌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜勾效,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一嘹悼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧层宫,春花似錦杨伙、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至毁菱,卻和暖如春米死,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贮庞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工峦筒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人窗慎。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓物喷,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子脯丝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345