從0開始寫一個直播間的禮物系統

白衣哥發表於2018-04-12
  • 前段時間公司APP要對直播間的禮物系統進行改版,由於以前直播的收入不在於禮物分成,所以以前的禮物系統是很簡單的一個展示而已.為適應主流直播間的禮物效果,特由此改版!
  • 先奉上 GitHub

1. 所有直播間的禮物系統,第一步使用者看到的無外乎都是禮物的列表介面

  • 縱觀主流直播間的禮物列表應該都是使用UICollectionView實現的,所以我也不例外,下面就是各種擼程式碼.效果如下

從0開始寫一個直播間的禮物系統

  • 看著效果還不錯吧.但是但是我突然發現一個問題.禮物展示的順序跟我想要的順序不一樣,跟資料的排序也不一致.看圖來說

從0開始寫一個直播間的禮物系統

  • 黃色的順序是我們想要的順序,但是現在順序確是紅色的.為什麼呢?我們都知道collectionview的滾動方向是有layout控制的.程式碼如下
    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    layout.itemSize = CGSizeMake(itemW, itemH);
    layout.minimumLineSpacing = 0;
    layout.minimumInteritemSpacing = 0;
    layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;

複製程式碼
  • 看程式碼之後才明白,因為我們設定的滾動方向是橫向滾動,所以系統會預設先把垂直方向的Item填充,然後再橫向填充,這就不難解釋為啥會是這種排序.如果換成垂直滾動呢?

從0開始寫一個直播間的禮物系統

  • 這樣也不滿足我們的需求,既然系統的不行,那麼只有拿出獨門武器,自定義一個flowlayout吧.讓它按照我們的要求去滾動,去排序.
- (void)prepareLayout {
    //自定義layout都必須重寫這個方法
    [super prepareLayout];
    
    //設定基本屬性
    CGFloat itemW = SCREEN_WIDTH/4.0;
    CGFloat itemH = itemW*105/93.8;
    self.itemSize = CGSizeMake(itemW, itemH);
    self.minimumLineSpacing = 0;
    self.minimumInteritemSpacing = 0;
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    
    //重新整理後清除所有已佈局的屬性 重新獲取
    [self.cellAttributesArray removeAllObjects];
    
    NSInteger cellCount = [self.collectionView numberOfItemsInSection:0];
    for (NSInteger i = 0; i < cellCount; i++) {
        //取出每一個的Item的佈局.重新賦值
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
        UICollectionViewLayoutAttributes *attibute = [self layoutAttributesForItemAtIndexPath:indexPath];
        NSInteger page = i / 8;//第幾頁
        NSInteger row = i % 4 + page*4;//第幾列
        NSInteger col = i / 4 - page*2;//第幾行
        attibute.frame = CGRectMake(row*itemW, col*itemH, itemW, itemH);
        //儲存所有已經重新賦值的佈局
        [self.cellAttributesArray addObject:attibute];
    }
}

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
    //返回當前可見區域內的已經計算好的佈局
    return self.cellAttributesArray;
}

複製程式碼
  • 寫出來之後心裡沾沾自喜,這樣應該可以實現了吧.看看效果吧

從0開始寫一個直播間的禮物系統

  • 應該可以看出來問題了吧,我選中的那個禮物第一頁和第二頁竟然都出現了,我明明設定了分頁滾動的呀.檢視層級結構如下

從0開始寫一個直播間的禮物系統

  • 原來是可愛的麼麼噠禮物被擠到外面了.由於沒有設定彈簧的效果,所以沒太注意少了一個禮物,那麼原因呢? 想了好久才想起來是不是滾動的範圍不夠,導致麼麼噠不顯示在介面中呢?又去扒了扒怎麼設定自定義的layout的contentoffset.最終找到一個方法.
- (CGSize)collectionViewContentSize{
    
    NSInteger cellCount = [self.collectionView numberOfItemsInSection:0];
    NSInteger page = cellCount / 8 + 1;
    return CGSizeMake(SCREEN_WIDTH*page, 0);
}

複製程式碼
  • 但是這樣做真的可以麼?看看效果吧

