基礎(chǔ)知識(shí)
輪播廣告(Banner)是一個(gè)常見的需求,以前一般就出現(xiàn)在最頂部者蠕,現(xiàn)在也可能出現(xiàn)在中間陋率。所以需要放在一個(gè)表格的cell中。無論出現(xiàn)在頂部還是出現(xiàn)在中間公条,或者是放在cell中,還是放在其他view或者控制其中迂曲,把它單獨(dú)做成一個(gè)view是最靈活的靶橱。view是最方便復(fù)用的一種形式。如果說界面層要考慮復(fù)用路捧,那么通過代碼實(shí)現(xiàn)自定義的view关霸,作為組件形式(不要代入不必要的邏輯和數(shù)據(jù)獲取,View僅僅考慮怎么顯示)還是非辰苌ǎ可取的队寇。如果再配上一個(gè)ViewModel作為對(duì)外的數(shù)據(jù)接口,那么將更靈活章姓。文件多出來佳遣,怎么取舍,根據(jù)實(shí)際權(quán)衡凡伊。
一般都用UIScrollView實(shí)現(xiàn)零渐,以前是計(jì)算frame,現(xiàn)在是引入第三方庫(kù)Masonry系忙,用AutoLayout通過加限制實(shí)現(xiàn)诵盼。
UIScrollView能夠滑動(dòng)本質(zhì)contentSize比frame大,就會(huì)滑動(dòng)银还,否則就不用滑動(dòng)风宁。所以UIScrollView加4個(gè)限制確定本身frame之外,還要再加width和height兩個(gè)條件蛹疯,用來確定contentSize戒财。絕對(duì)布局時(shí),算出一個(gè)size給它捺弦,AutoLayout時(shí)饮寞,一般是引入一個(gè)輔助的containerView,通過子view的大小羹呵,反向來決定父view的大小骂际。這兩者的思路是相反的疗琉,這點(diǎn)要理解一下冈欢。另外,還要注意一點(diǎn)的是盈简,在確定contentSize時(shí)凑耻,這個(gè)輔助的containerView不能當(dāng)做UIScrollView的子view看待太示,(在確定frame的時(shí)候確實(shí)是父子關(guān)系),這種情況下應(yīng)該把containerView和UIScrollView看成是同級(jí)別的兄弟view香浩。
如何理解contentSize和frame:contentSize就是平鋪所有內(nèi)容的全部范圍类缤,不要被手機(jī)屏幕大小限制住。而frame相當(dāng)于觀察的窗口邻吭,滑動(dòng)就是這個(gè)窗口在移動(dòng)餐弱。contentOffset就是用來描述這種滑動(dòng)的,包括x囱晴、y兩個(gè)變量膏蚓,跟iOS通用坐標(biāo)系一直,向右畸写,向下變大驮瞧。Banner一般向左滑動(dòng),只要考慮x方向枯芬,y方向不用操心论笔。
一般banner上會(huì)有指示當(dāng)前頁(yè)面的指示器,這個(gè)叫UIPageControl千所。這個(gè)控件比較簡(jiǎn)單狂魔。不過要理解的是,(1)UIScrollView的pagingEnabled屬性要設(shè)為YES淫痰,默認(rèn)是NO毅臊,這樣滑動(dòng)不滿一半會(huì)退回來,超過一半就會(huì)自動(dòng)往前走黑界,這樣就有一頁(yè)一頁(yè)翻的感覺管嬉。(2)小點(diǎn)移動(dòng)的是currentPage這個(gè)屬性來決定的,就是這個(gè)屬性對(duì)應(yīng)的小圓點(diǎn)的顏色跟其他的不一樣朗鸠,讓感覺是小點(diǎn)在移動(dòng)蚯撩。不過這個(gè)屬性值,往往就是用UIScrollView的contentOffset和frame做簡(jiǎn)單除法烛占,取整數(shù)部分而得到的胎挎。(3)UIPageControl自定義的空間比較小,一般市面上能看到的選中點(diǎn)是長(zhǎng)方形的那種忆家,一般都是用自定義View重新實(shí)現(xiàn)了這個(gè)控件犹菇。一般是用貝塞爾曲線重新畫,讓其行為看起來像UIPageControl而已芽卿。
自動(dòng)輪播的效果揭芍,一般是加一個(gè)定時(shí)器,隔固定時(shí)間卸例,設(shè)置UIScrollView的contentOffset和UIPageControl的currentPage称杨。還要考慮的事一旦檢測(cè)有手勢(shì)左右橫掃操作肌毅,這是人在手動(dòng)滑動(dòng),要把計(jì)時(shí)器停止姑原。這個(gè)功能實(shí)現(xiàn)稍微有點(diǎn)麻煩悬而,需要考慮的地方也有點(diǎn)多。
如何響應(yīng)點(diǎn)擊事件:方法1是將一個(gè)UIImageView和UIButton放在一個(gè)view容器中锭汛,UIImageView負(fù)責(zé)顯示圖片笨奠,UIButton負(fù)責(zé)響應(yīng)跳轉(zhuǎn)。方法2是在UIImageView上加一個(gè)tap手勢(shì)唤殴,來負(fù)責(zé)點(diǎn)擊的響應(yīng)艰躺。
基本思路
從UIView繼承一個(gè)自定義的BannerView,作為容器眨八,也作為使用的整體腺兴,通用做法
對(duì)外的接口是一更新函數(shù),參數(shù)是一個(gè)數(shù)組廉侧,數(shù)組成員是一個(gè)自定義的結(jié)構(gòu)页响,包含圖片url和跳轉(zhuǎn)url。
內(nèi)部提供三個(gè)固定的本地圖片和三個(gè)固定的跳轉(zhuǎn)url段誊,后臺(tái)沒有配置也能用闰蚕。這個(gè)只要使用上面那個(gè)對(duì)外接口就可以了,只是本地資源圖片加載方式不一樣连舍,需要增加一個(gè)變量没陡,指明是否來字本地。
UIScrollView的高度固定索赏,寬度和屏幕一樣寬盼玄,平鋪的格式。要留白潜腻,加一個(gè)UIEdgeInset的padding就可以了埃儿。這樣就是橫向滑動(dòng)了。
加一個(gè)輔助的containerView融涣,是UIScrollView的子view童番,4邊和UIScrollView完全重合。在確定UIScrollView的contentSize的時(shí)候威鹿,containerView和UIScrollView是兄弟view的關(guān)系剃斧,不是父子關(guān)系。containerView的高度和UIScrollView的高度一致忽你。containerView的寬度由里面包含的子ImageView來決定幼东。每個(gè)ImageView的高度和containerView一致,寬度和UIScrollView的寬度一致,每個(gè)挨個(gè)相連筋粗。最后一個(gè)ImageView的右邊界和containerView的右邊界一致。這樣炸渡,這些子ImageView就確定了containerView的寬度娜亿。而containerView的高度和寬度,就決定了UIScrollView的contentSize蚌堵。這點(diǎn)是用UIScrollView實(shí)現(xiàn)Banner中用AutoLayout來做最難理解的一點(diǎn)买决。
加一個(gè)UIPagecontrol,他直接加在最外面的BannerView上吼畏,x方向居中督赤,寬度由內(nèi)容決定。有自己的高度泻蚊,估計(jì)有40點(diǎn)左右躲舌。根據(jù)點(diǎn)的大小,可以加一個(gè)10點(diǎn)左右的高度限制性雄,這樣就平整了没卸。離底部設(shè)一個(gè)偏移量,比如10點(diǎn)左右秒旋,這樣做位置設(shè)定就相對(duì)靈活一點(diǎn)约计。UIPagecontrol和UIScrollView平級(jí)比較好,兩者本來也沒什么關(guān)系迁筛。用系統(tǒng)自帶的煤蚌,自定義沒有必要,重新設(shè)計(jì)得并不見得好看细卧。
自動(dòng)輪播功能不添加尉桩,像廣告,不好贪庙。引入計(jì)時(shí)器魄健,就要考慮run mode和多線程的事情,麻煩插勤。自己動(dòng)沽瘦,讓人煩,一看就知道廣告农尖。不動(dòng)析恋,又有小點(diǎn)提示,反而能吸引人去操作盛卡。
代碼示例
BannerViewModel.h
@interface BannerViewModel : NSObject
@property (nonatomic, strong) NSString *imageUrl;
@property (nonatomic, strong) NSString *redirctionUrl;
@property (nonatomic, assign) BOOL isLocalImage; // 這里為YES時(shí)助隧,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) {
// 這里是否可以將對(duì)應(yīng)的限制刪除?在Masonry沒找到相應(yīng)的接口
[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可以響應(yīng)事件
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); // 緊挨著前一個(gè)
} else {
make.left.mas_equalTo(0); // 第一個(gè)靠左
}
}];
lastView = subView;
}
// 最后一個(gè)靠右
[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; // 有一頁(yè)一頁(yè)翻的感覺;默認(rèn)是NO哩牍,普通的滑動(dòng)
self.scrollView.delegate = self;
self.scrollView.showsHorizontalScrollIndicator = NO; // 有小圓點(diǎn)提示棚潦,不需要滑動(dòng)指示。默認(rèn)是YES膝昆,有滑動(dòng)條
[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]; // 普通小圓點(diǎn)的顏色
self.pageControl.currentPageIndicatorTintColor = [UIColor cyanColor]; // 當(dāng)前小圓點(diǎn)的顏色
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); // 這個(gè)已經(jīng)確定了containerView的frame荚孵,和scrollView是父子關(guān)系妹窖。這是輔助view,不要考慮四周的留白.
make.height.mas_equalTo(self.scrollView.mas_height); // 這里通過containerView來確定scrollView的contentSize收叶,這里是平級(jí)的兄弟關(guān)系骄呼。如果寫成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
// 這里就是處理小圓點(diǎn)滑動(dòng)的地方。UIScrollViewDelegate沒有停止的函數(shù)哆致,只有這個(gè)停止減速的函數(shù)
- (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
參考文章
Masonry介紹與使用實(shí)踐(快速上手Autolayout)
UIScrollview與Autolayout的那點(diǎn)事