在app的开发过程当中,我们会根据业务需求,封装一些UI的控件,在写控件的过程中,我们经常会遇到这样的问题,在什么方法里导入控件需要的数据,在什么方法里对数据进行计算,在什么方法里绘制控件,那么这篇文章就讲一下我对一个UI控件构建的流程和理解
一个UI控件的构成总共分为4步:
- 导入数据,初始化控件
- 根据控件绘制的需求,对数据进行计算
- 绘制控件
- 提供一个方法用来根据新的数据刷新控件
下面我用一个常见的可横滑切换的SegmentedControl这个UI控件来解释这四个步骤应该怎么实现,效果图如下:
首先我们创建这个控件,叫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
(如果这些View
draw的时候需要用到某些变量值). - 该方法在调用
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/