手把手教你封装属于自己的分段滚动视图(上)

大家好,今天我来教大家封装一个滚动视图,额,其实我也不确定它应该叫什么名字,所以我就起名字为YCSegment了。看下图大家应该就能明白是神马。。

(还望各位高手指出不足)

依然推荐新手学习,这次的代码为Objective-C。

在本文中你将会学习到:

  • 封装

  • 懒加载

  • 协议/代理

  • KVO(键值观察者)

  • Target – Action 模式

  • 通过文本内容和字体计算Label的宽度或高度

结构如下:


首先我们从 item 开始:

不包括下面橘黄色的杠杠!!

上代码。

首先新建一个类,继承自UIView。我命名为:YCSegmentViewTitleItem

自定义一个初始化方法:

@interface YCSegmentViewTitleItem : UIView

///标题(我们希望可以后期更改标题)

@property (nonatomic,copy) NSString  *title;

///间距(我们希望可以控制如果有多个item他们文字的间距)

@property (nonatomic,assign) CGFloat  space;

……

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

……

@end

意图是给它一个需要显示的字符串和它的位置,然后让它自己去搞定。

@interface YCSegmentViewTitleItem ()

……

@property (nonatomic,strong) UILabel *titleLabel;

@end

@implementation YCSegmentViewTitleItem

