iOS 一個滑動選擇控制元件

QiShare發表於2019-03-18

級別: ★★☆☆☆
標籤:「iOS」「PagesContainer」「控制元件」
作者: dac_1033
審校: QiShare團隊

我們在移動專案開發中經常會用到滑動選擇功能,例如在“米家”App的首頁中,為了展示賬戶下所有智慧裝置,或者按型別、者房間展示部分相關裝置,介面上做了如下設計:

米家App首頁滑動選擇

下面我們就來介紹一下這個滑動選擇控制元件的實現過程。

1. 需求分析

上面動圖中展示的介面並不複雜,我們可以看的出來,這個滑動選擇功能主要分為兩個部分:頂部滑動選擇bar和底部可手動滑動的scrollView,並且這上下兩部分可以聯動。(底部scrollView中的子view)

滑動選擇介面的佈局

需求歸納如下:
(1)頂部為滑動可選topBar;
(2)topBar可手勢左右滑動;
(3)topBar中的可選項數量可擴充套件;
(4)topBar中的選中項自動滾動至螢幕可見區域;
(5)topBar中的每一項可點選選擇,選中項樣式有變化;
(6)topBar中的選中項底部有個遊標cursor,cursor的寬度可自適應;
(7)下部為可左右滑動scrollView;
(8)scrollView根據當前topBar選中項分屏展示,即topBar與scrollView聯動;

2. 控制元件的框架層次

我們最終要將頂部topBar和下部的scrollView作為一個整體控制元件使用,因此最外層為pageContainer(繼承於UIView)將二者封裝起來,整個控制元件的初始化、設定方法都在pageContainer中。則整個控制元件的框架很簡單,如下:

滑動選擇控制元件的框架

3. 具體實現

本問的實現的程式碼是由部門中的前輩封裝,根據專案需求我們自己可以做一些相應的改動,首先謝謝那些前輩寫出一個這麼簡單易用的控制元件,並提供給我們學習的機會。下面我們根據控制元件的框架層次結構,從下往上依次說明實現過程。

3.1 TopBarButton

本控制元件中的TopBarButton繼承於UIButton,是控制元件頂部滑動欄中的一個item,重寫UIButton的setSelected:方法就可以設定TopBarButton在選中時的狀態。如果你的專案中對TopBarButton還有其他樣式要求,可在該類中新增其他元素,並新增相應初始化方法。

//////// .h檔案
@interface QiTopBarButton : UIButton

@end


//////// .m檔案
#import "QiTopBarButton.h"

#define BADGE_WIDTH (13)
#define TitleFontSize (13)
#define ColorNormalDefault [UIColor blackColor]
#define ColorSelectedDefault [UIColor redColor];

@interface QiTopBarButton()

@end

@implementation QiTopBarButton

- (id)init {
    
    self = [super init];
    if (self) {
        _colorNormal = ColorNormalDefault;
        _colorSelected = ColorSelectedDefault;
        [self setTitleColor:_colorSelected forState:UIControlStateSelected];
        [self setTitleColor:_colorNormal forState:UIControlStateNormal];
        
        [self.titleLabel setFont:[UIFont systemFontOfSize:TitleFontSize]];

        // 設定其他子view、屬性等
    }
    return self;
}

- (void)setSelected:(BOOL)selected {
    
    [super setSelected:selected];
    if (selected) {
        [self setTitleColor:_colorSelected forState:UIControlStateNormal];
        UIFont *font = [UIFont systemFontOfSize:TitleFontSize + 1 weight:UIFontWeightBold];
        [self.titleLabel setFont:font];
    } else {
        [self setTitleColor:_colorNormal forState:UIControlStateNormal];
        UIFont *font = [UIFont systemFontOfSize:TitleFontSize weight:normal];
        [self.titleLabel setFont:font];
    }
}

@end

複製程式碼
3.2 PagesContainerTopBar

本控制元件中的PagesContainerTopBar繼承於UIView,其中的子view主要包括scrollView、一組topBarButton、cursor。

//////// .h檔案
#import <UIKit/UIKit.h>