從0開始寫一個直播間的禮物系統

  • 到此為止基本實現了一個主流的禮物列表介面.關於禮物的點選邏輯看看程式碼就可以了.在此就不多囉嗦了.(詳見程式碼 -- JPGiftView)

2. 點選傳送之後的禮物動畫效果展示

  • 最簡單的實現就是建立一個View在點選傳送後把當前選中的禮物資訊傳入這個展示禮物效果的view中,寫一個位移的動畫進行展示.如果連送,那麼就在view展示之前計算好一共連擊多少次禮物,然後直接展示x幾.如圖

從0開始寫一個直播間的禮物系統

  • 但是這樣的弊端肯定是很多,比如我會將一個使用者送其中一個禮物這樣算成一個完整的實際的禮物.同一個使用者送不同的禮物算是第二個完整的禮物.那麼每一個完整的禮物都是唯一的存在.如果使用上面的邏輯來處理,那麼你會發現出現各種讓你忍俊不禁的bug,比如,不同禮物的累加,不同禮物會進行頂替正在展示的當前禮物.....
  • 既然知道了bug的存在,那麼怎麼解決呢?首先我腦海中第一個想到的就是強大的佇列,一個蘋果幫我們封裝好的物件導向的類 -- NSOperationQueue .這樣我們就可以將每一個完整的禮物當成一個操作 -- NSOperation .加入佇列中,這樣就會自動按照順序去執行禮物的展示.道理和邏輯都想通了,怎麼實現是需要好好斟酌下咯!
  • 俗話說程式碼是不會騙人的,當我將一個個操作加入到佇列中的時候,又出bug.並沒有按照我們設想的一個個按照排隊的順序去執行.(系統有個依賴方法,但是想了想不太能實現需求,也就沒試)隨後去Google了一下,才知道原來系統提供的API只能加入操作,並不能在上一個操作結束的時候再去執行下一個操作.如果需要按照順序執行,就要自定義一個操作,然後在一個完整禮物禮物動畫展示完成後結束當前操作,那麼才會按順序去執行下一個操作!
  • 具體的程式碼可見 JPGiftOperation類
  • 自定義操作的主要是改變操作的兩個屬性 下圖所示,預設改為NO.使用@synthesize禁止系統的GET/SET,有開發者自己控制
  • 我們需要重寫star方法來建立操作(禮物動畫的展示)
- (void)start {
    
    if ([self isCancelled]) {
        _finished = YES;
        return;
    }
    
    _executing = YES;
    NSLog(@"當前佇列-- %@",self.model.giftName);
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
    
        [self.giftShowView showGiftShowViewWithModel:self.model completeBlock:^(BOOL finished,NSString *giftKey) {
            self.finished = finished;
            if (self.opFinishedBlock) {
                self.opFinishedBlock(finished,giftKey);
            }
        }];
    }];
    
}

複製程式碼

從0開始寫一個直播間的禮物系統

