Skip to content

UITableView及UICollectionView中播放的解决方案v2

changsanjiang edited this page Apr 28, 2021 · 4 revisions

以下内容已过期, 请参考最新版本(SJBaseVideoPlayer >= 3.6.3):

以下内容已弃用

操作步骤:

  1. 在Cell中添加一个视图, 该视图遵守了SJPlayModelPlayerSuperview协议.
  2. 设置播放资源时, 需传入SJPlayModel, 传入tableView及对应的indexPath.

详细介绍

在UITableView及UICollectionView中播放时, 因为存在某个Cell复用的情况, 我们一般需要播放器视图能够及时的隐藏和显示. 当我们手动添加一个播放器到Cell中时, 这个Cell可能会被复用到其他的位置, 这会导致播放器也随着Cell一起变动位置, 因为本身播放器就在Cell中, Cell在哪儿, 它就在哪儿.

为解决这个问题, 我在v1的版本中采用了视图tag的方式, 为播放器父视图添加一个tag, 通过tag定位父视图. 但是在实际使用中, 会发现新手容易头晕, 不知道为啥要如此配置, 出于此原因, 我提出了v2版本的解决方案, 希望这个方案比v1时的要容易上手吧. 对了, v1版本的虽然现在也可以使用, 但我已标识为过期方法, 后续的版本升级可能会删除这些过期的API.


播放一个资源之前, 需要将其URL及所处的视图层次, 传递给播放器管理类, 以便其能够适时的控制显示与隐藏, 播放与暂停等操作.

我们先从播放资源的创建看起, 播放的资源是通过SJVideoPlayerURLAsset创建的. 它由两部分组成:

  • 资源地址 (URL, 可以是本地文件/URL/AVAsset)
  • 视图层次链 (SJPlayModel)

创建SJVideoPlayerURLAsset, 赋值给播放器后即可播放.

    _player.URLAsset = [[SJVideoPlayerURLAsset alloc] initWithURL:URL playModel:[SJPlayModel playModelWithTableView:_tableView indexPath:indexPath]];

我们浏览一下SJVideoPlayerURLAsset接口定义.

@interface SJVideoPlayerURLAsset : NSObject<SJMediaModelProtocol>
- (nullable instancetype)initWithURL:(NSURL *)URL startPosition:(NSTimeInterval)startPosition playModel:(__kindof SJPlayModel *)playModel;
- (nullable instancetype)initWithURL:(NSURL *)URL startPosition:(NSTimeInterval)startPosition;
- (nullable instancetype)initWithURL:(NSURL *)URL playModel:(__kindof SJPlayModel *)playModel;
- (nullable instancetype)initWithURL:(NSURL *)URL;

/// 开始播放的位置, 单位秒
@property (nonatomic) NSTimeInterval startPosition;

/// 试用结束的位置, 单位秒
@property (nonatomic) NSTimeInterval trialEndPosition;

@property (nonatomic, strong, null_resettable) SJPlayModel *playModel;
- (id<SJVideoPlayerURLAssetObserver>)getObserver;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
@property (nonatomic, readonly) BOOL isM3u8;
@end

URL以及startPosition顾名思义, 而playModel是做什么的? 为什么要传入一个SJPlayModel对象?

  • SJPlayModel是用来解决在视图被复用的场景下(UITableViewCell或者UICollectionViewCell被复用时), 使管理类能够精确的定位播放器视图, 确定是否应该在当前复用的视图上显示与隐藏播放器视图.
  • 同时在滚动进入或离开屏幕时, 需要控制播放或暂停操作.

举个复用场景的例子:

一个cell开始时所处的位置index为0, 离开屏幕后回到复用池, 当它又被复用添加到其他位置时, 例如index变为了3, 这时候我们一般都是刷新UI以使它显示3位置对应的数据.

同样的, 我们不可能在每个cell上都添加一个播放器(这会非常消耗资源的, 并且仔细想想也会面临复用的问题), 因为同时播放的视频只有一个, 我们只需在vc里创建一个播放器复用它即可, 管理类需要一些参数知道此时是否是原来的位置, 以确定是否应该在当前复用的视图上显示播放器视图, 并进行播放或暂停等后续操作, 这就是为什么要传入一个SJPlayModel对象的原因.


到了这里, 你可能会想如何定位当前具体位置, 如何将播放器视图添加到父视图中的, 我们浏览一下SJPlayModel的接口定义:

/// 用于标识: 播放器父视图. 父视图需遵守该协议. 将来播放器视图会被管理类自动添加到此视图中.
@protocol SJPlayModelPlayerSuperview

