- 前段时间公司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改变当前操做的状态
- (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