在app的开发过程当中,我们会根据业务需求,封装一些UI的控件,在写控件的过程中,我们经常会遇到这样的问题,在什么方法里导入控件需要的数据,在什么方法里对数据进行计算,在什么方法里绘制控件,那么这篇文章就讲一下我对一个UI控件构建的流程和理解
一个UI控件的构成总共分为4步:
- 导入数据,初始化控件
- 根据控件绘制的需求,对数据进行计算
- 绘制控件
- 提供一个方法用来根据新的数据刷新控件
下面我用一个常见的可横滑切换的SegmentedControl这个UI控件来解释这四个步骤应该怎么实现,效果图如下:
WechatIMG62.jpeg
首先我们创建这个控件,叫SegmentedControl,基于UIControl,这个控件由一个UIScrollView跟多个CATextLayer组成
1. 导入数据,初始化控件
利用init方法创建控件,并导入控件所需的数据
- (id)initWithConfig:(SegmentedControlConfig *)config
{
if (self = [super init]) {
self.config = config;
[self initSegmentControl];
}
}
- (void)initSegmentControl
{
// 初始化该控件所需的properties
// 初始化UIScrollView
}
2. 根据控件绘制的需求,对数据进行计算
在一个控件里,我们会用layoutSubviews来进行数据计算,layoutSubviews将会在设置控件frame(非CGRectZero) 的时候被调用。官方给出的layoutSubviews解释是Subclasses can override this method as needed to perform more precise layout of their subviews,由于所有的CATextLayer的segments跟UIScrollView都是该控件的subviews,所以我们使用layoutSubviews来根据提供的数据计算它们的layouts
- (void)layoutSubviews
{
[super layoutSubviews];
[self updateSegmentsLayout];
}
- (void)updateSegmentsLayout
{
// 根据self.config计算所有的CATextLayer segment的宽度跟高度,并用两个array保存以便绘制时使用
// 根据总宽度计算UIScrollView的frame跟contentSize
}
补充说明下layoutSubviews的调用机制,在这几种情况下会被调用:
- 初始化不会触发
layoutSubviews,但是如果设置了不为CGRectZero的frame的时候就会触发 -
addSubview会触发,前提是frame不为CGRectZero - 设置
view的frame会触发,前提是frame不为CGRectZero - 滚动一个
UIScrollView会触发 - 旋转screen会触发
superview上的layoutSubviews -
setNeedsLayout手动触发layoutSubviews - 改变一个
UIView大小的时候也会触发superview上的layoutSubviews -
removeFromSuperview只会调用superview的layoutSubviews方法
3. 绘制控件
在一个控件里,我们会用drawRect:来绘制控件里所有的views, 控件在第一次displayed的时候会调用drawRect
- (void)drawRect:(CGRect)rect
{
// 根据updateSegmentsLayout方法里计算得到的segments宽度高度的数据,来绘制全部,上海,北京 etc. 的CATextLayer segments
}
补充说明下drawRect的调用机制,在这几种情况下会被调用:
- 如果在
UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect调用是在Controller->loadView,Controller->viewDidLoad两方法之后掉用的。所以不用担心在控制器中,这些View的drawRect就开始画了。这样可以在控制器中设置一些值给View(如果这些Viewdraw的时候需要用到某些变量值). - 该方法在调用
sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。 - 通过设置
contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。 - 直接调用
setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。 -
view第一次displayed会触发drawRect:
4. 根据新的数据刷新控件
由于我们用layoutSubviews,drawRect两个方法来计算,绘制控件,那么用新数据来刷新控件的方法会非常的简单并清晰,我们只需在获取新数据之后手动调用layoutSubviews,drawRect这两个方法即可
- (void)reload:(SegmentedConfig *)config
{
self.config = config;
[self setNeedsLayout]; // call layoutSubviews
[self setNeedsDisplay]; // call drawRect
}
5. 在业务层创建这个控件
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view addSubview: self.segmentedControl];
}
- (SegmentControl *)segmentedControl
{
if (!_segmentedControl) {
_segmentedControl = [[SegmentedControl alloc] initWithConfig:self.config];
_segmentedControl.frame = CGRectMake(0, 0, self.view.frame.size.width, 60); // will call layoutSubviews
}
return _segmentedControl;
}
控件完整代码如下:
- (id)initWithConfig:(SegmentedControlConfig *)config
{
if (self = [super init]) {
self.config = config;
[self initSegmentControl];
}
}
- (void)initSegmentControl
{
// 初始化该控件所需的properties
// 初始化UIScrollView
}
- (void)updateSegmentsLayout
{
// 根据self.config计算所有的CATextLayer segment的宽度跟高度,并用两个array保存以便绘制时使用
// 根据总宽度计算UIScrollView的frame跟contentSize
}
- (void)reload:(SegmentedConfig *)config
{
self.config = config;
[self setNeedsLayout]; // call layoutSubviews
[self setNeedsDisplay]; // call drawRect
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self updateSegmentsLayout];
}
- (void)drawRect:(CGRect)rect
{
// 根据updateSegmentsLayout方法里计算得到的segments宽度高度的数据,来绘制全部,上海,北京 etc. 的CATextLayer segments
}
总结下,创建一个控件的正确步骤是这样的:init -> layoutSubviews -> drawRect:
跟SegmentedControl这个控件配套的是下面可以横滑的PagerViewController的一整套控件,跟网易新闻的类似,近期会整理出来会放GitHub上
转载请注明出处,原文地址:http://kobedai.me/p9rsts-6h/








网友评论