tableview 展示小视频

Tips:这次的内容分为两篇文章讲述
01、[iOS]仿微博视频边下边播之封装播放器 讲述如何封装一个实现了边下边播并且缓存的视频播放器。
02、[iOS]仿微博视频边下边播之滑动TableView自动播放 讲述如何实现在tableView中滑动播放视频,并且是流畅,不阻塞线程,没有任何卡顿的实现滑动播放视频。同时也将讲述当tableView滚动时,以什么样的策略,来确定究竟哪一个cell应该播放视频。

微博视频的特点:

  • 秒拍团队主要致力于视频处理,微博的视频播放功能是由秒拍提供技术支持的。微博的视频一般都是不限时长的,所以它的特点是边下边播。
  • 说到视频播放就不能不提微信的短视频,微信的短视频限制时长为15秒,经过微信团队处理后,一个短视频的体积能控制在2MB以内。所以微信的视频是先下载,再读取下载好的视频文件进行播放,也就是所谓的先下后播。这个功能,微信的同行已经把源码分享出来了,在这里。
    效果图
    这个列表视频边下边播包含以下主要的功能点:

  • 01.必须是边下边播。

  • 02.如果缓存好的视频是完整的,就要把这个视频保存起来,下次再次加载这个视频的时候,就先检查本地有没有缓存好的视频。这一点对于节省用户流量,提升用户体验很重要。要实现这一点,也就是说,我们要手动干预系统播放器加载数据的内部实现,这个细节后面再讲。
  • 03.不阻塞线程,不卡顿,滑动如丝顺滑,这是保证用户体验最重要的一点。
  • 04.当tableView滚动时,以什么样的策略,来确定究竟哪一个cell应该播放视频。

可能你着急赶项目,只想尽快的把这个功能集成到你的项目,那么请你直接去 Github 上下载源码。需要说明的是,我上面说的功能点的第一和第二点,不用你关心,我已经帮你处理封装好了。但是,第三和第四点,需要你自己结合你自己的项目来定制,我只提供了模板和巨细无比的注释。

第一、AVPlayer基本使用?

首先从最基本的封装播放器开始。

01、AVPlayer?

AVPlayer播放视频需要涉及以下几个类:

  • AVURLAsset,是AVAsset的子类,负责网络连接,请求数据。
  • AVPlayerItem,会建立媒体资源动态视角的数据模型并保存AVPlayer播放资源的状态。说白了,就是数据管家。
  • AVPlayer,播放器,将数据解码处理成为图像和声音。
  • AVPlayerLayer,图像层,AVPlayer的图像要通过AVPlayerLayer呈现。

需要注意的是,AVPlayer的模式是,你不要主动调用play方法播放视频,而是等待AVPlayerItem告诉你,我已经准备好播放了,你现在可以播放了,所以我们要监听AVPlayerItem的状态,通过添加监听者的方式获取AVPlayerItem的状态:

// 添加监听
[_currentPlayerItem addObserver:self forKeyPath:@”status” options:NSKeyValueObservingOptionNew context:nil];

在监听结果中处理播放逻辑。当监听到播放器已经准备好播放的时候,就可以调用play方法。
注意点:如果视频还没准备好播放,你就把AVPlayerLayer图层添加到cell上,那么在播放器还没有准备好播放之前,负责显示的图像的图层会变成黑色,直到准备好播放,拿到数据,才会出现画面。这在列表中自动播放是应该极力避免的。所以,要等待播放器有图像输出的时候再添加显示的预览图层到cell上。

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
  if ([keyPath isEqualToString:@"status"]) {
      AVPlayerItem *playerItem = (AVPlayerItem *)object;
      AVPlayerItemStatus status = playerItem.status;
      switch (status) {
          case AVPlayerItemStatusUnknown:{

          }
              break;
          case AVPlayerItemStatusReadyToPlay:{
              [self.player play];
              self.player.muted = self.mute;
              // 显示图像逻辑
              [self handleShowViewSublayers];

          }
              break;
          case AVPlayerItemStatusFailed:{

          }
              break;
          default:
              break;
      }
  }
}

