04一个简单的UI控件-正在处理中...

要做的事

下面这个场景大家总是见过的


菊花下面写一句话

我们要做的就是这个"菊花下面写一句话"的UI控件。

思路

这个控件有三个显而易见的组件

  • 透明黑背景(coverView)
  • 一个菊花(activityIndicator)
  • 一句话(titleLabel)

很显然,思路就是根据这“一句话”计算coverView所需要的宽度,然后把菊花和这句话塞到coverView中去,稍微做下排版(居中、上下左右间距)就好了。

暴力实现

外部接口

- (nonnull instancetype)initWithFrame:(CGRect)frame title:(nonnull NSString *)title; 

内部实现

@implementation zkeyActivityIndicatorView

- (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title
{
    self = [super initWithFrame:frame];
    
    if (self) {
        self.backgroundColor = [UIColor clearColor];
        
        // activity indicator
        // defalt size of UIActivityIndicatorViewStyleWhiteLarge is 37*37
        CGFloat activityIndicatorWidth = 37.0;
        CGFloat activityIndicatorHeight = activityIndicatorWidth;
        UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
        
        // title label
        CGFloat titleLabelHeight = 21;
        UILabel *titleLabel = [[UILabel alloc] init];
        titleLabel.text = title;
        titleLabel.textColor = [UIColor whiteColor];
        titleLabel.textAlignment = NSTextAlignmentCenter;
        UIFont *titleFont = [UIFont systemFontOfSize:15.0];
        titleLabel.font = titleFont;
        
        //...
        CGFloat leadingAndTrailSpace = 30.0;
        CGFloat topAndBottomSpace = 15;
        CGFloat verticalSpace = 10.0;
        // caculate view width
        CGSize maxLabelSize = CGSizeMake(frame.size.height - 2 * leadingAndTrailSpace, 200);
        CGSize labelSize = [title boundingRectWithSize:maxLabelSize options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) attributes:[NSDictionary dictionaryWithObjectsAndKeys:titleLabel.font, NSFontAttributeName, nil] context:nil].size;
        
        CGFloat viewWith = MAX(activityIndicatorWidth, labelSize.width) + 2 * leadingAndTrailSpace;
        CGFloat viewHeight = activityIndicatorHeight + labelSize.height + 2 * topAndBottomSpace + verticalSpace;
        
        // cover view
        UIView *coverView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, viewWith, viewHeight)];
        coverView.center = CGPointMake(frame.size.width / 2.0, frame.size.width / 2.0);
        [self addSubview:coverView];
        
        coverView.backgroundColor = [UIColor blackColor];
        coverView.alpha = 0.8;
        coverView.layer.masksToBounds = YES;
        coverView.layer.cornerRadius = 5.0;
        
        
        // add activity indicator
        CGRect activityIndicatorFrame = CGRectMake(0, 0, activityIndicatorWidth, activityIndicatorHeight);
        activityIndicator.frame = activityIndicatorFrame;
        activityIndicator.center = CGPointMake(coverView.frame.size.width / 2.0, topAndBottomSpace + activityIndicatorHeight / 2.0);
        [coverView addSubview:activityIndicator];
        // add title lable
        CGRect titleLabelFrame = CGRectMake(0, activityIndicator.frame.origin.y + activityIndicatorHeight + verticalSpace, viewWith, titleLabelHeight);
        titleLabel.frame = titleLabelFrame;
        [coverView addSubview:titleLabel];
        
        // ...
        [activityIndicator startAnimating];
    }
    
    return self;
}

代码实在惨不忍睹,大家尽情地喷吧,下面开始优化。

优化

关于自定义控件的若干准则,请这篇文章:关于iOS控件开发的若干准则,请参见如何设计一个 iOS 控件? iOS 控件完全解析

使用懒加载整理代码

关于懒加载的概念及优缺点,请参见iOS开发之旅之懒加载
在custom getter中,我个人建议只进行逻辑和特性的初始化,具体的layout(frame的设置)在合适的地方进行(譬如说在layoutSubview中),尤其是那些需要不断适应屏幕尺寸(发生屏幕旋转事件)的控件。这里拿菊花下面的那个titleLabel举例:

#define TITLE_LABEL_FONT_SIZE 15.0f

- (UILabel *)titleLabel
{
    if (!_titleLabel) {
        _titleLabel = ({
            UILabel *label = [[UILabel alloc] init];
            // 逻辑初始化
            label.textAlignment = NSTextAlignmentCenter;
            label.textColor = [UIColor whiteColor];
            // 对于fontSize等其他控制变量使用宏定义 方便以后修改
            label.font = [UIFont systemFontOfSize:TITLE_LABEL_FONT_SIZE];

            label;
        });
    }
    
    return _titleLabel;
}

