431 lines
14 KiB
Objective-C
431 lines
14 KiB
Objective-C
//
|
||
// MedalsLevelIndicatorView.m
|
||
// YuMi
|
||
//
|
||
// Created by P on 2025/6/18.
|
||
//
|
||
|
||
#import "MedalsLevelIndicatorView.h"
|
||
#import "UIImage+Utils.h"
|
||
|
||
// 等级指示器视图
|
||
@interface LevelItemView : UIView
|
||
|
||
@property (nonatomic, strong) UILabel *levelLabel;
|
||
@property (nonatomic, strong) UIView *dotView; // 圆点视图
|
||
@property (nonatomic, strong) NetImageView *imageView; // 图片视图
|
||
@property (nonatomic, assign) BOOL isSelected;
|
||
@property (nonatomic, assign) NSInteger level;
|
||
@property (nonatomic, assign) BOOL hasImage;
|
||
@property (nonatomic, strong) UIImage *originalImage; // 保存原始彩色图片
|
||
@property (nonatomic, strong) UIImage *grayImage; // 保存灰度图片
|
||
@property (nonatomic, copy) NSString *cachedImageUrl; // 缓存图片URL,用于延迟加载
|
||
|
||
- (instancetype)initWithLevel:(NSInteger)level;
|
||
- (void)setSelected:(BOOL)selected animated:(BOOL)animated;
|
||
- (void)setImageUrl:(NSString *)imageUrl;
|
||
- (void)createImageViewIfNeeded;
|
||
|
||
@end
|
||
|
||
@implementation LevelItemView
|
||
|
||
- (instancetype)initWithLevel:(NSInteger)level {
|
||
self = [super init];
|
||
if (self) {
|
||
_level = level;
|
||
_isSelected = NO;
|
||
_hasImage = 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)createImageViewIfNeeded {
|
||
if (_imageView) {
|
||
return;
|
||
}
|
||
|
||
// 创建图片视图
|
||
_imageView = [[NetImageView alloc] init];
|
||
_imageView.contentMode = UIViewContentModeScaleAspectFill;
|
||
_imageView.clipsToBounds = YES;
|
||
[self addSubview:_imageView];
|
||
|
||
[_imageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.centerX.mas_equalTo(self);
|
||
make.bottom.mas_equalTo(_dotView.mas_top).offset(-4);
|
||
make.width.height.mas_equalTo(40);
|
||
}];
|
||
|
||
// 调整圆点位置
|
||
[_dotView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||
make.centerX.mas_equalTo(self);
|
||
make.top.mas_equalTo(_imageView.mas_bottom).offset(4);
|
||
make.width.height.mas_equalTo(6);
|
||
}];
|
||
|
||
// 如果有缓存的图片URL,加载图片
|
||
if (_cachedImageUrl) {
|
||
[self loadImageWithUrl:_cachedImageUrl];
|
||
_cachedImageUrl = nil;
|
||
}
|
||
}
|
||
|
||
- (void)setImageUrl:(NSString *)imageUrl {
|
||
_hasImage = YES;
|
||
_cachedImageUrl = imageUrl;
|
||
|
||
// 如果已经创建了imageView,直接加载图片
|
||
if (_imageView) {
|
||
[self loadImageWithUrl:imageUrl];
|
||
}
|
||
}
|
||
|
||
- (void)loadImageWithUrl:(NSString *)imageUrl {
|
||
// 调试时使用测试URL
|
||
#ifdef DEBUG
|
||
NSString *testUrl = @"https://img.toto.im/mw600/66b3de17ly1i2jopju47bj20xc1e0dx4.jpg.webp";
|
||
imageUrl = testUrl;
|
||
NSLog(@"调试模式: 使用测试图片URL: %@", testUrl);
|
||
#endif
|
||
|
||
// 加载图片并处理灰度效果
|
||
@kWeakify(self);
|
||
[_imageView loadImageWithUrl:imageUrl completion:^(UIImage * _Nullable image, NSURL * _Nonnull url) {
|
||
@kStrongify(self);
|
||
if (image) {
|
||
UIImage *cutImage = [image cutImage:CGSizeMake(40, 40)];
|
||
self.originalImage = cutImage;
|
||
self.grayImage = [cutImage grayscaleImage];
|
||
[self updateImageEffect];
|
||
}
|
||
}];
|
||
}
|
||
|
||
// 更新图片效果
|
||
- (void)updateImageEffect {
|
||
if (!_hasImage || !_imageView) {
|
||
return;
|
||
}
|
||
|
||
if (_isSelected && _originalImage) {
|
||
// 选中状态:显示彩色图片
|
||
self.imageView.image = _originalImage;
|
||
} else if (!_isSelected && _grayImage) {
|
||
// 非选中状态:显示灰度图片
|
||
self.imageView.image = _grayImage;
|
||
}
|
||
}
|
||
|
||
- (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 (self.hasImage) {
|
||
[self updateImageEffect];
|
||
}
|
||
};
|
||
|
||
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;
|
||
_indicatorType = MedalsLevelIndicatorTypeNormal;
|
||
|
||
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; // 每个等级指示器的宽度
|
||
|
||
// 计算可用宽度(考虑左右各20点的边距)
|
||
CGFloat availableWidth = self.bounds.size.width;// - 40.0; // 左右各减去20点
|
||
|
||
// 计算间距
|
||
CGFloat spacing = 0;
|
||
if (maxLevel > 1) {
|
||
spacing = (availableWidth - itemWidth * maxLevel) / (maxLevel - 1);
|
||
}
|
||
|
||
// 如果间距太小,设置一个最小值
|
||
spacing = MAX(spacing, 10.0);
|
||
|
||
// 起始位置(左边距20点)
|
||
CGFloat startX = 20.0;
|
||
|
||
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];
|
||
|
||
// 根据类型设置图片可见性
|
||
[self updateImageVisibility];
|
||
}
|
||
|
||
- (void)layoutSubviews {
|
||
[super layoutSubviews];
|
||
|
||
if (_maxLevel > 0 && _levelItems.count > 0) {
|
||
// 计算可用宽度(考虑左右各20点的边距)
|
||
CGFloat itemWidth = 25.0;
|
||
CGFloat availableWidth = self.bounds.size.width;// - 40.0; // 左右各减去20点
|
||
|
||
// 计算间距
|
||
CGFloat spacing = 0;
|
||
if (_maxLevel > 1) {
|
||
spacing = (availableWidth - itemWidth * _maxLevel) / (_maxLevel - 1);
|
||
}
|
||
|
||
// 如果间距太小,设置一个最小值
|
||
spacing = MAX(spacing, 10.0);
|
||
|
||
// 起始位置(左边距20点)
|
||
CGFloat startX = 20.0;
|
||
|
||
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);
|
||
}];
|
||
}
|
||
|
||
// 更新连接线
|
||
for (NSInteger i = 0; i < _connectionLines.count && i + 1 < _levelItems.count; i++) {
|
||
UIView *line = _connectionLines[i];
|
||
[line mas_updateConstraints:^(MASConstraintMaker *make) {
|
||
make.leading.mas_equalTo(_levelItems[i].dotView.mas_centerX);
|
||
make.trailing.mas_equalTo(_levelItems[i+1].dotView.mas_centerX);
|
||
}];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (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)setIndicatorType:(MedalsLevelIndicatorType)indicatorType {
|
||
_indicatorType = indicatorType;
|
||
|
||
// 如果已经创建了视图,需要根据类型进行调整
|
||
if (_levelItems.count > 0) {
|
||
// 如果是带图片类型,创建 imageView
|
||
if (indicatorType == MedalsLevelIndicatorTypeWithImage) {
|
||
for (LevelItemView *item in _levelItems) {
|
||
[item createImageViewIfNeeded];
|
||
}
|
||
}
|
||
[self updateImageVisibility];
|
||
}
|
||
}
|
||
|
||
// 根据指示器类型更新图片可见性
|
||
- (void)updateImageVisibility {
|
||
for (LevelItemView *item in _levelItems) {
|
||
if (item.imageView) {
|
||
item.imageView.hidden = (_indicatorType == MedalsLevelIndicatorTypeNormal);
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)setImageUrl:(NSString *)imageUrl forLevel:(NSInteger)level {
|
||
if (level < 1 || level > _maxLevel || level > _levelItems.count) {
|
||
return;
|
||
}
|
||
|
||
LevelItemView *item = _levelItems[level - 1];
|
||
|
||
// 根据指示器类型决定是否创建和加载图片
|
||
if (_indicatorType == MedalsLevelIndicatorTypeWithImage) {
|
||
[item createImageViewIfNeeded]; // 确保创建了 imageView
|
||
[item setImageUrl:imageUrl];
|
||
} else {
|
||
// 在普通模式下,只保存URL,不创建 imageView
|
||
[item setImageUrl:imageUrl];
|
||
}
|
||
}
|
||
|
||
- (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];
|
||
|
||
// 检查点击是否在 item 的边界内
|
||
if (CGRectContainsPoint(item.bounds, itemLocation)) {
|
||
NSInteger level = i + 1;
|
||
[self setSelectedLevel:level animated:YES];
|
||
|
||
if (_levelSelectedBlock) {
|
||
_levelSelectedBlock(level);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 计算布局参数
|
||
- (void)calculateLayoutParams:(CGFloat *)itemWidth spacing:(CGFloat *)spacing startX:(CGFloat *)startX forMaxLevel:(NSInteger)maxLevel {
|
||
*itemWidth = 25.0; // 每个等级指示器的宽度
|
||
|
||
// 计算可用宽度
|
||
CGFloat availableWidth = self.bounds.size.width;
|
||
|
||
// 计算间距
|
||
*spacing = 0;
|
||
if (maxLevel > 1) {
|
||
*spacing = (availableWidth - (*itemWidth) * maxLevel) / (maxLevel - 1);
|
||
}
|
||
|
||
// 如果间距太小,设置一个最小值
|
||
*spacing = MAX(*spacing, 10.0);
|
||
|
||
// 起始位置(左边距20点)
|
||
*startX = 20.0;
|
||
}
|
||
|
||
@end
|