到这里就可以播放一个网络或者本地视频了。但是,在播放过程中:建立连接–>请求数据–>统筹数据–>数据解码–>输出图像和声音,这些过程都是AVFoundation框架下,我上面列举的那些类自动帮我们完成的。
系统处理.png

要实现边下边播,并实现缓存功能,就必须拿到播放器的数据,也就是必须手动干预数据加载的过程。我们需要在网络层和解码层中间,插入一个我们自己需要的功能块,也就是我下图中的红色模块。
手动干预.png

02、AVAssetResourceLoaderDelegate?

  • 要实现在播放器请求中插入自己的模块的功能,我们需要借助于AVAssetResourceLoaderDelegate。我们用到的AVURLAsset下有一个AVAssetResourceLoader属性。

@property (nonatomic, readonly) AVAssetResourceLoader *resourceLoader;

  • 这个AVAssetResourceLoader是负责数据加载的,最最重要的是我们只要遵守了AVAssetResourceLoaderDelegate,就可以成为它的代理,成为它的代理以后,数据加载都会通过代理方法询问我们。这样,我们就找到切入口干预数据的加载了。

-(BOOL)resourceLoader:(AVAssetResourceLoader )resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest )loadingRequest;
-(void)resourceLoader:(AVAssetResourceLoader )resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest )loadingRequest;

  • 在正式进入数据干预之前,我们先看一个很重要的东西。我们知道视频数据都是容量巨大的连续媒体数据,所以请求数据的时候,我们要将请求策略置为streaming。这个策略的含义是,将容量巨大的连续媒体数据进行分段,分割为数量众多的小文件进行传递。

  • (NSURL )getSchemeVideoURL:(NSURL )url{
    // NSURLComponents用来替代NSMutableURL,可以readwrite修改URL。这里通过更改请求策略,将容量巨大的连续媒体数据进行分段
    // 分割为数量众多的小文件进行传递。采用了一个不断更新的轻量级索引文件来控制分割后小媒体文件的下载和播放,可同时支持直播和点播
    NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
    components.scheme = @”streaming”;
    return [components URL];
    }

第二、手动干预系统播放器加载数据?

01、如何使用NSURLSession来下载大文件?

在NSURLSession之前,大家都是使用NSURLConnection。如今在Xcode7中,NSURLConnection已经成为过期的类目了,我们常用的AFNNetwork也彻底抛弃了NSURLConnection,转向NSURLSession。现在看一下怎么使用NSURLSession:

// 替代NSMutableURL, 可以动态修改scheme
NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
actualURLComponents.scheme = @”http”;

// 创建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[actualURLComponents URL] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20.0];

// 修改请求数据范围
if (offset > 0 && self.videoLength > 0) {
[request addValue:[NSString stringWithFormat:@”bytes=%ld-%ld”,(unsigned long)offset, (unsigned long)self.videoLength - 1] forHTTPHeaderField:@”Range”];
}

// 重置
[self.session invalidateAndCancel];

// 创建Session,并设置代理
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];

// 创建会话对象
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request];

// 开始下载
[dataTask resume];

我们可以在NSURLSession的代理方法中获得下载的数据,拿到下载的数据以后,我们使用NSOutputStream,将数据写入到硬盘中存放临时文件的文件夹。在请求结束的时候,我们判断是否成功下载好文件,如果下载成功,就把这个文件转移到我们的存储成功文件的文件夹。如果下载失败,就把临时数据删除。

// 1.接收到服务器响应的时候
-(void)URLSession:(NSURLSession )session dataTask:(NSURLSessionDataTask )dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler;

// 2.接收到服务器返回数据的时候调用,会调用多次
-(void)URLSession:(NSURLSession )session dataTask:(NSURLSessionDataTask )dataTask didReceiveData:(NSData *)data;

// 3.请求结束的时候调用(成功|失败),如果失败那么error有值
-(void)URLSession:(NSURLSession )session task:(NSURLSessionTask )task didCompleteWithError:(NSError *)error;

02、AVAssetResourceLoader的代理?

为了更好的封装性和可维护性,新建一个文件,让这个文件负责和系统播放器对接数据。上面说到,只要这个文件遵守了AVAssetResourceLoaderDelegate协议,他就有资格代理系统播放器请求数据。并且系统会通过

