自定义流水布局(实现相册功能)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ldszw/article/details/50726203

自定义流水布局collectionViewFlowLayout

  • 本文主要讲解如何进行自定义流水布局来实现一个相册功能,如下图所示:

这里写图片描述

一、

  • 想要实现类似这样的相册功能有三种办法

    1> 利用scrollView来写,添加三个imageView,然后实时监控scrollView的滚动,一旦有imageView离开就立刻放入缓存池中以便用来复用(这种方法属于比较麻烦的)

    2> 利用tableView,但是tableView只支持竖直滚动,不支持水平,但是可以改变tableView的transfrom属性来实现(这种方法看起来有点不合常理)

    3> 利用Apple自带的collectionView(iOS6之后就用得比较广泛了),collectionView是一种比较牛逼的控件,利用好它我们可以做出很多漂亮的界面,collectionView默认是垂直滚动,但是它也支持水平滚动,而且也有重用机制,我们只需要负责填充数据,控制cell的缩放罢了(因此collectionView是首选)

  • TableView和CollectionView的排布区别

    1> tableView的排布是一行一行往下排布,而collectionView的排布完全取决于Laytout,怎样的Laytout布局决定了显示怎样的cell

    2> 因此我们可以看出,想要让界面显示得更加好看(如瀑布流等),就得自定义布局了

    接下来就开始实现功能啦!

二、实现如下图样式

这里写图片描述

  • 首先实现能展现数据并且水平滚动

    1> viewDidLoad方法中创建collectionView,并且注册cell

    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        CGFloat w = self.view.bounds.size.width;
        CGRect collectionViewFrame = CGRectMake(0, 100, w, 200);
    
        // 创建collectionView
        UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:collectionViewFrame collectionViewLayout:[[UICollectionViewFlowLayout  alloc] init]];
    
        // 设置代理和数据源
        collectionView.delegate = self;
        collectionView.dataSource = self;
    
        // 注册cell
        [collectionView registerNib:[UINib nibWithNibName:@"DSImageCell" bundle:nil] forCellWithReuseIdentifier:ID];
    
        // 添加到控制器View中
        [self.view addSubview:collectionView];
    }

2> 创建images数组模型,实现collectionView的数据源方法

@property (nonatomic, strong) NSMutableArray *images;
    - (NSArray *)images
{
        if (_images== nil) {

            self.images = [NSMutableArray array];

            for (int i = 1; i < 20; i++) {
                [self.images addObject:[NSString stringWithFormat:@"%d", i]];
            }
        }
        return _images;
}
    /**
 *  每一组有多少个cell
 */
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
        return self.images.count;
}

    /**
 *  第indexPath位置上显示什么样的cell
 */
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
        DSImageCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath];

        cell.image = self.images[indexPath.item];

        return cell;
}
注意:这里我用的是自定义cell(DSImageCell),并且用了xib来展示item,以下是DSImageCell的.h和.m文件
    @interface DSImageCell : UICollectionViewCell

    @property (nonatomic, copy) NSString *image;

    @end
    @interface DSImageCell ()

    @property (weak, nonatomic) IBOutlet UIImageView *iconView;


    @end

    @implementation DSImageCell

    - (void)awakeFromNib
    {
         // 边框颜色
        self.iconView.layer.borderColor = [UIColor whiteColor].CGColor;

        // 边框宽度
        self.iconView.layer.borderWidth = 3;

        // 变宽圆角
        self.iconView.layer.cornerRadius = 5;

        // 剪切边框超出范围
        self.iconView.layer.masksToBounds = YES;
    }

    - (void)setImage:(NSString *)image
    {
        _image = [image copy];

        self.iconView.image = [UIImage imageNamed:image];
    }

    @end

因为在创建collectionView的时候在init方法中传入的collectionViewLayout是系统自带的UICollectionViewFlowLayout,所以默认是垂直滚动的,所以此时就得自定义流水布局了!

三.

  • 自定义流水布局,创建一个DSLineLayout继承自UICollectionViewFlowLayout,在.m文件中实现如下需求:

    1.cell的缩放
    2.停止滚动后cell居中
    

    1> 设置滚动方向,cell的大小,cell之间的间隔,还有第一个cell和最后一个cell居中(注意:初始化一般在prepareLayout方法中进行,不能在init方法中,因为init方法中collectionView是没有尺寸的),而且每一个cell都有自己的UICollectionViewLayoutAttributes属性,该属性可以控制cell的位置,大小等

    /**
     *  一些初始化的工作最好在这里实现
     */
    - (void)prepareLayout
    {
            [super prepareLayout];

            // 水平方向
            self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
            // 设置item的宽高
            self.itemSize = CGSizeMake(DSItemWH, DSItemWH);
            // 设置cell之间的间距
            self.minimumLineSpacing = DSItemWH - 40;
            CGFloat inset = (self.collectionView.frame.size.width - DSItemWH) * 0.5;

            // 设置组头和组尾的inset(让第一个和最后一个cell显示在最中间)
            self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset);
    }

