Files
peko-ios/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.m
edwinQQQ 99db078b62 feat(勋章): 新增多等级高亮功能并优化勋章展示逻辑
refactor(勋章排行): 重构排行榜分页加载和刷新逻辑

fix(会话): 优化官方账号判断逻辑和跳转处理

style(UI): 调整勋章排行榜和游戏菜单UI布局

chore: 更新Podfile配置和bitcode框架列表
2025-07-03 19:35:17 +08:00

538 lines
17 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// MedalsCollectionViewCell.m
// YuMi
//
// Created by P on 2025/6/18.
//
#import "MedalsCollectionViewCell.h"
#import "MedalsModel.h"
#import <QGVAPWrapView.h>
#import "XPRoomGiftAnimationParser.h"
#import "MedalsLevelIndicatorView.h"
@interface MedalsCollectionViewCell () <VAPWrapViewDelegate, HWDMP4PlayDelegate>
@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