仿映客刷禮物效果---基本邏輯實(shí)現(xiàn)

最近做了個(gè)直播項(xiàng)目,需要用到彈幕和刷禮物。在網(wǎng)上找了許多開源代碼,發(fā)現(xiàn)都不是很適合自己的項(xiàng)目需求,于是利用空余時(shí)間將兩個(gè)功能都實(shí)現(xiàn)下,這里分享出來,供大家一起學(xué)習(xí)。(關(guān)于彈幕的實(shí)現(xiàn),大家可以參考我前面寫的一篇文章IOS 自定義彈幕實(shí)現(xiàn))

在開始我的實(shí)現(xiàn)方案之前,大家可以先參看下這篇文章iOS 基于 IM 實(shí)現(xiàn)仿映客刷禮物連擊效果,寫得很好。Demo中關(guān)于禮物連乘的動(dòng)畫效果,就是引用其中。但這位大神所用的緩存邏輯特別復(fù)雜,所以在控制緩存的時(shí)候有些小BUG,為了彌補(bǔ)這個(gè)缺陷,于是我就開始了這篇文章。

實(shí)現(xiàn)功能

程序最終實(shí)現(xiàn)的效果圖如下:

效果圖.png

這里的禮物1、2、3、4四個(gè)按鈕分別模擬四個(gè)人發(fā)的四種禮物,點(diǎn)擊一次,就代表發(fā)送一條禮物消息,實(shí)現(xiàn)邏輯功能如下:

  1. 點(diǎn)擊禮物1按鈕,會(huì)出現(xiàn)一個(gè)倒計(jì)時(shí)按鈕,并發(fā)送一條禮物消息
  2. 再次點(diǎn)擊倒計(jì)時(shí)按鈕,會(huì)再次發(fā)送一條禮物消息,禮物數(shù)量累加
  3. 在倒計(jì)時(shí)的時(shí)間內(nèi),倒計(jì)時(shí)按鈕如果沒有收到點(diǎn)擊事件,倒計(jì)時(shí)按鈕會(huì)隱藏,并且當(dāng)前用來展示禮物動(dòng)畫的cell也會(huì)隱藏
  4. 如果當(dāng)前全部的cell都在展示,這時(shí)你點(diǎn)擊了其他類型的禮物按鈕,這時(shí)的禮物消息會(huì)被緩存,等到當(dāng)前禮物動(dòng)畫執(zhí)行完時(shí),再去執(zhí)行緩存的
  5. 如果在短時(shí)間內(nèi)多次點(diǎn)擊連送按鈕,連乘動(dòng)畫也會(huì)緩存

實(shí)現(xiàn)界面功能如下:

  1. 可自定義cell樣式
  2. 可自定義cell的展示和隱藏動(dòng)畫
  3. 可監(jiān)聽cell的點(diǎn)擊事件

下面是我實(shí)現(xiàn)這些功能的基本邏輯與實(shí)現(xiàn)代碼。

基本邏輯

開始寫代碼之前,將功能的基本邏輯列出,這是個(gè)很好的習(xí)慣。特別是對(duì)于那些復(fù)雜的功能,這個(gè)習(xí)慣就顯得尤為重要。
功能要求:收到消息展示動(dòng)畫、可連乘、可緩存

功能要求看起好像很簡(jiǎn)單,如果真的實(shí)現(xiàn)可就不是那么容易了。

具體邏輯如下:

邏輯流程圖.png
  1. 收到一條禮物消息
  2. 檢測(cè)當(dāng)前是否有相同類型的禮物消息正在展示動(dòng)畫
  3. 有,將該消息加入到當(dāng)前的動(dòng)畫組中
  4. 沒有,檢測(cè)當(dāng)前是否有空閑的軌道用于展示動(dòng)畫
  5. 有,取出緩存中相同類型的消息,開始執(zhí)行動(dòng)畫
  6. 沒有,將當(dāng)前消息加入到消息緩存中
  7. 連乘動(dòng)畫完成,如果3秒內(nèi)沒有收到新的同類消息,就執(zhí)行隱藏動(dòng)畫
  8. 隱藏動(dòng)畫完成,再取緩存,開始下一個(gè)動(dòng)畫,直至無緩存為止

理清楚了邏輯之后,下面就是代碼實(shí)現(xiàn)了,真正的痛苦現(xiàn)在才開始!

實(shí)現(xiàn)代碼

這里我會(huì)根據(jù)邏輯順序來介紹代碼的實(shí)現(xiàn)。在收到一條消息之后,在外面只需調(diào)用PresentView對(duì)象的insertPresentMessages:接口方法,將消息插入進(jìn)來,insertPresentMessages:接口實(shí)現(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];
                    }
                }];
            }
        }
    }
}

