iOS仿淘宝类电商秒杀的分页控件
前言
最近公司一个电商应用要实现一个类似淘宝淘抢购页面逻辑的功能,起初本来想找个第三方的组件,后面发现网上并没有类似的实现。所以后面决定自己封装一个,效果如下所示:
演示.gif功能特点
- 实现了菜单切换的视觉差,效果棒棒哒;
- 使用简单,创建一个控制器直接继承
GFPageViewController
,设置需要添加的子控制器、标题、副标题就搞定; - 菜单大部分的样式都可进行自定义;
- 菜单遮罩的颜色、大小和箭头的大小也可以设置参数来控制;
- 菜单实现了防止用户连续点击功能;
- 支持pod导入.
组件导入
组件支持直接将组件文件夹拖入工程和使用Pods管理两种方式导入:
1、直接将组件文件夹拖入工程方式
把GFPageControler文件夹拖到工程中,选择copy
组件相关文件夹.png
2、Pods导入方式
pod 'GFPageController'
组件使用
1、基本使用方式
创建一个控制器继承自GFPageViewController
,创建完之后给控制器设置需要添加的子控制器(Array
)、标题(Array
)、副标题(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 = @[@"已结束",@"已结束",@"已结束",@"疯抢中",@"即将开始",@"即将开始",@"即将开始"];
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];
}
// 设置控制器数组
self.controllers = controllers;
// 设置标题数组
self.titles = titles;
// 设置副标题数组
self.subTitles = subTitles;
// 设置初始下标
self.selectIndex = 1;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
@end
OK,搞定,运行就可以看到效果,是不是很简单。
2、自定义菜单样式
可以看到上面没有一行设置菜单样式的代码,那是因为不设置菜单使用的是默认的样式,除此之外,菜单的样式还是可以自定义的, GFPageController
为大家提供了下面14个参数来控制菜单的样式显示:
/** 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;
/** 标题未选中时的颜色 */
@property (nonatomic, strong) UIColor *normalTitleColor;
/** 标题选中时的颜色 */
@property (nonatomic, strong) UIColor *selectedTitleColor;
/** 标题文字字体 */
@property (nonatomic, strong) UIFont *titleTextFont;
/** 标题文字高度 */
@property (nonatomic, assign) CGFloat titleTextHeight;
/** 副标题未选中时的颜色 */
@property (nonatomic, strong) UIColor *normalSubTitleColor;
/** 副标题选中时的颜色 */
@property (nonatomic, strong) UIColor *selectedSubTitleColor;
/** 副标题文字字体 */
@property (nonatomic, strong) UIFont *subTitleTextFont;
/** 副标题文字高度 */
@property (nonatomic, assign) CGFloat subTitleTextHeight;
大家可以自行尝试!
组件讲解
1、菜单视觉差实现
效果:
开始看淘宝里面的淘抢购页面时,发现了一个细节,如下:
可以发现,只要滚动到了中间红色那块区域的文字,颜色都会变成白色。。。
脑洞了很久也没有想到思路!后来网上查找,从一篇文章中得到了灵感 视错觉结合UI。
原理:
原理其实很简单:就是弄两个视图,内容和位置一样,只是他们的文字颜色不一样而已!
实现:
知道了原理,那就开始构思:
1、我的实现思路是用UICollectionView
来实现滚动菜单;
2、需要两个UICollectionView
,UICollectionViewCell
的文字内容一样,文字颜色区分;
#pragma mark - 创建两个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;//设置手指放开后的减速率(值域 0~1 值越小减速停止的时间越短),默认为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;//设置手指放开后的减速率(值域 0~1 值越小减速停止的时间越短),默认为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];
// 区分颜色
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
超出遮罩的部分的内容不显示出来;
self.maskView.clipsToBounds = YES;
2、使用UIBezierPath绘制遮罩
大家会发现这个遮罩是多边形的。
起初我的想法是用两种图片拼接起来,一张长方形,一张三角形,后来为了自定义性更高一点,改成了用UIBezierPath
来进行绘制,代码如下:
自定义一个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
到这里滚动菜单的实现就完成了。我的初衷其实就是把这个滚动菜单封装出来,后来发现使用这个菜单的大部分情况都是和多个子控制器一起使用,所以就再进行了一步封装,把控制器的逻辑都封装到了GFPageViewController
控制器中。
这样使用起来就很方便,直接创建一个控制器继承GFPageViewController
,再给他设置需要添加的子控制器、标题和副标题就OK了。
GFPageViewController
的实现主要是让菜单和添加的子控制器能够联动,核心代码如下:
// 添加视图
- (void)setupContentView {
[self.view addSubview:self.scrollView];
[self.view addSubview:self.gfSegmentedControl];
}
// 滚动到指定下标的控制器
- (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 {
// 设置选中的下标
self.menuView.selectIndex = index;
UIViewController *vc = self.childViewControllers[index];
vc.view.frame = self.scrollView.bounds;
[self.scrollView addSubview:vc.view];
}
#pragma mark - UIScrollViewDelegate
// 滚动动画结束后调用(代码导致)
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView*)scrollView {
// 添加控制器
if (self.controllers) {
// 获得索引
int index = (int)self.scrollView.contentOffset.x / self.scrollView.frame.size.width;
[self addChildViewAtIndex:index];
}
}
// 滚动结束(手势导致)
- (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) {
// 添加指定下标控制器
[self addChildViewAtIndex:self.selectIndex];
} else {
// 默认添加第一个控制器
[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;
}
结语
哈哈,这也算是自己第一次封装一个完整易用的组件,从中学习到了不少东西。
其中比如自定义View
的正确姿势;UIScrollView
中一些代理使用的细节问题;让自己的组件支持Pods等。这些问题我会单独抽出来进行总结,
最后大家对这个组件遇到的问题可以在简书wythetan上或者git上gaofengtan给我留言。
最后贴上项目的git地址GFPageController