-(BOOL)resourceLoader:(AVAssetResourceLoader )resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest )loadingRequest;

这个代理方法,把下载请求loadingRequest传给我们。拿到请求以后,首先把请求用一个数组保存起来。为什么要用数组保存起来?因为,当我们拿到请求去下载数据,到数据下载好,这个过程需要的时间是不确定的。

拿到请求以后,我们就需要调用上面封装的NSURLSession下载器来下载文件。

  • (void)dealLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest{
    NSURL *interceptedURL = [loadingRequest.request URL];
    NSRange range = NSMakeRange(loadingRequest.dataRequest.currentOffset, MAXFLOAT);

    if (self.manager) {
    if (self.manager.downLoadingOffset > 0)
    [self processPendingRequests];

    // 如果新的rang的起始位置比当前缓存的位置还大300k,则重新按照range请求数据
    if (self.manager.offset + self.manager.downLoadingOffset + 1024*300 < range.location
        // 如果往回拖也重新请求
        || self.manager.offset > range.location) {
        [self.manager setUrl:interceptedURL offset:range.location];
    }
    

    }
    else{
    self.manager = [JPDownloadManager new];
    self.manager.delegate = self;
    [self.manager setUrl:interceptedURL offset:0];
    }
    }

如果文件有下载好,就去检查下载好的数据长度有没有满足请求数据需要的长度,如果满足,就从硬盘的临时文件中取出对应的数据,并把这段数据填充给请求,然后把这个请求从请求列表数组中移除。播放器拿到了这段数据,就可以开始解码播放了。

// 判断此次请求的数据是否处理完全, 和填充数据
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest{
// 请求起始点
long long startOffset = dataRequest.requestedOffset;

// 当前请求点
if (dataRequest.currentOffset != 0)
    startOffset = dataRequest.currentOffset;

// 播放器拖拽后大于已经缓存的数据
if (startOffset > (self.manager.offset + self.manager.downLoadingOffset))
    return NO;

// 播放器拖拽后小于已经缓存的数据
if (startOffset < self.manager.offset)
    return NO;

NSData *fileData = [NSData dataWithContentsOfFile:_videoPath options:NSDataReadingMappedIfSafe error:nil];

NSInteger unreadBytes = self.manager.downLoadingOffset - self.manager.offset - (NSInteger)startOffset;
NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);

[dataRequest respondWithData:[fileData subdataWithRange:NSMakeRange((NSUInteger)startOffset- self.manager.offset, (NSUInteger)numberOfBytesToRespondWith)]];

long long endOffset = startOffset + dataRequest.requestedOffset;

BOOL didRespondFully = (self.manager.offset + self.manager.downLoadingOffset) >= endOffset;

return didRespondFully;

}

至此,手动干预播放视频的流程就走完了。已经可以正常播放视频了。
JPVideoPlayer.png

03、加载缓存数据逻辑?

接下来要做的就是实现,当下次播放同一个视频的时候,先去检查硬盘里有没有这个文件的缓存。借助于NSFileManager,我们可以查找指定的路径有没有存在指定的文件,从而判断有没有缓存可以启用。

NSFileManager *manager = [NSFileManager defaultManager];
NSString *savePath = [self fileSavePath];
savePath = [savePath stringByAppendingPathComponent:self.suggestFileName];
if ([manager fileExistsAtPath:savePath]) {
// 已经存在这个下载好的文件了
return;
}

至此,播放器封装完毕。
我将在下一篇文章 [iOS]仿微博视频边下边播之滑动TableView自动播放 ,讲述如何实现在tableView中滑动播放视频,并且是流畅,不阻塞线程,没有任何卡顿的实现滑动播放视频。同时也将讲述当tableView滚动时,以什么样的策略,来确定究竟哪一个cell应该播放视频。

tableView 显示Video

上篇文章讲述了封装一个边下边播,并且带有缓存功能的播放器。如果你还没有看,请点击跳转[iOS]仿微博视频边下边播之封装播放器 。接下来,讲述如何将这个播放器应用到tableView里。并且达到如下效果。
JPVideoPlayer.gif

01、dispatch_semaphore信号量?