//當動畫結束時 self.finished = YES; 然後手動觸發KVO改變當前操作的狀態
#pragma mark -  手動觸發 KVO
- (void)setExecuting:(BOOL)executing
{
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

- (void)setFinished:(BOOL)finished
{
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

複製程式碼
  • 這樣在動畫結束的時候,我們就能控制當前的操作也結束了.那麼系統會自動去佇列中執行下一個存在的操作.基本實現了佇列的效果.

從0開始寫一個直播間的禮物系統

  • 實現了佇列的效果後,那麼下一步,如果使用者對一個禮物進行連擊操作.該怎麼實現呢?看看現在的連擊是什麼效果吧

從0開始寫一個直播間的禮物系統

  • 這是什麼鬼,這是連擊麼.
  • 看來我們需要一個管理類來管理禮物的展示邏輯,按照一定的規則建立操作,加入佇列. 這樣 JPGiftShowManager類應運而生.
  • 我們需要在拿到當前點選的禮物資訊時,就可以判斷這個禮物的具體該怎麼展示,是排隊等著展示還是在當前展示的禮物的連擊,或者是排隊等待展示的禮物的累加等情況,這樣所有的邏輯都在這個管理類中實現,外部最少可以只需一句程式碼傳入禮物的資料就可以完美的展示一個禮物的動效了.想想就是很好的.
  • 讓我們寫一個展示禮物的方法入口吧,單例就不說了.
/**
 送禮物
 
 @param backView 禮物動效展示父view
 @param giftModel 禮物的資料
 @param completeBlock 展示完畢回撥
 */

- (void)showGiftViewWithBackView:(UIView *)backView
                            info:(JPGiftModel *)giftModel
                   completeBlock:(completeBlock)completeBlock;

複製程式碼
  • 前面說過每一個完整的禮物就是一個唯一的存在,只有相同的完整禮物才會執行連擊或者累加的操作.那麼怎麼區別唯一的禮物呢.我在禮物的Model中放了一個屬性 giftKey 使用禮物名和禮物的ID進行拼接而成(我在實際專案中是使用使用者的ID+禮物ID拼接,這樣肯定可以保證唯一性)
/** 禮物操作的唯一Key */
@property(nonatomic,copy)NSString *giftKey;

//在.m中 自己寫get方法
- (NSString *)giftKey {
    
    return [NSString stringWithFormat:@"%@%@",self.giftName,self.giftId];
}

複製程式碼
  • 那麼這樣的話我們在管理類中還至少需要兩個容器,來儲存已經傳進來的key和已經建立的操作.
/** 操作快取 */
@property (nonatomic,strong) NSCache *operationCache;
/** 當前禮物的key */
@property(nonatomic,strong) NSString *curentGiftKey;

複製程式碼
  • 最終的思路慢慢就確定了,當我們拿到一個新的禮物資料的時候,那麼我們就要判斷禮物的key是否與curentGiftKey相同,禮物的key對應的操作是否在operationCache中.
    if (self.curentGiftKey && [self.curentGiftKey isEqualToString:giftModel.giftKey]) {
        //有當前的禮物資訊
        if ([self.operationCache objectForKey:giftModel.giftKey]) {
            //當前存在操作 那麼就可以在當前操作上累加禮物 出現連擊效果

        }else {
            //當前操作已結束 重新建立
            JPGiftOperation *operation = [JPGiftOperation addOperationWithView:showView OnView:backView Info:giftModel completeBlock:^(BOOL finished,NSString *giftKey) {
                if (self.finishedBlock) {
                    self.finishedBlock(finished);
                }
                //移除操作
                [self.operationCache removeObjectForKey:giftKey];
                //清空唯一key
                self.curentGiftKey = @"";
            }];
            //儲存操作資訊
            [self.operationCache setObject:operation forKey:giftModel.giftKey];
            //操作加入佇列
            [queue addOperation:operation];
        }

    }else {
        //沒有禮物的資訊
        if ([self.operationCache objectForKey:giftModel.giftKey]) {
            //當前存在操作 說明是有禮物在排隊等待展示
        }else {
        //當前第一次展示這個禮物
            JPGiftOperation *operation = [JPGiftOperation addOperationWithView:showView OnView:backView Info:giftModel completeBlock:^(BOOL finished,NSString *giftKey) {
                if (self.finishedBlock) {
                    self.finishedBlock(finished);
                }
                //移除操作
                [self.operationCache removeObjectForKey:giftKey];
                //清空唯一key
                [self.curentGiftKeys removeObject:giftKey];
            }];
            operation.model.defaultCount += giftModel.sendCount;
            //儲存操作資訊
            [self.operationCache setObject:operation forKey:giftModel.giftKey];
            //操作加入佇列
            [queue addOperation:operation];
        }
    }

複製程式碼
  • 可能有的同學疑問了,這個當前禮物的key--self.curentGiftKey怎麼得來的呢? 請看這段程式碼
        [_giftShowView setShowViewKeyBlock:^(JPGiftModel *giftModel) {
            _curentGiftKey = giftModel.giftKey;
        }];

複製程式碼
  • 我在操作的star方法呼叫禮物展示的動畫的時候進行回撥,判斷條件當前第一次展示這個禮物,把key回撥給管理類.
    if (self.showViewKeyBlock && self.currentGiftCount == 0) {
        self.showViewKeyBlock(giftModel);
    }

複製程式碼
  • 這樣我們就可以拿到當前展示的key了.通過判斷是建立新的操作還是進行連擊的邏輯.
  • 雖然邏輯已經有了,但是具體的怎麼實現連擊的效果呢?因為我們的動畫我是在show完之後,使用dispatch_after進行隱藏並移除的.想要實現連擊,首先就要先解決怎麼在連擊的過程中,不會讓禮物展示的動畫結束消失.所以我就想到應該在禮物累加的過程中取消這個延遲執行的方法,取消完之後在建立延遲執行的方法.這樣每一次連擊的時候等於是重新建立了這個隱藏動畫的方法.
  • 最後查了資料使用dispatch_after還無法實現這個需求.找到了一個方法可以實現.只要當前展示的禮物的個數大於1了,就會去執行這個邏輯,取消-建立.如果就一個禮物那麼就按照正常的邏輯取消動畫.
if (self.currentGiftCount > 1) {
        [self p_SetAnimation:self.countLabel];
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hiddenGiftShowView) object:nil];//可以取消成功。
        [self performSelector:@selector(hiddenGiftShowView) withObject:nil afterDelay:animationTime];
        
    }else {
        [self performSelector:@selector(hiddenGiftShowView) withObject:nil afterDelay:animationTime];
    }