@protocol QiPagesContainerTopBarDelegate;

@interface QiPagesContainerTopBar : UIView

@property (nonatomic, weak) id<QiPagesContainerTopBarDelegate> target;

// 設定相鄰button之間的間距(兩個button的title之間的距離)
@property (nonatomic, assign) CGFloat buttonMargin;

#pragma mark - update style

- (void)setBackgroundImage:(UIImage *)image;

- (void)setBackgroundImageHidden:(BOOL)isHidden;

- (void)setCursorPosition:(CGFloat)percent;

- (void)updateConentWithTitles:(NSArray *)titles;

- (void)setIsButtonAlignmentLeft:(BOOL)isAlignmentLeft;

- (void)setShowSeperateLines:(BOOL)showSeperateLines;

- (void)setSelectedIndex:(NSInteger)idnex;

- (NSInteger)getSelectedIndex;

// 設定滑塊的顏色
- (void)setCursorColor:(UIColor *)color;
// 設定滑塊長度
- (void)setCursorWidth:(CGFloat)width;
// 設定滑塊長度
- (void)setCursorHeight:(CGFloat)height;
// 設定按鈕選中和未選中的顏色
- (void)setTextColor:(UIColor *)normalColor andSelectedColor:(UIColor *)selectedColor;

@end


@protocol QiPagesContainerTopBarDelegate <NSObject>

@optional
// 選中topBar的一項
- (void)topBarSelectIndex:(NSInteger)index;

// 重複點選topBar時會呼叫該方法
- (void)topBarSelectIndicator:(NSInteger)index;

@end


//////// .m檔案
#import "QiPagesContainerTopBar.h"
#import "QiTopBarButton.h"

//左右側的間距
#define MARGIN_HORI (0)
#define CursorHeightDefault (1.5)
#define BUTTON_MARGIN_DEFAULT   (20)

@interface QiPagesContainerTopBar ()

@property (nonatomic, strong) UIImageView *backgroundImageView;
@property (nonatomic, strong) UIScrollView *scrollView;

@property (nonatomic, strong) UIView *cursor;
@property (nonatomic, assign) CGFloat cursorWidth;
@property (nonatomic, assign) CGFloat cursorHeight;
@property (nonatomic, strong) NSArray *arrayButtons;
@property (nonatomic, assign) BOOL isButtonAlignmentLeft;
@property (nonatomic, strong) NSArray *arraySeperateLines;
@property (nonatomic, assign) BOOL showSeperateLines;

@end

@implementation QiPagesContainerTopBar

- (id)init {
    
    self = [super init];
    if (self) {
        [self setup];
    }
    return self;
}

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

- (id)initWithCoder:(NSCoder *)aDecoder {
    
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self setup];
    }
    return self;
}

- (void)setup {
    
    _buttonMargin = BUTTON_MARGIN_DEFAULT;
    _cursorHeight = CursorHeightDefault;
    
    _backgroundImageView = [[UIImageView alloc] initWithFrame:self.bounds];
    [self addSubview:_backgroundImageView];
    
    _scrollView = [[UIScrollView alloc] init];
    _scrollView.showsHorizontalScrollIndicator = NO;
    _scrollView.showsVerticalScrollIndicator = NO;
    _scrollView.bounces = NO;
    [self addSubview:_scrollView];
    
    _cursor = [[UIView alloc] initWithFrame:CGRectZero];
    _cursor.backgroundColor = [UIColor redColor];
    _cursor.layer.cornerRadius = _cursorHeight / 2.0;
    [_scrollView addSubview:_cursor];
}

