在开发过程中由于系统的控件有时候不能满足开发者的需求,学会制作自定义控件是很有必要的。这篇文章主要总结整理开发基本的自定义控件的步骤以及一些知识点的梳理。
目标
制作一个自定义控件,能够实现如下效果。
制作思路及分析
对这个控件的布局及结构进行分析。对这整个控件的封装实际上就是对一个View的封装,这个View的大小和屏幕大小相同,在它的顶部有一个子视图(topView),这个topView上面承载了我们需要的两个button控件,而imageView以及titleLabel是button中的子属性,图文为上下布局方式。
对控件展现的逻辑以及需求进行分析。在触发该控件(如在demo中通过点击导航栏中的按钮触发)后,控件中的topView部分整体从导航栏上方滑到导航栏下方,直至topview顶部与导航栏底部接合。与此同时,控件的其他部分变成30%不透明度的背景。在这整个过程中涉及到三个简单动画
1、topView的位置从导航栏上方-->导航栏下方
2、topView的不透明度从0-->1
3、控件的背景色从 clearColor --> blackColor,0.3
(这里有个做的不好的地方就是在改变控件背景色的时候,由于导航栏是带透明的,导致导航栏的颜色也被影响,但这个不是重点)
在点击button之后收回控件,然后跳转button的逻辑,或者点击其他地方收回控件。
实现过程
-
对UIView进行封装
1、新建一个继承UIView的类
2、在新建的类中添加子控件的属性(weak)
3、在initWithFrame方法中添加子控件
4、在layoutSubViews方法中设置子控件的frame
5、提供一个model属性,将我们要制作的控件抽象为这个model,负责数据的交互,重写model的构造方法
6、重写UIView类的setModel方法,并在其中为子控件赋值。
SelectButtonView代码如下
//1、新建一个继承UIView的类
#import <UIKit/UIKit.h>
@class SelectButtonModel;
@interface SelectButtonView : UIView
//5、提供一个model属性,将我们要制作的控件抽象为这个model,负责数据的交互,重写model的构造方法
@property (nonatomic,strong) SelectButtonModel *model;
@property (nonatomic, copy) void (^leftButtonAction)(BOOL shouldResponse);
@property (nonatomic, copy) void (^rightButtonAction)(BOOL shouldResponse);
@end
@interface SelectButtonMenu : UIControl
+ (void) showMenuInView:(UIView *)view
leftName:(NSString *)leftName
rightName:(NSString *)rightName
leftIcon:(NSString *)leftIcon
rightIcon:(NSString *)rightIcon
leftButtonClickAction:(void(^)(BOOL shoudResponse))leftButtonAction
rightButtonClickAction:(void(^)(BOOL shoudResponse))rightButtonAction
dismissAction:(void(^)())dismissAction;
@end
#import "SelectButtonView.h"
#import "SelectButtonModel.h"
#import "UIButton+Composing.h"
const CGFloat separateLineWidth = 0.5f;
//const CGFloat orgialY = 64.f;
#define orgialY ({([UIApplication sharedApplication].isStatusBarHidden)? 44.f : 64.f; })
const CGFloat viewHeight = 75.f;
@interface SelectButtonView()
//2、在新建的类中添加子控件的属性(weak)
@property (nonatomic, weak) UIButton *leftButton;
@property (nonatomic, weak) UIButton *rightButton;
@property (nonatomic, weak) UIView *topView;
@end
@implementation SelectButtonView
//3、在initWithFrame方法中添加子控件
- (instancetype) initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
//// 注意:该处不要给子控件设置frame与数据,可以在这里初始化子控件的属性
[self setBackgroundColor:[UIColor clearColor]];
self.opaque = NO;
//单击手势,用于实现单击屏幕其他地方收回控件
UITapGestureRecognizer *gestureRecognizer;
gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(singleTap:)];
[self addGestureRecognizer:gestureRecognizer];
UIView *topView = [[UIView alloc] init];
[topView setBackgroundColor:[UIColor lightGrayColor]];
self.topView = topView;
[self addSubview:topView];
UIButton *leftButton = [[UIButton alloc] init];
[leftButton setBackgroundColor:[UIColor whiteColor]];
[leftButton.titleLabel setFont:[UIFont systemFontOfSize:13.f]];
[leftButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[leftButton setBackgroundImage:[self imageWithColor:[UIColor lightGrayColor]] forState:UIControlStateHighlighted];
[leftButton addTarget:self action:@selector(leftButtonClicked) forControlEvents:UIControlEventTouchUpInside];
self.leftButton = leftButton;
[self.topView addSubview:leftButton];
UIButton *rightButton = [[UIButton alloc] init];
[rightButton setBackgroundColor:[UIColor whiteColor]];
[rightButton.titleLabel setFont:[UIFont systemFontOfSize:13.f]];
[rightButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[rightButton setBackgroundImage:[self imageWithColor:[UIColor lightGrayColor]] forState:UIControlStateHighlighted];
[rightButton addTarget:self action:@selector(rightButtonClicked) forControlEvents:UIControlEventTouchUpInside];
self.rightButton = rightButton;
[self.topView addSubview:rightButton];
}
return self;
}
/*4、在layoutSubViews方法中设置子控件的frame
以下情况会触发layoutSubViews方法
1)addSubView
2)改变View的Frame
3)滚动ScrollView
4)旋转Screen
5)改变子UIView的大小会触发父UIView上的layoutSubview事件
*/
- (void)layoutSubviews
{
[super layoutSubviews];
CGFloat topViewX = 0;
CGFloat topViewH = viewHeight;
CGFloat topViewY = orgialY - topViewH;
CGFloat topViewW = self.bounds.size.width;
self.topView.frame = CGRectMake(topViewX, topViewY, topViewW, topViewH);
CGFloat leftButtonX = 0;
CGFloat leftButtonY = separateLineWidth;
CGFloat leftButtonW = (topViewW - separateLineWidth)/2;
CGFloat leftButtonH = topViewH - separateLineWidth;
self.leftButton.frame = CGRectMake(leftButtonX, leftButtonY, leftButtonW, leftButtonH);
self.leftButton.contentEdgeInsets = UIEdgeInsetsMake(8.f, 0, -11.f, 0);
CGFloat rightButtonW = (topViewW - separateLineWidth)/2;
CGFloat rightButtonX = topViewW - rightButtonW;
CGFloat rightButtonY = separateLineWidth;
CGFloat rightButtonH = topViewH - separateLineWidth;
self.rightButton.frame = CGRectMake(rightButtonX, rightButtonY, rightButtonW, rightButtonH);
self.rightButton.contentEdgeInsets = UIEdgeInsetsMake(8.f, 0, -11.f, 0);
}
//6、重写UIView类的setModel方法,并在其中为子控件赋值
- (void)setModel:(SelectButtonModel *)model
{
_model = model;
[self.leftButton setTitle:model.leftButtonName forState:UIControlStateNormal];
[self.leftButton setImage:[UIImage imageNamed:model.leftButtonIcon] forState:UIControlStateNormal];
//composingWithStyle是对UIButton的一个扩展方法,用于设置UIButton内imageView与titleLabel的布局,在最后会给出
[self.leftButton composingWithStyle:GGImageComposingTop spacing:5.f];
[self.rightButton setTitle:model.rightButtonName forState:UIControlStateNormal];
[self.rightButton setImage:[UIImage imageNamed:model.rightButtonIcon] forState:UIControlStateNormal];
[self.rightButton composingWithStyle:GGImageComposingTop spacing:5.f];
}
model代码如下
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface SelectButtonModel : NSObject
@property (nonatomic,copy) NSString * leftButtonName;
@property (nonatomic,copy) NSString * rightButtonName;
@property (nonatomic,copy) NSString * leftButtonIcon;
@property (nonatomic,copy) NSString * rightButtonIcon;
@property (nonatomic,assign) CGFloat topViewHeight;
+ (instancetype)modelWithLeftName:(NSString *)leftName leftIcon:(NSString *)leftIcon rightName:(NSString *)rightName rightIcon:(NSString *)righticon;
- (instancetype)initWithLeftName:(NSString *)leftName leftIcon:(NSString *)leftIcon rightName:(NSString *)rightName rightIcon:(NSString *)righticon;
@end
#import "SelectButtonModel.h"
@implementation SelectButtonModel
+ (instancetype)modelWithLeftName:(NSString *)leftName leftIcon:(NSString *)leftIcon rightName:(NSString *)rightName rightIcon:(NSString *)righticon
{
return [[self alloc] initWithLeftName:leftName leftIcon:leftIcon rightName:rightName rightIcon:righticon];
}
- (instancetype)initWithLeftName:(NSString *)leftName leftIcon:(NSString *)leftIcon rightName:(NSString *)rightName rightIcon:(NSString *)righticon
{
if (self = [super init]) {
self.leftButtonName = leftName;
self.leftButtonIcon = leftIcon;
self.rightButtonName = rightName;
self.rightButtonIcon = righticon;
}
return self;
}
@end
-
实现动画
在对SelectButtonView做好UI上面的封装之后,接下来我们对其添加需要的动画效果,具体代码还是在SelectButtonView的layoutSubViews方法中实现。
现在我们将layoutSubViews方法修改如下
- (void)layoutSubviews
{
[super layoutSubviews];
CGFloat topViewX = 0;
CGFloat topViewH = viewHeight;
CGFloat topViewY = orgialY - topViewH;
CGFloat topViewW = self.bounds.size.width;
self.topView.frame = CGRectMake(topViewX, topViewY, topViewW, topViewH);
//其实就是一个简单的动画。。
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDelay:0.0f];
[UIView setAnimationDuration:0.5f];
[self setBackgroundColor:[UIColor colorWithRed:0 green:0 blue:0 alpha:0.3f]];
self.topView.frame = CGRectMake(0, orgialY, self.bounds.size.width, viewHeight);
[UIView commitAnimations];
CGFloat leftButtonX = 0;
CGFloat leftButtonY = separateLineWidth;
CGFloat leftButtonW = (topViewW - separateLineWidth)/2;
CGFloat leftButtonH = topViewH - separateLineWidth;
self.leftButton.frame = CGRectMake(leftButtonX, leftButtonY, leftButtonW, leftButtonH);
self.leftButton.contentEdgeInsets = UIEdgeInsetsMake(8.f, 0, -11.f, 0);
CGFloat rightButtonW = (topViewW - separateLineWidth)/2;
CGFloat rightButtonX = topViewW - rightButtonW;
CGFloat rightButtonY = separateLineWidth;
CGFloat rightButtonH = topViewH - separateLineWidth;
self.rightButton.frame = CGRectMake(rightButtonX, rightButtonY, rightButtonW, rightButtonH);
self.rightButton.contentEdgeInsets = UIEdgeInsetsMake(8.f, 0, -11.f, 0);
}
增加收回控件的dismiss方法
- (void)dismissMenu:(BOOL)animated
{
if(animated)
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDelay:0.0f];
[UIView setAnimationDuration:0.5f];
\\在这里设置动画的代理是为了在之后调用animationDidStop方法
[UIView setAnimationDelegate:self];
[UIView setAnimationWillStartSelector:@selector(animationDidStart:)];
[UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:)];
self.topView.frame = CGRectMake(0, orgialY - viewHeight, self.bounds.size.width, viewHeight);
self.backgroundColor = [UIColor clearColor];
[UIView commitAnimations];
}
else
{
[self removeFromSuperview];
}
}
-(void)animationDidStart:(CAAnimation *)anim
{
NSLog(@"animation is start ...");
}
//确保在动画播放结束之后再调用removeFromSuperview移除控件
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
[self removeFromSuperview];
}
//单击屏幕其他地方收回控件
- (void)singleTap:(UITapGestureRecognizer *)recognizer
{
[self dismissMenu:YES];
}
-
实现回调
注意到SelectView的.h文件中一下两个属性:
@property (nonatomic, copy) void (^leftButtonAction)(BOOL shouldResponse);
@property (nonatomic, copy) void (^rightButtonAction)(BOOL shouldResponse);
分别是左右按钮点击之后的block回调。
在代码中添加一下方法,也就是button的action方法
- (void)leftButtonClicked
{
BOOL shouldResponse = YES;
[self dismissMenu:YES];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.05f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.leftButtonAction) {
self.leftButtonAction(shouldResponse);
}
});
}
-(void)rightButtonClicked
{
BOOL shouldResponse = YES;
[self dismissMenu:YES];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.05f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.rightButtonAction) {
self.rightButtonAction(shouldResponse);
}
});
}
shouldResponse变量可以去掉,是之前考虑同一个按钮多次点击的情况增加的变量,但是对单纯实现控件没有影响。
其实到这里对SelectButtonView控件的制作以及封装已经算是完成了,但是处于其他原因考虑,我又在SelectButtonView外进行了一层封装(其实什么也没做),封装为一个叫SelectButtonMenu的类,在实际使用的时候只需要调用SelectButtonMenu的类方法showMenuInView...即可
代码如下
@interface SelectButtonMenu : UIControl
+ (void) showMenuInView:(UIView *)view
leftName:(NSString *)leftName
rightName:(NSString *)rightName
leftIcon:(NSString *)leftIcon
rightIcon:(NSString *)rightIcon
leftButtonClickAction:(void(^)(BOOL shoudResponse))leftButtonAction
rightButtonClickAction:(void(^)(BOOL shoudResponse))rightButtonAction
dismissAction:(void(^)())dismissAction;
+ (void) dismissMenu;
@end
static SelectButtonMenu * selectMenu;
@interface SelectButtonMenu()
@property (nonatomic, copy) void (^dismissAction)();
@end
@implementation SelectButtonMenu{
SelectButtonView *_selectButtonView;
}
+ (instancetype) shareMenu
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
selectMenu = [[SelectButtonMenu alloc] init];
});
return selectMenu;
}
+ (void) showMenuInView:(UIView *)view
leftName:(NSString *)leftName
rightName:(NSString *)rightName
leftIcon:(NSString *)leftIcon
rightIcon:(NSString *)rightIcon
leftButtonClickAction:(void(^)(BOOL shoudResponse))leftButtonAction
rightButtonClickAction:(void(^)(BOOL shoudResponse))rightButtonAction
dismissAction:(void(^)())dismissAction
{
[[self shareMenu] showMenuInView:view leftName:leftName rightName:rightName leftIcon:leftIcon rightIcon:rightIcon leftButtonClickAction:leftButtonAction rightButtonClickAction:rightButtonAction];
}
- (void) showMenuInView:(UIView *)view
leftName:(NSString *)leftName
rightName:(NSString *)rightName
leftIcon:(NSString *)leftIcon
rightIcon:(NSString *)rightIcon
leftButtonClickAction:(void(^)(BOOL shoudResponse))leftButtonAction
rightButtonClickAction:(void(^)(BOOL shoudResponse))rightButtonAction
{
NSParameterAssert(view);
if (_selectButtonView) {
[_selectButtonView dismissMenu:NO];
_selectButtonView = nil;
}
_selectButtonView = [[SelectButtonView alloc] initWithFrame:CGRectMake(0,0, view.bounds.size.width, view.bounds.size.height)];
SelectButtonModel *model = [SelectButtonModel modelWithLeftName:leftName leftIcon:leftIcon rightName:rightName rightIcon:rightIcon];
model.topViewHeight = viewHeight;
_selectButtonView.model = model;
[view addSubview:_selectButtonView];
[_selectButtonView showMenuInView:view];
[_selectButtonView setLeftButtonAction:leftButtonAction];
[_selectButtonView setRightButtonAction:rightButtonAction];
}
+ (void)dismissMenu
{
[[self shareMenu] dismissMenu];
}
- (void)dismissMenu {
if (_selectButtonView) {
[_selectButtonView dismissMenu:NO];
_selectButtonView = nil;
}
}
@end
最后!
在我们需要的controller中只需要写类似以下的一句代码
- (IBAction)action:(id)sender {
[SelectButtonMenu showMenuInView:self.view leftName:@"左按钮" rightName:@"右按钮" leftIcon:@"keyword" rightIcon:@"promotion" leftButtonClickAction:^(BOOL shoudResponse) {
//Do anything here..
} rightButtonClickAction:^(BOOL shoudResponse) {
//Do anything here..
} dismissAction:nil];
}
整个控件的制作和封装过程大致就是这样,不管是制作多么复杂的控件,都可以将其分解然后一步一步去实现它
最后给出UIButton的图文布局扩展代码
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger, GGComposingStyle)
{
GGImageComposingLeft = 0, //> 图左字右 默认
GGImageComposingRight = 1, //> 图右字左
GGImageComposingTop = 2, //> 图上字下
GGImageComposingBottom = 3, //> 图下字上
};
@interface UIButton (Composing)
- (void)composingWithStyle:(GGComposingStyle)style spacing:(CGFloat)spacing;
@end
#import "UIButton+Composing.h"
@implementation UIButton (Composing)
- (void)composingWithStyle:(GGComposingStyle)style spacing:(CGFloat)spacing
{
// 图为空 或 文本为空, 不做处理
if (!self.imageView.image || !self.titleLabel.text.length) {
return;
}
CGFloat imageOffsetX = (self.imageView.image.size.width + [self.titleLabel.text sizeWithFont:self.titleLabel.font].width) / 2 - self.imageView.image.size.width / 2;
CGFloat imageOffsetY = self.imageView.image.size.height / 2 + spacing / 2;
CGFloat labelOffsetX = (self.imageView.image.size.width + [self.titleLabel.text sizeWithFont:self.titleLabel.font].width / 2) - (self.imageView.image.size.width + [self.titleLabel.text sizeWithFont:self.titleLabel.font].width) / 2;
CGFloat labelOffsetY = [self.titleLabel.text sizeWithFont:self.titleLabel.font].height / 2 + spacing / 2;
switch (style) {
case GGImageComposingLeft:
self.imageEdgeInsets = UIEdgeInsetsMake(0, -spacing / 2, 0, spacing / 2);
self.titleEdgeInsets = UIEdgeInsetsMake(0, spacing / 2, 0, -spacing / 2);
break;
case GGImageComposingRight:
self.imageEdgeInsets = UIEdgeInsetsMake(0, [self.titleLabel.text sizeWithFont:self.titleLabel.font].width + spacing / 2, 0, -([self.titleLabel.text sizeWithFont:self.titleLabel.font].width + spacing / 2));
self.titleEdgeInsets = UIEdgeInsetsMake(0, -(self.imageView.image.size.width + spacing / 2), 0, self.imageView.image.size.width + spacing / 2);
break;
case GGImageComposingTop:
self.imageEdgeInsets = UIEdgeInsetsMake(-imageOffsetY, imageOffsetX, imageOffsetY, -imageOffsetX);
self.titleEdgeInsets = UIEdgeInsetsMake(labelOffsetY, -labelOffsetX, -labelOffsetY, labelOffsetX);
break;
case GGImageComposingBottom:
self.imageEdgeInsets = UIEdgeInsetsMake(imageOffsetY, imageOffsetX, -imageOffsetY, -imageOffsetX);
self.titleEdgeInsets = UIEdgeInsetsMake(-labelOffsetY, -labelOffsetX, labelOffsetY, labelOffsetX);
break;
default:
break;
}
}
@end
结束
最后给出github上 Demo 的地址。