// // TUIPhotoPreviewCell.m // TUIPhotoPreviewCell // // Created by lynx on 2024/8/21. // Copyright © 2024 Tencent. All rights reserved. // #import "TUIPhotoPreviewCell.h" #import "TUIMultimediaNavController.h" #import #import #import #import #import #import @implementation TUIAssetPreviewCell - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.backgroundColor = [UIColor blackColor]; [self configSubviews]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(photoPreviewCollectionViewDidScroll) name:@"photoPreviewCollectionViewDidScroll" object:nil]; } return self; } - (void)configSubviews { // Subclass override } #pragma mark - Notification - (void)photoPreviewCollectionViewDidScroll { // Subclass override } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end @implementation TUIPhotoPreviewCell - (void)configSubviews { self.previewView = [[TUIPhotoPreviewView alloc] initWithFrame:CGRectZero]; @weakify(self) [self.previewView setSingleTapGestureBlock:^{ @strongify(self) if (self.singleTapGestureBlock) { self.singleTapGestureBlock(); } }]; [self.previewView setImageProgressUpdateBlock:^(double progress) { @strongify(self) if (self.imageProgressUpdateBlock) { self.imageProgressUpdateBlock(progress); } }]; [self.contentView addSubview:self.previewView]; } - (void)setModel:(TUIAssetModel *)model { [super setModel:model]; self.previewView.model = model; } - (void)recoverSubviews { [self.previewView recoverSubviews]; } - (void)setScaleAspectFillCrop:(BOOL)scaleAspectFillCrop { _scaleAspectFillCrop = scaleAspectFillCrop; self.previewView.scaleAspectFillCrop = scaleAspectFillCrop; } - (void)setCropRect:(CGRect)cropRect { _cropRect = cropRect; self.previewView.cropRect = cropRect; } - (void)layoutSubviews { [super layoutSubviews]; self.previewView.frame = self.bounds; } @end @interface TUIPhotoPreviewView () @property (assign, nonatomic) BOOL isRequestingGIF; @end @implementation TUIPhotoPreviewView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.scrollView = [[UIScrollView alloc] init]; self.scrollView.bouncesZoom = YES; self.scrollView.maximumZoomScale = 4; self.scrollView.minimumZoomScale = 1.0; self.scrollView.multipleTouchEnabled = YES; self.scrollView.delegate = self; self.scrollView.scrollsToTop = NO; self.scrollView.showsHorizontalScrollIndicator = NO; self.scrollView.showsVerticalScrollIndicator = YES; self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; self.scrollView.delaysContentTouches = NO; self.scrollView.canCancelContentTouches = YES; self.scrollView.alwaysBounceVertical = NO; if (@available(iOS 11, *)) { self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } [self addSubview:self.scrollView]; self.imageContainerView = [[UIView alloc] init]; self.imageContainerView.clipsToBounds = YES; self.imageContainerView.contentMode = UIViewContentModeScaleAspectFill; [self.scrollView addSubview:self.imageContainerView]; self.imageView = [[UIImageView alloc] init]; self.imageView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:0.500]; self.imageView.contentMode = UIViewContentModeScaleAspectFill; self.imageView.clipsToBounds = YES; [self.imageContainerView addSubview:self.imageView]; self.iCloudErrorIcon = [[UIImageView alloc] init]; self.iCloudErrorIcon.image = [UIImage tui_imageNamedFromMyBundle:@"iCloudError"]; self.iCloudErrorIcon.hidden = YES; [self addSubview:self.iCloudErrorIcon]; self.iCloudErrorLabel = [[UILabel alloc] init]; self.iCloudErrorLabel.font = [UIFont systemFontOfSize:10]; self.iCloudErrorLabel.textColor = [UIColor whiteColor]; self.iCloudErrorLabel.text = [NSBundle tui_localizedStringForKey:@"iCloud sync failed"]; self.iCloudErrorLabel.hidden = YES; [self addSubview:self.iCloudErrorLabel]; UITapGestureRecognizer *tap1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)]; [self addGestureRecognizer:tap1]; UITapGestureRecognizer *tap2 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)]; tap2.numberOfTapsRequired = 2; [tap1 requireGestureRecognizerToFail:tap2]; [self addGestureRecognizer:tap2]; self.progressView = [[TUIProgressView alloc] init]; self.progressView.hidden = YES; [self addSubview:self.progressView]; } return self; } - (void)setModel:(TUIAssetModel *)model { _model = model; self.isRequestingGIF = NO; [self.scrollView setZoomScale:1.0 animated:NO]; if (TUIAssetMediaTypePhotoGif == model.type) { // Show thumbnails first [[TUIImageManager defaultManager] getPhotoWithAsset:model.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { if (photo) { self.imageView.image = photo; } [self resizeSubviews]; if (self.isRequestingGIF) { return; } // Show gif self.isRequestingGIF = YES; [[TUIImageManager defaultManager] getOriginalPhotoDataWithAsset:model.asset progressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { progress = progress > 0.02 ? progress : 0.02; dispatch_async(dispatch_get_main_queue(), ^{ BOOL iCloudSyncFailed = [TUICommonTools isICloudSyncError:error]; self.iCloudErrorLabel.hidden = !iCloudSyncFailed; self.iCloudErrorIcon.hidden = !iCloudSyncFailed; if (self.iCloudSyncFailedHandle) { self.iCloudSyncFailedHandle(model.asset, iCloudSyncFailed); } self.progressView.progress = progress; if (progress >= 1) { self.progressView.hidden = YES; } else { self.progressView.hidden = NO; } }); #ifdef DEBUG NSLog(@"[TUIMultimediaNavController] getOriginalPhotoDataWithAsset:%f error:%@", progress, error); #endif } completion:^(NSData *data, NSDictionary *info, BOOL isDegraded) { if (!isDegraded) { self.isRequestingGIF = NO; self.progressView.hidden = YES; self.imageView.image = [UIImage tui_animatedGIFWithData:data]; [self resizeSubviews]; } }]; } progressHandler:nil networkAccessAllowed:NO]; } else { if (model.type == TUIAssetMediaTypePhoto && model.editImage != nil) { self.imageView.image = model.editImage; [self resizeSubviews]; self.progressView.hidden = YES; if (self.imageProgressUpdateBlock) { self.imageProgressUpdateBlock(1); } } else { self.asset = model.asset; } } } - (void)setAsset:(PHAsset *)asset { if (_asset && self.imageRequestID) { [[PHImageManager defaultManager] cancelImageRequest:self.imageRequestID]; } _asset = asset; @weakify(self) self.imageRequestID = [[TUIImageManager defaultManager] getPhotoWithAsset:asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { @strongify(self) BOOL iCloudSyncFailed = !photo && [TUICommonTools isICloudSyncError:info[PHImageErrorKey]]; self.iCloudErrorLabel.hidden = !iCloudSyncFailed; self.iCloudErrorIcon.hidden = !iCloudSyncFailed; if (self.iCloudSyncFailedHandle) { self.iCloudSyncFailedHandle(asset, iCloudSyncFailed); } if (![asset isEqual:self->_asset]) return; if (photo) { self.imageView.image = photo; } [self resizeSubviews]; self.progressView.hidden = YES; if (self.imageProgressUpdateBlock) { self.imageProgressUpdateBlock(1); } if (!isDegraded) { self.imageRequestID = 0; } } progressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { @strongify(self) if (![asset isEqual:self->_asset]) return; self.progressView.hidden = NO; [self bringSubviewToFront:self.progressView]; progress = progress > 0.02 ? progress : 0.02; self.progressView.progress = progress; if (self.imageProgressUpdateBlock && progress < 1) { self.imageProgressUpdateBlock(progress); } if (progress >= 1) { self.progressView.hidden = YES; self.imageRequestID = 0; } } networkAccessAllowed:YES]; [self configMaximumZoomScale]; } - (void)recoverSubviews { [self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:NO]; [self resizeSubviews]; } - (void)resizeSubviews { self.imageContainerView.mm_origin = CGPointZero; self.imageContainerView.mm_w = self.scrollView.mm_w; UIImage *image = self.imageView.image; if (image.size.height / image.size.width > self.mm_h / self.scrollView.mm_w) { CGFloat width = image.size.width / image.size.height * self.scrollView.mm_h; if (width < 1 || isnan(width)) width = self.mm_w; width = floor(width); self.imageContainerView.mm_w = width; self.imageContainerView.mm_h = self.mm_h; self.imageContainerView.mm_centerX = self.scrollView.mm_w / 2; } else { CGFloat height = image.size.height / image.size.width * self.scrollView.mm_w; if (height < 1 || isnan(height)) height = self.mm_h; height = floor(height); self.imageContainerView.mm_h = height; self.imageContainerView.mm_centerY = self.mm_h / 2; } if (self.imageContainerView.mm_h > self.mm_h && self.imageContainerView.mm_h - self.mm_h <= 1) { self.imageContainerView.mm_h = self.mm_h; } CGFloat contentSizeH = MAX(self.imageContainerView.mm_h, self.mm_h); self.scrollView.contentSize = CGSizeMake(self.scrollView.mm_w, contentSizeH); [self.scrollView scrollRectToVisible:self.bounds animated:NO]; self.scrollView.alwaysBounceVertical = self.imageContainerView.mm_h <= self.mm_h ? NO : YES; self.imageView.frame = self.imageContainerView.bounds; [self refreshScrollViewContentSize]; } - (void)configMaximumZoomScale { self.scrollView.maximumZoomScale = 4.0; if ([self.asset isKindOfClass:[PHAsset class]]) { PHAsset *phAsset = (PHAsset *)self.asset; CGFloat aspectRatio = phAsset.pixelWidth / (CGFloat)phAsset.pixelHeight; if (aspectRatio > 1.5) { self.scrollView.maximumZoomScale *= aspectRatio / 1.5; } } } - (void)refreshScrollViewContentSize { // to do } - (void)layoutSubviews { [super layoutSubviews]; self.scrollView.frame = CGRectMake(10, 0, self.mm_w - 20, self.mm_h); static CGFloat progressWH = 40; CGFloat progressX = (self.mm_w - progressWH) / 2; CGFloat progressY = (self.mm_h - progressWH) / 2; self.progressView.frame = CGRectMake(progressX, progressY, progressWH, progressWH); [self recoverSubviews]; self.iCloudErrorIcon.frame = CGRectMake(20, [TUICommonTools tui_statusBarHeight] + 44 + 10, 28, 28); self.iCloudErrorLabel.frame = CGRectMake(53, [TUICommonTools tui_statusBarHeight] + 44 + 10, self.mm_w - 63, 28); } #pragma mark - UITapGestureRecognizer Event - (void)doubleTap:(UITapGestureRecognizer *)tap { if (self.scrollView.zoomScale > self.scrollView.minimumZoomScale) { self.scrollView.contentInset = UIEdgeInsetsZero; [self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES]; } else { CGPoint touchPoint = [tap locationInView:self.imageView]; CGFloat newZoomScale = MIN(self.scrollView.maximumZoomScale, 2.5); CGFloat xsize = self.frame.size.width / newZoomScale; CGFloat ysize = self.frame.size.height / newZoomScale; [self.scrollView zoomToRect:CGRectMake(touchPoint.x - xsize/2, touchPoint.y - ysize/2, xsize, ysize) animated:YES]; } } - (void)singleTap:(UITapGestureRecognizer *)tap { if (self.singleTapGestureBlock) { self.singleTapGestureBlock(); } } #pragma mark - UIScrollViewDelegate - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { return self.imageContainerView; } - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view { scrollView.contentInset = UIEdgeInsetsZero; } - (void)scrollViewDidZoom:(UIScrollView *)scrollView { [self refreshImageContainerViewCenter]; } - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale { [self refreshScrollViewContentSize]; } #pragma mark - Private - (void)refreshImageContainerViewCenter { CGFloat offsetX = (self.scrollView.mm_w > self.scrollView.contentSize.width) ? ((self.scrollView.mm_w - self.scrollView.contentSize.width) * 0.5) : 0.0; CGFloat offsetY = (self.scrollView.mm_h > self.scrollView.contentSize.height) ? ((self.scrollView.mm_h - self.scrollView.contentSize.height) * 0.5) : 0.0; self.imageContainerView.center = CGPointMake(self.scrollView.contentSize.width * 0.5 + offsetX, self.scrollView.contentSize.height * 0.5 + offsetY); } @end @implementation TUIVideoPreviewCell - (void)configSubviews { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillResignActiveNotification) name:UIApplicationWillResignActiveNotification object:nil]; self.iCloudErrorIcon = [[UIImageView alloc] init]; self.iCloudErrorIcon.image = [UIImage tui_imageNamedFromMyBundle:@"iCloudError"]; self.iCloudErrorIcon.hidden = YES; self.iCloudErrorLabel = [[UILabel alloc] init]; self.iCloudErrorLabel.font = [UIFont systemFontOfSize:10]; self.iCloudErrorLabel.textColor = [UIColor whiteColor]; self.iCloudErrorLabel.text = [NSBundle tui_localizedStringForKey:@"iCloud sync failed"]; self.iCloudErrorLabel.hidden = YES; } - (void)configPlayButton { if (self.playButton) { [self.playButton removeFromSuperview]; } self.playButton = [UIButton buttonWithType:UIButtonTypeCustom]; [self.playButton setImage:[UIImage tui_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal]; [self.playButton setImage:[UIImage tui_imageNamedFromMyBundle:@"MMVideoPreviewPlayHL"] forState:UIControlStateHighlighted]; [self.playButton addTarget:self action:@selector(playButtonClick) forControlEvents:UIControlEventTouchUpInside]; self.playButton.frame = CGRectMake(0, 64, self.mm_w, self.mm_h - 64 - 44); [self.contentView addSubview:self.playButton]; [self.contentView addSubview:self.iCloudErrorIcon]; [self.contentView addSubview:self.iCloudErrorLabel]; } - (void)setModel:(TUIAssetModel *)model { [super setModel:model]; [self configMoviePlayer]; } - (void)setVideoURL:(NSURL *)videoURL { _videoURL = videoURL; [self configMoviePlayer]; } - (void)configMoviePlayer { if (self.player) { [self.playerLayer removeFromSuperlayer]; self.playerLayer = nil; [self.player pause]; self.player = nil; } //有编辑时,优先使用编辑的url 和 Image if (self.model.editImage && self.model.type == TUIAssetMediaTypePhoto) { self.cover = self.model.editImage; } else if (self.model.editurl) { if (self.model.editImage) { self.cover = self.model.editImage; } AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:self.model.editurl]; [self configPlayerWithItem:playerItem]; } else { if (self.model && self.model.asset) { [[TUIImageManager defaultManager] getPhotoWithAsset:self.model.asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { BOOL iCloudSyncFailed = !photo && [TUICommonTools isICloudSyncError:info[PHImageErrorKey]]; self.iCloudErrorLabel.hidden = !iCloudSyncFailed; self.iCloudErrorIcon.hidden = !iCloudSyncFailed; if (self.iCloudSyncFailedHandle) { self.iCloudSyncFailedHandle(self.model.asset, iCloudSyncFailed); } if (photo) { self.cover = photo; } }]; [[TUIImageManager defaultManager] getVideoWithAsset:self.model.asset completion:^(AVPlayerItem *playerItem, NSDictionary *info) { dispatch_async(dispatch_get_main_queue(), ^{ BOOL iCloudSyncFailed = !playerItem && [TUICommonTools isICloudSyncError:info[PHImageErrorKey]]; self.iCloudErrorLabel.hidden = !iCloudSyncFailed; self.iCloudErrorIcon.hidden = !iCloudSyncFailed; if (self.iCloudSyncFailedHandle) { self.iCloudSyncFailedHandle(self.model.asset, iCloudSyncFailed); } [self configPlayerWithItem:playerItem]; }); }]; } else { AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:self.videoURL]; [self configPlayerWithItem:playerItem]; } } } - (void)configPlayerWithItem:(AVPlayerItem *)playerItem { self.player = [AVPlayer playerWithPlayerItem:playerItem]; self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; self.playerLayer.backgroundColor = [UIColor blackColor].CGColor; self.playerLayer.frame = self.bounds; [self.contentView.layer addSublayer:self.playerLayer]; [self configPlayButton]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pausePlayerAndShowNaviBar) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem]; } - (void)layoutSubviews { [super layoutSubviews]; self.playerLayer.frame = self.bounds; self.playButton.frame = CGRectMake(0, 64, self.mm_w, self.mm_h - 64 - 44); self.iCloudErrorIcon.frame = CGRectMake(20, [TUICommonTools tui_statusBarHeight] + 44 + 10, 28, 28); self.iCloudErrorLabel.frame = CGRectMake(53, [TUICommonTools tui_statusBarHeight] + 44 + 10, self.mm_w - 63, 28); } - (void)photoPreviewCollectionViewDidScroll { if (self.player && self.player.rate != 0.0) { [self pausePlayerAndShowNaviBar]; } } #pragma mark - Notification - (void)appWillResignActiveNotification { if (self.player && self.player.rate != 0.0) { [self pausePlayerAndShowNaviBar]; } } #pragma mark - Click Event - (void)playButtonClick { CMTime currentTime = self.player.currentItem.currentTime; CMTime durationTime = self.player.currentItem.duration; if (self.player.rate == 0.0f) { [[NSNotificationCenter defaultCenter] postNotificationName:@"TZ_VIDEO_PLAY_NOTIFICATION" object:self.player]; if (currentTime.value == durationTime.value) [self.player.currentItem seekToTime:CMTimeMake(0, 1)]; [self.player play]; [self.playButton setImage:nil forState:UIControlStateNormal]; [UIApplication sharedApplication].statusBarHidden = YES; if (self.singleTapGestureBlock) { self.singleTapGestureBlock(); } } else { [self pausePlayerAndShowNaviBar]; } } - (void)pausePlayerAndShowNaviBar { [self.player pause]; [self.playButton setImage:[UIImage tui_imageNamedFromMyBundle:@"MMVideoPreviewPlay"] forState:UIControlStateNormal]; if (self.singleTapGestureBlock) { self.singleTapGestureBlock(); } } @end @implementation TUIGifPreviewCell - (void)configSubviews { [self configPreviewView]; } - (void)configPreviewView { _previewView = [[TUIPhotoPreviewView alloc] initWithFrame:CGRectZero]; @weakify(self) [_previewView setSingleTapGestureBlock:^{ @strongify(self) [self signleTapAction]; }]; [self.contentView addSubview:_previewView]; } - (void)setModel:(TUIAssetModel *)model { [super setModel:model]; _previewView.model = self.model; } - (void)layoutSubviews { [super layoutSubviews]; _previewView.frame = self.bounds; } #pragma mark - Click Event - (void)signleTapAction { if (self.singleTapGestureBlock) { self.singleTapGestureBlock(); } } @end