Files
featherVoice/Pods/YBImageBrowser/Video/YBIBVideoCell.m
2025-08-08 10:49:36 +08:00

449 lines
16 KiB
Objective-C

//
// YBIBVideoCell.m
// YBImageBrowserDemo
//
// Created by 波儿菜 on 2019/7/10.
// Copyright © 2019 杨波. All rights reserved.
//
#import "YBIBVideoCell.h"
#import "YBIBVideoData.h"
#import "YBIBVideoData+Internal.h"
#import "YBIBCopywriter.h"
#import "YBIBIconManager.h"
#import <objc/runtime.h>
#import "YBIBVideoCell+Internal.h"
@interface NSObject (YBIBVideoPlayingRecord)
- (void)ybib_videoPlayingAdd:(NSObject *)obj;
- (void)ybib_videoPlayingRemove:(NSObject *)obj;
- (BOOL)ybib_noVideoPlaying;
@end
@implementation NSObject (YBIBVideoPlayingRecord)
- (NSMutableSet *)ybib_videoPlayingSet {
static void *kRecordKey = &kRecordKey;
NSMutableSet *set = objc_getAssociatedObject(self, kRecordKey);
if (!set) {
set = [NSMutableSet set];
objc_setAssociatedObject(self, kRecordKey, set, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return set;
}
- (void)ybib_videoPlayingAdd:(NSObject *)obj {
[[self ybib_videoPlayingSet] addObject:[NSString stringWithFormat:@"%p", obj]];
}
- (void)ybib_videoPlayingRemove:(NSObject *)obj {
[[self ybib_videoPlayingSet] removeObject:[NSString stringWithFormat:@"%p", obj]];
}
- (BOOL)ybib_noVideoPlaying {
return [self ybib_videoPlayingSet].count == 0;
}
@end
@interface YBIBVideoCell () <YBIBVideoDataDelegate, YBIBVideoViewDelegate, UIGestureRecognizerDelegate>
@end
@implementation YBIBVideoCell {
CGPoint _interactStartPoint;
BOOL _interacting;
}
#pragma mark - life cycle
- (void)dealloc {
[self.yb_backView ybib_videoPlayingRemove:self];
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initValue];
[self.contentView addSubview:self.videoView];
[self addGesture];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.videoView.frame = self.bounds;
}
- (void)initValue {
_interactStartPoint = CGPointZero;
_interacting = NO;
}
- (void)prepareForReuse {
((YBIBVideoData *)self.yb_cellData).delegate = nil;
self.videoView.thumbImageView.image = nil;
[self hideAuxiliaryView];
[self.videoView reset];
self.videoView.asset = nil;
[super prepareForReuse];
}
#pragma mark - private
- (void)hideAuxiliaryView {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
[self.yb_auxiliaryViewHandler() yb_hideToastWithContainer:self];
}
- (void)updateImageLayoutWithOrientation:(UIDeviceOrientation)orientation previousImageSize:(CGSize)previousImageSize {
YBIBVideoData *data = self.yb_cellData;
UIImage *image = self.videoView.thumbImageView.image;
CGSize imageSize = image.size;
CGRect imageViewFrame = [data yb_imageViewFrameWithContainerSize:self.yb_containerSize(orientation) imageSize:imageSize orientation:orientation];
CGFloat scale;
if (previousImageSize.width > 0 && previousImageSize.height > 0) {
scale = imageSize.width / imageSize.height - previousImageSize.width / previousImageSize.height;
} else {
scale = 0;
}
// '0.001' is admissible error.
if (ABS(scale) <= 0.001) {
self.videoView.thumbImageView.frame = imageViewFrame;
} else {
[UIView animateWithDuration:0.25 animations:^{
self.videoView.thumbImageView.frame = imageViewFrame;
}];
}
}
- (void)hideBrowser {
((YBIBVideoData *)self.yb_cellData).delegate = nil;
self.videoView.thumbImageView.hidden = NO;
self.videoView.autoPlayCount = 0;
[self.videoView reset];
[self.videoView hideToolBar:YES];
[self.videoView hidePlayButton];
self.yb_hideBrowser();
_interacting = NO;
}
- (void)hideToolViews:(BOOL)hide {
if (hide) {
self.yb_hideToolViews(YES);
} else {
if ([self.yb_backView ybib_noVideoPlaying]) {
self.yb_hideToolViews(NO);
}
}
}
#pragma mark - <YBIBCellProtocol>
@synthesize yb_currentOrientation = _yb_currentOrientation;
@synthesize yb_containerSize = _yb_containerSize;
@synthesize yb_backView = _yb_backView;
@synthesize yb_collectionView = _yb_collectionView;
@synthesize yb_isTransitioning = _yb_isTransitioning;
@synthesize yb_auxiliaryViewHandler = _yb_auxiliaryViewHandler;
@synthesize yb_hideStatusBar = _yb_hideStatusBar;
@synthesize yb_hideBrowser = _yb_hideBrowser;
@synthesize yb_hideToolViews = _yb_hideToolViews;
@synthesize yb_cellData = _yb_cellData;
@synthesize yb_currentPage = _yb_currentPage;
@synthesize yb_selfPage = _yb_selfPage;
@synthesize yb_cellIsInCenter = _yb_cellIsInCenter;
@synthesize yb_isRotating = _yb_isRotating;
- (void)setYb_cellData:(id<YBIBDataProtocol>)yb_cellData {
_yb_cellData = yb_cellData;
YBIBVideoData *data = (YBIBVideoData *)yb_cellData;
data.delegate = self;
UIDeviceOrientation orientation = self.yb_currentOrientation();
CGSize containerSize = self.yb_containerSize(orientation);
[self.videoView updateLayoutWithExpectOrientation:orientation containerSize:containerSize];
self.videoView.autoPlayCount = data.autoPlayCount;
self.videoView.topBar.cancelButton.hidden = data.shouldHideForkButton;
}
- (void)yb_orientationWillChangeWithExpectOrientation:(UIDeviceOrientation)orientation {
if (_interacting) [self restoreGestureInteractionWithDuration:0];
}
- (void)yb_orientationChangeAnimationWithExpectOrientation:(UIDeviceOrientation)orientation {
[self updateImageLayoutWithOrientation:orientation previousImageSize:self.videoView.thumbImageView.image.size];
CGSize containerSize = self.yb_containerSize(orientation);
[self.videoView updateLayoutWithExpectOrientation:orientation containerSize:containerSize];
}
- (UIView *)yb_foregroundView {
return self.videoView.thumbImageView;
}
- (void)yb_pageChanged {
if (self.yb_currentPage() != self.yb_selfPage()) {
[self.videoView reset];
[self hideToolViews:NO];
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
if (_interacting) [self restoreGestureInteractionWithDuration:0];
self.videoView.needAutoPlay = NO;
} else {
self.videoView.needAutoPlay = YES;
}
}
#pragma mark - <YBIBVideoDataDelegate>
- (void)yb_startLoadingAVAssetFromPHAssetForData:(YBIBVideoData *)data {}
- (void)yb_finishLoadingAVAssetFromPHAssetForData:(YBIBVideoData *)data {}
- (void)yb_startLoadingFirstFrameForData:(YBIBVideoData *)data {
if (!self.videoView.thumbImageView.image) {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self];
}
}
- (void)yb_finishLoadingFirstFrameForData:(YBIBVideoData *)data {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
}
- (void)yb_videoData:(YBIBVideoData *)data downloadingWithProgress:(CGFloat)progress {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self progress:progress];
}
- (void)yb_finishDownloadingForData:(YBIBVideoData *)data {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
}
- (void)yb_videoData:(YBIBVideoData *)data readyForAVAsset:(AVAsset *)asset {
self.videoView.asset = asset;
}
- (void)yb_videoData:(YBIBVideoData *)data readyForThumbImage:(UIImage *)image {
if (!self.videoView.isPlaying) {
self.videoView.thumbImageView.hidden = NO;
}
if (!self.videoView.thumbImageView.image) {
CGSize previousSize = self.videoView.thumbImageView.image.size;
self.videoView.thumbImageView.image = image;
[self updateImageLayoutWithOrientation:self.yb_currentOrientation() previousImageSize:previousSize];
}
}
- (void)yb_videoIsInvalidForData:(YBIBVideoData *)data {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
NSString *imageIsInvalid = [YBIBCopywriter sharedCopywriter].videoIsInvalid;
if (self.videoView.thumbImageView.image) {
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self text:imageIsInvalid];
} else {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self text:imageIsInvalid];
}
}
#pragma mark - <YBIBVideoViewDelegate>
- (BOOL)yb_isFreezingForVideoView:(YBIBVideoView *)view {
return self.yb_isTransitioning();
}
- (void)yb_preparePlayForVideoView:(YBIBVideoView *)view {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (!view.isPlaying && !view.isPlayFailed && self.yb_selfPage() == self.yb_currentPage()) {
[self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self];
}
});
}
- (void)yb_startPlayForVideoView:(YBIBVideoView *)view {
self.videoView.thumbImageView.hidden = YES;
[self.yb_backView ybib_videoPlayingAdd:self];
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
[self hideToolViews:YES];
}
- (void)yb_didPlayToEndTimeForVideoView:(YBIBVideoView *)view {
YBIBVideoData *data = (YBIBVideoData *)self.yb_cellData;
if (data.repeatPlayCount == NSUIntegerMax) {
[view preparPlay];
} else if (data.repeatPlayCount > 0) {
--data.repeatPlayCount;
[view preparPlay];
} else {
[self hideToolViews:NO];
}
}
- (void)yb_finishPlayForVideoView:(YBIBVideoView *)view {
[self.yb_backView ybib_videoPlayingRemove:self];
[self hideToolViews:NO];
}
- (void)yb_playFailedForVideoView:(YBIBVideoView *)view {
[self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
[self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self text:YBIBCopywriter.sharedCopywriter.videoError];
}
- (void)yb_respondsToTapGestureForVideoView:(YBIBVideoView *)view {
if (self.yb_isRotating()) return;
YBIBVideoData *data = self.yb_cellData;
if (data.singleTouchBlock) {
data.singleTouchBlock(data);
} else {
[self hideBrowser];
}
}
- (void)yb_cancelledForVideoView:(YBIBVideoView *)view {
if (self.yb_isRotating()) return;
[self hideBrowser];
}
- (CGSize)yb_containerSizeForVideoView:(YBIBVideoView *)view {
return self.yb_containerSize(self.yb_currentOrientation());
}
- (void)yb_autoPlayCountChanged:(NSUInteger)count {
YBIBVideoData *data = (YBIBVideoData *)self.yb_cellData;
data.autoPlayCount = count;
}
#pragma mark - <UIGestureRecognizerDelegate>
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
#pragma mark - gesture
- (void)addGesture {
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToPanGesture:)];
panGesture.cancelsTouchesInView = NO;
panGesture.delegate = self;
[self.videoView.tapGesture requireGestureRecognizerToFail:panGesture];
[self.videoView addGestureRecognizer:panGesture];
}
- (void)respondsToPanGesture:(UIPanGestureRecognizer *)pan {
if (self.yb_isRotating()) return;
if ((!self.videoView.thumbImageView.image && !self.videoView.isPlaying)) return;
YBIBInteractionProfile *profile = ((YBIBVideoData *)self.yb_cellData).interactionProfile;
if (profile.disable) return;
CGPoint point = [pan locationInView:self];
CGSize containerSize = self.yb_containerSize(self.yb_currentOrientation());
if (pan.state == UIGestureRecognizerStateBegan) {
_interactStartPoint = point;
} else if (pan.state == UIGestureRecognizerStateCancelled || pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateRecognized || pan.state == UIGestureRecognizerStateFailed) {
// End
if (_interacting) {
CGPoint velocity = [pan velocityInView:self.videoView];
BOOL velocityArrive = ABS(velocity.y) > profile.dismissVelocityY;
BOOL distanceArrive = ABS(point.y - _interactStartPoint.y) > containerSize.height * profile.dismissScale;
BOOL shouldDismiss = distanceArrive || velocityArrive;
if (shouldDismiss) {
[self hideBrowser];
} else {
[self restoreGestureInteractionWithDuration:profile.restoreDuration];
}
}
} else if (pan.state == UIGestureRecognizerStateChanged) {
if (_interacting) {
// Change
self.videoView.center = point;
CGFloat scale = 1 - ABS(point.y - _interactStartPoint.y) / (containerSize.height * 1.2);
if (scale > 1) scale = 1;
if (scale < 0.35) scale = 0.35;
self.videoView.transform = CGAffineTransformMakeScale(scale, scale);
CGFloat alpha = 1 - ABS(point.y - _interactStartPoint.y) / (containerSize.height * 0.7);
if (alpha > 1) alpha = 1;
if (alpha < 0) alpha = 0;
self.yb_backView.backgroundColor = [self.yb_backView.backgroundColor colorWithAlphaComponent:alpha];
} else {
// Start
if (CGPointEqualToPoint(_interactStartPoint, CGPointZero) || self.yb_currentPage() != self.yb_selfPage() || !self.yb_cellIsInCenter() || self.videoView.actionBar.isTouchInside) return;
CGPoint velocityPoint = [pan velocityInView:self.videoView];
CGFloat triggerDistance = profile.triggerDistance;
BOOL distanceArrive = ABS(point.y - _interactStartPoint.y) > triggerDistance && (ABS(point.x - _interactStartPoint.x) < triggerDistance && ABS(velocityPoint.x) < 500);
BOOL shouldStart = distanceArrive;
if (!shouldStart) return;
[self.videoView hideToolBar:YES];
_interactStartPoint = point;
CGRect startFrame = self.videoView.bounds;
CGFloat anchorX = (point.x - startFrame.origin.x) / startFrame.size.width,
anchorY = (point.y - startFrame.origin.y) / startFrame.size.height;
self.videoView.layer.anchorPoint = CGPointMake(anchorX, anchorY);
self.videoView.userInteractionEnabled = NO;
self.videoView.center = point;
[self hideToolViews:YES];
self.yb_hideStatusBar(NO);
self.yb_collectionView().scrollEnabled = NO;
_interacting = YES;
}
}
}
- (void)restoreGestureInteractionWithDuration:(NSTimeInterval)duration {
[self.videoView hideToolBar:NO];
CGSize containerSize = self.yb_containerSize(self.yb_currentOrientation());
void (^animations)(void) = ^{
self.yb_backView.backgroundColor = [self.yb_backView.backgroundColor colorWithAlphaComponent:1];
CGPoint anchorPoint = self.videoView.layer.anchorPoint;
self.videoView.center = CGPointMake(containerSize.width * anchorPoint.x, containerSize.height * anchorPoint.y);
self.videoView.transform = CGAffineTransformIdentity;
};
void (^completion)(BOOL finished) = ^(BOOL finished){
self.videoView.layer.anchorPoint = CGPointMake(0.5, 0.5);
self.videoView.center = CGPointMake(containerSize.width * 0.5, containerSize.height * 0.5);
self.videoView.userInteractionEnabled = YES;
self.yb_hideStatusBar(YES);
self.yb_collectionView().scrollEnabled = YES;
if (!self.videoView.isPlaying) [self hideToolViews:NO];;
self->_interactStartPoint = CGPointZero;
self->_interacting = NO;
};
if (duration <= 0) {
animations();
completion(NO);
} else {
[UIView animateWithDuration:duration animations:animations completion:completion];
}
}
#pragma mark - getters & setters
- (YBIBVideoView *)videoView {
if (!_videoView) {
_videoView = [YBIBVideoView new];
_videoView.delegate = self;
}
return _videoView;
}
@end