diff --git a/YuMi.xcodeproj/project.pbxproj b/YuMi.xcodeproj/project.pbxproj index 0079c57a..ecf3371e 100644 --- a/YuMi.xcodeproj/project.pbxproj +++ b/YuMi.xcodeproj/project.pbxproj @@ -486,6 +486,8 @@ 23FF42792AA6E19C0055733C /* HomeMenuSourceModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 23FF42782AA6E19C0055733C /* HomeMenuSourceModel.m */; }; 23FF428E2AAB2D3A0055733C /* XPCandyTreeBuyView.m in Sources */ = {isa = PBXBuildFile; fileRef = 23FF428D2AAB2D3A0055733C /* XPCandyTreeBuyView.m */; }; 4C0A5B842E02675300955219 /* MedalsCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A5B832E02675300955219 /* MedalsCollectionViewCell.m */; }; + 4C0A5B872E02BB1100955219 /* MedalsLevelIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A5B862E02BB1100955219 /* MedalsLevelIndicatorView.m */; }; + 4C0A5B8A2E02BC3900955219 /* MedalsDetailView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A5B892E02BC3900955219 /* MedalsDetailView.m */; }; 4C1064882E0014CF007E1586 /* NSMutableArray+Safe.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C1064872E0014CF007E1586 /* NSMutableArray+Safe.m */; }; 4C1119722DD7218300C18416 /* MyEventsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C1119712DD7218300C18416 /* MyEventsViewController.m */; }; 4C1392932D6D963700A6DFB5 /* SubRechargersViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C1392922D6D963600A6DFB5 /* SubRechargersViewController.m */; }; @@ -2637,6 +2639,10 @@ 23FF428D2AAB2D3A0055733C /* XPCandyTreeBuyView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XPCandyTreeBuyView.m; sourceTree = ""; }; 4C0A5B822E02675300955219 /* MedalsCollectionViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MedalsCollectionViewCell.h; sourceTree = ""; }; 4C0A5B832E02675300955219 /* MedalsCollectionViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MedalsCollectionViewCell.m; sourceTree = ""; }; + 4C0A5B852E02BB1100955219 /* MedalsLevelIndicatorView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MedalsLevelIndicatorView.h; sourceTree = ""; }; + 4C0A5B862E02BB1100955219 /* MedalsLevelIndicatorView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MedalsLevelIndicatorView.m; sourceTree = ""; }; + 4C0A5B882E02BC3900955219 /* MedalsDetailView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MedalsDetailView.h; sourceTree = ""; }; + 4C0A5B892E02BC3900955219 /* MedalsDetailView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MedalsDetailView.m; sourceTree = ""; }; 4C1064862E0014CF007E1586 /* NSMutableArray+Safe.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMutableArray+Safe.h"; sourceTree = ""; }; 4C1064872E0014CF007E1586 /* NSMutableArray+Safe.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMutableArray+Safe.m"; sourceTree = ""; }; 4C1119702DD7218300C18416 /* MyEventsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MyEventsViewController.h; sourceTree = ""; }; @@ -6848,6 +6854,10 @@ 4C886BE72E013C55006F0BA7 /* MedalsViewController.m */, 4C0A5B822E02675300955219 /* MedalsCollectionViewCell.h */, 4C0A5B832E02675300955219 /* MedalsCollectionViewCell.m */, + 4C0A5B852E02BB1100955219 /* MedalsLevelIndicatorView.h */, + 4C0A5B862E02BB1100955219 /* MedalsLevelIndicatorView.m */, + 4C0A5B882E02BC3900955219 /* MedalsDetailView.h */, + 4C0A5B892E02BC3900955219 /* MedalsDetailView.m */, ); path = Medals; sourceTree = ""; @@ -12589,6 +12599,7 @@ 23E9E99E2A80C7AF00B792F2 /* XPMineGuildPersonalBillRecordItemView.m in Sources */, E87DF4F52A42CC49009C1185 /* HomeMenuInfoModel.m in Sources */, 9BD63FAB277EE885006EB744 /* Api+RoomRadio.m in Sources */, + 4C0A5B8A2E02BC3900955219 /* MedalsDetailView.m in Sources */, E8A30BF328534B17003B4873 /* Api+FindNew.m in Sources */, E852D7412863249F001465ED /* XPMonentsReplyTableViewCell.m in Sources */, 237B94BC2A984DA7007853E3 /* XPTrumpetPresenter.m in Sources */, @@ -13178,6 +13189,7 @@ 545831A02C2AEFAF00364026 /* TenMicStageView.m in Sources */, 9BE9F0FF27FED76500667200 /* XPAnchorFansTaskModel.m in Sources */, E85E7BAA2A4EC99300B6D00A /* XPMineGiveDiamondPwdView.m in Sources */, + 4C0A5B872E02BB1100955219 /* MedalsLevelIndicatorView.m in Sources */, 23FF428E2AAB2D3A0055733C /* XPCandyTreeBuyView.m in Sources */, E88C72922828EA4E0047FB2B /* Music+CoreDataClass.m in Sources */, E84A2EA52A5288CB00D6AF8A /* XPGoldDetailsChooseRoomView.m in Sources */, diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.h b/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.h index cb0fd2d2..4fecc55f 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.h +++ b/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.h @@ -16,6 +16,18 @@ NS_ASSUME_NONNULL_BEGIN - (void)updateCell:(MedalSeriesVo *)model; +/** + * 当 cell 将要显示时调用 + * 用于恢复 mp4 播放 + */ +- (void)willDisplay; + +/** + * 当 cell 结束显示时调用 + * 用于暂停 mp4 播放 + */ +- (void)didEndDisplaying; + @end NS_ASSUME_NONNULL_END diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.m b/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.m index d6c4b0ac..9de94ebc 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.m +++ b/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.m @@ -9,241 +9,8 @@ #import "MedalsModel.h" #import #import "XPRoomGiftAnimationParser.h" +#import "MedalsLevelIndicatorView.h" -// 等级指示器视图 -@interface LevelItemView : UIView - -@property (nonatomic, strong) UILabel *levelLabel; -@property (nonatomic, strong) UIView *dotView; // 圆点视图 -@property (nonatomic, assign) BOOL isSelected; -@property (nonatomic, assign) NSInteger level; - -- (instancetype)initWithLevel:(NSInteger)level; -- (void)setSelected:(BOOL)selected animated:(BOOL)animated; - -@end - -@implementation LevelItemView - -- (instancetype)initWithLevel:(NSInteger)level { - self = [super init]; - if (self) { - _level = level; - _isSelected = NO; - - // 创建圆点视图 - _dotView = [[UIView alloc] init]; - _dotView.backgroundColor = UIColorFromRGB(0x8B54E8); - _dotView.layer.cornerRadius = 3; // 圆点半径 - _dotView.clipsToBounds = YES; - [self addSubview:_dotView]; - - // 创建等级标签 - self.levelLabel = [UILabel labelInitWithText:[NSString stringWithFormat:@"LV%ld", (long)level] - font:kFontMedium(10) - textColor:UIColorFromRGB(0x8B54E8)]; - self.levelLabel.textAlignment = NSTextAlignmentCenter; - [self addSubview:self.levelLabel]; - - // 布局 - [_dotView mas_makeConstraints:^(MASConstraintMaker *make) { - make.centerX.mas_equalTo(self); - make.top.mas_equalTo(self); - make.width.height.mas_equalTo(6); // 圆点大小 - }]; - - [self.levelLabel mas_makeConstraints:^(MASConstraintMaker *make) { - make.centerX.mas_equalTo(self); - make.top.mas_equalTo(_dotView.mas_bottom).offset(4); // 圆点和文字的间距 - make.leading.trailing.bottom.mas_equalTo(self); - }]; - } - return self; -} - -- (void)setSelected:(BOOL)selected animated:(BOOL)animated { - if (_isSelected == selected) { - return; - } - - _isSelected = selected; - - void (^updateBlock)(void) = ^{ - UIColor *color = selected ? [UIColor whiteColor] : UIColorFromRGB(0x8B54E8); - self.levelLabel.textColor = color; - self.dotView.backgroundColor = color; - }; - - if (animated) { - [UIView animateWithDuration:0.2 animations:updateBlock]; - } else { - updateBlock(); - } -} - -@end - -// 等级指示器容器视图 -@interface LevelIndicatorView : UIView - -@property (nonatomic, strong) NSMutableArray *levelItems; -@property (nonatomic, strong) NSMutableArray *connectionLines; // 连接线数组 -@property (nonatomic, assign) NSInteger maxLevel; -@property (nonatomic, assign) NSInteger selectedLevel; -@property (nonatomic, copy) void (^levelSelectedBlock)(NSInteger level); - -@end - -@implementation LevelIndicatorView - -- (instancetype)init { - self = [super init]; - if (self) { - _levelItems = [NSMutableArray array]; - _connectionLines = [NSMutableArray array]; - _selectedLevel = 1; - - UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; - [self addGestureRecognizer:tapGesture]; - } - return self; -} - -- (void)setupWithMaxLevel:(NSInteger)maxLevel { - if (maxLevel < 0) { - maxLevel = 1; - } - if (_maxLevel == maxLevel && _levelItems.count == maxLevel) { - return; - } - - _maxLevel = maxLevel; - - // 清除现有的等级指示器和连接线 - for (UIView *view in _levelItems) { - [view removeFromSuperview]; - } - [_levelItems removeAllObjects]; - - for (UIView *line in _connectionLines) { - [line removeFromSuperview]; - } - [_connectionLines removeAllObjects]; - - // 创建新的等级指示器 - CGFloat itemWidth = 25.0; // 每个等级指示器的宽度 - CGFloat spacing = 15.0; // 等级指示器之间的间距 - CGFloat totalWidth = itemWidth * maxLevel + spacing * (maxLevel - 1); - CGFloat startX = (self.bounds.size.width - totalWidth) / 2; - - for (NSInteger i = 1; i <= maxLevel; i++) { - // 创建等级指示器 - LevelItemView *levelItem = [[LevelItemView alloc] initWithLevel:i]; - [self addSubview:levelItem]; - [_levelItems addObject:levelItem]; - - // 设置位置 - CGFloat x = startX + (i - 1) * (itemWidth + spacing); - [levelItem mas_makeConstraints:^(MASConstraintMaker *make) { - make.width.mas_equalTo(itemWidth); - make.height.mas_equalTo(self); - make.centerY.mas_equalTo(self); - make.leading.mas_equalTo(self).offset(x); - }]; - - // 如果不是第一个,添加连接线 - if (i > 1) { - UIView *line = [[UIView alloc] init]; - line.backgroundColor = UIColorFromRGB(0x8B54E8); // 默认非选中颜色 - [self insertSubview:line atIndex:0]; - [_connectionLines addObject:line]; - - // 连接线位置:从上一个圆点到当前圆点 - [line mas_makeConstraints:^(MASConstraintMaker *make) { - make.height.mas_equalTo(1); // 线的高度 - make.centerY.mas_equalTo(levelItem.dotView); - make.leading.mas_equalTo(_levelItems[i-2].dotView.mas_centerX); - make.trailing.mas_equalTo(levelItem.dotView.mas_centerX); - }]; - } - } - - // 默认选中LV1 - [self setSelectedLevel:1 animated:NO]; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - - if (_maxLevel > 0 && _levelItems.count > 0) { - // 重新计算布局,确保居中显示 - CGFloat itemWidth = 25.0; - CGFloat spacing = 15.0; - CGFloat totalWidth = itemWidth * _maxLevel + spacing * (_maxLevel - 1); - CGFloat startX = (self.bounds.size.width - totalWidth) / 2; - - for (NSInteger i = 0; i < _levelItems.count; i++) { - LevelItemView *item = _levelItems[i]; - CGFloat x = startX + i * (itemWidth + spacing); - [item mas_updateConstraints:^(MASConstraintMaker *make) { - make.leading.mas_equalTo(self).offset(x); - }]; - } - } -} - -- (void)setSelectedLevel:(NSInteger)level animated:(BOOL)animated { - if (level < 1 || level > _maxLevel) { - return; - } - - _selectedLevel = level; - - // 更新等级指示器状态 - for (NSInteger i = 0; i < _levelItems.count; i++) { - LevelItemView *item = _levelItems[i]; - [item setSelected:(i + 1) <= level animated:animated]; - } - - // 更新连接线状态 - for (NSInteger i = 0; i < _connectionLines.count; i++) { - UIView *line = _connectionLines[i]; - // 连接线的状态取决于它连接的两个等级指示器是否都被选中 - // 如果连接线两端的等级指示器都被选中,则连接线也被选中 - BOOL isSelected = (i + 2) <= level; - - void (^updateBlock)(void) = ^{ - line.backgroundColor = isSelected ? [UIColor whiteColor] : UIColorFromRGB(0x8B54E8); - }; - - if (animated) { - [UIView animateWithDuration:0.2 animations:updateBlock]; - } else { - updateBlock(); - } - } -} - -- (void)handleTap:(UITapGestureRecognizer *)gesture { - CGPoint location = [gesture locationInView:self]; - - for (NSInteger i = 0; i < _levelItems.count; i++) { - LevelItemView *item = _levelItems[i]; - CGPoint itemLocation = [self convertPoint:location toView:item]; - - if (CGRectContainsPoint(item.bounds, itemLocation)) { - NSInteger level = i + 1; - [self setSelectedLevel:level animated:YES]; - - if (_levelSelectedBlock) { - _levelSelectedBlock(level); - } - break; - } - } -} - -@end @interface MedalsCollectionViewCell () @@ -255,10 +22,12 @@ @property(nonatomic, strong) XPRoomGiftAnimationParser *mp4Parser; @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UILabel *subLabel; -@property (nonatomic, strong) LevelIndicatorView *levelIndicatorView; +@property (nonatomic, strong) MedalsLevelIndicatorView *levelIndicatorView; @property (nonatomic, strong) MedalVos *displayModel; @property (nonatomic, strong) MedalSeriesItemVo *currentItemVo; +@property (nonatomic, assign) BOOL isVisible; // 跟踪 cell 是否可见 + @end @implementation MedalsCollectionViewCell @@ -323,12 +92,12 @@ }]; // 添加等级指示器 - self.levelIndicatorView = [[LevelIndicatorView alloc] init]; + 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.left.right.mas_greaterThanOrEqualTo(self.contentView).inset(8); + make.leading.trailing.mas_greaterThanOrEqualTo(self.contentView).inset(8); make.height.mas_equalTo(26); // 增加高度以适应圆点和文本 }]; @@ -345,10 +114,52 @@ } } }; + + // 设置通知监听 + [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]; + + // 隐藏 mp4 视图 + self.mp4View.hidden = YES; + self.imageView.hidden = NO; + + // 重置状态 + self.mp4Path = nil; + self.imagePath = nil; + self.isVisible = NO; + + // 清空文本 + self.titleLabel.text = @""; + self.subLabel.text = @""; +} + - (void)updateCell:(MedalSeriesVo *)model { MedalSeriesItemVo *itemVos = [model.medalSeries xpSafeObjectAtIndex:0]; self.currentItemVo = itemVos; @@ -356,7 +167,6 @@ // 配置等级指示器 [self.levelIndicatorView setupWithMaxLevel:itemVos.medalLevel]; - [self.levelIndicatorView setSelectedLevel:1 animated:NO]; [self updateDisplayWithCurrentModel]; @@ -371,9 +181,6 @@ } self.titleLabel.text = self.displayModel.name; -#if DEBUG - self.displayModel.expireSeconds = 1750233926; -#endif if (self.displayModel.expireSeconds == 0) { self.subLabel.text = YMLocalizedString(@"20.20.61_text_9"); } else { @@ -385,6 +192,9 @@ } - (void)setImagePath:(NSString *)imagePath { + // 停止之前的 mp4 播放 + [self stopMP4Playback]; + _imagePath = imagePath; self.mp4View.hidden = YES; self.imageView.hidden = NO; @@ -392,24 +202,117 @@ } - (void)setMp4Path:(NSString *)mp4Path { + // 如果是相同的 mp4 路径,不需要重新加载 + if ([_mp4Path isEqualToString:mp4Path]) { + 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 playHWDMP4:videoUrl repeatCount:-1 delegate:nil]; + // 只有当 cell 可见时才播放 + if (self.isVisible) { + [self.mp4View playHWDMP4:videoUrl repeatCount:-1 delegate:nil]; + } else { + // 存储 URL,但不立即播放 + self.mp4View.tag = 1; // 标记已准备好播放 + } } } failureBlock:^(NSError * _Nullable error) { - + NSLog(@"Failed to parse mp4: %@", error); }]; } +#pragma mark - MP4 播放控制 + +- (void)stopMP4Playback { + if (self.mp4View) { + [self.mp4View stopHWDMP4]; + self.mp4View.tag = 0; // 重置播放状态标记 + } +} + +- (void)pauseMP4Playback { + if (self.mp4View && !self.mp4View.hidden) { + [self.mp4View pauseHWDMP4]; + } +} + +- (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.mp4View playHWDMP4:videoUrl repeatCount:-1 delegate:nil]; + } + } failureBlock:nil]; + } else { + [self.mp4View resumeHWDMP4]; + } + } +} + +#pragma mark - 可见性管理 + +- (void)willDisplay { + self.isVisible = YES; + [self resumeMP4Playback]; +} + +- (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]; diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsDetailView.h b/YuMi/Modules/YMMine/View/Medals/MedalsDetailView.h new file mode 100644 index 00000000..1af3326f --- /dev/null +++ b/YuMi/Modules/YMMine/View/Medals/MedalsDetailView.h @@ -0,0 +1,18 @@ +// +// MedalsDetailView.h +// YuMi +// +// Created by P on 2025/6/18. +// + +#import +@class MedalSeriesVo; +NS_ASSUME_NONNULL_BEGIN + +@interface MedalsDetailView : UIView + +@property (nonatomic, strong) MedalSeriesVo *detailItemVo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsDetailView.m b/YuMi/Modules/YMMine/View/Medals/MedalsDetailView.m new file mode 100644 index 00000000..f7ed9792 --- /dev/null +++ b/YuMi/Modules/YMMine/View/Medals/MedalsDetailView.m @@ -0,0 +1,219 @@ +// +// MedalsDetailView.m +// YuMi +// +// Created by P on 2025/6/18. +// + +#import "MedalsDetailView.h" +#import "MedalsModel.h" +#import +#import "XPRoomGiftAnimationParser.h" +#import "MedalsLevelIndicatorView.h" + +@interface MedalsDetailView () + +@property (nonatomic, strong) UIImageView *bgLightImageView; +@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) MedalSeriesItemVo *currentSeriesItemVO; +@property (nonatomic, strong) MedalVos *displayModel; + +@end + +@implementation MedalsDetailView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + [self setupUI]; + } + return self; +} + +- (void)setupUI { + self.backgroundColor = [UIColor colorWithWhite:0 alpha:0.8]; + + [self addSubview:self.bgLightImageView]; + [self.bgLightImageView mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.leading.trailing.mas_equalTo(self); + make.height.mas_equalTo(self.bgLightImageView.mas_width); + }]; + + self.imageView = [[NetImageView alloc] init]; + self.imageView.contentMode = UIViewContentModeScaleAspectFill; + [self addSubview:self.imageView]; + [self.imageView mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.mas_equalTo(self); + make.top.mas_equalTo(kGetScaleWidth(135)); + make.size.mas_equalTo(CGSizeMake(228, 228)); + }]; + + [self addSubview:self.mp4View]; + [self.mp4View mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.mas_equalTo(self.imageView); + }]; + + self.titleLabel = [UILabel labelInitWithText:@"" font:kFontSemibold(20) textColor:[UIColor whiteColor]]; + self.subLabel = [UILabel labelInitWithText:@"" font:kFontRegular(12) textColor:[UIColor colorWithWhite:1 alpha:0.6]]; + self.titleLabel.textAlignment = NSTextAlignmentCenter; + self.subLabel.textAlignment = NSTextAlignmentCenter; + self.subLabel.numberOfLines = 2; + [self addSubview:self.titleLabel]; + [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.mas_equalTo(self); + make.top.mas_equalTo(self.imageView.mas_bottom).offset(8); + make.leading.trailing.mas_equalTo(self).inset(13); + }]; + [self addSubview:self.subLabel]; + [self.subLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.mas_equalTo(self); + make.top.mas_equalTo(self.titleLabel.mas_bottom).offset(12); + make.leading.trailing.mas_equalTo(self).inset(3); + }]; + + // 添加等级指示器 + self.levelIndicatorView = [[MedalsLevelIndicatorView alloc] init]; + [self addSubview:self.levelIndicatorView]; + [self.levelIndicatorView mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.mas_equalTo(self); + make.top.mas_equalTo(self.subLabel).offset(24); + make.leading.trailing.mas_greaterThanOrEqualTo(self).inset(8); + make.height.mas_equalTo(26); // 增加高度以适应圆点和文本 + }]; + + // 设置等级选择回调 + @kWeakify(self); + self.levelIndicatorView.levelSelectedBlock = ^(NSInteger level) { + @kStrongify(self); +// 处理等级选择事件 + if (self.currentSeriesItemVO && level <= self.currentSeriesItemVO.medalVos.count) { + MedalVos *selectedMedalVo = [self.currentSeriesItemVO.medalVos xpSafeObjectAtIndex:level - 1]; + if (selectedMedalVo) { + self.displayModel = selectedMedalVo; + [self updateDisplayWithCurrentModel]; + } + } + }; +} + +- (void)setDetailItemVo:(MedalSeriesVo *)detailItemVo { + _detailItemVo = detailItemVo; + self.currentSeriesItemVO = [detailItemVo.medalSeries xpSafeObjectAtIndex:0]; + self.displayModel = [self.currentSeriesItemVO.medalVos xpSafeObjectAtIndex:0]; + + [self.levelIndicatorView setupWithMaxLevel:self.currentSeriesItemVO.medalLevel]; + [self.levelIndicatorView setSelectedLevel:1 animated:NO]; + + [self updateDisplayWithCurrentModel]; +} + +- (void)updateDisplayWithCurrentModel { + if (self.displayModel) { + if ([self.displayModel.picUrl hasSuffix:@"mp4"]) { + [self setMp4Path:self.displayModel.picUrl]; + } else { + [self setImagePath:self.displayModel.picUrl]; + } + + self.titleLabel.text = self.displayModel.name; + self.subLabel.text = self.displayModel.medalDesc; + } +} + +- (void)setImagePath:(NSString *)imagePath { + // 停止之前的 mp4 播放 + [self stopMP4Playback]; + + _imagePath = imagePath; + self.mp4View.hidden = YES; + self.imageView.hidden = NO; + self.imageView.imageUrl = imagePath; +} + +- (void)setMp4Path:(NSString *)mp4Path { + // 如果是相同的 mp4 路径,不需要重新加载 + if ([_mp4Path isEqualToString:mp4Path]) { + 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 playHWDMP4:videoUrl repeatCount:-1 delegate:nil]; + } + } failureBlock:^(NSError * _Nullable error) { + NSLog(@"Failed to parse mp4: %@", error); + }]; +} + +#pragma mark - MP4 播放控制 + +- (void)stopMP4Playback { + if (self.mp4View) { + [self.mp4View stopHWDMP4]; + self.mp4View.tag = 0; // 重置播放状态标记 + } +} + +- (void)pauseMP4Playback { + if (self.mp4View && !self.mp4View.hidden) { + [self.mp4View pauseHWDMP4]; + } +} + +- (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.mp4View playHWDMP4:videoUrl repeatCount:-1 delegate:nil]; + } + } failureBlock:nil]; + } else { + [self.mp4View resumeHWDMP4]; + } + } +} + +#pragma mark - +- (UIImageView *)bgLightImageView { + if (!_bgLightImageView) { + _bgLightImageView = [[UIImageView alloc] initWithImage:kImage(@"medals_lights")]; + } + return _bgLightImageView; +} + +- (VAPView *)mp4View { + if (!_mp4View) { + _mp4View = [[VAPView alloc] init]; + _mp4View.contentMode = UIViewContentModeScaleAspectFit; + } + return _mp4View; +} + +@end diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.h b/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.h new file mode 100644 index 00000000..5a05d9e8 --- /dev/null +++ b/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.h @@ -0,0 +1,21 @@ +// +// MedalsLevelIndicatorView.h +// YuMi +// +// Created by P on 2025/6/18. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MedalsLevelIndicatorView : UIView + +@property (nonatomic, copy) void (^levelSelectedBlock)(NSInteger level); + +- (void)setupWithMaxLevel:(NSInteger)maxLevel; +- (void)setSelectedLevel:(NSInteger)level animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.m b/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.m new file mode 100644 index 00000000..123028a5 --- /dev/null +++ b/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.m @@ -0,0 +1,242 @@ +// +// MedalsLevelIndicatorView.m +// YuMi +// +// Created by P on 2025/6/18. +// + +#import "MedalsLevelIndicatorView.h" + +// 等级指示器视图 +@interface LevelItemView : UIView + +@property (nonatomic, strong) UILabel *levelLabel; +@property (nonatomic, strong) UIView *dotView; // 圆点视图 +@property (nonatomic, assign) BOOL isSelected; +@property (nonatomic, assign) NSInteger level; + +- (instancetype)initWithLevel:(NSInteger)level; +- (void)setSelected:(BOOL)selected animated:(BOOL)animated; + +@end + +@implementation LevelItemView + +- (instancetype)initWithLevel:(NSInteger)level { + self = [super init]; + if (self) { + _level = level; + _isSelected = NO; + + // 创建圆点视图 + _dotView = [[UIView alloc] init]; + _dotView.backgroundColor = UIColorFromRGB(0x8B54E8); + _dotView.layer.cornerRadius = 3; // 圆点半径 + _dotView.clipsToBounds = YES; + [self addSubview:_dotView]; + + // 创建等级标签 + self.levelLabel = [UILabel labelInitWithText:[NSString stringWithFormat:@"LV%ld", (long)level] + font:kFontMedium(10) + textColor:UIColorFromRGB(0x8B54E8)]; + self.levelLabel.textAlignment = NSTextAlignmentCenter; + [self addSubview:self.levelLabel]; + + // 布局 + [_dotView mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.mas_equalTo(self); + make.top.mas_equalTo(self); + make.width.height.mas_equalTo(6); // 圆点大小 + }]; + + [self.levelLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.mas_equalTo(self); + make.top.mas_equalTo(_dotView.mas_bottom).offset(4); // 圆点和文字的间距 + make.leading.trailing.bottom.mas_equalTo(self); + }]; + } + return self; +} + +- (void)setSelected:(BOOL)selected animated:(BOOL)animated { + if (_isSelected == selected) { + return; + } + + _isSelected = selected; + + void (^updateBlock)(void) = ^{ + UIColor *color = selected ? [UIColor whiteColor] : UIColorFromRGB(0x8B54E8); + self.levelLabel.textColor = color; + self.dotView.backgroundColor = color; + }; + + if (animated) { + [UIView animateWithDuration:0.2 animations:updateBlock]; + } else { + updateBlock(); + } +} + +@end + +// 等级指示器容器视图 +@interface MedalsLevelIndicatorView() + +@property (nonatomic, strong) NSMutableArray *levelItems; +@property (nonatomic, strong) NSMutableArray *connectionLines; // 连接线数组 +@property (nonatomic, assign) NSInteger maxLevel; +@property (nonatomic, assign) NSInteger selectedLevel; + +@end + +@implementation MedalsLevelIndicatorView + +- (instancetype)init { + self = [super init]; + if (self) { + _levelItems = [NSMutableArray array]; + _connectionLines = [NSMutableArray array]; + _selectedLevel = 1; + + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; + [self addGestureRecognizer:tapGesture]; + } + return self; +} + +- (void)setupWithMaxLevel:(NSInteger)maxLevel { + if (maxLevel < 0) { + maxLevel = 1; + } + if (_maxLevel == maxLevel && _levelItems.count == maxLevel) { + return; + } + + _maxLevel = maxLevel; + + // 清除现有的等级指示器和连接线 + for (UIView *view in _levelItems) { + [view removeFromSuperview]; + } + [_levelItems removeAllObjects]; + + for (UIView *line in _connectionLines) { + [line removeFromSuperview]; + } + [_connectionLines removeAllObjects]; + + // 创建新的等级指示器 + CGFloat itemWidth = 25.0; // 每个等级指示器的宽度 + CGFloat spacing = 15.0; // 等级指示器之间的间距 + CGFloat totalWidth = itemWidth * maxLevel + spacing * (maxLevel - 1); + CGFloat startX = (self.bounds.size.width - totalWidth) / 2; + + for (NSInteger i = 1; i <= maxLevel; i++) { + // 创建等级指示器 + LevelItemView *levelItem = [[LevelItemView alloc] initWithLevel:i]; + [self addSubview:levelItem]; + [_levelItems addObject:levelItem]; + + // 设置位置 + CGFloat x = startX + (i - 1) * (itemWidth + spacing); + [levelItem mas_makeConstraints:^(MASConstraintMaker *make) { + make.width.mas_equalTo(itemWidth); + make.height.mas_equalTo(self); + make.centerY.mas_equalTo(self); + make.leading.mas_equalTo(self).offset(x); + }]; + + // 如果不是第一个,添加连接线 + if (i > 1) { + UIView *line = [[UIView alloc] init]; + line.backgroundColor = UIColorFromRGB(0x8B54E8); // 默认非选中颜色 + [self insertSubview:line atIndex:0]; + [_connectionLines addObject:line]; + + // 连接线位置:从上一个圆点到当前圆点 + [line mas_makeConstraints:^(MASConstraintMaker *make) { + make.height.mas_equalTo(1); // 线的高度 + make.centerY.mas_equalTo(levelItem.dotView); + make.leading.mas_equalTo(_levelItems[i-2].dotView.mas_centerX); + make.trailing.mas_equalTo(levelItem.dotView.mas_centerX); + }]; + } + } + + // 默认选中LV1 + [self setSelectedLevel:1 animated:NO]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + if (_maxLevel > 0 && _levelItems.count > 0) { + // 重新计算布局,确保居中显示 + CGFloat itemWidth = 25.0; + CGFloat spacing = 15.0; + CGFloat totalWidth = itemWidth * _maxLevel + spacing * (_maxLevel - 1); + CGFloat startX = (self.bounds.size.width - totalWidth) / 2; + + for (NSInteger i = 0; i < _levelItems.count; i++) { + LevelItemView *item = _levelItems[i]; + CGFloat x = startX + i * (itemWidth + spacing); + [item mas_updateConstraints:^(MASConstraintMaker *make) { + make.leading.mas_equalTo(self).offset(x); + }]; + } + } +} + +- (void)setSelectedLevel:(NSInteger)level animated:(BOOL)animated { + if (level < 1 || level > _maxLevel) { + return; + } + + _selectedLevel = level; + + // 更新等级指示器状态 + for (NSInteger i = 0; i < _levelItems.count; i++) { + LevelItemView *item = _levelItems[i]; + [item setSelected:(i + 1) <= level animated:animated]; + } + + // 更新连接线状态 + for (NSInteger i = 0; i < _connectionLines.count; i++) { + UIView *line = _connectionLines[i]; + // 连接线的状态取决于它连接的两个等级指示器是否都被选中 + // 如果连接线两端的等级指示器都被选中,则连接线也被选中 + BOOL isSelected = (i + 2) <= level; + + void (^updateBlock)(void) = ^{ + line.backgroundColor = isSelected ? [UIColor whiteColor] : UIColorFromRGB(0x8B54E8); + }; + + if (animated) { + [UIView animateWithDuration:0.2 animations:updateBlock]; + } else { + updateBlock(); + } + } +} + +- (void)handleTap:(UITapGestureRecognizer *)gesture { + CGPoint location = [gesture locationInView:self]; + + for (NSInteger i = 0; i < _levelItems.count; i++) { + LevelItemView *item = _levelItems[i]; + CGPoint itemLocation = [self convertPoint:location toView:item]; + + if (CGRectContainsPoint(item.bounds, itemLocation)) { + NSInteger level = i + 1; + [self setSelectedLevel:level animated:YES]; + + if (_levelSelectedBlock) { + _levelSelectedBlock(level); + } + break; + } + } +} + +@end diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsViewController.m b/YuMi/Modules/YMMine/View/Medals/MedalsViewController.m index 4e668ee4..cfc04101 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsViewController.m +++ b/YuMi/Modules/YMMine/View/Medals/MedalsViewController.m @@ -10,8 +10,9 @@ #import "UserInfoModel.h" #import "TYCyclePagerView.h" #import "MedalsCollectionViewCell.h" +#import "MedalsDetailView.h" -typedef enum : NSUInteger { +typedef enum : NSInteger { MedalsCenterTab_TaskMedals = 1, MedalsCenterTab_ActivityMedals = 2, MedalsCenterTab_GloryMedals = 3, @@ -263,7 +264,7 @@ typedef enum : NSUInteger { if (self.isForMyMedals) { [self.presenter userMedals:self.userInfo.uid page:tabPage - type:tabPage]; + type:tabType]; } else { [self.presenter squareMedals:tabPage type:tabType]; @@ -354,27 +355,8 @@ typedef enum : NSUInteger { b.selected = NO; } sender.selected = YES; - - switch (sender.tag) { - case MedalsCenterTab_TaskMedals: - [self.presenter userMedals:self.userInfo.uid - page:1 - type:1]; - break; - case MedalsCenterTab_ActivityMedals: - [self.presenter userMedals:self.userInfo.uid - page:1 - type:2]; - break; - case MedalsCenterTab_GloryMedals: - [self.presenter userMedals:self.userInfo.uid - page:1 - type:3]; - break; - - default: - break; - } + self.currentTabType = sender.tag; + [self loadMedalsList:self.currentTabType page:1]; } #pragma mark - UICollectionView DataSource & Delegate @@ -395,24 +377,46 @@ typedef enum : NSUInteger { } - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + MedalsCollectionViewCell *cell = [MedalsCollectionViewCell cellFor:collectionView atIndexPath:indexPath]; + [cell updateCell:[self loadModel:indexPath.item]]; + return cell; +} + +// 处理 cell 的可见性 +- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { + if ([cell isKindOfClass:[MedalsCollectionViewCell class]]) { + [(MedalsCollectionViewCell *)cell willDisplay]; + } +} + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { + if ([cell isKindOfClass:[MedalsCollectionViewCell class]]) { + [(MedalsCollectionViewCell *)cell didEndDisplaying]; + } +} + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { + MedalsDetailView *view = [[MedalsDetailView alloc] initWithFrame:self.view.bounds]; + view.detailItemVo = [self loadModel:indexPath.item]; + [self.view addSubview:view]; +} + +- (MedalSeriesVo *)loadModel:(NSInteger)row { MedalSeriesVo *model = nil; switch (self.currentTabType) { case MedalsCenterTab_TaskMedals: - model = [self.datasourceTaskMedals xpSafeObjectAtIndex:indexPath.item]; + model = [self.datasourceTaskMedals xpSafeObjectAtIndex:row]; break; case MedalsCenterTab_ActivityMedals: - model = [self.datasourceActivityMedals xpSafeObjectAtIndex:indexPath.item]; + model = [self.datasourceActivityMedals xpSafeObjectAtIndex:row]; break; case MedalsCenterTab_GloryMedals: - model = [self.datasourceGloryMedals xpSafeObjectAtIndex:indexPath.item]; + model = [self.datasourceGloryMedals xpSafeObjectAtIndex:row]; break; default: break; } - - MedalsCollectionViewCell *cell = [MedalsCollectionViewCell cellFor:collectionView atIndexPath:indexPath]; - [cell updateCell:model]; - return cell; + return model; } #pragma mark - Lazy load