下面要说的就是短视频软件开发重中之重,仿抖音滑动播放视频的实现。
当我们首次进入播放短视频页面时,会优先判断当前的视频列表videoList是否有值,如果没有值或当前的视频的index大于videoList.count - 3 时,就会重新请求服务端,获取新的一组短视频。
下面时核心代码
if (!_videoList || _videoList.count == 0) {
_isHome = YES;
_currentIndex = 0;
_pages = 1;
self.videoList = [NSMutableArray array];
[self requestMoreVideo];//请求数据并加载页面
}
if (_currentIndex>=_videoList.count-3) {
_pages += 1;
[self requestMoreVideo];//请求数据并加载页面
}
- (void)requestMoreVideo {
WeakSelf;
[YBNetworking postWithUrl:url Dic:nil Suc:^(NSDictionary *data, NSString *code, NSString *msg) {
if ([code isEqual:@"0"]) {
NSArray *info = [data valueForKey:@"info"];
if (_pages==1) {
[_videoList removeAllObjects];
}
[_videoList addObjectsFromArray:info];
if (_isHome == YES) {
_isHome = NO;
_scrollViewOffsetYOnStartDrag = -100;
[weakSelf scrollViewDidEndScrolling];//加载页面
}
}
} Fail:^(id fail) {
}];
}
结下来我们要介绍加载页面的三种情况,这里我们会用到三个UIImageView,为firstImageView、secondImageView,thirdImageView,对应三个展示UI的View,分别为firstFront、secondFront、thirdFront,对应三个数据源lastHostDic、hostdic、nextHostDic:
第一种是刚进来currentIndex == 0(currentIndex是指当前滚动到第几个视频),这时候我们要设置UIScrollView的ContentOffset为(0,0), currentPlayerIV(当前UIIMageView)为firstImageView,currentFront(当前呈现UI的View)为firstFront。并且要预加载secondImageView的数据,这里不用处理thirdImageView,因为只能向下滑,不需要预加载thirdImageView并且滚到第二个的时候自然给第三个赋值:
//第一个
[self.backScrollView setContentOffset:CGPointMake(0, 0) animated:NO];
_currentPlayerIV = _firstImageView;
_currentFront = _firstFront;
/**
* _currentIndex=0时,重新处理下_secondImageView的封面、
* 不用处理_thirdImageView,因为滚到第二个的时候上面的判断自然给第三个赋值
*/
[_firstImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_hostdic valueForKey:@"thumb"])]];
[self setUserData:_hostdic withFront:_firstFront];
[self setVideoData:_hostdic withFront:_firstFront];
[_secondImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_nextHostDic valueForKey:@"thumb"])]];
[self setUserData:_nextHostDic withFront:_secondFront];
[self setVideoData:_nextHostDic withFront:_secondFront];
这里的setUerData和setVideoData是给页面加载数据的,详细实现为:
-(void)setUserData:(NSDictionary *)dataDic withFront:(FrontView*)front{
NSDictionary *musicDic = [dataDic valueForKey:@"musicinfo"];
id userinfo = [dataDic valueForKey:@"userinfo"];
NSString *dataUid;
NSString *dataIcon;
NSString *dataUname;
if ([userinfo isKindOfClass:[NSDictionary class]]) {
dataUid = [NSString stringWithFormat:@"%@",[userinfo valueForKey:@"id"]];
dataIcon = [NSString stringWithFormat:@"%@",[userinfo valueForKey:@"avatar"]];//右边最上面的带➕的头像图片
dataUname = [NSString stringWithFormat:@"@%@",[userinfo valueForKey:@"user_nicename"]];//左下角第一行@的作者名
}else{
dataUid = @"0";
dataIcon = @"";
dataUname = @"";
}
NSString *musicID = [NSString stringWithFormat:@"%@",[musicDic valueForKey:@"id"]];
NSString *musicCover = [NSString stringWithFormat:@"%@",[musicDic valueForKey:@"img_url"]];
//musicIV右下角转动的唱片上覆盖的歌曲背景图片
if ([musicID isEqual:@"0"]) {
[front.musicIV sd_setImageWithURL:[NSURL URLWithString:_hosticon]];
}else{
[front.musicIV sd_setImageWithURL:[NSURL URLWithString:musicCover]];
}
[front setMusicName:[NSString stringWithFormat:@"%@",[musicDic valueForKey:@"music_format"]]];
front.titleL.text = [NSString stringWithFormat:@"%@",[dataDic valueForKey:@"title"]];//左下角滚动的文字
front.nameL.text = dataUname;
[front.iconBtn sd_setBackgroundImageWithURL:[NSURL URLWithString:dataIcon] forState:UIControlStateNormal placeholderImage:[UIImage imageNamed:@"default_head.png"]];
//广告
NSString *is_ad_str = [NSString stringWithFormat:@"%@",[dataDic valueForKey:@"is_ad"]];
NSString *ad_url_str = [NSString stringWithFormat:@"%@",[dataDic valueForKey:@"ad_url"]];
CGFloat ad_img_w = 0;
if (![PublicObj checkNull:ad_url_str]&&[is_ad_str isEqual:@"1"]&&![PublicObj checkNull:front.titleL.text]) {
NSString *att_text = [NSString stringWithFormat:@"%@ ",front.titleL.text];
UIImage *ad_link_img = [UIImage imageNamed:@"广告-详情"];
NSMutableAttributedString *att_img = [NSMutableAttributedString yy_attachmentStringWithContent:ad_link_img contentMode:UIViewContentModeCenter attachmentSize:CGSizeMake(13, 13) alignToFont:SYS_Font(15) alignment:YYTextVerticalAlignmentCenter];
NSMutableAttributedString *title_att = [[NSMutableAttributedString alloc]initWithString:att_text];
//NSLog(@"-==-:%@==:%@==img:%@",att_text,title_att,att_img);
[title_att appendAttributedString:att_img];
NSRange click_range = [[title_att string] rangeOfString:[att_img string]];
title_att.yy_font = SYS_Font(15);
title_att.yy_color = [UIColor whiteColor];
title_att.yy_lineBreakMode = NSLineBreakByTruncatingHead;
title_att.yy_kern = [NSNumber numberWithFloat:0.2];
[title_att addAttribute:NSBackgroundColorAttributeName value:[UIColor clearColor] range:click_range];
[title_att yy_setTextHighlightRange:click_range color:[UIColor clearColor] backgroundColor:[UIColor clearColor] tapAction:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
//[YBMsgPop showPop:@"1111111"];
[self adJump:ad_url_str];
}];
front.titleL.preferredMaxLayoutWidth =_window_width*0.75;
front.titleL.attributedText = title_att;
ad_img_w = 30;
}
//计算名称长度 最长3行高度最大60
CGSize titleSize = [front.titleL.text boundingRectWithSize:CGSizeMake(_window_width*0.75, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:SYS_Font(15)} context:nil].size;
CGFloat title_h = titleSize.height>60?60:titleSize.height;
CGFloat title_w = _window_width*0.75;//titleSize.width>=(_window_width*0.75)?titleSize.width:titleSize.width+ad_img_w;
front.titleL.frame = CGRectMake(0, front.musicL.top-title_h, title_w, title_h);
front.nameL.frame = CGRectMake(0, front.titleL.top-25, front.botView.width, 25);
front.followBtn.frame = CGRectMake(front.iconBtn.left+12, front.iconBtn.bottom-13, 26, 26);
//广告
if ([is_ad_str isEqual:@"1"]) {
front.adLabel.hidden = NO;
front.adLabel.frame = CGRectMake(0, front.nameL.top-25, 45, 20);
}else{
front.adLabel.hidden = YES;
}
}
-(void)setVideoData:(NSDictionary *)videoDic withFront:(FrontView*)front{
_shares =[NSString stringWithFormat:@"%@",[videoDic valueForKey:@"shares"]];
_likes = [NSString stringWithFormat:@"%@",[videoDic valueForKey:@"likes"]];
_islike = [NSString stringWithFormat:@"%@",[videoDic valueForKey:@"islike"]];
_comments = [NSString stringWithFormat:@"%@",[videoDic valueForKey:@"comments"]];
NSString *isattent = [NSString stringWithFormat:@"%@",[NSString stringWithFormat:@"%@",[videoDic valueForKey:@"isattent"]]];
//_steps = [NSString stringWithFormat:@"%@",[info valueForKey:@"steps"]];
WeakSelf;
//dispatch_async(dispatch_get_main_queue(), ^{
//点赞数 评论数 分享数
if ([weakSelf.islike isEqual:@"1"]) {
[front.likebtn setImage:[UIImage imageNamed:@"home_zan_sel"] forState:0];
//weakSelf.likebtn.userInteractionEnabled = NO;
} else{
[front.likebtn setImage:[UIImage imageNamed:@"home_zan"] forState:0];
//weakSelf.likebtn.userInteractionEnabled = YES;
}
[front.likebtn setTitle:[NSString stringWithFormat:@"%@",_likes] forState:0];
front.likebtn = [PublicObj setUpImgDownText:front.likebtn];
[front.enjoyBtn setTitle:[NSString stringWithFormat:@"%@",_shares] forState:0];
front.enjoyBtn = [PublicObj setUpImgDownText:front.enjoyBtn];
[front.commentBtn setTitle:[NSString stringWithFormat:@"%@",_comments] forState:0];
front.commentBtn = [PublicObj setUpImgDownText:front.commentBtn];
if ([[Config getOwnID] isEqual:weakSelf.hostid] || [isattent isEqual:@"1"]) {
front.followBtn.hidden = YES;
}else{
[front.followBtn setImage:[UIImage imageNamed:@"home_follow"] forState:0];
front.followBtn.hidden = NO;
[front.followBtn.layer addAnimation:[PublicObj followShowTransition] forKey:nil];
}
//});
}
第二种是当你用手滑动的时候,currentIndex > 0 并且小于videoList.count - 1(即既不是第一个也不是最后一个视频),这时候会优先触发代理方法:
#pragma mark - scrollView delegate
//开始拖拽
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
lastContenOffset = scrollView.contentOffset.y;
//NSLog(@"111=====%f",scrollView.contentOffset.y);
_currentPlayerIV.jp_progressView.hidden = YES;//当前播放进度隐藏
self.scrollViewOffsetYOnStartDrag = scrollView.contentOffset.y;//记录开始拖拽的contentoffset
}
//结束拖拽
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
willDecelerate:(BOOL)decelerate {
endDraggingOffset = scrollView.contentOffset.y;//记录结束拖拽的位置
//NSLog(@"222=====%f",scrollView.contentOffset.y);
if (decelerate == NO) {
[self scrollViewDidEndScrolling];
}
}
//开始减速
-(void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
scrollView.scrollEnabled = NO;
//NSLog(@"333=====%f",scrollView.contentOffset.y);
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
//NSLog(@"currentIndex=====%.2f",scrollView.contentSize.height);
}
//结束减速
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
scrollView.scrollEnabled = YES;
//NSLog(@"444=====%f",scrollView.contentOffset.y);
if (lastContenOffset < scrollView.contentOffset.y && (scrollView.contentOffset.y-lastContenOffset)>=_window_height) {
NSLog(@"=====向上滚动=====");
_currentIndex++;
if (_currentIndex>_videoList.count-1) {
_currentIndex =_videoList.count-1;
}
}else if(lastContenOffset > scrollView.contentOffset.y && (lastContenOffset-scrollView.contentOffset.y)>=_window_height){
NSLog(@"=====向下滚动=====");
_currentIndex--;
if (_currentIndex<0) {
_currentIndex=0;
}
}else{
NSLog(@"=======本页拖动未改变数据=======");
if (scrollView.contentOffset.y == 0 && _currentIndex==0) {
[YBMsgPop showPop:@"已经到顶了哦^_^"];
}else if (scrollView.contentOffset.y == _window_height*2 && _currentIndex==_videoList.count-1){
[YBMsgPop showPop:@"没有更多了哦^_^"];
}
}
_currentPlayerIV.jp_progressView.hidden = NO;
[self scrollViewDidEndScrolling];
if (_requestUrl) {
if (_currentIndex>=_videoList.count-3) {
_pages += 1;
[self requestMoreVideo];
}
}
}
#pragma mark - Private
- (void)scrollViewDidEndScrolling {
if((self.scrollViewOffsetYOnStartDrag == self.backScrollView.contentOffset.y) && (endDraggingOffset!= _scrollViewOffsetYOnStartDrag)){
return;
}
//NSLog(@"7-8==%f====%f",self.scrollViewOffsetYOnStartDrag,self.backScrollView.contentOffset.y);
[self changeRoom];
}
这时当scrollview 滑动自动触发翻页时,则让UIScrollView迅速复位,这时候我们要设置UIScrollView的ContentOffset为(0,_window_height), _window_height为当前屏幕大小,currentPlayerIV(当前UIIMageView)为secondImageView,currentFront(当前呈现UI的View)为secondFront。并且要预加载firstImageView,firstFront和thirdImageView,thirdFront数据的。代码如下:
[_secondImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_hostdic valueForKey:@"thumb"])]];//这里设置视频的第一帧,用于在整个页面显示
[self setUserData:_hostdic withFront:_secondFront];
[self setVideoData:_hostdic withFront:_secondFront];
[self.backScrollView setContentOffset:CGPointMake(0, _window_height) animated:NO];
_currentPlayerIV = _secondImageView;
_currentFront = _secondFront;
if (_curentIndex>0) {
_lastHostDic = _videoList[_curentIndex-1];
[_firstImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_lastHostDic valueForKey:@"thumb"])]];
[self setUserData:_lastHostDic withFront:_firstFront];
[self setVideoData:_lastHostDic withFront:_firstFront];
}
if (_curentIndex < _videoList.count-1) {
_nextHostDic = _videoList[_curentIndex+1];
[_thirdImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_nextHostDic valueForKey:@"thumb"])]];
[self setUserData:_nextHostDic withFront:_thirdFront];
[self setVideoData:_nextHostDic withFront:_thirdFront];
}
第三种情况是滚动到最后一个,这时候我们要设置UIScrollView的ContentOffset为(0,_window_height*2), _window_height为当前屏幕大小,currentPlayerIV(当前UIIMageView)为thirdImageView,currentFront(当前呈现UI的View)为thirdFront。并且要预加载secondImageView,secondFront数据的。代码如下:
//最后一个
[self.backScrollView setContentOffset:CGPointMake(0, _window_height*2) animated:NO];
_currentPlayerIV = _thirdImageView;
_currentFront = _thirdFront;
/**
* _currentIndex=_videoList.count-1时,重新处理下_secondImageView的封面、
* 这个时候只能上滑 _secondImageView 给 _lastHostDic的值
*/
[_secondImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_lastHostDic valueForKey:@"thumb"])]];
[self setUserData:_lastHostDic withFront:_secondFront];
[self setVideoData:_lastHostDic withFront:_secondFront];
[_thirdImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_hostdic valueForKey:@"thumb"])]];
[self setUserData:_hostdic withFront:_thirdFront];
[self setVideoData:_hostdic withFront:_thirdFront];
当三种情况都介绍完后就涉及到短视频软件开发最终的播放了,这里我使用的是JPVideoPlayer播放器(完全开源的,有兴趣的可以自行下载研究原理),播放的主要代码如下:
//切记一定要先把当前播放的上一个关闭
[_currentPlayerIV jp_stopPlay];
//开始播放
[_currentPlayerIV jp_playVideoMuteWithURL:[NSURL URLWithString:_playUrl]
bufferingIndicator:[JPBufferView new]
progressView:[JPLookProgressView new]
configuration:^(UIView *view, JPVideoPlayerModel *playerModel) {
view.jp_muted = NO;//播放器的音频输出是否静音
_firstWatch = YES;
if (_currentPlayerIV.image.size.width>0 && (_currentPlayerIV.image.size.width >= _currentPlayerIV.image.size.height)) {
playerModel.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
}else{
playerModel.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
}
}];
下面有几个需要设置的重要代理
1)实现重复播放
//return返回NO以防止重播视频。 返回YES则重复播放视频。如果未实施,则默认为YES
- (BOOL)shouldAutoReplayForURL:(nonnull NSURL *)videoURL
{
return YES;
}
2)播放状态改变时需要做的相应处理,主要是页面消失的时候停止播放
//播放状态改变的时候触发
-(void)playerStatusDidChanged:(JPVideoPlayerStatus)playerStatus {
NSLog(@"=====7-8====%lu",(unsigned long)playerStatus);
if (_stopPlay == YES) {
NSLog(@"8-4:play-停止了");
_stopPlay = NO;
_firstWatch = NO;
//页面已经消失了,就不要播放了
[_currentPlayerIV jp_stopPlay];
}
if (playerStatus == JPVideoPlayerStatusPlaying) {
if (_bufferIV) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[_bufferIV removeFromSuperview];
});
}
}
if (playerStatus == JPVideoPlayerStatusReadyToPlay && _firstWatch==YES) {
//addview
}
if (playerStatus == JPVideoPlayerStatusStop && _firstWatch == YES) {
//finish
_firstWatch = NO;
}
}