#pragma mark - 設定各個控制元件的位置
- (void)layoutSubviews {
    
    [super layoutSubviews];
    
    CGSize size = self.frame.size;
    _backgroundImageView.frame = CGRectMake(0, 0, size.width, size.height);;
    _scrollView.frame = CGRectMake(0, 0, size.width, size.height);
    if ([_arrayButtons count] == 0) {
        return;
    }
    
    // 增加按鈕兩側的間距
    CGFloat contentWidth = MARGIN_HORI * 2;
    for (int i=0; i<[_arrayButtons count]; i++) {
        UIButton *button = [_arrayButtons objectAtIndex:i];
        contentWidth += button.frame.size.width;
    }
    
    // 按鈕未排滿整屏寬度時
    if (!_isButtonAlignmentLeft && contentWidth < size.width) {
        CGFloat buttonWidth = floorf((size.width - MARGIN_HORI * 2) / [_arrayButtons count]);
        for (UIButton *button in _arrayButtons) {
            CGRect frame = button.frame;
            frame.size.width = buttonWidth;
            button.frame = frame;
        }
    }
    
    // 設定按鈕位置
    NSInteger selectedIndex = 0;
    CGFloat xOffset = MARGIN_HORI;
    CGFloat buttonHeight = size.height;
    for (int i=0; i<[_arrayButtons count]; i++) {
        UIButton *button = [_arrayButtons objectAtIndex:i];
        CGRect frame = button.frame;
        frame.origin.x = xOffset;
        frame.origin.y = 0;
        frame.size.height = buttonHeight;
        button.frame = frame;
        xOffset += frame.size.width;
        if (button.selected) {
            selectedIndex = i;
        }
    }
    
    // 設定分割線位置
    for (int i=0; i<[_arraySeperateLines count]; i++) {
        UIView *line = [_arraySeperateLines objectAtIndex:i];
        line.hidden = !_showSeperateLines;
        
        UIButton *buttonPrev = [_arrayButtons objectAtIndex:i];
        UIButton *buttonNext = [_arrayButtons objectAtIndex:i+1];
        
        CGRect frame = line.frame;
        frame.origin.x = (CGRectGetMaxX(buttonPrev.frame) + CGRectGetMinX(buttonNext.frame))/2;
        line.frame = frame;
        
    }
    
    _scrollView.contentSize = CGSizeMake(xOffset + MARGIN_HORI, size.height);
    
    // 設定遊標位置
    UIButton *buttonSelected = [_arrayButtons objectAtIndex:selectedIndex];
    CGRect frame = buttonSelected.frame;
    [buttonSelected.titleLabel sizeToFit];
    CGFloat cursorWidth = _cursorWidth != 0 ? _cursorWidth : buttonSelected.titleLabel.frame.size.width;
    _cursor.frame = CGRectMake(frame.origin.x + (frame.size.width - cursorWidth) / 2, CGRectGetMaxY(frame) - _cursorHeight, cursorWidth, _cursorHeight);
}

#pragma mark - 建立各個button

- (void)updateConentWithTitles:(NSArray *)titles {
    
    for (UIButton *button in _arrayButtons) {
        [button removeFromSuperview];
    }
    
    if ([titles count] == 0) {
        return;
    }
    
    NSMutableArray *tempArray = [NSMutableArray array];
    for (int i=0; i<[titles count]; i++) {
        NSString *title = [titles objectAtIndex:i];
        UIButton *button = [self createCustomButtonWithTitle:title];
        button.titleLabel.font = [UIFont systemFontOfSize:14];
        button.tag = i;
        [button sizeToFit];
        CGRect frame = button.frame;
        frame.size.width += _buttonMargin;
        button.frame = frame;
        [_scrollView addSubview:button];
        [tempArray addObject:button];
    }
    _arrayButtons = [NSArray arrayWithArray:tempArray];
    [_scrollView bringSubviewToFront:_cursor];
    [self setSelectedIndex:0];
    
    CGFloat lineTop = CGRectGetHeight(self.frame) / 5;
    CGFloat lineHeight = CGRectGetHeight(self.frame) - lineTop * 2;
    tempArray = [NSMutableArray array];
    for (int i=0; i<[_arrayButtons count]-1; i++) {
        UIView *line = [[UIView alloc] initWithFrame:CGRectMake(0, lineTop, 0.8, lineHeight)];
        line.backgroundColor = [UIColor redColor];
        [_scrollView addSubview:line];
        [tempArray addObject:line];
    }
    _arraySeperateLines = [NSArray arrayWithArray:tempArray];
}

