最近做了個直播項目,需要用到彈幕和刷禮物阵面。在網(wǎng)上找了許多開源代碼,發(fā)現(xiàn)都不是很適合自己的項目需求份殿,于是利用空余時間將兩個功能都實現(xiàn)下膜钓,這里分享出來,供大家一起學(xué)習(xí)卿嘲。(關(guān)于彈幕的實現(xiàn)颂斜,大家可以參考我前面寫的一篇文章IOS 自定義彈幕實現(xiàn))
在開始我的實現(xiàn)方案之前,大家可以先參看下這篇文章iOS 基于 IM 實現(xiàn)仿映客刷禮物連擊效果,寫得很好拾枣。Demo中關(guān)于禮物連乘的動畫效果沃疮,就是引用其中。但這位大神所用的緩存邏輯特別復(fù)雜梅肤,所以在控制緩存的時候有些小BUG司蔬,為了彌補這個缺陷,于是我就開始了這篇文章姨蝴。
實現(xiàn)功能
程序最終實現(xiàn)的效果圖如下:
這里的禮物1俊啼、2、3左医、4四個按鈕分別模擬四個人發(fā)的四種禮物授帕,點擊一次,就代表發(fā)送一條禮物消息浮梢,實現(xiàn)邏輯功能如下:
- 點擊禮物1按鈕跛十,會出現(xiàn)一個倒計時按鈕,并發(fā)送一條禮物消息
- 再次點擊倒計時按鈕秕硝,會再次發(fā)送一條禮物消息芥映,禮物數(shù)量累加
- 在倒計時的時間內(nèi),倒計時按鈕如果沒有收到點擊事件,倒計時按鈕會隱藏奈偏,并且當(dāng)前用來展示禮物動畫的cell也會隱藏
- 如果當(dāng)前全部的cell都在展示坞嘀,這時你點擊了其他類型的禮物按鈕,這時的禮物消息會被緩存霎苗,等到當(dāng)前禮物動畫執(zhí)行完時姆吭,再去執(zhí)行緩存的
- 如果在短時間內(nèi)多次點擊連送按鈕,連乘動畫也會緩存
實現(xiàn)界面功能如下:
- 可自定義cell樣式
- 可自定義cell的展示和隱藏動畫
- 可監(jiān)聽cell的點擊事件
下面是我實現(xiàn)這些功能的基本邏輯與實現(xiàn)代碼唁盏。
基本邏輯
開始寫代碼之前内狸,將功能的基本邏輯列出,這是個很好的習(xí)慣厘擂。特別是對于那些復(fù)雜的功能昆淡,這個習(xí)慣就顯得尤為重要。
功能要求:收到消息展示動畫刽严、可連乘昂灵、可緩存
功能要求看起好像很簡單,如果真的實現(xiàn)可就不是那么容易了舞萄。
具體邏輯如下:
- 收到一條禮物消息
- 檢測當(dāng)前是否有相同類型的禮物消息正在展示動畫
- 有眨补,將該消息加入到當(dāng)前的動畫組中
- 沒有,檢測當(dāng)前是否有空閑的軌道用于展示動畫
- 有倒脓,取出緩存中相同類型的消息撑螺,開始執(zhí)行動畫
- 沒有,將當(dāng)前消息加入到消息緩存中
- 連乘動畫完成崎弃,如果3秒內(nèi)沒有收到新的同類消息甘晤,就執(zhí)行隱藏動畫
- 隱藏動畫完成,再取緩存饲做,開始下一個動畫线婚,直至無緩存為止
理清楚了邏輯之后,下面就是代碼實現(xiàn)了盆均,真正的痛苦現(xiàn)在才開始塞弊!
實現(xiàn)代碼
這里我會根據(jù)邏輯順序來介紹代碼的實現(xiàn)。在收到一條消息之后泪姨,在外面只需調(diào)用PresentView對象的insertPresentMessages:接口方法居砖,將消息插入進來,insertPresentMessages:接口實現(xiàn)代碼如下:
- (void)insertPresentMessages:(NSArray<id<PresentModelAble>> *)models
{
NSArray *siftArray = [self checkElementOfModels:models];
if (!siftArray.count) return;
for (int index = 0; index < siftArray.count; index++) {
id<PresentModelAble> obj = models[index];
PresentViewCell *cell = [self examinePresentingCell:obj];
if (cell) {
[cell shakeAnimationWithNumber:1];
}else {
[self.dataCaches addObject:obj];//將當(dāng)前消息加到緩存中
NSArray *cells = [self examinePresentViewCells];
if (cells.count) {
cell = cells.firstObject;
NSArray *objs = [self subarrayWithObj:obj];
__weak typeof(self) ws = self;
[cell showAnimationWithSender:[obj sender] giftName:[obj giftName] prepare:^{
if ([ws.delegate respondsToSelector:@selector(presentView:configCell:sender:giftName:)]) {
[ws.delegate presentView:ws configCell:cell sender:[obj sender] giftName:[obj giftName]];
}
} completion:^(BOOL finished) {
int index = 0;
while (index < objs.count) {
index++;
[cell shakeAnimationWithNumber:objs.count];
}
}];
}
}
}
}
這基本就是整個邏輯的實現(xiàn)了驴娃,看到這一堆的邏輯判斷,是不是感覺頭都大了循集。沒關(guān)系唇敞,接下來我會對這個代碼進行一步步的解析。
首先,調(diào)用了PresentView對象的checkElementOfModels:方法疆柔,這就是數(shù)據(jù)檢測咒精。
數(shù)據(jù)檢測
數(shù)據(jù)檢測就是對插入的模型數(shù)組進行過濾,因為我們需要通過這個模型來確定消息的類型(禮物類型是通過發(fā)送者和發(fā)送的禮物名來確定的)旷档,所以自定義的消息模型必須要遵守PresentModelAble協(xié)議模叙,協(xié)議要求如下:
@required
@property (copy, nonatomic) NSString *sender;
@property (copy, nonatomic) NSString *giftName;
故檢測數(shù)據(jù),其實就是檢測模型數(shù)組中的元素是否遵守了PresentModelAble協(xié)議鞋屈,并剔除其中沒有遵守協(xié)議的數(shù)據(jù)范咨。具體實現(xiàn)代碼如下:
- (NSArray *)checkElementOfModels:(NSArray<id<PresentModelAble>> *)models
{
NSMutableArray *siftArray = [NSMutableArray array];
for (id obj in models) {
if (![obj conformsToProtocol:@protocol(PresentModelAble)]) {
DebugLog(@"%@對象沒有遵守PresentModelAble協(xié)議", obj);
}else {
[siftArray addObject:obj];
}
}
return siftArray;
}
選出合適的數(shù)據(jù)之后,就是遍歷該數(shù)組厂庇,對每一個元素進行動畫檢測渠啊。
動畫檢測
動畫檢測就是檢測當(dāng)前是否有相同類型的消息正在展示動畫,即流程2权旷。這里是遍歷當(dāng)前所有的cell替蛉,如果cell上展示的消息類型與該消息類型一致,并且cell的動畫正在執(zhí)行拄氯,就返回該cell躲查,否則返回nil。具體實現(xiàn)代碼如下:
- (PresentViewCell *)examinePresentingCell:(id<PresentModelAble>)obj
{
for (PresentViewCell *cell in self.showCells) {
if ([cell.sender isEqualToString:[obj sender]] && [cell.giftName isEqualToString:[obj giftName]]) {
//當(dāng)前正在展示動畫
if (cell.state != AnimationStateNone) return cell;
}
}
return nil;
}
如果動畫檢測檢測到匹配的cell译柏,就在cell當(dāng)前的動畫隊列上添加一個連乘動畫(shakeAnimation)任務(wù)镣煮。
ShakeAnimation
連乘動畫就是在當(dāng)前禮物數(shù)量上做一個累加的動畫,即流程3艇纺。連乘動畫的具體實現(xiàn)怎静,大家可以自行查看demo中PresentLable對象的startAnimationDuration:completion:方法。因為連乘動畫執(zhí)行完黔衡,三秒后沒有收到新的任務(wù)就要開始cell的隱藏動畫(hiddenAnimation)蚓聘。所以開始連乘動畫前,需要取消掉前面延時三秒執(zhí)行的隱藏動畫任務(wù)盟劫,然后重新開始延時,即流程7夜牡。具體實現(xiàn)代碼如下:
- (void)shakeAnimationWithNumber:(NSInteger)number
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
__weak typeof(self) ws = self;
[self performSelector:@selector(hiddenAnimation) withObject:nil afterDelay:3.0];
_state = AnimationStateShaking;
self.shakeLable.text = [NSString stringWithFormat:@"X%ld", ++self.number];
[self.shakeLable startAnimationDuration:Duration completion:^(BOOL finish) {
if (number > 1) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ws startShakeAnimationWithNumber:(number - 1) completion:block];
});
}else {
_state = AnimationStateShaked;
if (block) {
block(YES);
}
}
}];
}
HiddenAnimation
隱藏動畫實現(xiàn)代碼也非常簡單,這里就不介紹了侣签。但是需要注意的地方是塘装,隱藏動畫執(zhí)行完成后需要將cell恢復(fù)到初始狀態(tài),保證cell在開始下一次展示動畫之前不會因為狀態(tài)的錯誤而導(dǎo)致流程判斷出錯影所。
如果動畫檢測沒有檢測到匹配的cell蹦肴,就開始cell的檢測
cell檢測
cell檢測就是檢測當(dāng)前是否有空閑的cell用于展示禮物消息動畫,即流程4.這里只需要遍歷所有的cell猴娩,判斷cell的動畫狀態(tài)就可以了阴幌。具體實現(xiàn)代碼如下:
- (NSArray<PresentViewCell *> *)examinePresentViewCells
{
NSMutableArray *freeCells = [NSMutableArray array];
for (PresentViewCell *cell in self.showCells) {
if (cell.state == AnimationStateNone) {
[freeCells addObject:cell];
}
}
return freeCells;
}
如果沒有空閑的cell用于展示動畫勺阐,就將當(dāng)前消息加入到緩存中,即添加到dataCaches這個數(shù)組中,即流程6矛双。
如果有空閑cell用于展示渊抽,就從空閑cell數(shù)組中取出一個cell,執(zhí)行cell的展示動畫(showAnimation)议忽,即流程5懒闷。
ShowAnimation
cell的展示動畫,其接口如下:
/**
* 顯示cell動畫
*
* @param sender 發(fā)送者
* @param name 禮物名
* @param prepare 準備動畫回調(diào)
* @param completion 動畫完成回調(diào)
*/
- (void)showAnimationWithSender:(NSString *)sender
giftName:(NSString *)name
prepare:(void (^)(void))prepare
completion:(void (^)(BOOL finished))completion;
因為展示動畫是用來展示禮物消息的動畫栈幸,在展示之前需要知道展示的禮物消息的類型愤估,所以需要傳入sender和name兩個參數(shù)。
展示動畫完成之后侦镇,就需要從緩存中取出與該消息相同類型的消息,然后開始連乘動畫灵疮,這些操作就可以早completion中完成。那prepare回調(diào)是干嘛呢壳繁?
其實這里還有一個問題:在開始cell的展示動畫之前震捣,我們就需要給這個cell設(shè)置需要展示的數(shù)據(jù)∧致可是cell是自定義的蒿赢,這是根本無法拿到自定義的cell,也就無法給這個cell設(shè)置數(shù)據(jù)渣触?
這里我的思路是:在開始cell的展示動畫之前羡棵,就是prepare回調(diào)中,給外界一個代理通知嗅钻,讓外面來執(zhí)行這個賦值操作皂冰。代理通知接口如下:
/**
* 禮物動畫即將展示的時調(diào)用,根據(jù)禮物消息類型為自定義的cell設(shè)置對應(yīng)的模型數(shù)據(jù)用于展示
*
* @param cell 用來展示動畫的cell
* @param sender 禮物發(fā)送者
* @param name 禮物名
*/
- (void)presentView:(PresentView *)presentView
configCell:(PresentViewCell *)cell
sender:(NSString *)sender
giftName:(NSString *)name;
到這里养篓,關(guān)于收到一條消息的流程就就全部處理完了秃流。最后一步就是對緩存邏輯進行處理了,即上面的流程8柳弄。
緩存處理
緩存處理就是當(dāng)一個禮物消息類型的動畫處理完舶胀,即cell的隱藏動畫執(zhí)行完成,就要從緩存中取下一個類型的禮物消息碧注,開始下一組動畫,直至無緩存為止嚣伐,即流程8。這里的隱藏動畫回調(diào)是通過代理實現(xiàn)的萍丐,具體實現(xiàn)代碼如下:
- (void)presentViewCell:(PresentViewCell *)cell operationQueueCompletionOfNumber:(NSInteger)number
{
if (self.dataCaches.count) {
id<PresentModelAble> obj = self.dataCaches.firstObject;
NSArray *objs = [self subarrayWithObj:obj];
__weak typeof(self) ws = self;
[cell showAnimationWithSender:[obj sender] giftName:[obj giftName] prepare:^{
if ([ws.delegate respondsToSelector:@selector(presentView:configCell:sender:giftName:)]) {
[ws.delegate presentView:ws configCell:cell sender:[obj sender] giftName:[obj giftName]];
}
} completion:^(BOOL finished) {
[cell shakeAnimationWithNumber:objs.count];
}];
// [self insertPresentMessages:self.dataCaches completion:self.completion];
}else {
[cell releaseVariable];
}
}
其實這里的處理就重復(fù)流程5轩端。最后緩存處理完了就調(diào)用releaseVariable方法釋放相關(guān)引用內(nèi)存。
至此逝变,整個刷禮物效果的基礎(chǔ)邏輯就以實現(xiàn)了基茵。這里就完了嗎刻撒?當(dāng)然,還沒有耿导!任何一個功能實現(xiàn)之后,沒有經(jīng)過反復(fù)的測試态贤、修改舱呻、優(yōu)化等流程的檢驗,就都不算完成悠汽。
后續(xù)
由于篇幅原因箱吕,關(guān)于這個刷禮物效果功能的優(yōu)化與bug的修改,我會在下一篇文章進行說明柿冲。
最后奉上Demo(優(yōu)化后)的下載地址