關(guān)鍵詞:iOS鸿捧、引導(dǎo)頁(yè)屹篓、自定義View、氣泡匙奴、AutoLayout堆巧、自動(dòng)布局、OC、Objective-C谍肤、CALayer啦租、CATextLayer、intrinsicContentSize
在上一篇文章 iOS: 引導(dǎo)頁(yè) UIScrollView 自動(dòng)布局(AutoLayout)詳解
中介紹了一個(gè)開(kāi)屏引導(dǎo)頁(yè)的實(shí)現(xiàn)荒揣,還有一種引導(dǎo)也很常用篷角,就是浮動(dòng)氣泡引導(dǎo)。說(shuō)白了就是在進(jìn)入應(yīng)用界面后為了防止用戶一臉懵逼系任,給關(guān)鍵的按鈕啊文字啊恳蹲,高亮一下,加上一堆小氣泡俩滥,氣泡里再加點(diǎn)文字介紹嘉蕾。這樣就能對(duì)界面起到一個(gè)說(shuō)明的作用,也能讓用戶順著你的思路使用霜旧。
氣泡引導(dǎo)的關(guān)鍵技術(shù)是自定義氣泡 View错忱,氣泡起到指示說(shuō)明和承載消息的作用,是由一張圖片和一段文字組成的颁糟,實(shí)現(xiàn)氣泡的方法有好幾種:
- UIView 組合:直接組合 UILabel 與 UIImageView
- CALayer: 使用 CATextLayer 結(jié)合 CALayer 寄宿圖
- 單 UILabel:?jiǎn)为?dú)使用 UILabel 并使用 CALayer 寄宿圖
其中最簡(jiǎn)單最靈活的實(shí)現(xiàn)方式就是第一種組合法航背,本文以引導(dǎo)氣泡功能為例,總結(jié)自定義氣泡 View (BubbleView)的組合方式的實(shí)現(xiàn)方法棱貌,并在后面簡(jiǎn)單介紹和分析一下本人嘗試后兩種方法遇到的坑??????玖媚。
需求
有三個(gè)需要引導(dǎo)的按鈕,每一個(gè)按鈕需要顯示一個(gè)氣泡對(duì)功能進(jìn)行說(shuō)明婚脱,一次只顯示一個(gè)氣泡今魔,每按一次屏幕顯示下一個(gè)氣泡。如圖:
基礎(chǔ)知識(shí):氣泡圖片如何合適地拉伸
合適地拉伸
氣泡的大小需要適應(yīng)文字內(nèi)容障贸,比如只有幾個(gè)字的時(shí)候氣泡要緊緊包裹文字不能過(guò)大:
文字多的時(shí)候就要顯示成兩行或更多:
直接用一張圖行不行错森?
直接用圖會(huì)產(chǎn)生整張圖片拉伸的效果:
拉伸圖片的方法
① 使用 UIImage 提供的拉伸方法:
- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets;
該方法是直接操作 UIImage 的,根據(jù)原始的 UIImage 生成一個(gè)拉伸過(guò)的 UIImage篮洁。
參數(shù) UIEdgeInsets capInsets
表示圖片四個(gè)方向上的固定區(qū)域大小涩维,中間區(qū)域就是可以拉伸的范圍:
可拉伸區(qū)域的大小會(huì)影響繪制的效率。官方文檔中指出袁波,可拉伸區(qū)域只有 1x1 的像素大小是效率最高的瓦阐。文檔原文
使用方法 resizableImageWithCapInsets
設(shè)置的時(shí)候單位是 point,我們知道一個(gè) point 在不同的設(shè)備上表示的像素可能不一樣篷牌,不太方便設(shè)置成 1x1 像素睡蟋。
② 在 Xcode IB 中對(duì)圖片進(jìn)行設(shè)置:
還可以使用圖形化編輯界面,這個(gè)功能藏的好深……
Slicing 在屬性窗口的最下端枷颊,其中填寫的數(shù)字的單位是像素而不是 point戳杀。注意對(duì)每個(gè)尺寸的圖需要進(jìn)行單獨(dú)設(shè)置该面,也就是說(shuō)有幾個(gè)圖就要設(shè)置幾次。設(shè)置的時(shí)候麻煩一些信卡,但使用的時(shí)候方便隔缀,可以直接在 Xcode IB 中設(shè)置給需要 UIImage 的屬性,也可以直接調(diào)用 + (NSImage *)imageNamed:(NSImageName)name;
獲取到有拉伸效果的 UIImage坐求,不必再調(diào)用 resizableImageWithCapInsets
蚕泽。這個(gè)方法設(shè)置 1x1 像素拉伸區(qū)域比較方便。
③ 還有一種更麻煩的方法:使用 CALayer 寄宿圖桥嗤,通過(guò) contentsCenter
屬性來(lái)設(shè)置可拉伸區(qū)域须妻,這里就不展開(kāi)了,可以參考這里:iOS核心動(dòng)畫高級(jí)技巧 - contents 屬性泛领。
組合 UILabel 與 UIImageView 實(shí)現(xiàn) BubbleView
自適應(yīng)的 UILabel 與 BubbleView
UILabel 的一個(gè)重要功能是自適應(yīng)大小荒吏,在自動(dòng)布局中分為幾種情況:
- 不設(shè)置寬度和高度,此時(shí) UILabel 會(huì)將文字顯示為一行渊鞋,并且有多寬顯示多寬绰更。
- 設(shè)置寬度約束不設(shè)置高度約束,此時(shí) UILabel 會(huì)滿足寬度約束锡宋,如果文字太多儡湾,寬度超出了顯示范圍會(huì)根據(jù)
numberOfLines
屬性計(jì)算高度,裁剪掉超出的部分执俩,如果沒(méi)超出或者numberOfLines = 0
則自動(dòng)調(diào)整高度顯示所有文字內(nèi)容徐钠。 - 同時(shí)設(shè)置了寬度和高度約束,此時(shí) UILabel 大小固定役首,內(nèi)容無(wú)法影響大小尝丐,如果顯示不下內(nèi)容會(huì)截?cái)唷?/li>
對(duì)氣泡來(lái)說(shuō),指定寬度最大值衡奥,不限制高度是比較常見(jiàn)的需求爹袁,但最好是什么情況都能支持。
最重要的文字自適應(yīng)已經(jīng)由 UILabel 解決了矮固,只要讓 BubbleView 的長(zhǎng)寬約束依賴于 UILabel 就能使 BubbleView 獲得與 UILabel 同樣的自適應(yīng)能力失息。
下面列出 BubbleView 的約束:
其實(shí)就是兩批約束:
- BubbleView 的四個(gè)邊對(duì)齊 UIImageView 的四個(gè)邊,表示 BubbleView 要與圖片大小相同档址。
- UIImageView 的四個(gè)邊對(duì)齊 UILabel 的四個(gè)邊盹兢,表示圖片大小要與文字相同,這幾個(gè)約束后面還需要通過(guò)代碼來(lái)設(shè)置 UILabel 在整個(gè) BubbleView 中的 padding辰晕。
圖中被拉伸的氣泡是 Xcode IB 的顯示問(wèn)題蛤迎,即使正確設(shè)置了 Slicing 也不能正確地顯示确虱,不過(guò)不耽誤運(yùn)行效果含友。
再看一下如何設(shè)置 BubbleView 的外部約束:
BubbleView 的位置沒(méi)有什么影響,可以隨意設(shè)置,關(guān)鍵在于寬度和高度的約束翻默,圖中所示使用了 width <= 253
來(lái)指定寬度最大值隧期。但由于 Xcode IB 不知道 BubbleView 能計(jì)算自己的大小因此會(huì)有紅色的錯(cuò)誤提示。
Content Hugging Priority 與 Content Compression Resistance Priority
這兩個(gè)特長(zhǎng)的東西是個(gè)啥玩意惠赫,別著急請(qǐng)接著上文繼續(xù)看把鉴。
自定義 View 想要告知 Xcode IB 自己能計(jì)算大小,并在 IB 中實(shí)時(shí)刷新效果儿咱,需要在 interface
聲明前加上 IB_DESIGNABLE
庭砍。一旦自定義 View 有修改,然后回到 xib 文件時(shí)就會(huì)觸發(fā) build 并且刷新 IB 界面混埠,在開(kāi)發(fā)過(guò)程中會(huì)比較慢和卡怠缸,我的 Air 能卡成??,而且 Xcode IB 中總有一些小問(wèn)題钳宪,不建議在開(kāi)發(fā)自定義 View 的過(guò)程中開(kāi)啟這個(gè)功能揭北。
雖然有紅色的錯(cuò)誤提示,但是不管它最終運(yùn)行也是正確的吏颖,只是看起來(lái)不爽……不行搔体,我受不了這個(gè)委屈,得研究研究怎么解決半醉,這一研究就發(fā)現(xiàn)了 Content Hugging Priority 與 Content Compression Resistance Priority 的神奇奧秘疚俱。
設(shè)置優(yōu)先級(jí)較低的定值寬高 width = 253 @100
和 height = 36 @100
,對(duì) Xcode IB 來(lái)說(shuō)就補(bǔ)上了缺失的寬和高不會(huì)再報(bào)錯(cuò)奉呛,而在運(yùn)行時(shí)會(huì)有 BubbleView 內(nèi)部 UILabel 傳遞過(guò)來(lái)的寬和高计螺,這個(gè)寬和高的約束優(yōu)先級(jí)就比較有趣了,是內(nèi)部的 UILabel 的 Content Hugging Priority 和 Content Compression Resistance Priority瞧壮,他們倆的默認(rèn)值是 250 和 750登馒,肯定比 100 要優(yōu)先,因此會(huì)忽略設(shè)置的這兩個(gè) width = 253 @100
和 height = 36 @100
咆槽。達(dá)到了敷衍 Xcode 又能正確運(yùn)行的目的陈轿。
Content Hugging(CH)與 Content Compression Resistance(CCR)是 UIView 的屬性,用來(lái)表示當(dāng)一個(gè) UIView 自己決定自己的大小的時(shí)候(比如 UILabel)秦忿,這個(gè)自定義大小在自動(dòng)布局體系內(nèi)的優(yōu)先級(jí)麦射。
- Content Hugging 表示不被拉伸的優(yōu)先級(jí)
- Content Compression Resistance 表示不被壓縮的優(yōu)先級(jí)
這兩個(gè)值都有兩個(gè)維度:水平方向和豎直方向。
如果通過(guò)約束計(jì)算出來(lái)的寬度或高度與自定義的大小有沖突灯谣,這時(shí)候 CH 和 CCR 就派上用場(chǎng)了潜秋。定義:
- 約束計(jì)算出來(lái)的寬高為
w
、h
- 自定義寬高為
iw
胎许、ih
- 最終結(jié)果寬高為
width
峻呛、height
- 約束為寬度
X
罗售、高度Y
- CH 寬和高分別為
CH-W
、CH-H
- CCR 寬和高分別為
CCR-W
钩述、CCR-H
- 優(yōu)先級(jí)為
.priority
寨躁。
偽代碼如下:
if (w > iw) width = X.priority > CH-W.priority ? w : iw;
if (w < iw) width = X.priority > CCR-W.priority ? w : iw;
if (h > ih) height = Y.priority > CH-H.priority ? h : ih;
if (h < ih) height = Y.priority > CCR-H.priority ? h : ih;
通常都用兩個(gè) UILabel 來(lái)實(shí)驗(yàn) CH 和 CCR 的效果,這也是關(guān)于 CH 與 CCR 最常見(jiàn)的 case牙勘,具體可以參考這篇文章职恳,iOS開(kāi)發(fā)之AutoLayout中的Content Hugging Priority和 Content Compression Resistance Priority解析
BubbleView 的接口
做為一個(gè)自定義 View,應(yīng)該提供給使用者怎樣的接口呢方面?BubbleView 是不提供圖片資源的放钦,因此需要外部指定圖片,同時(shí)跟圖片有關(guān)系的還有一個(gè)可選的 UIEdgeInsets 表示圖片拉伸信息恭金;另一個(gè)顯而易見(jiàn)的屬性是文字最筒,文字同樣也有個(gè) UIEdgeInsets,表示文字在整個(gè) BubbleView 中的 padding蔚叨;另外還有文字樣式的設(shè)置床蜘。
@interface BubbleView : UIView
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, assign) UIEdgeInsets imageCapInsets;
@property (nonatomic, copy) NSString* text;
@property (nonatomic, assign) UIEdgeInsets textEdgeInsets;
@property (nonatomic, strong) UIFont *font;
@property (nonatomic, strong) UIColor *textColor;
@end
使用 CALayer 圖層組合實(shí)現(xiàn) BubbleView
CALayer 是特別強(qiáng)大的,它是 UIKit 圖形部分的基礎(chǔ)蔑水,平常最常用的應(yīng)該就是設(shè)置圓角了吧:view.layer.cornerRadius
邢锯。它還有許多強(qiáng)大的高級(jí)功能,例如上文也提到過(guò) contentsCenter
可以用來(lái)拉伸氣泡圖搀别。實(shí)際上用 CALayer 實(shí)現(xiàn)的氣泡就用到了這個(gè)屬性丹擎。下面來(lái)簡(jiǎn)單分析一下。
同樣是一張圖片和一段文字歇父,圖片好說(shuō)蒂培,用寄宿圖,伸縮也沒(méi)問(wèn)題榜苫。文字就要用到 CATextLayer 了护戳,這個(gè) CATextLayer 簡(jiǎn)直就是 UILabel 啊,可以設(shè)置字體垂睬、顏色媳荒、換行行為等等,貌似什么功能都有的驹饺。
但 CATextLayer 這貨有一個(gè)最大的問(wèn)題是無(wú)法自適應(yīng)文字來(lái)調(diào)整自己的大小钳枕。CATextLayer 并不是 AutoLayout 體系中的,CATextLayer 的 frame
屬性需要明確的手動(dòng)設(shè)置赏壹,而不是自己自動(dòng)設(shè)置鱼炒。
那么怎么計(jì)算一段文字應(yīng)該占多大的矩形空間呢?比較原始的方法可以用 CoreText蝌借。也可以用比較簡(jiǎn)單的 NSAttributedString 的 boundingRectWithSize:options:context:
方法昔瞧。由于 CATextLayer 直接支持設(shè)置 NSAttributedString 文字俐巴,而且這兩種方法效果相同,因此就直接使用第二種方式計(jì)算硬爆。
雖然理論上很完美,但這個(gè)計(jì)算還是有點(diǎn)問(wèn)題擎鸠,因?yàn)?CATextLayer 這貨雖然支持 NSAttributedString缀磕,但并不是所有的樣式都支持,比如行間距就無(wú)法設(shè)置劣光。無(wú)法設(shè)置就沒(méi)辦法控制精確的樣式袜蚕,而且你也無(wú)法得知 CATextLayer 的默認(rèn)樣式的精確值,因此無(wú)法通過(guò) boundingRectWithSize:options:context:
方法來(lái)計(jì)算出精確的應(yīng)有尺寸绢涡。
根據(jù)經(jīng)驗(yàn)牲剃,行間距大概是 1,但經(jīng)過(guò)本人的實(shí)驗(yàn)雄可,并不精確凿傅,可能還要小一點(diǎn)。有些實(shí)驗(yàn)計(jì)算出來(lái)后大小就是不準(zhǔn)確数苫,實(shí)際繪制的文字區(qū)域要比計(jì)算出來(lái)的矩形區(qū)域要大聪舒。
既然沒(méi)法辦精確控制和計(jì)算,而且也導(dǎo)致最終氣泡效果有些問(wèn)題虐急,因此這個(gè)方法沒(méi)有應(yīng)用在實(shí)際項(xiàng)目中箱残。
這個(gè)方法本質(zhì)上相當(dāng)于實(shí)現(xiàn)一個(gè)帶邊距帶底圖的 UILabel,而且還要能自動(dòng)計(jì)算大小止吁,上問(wèn)提到了計(jì)算文字矩形的方法和問(wèn)題被辑,但還有另一個(gè)問(wèn)題待解決就是如何與 AutoLayout 系統(tǒng)溝通并最終決定大小。
首先要看 intrinsicContentSize 這個(gè)屬性敬惦,這是一個(gè)只讀屬性:
@property(nonatomic, readonly) CGSize intrinsicContentSize;
其實(shí)就是一個(gè)返回 CGSize 的無(wú)參數(shù)方法盼理,當(dāng)自定義 View 需要自己計(jì)算大小的時(shí)候,要重寫這個(gè)方法俄删,默認(rèn)實(shí)現(xiàn)是返回 CGSize(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric)
榜揖,UIViewNoIntrinsicMetric
表示沒(méi)有自定義大小。簡(jiǎn)單地說(shuō)抗蠢,這個(gè)方法是用來(lái)通知 AutoLayout 系統(tǒng)自己「本來(lái)應(yīng)該有多大」举哟,注意「本來(lái)應(yīng)該有多大」的判定時(shí)只能通過(guò)自己的屬性來(lái)判斷,而無(wú)法得知 AutoLayout 給你留了多大地方迅矛。
就像是父母對(duì)孩子說(shuō)妨猩,你要多少壓歲錢,雖然父母心中有數(shù)秽褒,但不告訴孩子啊壶硅,孩子只知道自己要一個(gè)游戲機(jī)威兜,于是說(shuō)那就 3000 吧,結(jié)果父母一翻白眼庐椒,給你 300 買個(gè)小霸王吧椒舵。
所以在父母只給 300 的前提下如何玩到游戲……那就只好再討價(jià)還價(jià)了。
在 - (void)layoutSublayersOfLayer:(CALayer *)layer;
執(zhí)行時(shí)可以通過(guò) layer.bounds.size
得知 AutoLayout 到底給你準(zhǔn)備了多大的空間约谈,這時(shí)可以記錄下來(lái)備用笔宿。通過(guò)調(diào)用 invalidateIntrinsicContentSize
這個(gè)方法通知 AutoLayout 系統(tǒng)重新計(jì)算大小,就會(huì)重新調(diào)用 intrinsicContentSize
方法棱诱,這時(shí)可以根據(jù)之前記錄的大小來(lái)重新計(jì)算泼橘,比如第一次 intrinsicContentSize
返回了 CGSize(3000, 40)
但在 layoutSublayersOfLayer
內(nèi)發(fā)現(xiàn)給你分配的大小是 CGSize(300, 40)
,這個(gè)時(shí)候按照寬度 200 重新計(jì)算文字矩形返回 CGSize(300, 400)
迈勋,這樣就計(jì)算出了在規(guī)定了最大寬度時(shí)的文字覺(jué)醒炬灭。
孩子說(shuō) 300 買不了游戲機(jī),每天多玩兩個(gè)小時(shí)平板電腦吧靡菇,結(jié)果父母一翻白眼重归,多玩半個(gè)小時(shí)。
所以討價(jià)還價(jià)一次還是不夠厦凤,最終大小還得再來(lái)一次提前,看看父母在高度上的容忍底線在哪里……這是一個(gè)非常復(fù)雜的過(guò)程就不繼續(xù)分析了,有興趣的可以重寫一下 UILabel 的 intrinsicContentSize
方法打個(gè) log 看看會(huì)被調(diào)用多少次泳唠,看到 UILabel 也要調(diào)用 n 次才行狈网,就平衡了。
根本原因還是單方向的溝通造成的笨腥,intrinsicContentSize
方法本身并不知道對(duì)自己的大小限制是怎樣的拓哺,必須靠來(lái)來(lái)回回的問(wèn)答方式迂回地解決這個(gè)問(wèn)題。熟悉安卓的朋友可以對(duì)比一下安卓的做法脖母,安卓的 onMeasure
方法傳入的參數(shù)就是父控件對(duì)子控件的要求士鸥,子控件只要在重寫的 onMeasure
方法中根據(jù)父控件的要求設(shè)置自己的大小就可以了,一次搞定不用反復(fù)溝通谆级。
關(guān)于 intrinsicContentSize
的具體用法可以參考這篇文章:只有20%的iOS程序員能看懂:詳解intrinsicContentSize 及 約束優(yōu)先級(jí)/content Hugging/content Compression Resistance
單個(gè) UILabel 的實(shí)現(xiàn)
這是個(gè)有趣的方式烤礁,它的問(wèn)題更多,但在某些情況下還是正確的肥照,而且它是最簡(jiǎn)單的一種方案脚仔。
還是通過(guò) CALayer,給 UILabel 的根 CALayer 設(shè)置寄宿圖表示氣泡圖片舆绎。這個(gè)思路貌似可以鲤脏,經(jīng)過(guò)一次試驗(yàn)也是可以的。但問(wèn)題在于顯示中文時(shí)能正確將氣泡鋪在文字底部,而顯示英文時(shí)文字沒(méi)了猎醇。
真是一個(gè)神奇的效果窥突,經(jīng)過(guò)調(diào)試分析發(fā)現(xiàn),顯示中文時(shí)用的是額外的一個(gè) CALayer硫嘶,這時(shí) UILabel 的根 CALayer 就會(huì)顯示在額外的 CALayer 之下阻问,達(dá)成了氣泡成就;顯示英文時(shí)就直接繪制在根 CALayer 上了沦疾,這個(gè)時(shí)候再設(shè)置寄宿圖称近,就會(huì)將文字覆蓋掉……
因此最終也沒(méi)有采用這個(gè)方法。
結(jié)論
研究過(guò)若干種方法曹鸠,回頭看看組合方式的實(shí)現(xiàn),簡(jiǎn)單斥铺、無(wú)坑彻桃、可靠,還是用最簡(jiǎn)單的組合方式吧晾蜘。