- (UIButton *)createCustomButtonWithTitle:(NSString *)title {
    
    UIButton *button = [[QiTopBarButton alloc] init];
    [button setTitle:title forState:UIControlStateNormal];
    [button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
    return button;
}

/** 點選topbar的某一項 */
- (void)buttonClicked:(id)sender {
    
    UIButton *button = (UIButton *)sender;
    NSInteger tag = button.tag;
    
    if (button.selected) {
        if (_target && [_target respondsToSelector:@selector(topBarSelectIndicator:)]) {
            [_target topBarSelectIndicator:tag];
        }
        return;
    }

    [self setSelectedIndex:tag];
    if (_target && [_target respondsToSelector:@selector(topBarSelectIndex:)]) {
        [_target topBarSelectIndex:tag];
    }
}

#pragma mark 設定按鈕的位置
- (void)setIsButtonAlignmentLeft:(BOOL)isAlignmentLeft {
    
    _isButtonAlignmentLeft = isAlignmentLeft;
}

- (void)setShowSeperateLines:(BOOL)showSeperateLines {
    
    _showSeperateLines = showSeperateLines;
}

#pragma mark 更新和設定位置
- (void)setSelectedIndex:(NSInteger)index {
    
    if (index > [_arrayButtons count]) {
        return;
    }
    
    for (int i=0; i<[_arrayButtons count]; i++) {
        UIButton *button = [_arrayButtons objectAtIndex:i];
        button.selected = (i == index);
    }
    [self updateScrollViewPosition];
}

- (NSInteger)getSelectedIndex {
    
    NSInteger selectedIndex = 0;
    for (int i=0; i<[_arrayButtons count]; i++) {
        UIButton *button = [_arrayButtons objectAtIndex:i];
        if (button.selected) {
            selectedIndex = i;
        }
    }
    return selectedIndex;
}

- (void)scrollRectToCenter:(CGRect)frame {
    
    CGSize size = self.frame.size;
    CGSize contentSize = self.scrollView.contentSize;
    
    CGFloat targetX = frame.origin.x - (size.width - frame.size.width) / 2;
    CGFloat targetEndX = targetX + size.width;
    
    if (targetX < 0) {
        targetX = 0;
    }
    if (targetEndX > contentSize.width) {
        targetEndX = contentSize.width;
    }
    CGRect targetRect = CGRectMake(targetX, 0, targetEndX - targetX, frame.size.height);
    
    [self.scrollView scrollRectToVisible:targetRect animated:YES];
}

- (void)updateScrollViewPosition {
    
    CGSize size = self.frame.size;
    CGSize contentSize = self.scrollView.contentSize;
    if (size.width >= contentSize.width) {
        return;
    }
    
    CGRect frame = CGRectZero;
    for (int i=0; i<[_arrayButtons count]; i++) {
        UIButton *button = [_arrayButtons objectAtIndex:i];
        if (button.selected) {
            frame = button.frame;
        }
    }
    
    [self scrollRectToCenter:frame];
}

- (void)setCursorPosition:(CGFloat)percent {
    
    CGFloat indexOffset = percent * [_arrayButtons count];
    NSInteger preIndex = floorf(indexOffset);
    NSInteger nextIndex = ceilf(indexOffset);
    
    if (preIndex >= 0 && nextIndex <= [_arrayButtons count]) {
        UIButton *preBtn = [_arrayButtons objectAtIndex:preIndex];
        UIButton *nextBtn = [_arrayButtons objectAtIndex:nextIndex];
        
        CGFloat cursorWidth = _cursorWidth;
        if (_cursorWidth == 0) {
            cursorWidth = preBtn.titleLabel.frame.size.width + (indexOffset - preIndex) * (nextBtn.titleLabel.frame.size.width - preBtn.titleLabel.frame.size.width);
            CGRect frame = _cursor.frame;
            frame.size.width = cursorWidth;
            _cursor.frame = frame;
        }
        
        CGFloat cursorCenterX = preBtn.center.x + (indexOffset - preIndex) * (nextBtn.center.x - preBtn.center.x);
        _cursor.center = CGPointMake(cursorCenterX , _cursor.center.y);
    }
}

- (QiTopBarButton *)getCustomButtonAtIndex:(NSInteger)index {
    
    if (index >= 0 && index < [_arrayButtons count]) {
        QiTopBarButton *button = [_arrayButtons objectAtIndex:index];
        if ([button isKindOfClass:[QiTopBarButton class]]) {
            return button;
        }
    }
    return nil;
}

- (void)setBackgroundImage:(UIImage *)image {
    
    _backgroundImageView.image = image;
}

- (void)setBackgroundImageHidden:(BOOL)isHidden {
    
    _backgroundImageView.hidden = isHidden;
}

- (void)setCursorColor:(UIColor *)color {
    
    _cursor.backgroundColor = color;
}

- (void)setCursorWidth:(CGFloat)width {
    
    _cursorWidth = width;
}

- (void)setTextColor:(UIColor *)normalColor andSelectedColor:(UIColor *)selectedColor {
    
    for (QiTopBarButton *button in _arrayButtons) {
        button.colorNormal = normalColor;
        button.colorSelected = selectedColor;
        [button setTitleColor:normalColor forState:UIControlStateNormal];
        [button setTitleColor:selectedColor forState:UIControlStateSelected];
    }
}

@end

複製程式碼
3.3 PagesContainer

PagesContainer是最外層的View,使用者直接呼叫例項化PagesContainer並呼叫相應方法進行設定,即可使用該滑動選擇控制元件。PagesContainer將頂部topBar和下部scrollView封裝在內,topBar的點選事件與scrollView的滑動事件均傳遞到scrollViewDidScroll:方法中,再呼叫topBar的setCursorPosition:方法來設定遊標的位置。

//////// .h檔案
#import <UIKit/UIKit.h>

@protocol QiPagesContainerDelegate;
@class QiPagesContainerTopBar;

@interface QiPagesContainer : UIView

@property (nonatomic, weak) id<QiPagesContainerDelegate>delegate;

/**
 設定相鄰button之間的間距。間距是從該button的文字結束到下個button的文字開始之間的距離
 預設值是20
 */
- (void)setButtonMargin:(CGFloat)margin;

/**
 設定頂部的標題
 */
- (void)updateContentWithTitles:(NSArray *)titles;

/**
 設定頂部按鈕位置
 */
- (void)setIsButtonAlignmentLeft:(BOOL)isAlignmentLeft;

/*!
 設定是否顯示按鈕間分割線
 */
- (void)setShowSeperateLines:(BOOL)showSeperateLines;

/**
 設定底部的View,每個View會佔據該容器的大小
 */
- (void)updateContentWithViews:(NSArray *)views;

/**
 設定所應選擇的頁,不通知外部
 */
- (void)setDefaultSelectedPageIndex:(NSInteger)index;

/**
 設定所應選擇的頁
 */
- (void)setSelectedPageIndex:(NSInteger)index;

/**
 得到當前的頁面
 */
- (NSInteger)getCurrentPageIndex;

/**
 得到當前正在顯示的view
 */
- (UIView *)getCurrentPageView;

/**
 得到index對應的view
 */
- (UIView *)getPageViewWithIndex:(NSInteger)index;

/**
 獲取頂部的tabBar
 */
- (QiPagesContainerTopBar*)topBar;

/**
 設定按鈕選中和未選中的顏色
 */ 
- (void)setTextColor:(UIColor *)normalColor andSelectedColor:(UIColor *)selectedColor;

// 設定滑塊的顏色
- (void)setCursorColor:(UIColor *)color;
// 設定滑塊長度
- (void)setCursorWidth:(CGFloat)width;
// 設定滑塊長度
- (void)setCursorHeight:(CGFloat)height;

@end


@protocol QiPagesContainerDelegate <NSObject>

@optional
/** page切換 */
- (void)pageContainder:(QiPagesContainer *)container selectIndex:(NSInteger)index;

/** 點選當前的指示器 */
- (void)onClickPageIndicator:(QiPagesContainer *)container selectIndex:(NSInteger)index;

@end


//////// .m檔案
#import "QiPagesContainer.h"
#import "QiPagesContainerTopBar.h"
#import "QiAllowPanGestureScrollView.h"

#define TOPBAR_HEIGHT  34

@interface QiPagesContainer () <UIScrollViewDelegate, QiPagesContainerTopBarDelegate>

@property (nonatomic, strong) QiPagesContainerTopBar *topBar;
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIView *bottomLineView;

@property (nonatomic, strong) NSArray *arrayViews;
@property (nonatomic, assign) NSInteger currentPageIndex;

@end

@implementation QiPagesContainer

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

- (id)init {
    
    self = [super init];
    if (self) {
        [self setup];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self setup];
    }
    return self;
}

