iOS 模仿某宝商品规格选择弹框
前言:
代码生涯里总能碰到一些稍微复杂点的东西,先是花时间弄出来了,然后又发现跟不上需求了,调整了一下代码思路,然后是重构重写,最后应该是丢弃了这份代码。在这里做个笔记,下辈子应该也用不上。
1.我们这是一个卖电子产品的项目
2.我们有一个选择商品规格的页面(选颜色,内存,套餐,数量...)
3.我们打算抄袭某宝的弹框
4.这是目前我觉得最好的方案去实现这个页面
5.过了两个星期,需求又改了,有些商品规格文字超长,要做多行显示。(***)
效果图
商品选择.gif
其实我们的业务逻辑有点复杂,本次代码只是研究一下,这样的 UI 如何去写。大体上就跟 UITableView 一样。主要有两个类, 一个类 继承自 UIScrollView, 一个类继承自 UIView。
以下是代码的重点部分,次要的代码没有贴出来,完整的示例代码在 https://github.com/gityuency/ObjectiveCTools
上代码: ItemView.h
1.协议:ItemViewDelegate 是当你点击了这个小格子触发的代理方法
2.暴露 NSIndexPath 这个属性,这个方便索引当前view 位置(第几行的第几个)
3.暴露选中和未选中属性,这个可以用来设置选中和未选中的样式
4.暴露不可点击属性,用于设置不可点击样式
5.暴露方法用于设置文本,字体,宽度,这是影响这个 View 大小的因素,所以放到外面设置。
#import <UIKit/UIKit.h>
@class ItemView;
/// 视图的协议
@protocol ItemViewDelegate <NSObject>
@optional
-(void)didSelected:(ItemView *)itemView;
@end
/// 格子
@interface ItemView : UIView
/// 按钮事件代理
@property (nonatomic, weak) id<ItemViewDelegate> delegate;
/// 是否是多行
@property (nonatomic, assign) BOOL isMultiLines;
/// 位置
@property (nonatomic, strong) NSIndexPath *indexPath;
/// 设置选中状态
@property (nonatomic, assign) BOOL itemSelected;
/// 设置不可选
@property (nonatomic, assign) BOOL itemDisable;
/// 设置 文本框的文字 文字的最小宽度 最大宽度 文字的字体
- (void)setText:(NSString *)text minWith:(CGFloat)minWith maxWith:(CGFloat)maxWith font:(UIFont *)font;
@end
ItemView.m
1.添加一个点击手势,子视图的用户交互都关闭
2.在设置视图文字的时候,计算他们的宽度
3.重写 setter 方法,去改变样式
#import "ItemView.h"
@interface ItemView ()
/// 文本框
@property (nonatomic, strong) UILabel *label;
/// 装载内容
@property (nonatomic, strong) UIView *contentView;
/// 最小宽度
@property (nonatomic, assign) CGFloat *minWidth;
/// 选中的标题颜色 边框色
@property (nonatomic, strong) UIColor *selectedTitleColro;
/// 选中背景色
@property (nonatomic, strong) UIColor *selectedBgColor;
/// 未选中背景色
@property (nonatomic, strong) UIColor *unSelectedBgColor;
/// 未选中的标题颜色
@property (nonatomic, strong) UIColor *unSelectedTitleColor;
/// 不可点击的标题颜色
@property (nonatomic, strong) UIColor *disableTitleColor;
/// 点击事件(手势)
@property (nonatomic, strong) UITapGestureRecognizer *g;
@end
@implementation ItemView
- (instancetype)init {
self = [super init];
if (self) {
_disableTitleColor = [UIColor grayColor];
_selectedBgColor = [UIColor colorWithRed:1.00 green:0.91 blue:0.91 alpha:1.00];
_unSelectedBgColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.95 alpha:1.00];
_unSelectedTitleColor = [UIColor darkTextColor];
_selectedTitleColro = [UIColor redColor];
_minWidth = 0;
_contentView = [[UIView alloc] init];
_contentView.userInteractionEnabled = NO;
_contentView.layer.cornerRadius = 5;
[self addSubview:_contentView];
_label = [[UILabel alloc] init];
_label.textAlignment = NSTextAlignmentCenter;
_label.userInteractionEnabled = NO;
_label.numberOfLines = 0;
[self addSubview:_label];
_isMultiLines = NO;
_g = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(click)];
_g.numberOfTapsRequired = 1;
[self addGestureRecognizer:_g];
}
return self;
}
- (void)click{
if (self.delegate && [self.delegate respondsToSelector:@selector(didSelected:)]) {
[self.delegate didSelected:self];
}
}
/// 设置 文本框的文字 文字的最小宽度 文字的字体
- (void)setText:(NSString *)text minWith:(CGFloat)minWith maxWith:(CGFloat)maxWith font:(UIFont *)font {
CGFloat offset = 10;
NSDictionary *dic = @{NSFontAttributeName: font};
CGFloat maxW = [UIScreen mainScreen].bounds.size.width - offset * 2;
CGSize stringSize = [text boundingRectWithSize:CGSizeMake(maxW, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:dic context:nil].size;
CGFloat singleLineWidth = [text sizeWithAttributes:dic].width; //计算单行字符串长度
if (singleLineWidth > maxW) { //判断是否多行
stringSize.width = maxW;
self.isMultiLines = YES;
} else if (stringSize.width < minWith) { //判断是否小于最小宽度
stringSize.width = minWith;
}
self.label.frame = CGRectMake(offset, offset, stringSize.width, stringSize.height);
self.contentView.frame = CGRectMake(offset * 0.5, offset * 0.5, stringSize.width + offset, stringSize.height + offset);
self.frame = CGRectMake(0, 0, self.label.frame.size.width + offset * 2, self.label.frame.size.height + offset * 2);
self.label.text = text;
self.label.font = font;
}
- (void)setItemSelected:(BOOL)itemSelected {
_itemSelected = itemSelected;
if (_itemSelected) {
[self.g setEnabled:YES];
self.contentView.layer.borderWidth = 1;
self.contentView.layer.borderColor = self.selectedTitleColro.CGColor;
self.contentView.backgroundColor = self.selectedBgColor;
self.label.textColor = self.selectedTitleColro;
} else {
[self.g setEnabled:YES];
self.contentView.layer.borderWidth = 0;
self.contentView.backgroundColor = self.unSelectedBgColor;
self.label.textColor = self.unSelectedTitleColor;
}
}
- (void)setItemDisable:(BOOL)itemDisable {
_itemDisable = itemDisable;
if (_itemDisable) {
[self.g setEnabled:NO];
self.contentView.layer.borderWidth = 0;
self.contentView.backgroundColor = self.unSelectedBgColor;
self.label.textColor = self.disableTitleColor;
}
}
@end
ItemCollectionView.h
1.仿照 UITableView 写一下代理,必选方法得到 个数,格子是什么, 可选方法得到 头视图,点击事件,行数。
2.暴露一个方法用于创建这个视图。
#import <UIKit/UIKit.h>
#import "ItemView.h"
@class ItemCollectionView;
/// 视图的协议
@protocol ItemViewDataSource <NSObject>
@required
/// 每一个格子是什么
- (ItemView *)itemCollectionView:(ItemCollectionView *)itemCollectionView cellForRowAtIndexPath:(NSIndexPath *)indexpath;
/// 每一行有多少个格子
- (NSInteger)itemCollectionView:(ItemCollectionView *)itemCollectionView numberOfRowsInSection:(NSInteger)section;
@optional
/// 一共有多少行
- (NSInteger)numberOfSectionsAt:(ItemCollectionView *)itemCollectionView;
/// 设置每一行的头视图
- (UIView *)itemCollectionView:(ItemCollectionView *)itemCollectionView headerInSection:(NSInteger)section;
/// 点击事件
- (void)itemCollectionView:(ItemCollectionView *)itemCollectionView didSelectedIndexPath:(NSIndexPath *)indexpath;
@end
/// 装载所有的小格子
@interface ItemCollectionView : UIScrollView
/// 数据代理
@property (nonatomic, weak) id<ItemViewDataSource> dataSource;
/// 重新创作视图
- (void)createCollectionView;
@end
ItemCollectionView.m
- 考虑到初始化可能使用代码或者 XIB,所以两个初始化经过的函数都调用了一个公共的初始化属性方法 setUp
2.在方法 createCollectionView 里面先强制进行布局,来得到实际的尺寸,因为从 XIB 加载会有情况还没拿到尺寸就开始布局了
3.在initView方法里面创建这些小格子。考虑到转屏等一些影响布局的因素, 每次进这个函数的时候,就把原来已经创建的格子全部扔掉,重新再来。这当然是一个不好的设计,但是没有去优化了。如果因为数据的改变(比如文字长度变了)那还是得重新算,干脆都重新算,少写点代码。
4.两层For循环,然后从代理函数里面去取得这个格子,就可以拿到这个格子的尺寸。当然个数也都能拿到。把这些小格子都放到数组里面。
5.当有格子被点击了,点击的格子设置为选中状态,其余的格子设置为未选中状态。在小格子的代理方法里面, 直接取得小格子属性 indexPath.section, 然后在小格子数字里找到对应的行,循环遍历,先全都设置为未选中(不可选的格子跳过),再把点击的格子设置为选中。
#import "ItemCollectionView.h"
@interface ItemCollectionView () <ItemViewDelegate>
/// 内容视图
@property (nonatomic, strong) UIView *contentView;
/// 每一行有多少个 默认 0 个
@property (nonatomic, assign) CGFloat rowCountOfSection;
/// 一共有多少行 默认1行
@property (nonatomic, assign) CGFloat sectionCount;
/// 临时数组
@property (nonatomic, strong) NSMutableArray *viewsArray;
/// 内部控件集合
@property (nonatomic, strong) NSArray *itemViewArray;
@end
@implementation ItemCollectionView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setUp];
}
return self;
}
- (void)awakeFromNib {
[super awakeFromNib];
[self setUp];
}
- (void)setUp {
_rowCountOfSection = 0;
_sectionCount = 1;
_viewsArray = [NSMutableArray array];
}
- (void)initView {
if (self.contentView != nil) {
[self.contentView removeFromSuperview];
}
self.contentView = [[UIView alloc] init];
[self addSubview:self.contentView];
/// 有多少行, 可选代理
if (self.dataSource && [self.dataSource respondsToSelector:@selector(numberOfSectionsAt:)]) {
self.sectionCount = [self.dataSource numberOfSectionsAt:self];
}
/// 每一行有多少个格子,必选
if (self.dataSource) {
BOOL a = [self.dataSource respondsToSelector:@selector(itemCollectionView:numberOfRowsInSection:)];
BOOL b = [self.dataSource respondsToSelector:@selector(itemCollectionView:cellForRowAtIndexPath:)];
NSAssert((a && b), @"代理对象没有遵守协议!");
} else {
NSAssert(NO, @"代理对象为空!");
}
CGFloat allSectionHeight = 0;
for (NSInteger i = 0; i < self.sectionCount; i++) {
UIView *sectionView = [[UIView alloc] init];
UIView *headView = [self.dataSource itemCollectionView:self headerInSection:i];
headView.frame = CGRectMake(headView.frame.origin.x, 0, self.bounds.size.width, headView.frame.size.height);
[sectionView addSubview:headView];
CGFloat offsetY = headView.frame.size.height;
UIView *lastrowCell;
self.rowCountOfSection = [self.dataSource itemCollectionView:self numberOfRowsInSection:i];
NSMutableArray *array = [NSMutableArray array];
ItemView *lastCell = nil; //最后一个单元
for (NSInteger k = 0; k < self.rowCountOfSection; k++) {
NSIndexPath *index = [NSIndexPath indexPathForRow:k inSection:i];
ItemView *rowCell = [self.dataSource itemCollectionView:self cellForRowAtIndexPath: index];
rowCell.delegate = self;
rowCell.indexPath = index;
if (lastCell) { //最后一个单元存在
if (CGRectGetMaxX(lastCell.frame) + rowCell.frame.size.width > self.bounds.size.width) { //新来的这个在后面放不下
offsetY += lastCell.frame.size.height; //放不下就放到下一行
rowCell.frame = CGRectMake(0, offsetY, rowCell.frame.size.width, rowCell.frame.size.height);
} else {
if (lastCell.isMultiLines) { //上一个是多行, 当前就另起一行
offsetY += lastCell.frame.size.height;
}
rowCell.frame = CGRectMake(CGRectGetMaxX(lastCell.frame), offsetY, rowCell.frame.size.width, rowCell.frame.size.height);
}
} else { //最后一个单元不存在
rowCell.frame = CGRectMake(0, offsetY, rowCell.frame.size.width, rowCell.frame.size.height);
}
lastCell = rowCell;
[sectionView addSubview:rowCell];
lastrowCell = rowCell;
[array addObject:rowCell];
}
CGFloat sectionHeight = CGRectGetMaxY(lastrowCell.frame) + 5; // +5是为了底部多留点空白
sectionView.frame = CGRectMake(0, allSectionHeight, self.bounds.size.width, sectionHeight);
[self.contentView addSubview:sectionView];
allSectionHeight += sectionHeight;
if (i != self.sectionCount - 1) { //最后一行不要分割线
UIView *line = [[UIView alloc] initWithFrame:CGRectMake(0, sectionView.frame.size.height - 1, sectionView.frame.size.width, 1)];
line.backgroundColor = [UIColor darkTextColor];
[sectionView addSubview:line];
}
[self.viewsArray addObject:array];
}
self.contentView.frame = CGRectMake(0, 0, self.bounds.size.width, allSectionHeight);
self.contentSize = self.contentView.frame.size;
self.itemViewArray = self.viewsArray;
}
#pragma mark - ItemViewDelegate
- (void)didSelected:(ItemView *)itemView {
for (ItemView *v in self.itemViewArray[itemView.indexPath.section]) {
if (v.itemDisable) { continue; }
v.itemSelected = NO;
}
itemView.itemSelected = YES;
if (self.dataSource && [self.dataSource respondsToSelector:@selector(itemCollectionView:didSelectedIndexPath:)]) {
[self.dataSource itemCollectionView:self didSelectedIndexPath:itemView.indexPath];
}
}
- (void)createCollectionView {
[self setNeedsLayout];
[self layoutIfNeeded];
[self initView];
}
@end