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

  • 前段時間公司APP要對直播間的禮物系統進行改版,因爲之前直播的收入不在於禮物分紅,因此之前的禮物系統是很簡單的一個展現而已.爲適應主流直播間的禮物效果,特由此改版!
  • 先奉上 GitHub

1. 全部直播間的禮物系統,第一步用戶看到的無外乎都是禮物的列表界面

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

  • 看着效果還不錯吧.可是可是我忽然發現一個問題.禮物展現的順序跟我想要的順序不同,跟數據的排序也不一致.看圖來講

  • 黃色的順序是咱們想要的順序,可是如今順序確是紅色的.爲何呢?咱們都知道collectionview的滾動方向是有layout控制的.代碼以下
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    layout.itemSize = CGSizeMake(itemW, itemH);
    layout.minimumLineSpacing = 0;
    layout.minimumInteritemSpacing = 0;
    layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;

複製代碼
  • 看代碼以後才明白,由於咱們設置的滾動方向是橫向滾動,因此係統會默認先把垂直方向的Item填充,而後再橫向填充,這就不難解釋爲啥會是這種排序.若是換成垂直滾動呢?

  • 這樣也不知足咱們的需求,既然系統的不行,那麼只有拿出獨門武器,自定義一個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;
}

複製代碼
  • 寫出來以後內心沾沾自喜,這樣應該能夠實現了吧.看看效果吧

  • 應該能夠看出來問題了吧,我選中的那個禮物第一頁和第二頁居然都出現了,我明明設置了分頁滾動的呀.查看層級結構以下

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

複製代碼
  • 可是這樣作真的能夠麼?看看效果吧

  • 到此爲止基本實現了一個主流的禮物列表界面.關於禮物的點擊邏輯看看代碼就能夠了.在此就很少囉嗦了.(詳見代碼 -- JPGiftView)

2. 點擊發送以後的禮物動畫效果展現

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

  • 可是這樣的弊端確定是不少,好比我會將一個用戶送其中一個禮物這樣算成一個完整的實際的禮物.同一個用戶送不一樣的禮物算是第二個完整的禮物.那麼每個完整的禮物都是惟一的存在.若是使用上面的邏輯來處理,那麼你會發現出現各類讓你忍俊不由的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);
            }
        }];
    }];
    
}

複製代碼

//當動畫結束時 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"];
}

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

  • 實現了隊列的效果後,那麼下一步,若是用戶對一個禮物進行連擊操做.該怎麼實現呢?看看如今的連擊是什麼效果吧

  • 這是什麼鬼,這是連擊麼.
  • 看來咱們須要一個管理類來管理禮物的展現邏輯,按照必定的規則建立操做,加入隊列. 這樣 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];
複製代碼
  • 寫到這裏,讓咱們看看如今的效果吧.

  • 總算實現了.準備交工測試的時候,咱們產品又加了一個需求(此處省略點字).讓禮物第一次展現的時候放一個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);
            }
        }];
複製代碼
  • 下面看一個效果

  • 寫到這裏,其實這個功能已經實現了產品的全部需求.咱們項目中使用的也是隻是到這裏的功能.
  • 可是我本身確在想了,如今主流的不都是支持同時顯示兩個禮物的信息麼,那麼該怎麼實現呢.
  • 思考中...
  • 既然一個隊列顯示一個禮物,那麼要顯示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];
        }
    }

複製代碼
  • 效果以下

  • 那麼到這裏,整個結束了.第一次寫這麼長的文章,仍是技術方面.不少不足之處我本身都能感受到.不少都描述不出來而且基礎有點薄弱.不少地方不能特別確定只能笨笨的去用代碼實驗.最終運氣比較好,在工期內完成了這個改版.不足之處,請多多指教.
  • 送上GitHub地址 GitHub
相關文章
相關標籤/搜索