- (void)setup {
    
    _topBar = [[QiPagesContainerTopBar alloc] initWithFrame:CGRectZero];
    _topBar.target = self;
    [self addSubview:_topBar];

    _scrollView = [[QiAllowPanGestureScrollView alloc] initWithFrame:CGRectZero];
    _scrollView.delegate = self;
    _scrollView.pagingEnabled = YES;
    _scrollView.showsHorizontalScrollIndicator = NO;
    [_scrollView setScrollsToTop:NO];
    [_scrollView setAlwaysBounceHorizontal:NO];
    [_scrollView setAlwaysBounceVertical:NO];
    [_scrollView setBounces:NO];
    [self addSubview:self.scrollView];
    
    [self layoutSubviews];
}

- (void)layoutSubviews {
    
    [super layoutSubviews];
    
    CGSize size = self.frame.size;
    CGFloat scrollViewHeight = size.height - TOPBAR_HEIGHT;
    _topBar.frame = CGRectMake(0, 0, size.width, TOPBAR_HEIGHT);
    _scrollView.frame = CGRectMake(0, TOPBAR_HEIGHT, size.width, scrollViewHeight);
    
    
    for (int i=0; i<[_arrayViews count]; i++) {
        UIView *v = [_arrayViews objectAtIndex:i];
        v.frame = CGRectMake(i*size.width, 0, size.width, scrollViewHeight);
    }
    _scrollView.contentSize = CGSizeMake(size.width * [_arrayViews count], scrollViewHeight);
}

