Banner的实现过程描述

基础知识

  • 轮播广告(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

参考文章

Masonry介绍与使用实践(快速上手Autolayout)

UIScrollview与Autolayout的那点事

iOS开发-UIImageView响应点击事件

UIImageView响应点击事件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,875评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,569评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,475评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,459评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,537评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,563评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,580评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,326评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,773评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,086评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,252评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,921评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,566评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,190评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,435评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,129评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,125评论 2 352

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,086评论 4 62
  • 通过一个关于相册浏览的简单应用--图片缩放,垂直滑动图片,翻页效果,大家将学到UIScrollView相关的知识。...
    Magenta_she阅读 1,249评论 2 9
  • 儿子回来了,回来后依然不肯动书包,手机不离手。我似乎也能理解,在校整整五天的时间不能碰手机,回来后似乎就要放纵一下...
    旦子阅读 140评论 0 2
  • 序 2017,过去了90天,3个月,1个季。每年伊始,都有自己的小目标。但常常半路流产。若再这样听之任之,...
    一个教书匠的自白阅读 270评论 0 1
  • 所有的痛苦,都必须面对,而且要相处,你选择逃避,它们会永远在那里,在那里嘲笑得看着你,看着你有一天多么不情愿,囧顿...
    姜与小岛阅读 218评论 0 2