dispatch_semaphore 信号量基于计数器的一种多线程同步机制。在多个线程访问共有资源时候,会因为多线程的特性而引发数据出错的问题。

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

// “创建方法里会传入一个long型的参数,这个东西你可以想象是一个库存”
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

NSMutableArray *array = [NSMutableArray array];

for (int index = 0; index < 10000; index++) {
dispatch_async(queue, ^(){
// “每运行一次,会先清一个库存,如果库存为0,那么根据传入的等待时间,决定等待增加库存的时间
//如果设置为DISPATCH_TIME_FOREVER,那么意思就是永久等待增加库存,否则就永远不往下面走”
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    NSLog(@"addd :%d", index);         

    [array addObject:[NSNumber numberWithInt:index]];

    // 每运行一次,增加一个库存
    dispatch_semaphore_signal(semaphore);
});

}

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
如果semaphore计数大于等于1.计数-1,返回,程序继续运行。
如果计数为0,则等待。
这里设置的等待时间是一直等待。

dispatch_semaphore_signal(semaphore);
计数+1.
在这两句代码中间的执行代码,每次只会允许一个线程进入,这样就有效的保证了在多线程环境下,只能有一个线程进入。

  • AVPlayer底层是有信号量等待的特性的。具体表现在,“AVPlayer的replaceCurrentItemWithPlayerItem(用来切换视频的)方法在切换视频时底层会调用信号量等待,然后导致当前线程卡顿,如果在UITableViewCell中切换视频播放使用这个方法,会导致当前线程冻结几秒钟。” 这里说的线程是UI线程,即主线程,主线程冻结的结果就是主线程阻塞,带来卡顿和不流畅。

  • 你可能会想,那就不要在主线程切换视频,不就不卡顿主线程了吗?如果你这么做,那你就不能保证视频播放是及时响应的。同时,因为子线程你不知道什么时候调用,你也不能保证你能及时关闭不需要播放的视频。也就是说,如果基于以上思路,当你滑动tableView时,可能会出现多个cell同时播放视频,而且会出现你要播的播不了,你想掐死的掐不死。

02、切换视频解决方案?

在tableView里播放视频,当用户滑动时,肯定是频繁切换视频的。上面讲了使用AVPlayer自带的replaceCurrentItemWithPlayerItem来切换视频带来的问题,我们得出的结论是:

  • 不能使用replaceCurrentItemWithPlayerItem方法切换视频。
  • 不能在子线程切换视频。

解决方案一

当出现这种问题的时候,我只能跑到官方文档去找答案了。

@interface AVQueuePlayer : AVPlayer

我在官方文档里找到AVQueuePlayer,他是AVPlayer的一个子类,他会自己维护一个播放队列。并且提供方法,可以插入播放条目,也可以移除播放条目,然后切换视频。

// 创建AVQueuePlayer
NSArray *items = <#An array of player items#>;
AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];

// 插入item
- (void)insertItem:(AVPlayerItem )item afterItem:(nullable AVPlayerItem )afterItem;

// 移除item
- (void)removeItem:(AVPlayerItem *)item;

// 切换视频
- (void)advanceToNextItem;

这个类还是很好使的,可以在不卡顿主线程的情况下流畅切换视频。但是他有他的坑,是我多次试验以后发现的,就是重播视频的时候,如果你放在那里不动,他大概会重复播放十次左右,然后播放器就莫名其妙的死掉了,这个时候你没有办法重新唤醒他。至于什么原因导致的,我暂时还没有找到。如果你知道,请你务必在下面留言,让更多人看到。

解决方案二

上面的那个方案被我否了,接下来,我采取的是尝试每次切换视频的时候都重新创建播放器,重新建立网络请求。总之,就是所有的配置都重新创建。刚开始的时候,我担心这样会造成处理器负担,但是实际使用起来,发现并没有任何性能问题。但是前提是,在重新创建之前,把所有的请求释放掉,同时把之前的播放器也释放,还有预览图层也释放。

03、重播?

先来看一下AVFoundation下用来表示时间的结构体CMTime,AVFoundation下的时间刻度是以最精准的分数形式来表示的。他有两个重要的值,value表示分子,timescale表示分母。