複製程式碼
  • 具體的連擊程式碼是通過什麼實現的呢?在展示禮物動畫的view中有兩個屬性.一個傳進來的使用者當前點選所送的禮物總數(此處預設都是1),一個是當前展示的禮物總數.
/** 禮物數 */
@property(nonatomic,assign) NSInteger giftCount;
/** 當前禮物總數 */
@property(nonatomic,assign) NSInteger currentGiftCount;
複製程式碼
  • 什麼時候會發生連擊效果和排隊累加效果呢?
  • 連擊效果 - 當前展示的self.curentGiftKey和拿到的新的禮物的key是一致的並且操作緩衝池中還存在當前key對應的操作.這樣會發生連擊效果.那麼此時我們只需要給giftCount賦值使用者選中的禮物數(當前預設都是一次送一個).
            JPGiftOperation *op = [self.operationCache objectForKey:giftModel.giftKey];
            op.giftShowView.giftCount = giftModel.sendCount;
            
            //限制一次禮物的連擊最大值
            if (op.giftShowView.currentGiftCount >= giftMaxNum) {
                //移除操作
                [self.operationCache removeObjectForKey:giftModel.giftKey];
                //清空唯一key
                self.curentGiftKey = @"";
            }

複製程式碼
  • 讓我們看看賦值之後的具體操作,拿到傳進來的當前的禮物點選數後累加到總禮物數上,然後賦值.是不是看到熟悉的程式碼.沒看錯,延遲隱藏的方法也是在這裡控制的.這樣就實現了連擊的效果.
- (void)setGiftCount:(NSInteger)giftCount {
    
    _giftCount = giftCount;
    self.currentGiftCount += giftCount;
    self.countLabel.text = [NSString stringWithFormat:@"x %zd",self.currentGiftCount];
    NSLog(@"累計禮物數 %zd",self.currentGiftCount);
    if (self.currentGiftCount > 1) {
        [self p_SetAnimation:self.countLabel];
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hiddenGiftShowView) object:nil];//可以取消成功。
        [self performSelector:@selector(hiddenGiftShowView) withObject:nil afterDelay:animationTime];
        
    }else {
        [self performSelector:@selector(hiddenGiftShowView) withObject:nil afterDelay:animationTime];
    }
}

複製程式碼
  • 排隊累加 - 在拿到當前使用者點選的key之後與當前展示禮物的key比較不一樣,但是這個點選的key對應的操作是存在的.那麼就說明這個禮物正在等待展示,那麼我們就要對這個沒有展示的禮物進行累加.我稱之為排隊累加.
            JPGiftOperation *op = [self.operationCache objectForKey:giftModel.giftKey];
            op.model.defaultCount += giftModel.sendCount;
            //限制一次禮物的連擊最大值
            if (op.model.defaultCount >= giftMaxNum) {
                //移除操作
                [self.operationCache removeObjectForKey:giftModel.giftKey];
                //清空唯一key
                self.curentGiftKey = @"";
            }

