程序员iOS Developer

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

2015-12-24  本文已影响828人  彬哲

要做的事

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


菊花下面写一句话

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

思路

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

很显然,思路就是根据这“一句话”计算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;
}
// 因为这两个方法已经在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,欢迎大家使用。

上一篇下一篇

猜你喜欢

热点阅读