UICollectionView之滚动图片缩放

开始前的准备
  • 先看下效果,这个效果是使用UICollectionView实现的,通过自定义继承自系统的流水布局
kobe.gif
  • 如果你对上面效果感兴趣,那非常欢迎你继续往下看,首先我需要先说明几个关于自定义继承自系统的流水布局的几个关键方法,实现这个效果的基础就是先清楚这几个方法哦
    实现这个效果,就默认大家已经掌握了collectionView的最基本使用了哦,所以这里就不在详细说明collectionView的一些基本属性和方法啦
基本结构

实现该效果其实也不是很复杂的哦

  • 创建一个UICollectionView,尺寸和屏幕一样大
  • 自定义Cell继承自UICollectionViewCell --- LBPhotoCell
  • 自定义布局,继承自系统的UICollectionViewFlowLayout --- LBVerLayout
关键方法说明

可能会枯燥,但是这个是理解这个效果的前提哦,我也会非常用心的讲解

1)重写   - (void)prepareLayout

 - 该方法是准备布局,会在cell显示之前调用,可以在该方法中设置布局的一些属性,比如滚动方向,cell之间的水平间距,以及行间距等
 - 也建议在这个方法中做布局的初始化操作,不建议在init方法中初始化,这个时候可能CollectionView还没有创建,官方文档也有明确说明哦
 - 如果重写了该方法,一定要调用父类的prepareLayout
2) 重写   - (NSArray *)layoutAttributesForElementsInRect:(CGRect):rect
 
 - 该方法的返回值是一个存放着rect范围内所有元素的布局属性的数组
 - 数组里面的对象决定了rect范围内所有元素的排布(frame)
 - 里面存放的都是UICollectionViewLayoutAttributes对象,该对象决定了cell的排布样式
 - 一个cell就对应一个UICollectionViewLayoutAttributes对象
 - UICollectionViewLayoutAttributes对象决定了cell的frame
3) 重写   - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
  - 是否允许在里面cell位置改变的时候重新布局
  - 默认是NO,返回YES的话,该方法内部重新会按顺序调用以下2个方法
      **- (void)prepareLayout
      **- (NSArray *)layoutAttributesForElementsInRect:(CGRect):rect

4)重写   - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
 - proposedContentOffset:原本情况下,collectionview停止滚动时最终的偏移量
    **滑动的时候手松开因为惯性并不会立即停止,会再滚动一会才会真正停止,这个属性就是记录这个真正停止这一刻的偏移量
    **我们这个效果是手指松开,完全停止滚动的时候,离屏幕中间y值最近的cell自动滚动到屏幕的中间
    **所以我们需要利用该方法的返回值,这个返回值就是需要我们给一个偏移量,这个collectionview在它由于惯性滚动结束后,再去多滚动我们给的这一部分偏移量
 - velocity:滚动速率,可以根据velocity的x或y判断它是向上/向下/向右/向左滑动
    **这个参数在这里没有什么用,但是这个参数本身还是非常有用的,我之前使用过它来判断当前tabbleview是向上滑还是向下滑,这个时候可以通过这个判断很简单的就控制是隐藏tabBar或者显示tabBar,或者是隐藏显示导航条,使用很爽
具体实现

复杂的方法说明后,终于迎来了更为枯燥的撸码时刻~~~
1)在ViewController.m文件中

static NSString * const LBPhoto = @"kobe";

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建布局
    LBVerLayout *layout = [[LBVerLayout alloc] init];
    layout.itemSize = CGSizeMake(150, 150);
    
    // 创建collectionView
    CGFloat collectionW = self.view.frame.size.width;
    CGFloat collectionH = self.view.frame.size.height;
    CGRect frame = CGRectMake(0, 0, collectionW , collectionH);
    UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
    collectionView.dataSource = self;
    collectionView.delegate = self;
    collectionView.backgroundColor = [UIColor clearColor];
    [self.view addSubview:collectionView];
    
    // 注册cell,我这里是使用的xib
    [collectionView registerNib:[UINib nibWithNibName:NSStringFromClass([LBPhotoCell class]) bundle:nil] forCellWithReuseIdentifier:LBPhoto];    

}

#pragma mark - <UICollectionViewDataSource>
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 20;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{ 
    //创建cell
    LBPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:LBPhoto forIndexPath:indexPath];
    //重写imageName的set方法,内部其实就是给cell的imageView赋值(imageView是我们自己给cell添加的子控件)
    cell.imageName = [NSString stringWithFormat:@"kobe_%zd", indexPath.item ];
}