typedef struct
{
CMTimeValue value; // 分子
CMTimeScale timescale; // 分母
CMTimeFlags flags;
CMTimeEpoch epoch;
} CMTime;

比如说我们要表示视频的起点,就是0秒,那就可以写成CMTimeMake(0, 1)。

我们的播放器是支持自动重播的,所以我们要在系统的通知中心监听播放器播放完成的通知,在接收到通知后,进行对应的处理。

// 监听播放结束通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidPlayToEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];

  • (void)playerItemDidPlayToEnd:(NSNotification *)notification{
    // 重复播放, 从起点开始重播, 没有内存暴涨
    __weak typeof(self) weak_self = self;
    [self.player seekToTime:CMTimeMake(0, 1) completionHandler:^(BOOL finished) {
    __strong typeof(weak_self) strong_self = weak_self;
    if (!strong_self) return;
    [strong_self.player play];
    }];
    }

04、滑动tableView自动播放?

首先是一启动,应该自动去可见cell中查找第一个需要播放视频的cell,如果找到就开始播放。

-(void)playVideoInVisiableCells{

NSArray *visiableCells = [self.tableView visibleCells];

// 在可见cell中找到第一个有视频的cell
JPVideoPlayerCell *videoCell = nil;
for (JPVideoPlayerCell *cell in visiableCells) {
if (cell.videoPath.length > 0) {
videoCell = cell;
break;
}
}

// 如果找到了, 就开始播放视频
if (videoCell) {
self.playingCell = videoCell;
self.currentVideoPath = videoCell.videoPath;
JPVideoPlayer *player = [JPVideoPlayer sharedInstance];
[player playWithUrl:[NSURL URLWithString:videoCell.videoPath] showView:videoCell.containerView];
player.mute = YES;
}
}

接下来就是滚动tableView的时候,播放视频。在做之前,首先,我们要先制定一个规则来确定,滚动的时候究竟哪一个cell应该播放视频。我画了一张图,一起来看一下。
播放规则.png

我的规则是:当tableView滑动的时候,我会播放可见cell中,最靠近屏幕中心的那个cell的视频。如果最靠近屏幕中心的那个cell没有视频,就会按照这个规则去其他可见cell中找,如果都没有找到,就不播放视频。规则有了,接下来就是去实现。其实,实现的时候,我们应该换一个思路,就是,只有那个cell需要播放视频,才会参与筛选是否是最靠近屏幕中心的cell。这样就避免了递归查找。

-(void)handleScroll{

// 找到下一个要播放的cell(最在屏幕中心的)
JPVideoPlayerCell *finnalCell = nil;
NSArray *visiableCells = [self.tableView visibleCells];
NSMutableArray *indexPaths = [NSMutableArray array];
CGFloat gap = MAXFLOAT;
for (JPVideoPlayerCell *cell in visiableCells) {

[indexPaths addObject:cell.indexPath];

if (cell.videoPath.length > 0) { // 如果这个cell有视频
  CGPoint coorCentre = [cell.superview convertPoint:cell.center toView:nil];
  CGFloat delta = fabs(coorCentre.y-[UIScreen mainScreen].bounds.size.height*0.5);
  if (delta < gap) {
      gap = delta;
      finnalCell = cell;
    }
  }

}

// 注意, 如果正在播放的cell和finnalCell是同一个cell, 不应该在播放
if (finnalCell != nil && self.playingCell != finnalCell) {
[[JPVideoPlayer sharedInstance]stop];
[[JPVideoPlayer sharedInstance]playWithUrl:[NSURL URLWithString:finnalCell.videoPath] showView:finnalCell.containerView];
self.playingCell = finnalCell;
self.currentVideoPath = finnalCell.videoPath;
[JPVideoPlayer sharedInstance].mute = YES;
return;
}

// 再看正在播放视频的那个cell移出视野, 则停止播放
BOOL isPlayingCellVisiable = YES;
if (![indexPaths containsObject:self.playingCell.indexPath]) {
isPlayingCellVisiable = NO;
}
if (!isPlayingCellVisiable && self.playingCell) {
[self stopPlay];
}

}