@end

/// 用于标识: 嵌套的视图. 在嵌套场景中, 嵌套的视图需遵守该协议. 管理类将通过这条链一层一层找到父视图.
/// 例如: UITableViewCell 中内嵌的一个 UICollectionView<SJPlayModelNestedView>, 播放器将来要在 UICollectionViewCell 中的某个视图上播放.
///      由于`tableView`以及`collectionView`都存在复用的情况, 因此需要添加该标记建立视图层次链. 管理类通过这条链来定位具体位置.
@protocol SJPlayModelNestedView

@end


@interface SJPlayModel: NSObject

#pragma mark - UIView

- (instancetype)init;
 
#pragma mark - UITableView

/// - UITableView
///     - UITableViewCell
///         - PlayerSuperview<SJPlayModelPlayerSuperview>
///             - player
+ (instancetype)playModelWithTableView:(__weak UITableView *)tableView indexPath:(NSIndexPath *)indexPath;

/// - UITableView
///     - UITableView.TableHeaderView
///         - PlayerSuperview<SJPlayModelPlayerSuperview>
///             - player
+ (instancetype)playModelWithTableView:(__weak UITableView *)tableView tableHeaderView:(__weak UIView *)tableHeaderView;

/// - UITableView
///     - UITableView.TableFooterView
///         - PlayerSuperview<SJPlayModelPlayerSuperview>
///             - player
+ (instancetype)playModelWithTableView:(__weak UITableView *)tableView tableFooterView:(__weak UIView *)tableFooterView;

/// - UITableView
///     - UITableViewSectionHeaderView
///         - PlayerSuperview<SJPlayModelPlayerSuperview>
///             - player
+ (instancetype)playModelWithTableView:(__weak UITableView *)tableView inHeaderForSection:(NSInteger)section;

/// - UITableView
///     - UITableViewSectionFooterView
///         - PlayerSuperview<SJPlayModelPlayerSuperview>
///             - player
+ (instancetype)playModelWithTableView:(__weak UITableView *)tableView inFooterForSection:(NSInteger)section;


#pragma mark - UICollectionView

/// - UICollectionView
///     - UICollectionViewCell
///         - PlayerSuperview<SJPlayModelPlayerSuperview>
///             - player
+ (instancetype)playModelWithCollectionView:(__weak UICollectionView *)collectionView indexPath:(NSIndexPath *)indexPath;

/// - UICollectionView
///     - UICollectionElementKindSectionHeader
///         - PlayerSuperview<SJPlayModelPlayerSuperview>
///             - player
+ (instancetype)playModelWithCollectionView:(UICollectionView *__weak)collectionView inHeaderForSection:(NSInteger)section;

/// - UICollectionView
///     - UICollectionElementKindSectionFooter
///         - PlayerSuperview<SJPlayModelPlayerSuperview>
///             - player
+ (instancetype)playModelWithCollectionView:(UICollectionView *__weak)collectionView inFooterForSection:(NSInteger)section;


#pragma mark - 视图嵌套情况下使用

/**
 嵌套链.  当存在视图嵌套情况时, 可以通过该方法与`nextPlayModel`建立视图层次链接
 
 1. - UITableView
      - UITableViewHeaderView
          - UICollectionView<SJPlayModelNestedView>
              - UICollectionViewCell
                  - PlayerSuperview<SJPlayModelPlayerSuperview>
                      - player
 
 \code
 SJPlayModel *one = [SJPlayModel playModelWithCollectionView:collectionView indexPath:cellIndexPath];
 one.nextPlayModel = [SJPlayModel playModelWithTableView:tableView tableHeaderView:tableHeaderView];
 \endcode
 
 2. - UITableView
      - UITableViewCell1
          - UICollectionView<SJPlayModelNestedView>
              - UICollectionViewCell2
                  - PlayerSuperview<SJPlayModelPlayerSuperview>
                      - player

 \code
 SJPlayModel *one = [SJPlayModel playModelWithCollectionView:collectionView indexPath:cellIndexPath2];
 one.nextPlayModel = [SJPlayModel playModelWithTableView:tableView indexPath:cellIndexPath1];
 \endcode
 
 3. - UICollectionView1
      - UICollectionViewCell1
          - UICollectionView2<SJPlayModelNestedView>
              - UICollectionViewCell2
                  - PlayerSuperview<SJPlayModelPlayerSuperview>
                      - player
 \code
 SJPlayModel *one = [SJPlayModel playModelWithCollectionView:collectionView2 indexPath:cellIndexPath2];
 one.nextPlayModel = [SJPlayModel playModelWithCollectionView:collectionView1 indexPath:cellIndexPath1];
 \endcode
 */
