前言
最近公司一個電商應(yīng)用要實現(xiàn)一個類似淘寶淘搶購頁面邏輯的功能森枪,起初本來想找個第三方的組件,后面發(fā)現(xiàn)網(wǎng)上并沒有類似的實現(xiàn)审孽。所以后面決定自己封裝一個县袱,效果如下所示:
功能特點
- 實現(xiàn)了菜單切換的視覺差,效果棒棒噠佑力;
- 使用簡單式散,創(chuàng)建一個控制器直接繼承
GFPageViewController
,設(shè)置需要添加的子控制器打颤、標(biāo)題暴拄、副標(biāo)題就搞定漓滔; - 菜單大部分的樣式都可進(jìn)行自定義;
- 菜單遮罩的顏色乖篷、大小和箭頭的大小也可以設(shè)置參數(shù)來控制响驴;
- 菜單實現(xiàn)了防止用戶連續(xù)點擊功能;
- 支持pod導(dǎo)入.
組件導(dǎo)入
組件支持直接將組件文件夾拖入工程和使用Pods管理兩種方式導(dǎo)入:
1撕蔼、直接將組件文件夾拖入工程方式
把GFPageControler文件夾拖到工程中豁鲤,選擇copy
2、Pods導(dǎo)入方式
pod 'GFPageController'
組件使用
1鲸沮、基本使用方式
創(chuàng)建一個控制器繼承自GFPageViewController
琳骡,創(chuàng)建完之后給控制器設(shè)置需要添加的子控制器(Array
)、標(biāo)題(Array
)讼溺、副標(biāo)題(Array
):
#import <UIKit/UIKit.h>
#import "GFPageViewController.h"
@interface PageViewController : GFPageViewController
@end
@implementation PageViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self configureContentView];
}
- (void)configureContentView {
NSArray *titles = @[@"8:00",@"10:00",@"12:00",@"14:00",@"16:00",@"18:00",@"20:00"];
NSArray *subTitles = @[@"已結(jié)束",@"已結(jié)束",@"已結(jié)束",@"瘋搶中",@"即將開始",@"即將開始",@"即將開始"];
NSMutableArray *controllers = [NSMutableArray new];
for (int i = 0; i < 7; i ++) {
UIViewController *vc = [[UIViewController alloc] init];
vc.view.backgroundColor = [UIColor colorWithRed:arc4random_uniform(255)/255.0 green:arc4random_uniform(255)/255.0 blue:arc4random_uniform(255)/255.0 alpha:1.0f];
[controllers addObject:vc];
}
// 設(shè)置控制器數(shù)組
self.controllers = controllers;
// 設(shè)置標(biāo)題數(shù)組
self.titles = titles;
// 設(shè)置副標(biāo)題數(shù)組
self.subTitles = subTitles;
// 設(shè)置初始下標(biāo)
self.selectIndex = 1;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
@end
OK楣号,搞定,運行就可以看到效果怒坯,是不是很簡單竖席。
2、自定義菜單樣式
可以看到上面沒有一行設(shè)置菜單樣式的代碼敬肚,那是因為不設(shè)置菜單使用的是默認(rèn)的樣式,除此之外束析,菜單的樣式還是可以自定義的艳馒, GFPageController
為大家提供了下面14個參數(shù)來控制菜單的樣式顯示:
/** MenuItem 的寬度 */
@property (nonatomic, assign) CGFloat itemWidth;
/** Menu 的高度 */
@property (nonatomic, assign) CGFloat menuHeight;
/** Menu 背景顏色 */
@property (nonatomic, strong) UIColor *menuBackgroundColor;
/** Menu mask的填充顏色 */
@property (nonatomic, strong) UIColor *maskFillColor;
/** Menu mask三角形的寬度 */
@property (nonatomic, assign) CGFloat triangleWidth;
/** Menu mask三角形的高度 */
@property (nonatomic, assign) CGFloat triangleHeight;
/** 標(biāo)題未選中時的顏色 */
@property (nonatomic, strong) UIColor *normalTitleColor;
/** 標(biāo)題選中時的顏色 */
@property (nonatomic, strong) UIColor *selectedTitleColor;
/** 標(biāo)題文字字體 */
@property (nonatomic, strong) UIFont *titleTextFont;
/** 標(biāo)題文字高度 */
@property (nonatomic, assign) CGFloat titleTextHeight;
/** 副標(biāo)題未選中時的顏色 */
@property (nonatomic, strong) UIColor *normalSubTitleColor;
/** 副標(biāo)題選中時的顏色 */
@property (nonatomic, strong) UIColor *selectedSubTitleColor;
/** 副標(biāo)題文字字體 */
@property (nonatomic, strong) UIFont *subTitleTextFont;
/** 副標(biāo)題文字高度 */
@property (nonatomic, assign) CGFloat subTitleTextHeight;
大家可以自行嘗試!
組件講解
1员寇、菜單視覺差實現(xiàn)
效果:
開始看淘寶里面的淘搶購頁面時弄慰,發(fā)現(xiàn)了一個細(xì)節(jié),如下:
可以發(fā)現(xiàn)蝶锋,只要滾動到了中間紅色那塊區(qū)域的文字陆爽,顏色都會變成白色。扳缕。慌闭。
腦洞了很久也沒有想到思路!后來網(wǎng)上查找躯舔,從一篇文章中得到了靈感 視錯覺結(jié)合UI驴剔。
原理:
原理其實很簡單:就是弄兩個視圖,內(nèi)容和位置一樣粥庄,只是他們的文字顏色不一樣而已丧失!
實現(xiàn):
知道了原理,那就開始構(gòu)思:
1惜互、我的實現(xiàn)思路是用UICollectionView
來實現(xiàn)滾動菜單布讹;
2琳拭、需要兩個UICollectionView
,UICollectionViewCell
的文字內(nèi)容一樣描验,文字顏色區(qū)分白嘁;
#pragma mark - 創(chuàng)建兩個UICollectionView
// collectionViewTop
- (UICollectionView *)collectionViewTop {
if (!_collectionViewTop) {
_collectionViewTop = [[UICollectionView alloc] initWithFrame:CGRectMake(-self.collectionViewEdge, 0, self.bounds.size.width, self.itemHeight) collectionViewLayout:self.flowLayout];
[_collectionViewTop registerClass:[GFMenuItem class] forCellWithReuseIdentifier:GFMENUITEM_NIBNAME];
_collectionViewTop.tag = TOP_COLLECTIONVIEW_TAG;
_collectionViewTop.backgroundColor = [UIColor clearColor];
_collectionViewTop.showsHorizontalScrollIndicator = NO;
_collectionViewTop.decelerationRate = 0;//設(shè)置手指放開后的減速率(值域 0~1 值越小減速停止的時間越短),默認(rèn)為1
_collectionViewTop.delegate = self;
_collectionViewTop.dataSource = self;
}
return _collectionViewTop;
}
// collectionViewBottom
- (UICollectionView *)collectionViewBottom {
if (!_collectionViewBottom) {
_collectionViewBottom = [[UICollectionView alloc] initWithFrame:self.bounds collectionViewLayout:self.flowLayout];
[_collectionViewBottom registerClass:[GFMenuItem class] forCellWithReuseIdentifier:GFMENUITEM_NIBNAME];
_collectionViewBottom.tag = BOTTOM_COLLECTIONVIEW_TAG;
_collectionViewBottom.backgroundColor = [UIColor clearColor];
_collectionViewBottom.showsHorizontalScrollIndicator = NO;
_collectionViewBottom.decelerationRate = 0;//設(shè)置手指放開后的減速率(值域 0~1 值越小減速停止的時間越短),默認(rèn)為1
_collectionViewBottom.delegate = self;
_collectionViewBottom.dataSource = self;
}
return _collectionViewBottom;
}
// flowLayout
- (UICollectionViewFlowLayout *)flowLayout {
if (!_flowLayout) {
_flowLayout = [[UICollectionViewFlowLayout alloc] init];
_flowLayout.itemSize = CGSizeMake(self.itemWidth, self.itemHeight);
_flowLayout.sectionInset = UIEdgeInsetsMake(0, self.collectionViewEdge, 0, self.collectionViewEdge);
_flowLayout.minimumLineSpacing = 0;
_flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
}
return _flowLayout;
}
#pragma mark - UICollectionViewDataSource
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
GFMenuItem *cell = [collectionView dequeueReusableCellWithReuseIdentifier:GFMENUITEM_NIBNAME forIndexPath:indexPath];
NSString *title = self.titles[indexPath.row];
NSString *subTitle = self.subTitles[indexPath.row];
// 區(qū)分顏色
if (collectionView.tag == BOTTOM_COLLECTIONVIEW_TAG) {
cell.titleColor = self.normalTitleColor;
cell.subTitleColor = self.normalSubTitleColor;
} else {
cell.titleColor = self.selectedTitleColor;
cell.subTitleColor = self.selectedSubTitleColor;
}
cell.titleText = title;
cell.subTitleText = subTitle;
return cell;
}
3、兩個UICollectionView
的滾動需要同步挠乳;
#pragma makr - 同步滾動
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
UICollectionView *collectionView = (UICollectionView *)scrollView;
//同步兩個collectionView的滾動
if (collectionView.tag == BOTTOM_COLLECTIONVIEW_TAG) {
[_collectionViewTop setContentOffset:collectionView.contentOffset];
} else {
[_collectionViewBottom setContentOffset:collectionView.contentOffset];
}
}
3权薯、需要一個遮罩,一個UICollectionView
在遮罩下面睡扬,一個在遮罩上面盟蚣;
[self addSubview:self.collectionViewBottom];
[self addSubview:self.maskView];
[self.maskView addSubview:self.collectionViewTop];
4、在遮罩上面的UICollectionView
超出遮罩的部分的內(nèi)容不顯示出來卖怜;
self.maskView.clipsToBounds = YES;
2屎开、使用UIBezierPath繪制遮罩
大家會發(fā)現(xiàn)這個遮罩是多邊形的。
起初我的想法是用兩種圖片拼接起來马靠,一張長方形奄抽,一張三角形,后來為了自定義性更高一點甩鳄,改成了用UIBezierPath
來進(jìn)行繪制逞度,代碼如下:
自定義一個View
繼承自UIView
:
#import "GFMaskView.h"
@implementation GFMaskView
- (void)drawRect:(CGRect)rect {
UIBezierPath *path = [UIBezierPath bezierPath];
// 畫矩形
path = [self drawReact:CGRectMake(0, 0, rect.size.width, rect.size.height - self.triangleHeight) fillColor:self.fillColor];
// 畫三角形
CGPoint pointOne = CGPointMake(rect.size.width/2 - self.triangleWidth/2, rect.size.height - self.triangleHeight);
CGPoint pointTwo = CGPointMake(rect.size.width/2, rect.size.height);
CGPoint pointThree = CGPointMake(rect.size.width/2 + self.triangleWidth/2, rect.size.height - self.triangleHeight);
path = [self drawTrianglePointOne:pointOne pointTwo:pointTwo pointThree:pointThree fillColor:self.fillColor];
}
// 畫矩形
- (UIBezierPath *)drawReact:(CGRect)rect fillColor:(UIColor *)fillColor {
UIBezierPath *rectPath = [UIBezierPath bezierPathWithRect:rect];
[fillColor setFill];
[rectPath fill];
return rectPath;
}
// 畫三角形
- (UIBezierPath *)drawTrianglePointOne:(CGPoint)pointOne pointTwo:(CGPoint)pointTwo pointThree:(CGPoint)pointThree fillColor:(UIColor *)fillColor {
UIBezierPath *trianglePath = [UIBezierPath bezierPath];
// 起點
[trianglePath moveToPoint:pointOne];
// draw the lines
[trianglePath addLineToPoint:pointTwo];
[trianglePath addLineToPoint:pointThree];
[trianglePath closePath];
[fillColor set];
[trianglePath fill];
return trianglePath;
}
3、GFPageViewController
到這里滾動菜單的實現(xiàn)就完成了妙啃。我的初衷其實就是把這個滾動菜單封裝出來档泽,后來發(fā)現(xiàn)使用這個菜單的大部分情況都是和多個子控制器一起使用,所以就再進(jìn)行了一步封裝揖赴,把控制器的邏輯都封裝到了GFPageViewController
控制器中馆匿。
這樣使用起來就很方便,直接創(chuàng)建一個控制器繼承GFPageViewController
燥滑,再給他設(shè)置需要添加的子控制器渐北、標(biāo)題和副標(biāo)題就OK了。
GFPageViewController
的實現(xiàn)主要是讓菜單和添加的子控制器能夠聯(lián)動铭拧,核心代碼如下:
// 添加視圖
- (void)setupContentView {
[self.view addSubview:self.scrollView];
[self.view addSubview:self.gfSegmentedControl];
}
// 滾動到指定下標(biāo)的控制器
- (void)scrollControllerAtIndex:(int)index {
CGFloat offsetX = index * GF_SCREEN_WIDTH;
CGPoint offset = CGPointMake(offsetX, 0);
if (fabs(self.scrollView.contentOffset.x - offset.x) > GF_SCREEN_WIDTH) {
[self.scrollView setContentOffset:offset animated:NO];
// 獲得索引
int index = (int)self.scrollView.contentOffset.x / self.scrollView.frame.size.width;
[self addChildViewAtIndex:index];
} else {
[self.scrollView setContentOffset:offset animated:YES];
}
}
// 添加子控制器的View到ScrollView上
- (void)addChildViewAtIndex:(int)index {
// 設(shè)置選中的下標(biāo)
self.menuView.selectIndex = index;
UIViewController *vc = self.childViewControllers[index];
vc.view.frame = self.scrollView.bounds;
[self.scrollView addSubview:vc.view];
}
#pragma mark - UIScrollViewDelegate
// 滾動動畫結(jié)束后調(diào)用(代碼導(dǎo)致)
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView*)scrollView {
// 添加控制器
if (self.controllers) {
// 獲得索引
int index = (int)self.scrollView.contentOffset.x / self.scrollView.frame.size.width;
[self addChildViewAtIndex:index];
}
}
// 滾動結(jié)束(手勢導(dǎo)致)
- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView {
[self scrollViewDidEndScrollingAnimation:scrollView];
}
#pragma mark - getter
- (UIScrollView *)scrollView {
if (!_scrollView) {
CGFloat scrollViewY = _menuHeight;
if (self.navigationController && !self.navigationController.navigationBar.hidden) {
scrollViewY = _menuHeight + 64;
}
_scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, scrollViewY, GF_SCREEN_WIDTH, GF_SCREEN_HEIGHT - _menuHeight)];
_scrollView.contentSize = CGSizeMake(_controllers.count * GF_SCREEN_WIDTH, 0);
_scrollView.pagingEnabled = YES;
_scrollView.bounces = NO;
_scrollView.showsHorizontalScrollIndicator = NO;
_scrollView.delegate = self;
}
return _scrollView;
}
- (GFMenuView *)gfSegmentedControl {
if (!_menuView) {
CGFloat segmentedControlY = 0;
if (self.navigationController && !self.navigationController.navigationBar.hidden) {
segmentedControlY = 64;
}
_menuView = [GFMenuView gfMenuViewWithFrame:CGRectMake(0, segmentedControlY, GF_SCREEN_WIDTH, _menuHeight) titles:_titles subTitles:_subTitles];
gfWeakSelf(weakSelf);
_menuView.clickIndexBlock = ^(int clickIndex) {
[weakSelf scrollControllerAtIndex:clickIndex];
};
}
return _menuView;
}
#pragma mark - setter
// set dataSource
- (void)setControllers:(NSArray<UIViewController *> *)controllers {
_controllers = [controllers copy];
self.scrollView.contentSize = CGSizeMake(_controllers.count * GF_SCREEN_WIDTH, 0);
// 添加子控制器
for (UIViewController *vc in controllers) {
[self addChildViewController:vc];
[vc didMoveToParentViewController:self];
}
if (self.selectIndex != 0) {
// 添加指定下標(biāo)控制器
[self addChildViewAtIndex:self.selectIndex];
} else {
// 默認(rèn)添加第一個控制器
[self addChildViewAtIndex:0];
}
}
- (void)setTitles:(NSArray<NSString *> *)titles {
_titles = [titles copy];
self.gfSegmentedControl.titles = titles;
}
- (void)setSubTitles:(NSArray<NSString *> *)subTitles {
_subTitles = [subTitles copy];
self.gfSegmentedControl.subTitles = subTitles;
}
結(jié)語
哈哈赃蛛,這也算是自己第一次封裝一個完整易用的組件,從中學(xué)習(xí)到了不少東西搀菩。
其中比如自定義View
的正確姿勢焊虏;UIScrollView
中一些代理使用的細(xì)節(jié)問題;讓自己的組件支持Pods等秕磷。這些問題我會單獨抽出來進(jìn)行總結(jié)诵闭,
最后大家對這個組件遇到的問題可以在簡書wythetan上或者git上gaofengtan給我留言。
最后貼上項目的git地址GFPageController