同理对coverView和activityIndicator使用懒加载,这样子代码看起来就舒服多了。

控件使用场景(需求分析)

使用这个控件的一般情形:用户在页面激活了与服务器交互的事件,客户端提示用户耐性等待。例如支付宝“设置头像”模块中,选好照片后,便会出现这个控件(透明黑背景+菊花+正在设置中...)。然而在同一个页面中,用户可以激活与服务器交互的事件可能有多个,所以可能需要多个“菊花+一句话”,如果使用多个实例,显然在性能上是划不来的。所以应该是重新设置控件中的“一句话”,控件根据这句话自适应调整子视图布局,然后直接继续使用这个实例就行。

接口设计
  • 使用者可以设置控件上显示的消息,同时控件应自适应地改变子视图的布局
- (void)setTitle:(nonnull NSString *)title;
- (void)setTitle:(NSString *)title
{
    // calculate the width of cover view according to the title
    // set the coverView in the center and adjust other subviews's layout correspondingly.
    self.coverView.frame = [self frameOfCoverViewWithTitle:title];
    self.activityIndicator.frame = CGRectMake((self.coverView.frame.size.width - ACTIVITY_INDICATOR_WIDTH) / 2.0f, TOP_OR_BOTTOM_SPACE, ACTIVITY_INDICATOR_WIDTH, ACTIVITY_INDICATOR_HEIGHT);
    self.titleLabel.frame = CGRectMake(0, self.activityIndicator.frame.origin.y + ACTIVITY_INDICATOR_HEIGHT + VERTICAL_SPACE, self.coverView.frame.size.width, TITLE_LABEL_HEIGHT);
    
    self.titleLabel.text = title;
}
  • 无论用户使用何种初始化方式(系统或者自定义),应该得到相同的实例
    对于一个UI控件,用户可能通过 alloc + init + setFrame 或 alloc + initWithFrame 或 alloc + initWithFrame:andTitle:(自定义方法) 来得到一个实例。所以我们可能需要重写以下方法
// 因为这两个方法已经在UIView的接口文件中提供了,所以真正的接口文件里无需这两行代码
- (instancetype)initWithFrame:(CGRect)frame;
- (void)setFrame:(CGRect)frame;
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    
    if (self) {
        self.backgroundColor = [UIColor clearColor];
        // additional initialization
        
        [self addSubview:self.coverView];
    }
    
    return self;
}

- (void)setFrame:(CGRect)frame
{
    [super setFrame:frame];
    // there is no additional initialization
    
    [self addSubview:self.coverView];
}

自定义的初始化方法如下

- (nonnull instancetype)initWithFrame:(CGRect)frame title:(nonnull NSString *)title
{
    self = [super initWithFrame:frame];
    
    if (self) {
        self.backgroundColor = [UIColor clearColor];
        
        [self addSubview:self.coverView];
        
        [self setTitle:title];
    }
    
    return self;
}

加上注释后,最终的接口文件如下

@interface zkeyActivityIndicatorView : UIView

/*
 * there is no more work to do if you use the following initializer to get an instance.
 */
- (nonnull instancetype)initWithFrame:(CGRect)frame title:(nonnull NSString *)title;


/*
 * 1. Use this method to set alert title if you use system initializer(eg. initwithFrame:)
 * 2. Change the title when you needed, the layout of subview will auto fit according to the title.
 */
- (void)setTitle:(nonnull NSString *)title;

@end
性能优化

主要的性能优化体现在:控件从父视图中移除(或者控件被隐藏)的时候,停止菊花的转动。根据这个思路,需要重写UIView的三个方法

/*
 * performance improvement
 * start animation when the view is added to superView
 * stop the animation when the view is removed from superView
 */
- (void)removeFromSuperview
{
    [super removeFromSuperview];
    
    [self.activityIndicator stopAnimating];
}

- (void)didMoveToSuperview
{
    [self.activityIndicator startAnimating];
}

- (void)setHidden:(BOOL)hidden
{
    [super setHidden:hidden];
    
    if (hidden) {
        [self.activityIndicator stopAnimating];
    } else {
        [self.activityIndicator startAnimating];
    }
}
其他部分的优化

当外部使用者传入的title比较长的时候,控件可见部分(coverView)的宽度应该有个限制,同时根据title计算titleLabel的行数(或者将UILabel替换成UITextView)。


长title的处理

我的做法是根据传入的外部传入的frame和去计算coverView和titleLabel的最大size,设定titleLabel.numberOfLines为2。这样下来,控件大约能显示30个字,足够用了,而且相对于textView来说,这种解决方案比较简单。下面是新的控件尺寸的计算方法,完整的代码可以通过访问我的github得到。