這基本就是整個(gè)邏輯的實(shí)現(xiàn)了,看到這一堆的邏輯判斷,是不是感覺頭都大了。沒關(guān)系,接下來我會(huì)對(duì)這個(gè)代碼進(jìn)行一步步的解析。

首先,調(diào)用了PresentView對(duì)象的checkElementOfModels:方法,這就是數(shù)據(jù)檢測(cè)。

數(shù)據(jù)檢測(cè)

數(shù)據(jù)檢測(cè)就是對(duì)插入的模型數(shù)組進(jìn)行過濾,因?yàn)槲覀冃枰ㄟ^這個(gè)模型來確定消息的類型(禮物類型是通過發(fā)送者和發(fā)送的禮物名來確定的),所以自定義的消息模型必須要遵守PresentModelAble協(xié)議,協(xié)議要求如下:

@required
@property (copy, nonatomic) NSString *sender;
@property (copy, nonatomic) NSString *giftName;

故檢測(cè)數(shù)據(jù),其實(shí)就是檢測(cè)模型數(shù)組中的元素是否遵守了PresentModelAble協(xié)議,并剔除其中沒有遵守協(xié)議的數(shù)據(jù)。具體實(shí)現(xiàn)代碼如下:

- (NSArray *)checkElementOfModels:(NSArray<id<PresentModelAble>> *)models
{
    NSMutableArray *siftArray = [NSMutableArray array];
    for (id obj in models) {
        if (![obj conformsToProtocol:@protocol(PresentModelAble)]) {
            DebugLog(@"%@對(duì)象沒有遵守PresentModelAble協(xié)議", obj);
        }else {
            [siftArray addObject:obj];
        }
    }
    return siftArray;
}

選出合適的數(shù)據(jù)之后,就是遍歷該數(shù)組,對(duì)每一個(gè)元素進(jìn)行動(dòng)畫檢測(cè)。

動(dòng)畫檢測(cè)

動(dòng)畫檢測(cè)就是檢測(cè)當(dāng)前是否有相同類型的消息正在展示動(dòng)畫,即流程2。這里是遍歷當(dāng)前所有的cell,如果cell上展示的消息類型與該消息類型一致,并且cell的動(dòng)畫正在執(zhí)行,就返回該cell,否則返回nil。具體實(shí)現(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)前正在展示動(dòng)畫
            if (cell.state != AnimationStateNone) return cell;
        }
    }
    return nil;
}

如果動(dòng)畫檢測(cè)檢測(cè)到匹配的cell,就在cell當(dāng)前的動(dòng)畫隊(duì)列上添加一個(gè)連乘動(dòng)畫(shakeAnimation)任務(wù)。

ShakeAnimation

連乘動(dòng)畫就是在當(dāng)前禮物數(shù)量上做一個(gè)累加的動(dòng)畫,即流程3。連乘動(dòng)畫的具體實(shí)現(xiàn),大家可以自行查看demo中PresentLable對(duì)象的startAnimationDuration:completion:方法。因?yàn)檫B乘動(dòng)畫執(zhí)行完,三秒后沒有收到新的任務(wù)就要開始cell的隱藏動(dòng)畫(hiddenAnimation)。所以開始連乘動(dòng)畫前,需要取消掉前面延時(shí)三秒執(zhí)行的隱藏動(dòng)畫任務(wù),然后重新開始延時(shí),即流程7。具體實(shí)現(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

隱藏動(dòng)畫實(shí)現(xiàn)代碼也非常簡(jiǎn)單,這里就不介紹了。但是需要注意的地方是,隱藏動(dòng)畫執(zhí)行完成后需要將cell恢復(fù)到初始狀態(tài),保證cell在開始下一次展示動(dòng)畫之前不會(huì)因?yàn)闋顟B(tài)的錯(cuò)誤而導(dǎo)致流程判斷出錯(cuò)。

如果動(dòng)畫檢測(cè)沒有檢測(cè)到匹配的cell,就開始cell的檢測(cè)

cell檢測(cè)

cell檢測(cè)就是檢測(cè)當(dāng)前是否有空閑的cell用于展示禮物消息動(dòng)畫,即流程4.這里只需要遍歷所有的cell,判斷cell的動(dòng)畫狀態(tài)就可以了。具體實(shí)現(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)畫,就將當(dāng)前消息加入到緩存中,即添加到dataCaches這個(gè)數(shù)組中,即流程6。
如果有空閑cell用于展示,就從空閑cell數(shù)組中取出一個(gè)cell,執(zhí)行cell的展示動(dòng)畫(showAnimation),即流程5。