这里没有难点,唯一可以讲一下的就是坐标之间的转换。我们拿到的cell的中心点的坐标是tableView的坐标,但是我们计算各个中心点离屏幕中心点之间的距离,用的是屏幕Window的坐标,所以要先将这个中心点的坐标转换为屏幕的坐标,再进行计算。但是,你也可以不转换坐标,因为他们是同一个坐标系的(tableView的坐标系)。可是,我个人的编程习惯是先转换,再计算。因为我觉得会比较清晰一点。尤其是当我们做复杂的过渡动画的时候,有这个意识,你会发现条理会很清晰。

05、什么时候播?

你肯定告诉我,滑动的时候播。这个回答是正确的,但是也是不正确的,因为我们尝试用编程的思想来思考这个问题。tableView的滚动过程分为两种情况:

  • 将要开始拖动 –> 开始拖动 –> 滚动 –> 松手 –> 静止 –> 结束
  • 将要开始拖动 –> 开始拖动 –> 滚动 –> 松手 –> 开始减速 –> 减速完成 –> 静止 –> 结束

首先要肯定的是,不能在滚动的时候调用视频播放的逻辑。这一点应该没有异议。原因是,第一,这个方法会来很多很多次,而且从实现上来说,不可能一调用滚动的代理就实现播放。第二从用户角度来说,在滑动的时候,他自己也没有决定要看哪一个cell。所以在滚动时,我们不做反应。

其次是开始拖动时,也不要作反应,因为,这个时候作反应没有意义。松手的时候,因为有松手时静止和松手时滚动两种情况,所以我不做处理。

逐渐排除下来,最后,适合调用播放逻辑的,只剩下“静止”这个动作了。我们来看一下静止对应的代理方法:

// 松手时已经静止,只会调用scrollViewDidEndDragging
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
if (decelerate == NO) { // scrollView已经完全静止
[self handleScroll];
}
}

// 松手时还在运动, 先调用scrollViewDidEndDragging,在调用scrollViewDidEndDecelerating
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
// scrollView已经完全静止
[self handleScroll];
}

做到这里,我们已经能够实现tableView在滑动的时候,流畅的播放视频了。

06、现有的问题(bug)?

很多有经验的老司机,应该已经看出来问题了。问题就是,我们现有的规则是:

  • 在可见cell中,播放最靠近屏幕中心的cell的视频。
  • 当tableView滑动静止的时候调用视频播放逻辑。

不可及cell.png

还记得这张图吗?仔细想一下,按照我们上面的规则,cell01是永远不可能在静止的时候成为离屏幕中心最近那个cell的(也有例外,那就是其他三个cell都没有要播放视频。但是,我们的程序不能有设计缺陷)。同样的,底部也有一个这样的cell,不能滚动到屏幕中心。我把这样的cell叫做“滑动不可及cell”,下文都会以这个词来称呼归类这一类cell。

所以,我在上面的两点规则上加了一条:

  • 如果“滑动不可及cell”完整出现在视野,那么优先播放“滑动不可及cell”的视频,注意,这里说得是“滑动不可及cell完整出现在视野”,注意点是“完整出现”。

有了规则就依照这个规则来解决。首先,我们面对的第一个问题是,我怎么知道我的列表里有几个这样的cell?不知道,没关系,我们可以实际测量。我以iPhone6s为样机,进行了测量,我这里的测量前提是,我们的cell上的视频尺寸和cell的尺寸是一致的。我的测量结果如下:

每屏cell个数 4 3 2
滑动不可及cell个数 1 1 0

我这里需要说明的是,我不可能知道你项目的具体需求,但是,如果你的实际需求和我文章中的不一样,那你根据我现有思路进行更改就可以了。

接下来继续,根据以上的分析,我首先把测量结果保存到一个字典中,以每屏可见cell的最大个数为key, 对应的不能播放视频的cell为value。以后,你只要根据行高和屏幕高度这两个值算出每屏有多少cell,就能取出有多少个cell是滑动不可及cell。

