基础知识
轮播广告(Banner)是一个常见的需求,以前一般就出现在最顶部,现在也可能出现在中间。所以需要放在一个表格的cell中。无论出现在顶部还是出现在中间,或者是放在cell中,还是放在其他view或者控制其中,把它单独做成一个view是最灵活的。view是最方便复用的一种形式。如果说界面层要考虑复用,那么通过代码实现自定义的view,作为组件形式(不要代入不必要的逻辑和数据获取,View仅仅考虑怎么显示)还是非常可取的。如果再配上一个ViewModel作为对外的数据接口,那么将更灵活。文件多出来,怎么取舍,根据实际权衡。
一般都用UIScrollView实现,以前是计算frame,现在是引入第三方库Masonry,用AutoLayout通过加限制实现。
UIScrollView能够滑动本质contentSize比frame大,就会滑动,否则就不用滑动。所以UIScrollView加4个限制确定本身frame之外,还要再加width和height两个条件,用来确定contentSize。绝对布局时,算出一个size给它,AutoLayout时,一般是引入一个辅助的containerView,通过子view的大小,反向来决定父view的大小。这两者的思路是相反的,这点要理解一下。另外,还要注意一点的是,在确定contentSize时,这个辅助的containerView不能当做UIScrollView的子view看待,(在确定frame的时候确实是父子关系),这种情况下应该把containerView和UIScrollView看成是同级别的兄弟view。
如何理解contentSize和frame:contentSize就是平铺所有内容的全部范围,不要被手机屏幕大小限制住。而frame相当于观察的窗口,滑动就是这个窗口在移动。contentOffset就是用来描述这种滑动的,包括x、y两个变量,跟iOS通用坐标系一直,向右,向下变大。Banner一般向左滑动,只要考虑x方向,y方向不用操心。
一般banner上会有指示当前页面的指示器,这个叫UIPageControl。这个控件比较简单。不过要理解的是,(1)UIScrollView的pagingEnabled属性要设为YES,默认是NO,这样滑动不满一半会退回来,超过一半就会自动往前走,这样就有一页一页翻的感觉。(2)小点移动的是currentPage这个属性来决定的,就是这个属性对应的小圆点的颜色跟其他的不一样,让感觉是小点在移动。不过这个属性值,往往就是用UIScrollView的contentOffset和frame做简单除法,取整数部分而得到的。(3)UIPageControl自定义的空间比较小,一般市面上能看到的选中点是长方形的那种,一般都是用自定义View重新实现了这个控件。一般是用贝塞尔曲线重新画,让其行为看起来像UIPageControl而已。
自动轮播的效果,一般是加一个定时器,隔固定时间,设置UIScrollView的contentOffset和UIPageControl的currentPage。还要考虑的事一旦检测有手势左右横扫操作,这是人在手动滑动,要把计时器停止。这个功能实现稍微有点麻烦,需要考虑的地方也有点多。
如何响应点击事件:方法1是将一个UIImageView和UIButton放在一个view容器中,UIImageView负责显示图片,UIButton负责响应跳转。方法2是在UIImageView上加一个tap手势,来负责点击的响应。
基本思路
从UIView继承一个自定义的BannerView,作为容器,也作为使用的整体,通用做法
对外的接口是一更新函数,参数是一个数组,数组成员是一个自定义的结构,包含图片url和跳转url。
内部提供三个固定的本地图片和三个固定的跳转url,后台没有配置也能用。这个只要使用上面那个对外接口就可以了,只是本地资源图片加载方式不一样,需要增加一个变量,指明是否来字本地。
UIScrollView的高度固定,宽度和屏幕一样宽,平铺的格式。要留白,加一个UIEdgeInset的padding就可以了。这样就是横向滑动了。
加一个辅助的containerView,是UIScrollView的子view,4边和UIScrollView完全重合。在确定UIScrollView的contentSize的时候,containerView和UIScrollView是兄弟view的关系,不是父子关系。containerView的高度和UIScrollView的高度一致。containerView的宽度由里面包含的子ImageView来决定。每个ImageView的高度和containerView一致,宽度和UIScrollView的宽度一致,每个挨个相连。最后一个ImageView的右边界和containerView的右边界一致。这样,这些子ImageView就确定了containerView的宽度。而containerView的高度和宽度,就决定了UIScrollView的contentSize。这点是用UIScrollView实现Banner中用AutoLayout来做最难理解的一点。
加一个UIPagecontrol,他直接加在最外面的BannerView上,x方向居中,宽度由内容决定。有自己的高度,估计有40点左右。根据点的大小,可以加一个10点左右的高度限制,这样就平整了。离底部设一个偏移量,比如10点左右,这样做位置设定就相对灵活一点。UIPagecontrol和UIScrollView平级比较好,两者本来也没什么关系。用系统自带的,自定义没有必要,重新设计得并不见得好看。
自动轮播功能不添加,像广告,不好。引入计时器,就要考虑run mode和多线程的事情,麻烦。自己动,让人烦,一看就知道广告。不动,又有小点提示,反而能吸引人去操作。
代码示例
BannerViewModel.h
@interface BannerViewModel : NSObject
@property (nonatomic, strong) NSString *imageUrl;
@property (nonatomic, strong) NSString *redirctionUrl;
@property (nonatomic, assign) BOOL isLocalImage; // 这里为YES时,imageUrl放图片的文件名,使用[UIImage ImagenNamed:]加载图片
@end
BannerView.h
#import <UIKit/UIKit.h>
#import "BannerViewModel.h"
@interface BannerView : UIView
- (void)updateWithViewModelArray:(NSArray<BannerViewModel *> *)viewModelArray;
@end
BannerView.m
#import "BannerView.h"
#define kScrollViewEdgeInsetTop 0
#define kScrollViewEdgeInsetLeft 0
#define kScrollViewEdgeInsetBottom 0
#define kScrollViewEdgeInsetRight 0
#define kPageControlSpaceBottom 10
#define kPageControlHegith 10
#define kPlaceHoldImageFileName @"place_hold"
#define kTagBase 100
@interface BannerView () <UIScrollViewDelegate>
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIView *containerView;
@property (nonatomic, strong) UIPageControl *pageControl;
@property (nonatomic, strong) NSArray <BannerViewModel *> *viewModelArray;
@end
@implementation BannerView
#pragma mark - interface
- (void)updateWithViewModelArray:(NSArray<BannerViewModel *> *)viewModelArray {
if (nil == viewModelArray) {
return;
}
if (viewModelArray.count < 1) {
return;
}
self.viewModelArray = viewModelArray;
for (UIView *view in self.containerView.subviews) {
// 这里是否可以将对应的限制删除?在Masonry没找到相应的接口
[view removeFromSuperview];
}
BannerViewModel *viewModel = nil;
UIImageView *lastView = nil;
for (NSInteger i = 0; i < viewModelArray.count; i++) {
viewModel = viewModelArray[i];
UIImageView *subView = [[UIImageView alloc] init];
subView.contentMode = UIViewContentModeScaleAspectFill;
subView.clipsToBounds = YES; // 防止图片越界显示
subView.tag = kTagBase + i;
subView.userInteractionEnabled = YES; // 让ImageView可以响应事件
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageTouched:)];
[subView addGestureRecognizer:tap];
if (viewModel.isLocalImage) {
subView.image = [UIImage imageNamed:viewModel.imageUrl];
} else {
[subView sd_setImageWithPreviousCachedImageWithURL:[NSURL URLWithString:viewModel.imageUrl] andPlaceholderImage:[UIImage imageNamed:kPlaceHoldImageFileName] options:SDWebImageRetryFailed progress:nil completed:nil];
}
[self.containerView addSubview:subView];
[subView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.mas_equalTo(0); // 上下紧挨着,高度都一样
make.width.mas_equalTo(self.scrollView.mas_width); // 宽度都一样,都是“一屏”
if (lastView) {
make.left.mas_equalTo(lastView.mas_right); // 紧挨着前一个
} else {
make.left.mas_equalTo(0); // 第一个靠左
}
}];
lastView = subView;
}
// 最后一个靠右
[lastView mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.mas_equalTo(0);
}];
self.pageControl.numberOfPages = viewModelArray.count;
self.pageControl.currentPage = 0;
// 重新布局
[self setNeedsUpdateConstraints];
}
#pragma mark - life cycle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.pagingEnabled = YES; // 有一页一页翻的感觉;默认是NO,普通的滑动
self.scrollView.delegate = self;
self.scrollView.showsHorizontalScrollIndicator = NO; // 有小圆点提示,不需要滑动指示。默认是YES,有滑动条
[self addSubview:self.scrollView];
self.containerView = [[UIView alloc] init];
[self.scrollView addSubview:self.containerView]; // 辅助视图,用来确定contentSize
self.pageControl = [[UIPageControl alloc] init];
self.pageControl.pageIndicatorTintColor = [UIColor whiteColor]; // 普通小圆点的颜色
self.pageControl.currentPageIndicatorTintColor = [UIColor cyanColor]; // 当前小圆点的颜色
self.pageControl.hidesForSinglePage = YES; // 只有一张图片,就没有必要显示了
[self addSubview:self.pageControl];
}
return self;
}
// tell UIKit that you are using AutoLayout
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
// this is Apple's recommended place for adding/updating constraints
- (void)updateConstraints {
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(0).insets(UIEdgeInsetsMake(kScrollViewEdgeInsetTop, kScrollViewEdgeInsetLeft, kScrollViewEdgeInsetBottom, kScrollViewEdgeInsetRight)); // 确定scrollView的位置.
}];
[self.containerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(0); // 这个已经确定了containerView的frame,和scrollView是父子关系。这是辅助view,不要考虑四周的留白.
make.height.mas_equalTo(self.scrollView.mas_height); // 这里通过containerView来确定scrollView的contentSize,这里是平级的兄弟关系。如果写成make.height.mas_equalTo(0),则不起效果
}];
[self.pageControl mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(0);
make.height.mas_equalTo(kPageControlHegith); // 这里可以不加,不过pageControl自身的高度太大,不利于定位
make.bottom.mas_equalTo(-kPageControlSpaceBottom);
}];
//according to apple super should be called at end of method
[super updateConstraints];
}
#pragma mark - UIScrollViewDelegate
// 这里就是处理小圆点滑动的地方。UIScrollViewDelegate没有停止的函数,只有这个停止减速的函数
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
NSInteger page = self.scrollView.contentOffset.x / self.scrollView.frame.size.width;
self.pageControl.currentPage = page;
}
#pragma mark - action
- (void)imageTouched:(UITapGestureRecognizer *)gestture {
if (nil == self.viewModelArray) {
return;
}
if (self.viewModelArray.count < 1) {
return;
}
UIView *view = gestture.view;
NSInteger tag = view.tag;
NSInteger index = tag - kTagBase;
BannerViewModel *viewModel = self.viewModelArray[index];
if (nil != viewModel.redirctionUrl) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:viewModel.redirctionUrl]];
}
}
@end