- (CGRect)frameOfCoverViewWithTitle:(NSString *)title
{
    CGSize labelSize = [self sizeForText:title];
    
    CGFloat viewWith = MAX(ACTIVITY_INDICATOR_WIDTH, labelSize.width) + 2 * LEADING_OR_TRAIL_SPACE;
    viewWith = MIN(viewWith, self.frame.size.width);
    
    CGFloat viewHeight = ACTIVITY_INDICATOR_HEIGHT + labelSize.height + 2 * TOP_OR_BOTTOM_SPACE + VERTICAL_SPACE;
    viewHeight = MIN(viewHeight, self.frame.size.height);
    
    CGRect frame = CGRectMake((self.frame.size.width - viewWith) / 2.0f, (self.frame.size.height - viewHeight) / 2.0, viewWith, viewHeight);
    
    return frame;
}

- (CGSize)sizeForText:(NSString *)text
{
    CGFloat maxLabelWidth = self.frame.size.width - 2 * LEADING_OR_TRAIL_SPACE;
    CGFloat maxLabelHeight = self.frame.size.height - 2 * TOP_OR_BOTTOM_SPACE - VERTICAL_SPACE - ACTIVITY_INDICATOR_HEIGHT
    ;
    CGSize maxLabelSize = CGSizeMake(maxLabelWidth, maxLabelHeight);
    
    CGSize labelSize = [text boundingRectWithSize:maxLabelSize options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) attributes:[NSDictionary dictionaryWithObjectsAndKeys:self.titleLabel.font, NSFontAttributeName, nil] context:nil].size;
    
    return labelSize;
}

- (void)setTitle:(NSString *)title
{
    // calculate the width of cover view according to the title
    // set the coverView in the center and adjust other subviews's layout correspondingly.
    self.coverView.frame = [self frameOfCoverViewWithTitle:title];
    self.activityIndicator.frame = CGRectMake((self.coverView.frame.size.width - ACTIVITY_INDICATOR_WIDTH) / 2.0f, TOP_OR_BOTTOM_SPACE, ACTIVITY_INDICATOR_WIDTH, ACTIVITY_INDICATOR_HEIGHT);
    
    CGSize titleLabelSize = [self sizeForText:title];
    self.titleLabel.frame = CGRectMake((self.coverView.frame.size.width - titleLabelSize.width) / 2.0f, self.activityIndicator.frame.origin.y + ACTIVITY_INDICATOR_HEIGHT + VERTICAL_SPACE, titleLabelSize.width, titleLabelSize.height);
    
    self.titleLabel.text = title;
}

鲁棒性

外界可能多次改编zkeyActivityIndicatorView.frame,根据之前的代码

- (void)setFrame:(CGRect)frame
{
    [super setFrame:frame];
    // there is no additional initialization

    [self addSubview:self.coverView];
}

coverView可能会被重复添加。而且,在几种初始化方式中,有相当部分的代码是重复的,don't repeat yourself! 所以需要做一下优化处理

- (instancetype)init
{
    self = [super init];
    
    if (self) {
        [self customInitialized];
    }
    
    return self;
}


- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    
    if (self) {
        [self customInitialized];
    }
    
    return self;
}


- (nonnull instancetype)initWithFrame:(CGRect)frame title:(nonnull NSString *)title
{
    self = [super initWithFrame:frame];
    
    if (self) {
        [self customInitialized];
        [self setTitle:title];
    }
    
    return self;
}

- (void)customInitialize
{
    self.backgroundColor = [UIColor clearColor];
    
    [self addSubview:self.coverView];
}

总结

写一个在功能和鲁棒上都比较完善的UI控件的确不容易,接下来准备写一个图片自动轮播的控件。希望我的文章能帮助到大家。

代码和Demo已上传到Github,欢迎大家使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,142评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,952评论 4 60
  • 微弱的灯光下 那缓缓的步伐向前方卖力的走去 是我熟悉的身影 那宽厚的肩背 是我依赖过的地方 灯光越来越暗 影子越来...
    小丑先生i阅读 512评论 6 5
  • 爱你一生不变,相爱的人都希望对方说出这样的一句话。 但说出来的确非常容易,做出来就要用一生的精力去兑现。 男人听到...
    小葵科技姜建峰阅读 404评论 0 0
  • 今天来聊聊什么是超级认知笔记术。 第一站/1个定义:如何打造场景化有序笔记体系 探秘的第一步,我们首先要对笔记有所...
    北小野阅读 467评论 0 5