-(NSDictionary *)dictOfVisiableAndNotPlayCells{
// 以每屏可见cell的最大个数为key, 对应的不能播放视频的cell为value
// 只有每屏可见cell数在3以上时,才会出现滑动时有cell的视频永远播放不到
// 以下值都是实际测量得到
if (!_dictOfVisiableAndNotPlayCells) {
_dictOfVisiableAndNotPlayCells = @{
@”4” : @”1”,
@”3” : @”1”,
};
}
return _dictOfVisiableAndNotPlayCells;
}

其次,我把cell归为三个类型,我用一个枚举来表示:

// 播放滑动不可及cell的类型
typedef NS_ENUM(NSUInteger, PlayUnreachCellStyle) {
PlayUnreachCellStyleUp = 1, // 顶部不可及
PlayUnreachCellStyleDown = 2, // 底部不可及
PlayUnreachCellStyleNone = 3 // 播放滑动可及cell
};

当每个cell来到cellForRowAtIndexPath方法的时候,我就根据cell的row数和最大不可及cell数,给每个cell打一个标签。

-(UITableViewCell )tableView:(UITableView )tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

JPVideoPlayerCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseID forIndexPath:indexPath];

if (self.maxNumCannotPlayVideoCells > 0) {
    if (indexPath.row <= self.maxNumCannotPlayVideoCells-1) {
        cell.cellStyle = PlayUnreachCellStyleUp;
    }
    else if (indexPath.row >= self.listArr.count-self.maxNumCannotPlayVideoCells){
        cell.cellStyle = PlayUnreachCellStyleDown;
    }
    else{
        cell.cellStyle = PlayUnreachCellStyleNone;
    }
}

return cell;

}

在播放逻辑里加入这些代码,用来维护我上面加的那条规则:

  // 优先查找滑动不可及cell
    if (cell.cellStyle != PlayUnreachCellStyleNone) {
        // 并且不可及cell要全部露出
        if (cell.cellStyle == PlayUnreachCellStyleUp) {
            CGPoint cellLeftUpPoint = cell.frame.origin;
            // 不要在边界上
            cellLeftUpPoint.y += 1;
            CGPoint coorPoint = [cell.superview convertPoint:cellLeftUpPoint toView:nil];
            CGRect windowRect = self.view.window.bounds;
            BOOL isContain = CGRectContainsPoint(windowRect, coorPoint);
            if (isContain) {
                finnalCell = cell;
            }
        }
        else if (cell.cellStyle == PlayUnreachCellStyleDown){
            CGPoint cellLeftUpPoint = cell.frame.origin;
            cellLeftUpPoint.y += cell.bounds.size.height;
            // 不要在边界上
            cellLeftUpPoint.y -= 1;
            CGPoint coorPoint = [cell.superview convertPoint:cellLeftUpPoint toView:nil];
            CGRect windowRect = self.view.window.bounds;
            BOOL isContain = CGRectContainsPoint(windowRect, coorPoint);
            if (isContain) {
                finnalCell = cell;
            }
        }
    }

到这里为止,我们的播放逻辑基本上没有问题了。

07、真的没有问题了?

其实还是有问题的,就问题就是,当你快速滑动的时候,会出现cell循环利用的图像错误。可以想象,在快速滑动的时候,我们没有做任何处理,上个cell的视频图像在cell移出视野时没有清除,那当这个cell被循环利用的时候,就会把上个cell的图像带到下一个cell,这样就会有显示问题。

其实解决方案很简单,就是当cell移出视野,把对应的图层去掉,播放器释放。

// 快速滑动循环利用问题
-(void)handleQuickScroll{

if (!self.playingCell) return;

NSArray *visiableCells = [self.tableView visibleCells];

NSMutableArray *indexPaths = [NSMutableArray array];
for (JPVideoPlayerCell *cell in visiableCells) {
    [indexPaths addObject:cell.indexPath];
}

BOOL isPlayingCellVisiable = YES;
if (![indexPaths containsObject:self.playingCell.indexPath]) {
    isPlayingCellVisiable = NO;
}

// 当前播放视频的cell移出视线, 或者cell被快速的循环利用了, 都要移除播放器
if (!isPlayingCellVisiable || ![self.playingCell.videoPath isEqualToString:self.currentVideoPath]) {
    [self stopPlay];
}

}

好的,真的没有问题了

原文地址 : http://www.jianshu.com/p/3946317760a6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值