iOS自定义UICollectionViewLayout实现瀑布流布局

移动端访问不佳,请访问我的个人博客

最近项目中需要用到瀑布流的效果,但是用UICollectionViewFlowLayout又达不到效果,自己动手写了一个瀑布流的layout,下面是我的心路路程
先上效果图与demo地址

因为是用UICollectionView来实现瀑布流的,决定继承UICollectionViewLayout来自定义一个layout来实现一个简单瀑布流的布局,下面是需要重写的方法:

重写这个属性得出UICollectionView的ContentSize:collectionViewContentSize
重写这个方法来得到每个item的布局:layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
重写这个方法给UICollectionView所有item的布局:layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
重写这个方法来实现UICollectionView前的操作:prepare()

实现思路

通过代理模式获得到需要的列数和每一item的高度,用过列数与列之间的间隔和UICollectionView的宽度来得出每一列的宽度,item从左边到右布局,下一列的item放到高度最小的列下面,防止每列的高度不均匀,下面贴上代码和注释:

import UIKit

@objc protocol WCLWaterFallLayoutDelegate {
 //waterFall的列数
 func columnOfWaterFall(_ collectionView: UICollectionView) -> Int
 //每个item的高度
 func waterFall(_ collectionView: UICollectionView, layout waterFallLayout: WCLWaterFallLayout, heightForItemAt indexPath: IndexPath) -> CGFloat
}

class WCLWaterFallLayout: UICollectionViewLayout {

 //代理
 weak var delegate: WCLWaterFallLayoutDelegate?
 //行间距
 @IBInspectable var lineSpacing: CGFloat = 0
 //列间距
 @IBInspectable var columnSpacing: CGFloat = 0
 //section的top
 @IBInspectable var sectionTop: CGFloat = 0 {
 willSet {
  sectionInsets.top = newValue
 }
 }
 //section的Bottom
 @IBInspectable var sectionBottom: CGFloat = 0 {
 willSet {
  sectionInsets.bottom = newValue
 }
 }
 //section的left
 @IBInspectable var sectionLeft: CGFloat = 0 {
 willSet {
  sectionInsets.left = newValue
 }
 }
 //section的right
 @IBInspectable var sectionRight: CGFloat = 0 {
 willSet {
  sectionInsets.right = newValue
 }
 }
 //section的Insets
 @IBInspectable var sectionInsets: UIEdgeInsets = UIEdgeInsets.zero
 //每行对应的高度
 private var columnHeights: [Int: CGFloat]   = [Int: CGFloat]()
 private var attributes: [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()

 //MARK: Initial Methods
 init(lineSpacing: CGFloat, columnSpacing: CGFloat, sectionInsets: UIEdgeInsets) {
 super.init()
 self.lineSpacing = lineSpacing
 self.columnSpacing = columnSpacing
 self.sectionInsets = sectionInsets
 }

 required init?(coder aDecoder: NSCoder) {
 super.init(coder: aDecoder)
 }

 //MARK: Public Methods

 //MARK: Override
 override var collectionViewContentSize: CGSize {
 var maxHeight: CGFloat = 0
 for height in columnHeights.values {
  if height > maxHeight {
  maxHeight = height
  }
 }
 return CGSize.init(width: collectionView?.frame.width ?? 0, height: maxHeight + sectionInsets.bottom)
 }

 override func prepare() {
 super.prepare()
 guard collectionView != nil else {
  return
 }
 if let columnCount = delegate?.columnOfWaterFall(collectionView!) {
  for i in 0..<columnCount {
  columnHeights[i] = sectionInsets.top
  }
 }
 let itemCount = collectionView!.numberOfItems(inSection: 0)
 attributes.removeAll()
 for i in 0..<itemCount {
  if let att = layoutAttributesForItem(at: IndexPath.init(row: i, section: 0)) {
  attributes.append(att)
  }
 }
 }

 override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
 if let collectionView = collectionView {
  //根据indexPath获取item的attributes
  let att = UICollectionViewLayoutAttributes.init(forCellWith: indexPath)
  //获取collectionView的宽度
  let width = collectionView.frame.width
  if let columnCount = delegate?.columnOfWaterFall(collectionView) {
  guard columnCount > 0 else {
   return nil
  }
  //item的宽度 = (collectionView的宽度 - 内边距与列间距) / 列数
  let totalWidth = (width - sectionInsets.left - sectionInsets.right - (CGFloat(columnCount) - 1) * columnSpacing)
  let itemWidth = totalWidth / CGFloat(columnCount)
  //获取item的高度,由外界计算得到
  let itemHeight = delegate?.waterFall(collectionView, layout: self, heightForItemAt: indexPath) ?? 0
  //找出最短的那一列
  var minIndex = 0
  for column in columnHeights {
   if column.value < columnHeights[minIndex] ?? 0 {
   minIndex = column.key
   }
  }
  //根据最短列的列数计算item的x值
  let itemX = sectionInsets.left + (columnSpacing + itemWidth) * CGFloat(minIndex)
  //item的y值 = 最短列的最大y值 + 行间距
  let itemY = (columnHeights[minIndex] ?? 0) + lineSpacing
  //设置attributes的frame
  att.frame = CGRect.init(x: itemX, y: itemY, width: itemWidth, height: itemHeight)
  //更新字典中的最大y值
  columnHeights[minIndex] = att.frame.maxY
  }
  return att
 }
 return nil
 }

 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
 return attributes
 }
}