至于DSItemWH就是cell的默认宽高

static const CGFloat DSItemWH = 100;

这个时候就可以实现上面图片所展示的样式了,所以接下来就需要实现cell在滚动中的缩放(也就是需要时刻监听屏幕范围内cell,此时得重写两个方法)

第一个方法:判断是否要在collectionView的边界发生改变的时候重新布局cell,该方法默认返回NO,如果返回YES,内部会重新调用prepareLayout和layoutAttributesForElementsInRect方法获得所有cell的布局属性

/**
 *  只要边界改变的时候重新布局性
 */
 -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    return YES;
}

第二个方法:也就是刚才上面所说的layoutAttributesForElementsInRect方法,该方法可以在滚动中重新布局cell,然后返回rect范围的所有cell的UICollectionViewLayoutAttributes属性,所以想要实现滚动中控制cell的缩放就得在这个方法里面实现

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return nil;
}

下面是实现的思路:首先取得屏幕可见的rect范围,然后从遍历所有cell的UICollectionViewLayoutAttributes,判断cell是否在屏幕内,然后根据cell的centerX和屏幕的centerX之间的距离来算出需要缩放的比例,然后赋值给cell的transfrom属性

/**
 *  选中该rect内的所有子控件
 *
 *  @param rect 所有item加起来的rect
 *
 *  @return 所有item
 */
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
    // 0.屏幕的可见范围
    CGRect visiableRect;
    visiableRect.size = self.collectionView.frame.size;
    visiableRect.origin = self.collectionView.contentOffset;

    // 1.取出所有item的UICollectionViewLayoutAttributes
    NSArray *attributeArray = [super layoutAttributesForElementsInRect:rect];

    // 获取屏幕中点的X
    CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;

    // 2.遍历所有的布局属性
    for (UICollectionViewLayoutAttributes *attrs in attributeArray)
    {
        // 如果不在屏幕上,直接跳过(两个rect是否相交)
        if (!CGRectIntersectsRect(visiableRect, attrs.frame)) continue;

        // 获取item的中点X
        CGFloat itemCenterX = attrs.center.x;

        CGFloat scale = 1+ 0.8 * (1 - ABS(centerX - itemCenterX) / (self.collectionView.frame.size.width * 0.5));
        attrs.transform = CGAffineTransformMakeScale(scale, scale);
    }

    return attributeArray;
}

接下来,相册功能就差cell滚动停止后自动回到中点位置
在实现之前还得知道一个方法

/**
 *  用来设置collectionView停止滚动那一刻的位置
 *
 *  @param proposedContentOffset 原本collectionView停止滚动那一刻的位置
 *  @param velocity              滚动速度
 *
 *  @return 想要停止滚动的位置
 */
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{ 
}

该方法设置collectionView停止滚动那一刻的位置, proposedContentOffset表示**原本**collectionView停止滚动那一刻的位置, velocity滚动速度,而返回值就是你想要设置的位置.接下来说下实现思路:

首先获取屏幕的centerX,然后获取滚动结束后还在屏幕里面所有cell的centerX,并且找出离屏幕中点X最近的cell,求出两个centerX之间的差值(不需要求绝对值,因为差值又可能为正/负),所以cell停下来的位置就是proposedContentOffset.x加上这段差值

    // 1.获取屏幕中点的X
    CGFloat centerX = proposedContentOffset.x + self.collectionView.frame.size.width * 0.5;

    // 2.计算屏幕滚动最后一刻的位置(lastRect大小和collectionView的frame一样)
    CGRect lastRect;
    lastRect.origin = proposedContentOffset;
    lastRect.size = self.collectionView.frame.size;

    // 3.取出lastRect范围内的item属性
    NSArray *attributeArray = [super layoutAttributesForElementsInRect:lastRect];

    // 4.遍历lastRect范围内的item属性
    CGFloat adjustContentOffet = MAXFLOAT;
    for (UICollectionViewLayoutAttributes *attrs in attributeArray) {
        // 选出其中item的中点X和collectionView中点X的绝对值最小的item出来
        if (ABS(attrs.center.x - centerX) < ABS(adjustContentOffet)) {
            adjustContentOffet = attrs.center.x - centerX; // 该差值可能为正/负
        }
    }

    return CGPointMake(proposedContentOffset.x + adjustContentOffet, proposedContentOffset.y);

到此为止,整个相册功能就实现完毕了,以上代码基本上都在了,由于代码里面注释写得十分清楚,所以就不多以言语代替了,而且因为是系统自带的循环利用,所以用起来一点都不会卡!

猜你喜欢

转载自blog.csdn.net/ldszw/article/details/50726203
今日推荐