- (void)setButtonMargin:(CGFloat)margin {
    
    _topBar.buttonMargin = margin;
}

#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    
    if (scrollView.contentSize.width > 0) {
        [_topBar setCursorPosition:scrollView.contentOffset.x / scrollView.contentSize.width];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    
    NSInteger pageIndex = scrollView.contentOffset.x / scrollView.frame.size.width;
    if (pageIndex != _currentPageIndex) {
        _currentPageIndex = pageIndex;
        [_topBar setSelectedIndex:pageIndex];
        [self notifyDelegateSelectedIndex:pageIndex];
    }
}

#pragma mark - update content
- (void)setDefaultSelectedPageIndex:(NSInteger)index {
    
    if (index >= 0 && index <= [_arrayViews count] && index != _currentPageIndex) {
        [_topBar setSelectedIndex:index];
        _currentPageIndex = index;
        
        [_scrollView setContentOffset:CGPointMake(index * _scrollView.frame.size.width, 0) animated:YES];
    }
}

- (void)setSelectedPageIndex:(NSInteger)index {
    
    if (index >= 0 && index <= [_arrayViews count] && index != _currentPageIndex) {
        [_topBar setSelectedIndex:index];
        [self topBarSelectIndex:index];
    }
}

- (NSInteger)getCurrentPageIndex {
    
    return [_topBar getSelectedIndex];
}

- (UIView *)getCurrentPageView {
    
    return [_arrayViews objectAtIndex:[_topBar getSelectedIndex]];
}

- (UIView *)getPageViewWithIndex:(NSInteger)index {
    
    if (index<[_arrayViews count]) {
        return [_arrayViews objectAtIndex:index];
    } else {
        return nil;
    }
}