複製程式碼
  • 不知道有沒有注意到這兩個邏輯處理的不一樣.沒看錯,就是這兩個屬性,一個是賦值,一個累加賦值.defaultCount是我給每一個禮物預設的點選數0.只有點選之後才會進行累加.比如,送了一個累加之後defaultCount就是1,那麼在我第一個展示的時候,禮物右邊的數字就是defaultCount的數值.只有在連擊的時候使用的self.currentGiftCount的數值.
op.giftShowView.giftCount = giftModel.sendCount;
op.model.defaultCount += giftModel.sendCount;
複製程式碼
  • 回頭看下那麼判斷邏輯那,在完全的第一次建立禮物展示時使用的也是defaultCount.
  • 最終在show的方法中還是呼叫了這個方法來展示動畫
        self.currentGiftCount = 0;
        [self setGiftCount:giftModel.defaultCount];
複製程式碼
  • 寫到這裡,讓我們看看現在的效果吧.

從0開始寫一個直播間的禮物系統

  • 總算實現了.準備交工測試的時候,我們產品又加了一個需求(此處省略點字).讓禮物第一次展示的時候放一個gif圖.而且同一個禮物在連擊的時候只展示一次.呀呀呀呀.
  • 這樣就以為可以難倒我了麼.嘿嘿,還記得前面的一個方法麼,現在剛好可以用到了.剛好符合產品的需求,只在第一次展示當前禮物的時候回撥.
    if (self.showViewKeyBlock && self.currentGiftCount == 0) {
        self.showViewKeyBlock(giftModel);
    }

複製程式碼
  • 這樣的話就要改變管理類的方法了,因為我們需要一個回撥告訴控制器,我的禮物開始展示了,你趕緊給我展示gif.
/**
 送禮物

 @param backView 禮物需要展示的父view
 @param giftModel 禮物的資料
 @param completeBlock 回撥
 */
- (void)showGiftViewWithBackView:(UIView *)backView
                            info:(JPGiftModel *)giftModel
                   completeBlock:(completeBlock)completeBlock
       completeShowGifImageBlock:(completeShowGifImageBlock)completeShowGifImageBlock;
複製程式碼
  • 那麼在回撥的方法中我們就直接在調起這個回撥剩下的就讓控制器去處理吧.(各位同學可以酌情使用這個功能)
[_giftShowView setShowViewKeyBlock:^(JPGiftModel *giftModel) {
            _curentGiftKey = giftModel.giftKey;
            if (weakSelf.completeShowGifImageBlock) {
                weakSelf.completeShowGifImageBlock(giftModel);
            }
        }];
複製程式碼
  • 下面看一個效果

從0開始寫一個直播間的禮物系統

  • 寫到這裡,其實這個功能已經實現了產品的所有需求.我們專案中使用的也是隻是到這裡的功能.
  • 但是我自己確在想了,現在主流的不都是支援同時顯示兩個禮物的資訊麼,那麼該怎麼實現呢.
  • 思考中...
  • 既然一個佇列顯示一個禮物,那麼要顯示2個或者更多是不是需要更多的佇列去展示呢?那麼就試一試吧.
  • 兩個佇列,兩個可以展示動畫的view,還有key不在是NSString ,變成一個陣列,以便放下當前展示的兩個禮物的key.
/** 佇列 */
@property(nonatomic,strong) NSOperationQueue *giftQueue1;
@property(nonatomic,strong) NSOperationQueue *giftQueue2;
/** showgift */
@property(nonatomic,strong) JPGiftShowView *giftShowView1;
@property(nonatomic,strong) JPGiftShowView *giftShowView2;
/** 操作快取 */
@property (nonatomic,strong) NSCache *operationCache;
/** 當前禮物的keys */
@property(nonatomic,strong) NSMutableArray *curentGiftKeys;
複製程式碼
  • 只需要在建立操作加入佇列的時候判斷當前哪個佇列中的運算元比較少,那麼就將新建立的操作加入到這個佇列中等待展示.全部流程程式碼如下.