最后附带demo地址,大家喜欢的话可以star一下

上面是简单的瀑布流的实现过程,希望大家能学到东西,有很多地方考虑的不足,欢迎大家交流学习,谢谢大家的阅读。

(0)

相关推荐

  • iOS自定义UICollectionViewFlowLayout实现图片浏览效果

    以前瀑布流的时候使用过UICollectionView,但是那时使用的是系统自带的UICollectionViewFlowLayout布局,今天看文章,看到UICollectionViewFlowLayout自定义相关的东西,于是动手写了一个简单图片浏览的demo,熟练一些UICollectionViewFlowLayout自定义布局. #import <UIKit/UIKit.h> @interface JWCollectionViewFlowLayout : UICollectionVie

  • Swift自定义iOS中的TabBarController并为其添加动画

    自定义TabBarController 有时候默认的TabBarController不能满足我们的开发需求,比如你想用彩色的图标,系统却只调用图标的轮廓,所以我们需要自己定义一下TabBar. 方法一:修改TabBarController中的TabBar 新建 CustomTabBarController 类继承自 UITabBarController,并在Storyboard中设置: 首先自定义 tabBar 的背景,在 viewDidLoad() 方法中添加: 复制代码 代码如下: // 用

  • iOS自定义推送消息提示框

    看到标题你可能会觉得奇怪 推送消息提示框不是系统自己弹出来的吗? 为什么还要自己自定义呢? 因为项目需求是这样的:最近需要做 远程推送通知 和一个客服系统 包括店铺客服和官方客服两个模块 如果有新的消息推送的时候 如果用户当前不在客服界面的时候  要求无论是在app前台 还是app退到后台 顶部都要弹出系统的那种消息提示框 这样的需求 我们就只能自定义一个在app内 弹出消息提示框 实现步骤如下: 1.我们自定义一个view 为 STPushView 推送消息的提示框view  #import

  • IOS轻松几步实现自定义转场动画

    一.系统提供的转场动画 目前,系统给我们提供了push/pops和present/dismiss两种控制器之间跳转方.当然,通过设置UIModalTransitionStyle属性,可以实现下面4种modal效果,相信大家都比较熟悉了,这里就不再展示效果图. UIModalTransitionStyleCoverVertical // 从下往上, UIModalTransitionStyleFlipHorizontal // 水平翻转 UIModalTransitionStyleCrossDis

  • IOS 自定义UICollectionView的头视图或者尾视图UICollectionReusableView

    IOS 自定义UICollectionView的头视图或者尾视图UICollectionReusableView 其实看标题就知道是需要继承于UICollectionReusableView,实现一个满足自己需求的视图.那么如何操作了,看下面代码: ViewController.m文件中 #import "ViewController.h" #import "LSHControl.h" #import "SHCollectionReusableView.h

  • 实例解析iOS开发中系统音效以及自定义音效的应用

    一.访问声音服务 添加框架AudioToolBox以及要播放的声音文件,另外还需要在实现声音服务的类中导入该框架的接口文件: #import <AudioToolbox/AudioToolbox.h> 播放系统声音,需要两个函数是AudioServicesCreateSystemSoundID和AudioServicesPlaySystemSound,还需要声明一个类型为SystemSoundID类型的变量,它表示要使用的声音文件. 复制代码 代码如下: -(IBAction) playSys

  • iOS自定义button抖动效果并实现右上角删除按钮

    遇到过这种需求要做成类似与苹果删除软件时的动态效果. 1.长按抖动; 2.抖动时出现一个X; 3.点击x,删除button; 4.抖动时,点击按钮,停止抖动; 下面是我的设计思路: 1.继承UIButton: 2.给button在右上角添加一个按钮: 3.给button添加长按手势: 4.给button添加遮盖,抖动时可以拦截点击事件: 有更好的做法,还请斧正. // .m文件 #import "DZDeleteButton.h" #import "UIView+Extens

  • iOS开发中使用Quartz2D绘图及自定义UIImageView控件

    绘制基本图形 一.简单说明 图形上下文(Graphics Context):是一个CGContextRef类型的数据 图形上下文的作用:保存绘图信息.绘图状态 决定绘制的输出目标(绘制到什么地方去?)(输出目标可以是PDF文件.Bitmap或者显示器的窗口上) 相同的一套绘图序列,指定不同的Graphics Context,就可将相同的图像绘制到不同的目标上. Quartz2D提供了以下几种类型的Graphics Context: Bitmap Graphics Context PDF Grap

  • IOS手势操作(拖动、捏合、旋转、点按、长按、轻扫、自定义)

    下面通过图文并茂的方式给大家分享下IOS手势操作(拖动.捏合.旋转.点按.长按.轻扫.自定义)的相关内容. 1.UIGestureRecognizer 介绍 手势识别在 iOS 中非常重要,他极大地提高了移动设备的使用便捷性. iOS 系统在 3.2 以后,他提供了一些常用的手势(UIGestureRecognizer 的子类),开发者可以直接使用他们进行手势操作. UIPanGestureRecognizer(拖动) UIPinchGestureRecognizer(捏合) UIRotatio

  • 详解在iOS App中自定义和隐藏状态栏的方法

    自定义状态栏 有时候,需要在状态栏上显示一些自定义信息,比如新浪微博的官方iOS客户端:告知用户信息处于发送队列.发送成功或者发送失败. 如上图,通过在状态栏显示自定义信息,可以给用户友好又不影响软件使用的提示. 为此,我们显得定义一个自定义状态栏类,包含一个显示信息的Label: 复制代码 代码如下: @interface CustomStatusBar : UIWindow  {      UILabel *_messageLabel;  }    - (void)showStatusMes

  • iOS开发之自定义UITextField的方法

    UITextField是IOS开发中用户交互中重要的一个控件,常被用来做账号密码框,输入信息框等. 观察效果图 UITextField有以下几种特点: 1.默认占位文字是灰色的 2.当光标点上去时,占位文字变为白色 3.光标是白色的 接下来我们通过不同的方法来解决问题 一.将xib中的UITextField与代码关联 通过NSAttributeString方法来更改占位文字的属性 (void)viewDidLoad { [super viewDidLoad]; // Do any additio

随机推荐