ShowAnimation

cell的展示動(dòng)畫,其接口如下:

/**
 *  顯示cell動(dòng)畫
 *
 *  @param sender     發(fā)送者
 *  @param name       禮物名
 *  @param prepare    準(zhǔn)備動(dòng)畫回調(diào)
 *  @param completion 動(dòng)畫完成回調(diào)
 */
- (void)showAnimationWithSender:(NSString *)sender
                       giftName:(NSString *)name
                        prepare:(void (^)(void))prepare
                     completion:(void (^)(BOOL finished))completion;

因?yàn)檎故緞?dòng)畫是用來展示禮物消息的動(dòng)畫,在展示之前需要知道展示的禮物消息的類型,所以需要傳入sender和name兩個(gè)參數(shù)。
展示動(dòng)畫完成之后,就需要從緩存中取出與該消息相同類型的消息,然后開始連乘動(dòng)畫,這些操作就可以早completion中完成。那prepare回調(diào)是干嘛呢?
其實(shí)這里還有一個(gè)問題:在開始cell的展示動(dòng)畫之前,我們就需要給這個(gè)cell設(shè)置需要展示的數(shù)據(jù)??墒莄ell是自定義的,這是根本無法拿到自定義的cell,也就無法給這個(gè)cell設(shè)置數(shù)據(jù)?
這里我的思路是:在開始cell的展示動(dòng)畫之前,就是prepare回調(diào)中,給外界一個(gè)代理通知,讓外面來執(zhí)行這個(gè)賦值操作。代理通知接口如下:

/**
 *  禮物動(dòng)畫即將展示的時(shí)調(diào)用,根據(jù)禮物消息類型為自定義的cell設(shè)置對(duì)應(yīng)的模型數(shù)據(jù)用于展示
 *
 *  @param cell        用來展示動(dòng)畫的cell
 *  @param sender      禮物發(fā)送者
 *  @param name        禮物名
 */
- (void)presentView:(PresentView *)presentView
               configCell:(PresentViewCell *)cell
             sender:(NSString *)sender
           giftName:(NSString *)name;

到這里,關(guān)于收到一條消息的流程就就全部處理完了。最后一步就是對(duì)緩存邏輯進(jìn)行處理了,即上面的流程8。

緩存處理

緩存處理就是當(dāng)一個(gè)禮物消息類型的動(dòng)畫處理完,即cell的隱藏動(dòng)畫執(zhí)行完成,就要從緩存中取下一個(gè)類型的禮物消息,開始下一組動(dòng)畫,直至無緩存為止,即流程8。這里的隱藏動(dòng)畫回調(diào)是通過代理實(shí)現(xiàn)的,具體實(shí)現(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];
    }
}

其實(shí)這里的處理就重復(fù)流程5。最后緩存處理完了就調(diào)用releaseVariable方法釋放相關(guān)引用內(nèi)存。
至此,整個(gè)刷禮物效果的基礎(chǔ)邏輯就以實(shí)現(xiàn)了。這里就完了嗎?當(dāng)然,還沒有!任何一個(gè)功能實(shí)現(xiàn)之后,沒有經(jīng)過反復(fù)的測(cè)試、修改、優(yōu)化等流程的檢驗(yàn),就都不算完成。

后續(xù)

由于篇幅原因,關(guān)于這個(gè)刷禮物效果功能的優(yōu)化與bug的修改,我會(huì)在下一篇文章進(jìn)行說明。

最后奉上Demo(優(yōu)化后)的下載地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,071評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,409評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,360評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,895評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,123評(píng)論 0 286
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,643評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,559評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,742評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評(píng)論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,981評(píng)論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評(píng)論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,354評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,707評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容

  • 上一篇文章《仿映客刷禮物效果---基本邏輯實(shí)現(xiàn)》中,分析了刷禮物效果的基本流程與具體實(shí)現(xiàn)代碼。但還有一些BUG和一...
    Rasping閱讀 2,442評(píng)論 26 18
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,147評(píng)論 4 61
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,593評(píng)論 25 707
  • 你出生7300天后,01:29,你20歲了,你也是大人了,從認(rèn)識(shí)到成為大人,你都沒有在改變,而數(shù)分鐘之后,你真的會(huì)...
    歐哈娜七封閱讀 228評(píng)論 0 0
  • 如果不是先看了《刻意練習(xí)》估計(jì)我看不懂這本《學(xué)習(xí)之道》。這本書是曾經(jīng)叱咤象棋界以及太極拳界的Josh Waitzk...
    一盎司閱讀 283評(píng)論 0 0