高效图片轮播,两个ImageView实现

作者: codingZero | 来源:发表于2016-03-18 11:45 被阅读11251次

    导语

    在不少项目中,都会有图片轮播这个功能,现在网上关于图片轮播的框架层出不穷,千奇百怪,笔者根据自己的思路,用两个imageView也实现了图片轮播,这里说说笔者的主要思路以及大概步骤,具体代码请看这里,如果觉得好用,请献上你的star

    该轮播框架的优势

    1.文件少,代码简洁
    2.不依赖任何其他第三方库
    3.同时支持本地图片及网络图片
    4.自带图片下载与缓存

    实际使用

    我们先看demo,代码如下


    运行效果


    轮播实现步骤

    接下来,笔者将从各方面逐一分析

    层级结构

    最底层是一个UIView,上面有一个UIScrollView以及UIPageControl,scrollView上有两个UIImageView,imageView宽高 = scrollview宽高 = view宽高


    轮播原理

    假设轮播控件的宽度为x高度为y,我们设置scrollview的contentSize.width为3x,并让scrollview的水平偏移量为x,既显示最中间内容

    scrollView.contentSize = CGSizeMake(3x, y);
    scrollView.contentOffset = CGPointMake(x, 0);
    

    将imageView添加到scrollview内容视图的中间位置


    接下来使用代理方法scrollViewDidScroll来监听scrollview的滚动,定义一个枚举变量来记录滚动的方向

    typedef enum{
      DirecNone,
      DirecLeft,
      DirecRight
    } Direction;
    
    @property (nonatomic, assign) Direction direction;
    
    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
      self.direction = scrollView.contentOffset.x >x? DirecLeft : DirecRight;
    }
    

    使用KVO来监听direction属性值的改变

    [self addObserver:self forKeyPath:@"direction" options:NSKeyValueObservingOptionNew context:nil];
    

    判断滚动的方向,当偏移量大于x,表示左移,则将otherImageView加在右边,偏移量小于x,表示右移,则将otherImageView加在左边


    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
       //self.currIndex表示当前显示图片的索引,self.nextIndex表示将要显示图片的索引
      //_images为图片数组
      if(change[NSKeyValueChangeNewKey] == change[NSKeyValueChangeOldKey]) return;
      if ([change[NSKeyValueChangeNewKey] intValue] == DirecRight) {
        self.otherImageView.frame = CGRectMake(0, 0, self.width, self.height);
        self.nextIndex = self.currIndex - 1;
        if (self.nextIndex < 0) self.nextIndex = _images.count – 1;
      } else if ([change[NSKeyValueChangeNewKey] intValue] == DirecLeft){
        self.otherImageView.frame = CGRectMake(CGRectGetMaxX(_currImageView.frame), 0, self.width, self.height);
        self.nextIndex = (self.currIndex + 1) % _images.count;
      }
      self.otherImageView.image = self.images[self.nextIndex];
    }
    

    通过代理方法scrollViewDidEndDecelerating来监听滚动结束,结束后,会变成以下两种情况


    此时,scrollview的偏移量为0或者2x,我们通过代码再次将scrollview的偏移量设置为x,并将currImageView的图片修改为otherImageView的图片

    - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
      [self pauseScroll];
    }
    
    - (void)pauseScroll {
      self.direction = DirecNone;//清空滚动方向
        //判断最终是滚到了右边还是左边
      int index = self.scrollView.contentOffset.x / x;
      if (index == 1) return; //等于1表示最后没有滚动,返回不做任何操作
      self.currIndex = self.nextIndex;//当前图片索引改变
      self.pageControl.currentPage = self.currIndex;
      self.currImageView.frame = CGRectMake(x, 0, x, y);
      self.currImageView.image = self.otherImageView.image;
      self.scrollView.contentOffset = CGPointMake(x, 0);
    }
    

    那么我们看到的还是currImageView,只不过展示的是下一张图片,如图,又变成了最初的效果


    自动滚动

    轮播的功能实现了,接下来添加定时器让它自动滚动,相当简单

    - (void)startTimer {
       //如果只有一张图片,则直接返回,不开启定时器
       if (_images.count <= 1) return;
       //如果定时器已开启,先停止再重新开启
       if (self.timer) [self stopTimer];
       self.timer = [NSTimer timerWithTimeInterval:self.time target:self selector:@selector(nextPage) userInfo:nil repeats:YES];
       [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    }
    
    - (void)nextPage {
        //动画改变scrollview的偏移量就可以实现自动滚动
      [self.scrollView setContentOffset:CGPointMake(self.width * 2, 0) animated:YES];
    }
    
    注意

    setContentOffset:animated:方法执行完毕后不会调用scrollview的scrollViewDidEndDecelerating方法,但是会调用scrollViewDidEndScrollingAnimation方法,因此我们要在该方法中调用pauseScroll

    - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
      [self pauseScroll];
    }
    

    拖拽时停止自动滚动

    当我们手动拖拽图片时,需要停止自动滚动,此时我们只需要让定时器失效就行了,当停止拖拽时,重新启动定时器

    - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
      [self.timer invalidate];
    }
    
    - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
      [self startTimer];
    }
    

    加载图片

    实际开发中,我们很少会轮播本地图片,大部分都是服务器获取的,也有可能既有本地图片,也有网络图片,那要如何来加载呢?

    定义4个属性
    NSArray imageArray:暴露在.h文件中,外界将要加载的图片或路径数组赋值给该属性
    NSMutableArray images:用来存放图片的数组
    NSMutableDictionary imageDic:用来缓存图片的字典,key为URL
    NSMutableDictionary operationDic:用来保存下载操作的字典,key为URL

    判断外界传入的是图片还是路径,如果是图片,直接加入图片数组中,如果是路径,先添加一个占位图片,然后根据路径去下载图片

    _images = [NSMutableArray array];
    for (int i = 0; i < imageArray.count; i++) {
        if ([imageArray[i] isKindOfClass:[UIImage class]]) {
          [_images addObject:imageArray[i]];//如果是图片,直接添加到images中
        } else if ([imageArray[i] isKindOfClass:[NSString class]]){
          [_images addObject:[UIImage imageNamed:@"placeholder"]];//如果是路径,添加一个占位图片到images中
          [self downloadImages:i];  //下载网络图片
        }
      }
    

    下载图片,先从缓存中取,如果有,则替换之前的占位图片,如果没有,去沙盒中取,如果有,替换占位图片,并添加到缓存中,如果没有,开启异步线程下载

    - (void)downloadImages:(int)index {
      NSString *key = _imageArray[index];
      //从字典缓存中取图片
      UIImage *image = [self.imageDic objectForKey:key];
      if (image) {
        _images[index] = image;//如果图片存在,则直接替换之前的占位图片
      }else{
        //字典中没有从沙盒中取图片
        NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
        NSString *path = [cache stringByAppendingPathComponent:[key lastPathComponent]];
        NSData *data = [NSData dataWithContentsOfFile:path];
        if (data) {
                 //沙盒中有,替换占位图片,并加入字典缓存中
          image = [UIImage imageWithData:data];
          _images[index] = image;
          [self.imageDic setObject:image forKey:key];
        }else{
           //字典沙盒都没有,下载图片
          NSBlockOperation *download = [self.operationDic objectForKey:key];//查看下载操作是否存在
          if (!download) {//不存在
            //创建一个队列,默认为并发队列
            NSOperationQueue *queue = [[NSOperationQueue alloc] init];
            //创建一个下载操作
            download = [NSBlockOperation blockOperationWithBlock:^{
              NSURL *url = [NSURL URLWithString:key];
              NSData *data = [NSData dataWithContentsOfURL:url];
               if (data) {
                            //下载完成后,替换占位图片,存入字典并写入沙盒,将下载操作从字典中移除掉
                UIImage *image = [UIImage imageWithData:data];
                [self.imageDic setObject:image forKey:key];
                self.images[index] = image;
                            //如果只有一张图片,需要在主线程主动去修改currImageView的值
                if (_images.count == 1) [_currImageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
                [data writeToFile:path atomically:YES];
                [self.operationDic removeObjectForKey:key]; 
                }
            }];
            [queue addOperation:download];
            [self.operationDic setObject:download forKey:key];//将下载操作加入字典
          }
        }
      }
    }
    

    监听图片点击

    当图片被点击的时候,我们往往需要执行某些操作,因此需要监听图片的点击,思路如下

    1.定义一个block属性暴露给外界void(^imageClickBlock)(NSInteger index)
    (不会block的可以用代理,或者看这里
    2.设置currImageView的userInteractionEnabled为YES
    3.给currImageView添加一个点击的手势
    4.在手势方法里调用block,并传入图片索引

    结束语

    上面是笔者的主要思路以及部分代码,需要源码的请前往笔者的github下载,https://github.com/codingZero/XRCarouselView,记得献上你的星星哦

    相关文章

      网友评论

      • 这小子:ios10.3 iPhone 6 Plus的gif的url展示不了,是版本兼容问题吗?
      • Young森:-[UIView setImage:]: unrecognized selector sent to instance 0x103d8b8e0
      • Young森:我自定义pagecontrol的图片
      • Young森:我自定义pagecontrol的时候一直崩溃这个[UIView setImage:]: unrecognized selector sent to instance 0x157efa90
      • John伟杰:1.存在滚动bug;2.控件对象内存释放不了,看过代码是自身没有将timer置空;3.对象释放后,对象的某些属性没有释放得了,导致运行内存一直降不下来,这样迟早会造成内存泄漏,估计是代码块使用上的问题,修改了下载图片的代码块后发现内存增加没那么严重,但是问题还是没能完全解决
        charlotte2018:@John伟杰 我觉得定时器是释放不了的,一般你还可以在控制器将要消失的时候,释放定时器,现在是个view,所以你没法释放,只有调用invaludate才释放,但是没有这个时机。
      • sll_:有bug啊作者
      • shine丶明:楼主可以设置page的点间距吗
        shine丶明:@codingZero 我尝试自定义pageController,看看能不能控制间距
        codingZero:因为一般不会去修改page的间距,所以没有提供对应的方法
      • Mg明明就是你:大神,我给你星星了 6666啊
      • PGOne爱吃饺子:先给你点个赞吧
      • _Andy_:大神 #pragma mark nib创建
        - (void)awakeFromNib {
        [self initSubView];
        }
        为什么缺失 [super awakeFromNib]; 加上会有什么问题吗?
        _Andy_:@codingZero 其实 我只是想去掉⚠️:joy:
        codingZero:加不加感觉没啥影响,所以就没加,你可以加上试一下
      • 雷鸣1010:转载,,注明出处可以么
        codingZero:@雷鸣1010 可以
      • qbylucky:self.scrollView.contentSize = CGSizeMake(self.width * 5, 0); 这个地方不应该是self.width * 3吗?有点看不懂了
        qbylucky:@codingZero 哦。谢谢回复
        codingZero:@qbylucky 刚开始是*3,后来发现快速滑动时会出现卡顿的情况,改成*5就好了。
      • 3a93b6ca03e3:你好啊,我在cell里创建轮播,但是好像缓存不起作用,请问怎么回事呢
        codingZero:@烏先森 没有URL是拿不到缓存的,如果有需要的话,那只能保存在本地了
        3a93b6ca03e3:@codingZero 我在cell里创建轮播,那是不是图片数组要保存在本地啊?要不刚打开时还没请求数据时,拿不到图片URL,是空白的
        codingZero:@烏先森 刚刚试了一下,是有缓存的,你看看是不是哪里弄错了。
      • imageURL:思路很好, 简洁明了.
      • a7c6cec64788: [_images addObject:[UIImage imageNamed:@"XRPlaceholder"]];是在这句代码里面改么?
        a7c6cec64788:@codingZero 嗯嗯 好啦 刚才不知道怎么弄的 添加不上去
        codingZero:@a7c6cec64788 是的,改成[_images addObject:[UIImage new]];
      • a7c6cec64788:那个uiimage 可以直接改成[UIImage new]或[[UIImage alloc] init]?我试啦一下 改不了啊
        codingZero:@a7c6cec64788 可以改啊,怎么可能改不了呢
      • a7c6cec64788:那个 可以不加载那张占位图片么 怎么解决这个问题啊?
        codingZero:@a7c6cec64788 能不能贴一下报错信息,在模拟器上也会崩溃吗?因为我这边没有这个问题,所以我没法调试
        a7c6cec64788:@codingZero 如果改了的话 就会崩啦
        codingZero:@a7c6cec64788 .h文件里面有说明的,你可以去看一下
      • a7c6cec64788:第一次 在真机上运行的时候 如果把你的默认图片 删掉啦 并且把[_images addObject:[UIImage imageNamed:@"XRPlaceholder"]]; 这句代码 注释掉 就会崩掉 如果不注释 执行一次 就不会崩啦 不知道是什么原因
      • a7c6cec64788:楼主 你好 这个在iPhone6上面 9.3.3的系统 运行不起来 一直崩啊
      • o_Cooper_o: [_images addObject:[UIImage imageNamed:@"XRPlaceholder"]];
        怎么崩溃到这里了 是怎么回事;
        codingZero:@寄忧谷 能把崩溃原因贴一下吗,你看看XRPlaceholder这个图片有没有,有时候可能没拉下来
      • FMengz:博主,当快速滑动时,会出现图片卡在两张中间的情况,(eg:第一张的一部分和第二张图片一部分都会出现,并且卡在那里,直到下次滚动才会恢复正常),请问怎么解决?
        FMengz:@codingZero 我就是那个无聊的人 :joy: :joy:
        codingZero:@iOS_PJChao 看了一下,可能是滚动过快导致分页异常,目前不清楚为什么会出现这种情况,bug已修复,欢迎下载最新版使用
        codingZero:@iOS_PJChao 这个问题老早就存在了,不过没找到原因,我觉得用户一般不会无聊到不停的滑动,所以就没有管它了,有空我看看
      • 布鲁克先生:从内存开销来说,不如collectionView,只使用一个轮播图,内存就达到将近100M,使用collectionView内存开销就好多了,不超过40M
        codingZero:@布鲁克先生 这是图片的问题,把gif去掉就只有30几M了
      • YungFan:牛逼 赞一个
      • dd25f9257b81:思路赞 学习了
      • SkySongK:DEMO里面设置scrollowView的ContentSize
        - (void)setScrollViewContentSize {
        if (_images.count > 1) {
        self.scrollView.contentSize = CGSizeMake(self.width * 5, 0);
        self.scrollView.contentOffset = CGPointMake(self.width * 2, 0);
        没看明白怎么乘以5?
        codingZero:@蓝冰Song 之前是*3的,但是如果手动滑动速度过快的话,会出现卡顿的现象,*5就没有这个问题了
      • blurryssky:实现了效果固然很棒,但其实代码远无需如此复杂,复杂的代码必然适用的范围不广,用好系统提供的类即可,详情可看我的文章
        codingZero:@blurryssky 没有所谓的好不好,要看具体怎么实现,collectionView的好处就是cell复用,我这个方式总共只有两个imageView,所以不需要复用
        blurryssky:@codingZero scroll view 实现是肯定没有collection view 好的
        codingZero:@blurryssky 你的我看了,是比我的代码少,但是用collectionView实现的轮播,网上实在是太多了,github上就有一百多个,我之所以用scrollview,并不是因为scrollview一定比collectionView好,而是换一种方式来实现而已
      • ff6250868c5d:请问为什么在Storyboard拖的View,做好约束后,在模拟器运行后约束失效,View比屏幕大,突出屏幕了。
        codingZero:@sandouchan 看了一下,确实有这个问题,主要是因为我计算尺寸的时候,获取到的控制器view的尺寸是不对的
        ff6250868c5d:@codingZero 我刚刚测试了几次了~就算是你github上的代码也是有这个问题哦~,就是说Storyboard设置了什么尺寸,就算约束做好了~跑在模拟器上的尺寸不会变,就拿你的DEMO来说,你的DEMO是4.7寸的~如果跑在5.5寸模拟器上,那个轮播View还是4.7寸的Size。
        codingZero:@sandouchan 没有遇到过这种情况,能把你代码发我看下么?yd13150@vip.qq.com
      • MrFire_:大赞!思路超棒!
      • 3a93b6ca03e3:我在我项目里用,但是测试无网络后也没有图片了,但是你的demo无网络还是有图片,不知道是不是我项目什么原因造成的
        codingZero: @烏先森 你的图片url应该是服务器给你的吧,你没有网络当然就拿不到图片url啊,我的demo里url是写死的,所以只要加载过一次,以后没网也有图片
      • YLeaves:包装导航控制器 加载显示的第一张图片变形,拖拽图片也会变形
        codingZero:@LeavesCoder 已修复,谢谢反馈
      • cb3fc6332154:不知为何,你的代码如果连续滑动,每次切换图片都会暂停一下(就是不跟手的感觉)。可能还需要详细读代码才能发现原因。这点体验不是特别好,其他方面还是相当棒的!
        codingZero:@弗丁老爹 你好,我这边试了一下,好像没有你说的那个问题
      • 方同学哈:收藏了
      • 攻城狮1206:代码封装的不错 :+1:
      • df57d5e6a14d:标记一下
      • 阶梯:6666……
      • 汾酒iOSer:写的很好,不过有一个问题,现在是默认自动向左滑动,我在手动向右滑动的时候,当滑到第一张的时候继续向右滑动,图片总是显示第一张的。不会显示,最后一张。
        codingZero: @AmbiTion_D 多谢提醒,已改正
      • 095b62ead3cd:赞赞赞
      • 篮球火:思路清晰,性能好吧
      • qiongyong:牛.....代码简单明了
      • Sunxxxxx丶:很好。
      • MarkTang:程序挂起时 定时器还在继续走
      • 杰米:简介简洁明了
      • 细雨菲菲v:你好,我想问一下imageDic和operationDic是在哪儿创建的,我看的是你的demon
        codingZero:@细雨菲菲v 这个问题问的好,是我自己SB了,忘记写懒加载了,已改正
      • 鼻毛长长:为什么要KVO自己属性?
        codingZero:@别问为什么 我也没找出原因来 :joy:
        别问为什么:#pragma mark KVO监听
        - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
        if(change[NSKeyValueChangeNewKey] == change[NSKeyValueChangeOldKey]) return;
        if ([change[NSKeyValueChangeNewKey] intValue] == DirecRight) {

        NSLog(@"111111111");

        self.otherImageView.frame = CGRectMake(0, 0, self.width, self.height);
        self.nextIndex = self.currIndex - 1;
        if (self.nextIndex < 0) self.nextIndex = _images.count - 1;
        } else if ([change[NSKeyValueChangeNewKey] intValue] == DirecLeft){

        NSLog(@"2222222222");

        self.otherImageView.frame = CGRectMake(CGRectGetMaxX(_currImageView.frame), 0, self.width, self.height);
        self.nextIndex = (self.currIndex + 1) % _images.count;
        }
        self.otherImageView.image = self.images[self.nextIndex];
        }


        添加打印,每次滚动打印两次是什么原因...
        codingZero:@鼻毛长长 也可以不用KVO,直接在scrollviewDidScroll里面判断方向,但是这样的话就必须搞一个变量来记录上一次的方向,用了KVO就可以省去这个麻烦,而且看起来逼格高一点 :grin:
      • 马爷:赞,楼主的思路真的是🐂
      • waynett:将创建很多下载queue,这个地方代码貌似有问题!
        dd25f9257b81:还可以完善一下下载队列
        codingZero:@waynett 是的,每一个下载任务都会创建一个队列,也可以搞个队列属性,所有下载任务都用同一个队列
        马爷:@waynett 什么问题呢?
      • 魅璃儿:……
      • d778e83d24f8:很喜欢楼主这个思路,十分感谢你的分享,受益匪浅。
      • 花开风吹过:大牛牛
      • 文兴:思路很好!之前写的一个js轮播思路跟这个差不多
      • HelloYeah:封装的非常漂亮.
      • 58bc6c29de0a:收藏,感觉不错
      • 剁椒鱼尾:还没有仿写,但感觉作者思路很好,先收藏下来
      • 张悠扬:辛苦了,点个赞。

      本文标题:高效图片轮播,两个ImageView实现

      本文链接:https://www.haomeiwen.com/subject/mmgclttx.html