- (void)updateContentWithTitles:(NSArray *)titles {
    
    [_topBar updateConentWithTitles:titles];
}

- (void)setIsButtonAlignmentLeft:(BOOL)isAlignmentLeft {
    
    [_topBar setIsButtonAlignmentLeft:isAlignmentLeft];
}

- (void)setShowSeperateLines:(BOOL)showSeperateLines {
    
    [_topBar setShowSeperateLines:showSeperateLines];
}

- (void)updateContentWithViews:(NSArray *)views {
    
    for (UIView *view in _arrayViews) {
        [view removeFromSuperview];
    }
    if ([views count] == 0) {
        return;
    }
    _arrayViews = [NSArray arrayWithArray:views];
    
    for (int i=0; i<[views count]; i++) {
        UIView *view = [views objectAtIndex:i];
        [_scrollView addSubview:view];
    }
    [self layoutSubviews];
}

#pragma mark - QIPagesContainerTopBarDelegate
- (void)topBarSelectIndex:(NSInteger)index {
    
    if (index < [_arrayViews count]) {
        _currentPageIndex = index;

        [_scrollView setContentOffset:CGPointMake(index * _scrollView.frame.size.width, 0) animated:YES];
        [self notifyDelegateSelectedIndex:index];
    }
}
/** 重複點選topBar時會呼叫該方法 */
- (void)topBarSelectIndicator:(NSInteger)index {
    
    if (index < [_arrayViews count]) {
        _currentPageIndex = index;
        [_scrollView setContentOffset:CGPointMake(index * _scrollView.frame.size.width, 0) animated:YES];

        if (_delegate && [_delegate respondsToSelector:@selector(onClickPageIndicator:selectIndex:)]) {
            [_delegate onClickPageIndicator:self selectIndex:index];
        }

    }
}

- (void)notifyDelegateSelectedIndex:(NSInteger )index {
    
    if (_delegate && [_delegate respondsToSelector:@selector(pageContainder:selectIndex:)]) {
        [_delegate pageContainder:self selectIndex:index];
    }
}

#pragma mark - 設定按鈕選中和未選中的顏色
- (void)setTextColor:(UIColor *)normalColor andSelectedColor:(UIColor *)selectedColor {
    
    [_topBar setTextColor:normalColor andSelectedColor:selectedColor];
}

#pragma mark - 設定滑塊的顏色
- (void)setCursorColor:(UIColor *)color {
    
    [_topBar setCursorColor:color];
}

#pragma mark - 設定滑塊長度
- (void)setCursorWidth:(CGFloat)width {
    
    [_topBar setCursorWidth:width];
}

#pragma mark - 設定滑塊長度
- (void)setCursorHeight:(CGFloat)height {
    
    [_topBar setCursorHeight:height];
}

#pragma mark - 獲取頂部的tabBar 
- (QiPagesContainerTopBar*)topBar{
    return _topBar;
}

@end
複製程式碼
3.3 控制元件在專案中的呼叫

1)實現QiPagesContainerDelegate協議,監聽控制元件的選擇事件:

#pragma mark - IHPagesContainerDelegate

- (void)pageContainder:(QiPagesContainer *)container selectIndex:(NSInteger)index {
    
}


- (void)onClickPageIndicator:(QiPagesContainer *)container selectIndex:(NSInteger)index {
    
}
複製程式碼

2)初始化控制元件,並設定控制元件樣式:

