From 5113ccd4f4fcda0cb4aff896a0f901d7befa4bf2 Mon Sep 17 00:00:00 2001 From: hfk <416567352@qq.com> Date: Thu, 7 Aug 2025 19:09:43 +0800 Subject: [PATCH] =?UTF-8?q?live=E5=9B=BE=E7=9A=84=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E4=B8=8E=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project.pbxproj | 6 +- .../TZImagePickerController/TZAssetCell.h | 2 + .../TZImagePickerController/TZAssetCell.m | 22 +- .../TZImagePickerController/TZAssetModel.h | 2 + .../TZImagePickerController/TZAssetModel.m | 1 + .../TZImagePickerController/TZImageManager.h | 3 + .../TZImagePickerController/TZImageManager.m | 43 ++- .../photo_livephoto@2x.png | Bin 0 -> 752 bytes .../photo_livephoto@3x.png | Bin 0 -> 1161 bytes .../photo_livephoto_slash@2x.png | Bin 0 -> 719 bytes .../photo_livephoto_slash@3x.png | Bin 0 -> 1148 bytes .../TZImagePickerController.h | 6 + .../TZImagePickerController.m | 10 + .../TZPhotoPickerController.m | 9 +- .../TZPhotoPreviewCell.h | 31 ++ .../TZPhotoPreviewCell.m | 332 ++++++++++++++++++ .../TZPhotoPreviewController.m | 14 +- 17 files changed, 472 insertions(+), 9 deletions(-) create mode 100644 TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_livephoto@2x.png create mode 100644 TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_livephoto@3x.png create mode 100644 TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_livephoto_slash@2x.png create mode 100644 TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_livephoto_slash@3x.png diff --git a/TZImagePickerController.xcodeproj/project.pbxproj b/TZImagePickerController.xcodeproj/project.pbxproj index 8818a96a..c362ff6b 100644 --- a/TZImagePickerController.xcodeproj/project.pbxproj +++ b/TZImagePickerController.xcodeproj/project.pbxproj @@ -531,7 +531,7 @@ TargetAttributes = { 900E657B1C2BB8D5003D9A9E = { CreatedOnToolsVersion = 7.2; - DevelopmentTeam = 3TA49P2Q58; + DevelopmentTeam = 9YHXDAVF4M; ProvisioningStyle = Automatic; }; 900E65941C2BB8D5003D9A9E = { @@ -854,7 +854,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 3TA49P2Q58; + DEVELOPMENT_TEAM = 9YHXDAVF4M; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/TZImagePickerController", @@ -879,7 +879,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = LTFQDC2QVX; + DEVELOPMENT_TEAM = 9YHXDAVF4M; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/TZImagePickerController", diff --git a/TZImagePickerController/TZImagePickerController/TZAssetCell.h b/TZImagePickerController/TZImagePickerController/TZAssetCell.h index 67051e8f..4a194c0c 100644 --- a/TZImagePickerController/TZImagePickerController/TZAssetCell.h +++ b/TZImagePickerController/TZImagePickerController/TZAssetCell.h @@ -26,12 +26,14 @@ typedef enum : NSUInteger { @property (nonatomic, copy) void (^didSelectPhotoBlock)(BOOL); @property (nonatomic, assign) TZAssetCellType type; @property (nonatomic, assign) BOOL allowPickingGif; +@property (nonatomic, assign) BOOL allowPickingLiveImage; @property (nonatomic, assign) BOOL allowPickingMultipleVideo; @property (nonatomic, copy) NSString *representedAssetIdentifier; @property (nonatomic, assign) int32_t imageRequestID; @property (nonatomic, strong) UIImage *photoSelImage; @property (nonatomic, strong) UIImage *photoDefImage; +@property (nonatomic, strong) UIImageView *toggleLiveImageView; @property (nonatomic, assign) BOOL showSelectBtn; @property (assign, nonatomic) BOOL allowPreview; diff --git a/TZImagePickerController/TZImagePickerController/TZAssetCell.m b/TZImagePickerController/TZImagePickerController/TZAssetCell.m index 8cae3bd3..3268dd68 100644 --- a/TZImagePickerController/TZImagePickerController/TZAssetCell.m +++ b/TZImagePickerController/TZImagePickerController/TZAssetCell.m @@ -109,6 +109,8 @@ - (void)setType:(TZAssetCellType)type { _selectPhotoButton.hidden = YES; } + _toggleLiveImageView.hidden = YES; + if (type == TZAssetCellTypeVideo) { self.bottomView.hidden = NO; self.timeLength.text = _model.timeLength; @@ -121,6 +123,8 @@ - (void)setType:(TZAssetCellType)type { self.videoImgView.hidden = YES; _timeLength.tz_left = 5; _timeLength.textAlignment = NSTextAlignmentLeft; + }else if (type == TZAssetCellTypeLivePhoto && self.allowPickingLiveImage) { + self.toggleLiveImageView.hidden = NO; } } @@ -338,7 +342,18 @@ - (UILabel *)indexLabel { } return _indexLabel; } - +- (UIImageView *)toggleLiveImageView{ + if (!_toggleLiveImageView) { + UIImage *image = [UIImage tz_imageNamedFromMyBundle:@"photo_livephoto"]; + if (@available(iOS 13.0, *)) { + image = [[UIImage systemImageNamed:@"livephoto"] imageWithTintColor:UIColor.whiteColor renderingMode:UIImageRenderingModeAlwaysOriginal]; + } + _toggleLiveImageView = [[UIImageView alloc]initWithImage:image]; + _toggleLiveImageView.contentMode = UIViewContentModeScaleAspectFit; + [self.contentView addSubview:_toggleLiveImageView]; + } + return _toggleLiveImageView; +} - (TZProgressView *)progressView { if (_progressView == nil) { _progressView = [[TZProgressView alloc] init]; @@ -365,6 +380,8 @@ - (void)layoutSubviews { _indexLabel.frame = _selectImageView.frame; _imageView.frame = self.bounds; + self.toggleLiveImageView.frame = CGRectMake(3, 3, 18, 18); + static CGFloat progressWH = 20; CGFloat progressXY = (self.tz_width - progressWH) / 2; _progressView.frame = CGRectMake(progressXY, progressXY, progressWH, progressWH); @@ -381,7 +398,8 @@ - (void)layoutSubviews { [self.contentView bringSubviewToFront:_selectPhotoButton]; [self.contentView bringSubviewToFront:_selectImageView]; [self.contentView bringSubviewToFront:_indexLabel]; - + [self.contentView bringSubviewToFront:_toggleLiveImageView]; + if (self.assetCellDidLayoutSubviewsBlock) { self.assetCellDidLayoutSubviewsBlock(self, _imageView, _selectImageView, _indexLabel, _bottomView, _timeLength, _videoImgView); } diff --git a/TZImagePickerController/TZImagePickerController/TZAssetModel.h b/TZImagePickerController/TZImagePickerController/TZAssetModel.h index b8bb80ff..475ee7ab 100755 --- a/TZImagePickerController/TZImagePickerController/TZAssetModel.h +++ b/TZImagePickerController/TZImagePickerController/TZAssetModel.h @@ -26,6 +26,8 @@ typedef enum : NSUInteger { @property (nonatomic, assign) TZAssetModelMediaType type; @property (nonatomic, copy) NSString *timeLength; @property (nonatomic, assign) BOOL iCloudFailed; +/// 是否使用 Live Photo 模式,默认 YES +@property (nonatomic, assign) BOOL useLivePhoto; /// Init a photo dataModel With a PHAsset /// 用一个PHAsset实例,初始化一个照片模型 diff --git a/TZImagePickerController/TZImagePickerController/TZAssetModel.m b/TZImagePickerController/TZImagePickerController/TZAssetModel.m index 4a534fdc..78a241ce 100644 --- a/TZImagePickerController/TZImagePickerController/TZAssetModel.m +++ b/TZImagePickerController/TZImagePickerController/TZAssetModel.m @@ -16,6 +16,7 @@ + (instancetype)modelWithAsset:(PHAsset *)asset type:(TZAssetModelMediaType)type model.asset = asset; model.isSelected = NO; model.type = type; + model.useLivePhoto = (model.type == TZAssetModelMediaTypeLivePhoto ? YES:NO); return model; } diff --git a/TZImagePickerController/TZImagePickerController/TZImageManager.h b/TZImagePickerController/TZImagePickerController/TZImageManager.h index f7f5ff15..dae312a8 100755 --- a/TZImagePickerController/TZImagePickerController/TZImageManager.h +++ b/TZImagePickerController/TZImagePickerController/TZImageManager.h @@ -80,6 +80,9 @@ // 该方法中,completion只会走一次 - (PHImageRequestID)getOriginalPhotoDataWithAsset:(PHAsset *)asset completion:(void (^)(NSData *data,NSDictionary *info,BOOL isDegraded))completion; - (PHImageRequestID)getOriginalPhotoDataWithAsset:(PHAsset *)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler completion:(void (^)(NSData *data,NSDictionary *info,BOOL isDegraded))completion; +/// Get livePhoto 获得实况图照片 +- (PHImageRequestID)getLivePhotoWithAsset:(PHAsset *)asset completion:(void (^)(PHLivePhoto *livePhoto, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler; + /// Get Image For VideoURL - (UIImage *)getImageWithVideoURL:(NSURL *)videoURL; diff --git a/TZImagePickerController/TZImagePickerController/TZImageManager.m b/TZImagePickerController/TZImagePickerController/TZImageManager.m index 3e876033..6b3d5431 100755 --- a/TZImagePickerController/TZImagePickerController/TZImageManager.m +++ b/TZImagePickerController/TZImagePickerController/TZImageManager.m @@ -17,6 +17,9 @@ @interface TZImageManager () @end @implementation TZImageManager +{ + PHCachingImageManager *_phCachingImageManager; +} CGSize AssetGridThumbnailSize; CGFloat TZScreenWidth; @@ -257,7 +260,8 @@ - (TZAssetModel *)assetModelWithAsset:(PHAsset *)asset allowPickingVideo:(BOOL)a if (!allowPickingVideo && type == TZAssetModelMediaTypeVideo) return nil; if (!allowPickingImage && type == TZAssetModelMediaTypePhoto) return nil; if (!allowPickingImage && type == TZAssetModelMediaTypePhotoGif) return nil; - + if (!allowPickingImage && type == TZAssetModelMediaTypeLivePhoto) return nil; + PHAsset *phAsset = (PHAsset *)asset; if (self.hideWhenCanNotSelect) { // 过滤掉尺寸不满足要求的图片 @@ -278,7 +282,10 @@ - (TZAssetModelMediaType)getAssetType:(PHAsset *)asset { else if (phAsset.mediaType == PHAssetMediaTypeAudio) type = TZAssetModelMediaTypeAudio; else if (phAsset.mediaType == PHAssetMediaTypeImage) { if (@available(iOS 9.1, *)) { - // if (asset.mediaSubtypes == PHAssetMediaSubtypePhotoLive) type = TZAssetModelMediaTypeLivePhoto; + // PHAssetMediaSubtype 是一个 位掩码(bitmask)类型,多个值可以 按位“或”组合在一起,判断一个类型是否包含某一项(比如是否包含 Live)时,不能用 == + // asset.mediaSubtypes & PHAssetMediaSubtypePhotoLive 等价于: + // 判断 asset.mediaSubtypes 的二进制值中,是否包含 Live Photo(1 << 2,对应二进制位是00000100) + if (asset.mediaSubtypes & PHAssetMediaSubtypePhotoLive) type = TZAssetModelMediaTypeLivePhoto; } // Gif if ([[phAsset valueForKey:@"filename"] hasSuffix:@"GIF"]) { @@ -504,6 +511,31 @@ - (PHImageRequestID)getOriginalPhotoDataWithAsset:(PHAsset *)asset progressHandl }]; } +- (PHImageRequestID)getLivePhotoWithAsset:(PHAsset *)asset completion:(void (^)(PHLivePhoto *livePhoto, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler{ + if (!asset) { + if (completion) completion(nil, nil); + return -1; + } + if ([[PHCachingImageManager class] instancesRespondToSelector:@selector(requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler:)]) { + PHLivePhotoRequestOptions *livePhotoRequestOptions = [[PHLivePhotoRequestOptions alloc] init]; + livePhotoRequestOptions.networkAccessAllowed = YES; // 允许访问网络 + livePhotoRequestOptions.progressHandler = phProgressHandler; + int32_t imageRequestID = [[[TZImageManager manager] phCachingImageManager] requestLivePhotoForAsset:asset + targetSize:PHImageManagerMaximumSize + contentMode:PHImageContentModeAspectFit + options:livePhotoRequestOptions + resultHandler:^(PHLivePhoto * _Nullable livePhoto, NSDictionary * _Nullable info) { + if (completion) { + completion(livePhoto, info); + } + }]; + return imageRequestID; + }else { + if (completion) completion(nil, nil); + return -1; + } +} + - (UIImage *)getImageWithVideoURL:(NSURL *)videoURL { AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:videoURL options:nil]; if (!asset) { @@ -1063,6 +1095,13 @@ - (UIImage *)fixOrientation:(UIImage *)aImage { return img; } +- (PHCachingImageManager *)phCachingImageManager { + if (!_phCachingImageManager) { + _phCachingImageManager = [[PHCachingImageManager alloc] init]; + } + return _phCachingImageManager; +} + #pragma clang diagnostic pop @end diff --git a/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_livephoto@2x.png b/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_livephoto@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d63bc403dca0f88dcdf446c12da983917ef65508 GIT binary patch literal 752 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dylLLH0T!HleK?W{btHuGHWmppA z7tC;zmrYD?v(&{i%jTx*DREBFkh@ZA>UylPAUoc6&%$Y)B{6E-d%}Z#SGRdO9hy~< zQa>%nl!1Zqwx^3@h{fsL;ODcN9C&xZ%Le!BfR;beYZIeboX`lgwzr?sX(cC;+vw~%vv?fk$dT;qq~A+7^U zUr5h?6&_^#@vGkDj%>|M&pcS?C^xM1x@n)3A85-sjq`e|p@ZtS3l)3LZP?Zz**Ym< zFMH*^33HbSJm6wcOq=B55_erdl;QPK!yAnWKg~|CKhVFN_jBQ)s9Eo|dn&H8CzvhX zR<6q4FF0NAqqtW(XHGO8; zcT-Y>X6;Wle!qDcL*W8X8Siak*`^&#o7 zhg%GsQdD17pYgGihF?5*PQ)>$g0)uFLgMx&O{wx$yX7Tb3=FQPXNLWmQpA zaBaR|Gn&&fjmS<*ZCk%hJ zJ-U5jP7v!>vtQCzz0JCrT0ON+zFXt9{6T-iBI5~56;oDmL|R?C^F_SKjP=B(spoZf zUJ5YH%#c5RQxTh+kafs-OtoC{m9Lk z<=x6Y4@;iDJf&{b|EvGBkIu$R&l?|D?flYTbjon0{=+h52f>IeySyC&LiWtA8WBrP z3;l)UZS8AWW=Ot?;(Yn*aC+wIm5VPibm{KB{arN9^w>$qpAUuq`f$y2&im?nS76i8 zCrS(TXBh81ymQ*})huhz@CnrfUgYa(QJ>WJ|Mx1z&XzL4vWu@jd%0)$s^3jaPN}W@ zxbB%*Rh)BfHO~P@v)LJHduN1yvO_DJx2bo?-xgWE#&Nkj0n z2!q*&xT~LR`V{#|v9H)kTs$zjB&x+?XRF{6X|a2sGwRAYdzxb}i9U8Tu1Gxkrn=!z zpQ!P!#&G`gQ_jmzELmbIG_~8&ZL;U>wPx2Bc~S82`~r$b5y4t?P7 zY4?@Ce>B48&~E1UUluIVOx&9D=K;G(rcB=V_~bVqpY}RDKgzmvCD&Y&KMR&H9;;)x z(f(}e&U1$%Y>!oL+v}5_y|N&ZVM6E&UHMiQmdVR?RAy#wIdtqB{u~N>^(Adg2p1j)TPn?d|s^Wisd&?STH3`++VEi!K~mN zrEfR9Z}whvCw2zwiB(5u?Yy>if`?xtSK`@UM~Xh|OYuyY*Y#9`Q-syqM_|(Vf6<@h zq>AHozB7H6=9|K>;@9f;wYk+Bwp~8C;Ixtkmw83vqSzNZg9~RJxY!&SRK#8W)AdS2 zLciCWJLes2=A6uV_mT7aj%m|h&laDXnO@YU{ktLMx!~i&93P%uQC?zpN~4}lzT~E# z?DB`t7*BOAzuhG*d%|Fz)5K-^MK$YEK9=nL|L?if>t4N$&Z-*Ep1etTR5ouG!vq*2t&a31%zrhQ4?r^L^Xz z6K=mRNXu?Dc*@pkR-ap@yLn}vhTYkWmX5y`7u$sX_N`t!!__g@=up4#&Px~XZ!l5O fwf*~U;(x}IoYr-BGk$FXW?lwQS3j3^P6(BJu0Z<#-~ivuGkSrp(JKk^ z3ud^<&$Du!u*}vON)lIdG}zA6abB2Tkd|cE)8=iie_-e0iLq|}tM^ug9RBS% zBbR}J@tCKJV~EA++{v9~iw$^OQx8`#Zhr605j|_k+du!`_v#*Ix%6uei}${1a+4Y! za|vg7XVh4%%_x2IEb6n66CdmB&&nGn*^4}QEw4exfa2A41h9$2D!e`>OqpT{|~r5cx*Gx836 ziJVnx#?rZTzwlLthUKbP7j!r67g5}O=|yn+v(4YU_#Ui0`fbIGW#`fsE|#1q_T1xt zp5eEDwa0RPvlUza|I@m{ZHvUNls|ue3Ef!M9QnIXsM+U?OzV`xpKTnsn6on4|McGU z+EmTi@qYT6Q*S?S3@d&%sGJe$B=zD;^h|B`;gUyqk=tltt~pYZAW ov8qelbHvvx{Ag>CtNqWwS1Z}~azoG>U|L}CboFyt=akR{09?C^AOHXW literal 0 HcmV?d00001 diff --git a/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_livephoto_slash@3x.png b/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_livephoto_slash@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a5045c0cbbed23f0a0a42856b0a566c3c301b371 GIT binary patch literal 1148 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-slLLH0T!HleK?W{btHuGHWmppA z7tC<;66ZDcGM=;f{7TYES^6dtTU~^EPwe$jYdjp`Ybh_+1@ za&p}dIwd}5p5lSE&HJa`>9{@T;I5XI7yH}3J+3)EXOmN7;JoKf`AjTq4Y^LM!tZlR zE~-8x$LG)f#xL^S#3vk2zy6pR5a1HpYD&DKMuF*bgvMv6`hAV~3qSBA-h&lIs=Hi<_6PLYupSX29 z^Ms-=5sw%8&-M>E{wnrEb6}!h=?4L)FEIj{iLLMM-1>EN)^?F+AtKR#SPYk2J;|65 zw^4Veb(_{BjdGLU7LMxIBE8&>yo&T^a49$$ZE&w6+8{WxuC-li(`~VpT<@>99WmZ1 zDCps-m8R_aB8g-1h1SRmm5*;O{h}-PChwcqZ&!^;$*Zovt5~d?Co_q8{k8R-TzSkP zO3M`=eZOMz;AHmC41Ke@z$?BXnLQW!Blaiwy-O8nWAoq9-7B#0he`ERe@-j!g7vx# zf*y(-)$DH$e`2ZKc<%|%Tvlzy^uzv^tRfeC&cxkp%ef`Io#{bib=c}_ogWrBa!kKf zbu0Kw+D!9T4&s}>Z523qi{B2_<&Q{$r>M&(@sh=iE<+RsNqg zZ@Gf#lGNM34!)UMta!k=zvx~=cv5Qkq>$hlCTZ&p4@}s%w)D`Y)ec{uCMokheQ~jy zJNs0WfP2{b6tn=Fg_gIxfIpedW zrr-Iq@aw^ChuDo*J+s@zvM*I$ +#import @class TZAssetModel; @interface TZAssetPreviewCell : UICollectionViewCell @@ -74,3 +75,33 @@ @interface TZGifPreviewCell : TZAssetPreviewCell @property (strong, nonatomic) TZPhotoPreviewView *previewView; @end + + +@interface TZLivePhotoPreviewCell : TZAssetPreviewCell + +@property (nonatomic, strong) UIImageView *imageView; +@property (nonatomic, strong) UIScrollView *scrollView; +@property (nonatomic, strong) UIView *imageContainerView; +@property (nonatomic, strong) TZProgressView *progressView; +@property (nonatomic, strong) UIImageView *iCloudErrorIcon; +@property (nonatomic, strong) UILabel *iCloudErrorLabel; +@property (nonatomic, strong) UIButton *useLivePhotoButton; +@property (nonatomic, strong) PHLivePhotoView *livePhotoView; + +@property (nonatomic, copy) void (^iCloudSyncFailedHandle)(id asset, BOOL isSyncFailed); + +@property (nonatomic, strong) id asset; +@property (nonatomic, strong) PHLivePhoto *livePhoto; +@property (nonatomic, copy) void (^imageProgressUpdateBlock)(double progress); + +@property (nonatomic, assign) int32_t imageRequestID; + +@property (nonatomic, assign) BOOL canPlayLivePhoto; + +- (void)recoverSubviews; + +- (void)prepareForDisplay; + +- (void)prepareForHide; + +@end diff --git a/TZImagePickerController/TZImagePickerController/TZPhotoPreviewCell.m b/TZImagePickerController/TZImagePickerController/TZPhotoPreviewCell.m index 1c26b7f0..adae5876 100644 --- a/TZImagePickerController/TZImagePickerController/TZPhotoPreviewCell.m +++ b/TZImagePickerController/TZImagePickerController/TZPhotoPreviewCell.m @@ -578,3 +578,335 @@ - (void)signleTapAction { } @end + + +@interface TZLivePhotoPreviewCell () + +@property (nonatomic, assign) BOOL isRequestingLive; + + +@end +@implementation TZLivePhotoPreviewCell + +- (void)configSubviews { + + _scrollView = [[UIScrollView alloc] init]; + _scrollView.bouncesZoom = YES; + _scrollView.maximumZoomScale = 4; + _scrollView.minimumZoomScale = 1.0; + _scrollView.multipleTouchEnabled = YES; + _scrollView.delegate = self; + _scrollView.scrollsToTop = NO; + _scrollView.showsHorizontalScrollIndicator = NO; + _scrollView.showsVerticalScrollIndicator = YES; + _scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _scrollView.delaysContentTouches = NO; + _scrollView.canCancelContentTouches = YES; + _scrollView.alwaysBounceVertical = NO; + if (@available(iOS 11, *)) { + _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } + [self.contentView addSubview:_scrollView]; + + _imageContainerView = [[UIView alloc] init]; + _imageContainerView.clipsToBounds = YES; + _imageContainerView.contentMode = UIViewContentModeScaleAspectFill; + [_scrollView addSubview:_imageContainerView]; + + _imageView = [[UIImageView alloc] init]; + _imageView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:0.500]; + _imageView.contentMode = UIViewContentModeScaleAspectFill; + _imageView.clipsToBounds = YES; + [_imageContainerView addSubview:_imageView]; + + _livePhotoView = [[PHLivePhotoView alloc] initWithFrame:CGRectZero]; + _livePhotoView.userInteractionEnabled = NO; + [_imageContainerView addSubview:_livePhotoView]; + + _useLivePhotoButton = [UIButton buttonWithType:UIButtonTypeCustom]; + + UIImage *openImage = [UIImage tz_imageNamedFromMyBundle:@"photo_livephoto"]; + UIImage *closeImage = [UIImage tz_imageNamedFromMyBundle:@"photo_livephoto_slash"]; + if (@available(iOS 13.0, *)) { + UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:13 weight:UIImageSymbolWeightRegular]; + openImage = [[UIImage systemImageNamed:@"livephoto" withConfiguration:config] + imageWithTintColor:[UIColor whiteColor] + renderingMode:UIImageRenderingModeAlwaysOriginal]; + + closeImage = [[UIImage systemImageNamed:@"livephoto.slash" withConfiguration:config] + imageWithTintColor:[UIColor whiteColor] + renderingMode:UIImageRenderingModeAlwaysOriginal]; + + } + [_useLivePhotoButton setImage:openImage forState:UIControlStateNormal]; + [_useLivePhotoButton setImage:closeImage forState:UIControlStateSelected]; + [_useLivePhotoButton setTitle:@" LIVE" forState:UIControlStateNormal]; + [_useLivePhotoButton setTitle:@" 关闭" forState:UIControlStateSelected]; + [_useLivePhotoButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + _useLivePhotoButton.titleLabel.font = [UIFont systemFontOfSize:12]; + [_useLivePhotoButton addTarget:self action:@selector(useLivePhotoButtonClick:) forControlEvents:UIControlEventTouchUpInside]; + _useLivePhotoButton.layer.cornerRadius = 12; + _useLivePhotoButton.layer.masksToBounds = YES; + _useLivePhotoButton.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.300]; + [_imageContainerView addSubview:_useLivePhotoButton]; + + + _iCloudErrorIcon = [[UIImageView alloc] init]; + _iCloudErrorIcon.image = [UIImage tz_imageNamedFromMyBundle:@"iCloudError"]; + _iCloudErrorIcon.hidden = YES; + [self.contentView addSubview:_iCloudErrorIcon]; + _iCloudErrorLabel = [[UILabel alloc] init]; + _iCloudErrorLabel.font = [UIFont systemFontOfSize:10]; + _iCloudErrorLabel.textColor = [UIColor whiteColor]; + _iCloudErrorLabel.text = [NSBundle tz_localizedStringForKey:@"iCloud sync failed"]; + _iCloudErrorLabel.hidden = YES; + [self.contentView addSubview:_iCloudErrorLabel]; + + UITapGestureRecognizer *tap1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)]; + [self.contentView addGestureRecognizer:tap1]; + UITapGestureRecognizer *tap2 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)]; + tap2.numberOfTapsRequired = 2; + [tap1 requireGestureRecognizerToFail:tap2]; + [self.contentView addGestureRecognizer:tap2]; + + [self configProgressView]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillResignActiveNotification) name:UIApplicationWillResignActiveNotification object:nil]; + +} + +- (void)configProgressView { + _progressView = [[TZProgressView alloc] init]; + _progressView.hidden = YES; + [self.contentView addSubview:_progressView]; +} +- (void)setModel:(TZAssetModel *)model { + + [super setModel:model]; + + [_scrollView setZoomScale:1.0 animated:NO]; + + self.canPlayLivePhoto = NO; + [self stopLivePlayback]; + self.imageView.hidden = NO; + self.livePhotoView.hidden = self.useLivePhotoButton.hidden = YES; + self.livePhotoView.livePhoto = nil; + self.imageView.image = nil; + + self.asset = model.asset; +} +- (void)setAsset:(PHAsset *)asset { + + if (_asset && self.imageRequestID) { + [[PHImageManager defaultManager] cancelImageRequest:self.imageRequestID]; + } + + _asset = asset; + + // 先显示缩略图 + [[TZImageManager manager] getPhotoWithAsset:asset completion:^(UIImage *photo, NSDictionary *info, BOOL isDegraded) { + if (photo) { + self.imageView.image = photo; + } + [self resizeSubviews]; + if (self.isRequestingLive) { + return; + } + // 再显示live图 + self.isRequestingLive = YES; + self.imageRequestID = [[TZImageManager manager] getLivePhotoWithAsset:self.model.asset completion:^(PHLivePhoto *livePhoto, NSDictionary *info) { + self.isRequestingLive = NO; + self.progressView.hidden = YES; + if (!livePhoto){ + self.imageRequestID = 0; + self.livePhotoView.hidden = self.useLivePhotoButton.hidden = YES; + self.imageView.hidden = NO; + return; + } + + self.livePhoto = livePhoto; + self.livePhotoView.livePhoto = self.livePhoto; + + self.livePhotoView.hidden = self.useLivePhotoButton.hidden = NO; + self.useLivePhotoButton.selected = !self.model.useLivePhoto; + self.imageView.hidden = YES; + + [self resizeSubviews]; + + // ✅ 加载完成后,根据当前是否允许播放来决定 + if (self.canPlayLivePhoto && self.model.useLivePhoto) { + [self startLivePlayback]; + } + } withProgressHandler:^(double progress, NSError * _Nullable error, BOOL * _Nonnull stop, NSDictionary * _Nullable info) { + progress = progress > 0.02 ? progress : 0.02; + dispatch_async(dispatch_get_main_queue(), ^{ + BOOL iCloudSyncFailed = [TZCommonTools isICloudSyncError:error]; + self.iCloudErrorLabel.hidden = !iCloudSyncFailed; + self.iCloudErrorIcon.hidden = !iCloudSyncFailed; + if (self.iCloudSyncFailedHandle) { + self.iCloudSyncFailedHandle(asset, iCloudSyncFailed); + } + + self.progressView.progress = progress; + if (progress >= 1) { + self.progressView.hidden = YES; + self.imageRequestID = 0; + } else { + self.progressView.hidden = NO; + } + }); + }]; + } progressHandler:nil networkAccessAllowed:NO]; + + [self configMaximumZoomScale]; +} +- (void)recoverSubviews { + [_scrollView setZoomScale:_scrollView.minimumZoomScale animated:NO]; + [self resizeSubviews]; +} +- (void)resizeSubviews { + _imageContainerView.tz_origin = CGPointZero; + _imageContainerView.tz_width = self.scrollView.tz_width; + + UIImage *image = _imageView.image; + if (image.size.height / image.size.width > self.tz_height / self.scrollView.tz_width) { + CGFloat width = image.size.width / image.size.height * self.scrollView.tz_height; + if (width < 1 || isnan(width)) width = self.tz_width; + width = floor(width); + + _imageContainerView.tz_width = width; + _imageContainerView.tz_height = self.tz_height; + _imageContainerView.tz_centerX = self.scrollView.tz_width / 2; + } else { + CGFloat height = image.size.height / image.size.width * self.scrollView.tz_width; + if (height < 1 || isnan(height)) height = self.tz_height; + height = floor(height); + _imageContainerView.tz_height = height; + _imageContainerView.tz_centerY = self.tz_height / 2; + } + if (_imageContainerView.tz_height > self.tz_height && _imageContainerView.tz_height - self.tz_height <= 1) { + _imageContainerView.tz_height = self.tz_height; + } + CGFloat contentSizeH = MAX(_imageContainerView.tz_height, self.tz_height); + _scrollView.contentSize = CGSizeMake(self.scrollView.tz_width, contentSizeH); + [_scrollView scrollRectToVisible:self.bounds animated:NO]; + _scrollView.alwaysBounceVertical = _imageContainerView.tz_height <= self.tz_height ? NO : YES; + _imageView.frame = _imageContainerView.bounds; + _livePhotoView.frame = _imageContainerView.bounds; +} + +- (void)configMaximumZoomScale { + + _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)layoutSubviews { + [super layoutSubviews]; + _scrollView.frame = CGRectMake(10, 0, self.tz_width - 20, self.tz_height); + static CGFloat progressWH = 40; + CGFloat progressX = (self.tz_width - progressWH) / 2; + CGFloat progressY = (self.tz_height - progressWH) / 2; + _progressView.frame = CGRectMake(progressX, progressY, progressWH, progressWH); + [self recoverSubviews]; + _iCloudErrorIcon.frame = CGRectMake(20, [TZCommonTools tz_statusBarHeight] + 44 + 10, 28, 28); + _iCloudErrorLabel.frame = CGRectMake(53, [TZCommonTools tz_statusBarHeight] + 44 + 10, self.tz_width - 63, 28); + CGFloat MinY = 0; + CGFloat imageContainerMinY = CGRectGetMinY(self.imageContainerView.frame); + if (imageContainerMinY < [TZCommonTools tz_statusBarHeight] + 44) { + MinY = ([TZCommonTools tz_statusBarHeight] + 44) - imageContainerMinY; + } + self.useLivePhotoButton.frame = CGRectMake(CGRectGetMinX(self.livePhotoView.frame) + 16, MinY + 16, 56, 24); +} +#pragma mark - UITapGestureRecognizer Event +- (void)doubleTap:(UITapGestureRecognizer *)tap { + if (_scrollView.zoomScale > _scrollView.minimumZoomScale) { + _scrollView.contentInset = UIEdgeInsetsZero; + [_scrollView setZoomScale:_scrollView.minimumZoomScale animated:YES]; + } else { + CGPoint touchPoint = [tap locationInView:self.imageView]; + CGFloat newZoomScale = MIN(_scrollView.maximumZoomScale, 2.5); + CGFloat xsize = self.frame.size.width / newZoomScale; + CGFloat ysize = self.frame.size.height / newZoomScale; + [_scrollView zoomToRect:CGRectMake(touchPoint.x - xsize/2, touchPoint.y - ysize/2, xsize, ysize) animated:YES]; + } +} + +- (void)singleTap:(UITapGestureRecognizer *)tap { + if (self.singleTapGestureBlock) { + self.singleTapGestureBlock(); + } +} +- (void)prepareForDisplay { + self.canPlayLivePhoto = YES; + [self recoverSubviews]; + if (self.livePhoto && self.model.useLivePhoto) { + [self startLivePlayback]; + } +} + +- (void)prepareForHide { + self.canPlayLivePhoto = NO; + [self stopLivePlayback]; + [self recoverSubviews]; +} + +- (void)startLivePlayback { + if (!self.livePhoto || !self.model.useLivePhoto || !self.canPlayLivePhoto) { + [self stopLivePlayback]; + return; + } + if (self.livePhotoView.livePhoto != self.livePhoto) { + self.livePhotoView.livePhoto = self.livePhoto; + } + // ✅ 播放前加入轻微震动反馈 + if (@available(iOS 10.0, *)) { + UIImpactFeedbackGenerator *feedback = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; + [feedback prepare]; + [feedback impactOccurred]; + } + [self.livePhotoView startPlaybackWithStyle:PHLivePhotoViewPlaybackStyleFull]; +} + +- (void)useLivePhotoButtonClick:(UIButton *)sender { + self.model.useLivePhoto = !self.model.useLivePhoto; + self.useLivePhotoButton.selected = !self.model.useLivePhoto; + [self startLivePlayback]; +} +- (void)stopLivePlayback { + [self.livePhotoView stopPlayback]; +} +- (void)appWillResignActiveNotification { + [self stopLivePlayback]; +} + +//MARK: UIScrollViewDelegate +- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { + return _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 { + +} +//MARK: Private +- (void)refreshImageContainerViewCenter { + CGFloat offsetX = (_scrollView.tz_width > _scrollView.contentSize.width) ? ((_scrollView.tz_width - _scrollView.contentSize.width) * 0.5) : 0.0; + CGFloat offsetY = (_scrollView.tz_height > _scrollView.contentSize.height) ? ((_scrollView.tz_height - _scrollView.contentSize.height) * 0.5) : 0.0; + self.imageContainerView.center = CGPointMake(_scrollView.contentSize.width * 0.5 + offsetX, _scrollView.contentSize.height * 0.5 + offsetY); +} + + +@end diff --git a/TZImagePickerController/TZImagePickerController/TZPhotoPreviewController.m b/TZImagePickerController/TZImagePickerController/TZPhotoPreviewController.m index b8edd50a..d731801c 100644 --- a/TZImagePickerController/TZImagePickerController/TZPhotoPreviewController.m +++ b/TZImagePickerController/TZImagePickerController/TZPhotoPreviewController.m @@ -213,7 +213,8 @@ - (void)configCollectionView { [_collectionView registerClass:[TZPhotoPreviewCell class] forCellWithReuseIdentifier:@"TZPhotoPreviewCellGIF"]; [_collectionView registerClass:[TZVideoPreviewCell class] forCellWithReuseIdentifier:@"TZVideoPreviewCell"]; [_collectionView registerClass:[TZGifPreviewCell class] forCellWithReuseIdentifier:@"TZGifPreviewCell"]; - + [_collectionView registerClass:[TZLivePhotoPreviewCell class] forCellWithReuseIdentifier:@"TZLivePhotoPreviewCell"]; + TZImagePickerController *_tzImagePickerVc = (TZImagePickerController *)self.navigationController; if (_tzImagePickerVc.scaleAspectFillCrop && _tzImagePickerVc.allowCrop) { _collectionView.scrollEnabled = NO; @@ -522,6 +523,13 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cell model.iCloudFailed = isSyncFailed; [weakSelf didICloudSyncStatusChanged:model]; }; + }else if (model.type == TZAssetModelMediaTypeLivePhoto && _tzImagePickerVc.allowPickingLiveImage) { + cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TZLivePhotoPreviewCell" forIndexPath:indexPath]; + TZLivePhotoPreviewCell *currentCell = (TZLivePhotoPreviewCell *)cell; + currentCell.iCloudSyncFailedHandle = ^(id asset, BOOL isSyncFailed) { + model.iCloudFailed = isSyncFailed; + [weakSelf didICloudSyncStatusChanged:model]; + }; } else { NSString *reuseId = model.type == TZAssetModelMediaTypePhotoGif ? @"TZPhotoPreviewCellGIF" : @"TZPhotoPreviewCell"; cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseId forIndexPath:indexPath]; @@ -564,6 +572,8 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cell - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { if ([cell isKindOfClass:[TZPhotoPreviewCell class]]) { [(TZPhotoPreviewCell *)cell recoverSubviews]; + }else if ([cell isKindOfClass:[TZLivePhotoPreviewCell class]]) { + [(TZLivePhotoPreviewCell *)cell prepareForDisplay]; } } @@ -575,6 +585,8 @@ - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:( if (videoCell.player && videoCell.player.rate != 0.0) { [videoCell pausePlayerAndShowNaviBar]; } + } else if ([cell isKindOfClass:[TZLivePhotoPreviewCell class]]) { + [(TZLivePhotoPreviewCell *)cell prepareForHide]; } }