- (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title {

    self = [super initWithFrame:frame];

    if (self) {

        self.space = 8;

        self.title = title;

    }

    return self;

}

……

@end

大家有没有发现,在这里进行了一些赋值,但是我们没有初始化titleLabel啊!

而且为毛线我们要给 self.title 赋值为title !

其实是这样的:

@implementation YCSegmentViewTitleItem

……

- (void)setTitle:(NSString *)title {

    _title = title;

    self.titleLabel.text = title;

    [self setNeedsLayout];

}

……

@end

我们实现了title属性的setter方法,在里面给实例变量_title赋值用来保存值,然后我们给self.titleLabel.text赋值为title, 最后希望它从新布局,因为可能会改变宽度啊。

那么titleLabel在哪初始化?

这里我用了懒加载的方法来初始化这个titleLabel:

@implementation YCSegmentViewTitleItem

………

- (UILabel *)titleLabel {

    if (!_titleLabel) {

        _titleLabel = [[UILabel alloc] initWithFrame:self.bounds];

        _titleLabel.textColor = _defaultTitleColor_Normal;

        _titleLabel.font = _defaultFont;

        _titleLabel.textAlignment = NSTextAlignmentCenter;

        [self addSubview:_titleLabel];

    }

    return _titleLabel;

}

@end

包括了初始化、颜色、字体、居中、添加为self的子视图等。。

!小贴士:


Objective-C中的懒加载的写法是重写某一个属性的getter。因为有些情况下我们并不能够确定什么时候初始化一个数组或者字典。多数用在网络请求之后保存结果等。。万一这个数组或者字典你用不到呢?给它个内存空间多浪费呀。所以什么时候用什么时候初始化。

注意!在getter中不要用self.titleLabel!会产生递归的。。。因为self.titleLabel实际上就是[self titleLabel]或[self setTitleLabel:_]~

懒加载节省了系统响应时间,提升了系统性能,非常具有利用价值。( 虽然在本项目中没什么体现。。以后有机会详细讲解喽 )


这样,一旦调用初始化方法,就完成了label的赋值操作等。接下来我们还需要布局:

……

#define _MinWidth  32

#define _MaxWidth  YCMainScreenWidth / 2

……

@implementation YCSegmentViewTitleItem

……

- (void)layoutSubviews {

    CGFloat buttonWidth = [self calcuWidth];

    self.frame = CGRectMake(self.x, self.y, buttonWidth + self.space, self.height);

    self.titleLabel.frame = CGRectMake(self.space / 2, 0, buttonWidth, self.height);

}

//在没有title为空的时候,返回最小宽度,不然就没有宽度了

- (CGFloat)calcuWidth {

    if (self.title == nil) {

///大家想一想,在初始化的时候我没有判断`title`是否为`nil`

///这样依然会初始化`titleLabel`

///我该怎么做来避免无用的初始化?

        return _MinWidth;

    }

    UIFont *font = self.font == nil ? _defaultFont : self.font;

    CGRect frame = [self.title boundingRectWithSize:CGSizeMake(_MaxWidth, self.height) options:(NSStringDrawingUsesLineFragmentOrigin) attributes:@{NSFontAttributeName: font} context:nil];

    CGFloat width = frame.size.width;

    return width > _MinWidth ? width : _MinWidth;

}

……

@end

然后我们希望在外界可以知道他应该具有的宽度,而且想要方便调用:

@interface YCSegmentViewTitleItem : UIView

……

+ (CGFloat)calcuWidth:(NSString *)title;

……

@end

@implementation YCSegmentViewTitleItem

……

+ (CGFloat)calcuWidth:(NSString *)title {

    YCSegmentViewTitleItem *item = [[YCSegmentViewTitleItem alloc] initWithFrame:(CGRectZero) title:title];

    return [item calcuWidth] + item.space;

}

……

@end

然后希望它被选中时候改变个颜色

@interface YCSegmentViewTitleItem : UIView

……

@property (nonatomic,assign) BOOL  highlight;

……

@end

@implementation YCSegmentViewTitleItem

……

- (void)setHighlight:(BOOL)highlight {

    _highlight = highlight;

    self.titleLabel.textColor = highlight == YES ? _hasHighlightColor == YES ? self.highlightColor : _defaultTitleColor_Highlight : _hasNormalColor == YES ? self.normalColor : _defaultTitleColor_Normal;

}

……

@end

然后我们还需要他能够触发点击事件:

@interface YCSegmentViewTitleItem : UIView

……

- (void)addTarget:(id)target action:(SEL)action;

……

@end

@interface YCSegmentViewTitleItem ()

{

    ……

    id   _target;

    SEL  _action;

}

……

@end

@implementation YCSegmentViewTitleItem

……

- (void)addTarget:(id)target action:(SEL)action {

    _target = target;

    _action = action;

}

……

@end

好了,我们已经保存了target和action,但是什么时候触发?!

我们希望点击内部并在内部抬起的时候让target去执行action,所以我们需要一个实例变量来记录是不是在内部点击……

@interface YCSegmentViewTitleItem ()

{

    ……

    BOOL _touchedFlag;

}

……

@end

@implementation YCSegmentViewTitleItem

……

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    UITouch *touch = touches.anyObject;///获取一个点击

    CGPoint touchPoint = [touch locationInView:self];///获取这个点击在哪个位置

    if (touchPoint.x >= 0 & touchPoint.x = 0 && touchPoint.y  *)touches withEvent:(UIEvent *)event {

    UITouch *touch = touches.anyObject;

    CGPoint touchPoint = [touch locationInView:self];

    if (touchPoint.x >= 0

这样子主要接口实现就ok了~(半个UIButton的感觉有没有!但是UIButton并不是怎么写的)


接下来是itemContent:

这个itemContent实际上我命名为:YCSegmentItemsContentView。

它是承载item的容器,并且来布局item。

它来负责控制高亮哪一个item,或者传递出点击了哪个一个item的消息。

首先我们先自定义初始化方法:

@interface YCSegmentItemsContentView : UIView

……

- (instancetype)initWithFrame:(CGRect)frame titles:(NSArray *)titles;

……

@end

我们希望告诉它它应该显示在什么位置,并且显示多少个标题。剩下的让他自己去解决。

@interface YCSegmentItemsContentView ()

{

    CGFloat _buttonWidthSUM;

    YCSegmentViewTitleItem *_currentItem;

}

@property (nonatomic,strong) UIView *buttonContentView;

@property (nonatomic,strong) UIView *line;

@property (nonatomic,strong) NSMutableArray *buttonsArray;

@property (nonatomic,strong) NSMutableArray *buttonWidths;

@property (nonatomic,strong) NSArray *items;

@end

@implementation YCSegmentItemsContentView

……

- (instancetype)initWithFrame:(CGRect)frame titles:(NSArray *)titles {

    if (self = [super initWithFrame:frame]) {

        self.items = [titles copy];

        [self setupAllButtons];

    }

    return self;

}

- (void)setupAllButtons {

    self.backgroundColor    = [UIColor groupTableViewBackgroundColor];

    self.buttonContentView = [[UIView alloc] initWithFrame:CGRectZero];

    self.buttonContentView.backgroundColor = [UIColor whiteColor];

    [self addSubview:self.buttonContentView];

    self.line = [[UIView alloc] initWithFrame:CGRectZero];

    self.line.backgroundColor = [UIColor orangeColor];

    [self addSubview:self.line];

    ///遍历所有的标题,(我这里`self.items`明明有歧义。。)

    for (NSString *title in self.items) {

        ///初始化一个`item`

        YCSegmentViewTitleItem *item = [[YCSegmentViewTitleItem alloc] initWithFrame:(CGRectZero) title:title];

        [item addTarget:self action:@selector(buttonAction:)];//添加点击事件

        [self.buttonsArray addObject:item];//添加到数组中

        [self.buttonContentView addSubview:item];

        ///计算`item`的宽度

        CGFloat width = [YCSegmentViewTitleItem calcuWidth:title];

        ///保存宽度以备布局(NSArray只能保存对象类型)

        [self.buttonWidths addObject:[NSNumber numberWithDouble:width]];

        ///计算所有item的宽度和

        _buttonWidthSUM += width;

        if (_currentItem == nil) {

            /// 默认高亮第一个item,当`_currentItem`第一次赋值之后就跳过

            _currentItem = item;

            item.highlight = YES;

        }

    }

}

……

///懒加载又出现了

- (NSMutableArray *)buttonsArray{

    if (!_buttonsArray) {

        _buttonsArray = [NSMutableArray array];

    }

    return _buttonsArray;

}

- (NSMutableArray *)buttonWidths{

    if (!_buttonWidths) {

        _buttonWidths = [NSMutableArray array];

    }

    return _buttonWidths;

}

@end

保存所有item的宽度和宽度和是为了我们希望item的宽度跟随内容,并且item之间的距离是等宽的。(item有space属性,是不是没有用到?- – 确实没啥子用哈~)

接下来是布局

@implementation YCSegmentItemsContentView

……

- (void)layoutSubviews {

    CGFloat height = self.bounds.size.height;

    CGFloat width  = self.bounds.size.width;

    self.buttonContentView.frame = CGRectMake(0, 0, width, height - 2);

    CGFloat spacing = 0;

    if (_buttonWidthSUM >= width) {

        spacing = 0;

    } else {

        spacing = (width - _buttonWidthSUM) / (_buttonWidths.count + 1);

    }

    for (int x = 0; x

布局完成了。。接下来呢我们想要通过外接知道当前应该选中哪一个item,并且也希望告诉外接主动选择了哪一个item,怎么告诉?!用代理或者block都可以。在这里我们选择代理:

@protocol YCSegmentItemsContentViewDelegate;

@interface YCSegmentItemsContentView : UIView

……

@property (nonatomic,assign) id delegate;

@property (nonatomic,assign) NSInteger page;

……

@end

@protocol YCSegmentItemsContentViewDelegate

- (void)didSelectedButtonAtIndex:(NSInteger)index;

@end

@implementation YCSegmentItemsContentView

……

///这是每一个item的点击事件

- (void)buttonAction:(YCSegmentViewTitleItem *)sender {

    NSInteger index = [self.buttonsArray indexOfObject:sender];

    [self setPage:index];

    if (self.delegate & [self.delegate respondsToSelector:@selector(didSelectedButtonAtIndex:)]) {

        [self.delegate didSelectedButtonAtIndex:index];

    }

}

- (void)setPage:(NSInteger)page {

    if (_page == page) {

        return;

    }

    _page = page;

    [self moveToPage:page];

}

- (void)moveToPage:(NSInteger)page {

    if (page > self.buttonsArray.count) {

        return;

    }

    YCSegmentViewTitleItem *item = self.buttonsArray[page];

    _currentItem.highlight = NO;

    _currentItem = item;

    item.highlight = YES;

    [UIView animateWithDuration:0.2 animations:^{

        CGRect buttonFrame = item.frame;

        CGRect lineFrame = self.line.frame;

        lineFrame.origin.x = buttonFrame.origin.x;

        lineFrame.size.width = buttonFrame.size.width;

        self.line.frame = lineFrame;

    } completion:^(BOOL finished) {

        if (finished) {

        }

    }];

}

……

@end

外界设置代理之后,就可以让代理去执行我们想要让他执行的协议方法,但在这之前,需要确定代理是否存在,代理有没有实现协议方法。否则会崩溃哦。

好了,现在这个控件也可以实现和其他控件的交互了。

原文地址:https://www.cnblogs.com/fengmin/p/5478396.html