- (void)showGiftViewWithBackView:(UIView *)backView info:(JPGiftModel *)giftModel completeBlock:(completeBlock)completeBlock completeShowGifImageBlock:(completeShowGifImageBlock)completeShowGifImageBlock {
    
    self.completeShowGifImageBlock = completeShowGifImageBlock;
    
    if (self.curentGiftKeys.count && [self.curentGiftKeys containsObject:giftModel.giftKey]) {
        //有當前的禮物資訊
        if ([self.operationCache objectForKey:giftModel.giftKey]) {
            //當前存在操作
            JPGiftOperation *op = [self.operationCache objectForKey:giftModel.giftKey];
            op.giftShowView.giftCount = giftModel.sendCount;
            
            //限制一次禮物的連擊最大值
            if (op.giftShowView.currentGiftCount >= giftMaxNum) {
                //移除操作
                [self.operationCache removeObjectForKey:giftModel.giftKey];
                //清空唯一key
                [self.curentGiftKeys removeObject:giftModel.giftKey];
            }

        }else {
            NSOperationQueue *queue;
            JPGiftShowView *showView;
            if (self.giftQueue1.operations.count <= self.giftQueue2.operations.count) {
                queue = self.giftQueue1;
                showView = self.giftShowView1;
            }else {
                queue = self.giftQueue2;
                showView = self.giftShowView2;
            }

            //當前操作已結束 重新建立
            JPGiftOperation *operation = [JPGiftOperation addOperationWithView:showView OnView:backView Info:giftModel completeBlock:^(BOOL finished,NSString *giftKey) {
                if (self.finishedBlock) {
                    self.finishedBlock(finished);
                }
                //移除操作
                [self.operationCache removeObjectForKey:giftKey];
                //清空唯一key
                [self.curentGiftKeys removeObject:giftKey];
            }];
            operation.model.defaultCount += giftModel.sendCount;
            //儲存操作資訊
            [self.operationCache setObject:operation forKey:giftModel.giftKey];
            //操作加入佇列
            [queue addOperation:operation];
        }

    }else {
        //沒有禮物的資訊
        if ([self.operationCache objectForKey:giftModel.giftKey]) {
            //當前存在操作
            JPGiftOperation *op = [self.operationCache objectForKey:giftModel.giftKey];
            op.model.defaultCount += giftModel.sendCount;
            
            //限制一次禮物的連擊最大值
            if (op.model.defaultCount >= giftMaxNum) {
                //移除操作
                [self.operationCache removeObjectForKey:giftModel.giftKey];
                //清空唯一key
                [self.curentGiftKeys removeObject:giftModel.giftKey];
            }

        }else {
            NSOperationQueue *queue;
            JPGiftShowView *showView;
            if (self.giftQueue1.operations.count <= self.giftQueue2.operations.count) {
                queue = self.giftQueue1;
                showView = self.giftShowView1;
            }else {
                queue = self.giftQueue2;
                showView = self.giftShowView2;
            }

            JPGiftOperation *operation = [JPGiftOperation addOperationWithView:showView OnView:backView Info:giftModel completeBlock:^(BOOL finished,NSString *giftKey) {
                if (self.finishedBlock) {
                    self.finishedBlock(finished);
                }
                //移除操作
                [self.operationCache removeObjectForKey:giftKey];
                //清空唯一key
                [self.curentGiftKeys removeObject:giftKey];
            }];
            operation.model.defaultCount += giftModel.sendCount;
            //儲存操作資訊
            [self.operationCache setObject:operation forKey:giftModel.giftKey];
            //操作加入佇列
            [queue addOperation:operation];
        }
    }

複製程式碼
  • 效果如下

從0開始寫一個直播間的禮物系統

  • 那麼到這裡,整個結束了.第一次寫這麼長的文章,還是技術方面.很多不足之處我自己都能感覺到.很多都描述不出來並且基礎有點薄弱.很多地方不能特別肯定只能笨笨的去用程式碼實驗.最終運氣比較好,在工期內完成了這個改版.不足之處,請多多指教.
  • 送上GitHub地址 GitHub

相關文章