- (void) setupViews {
    
    CGFloat margin = 0;
    CGSize size = self.view.frame.size;
    
    NSArray *titles = [NSArray arrayWithObjects:@"我的裝置", @"臥室", @"廚房廚房廚房廚房", @"門廳", nil];
    NSMutableArray *tempTableViews = [NSMutableArray array];
    _pageContainer = [[QiPagesContainer alloc] initWithFrame:CGRectMake(margin, 0, size.width - margin * 2, size.height)];
    _pageContainer.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [_pageContainer setBackgroundColor:[UIColor lightGrayColor]];
    _pageContainer.delegate = self;
    [_pageContainer updateContentWithTitles:titles];
    [_pageContainer setIsButtonAlignmentLeft:YES];
    [_pageContainer setCursorHeight:3.0];
    [_pageContainer setCursorColor:[UIColor whiteColor]];
    [_pageContainer setTextColor:[UIColor whiteColor] andSelectedColor:[UIColor whiteColor]];
    
    UIView *topBar = (UIView *)[_pageContainer topBar];
    for (int i=0; i<[titles count]; i++) {
        UIView *subView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height - topBar.frame.size.height)];
        subView.backgroundColor = [UIColor colorWithRed:arc4random()%255/255.0 green:arc4random()%255/255.0 blue:arc4random()%255/255.0 alpha:0.3];
        [tempTableViews addObject:subView];
    }
    [_pageContainer updateContentWithViews:tempTableViews];
    
    [self.view addSubview:_pageContainer];
}
複製程式碼

自定義控制元件展示:

自定義滑動選擇控制元件

4. 關於cursor大小及位置設定的說明

cursor被封裝在PagesContainerTopBar內,設定cursor的位置及大小的方法是setCursorPosition:,在設定之前遊標之前,我們可以獲取到的資訊:

  • setCursorPosition:位於PagesContainer中的scrollViewDidScroll:方法之中,即PagesContainer中的scrollView滾動時就會呼叫這個設定遊標的方法,傳入的引數是一個百分比scrollView.contentOffset.x / scrollView.contentSize.width;
  • 頂部的PagesContainerTopBar點選選中事件、下部的scrollView滾動事件,均會走scrollViewDidScroll:回撥方法

我們要滿足的需求:

  • 在設定cursor的位置時,遊標的寬度大小根據位於遊標之前按鈕(preBtn)寬度和之後按鈕(nextBtn)寬度動態變化,即靠近preBtn時cursor的寬就越接近preBtn的寬,靠近nextBtn時就越接近nextBtn的寬。

實現思路:

1)獲取當前索引的偏移量(float型),並計算並找出preBtn和nextBtn:

CGFloat indexOffset = percent * [_arrayButtons count];
NSInteger preIndex = floorf(indexOffset);
NSInteger nextIndex = ceilf(indexOffset);

UIButton *preBtn = [_arrayButtons objectAtIndex:preIndex];
UIButton *nextBtn = [_arrayButtons objectAtIndex:nextIndex];
複製程式碼

2)ursorWidth的最終取值是與選定按鈕的title等寬,ursorWidth的值***始終以preBtn為標準***,並根據當前滑動偏移量indexOffset-preIndex與按鈕的title長度只差的乘積進行變化:

// 如果cursor的長度是按比例變化的
ursorWidth = preBtn.titleLabel.frame.size.width + (indexOffset - preIndex) * (nextBtn.titleLabel.frame.size.width - preBtn.titleLabel.frame.size.width);
複製程式碼

3)控制元件底部的scrollView滑動時會不斷觸發scrollViewDidScroll:,便也會多次呼叫setCursorPosition:方法傳入位置,cursorWidth實時變化並更新至cursor,並設定cursor的確切位置,這樣就可以看到順暢的cursor位置及寬度變化:

CGRect frame = _cursor.frame;
frame.size.width = cursorWidth;
 _cursor.frame = frame;
        
CGFloat cursorCenterX = preBtn.center.x + (indexOffset - preIndex) * (nextBtn.center.x - preBtn.center.x);
_cursor.center = CGPointMake(cursorCenterX , _cursor.center.y);
複製程式碼

工程原始碼GitHub地址


小編微信:可加並拉入《QiShare技術交流群》。

iOS 一個滑動選擇控制元件

關注我們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公眾號)

推薦文章:
iOS 常用除錯方法:LLDB命令
iOS 常用除錯方法:斷點
iOS 常用除錯方法:靜態分析
iOS訊息轉發
iOS 自定義拖拽式控制元件:QiDragView
iOS 自定義卡片式控制元件:QiCardView
iOS Wireshark抓包
iOS Charles抓包
奇舞週刊

相關文章