新增 MedalsDetailView 和 MedalsLevelIndicatorView 组件,更新 MedalsCollectionViewCell 以支持 MP4 播放控制和可见性管理,优化相关逻辑以提升用户体验,保持代码结构一致性。

This commit is contained in:
edwinQQQ
2025-06-18 18:31:13 +08:00
parent 413b2c6944
commit 887bb19056
8 changed files with 704 additions and 273 deletions

View File

@@ -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 = "<group>"; };
4C0A5B822E02675300955219 /* MedalsCollectionViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MedalsCollectionViewCell.h; sourceTree = "<group>"; };
4C0A5B832E02675300955219 /* MedalsCollectionViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MedalsCollectionViewCell.m; sourceTree = "<group>"; };
4C0A5B852E02BB1100955219 /* MedalsLevelIndicatorView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MedalsLevelIndicatorView.h; sourceTree = "<group>"; };
4C0A5B862E02BB1100955219 /* MedalsLevelIndicatorView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MedalsLevelIndicatorView.m; sourceTree = "<group>"; };
4C0A5B882E02BC3900955219 /* MedalsDetailView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MedalsDetailView.h; sourceTree = "<group>"; };
4C0A5B892E02BC3900955219 /* MedalsDetailView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MedalsDetailView.m; sourceTree = "<group>"; };
4C1064862E0014CF007E1586 /* NSMutableArray+Safe.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMutableArray+Safe.h"; sourceTree = "<group>"; };
4C1064872E0014CF007E1586 /* NSMutableArray+Safe.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMutableArray+Safe.m"; sourceTree = "<group>"; };
4C1119702DD7218300C18416 /* MyEventsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MyEventsViewController.h; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -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 */,

View File

@@ -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

View File

@@ -9,241 +9,8 @@
#import "MedalsModel.h"
#import <QGVAPWrapView.h>
#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<LevelItemView *> *levelItems;
@property (nonatomic, strong) NSMutableArray<UIView *> *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];

View File

@@ -0,0 +1,18 @@
//
// MedalsDetailView.h
// YuMi
//
// Created by P on 2025/6/18.
//
#import <UIKit/UIKit.h>
@class MedalSeriesVo;
NS_ASSUME_NONNULL_BEGIN
@interface MedalsDetailView : UIView
@property (nonatomic, strong) MedalSeriesVo *detailItemVo;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,219 @@
//
// MedalsDetailView.m
// YuMi
//
// Created by P on 2025/6/18.
//
#import "MedalsDetailView.h"
#import "MedalsModel.h"
#import <QGVAPWrapView.h>
#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

View File

@@ -0,0 +1,21 @@
//
// MedalsLevelIndicatorView.h
// YuMi
//
// Created by P on 2025/6/18.
//
#import <UIKit/UIKit.h>
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

View File

@@ -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<LevelItemView *> *levelItems;
@property (nonatomic, strong) NSMutableArray<UIView *> *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

View File

@@ -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