@property (nonatomic, strong, nullable) __kindof SJPlayModel *nextPlayModel;
@end

定义中, 可以看到有两个用于标记的协议, SJPlayModelPlayerSuperview以及SJPlayModelNestedView:

  • SJPlayModelPlayerSuperview用来标记某个视图是播放器父视图, 将来可以添加播放器到这个视图上.
  • SJPlayModelNestedView用来标记内嵌的那个视图, 正如例子中的collectionView可能会被嵌入到一个tableViewCell中. 由于tableView以及collectionView都存在复用的情况, 因此需要添加该标记建立视图层次链. 管理类通过这条链来定位具体位置.

那如何定位当前具体位置?

我们以在UITableView中播放的例子来介绍一下, 其中SJPlayModel的创建如下:

SJPlayModel *model = [SJPlayModel playModelWithTableView:_tableView indexPath:indexPath];

相应的Cell中应该也必须有一个视图来容纳播放器视图, 管理类将在合适的时机添加到该视图中, 无需手动添加.

/// 播放器父视图, 将来管理类会添加播放器到该视图中
@interface SJPlayerSuperview : UIView<SJPlayModelPlayerSuperview>

@end

@interface SJDemoTableViewCell : UITableViewCell
/// 播放器父视图, 将来管理类会添加播放器到该视图中. 该类遵守了`SJPlayModelPlayerSuperview`协议. 
@property (nonatomic, strong) SJPlayerSuperview *playerSuperview;
/// .....
/// .....
@property (nonatomic, strong) UIImageView *avatarImageView;
@property (nonatomic, strong) UILabel *usernameLabel;
@end

当设置资源进行播放后, 管理类会监听TableView的滚动, 会在设置资源时及后续滚动触发时, 由NSIndexPath来定位具体的Cell, 判断其是否显示在屏幕中. 如果已显示, 会尝试获取播放器父视图, 我们之前看到的标记协议SJPlayModelPlayerSuperview 就是用于此处的, 根据它获取父视图, 并添加到其中, 实现如下:

/// 注意: 此处为内部源码实现, 实际使用中, 只需按照之前的步骤配置好 playModel 即可

/// 管理类可能进行的操作
UIView *superview = [_tableView viewWithProtocol:@protocol(SJPlayModelPlayerSuperview) atIndexPath:_indexPath];
[superview addSubview:player.view];

/// 对应方法具体实现如下: 

@implementation UIScrollView (SJBaseVideoPlayerExtended)
/// .........
/// .........

///
/// 获取对应视图
///
- (nullable __kindof UIView *)viewWithProtocol:(Protocol *)protocol atIndexPath:(NSIndexPath *)indexPath {
    if ( indexPath == nil ) return nil;
    __kindof UIView *_Nullable cell = nil;
    if      ( [self isKindOfClass:UITableView.class] ) {
        cell = [(UITableView *)self cellForRowAtIndexPath:indexPath];
    }
    else if ( [self isKindOfClass:UICollectionView.class] ) {
        cell = [(UICollectionView *)self cellForItemAtIndexPath:indexPath];
    }
    return cell != nil ? [cell viewWithProtocol:protocol] : nil;
}

/// .........
/// .........
@end


@implementation UIView (SJBaseVideoPlayerExtended)
/// .........
/// .........

///
/// 寻找实现了该协议的视图, 包括自己
///
- (__kindof UIView *_Nullable)viewWithProtocol:(Protocol *)protocol {
    if ( [self conformsToProtocol:protocol] ) {
        return self;
    }
    
    for ( UIView *subview in self.subviews ) {
        UIView *target = [subview viewWithProtocol:protocol];
        if ( target != nil ) return target;
    }
    return nil;
}

/// .........
/// .........
@end

至此我们从使用者的角度来梳理一下流程:

  1. 在Cell中添加一个视图, 该视图遵守了SJPlayModelPlayerSuperview协议.
  2. 设置播放资源时, 需传入SJPlayModel, 传入tableView及对应的indexPath.

完成以上步骤, 播放器视图将会被自动添加到父视图中.


理解了以上步骤, 那么在遇到嵌套的场景中, 就可以按照上面的套路创建相应SJPlayModel, 将其通过nextPlayModel链接起来, 赋值播放即可.

Clone this wiki locally