// // MedalsCollectionViewCell.m // YuMi // // Created by P on 2025/6/18. // #import "MedalsCollectionViewCell.h" #import "MedalsModel.h" #import #import "XPRoomGiftAnimationParser.h" #import "MedalsLevelIndicatorView.h" @interface MedalsCollectionViewCell () @property(nonatomic, copy) NSString *imagePath; @property(nonatomic, copy) NSString *mp4Path; @property(nonatomic, strong) NetImageView *imageView; @property(nonatomic, strong) VAPView *mp4View; @property(nonatomic, strong) XPRoomGiftAnimationParser *mp4Parser; @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UILabel *subLabel; @property (nonatomic, strong) MedalsLevelIndicatorView *levelIndicatorView; @property (nonatomic, strong) MedalVo *displayModel; @property (nonatomic, strong) MedalSeriesItemVo *currentItemVo; @property (nonatomic, assign) BOOL isVisible; // 跟踪 cell 是否可见 @end @implementation MedalsCollectionViewCell + (NSString *)cellID { return NSStringFromClass([MedalsCollectionViewCell class]); } + (void)registerTo:(UICollectionView *)collectionView { [collectionView registerClass:[self class] forCellWithReuseIdentifier:[self cellID]]; } + (instancetype)cellFor:(UICollectionView *)collectionView atIndexPath:(NSIndexPath *)index { MedalsCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[self cellID] forIndexPath:index]; return cell; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self.contentView addGradientBackgroundWithColors:@[ UIColorFromRGB(0x41007b), UIColorFromRGB(0x290858) ] startPoint:CGPointMake(0.5, 0) endPoint:CGPointMake(0.5, 1) cornerRadius:8]; [self.contentView setAllCornerRadius:8 borderWidth:1 borderColor:UIColorFromRGB(0xa166bf)]; self.imageView = [[NetImageView alloc] init]; self.imageView.contentMode = UIViewContentModeScaleAspectFill; [self.contentView addSubview:self.imageView]; [self.imageView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.mas_equalTo(self.contentView); make.top.mas_equalTo(13); make.leading.trailing.mas_equalTo(self.contentView).inset(13); make.height.mas_equalTo(self.imageView.mas_width); }]; [self.contentView addSubview:self.mp4View]; [self.mp4View mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.mas_equalTo(self.imageView); }]; self.titleLabel = [UILabel labelInitWithText:@"" font:kFontMedium(14) textColor:[UIColor whiteColor]]; self.titleLabel.adjustsFontSizeToFitWidth = YES; self.titleLabel.minimumScaleFactor = 0.5; self.subLabel = [UILabel labelInitWithText:@"" font:kFontRegular(11) textColor:[UIColor colorWithWhite:1 alpha:0.6]]; self.titleLabel.textAlignment = NSTextAlignmentCenter; self.subLabel.textAlignment = NSTextAlignmentCenter; [self.contentView addSubview:self.titleLabel]; [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.mas_equalTo(self.contentView); make.top.mas_equalTo(self.imageView.mas_bottom).offset(3); make.leading.trailing.mas_equalTo(self.contentView).inset(10); }]; [self.contentView addSubview:self.subLabel]; [self.subLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.mas_equalTo(self.contentView); make.top.mas_equalTo(self.titleLabel.mas_bottom).offset(7); make.leading.trailing.mas_equalTo(self.contentView).inset(3); }]; // 添加等级指示器 self.levelIndicatorView = [[MedalsLevelIndicatorView alloc] init]; [self.contentView addSubview:self.levelIndicatorView]; [self.levelIndicatorView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.mas_equalTo(self.contentView); make.bottom.mas_equalTo(self.contentView).offset(-8); make.leading.trailing.mas_greaterThanOrEqualTo(self.contentView).inset(8); make.height.mas_equalTo(26); // 增加高度以适应圆点和文本 }]; // 设置等级选择回调 // @kWeakify(self); // self.levelIndicatorView.levelSelectedBlock = ^(NSInteger level) { // @kStrongify(self); // // 处理等级选择事件 // if (self.currentItemVo && level <= self.currentItemVo.medalVos.count) { // MedalVo *selectedMedalVo = [self.currentItemVo.medalVos xpSafeObjectAtIndex:level - 1]; // if (selectedMedalVo) { // self.displayModel = selectedMedalVo; // [self updateDisplayWithCurrentModel]; // } // } // }; // 设置通知监听 [self setupNotifications]; } return self; } - (void)setupNotifications { // 监听应用进入后台和恢复前台的通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; // 监听内存警告通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; } - (void)prepareForReuse { [super prepareForReuse]; // 停止播放 [self stopMP4Playback]; // 更彻底地重置 mp4View [self resetMP4View]; // 隐藏视图 self.mp4View.hidden = YES; self.imageView.hidden = YES; // 重置状态 self.mp4Path = nil; self.imagePath = nil; self.isVisible = NO; self.displayModel = nil; // 重置数据模型 self.currentItemVo = nil; // 重置当前项 // 清空文本 self.titleLabel.text = @""; self.subLabel.text = @""; // 重置等级指示器 [self.levelIndicatorView resetToLevel:0]; // 清空图片 self.imageView.image = nil; self.imageView.imageUrl = @""; } - (void)updateCell:(MedalSeriesItemVo *)model isForSquare:(BOOL)isSquare { // MedalSeriesItemVo *itemVos = [model.medalSeries xpSafeObjectAtIndex:0]; self.currentItemVo = model; // 设置指示器类型为不带图片 self.levelIndicatorView.indicatorType = MedalsLevelIndicatorTypeNormal; // 配置等级指示器 [self.levelIndicatorView setupWithMaxLevel:model.medalLevel]; if (isSquare) { self.subLabel.hidden = YES; self.displayModel = [model.medalVos xpSafeObjectAtIndex:model.medalVos.count - 1]; [self.levelIndicatorView setSelectedLevel:model.medalLevel animated:NO]; } else { self.levelIndicatorView.userInteractionEnabled = YES; self.displayModel = [model.medalVos xpSafeObjectAtIndex:0]; NSMutableArray *arr = @[].mutableCopy; for (MedalVo *vo in model.medalVos) { [arr addObject:@(vo.level)]; if (vo.level > self.displayModel.level) { self.displayModel = vo; } } [self.levelIndicatorView setHighlightLevels:arr animated:NO]; } self.levelIndicatorView.userInteractionEnabled = !isSquare; [self updateDisplayWithCurrentModel]; } - (void)updateDisplayWithCurrentModel { if (!self.displayModel || !self.currentItemVo) { [self showDefaultPlaceholder]; return; } // 优化后的判断逻辑:更严格的 MP4 URL 验证 NSString *mp4Url = self.displayModel.mp4Url; NSString *picUrl = self.displayModel.picUrl; // 首先检查是否有明确的 MP4 URL if (![NSString isEmpty:mp4Url] && [self isValidMP4URL:mp4Url]) { [self setMp4Path:mp4Url]; [self updateTextLabels]; return; } // 检查 picUrl 是否实际上是 MP4(兼容旧逻辑) if (![NSString isEmpty:picUrl]) { if ([self isValidMP4URL:picUrl]) { [self setMp4Path:picUrl]; [self updateTextLabels]; return; } else if ([NSString isValidImageURL:picUrl]) { [self setImagePath:picUrl]; [self updateTextLabels]; return; } } // 都不满足,显示默认占位图 [self showDefaultPlaceholder]; // 设置文本信息(无论是否有有效的媒体内容都要设置) [self updateTextLabels]; } #pragma mark - 私有方法 /// 验证是否为有效的 MP4 URL - (BOOL)isValidMP4URL:(NSString *)url { if ([NSString isEmpty:url]) { return NO; } // 检查 URL 是否以 mp4 结尾或包含 mp4 标识 NSString *lowercaseUrl = [url lowercaseString]; return [lowercaseUrl hasSuffix:@".mp4"] || [lowercaseUrl containsString:@"mp4"] || [lowercaseUrl containsString:@"video"]; } /// 更新文本标签 - (void)updateTextLabels { self.titleLabel.text = self.currentItemVo.seriesName; self.subLabel.text = [self.displayModel expireDateString]; } /// 显示默认占位图 - (void)showDefaultPlaceholder { [self stopMP4Playback]; self.mp4View.hidden = YES; self.imageView.hidden = NO; // 设置默认占位图 self.imageView.imageUrl = @""; self.imageView.image = [UIImageConstant defaultEmptyPlaceholder]; } /// 根据新的优先级逻辑获取勋章的图片URL - (NSString *)getImageUrlForMedal:(MedalVo *)medal { if (!medal) { return @""; } #if 0 // Debug 环境:如果数据不满足新条件,使用原有逻辑 if ([NSString isEmpty:medal.mp4Url] && ![NSString isEmpty:medal.picUrl]) { return medal.picUrl; } #endif // 新逻辑:优先使用 mp4URL if (![NSString isEmpty:medal.mp4Url]) { return medal.mp4Url; } else if (![NSString isEmpty:medal.picUrl]) { // mp4URL 为空,使用 picURL (需要验证是否为有效图片) if ([NSString isValidImageURL:medal.picUrl]) { return medal.picUrl; } else { // picURL 不是有效图片,返回空字符串 return @""; } } else { // 都为空,返回空字符串 return @""; } } - (void)setImagePath:(NSString *)imagePath { if ([NSString isEmpty:imagePath]) { self.imageView.hidden = YES; return; } // 停止之前的 mp4 播放 [self stopMP4Playback]; _imagePath = imagePath; self.mp4View.hidden = YES; self.imageView.hidden = NO; self.imageView.imageUrl = imagePath; } - (void)setMp4Path:(NSString *)mp4Path { if ([NSString isEmpty:mp4Path]) { self.mp4View.hidden = YES; return; } // 停止之前的 mp4 播放 [self stopMP4Playback]; _mp4Path = mp4Path; self.mp4View.hidden = NO; self.imageView.hidden = YES; if (!_mp4Parser) { self.mp4Parser = [[XPRoomGiftAnimationParser alloc] init]; } @kWeakify(self); [self.mp4Parser parseWithURL:mp4Path completionBlock:^(NSString * _Nullable videoUrl) { @kStrongify(self); if (![NSString isEmpty:videoUrl]) { // 延迟播放机制:先标记为准备就绪,等待明确的播放信号 self.mp4View.tag = 1; // 标记已准备好播放 // 如果当前 Cell 可见,立即播放 if (self.isVisible) { [self startMP4PlaybackWithURL:videoUrl]; } } } failureBlock:^(NSError * _Nullable error) { @kStrongify(self); NSLog(@"[MedalsCollectionViewCell] Failed to parse mp4: %@, fallback to image", error); // MP4 解析失败,降级使用 picURL [self handleMP4FailureWithFallback]; }]; } /// 开始 MP4 播放 - (void)startMP4PlaybackWithURL:(NSString *)videoUrl { if (![NSString isEmpty:videoUrl] && self.mp4View && !self.mp4View.hidden) { [self.mp4View playHWDMP4:videoUrl repeatCount:-1 delegate:self]; NSLog(@"[MedalsCollectionViewCell] Started MP4 playback: %@", videoUrl); } } /// 处理 MP4 播放失败,降级使用 picURL - (void)handleMP4FailureWithFallback { if (![NSString isEmpty:self.displayModel.picUrl]) { if ([NSString isValidImageURL:self.displayModel.picUrl]) { [self setImagePath:self.displayModel.picUrl]; } else { [self showDefaultPlaceholder]; } } else { [self showDefaultPlaceholder]; } } #pragma mark - MP4 播放控制 /// 重置 MP4 视图,清理所有缓存内容 - (void)resetMP4View { if (self.mp4View) { // 方案1: 基础重置(推荐) [self.mp4View stopHWDMP4]; self.mp4View.tag = 0; self.mp4View.hidden = YES; // 方案2: 完全重置(如果遇到严重的缓存问题才启用) if (YES) { // 根据需要设置为 NO 来禁用完全重置 [self.mp4View removeFromSuperview]; _mp4View = nil; // 重新添加 mp4View [self.contentView addSubview:self.mp4View]; [self.mp4View mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.mas_equalTo(self.imageView); }]; } // 方案3: 强制清理内存(备用方案) // 通过设置新的frame来触发内部清理 if (NO) { // 可以启用此方案作为替代 CGRect currentFrame = self.mp4View.frame; self.mp4View.frame = CGRectZero; self.mp4View.frame = currentFrame; } } } - (void)stopMP4Playback { if (self.mp4View) { [self.mp4View stopHWDMP4]; self.mp4View.tag = 0; // 重置播放状态标记 } } - (void)pauseMP4Playback { if (self.mp4View && !self.mp4View.hidden) { [self.mp4View stopHWDMP4]; } } - (void)resumeMP4Playback { if (self.mp4View && !self.mp4View.hidden && self.mp4Path) { if (self.mp4View.tag == 1) { // 已准备好但尚未播放 @kWeakify(self); [self.mp4Parser parseWithURL:self.mp4Path completionBlock:^(NSString * _Nullable videoUrl) { @kStrongify(self); if (![NSString isEmpty:videoUrl] && self.isVisible) { [self startMP4PlaybackWithURL:videoUrl]; } } failureBlock:^(NSError * _Nullable error) { @kStrongify(self); // MP4 恢复播放失败,降级使用 picURL [self handleMP4FailureWithFallback]; }]; } else { [self.mp4View resumeHWDMP4]; } } } - (void)vapWrap_viewDidFailPlayMP4:(NSError *)error { NSLog(@"%@", error); } #pragma mark - HWDMP4PlayDelegate - (BOOL)shouldStartPlayMP4:(VAPView *)container config:(QGVAPConfigModel *)config { return YES; } - (void)viewDidFinishPlayMP4:(NSInteger)totalFrameCount view:(VAPView *)container { // MP4 播放完成,循环播放会自动重新开始 } - (void)viewDidStopPlayMP4:(NSInteger)lastFrameIndex view:(VAPView *)container { // MP4 播放停止 } - (void)viewDidFailPlayMP4:(NSError *)error { NSLog(@"MP4 播放失败: %@", error); // MP4 播放失败,降级使用 picURL [self handleMP4FailureWithFallback]; } #pragma mark - 可见性管理 - (void)willDisplay { self.isVisible = YES; // return; // 如果有准备好的 MP4,立即开始播放 if (self.mp4View.tag == 1 && !self.mp4View.hidden && self.mp4Path) { [self resumeMP4Playback]; } else if (!self.mp4View.hidden) { // 恢复之前暂停的播放 [self resumeMP4Playback]; } NSLog(@"[MedalsCollectionViewCell] willDisplay - isVisible: %@, mp4Path: %@", self.isVisible ? @"YES" : @"NO", self.mp4Path ?: @"nil"); } - (void)didEndDisplaying { self.isVisible = NO; [self pauseMP4Playback]; } #pragma mark - 通知处理 - (void)appDidEnterBackground { [self pauseMP4Playback]; } - (void)appWillEnterForeground { if (self.isVisible) { [self resumeMP4Playback]; } } - (void)didReceiveMemoryWarning { // 内存警告时停止播放 if (!self.isVisible) { [self stopMP4Playback]; } } #pragma mark - 生命周期 - (void)dealloc { // 停止播放 [self stopMP4Playback]; // 移除通知观察者 [[NSNotificationCenter defaultCenter] removeObserver:self]; // 清理资源 self.mp4Parser = nil; NSLog(@"MedalsCollectionViewCell dealloc"); } - (VAPView *)mp4View { if (!_mp4View) { _mp4View = [[VAPView alloc] init]; _mp4View.contentMode = UIViewContentModeScaleAspectFit; // [_mp4View enableOldVersion:YES]; } return _mp4View; } @end