2)在继承自UICollectionViewCell的自定义cell的.m文件中

@interface LBPhotoCell()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation LBPhotoCell

- (void)awakeFromNib {
    //给imageView的图层设置边框宽度以及边框颜色
    self.imageView.layer.borderWidth = 10;
    self.imageView.layer.borderColor = [UIColor blackColor].CGColor;
}

//重写imageName的set方法,外界传一个图片名给我们,我们在cell内部给cell的子控件赋值
- (void)setImageName:(NSString *)imageName
{
    _imageName = [imageName copy];
    self.imageView.image = [UIImage imageNamed:imageName];
}
@end

3)最后一步,最为关键的一步,赋值的计算都是在这个类里面实现的
在继承自系统的UICollectionViewFlowLayout的类的.m文件中

- (void)prepareLayout
{
    [super prepareLayout];
    // 垂直滚动
    self.scrollDirection = UICollectionViewScrollDirectionVertical;
    self.minimumInteritemSpacing = 20;

    // 设置collectionView里面内容的内边距(上、左、下、右)
    CGFloat inset = (self.collectionView.frame.size.width - 2*self.itemSize.width) /3;
    self.sectionInset = UIEdgeInsetsMake(inset, inset, inset, inset);
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    return YES;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
     // 拿到系统已经帮我们计算好的布局属性数组,然后对其进行拷贝一份,后续用这个新拷贝的数组去操作
    NSArray * originalArray   = [super layoutAttributesForElementsInRect:rect];
    NSArray * curArray = [[NSArray alloc] initWithArray:originalArray copyItems:YES];
    
    // 计算collectionView中心点的y值(这个中心点可不是屏幕的中线点哦,是整个collectionView的,所以是包含在屏幕之外的偏移量的哦)
    CGFloat centerY = self.collectionView.contentOffset.y + self.collectionView.frame.size.height * 0.5;
    
    // 拿到每一个cell的布局属性,在原有布局属性的基础上,进行调整
    for (UICollectionViewLayoutAttributes *attrs in curArray) {
        // cell的中心点y 和 collectionView最中心点的y值 的间距的绝对值
        CGFloat space = ABS(attrs.center.y - centerY);
        
        // 根据间距值 计算 cell的缩放比例
        // 间距越大,cell离屏幕中心点越远,那么缩放的scale值就小
        CGFloat scale = 1 - space / self.collectionView.frame.size.height;
        
        // 设置缩放比例
        attrs.transform = CGAffineTransformMakeScale(scale, scale);
    }
    
    return curArray;
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    // 计算出停止滚动时(不是松手时)最终显示的矩形框
    CGRect rect;
    rect.origin.y = proposedContentOffset.y;
    rect.origin.x = 0;
    rect.size = self.collectionView.frame.size;
    
    // 获得系统已经帮我们计算好的布局属性数组
    NSArray *array = [super layoutAttributesForElementsInRect:rect];
    
    // 计算collectionView最中心点的y值
    // 再啰嗦一下,这个proposedContentOffset是系统帮我们已经计算好的,当我们松手后它惯性完全停止后的偏移量
    CGFloat centerY = proposedContentOffset.y + self.collectionView.frame.size.height * 0.5;
    
    // 当完全停止滚动后,离中点Y值最近的那个cell会通过我们多给出的偏移量回到屏幕最中间
    // 存放最小的间距值
    // 先将间距赋值为最大值,这样可以保证第一次一定可以进入这个if条件,这样可以保证一定能闹到最小间距
    CGFloat minSpace = MAXFLOAT;
    for (UICollectionViewLayoutAttributes *attrs in array) {
        if (ABS(minSpace) > ABS(attrs.center.y - centerY)) {
            minSpace = attrs.center.y - centerY;
        }
    }
    // 修改原有的偏移量
    proposedContentOffset.y += minSpace;
    return proposedContentOffset;
}

点击这里下载代码

(ps:感谢@予人与人,亲自敲了代码发现了我漏掉的一个问题,非常感谢)

结束语
  • OK,这个效果也基本实现了,忙碌的周末也快结束了,明天公司会有新人过来面试,一些不错的面试题有机会的话会继续跟大家分享的
  • 慢慢发现把自己稍微了解的东西分享出去这样非常有助于自己的提升,感觉自己会不是牛逼的,最牛逼的是把自己会的能分享给别人,过程中会发现很多细节
  • 今后会经常性的和大家分享实用有趣的东西,我们共同提升

推荐阅读更多精彩内容