最近一直忙两个项目,有段时间没有学习新东西了,感觉已经out了.项目中用到一个行业选择的功能.(本文参照SKTagView编写,不知道出自哪位大神)。效果图:↓
(代码纯属copy,如有不适,同志,请坚持看完!)
我开始的思路是用collectionView去实现,不过总觉的不太好,我这里需求是单选,总觉得交互会很麻烦,所以就扒了点代码,看了大神们的实现思路模仿一下。
整体思路:首先是有三个组成部分分别是;
Model : 来存储所创建的每个标签的属性(后续TagModel就代表Model);
View : 盛放标签的View,类中计算标签的行数高度,初始化方法,便利构造器等一系列(后续TagView就代表这个View);
Button: 自定义Btn通过Model进行相应设置,在View中创建Button;
主题流程就是:
1.创建TagView
2.遍历数据源(需要展示的所有标签)创建TagModel(注意此时TagView只是创建视图并没有创建视图上的标签或者说Btn),当创建一个TagModel我们就去通知TagView去根据这个TagModel(包含btn属性)去创建Btn。下图解:↓
下面代码:
TagModel
#DzTag.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface DzTag : NSObject
@property (copy, nonatomic, nullable) NSString *text;
// 需要设置Btn的title属性时候用这个
@property (copy, nonatomic, nullable) NSAttributedString *attributedText;
// 字体颜色
@property (strong, nonatomic, nullable) UIColor *textColor;
// btn背景色
@property (strong, nonatomic, nullable) UIColor *bgColor;
// 高亮背景色
@property (strong, nonatomic, nullable) UIColor *highlightedBgColor;
// 背景图片
@property (strong, nonatomic, nullable) UIImage *bgImg;
@property (strong, nonatomic, nullable) UIImage *sebgColor;
// 圆角
@property (assign, nonatomic) CGFloat cornerRadius;
// 边框颜色
@property (strong, nonatomic, nullable) UIColor *borderColor;
// 边框宽度
@property (assign, nonatomic) CGFloat borderWidth;
// 内边距
@property (assign, nonatomic) UIEdgeInsets padding;
@property (strong, nonatomic, nullable) UIFont *font;
@property (assign, nonatomic) CGFloat fontSize;
//默认:YES
@property (assign, nonatomic) BOOL enable;
- (nonnull instancetype)initWithText: (nonnull NSString *)text;
+ (nonnull instancetype)tagWithText: (nonnull NSString *)text;
@end
#DzTag.m
#import "DzTag.h"
static const CGFloat kDefaultFontSize = 13.0;
@implementation DzTag
- (instancetype)init {
self = [super init];
if (self) {
_fontSize = kDefaultFontSize;
_textColor = [UIColor blackColor];
_bgColor = [UIColor whiteColor];
_enable = YES;
}
return self;
}
// 初始化方法
- (instancetype)initWithText: (NSString *)text {
self = [self init];
if (self) {
_text = text;
}
return self;
}
// 遍历构造器
+ (instancetype)tagWithText: (NSString *)text {
return [[self alloc] initWithText: text];
}
@end
自定义Button
#DzTagButton.h
#import <UIKit/UIKit.h>
@class DzTag;
@interface DzTagButton : UIButton
+ (nonnull instancetype)buttonWithTag: (nonnull DzTag *)tag;
@end
#DzTagButton.m
#import "DzTagButton.h"
#import "DzTag.h"
@implementation DzTagButton
// 创建Button,并且设置Button属性
+ (instancetype)buttonWithTag: (DzTag *)tag {
// 创建
DzTagButton *btn = [super buttonWithType:UIButtonTypeCustom];
// 是否使用attributedText
if (tag.attributedText) {
[btn setAttributedTitle: tag.attributedText forState: UIControlStateNormal];
} else {
[btn setTitle: tag.text forState:UIControlStateNormal];
[btn setTitleColor: tag.textColor forState: UIControlStateNormal];
btn.titleLabel.font = tag.font ?: [UIFont systemFontOfSize: tag.fontSize];
}
btn.backgroundColor = tag.bgColor;
btn.contentEdgeInsets = tag.padding;
btn.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
// 设置背景图
if (tag.bgImg) {
[btn setBackgroundImage: tag.bgImg forState: UIControlStateNormal];
}
// 设置颜色
if (tag.sebgColor) {
[btn setBackgroundImage:tag.sebgColor forState:(UIControlStateSelected)];
}
// 设置边框颜色
if (tag.borderColor) {
btn.layer.borderColor = tag.borderColor.CGColor;
}
// 设置变宽宽度
if (tag.borderWidth) {
btn.layer.borderWidth = tag.borderWidth;
}
// 是否启用
btn.userInteractionEnabled = tag.enable;
// 是否要高亮效果
if (tag.enable) {
UIColor *highlightedBgColor = tag.highlightedBgColor ?: [self darkerColor:btn.backgroundColor];
[btn setBackgroundImage:[self imageWithColor:highlightedBgColor] forState:UIControlStateHighlighted];
}
// Btn圆角
btn.layer.cornerRadius = tag.cornerRadius;
btn.layer.masksToBounds = YES;
return btn;
}
// 根据颜色生成图片
+ (UIImage *)imageWithColor:(UIColor *)color {
CGRect rect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
// 如果有颜色就返回,没有就不要
+ (UIColor *)darkerColor:(UIColor *)color {
CGFloat h, s, b, a;
if ([color getHue:&h saturation:&s brightness:&b alpha:&a])
return [UIColor colorWithHue:h
saturation:s
brightness:b * 0.85
alpha:a];
return color;
}
@end
TagView
#DzTagView.h
#import <UIKit/UIKit.h>
#import "DzTag.h"
@class DzTagButton;
@interface DzTagView : UIView
#pragma mark -- 属性
// 视图内边距
@property (assign, nonatomic) UIEdgeInsets padding;
// 行间距
@property (assign, nonatomic) CGFloat lineSpacing;
// 每个item间距
@property (assign, nonatomic) CGFloat interitemSpacing;
// 最大宽
@property (assign, nonatomic) CGFloat preferredMaxLayoutWidth;
//!< 固定宽度
@property (assign, nonatomic) CGFloat regularWidth;
//!< 固定高度
@property (nonatomic,assign ) CGFloat regularHeight;
// 单行模式
@property (assign, nonatomic) BOOL singleLine;
// block点击回调
@property (copy, nonatomic, nullable) void (^didTapTagAtIndex)(NSUInteger index,UIButton * _Nullable btn);
#pragma mark -- 方法
// 创建方法
- (void)addTag: (nonnull DzTag *)tag;
// 添加item(指定位置添加)
- (void)insertTag: (nonnull DzTag *)tag atIndex:(NSUInteger)index;
// 移除item
- (void)removeTag: (nonnull DzTag *)tag;
// 根据位置移除
- (void)removeTagAtIndex: (NSUInteger)index;
// 移除所有
- (void)removeAllTags;
@end
#DzTagView.m代码比较多我先总结下结构和声明周期.
.m一共分三大部分 :↓
①:生命周期。
②:getter,setter。
③:一些共有方法包括创建·添加·删除item等。
其他解释我想类中注释够用了,几乎每行都有了😆
#核心:重写 intrinsicContentSize 为内容返回恰当的大小,无论何时有任何会影响固有内容尺寸的改变发生时,调用 invalidateIntrinsicContentSize进行更新(如果想要在app运行时改变 intrinsicContentSize,可以调用invalidateIntrinsicContentSize()方法来更新)
#DzTagView.m
#import "DzTagView.h"
#import "DzTagButton.h"
@interface DzTagView ()
@property (strong, nonatomic, nullable) NSMutableArray *tags;
//用来表示是否需要重新加载一次,一般添加或者删除item时候改变此值状态,用来重新加载,节省内存使用
@property (assign, nonatomic) BOOL didSetup;
@property (nonatomic,assign) BOOL isIntrinsicWidth; //!<是否宽度固定
@property (nonatomic,assign) BOOL isIntrinsicHeight; //!<是否高度固定
@end
@implementation DzTagView
#pragma mark - public方法
// NSParameterAssert() ↓
// 断言评估一个条件,如果条件为 false
// 调用当前线程的断点句柄
// 每一个线程有它自已的断点句柄
// 它是一个 NSAsserttionHandler类的对象。
// 当被调用时,断言句柄打印一个错误信息,该条信息中包含了方法名、类名或函数名。
// 然后,它就抛出一个 NSInternalInconsistencyException 异常
// 创建item方法
- (void)addTag: (DzTag *)tag {
// 断言
NSParameterAssert(tag);
DzTagButton *btn = [DzTagButton buttonWithTag: tag];
// 添加事件
[btn addTarget: self action: @selector(onTag:) forControlEvents: UIControlEventTouchUpInside];
[self addSubview: btn];
[self.tags addObject: tag];
// 更新布局
self.didSetup = NO;
[self invalidateIntrinsicContentSize];
}
// 在某位置添加tag
- (void)insertTag: (DzTag *)tag atIndex: (NSUInteger)index {
// 断言
NSParameterAssert(tag);
// 如果这个位置是在最后位置直接添加
if (index + 1 > self.tags.count) {
[self addTag: tag];
} else {
// 创建BTN
DzTagButton *btn = [DzTagButton buttonWithTag: tag];
// 添加事件
[btn addTarget: self action: @selector(onTag:) forControlEvents: UIControlEventTouchUpInside];
// 相应位置添加子视图
[self insertSubview: btn atIndex: index];
// 数据源相应位置添加tag
[self.tags insertObject: tag atIndex: index];
// 更新布局
self.didSetup = NO;
[self invalidateIntrinsicContentSize];
}
}
// 根据tag删除item
- (void)removeTag: (DzTag *)tag {
// 断言
NSParameterAssert(tag);
// 根据tag获取相应index
NSUInteger index = [self.tags indexOfObject: tag];
//NSNotFound表示请求操作的某个内容或者item没有发现,或者不存在。
if (NSNotFound == index) {
return;
}
// 删除数据和UI
[self.tags removeObjectAtIndex: index];
if (self.subviews.count > index) {
[self.subviews[index] removeFromSuperview];
}
// 重新布局
self.didSetup = NO;
[self invalidateIntrinsicContentSize];
}
// 根据index删除某个item
- (void)removeTagAtIndex: (NSUInteger)index {
// 越界保护
if (index + 1 > self.tags.count) {
return;
}
// 删除相应item
[self.tags removeObjectAtIndex: index];
// 删除UI
if (self.subviews.count > index) {
[self.subviews[index] removeFromSuperview];
}
// 重新布局
self.didSetup = NO;
[self invalidateIntrinsicContentSize];
}
// 删除所有item
- (void)removeAllTags {
// 先删除数据源
[self.tags removeAllObjects];
// 删除所有UI子视图
for (UIView *view in self.subviews) {
[view removeFromSuperview];
}
// 重新布局
self.didSetup = NO;
[self invalidateIntrinsicContentSize];
}
#pragma mark - 点击item响应事件
- (void)onTag: (UIButton *)btn {
if (self.didTapTagAtIndex) {
self.didTapTagAtIndex([self.subviews indexOfObject: btn], btn);
}
}
#pragma mark -- 生命周期
//TODO: 生命周期开始 第①步
// UIView的属性intrinsicContentSize 返回一个CGSize(用来在外部计算高度)
-(CGSize)intrinsicContentSize {
// 如果没有数据源一个item都没有则返回长宽高位置都是0
if (!self.tags.count) {
return CGSizeZero;
}
// 计算大小需要的属性
NSArray *subviews = self.subviews;
// 前一视图
UIView *previousView = nil;
// 上边距
CGFloat topPadding = self.padding.top;
// 下边距
CGFloat bottomPadding = self.padding.bottom;
// 左边距
CGFloat leftPadding = self.padding.left;
// 右边距
CGFloat rightPadding = self.padding.right;
// item间距
CGFloat itemSpacing = self.interitemSpacing;
// 行间距
CGFloat lineSpacing = self.lineSpacing;
// 当前X
CGFloat currentX = leftPadding;
// 视图内在视图高度 / 最后返回
CGFloat intrinsicHeight = topPadding;
// 视图内在视图宽 / 最后返回
CGFloat intrinsicWidth = leftPadding;
// 如果非单行显示 并且最大宽度大于0
if (!self.singleLine && self.preferredMaxLayoutWidth > 0) {
// 行数
NSInteger lineCount = 0;
// 遍历subViews
for (UIView *view in subviews) {
// 获取子view的size
CGSize size = view.intrinsicContentSize;
// 宽度和高度通过参数的0或者非0来进行赋值,却别是否用固定宽度(三目)
CGFloat width = self.isIntrinsicWidth ? self.regularWidth : size.width;
CGFloat height = self.isIntrinsicHeight ? self.regularHeight: size.height;
// 如果已经有item存在
if (previousView) {
// 确定x
currentX += itemSpacing;
// 如果当前itemX + 新item宽度 + 右边距 < 视图最大宽度
if (currentX + width + rightPadding <= self.preferredMaxLayoutWidth) {
// 本行排列
currentX += width;
} else { // // 如果当前itemX + 新item宽度 + 右边距 < 视图最大宽度 则 换行添加
// 行数自加1
lineCount ++;
// 跟新x
currentX = leftPadding + width;
// 更新View高度
intrinsicHeight += height;
}
} else { // 添加第一个item会走
lineCount ++;
// 更新宽高
intrinsicHeight += height;
currentX += width;
}
// 赋值前一view
previousView = view;
// 更新宽度
intrinsicWidth = MAX(intrinsicWidth, currentX + rightPadding);
}
// 计算最终高度
intrinsicHeight += bottomPadding + lineSpacing * (lineCount - 1);
} else { // 单行显示时候计算size
for (UIView *view in subviews) {
CGSize size = view.intrinsicContentSize;
intrinsicWidth += self.isIntrinsicWidth ? self.regularWidth : size.width;
}
// 最终宽高
intrinsicWidth += itemSpacing * (subviews.count - 1) + rightPadding;
intrinsicHeight += ((UIView *)subviews.firstObject).intrinsicContentSize.height + bottomPadding;
}
// 返回一个CGSize
return CGSizeMake(intrinsicWidth, intrinsicHeight);
}
//TODO: 生命周期 第②步
- (void)layoutSubviews {
// 非单行显示
if (!self.singleLine) {
self.preferredMaxLayoutWidth = self.frame.size.width;
}
[super layoutSubviews];
[self layoutTags];
}
//TODO: 生命周期 第③步
// 内部布局(实现过程等同于intrinsicContentSize方法中的计算)
- (void)layoutTags {
if (self.didSetup || !self.tags.count) {
return;
}
NSArray *subviews = self.subviews;
UIView *previousView = nil;
CGFloat topPadding = self.padding.top;
CGFloat leftPadding = self.padding.left;
CGFloat rightPadding = self.padding.right;
CGFloat itemSpacing = self.interitemSpacing;
CGFloat lineSpacing = self.lineSpacing;
CGFloat currentX = leftPadding;
if (!self.singleLine && self.preferredMaxLayoutWidth > 0) {
for (UIView *view in subviews) {
CGSize size = view.intrinsicContentSize;
CGFloat width1 = self.isIntrinsicWidth?self.regularWidth:size.width;
CGFloat height1 = self.isIntrinsicHeight?self.regularHeight:size.height;
if (previousView) {
// CGFloat width = size.width;
currentX += itemSpacing;
if (currentX + width1 + rightPadding <= self.preferredMaxLayoutWidth) {
view.frame = CGRectMake(currentX, CGRectGetMinY(previousView.frame), width1, height1);
currentX += width1;
} else {
CGFloat width = MIN(width1, self.preferredMaxLayoutWidth - leftPadding - rightPadding);
view.frame = CGRectMake(leftPadding, CGRectGetMaxY(previousView.frame) + lineSpacing, width, height1);
currentX = leftPadding + width;
}
} else {
CGFloat width = MIN(width1, self.preferredMaxLayoutWidth - leftPadding - rightPadding);
view.frame = CGRectMake(leftPadding, topPadding, width, height1);
currentX += width;
}
previousView = view;
}
} else {
for (UIView *view in subviews) {
CGSize size = view.intrinsicContentSize;
view.frame = CGRectMake(currentX, topPadding, self.isIntrinsicWidth?self.regularWidth:size.width, self.isIntrinsicHeight?self.regularHeight:size.height);
currentX += self.isIntrinsicWidth?self.regularWidth:size.width;
previousView = view;
}
}
self.didSetup = YES;
}
#pragma mark - setting getter方法
// 数据源
- (NSMutableArray *)tags {
if(!_tags) {
_tags = [NSMutableArray array];
}
return _tags;
}
// 最大宽度setter方法
- (void)setPreferredMaxLayoutWidth: (CGFloat)preferredMaxLayoutWidth {
if (preferredMaxLayoutWidth != _preferredMaxLayoutWidth) {
_preferredMaxLayoutWidth = preferredMaxLayoutWidth;
_didSetup = NO;
[self invalidateIntrinsicContentSize];
}
}
// 重写setter给bool赋值
- (void)setRegularWidth:(CGFloat)intrinsicWidth{
if (_regularWidth != intrinsicWidth) {
_regularWidth = intrinsicWidth;
if (intrinsicWidth == 0) {
self.isIntrinsicWidth = NO;
}else{
self.isIntrinsicWidth = YES;
}
}
}
- (void)setRegularHeight:(CGFloat)intrinsicHeight{
if (_regularHeight != intrinsicHeight) {
_regularHeight = intrinsicHeight;
if (intrinsicHeight == 0){
self.isIntrinsicHeight = NO;
}
else{
self.isIntrinsicHeight = YES;
}
}
}
@end
Demo下载地址:https://github.com/rundonkey/DzTagView.git
Eed
技术交流互相学习QQ号412282037,每天进步一点点🙂