新建 iOS App 项目,打开 Main.storyboad,拖入一个 CollectionView,为其创建布局约束如下:

为 CollectionView 创建一个 IBOutlet 连接:

@IBOutlet weak var collectionView: UICollectionView!

新建 swift 文件,充当我们的 model ,这就是我们要渲染在 cell 上的数据:

public struct SalonEntity {// MARK: - Variables/// Namepublic internal(set) var name: String?/// Addresspublic internal(set) var address: String?// MARK: - Init/// Convenience initpublic init(name: String, address: String) {self.name = nameself.address = address}
}

新建 UICollectionViewCell 子类 SalonSelectorCollectionViewCell。打开 SalonSelectorCollectionViewCell.xib,创建如下 UI :

SalonSelectorCollectionViewCell 目前还是十分简单:

class SalonSelectorCollectionViewCell: UICollectionViewCell {@IBOutlet weak var containerView: UIView!@IBOutlet weak var salonNameLabel: UILabel!@IBOutlet weak var salonAddressLabel: UILabel!@IBOutlet weak var separatorLine: UIView!func configure(with salon: SalonEntity) {salonNameLabel.text = salon.namesalonAddressLabel.text = salon.address}override func prepareForReuse() {super.prepareForReuse()salonNameLabel.text = nilsalonAddressLabel.text = nil}
}
extension UICollectionViewCell {class var reuseIdentifier: String { return NSStringFromClass(self).components(separatedBy: ".").last! }
}

打开 ViewController.swift,在 viewDidLoad 中:

        collectionView.register(UINib(nibName: "SalonSelectorCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: SalonSelectorCollectionViewCell.reuseIdentifier)collectionView.dataSource = selfcollectionView.delegate = self

然后实现 UICollectionViewDataSource:

extension ViewController: UICollectionViewDataSource {public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {salons.count}public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {guard let selectorCell = collectionView.dequeueReusableCell(withReuseIdentifier: SalonSelectorCollectionViewCell.reuseIdentifier, for: indexPath) as? SalonSelectorCollectionViewCell else { return UICollectionViewCell() }let salon = salons[indexPath.item]selectorCell.configure(with: salon)return selectorCell}
}
extension ViewController: UICollectionViewDelegate {public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {}func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {}
}

运行 App,collect view 中显示出 5 个 cell:

接下来,我们要利用 UICollectionViewDelegate 协议让 collection view 在选中状态下显示一点不同的样式:

    public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)guard let cell = collectionView.cellForItem(at: indexPath) as? SalonSelectorCollectionViewCell else { return }cell.containerView.backgroundColor = .lightGray}func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {guard let cell = collectionView.cellForItem(at: indexPath) as? SalonSelectorCollectionViewCell else { return }cell.containerView.backgroundColor = .white}

这样当选中一个 cell 时,cell 背景色变成灰色。但这样显然不够酷。我们需要为它添加一点动画。

首先,我们为 SalonSelectorCollectionViewCell 增加一个状态:

    enum State {case collapsedcase expandedvar backgroundColor: UIColor {switch self {case .collapsed:return UIColor.lightGraycase .expanded:return .white}}}

State 有两种状态:collapsed 和 expanded,二者的不同在于 backgroundColor - collapse 状态下这个值时灰色,而 expanded 状态下为白色,就类似于我们刚才所做的,当 cell 选中时是一个颜色,反选时是另一个颜色。

当然除了背景色外,我们还需要让 cell 在两种不同的状态下做一些 UI 上的改变,比如在 expanded 状态下让 cell 变得更大一点。这需要我们为一些布局约束创建一些 IBOutlet:

    @IBOutlet weak var interLabelsPaddingConstraint: NSLayoutConstraint!   // 两个 label 间的 padding@IBOutlet weak var separatorLineWidthConstraint: NSLayoutConstraint!   // 中间细线的宽@IBOutlet weak var separatorLineHeightConstraint: NSLayoutConstraint!  // 中间细线的高@IBOutlet weak var containerViewHeightConstraint: NSLayoutConstraint!  // 整个 cell 的高@IBOutlet weak var containerViewWidthConstraint: NSLayoutConstraint!   // 整个 cell 的宽@IBOutlet weak var salonNameLeadingConstraint: NSLayoutConstraint!     // 沙龙名(上面的 label)的 leading@IBOutlet weak var salonAddressLeadingConstraint: NSLayoutConstraint!  // 沙龙地址(下面的 label)的 leading

同时在 enm State 的定义中,规定在不同状态( collapase 状态和 expanded 状态)下对应约束的 constant 值,总的来说除了背景色的不同外,会让 cell 在 expanded 状态下显得稍大一些,同时 collapsed 状态下中间的分割线是不可见的:

enum State {...var interLabelPadding: CGFloat {switch self {case .collapsed:return 6case .expanded:return 56}}var separatorWidth: CGFloat {switch self {case .collapsed:return 0case .expanded:return 240}}var separatorHeight: CGFloat {switch self {case .collapsed:return 0case .expanded:return 2}}var salonNameLeadingConstant: CGFloat {switch self {case .collapsed:return 20case .expanded:return 40}}var salonAddressLeadingConstant: CGFloat {switch self {case .collapsed:return 60case .expanded:return 80}}var containerWidth: CGFloat {switch self {case .collapsed:return 250case .expanded:return 320}}var containerHeight: CGFloat {switch self {case .collapsed:return 150case .expanded:return 200}}
}

然后为 SalonSelecotrCollectionViewCell 增加一个属性:

    var state: State = .collapsed {didSet {guard oldValue != state else { return }updateViewConstraints()}}

然后在 updateViewConstraints 方法中,根据不同状态去修改约束常量:

    private func updateViewConstraints() {containerView.backgroundColor = state.backgroundColorcontainerViewWidthConstraint.constant = state.containerWidthcontainerViewHeightConstraint.constant = state.containerHeightsalonNameLeadingConstraint.constant = state.salonNameLeadingConstantsalonAddressLeadingConstraint.constant = state.salonAddressLeadingConstantinterLabelsPaddingConstraint.constant = state.interLabelPaddingseparatorLineWidthConstraint.constant = state.separatorWidthseparatorLineHeightConstraint.constant = state.separatorHeightlayoutIfNeeded()}

当然,默认情况下 cell 是 collapsed 状态(反选):

    override func prepareForReuse() {...state = .collapsed}

回到 view controller 修改 didSelectItemAt 方法:

    public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)guard let cell = collectionView.cellForItem(at: indexPath) as? SalonSelectorCollectionViewCell else { return }cell.containerView.backgroundColor = .lightGrayUIView.animate(withDuration: 0.3) {cell.state = .expanded}}

实际上,didDeselectItemAt 方法是不必要的,我们可以删除它了。

运行 App,现在我们选中 cell 时,cell 背景色从浅灰变成白色,同时 cell 放大:

通常情况下选择一个 cell 需要你点击它,但我们经常会在某些 app 中看到,有时候 cell 并不需要点击,只需要将它滚动到视图中心就回自动选中,这是怎么做到的?

这实际上利用了 UIScrollView 的相关代理而非 UICollectionView。回到 ViewController.swift,实现如下方法:

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {var offsetAdjustment = CGFloat.greatestFiniteMagnitudelet horizontalCenter = targetContentOffset.pointee.x + collectionView.bounds.width / 2let targetRect = CGRect(origin: targetContentOffset.pointee, size: collectionView.bounds.size)guard let layoutAttributes = collectionView.collectionViewLayout.layoutAttributesForElements(in: targetRect) else { return }for layoutAttribute in layoutAttributes {let itemHorizontalCenter = layoutAttribute.center.xif abs(itemHorizontalCenter - horizontalCenter) < abs(offsetAdjustment) {offsetAdjustment = itemHorizontalCenter - horizontalCenter}}targetContentOffset.pointee.x += offsetAdjustment}

这样,在滚动 scroll view 时,当你释放手指时,这个方法回自动将 scroll view 滚动的位置调整到 cell 中心对齐,当然,前提是 contentView 有足够的空间(例外情况:第一个 cell 和最后一个 cell)。你可以运行 App 看看效果。

然后定义一个新枚举,用于记录 ScrollView 的滚动状态:

enum SelectionCollectionViewScrollingState {case idlecase scrolling(animateSelectionFrame: Bool)
}

idle 表示 scroll view 已经停止滚动,scrolling 表示还在滚动。在 ViewController 中定义一个 SelectorCollectionViewScrollingState 属性:

    private var scrollingState: SelectionCollectionViewScrollingState = .idle {didSet {if scrollingState != oldValue {updateSelection()}}}

这里对 SelectionCollectionViewScrollingState 进行了 != 比较,需要让 SelectionCollectionViewScrollingState 实现 Equatable 协议:

extension SelectionCollectionViewScrollingState: Equatable {public static func ==(lhs: SelectionCollectionViewScrollingState, rhs: SelectionCollectionViewScrollingState) -> Bool {switch (lhs, rhs) {case (.idle, .idle):return truecase (.scrolling(_), .scrolling(_)):return truedefault:return false}}
}

当 scrollingState 发生改变时,调用 updateSelection 去修改 cell 的状态:

     func updateSelection() {func updateSelection() {UIView.animate(withDuration: 0.15) { () -> Void inguard let indexPath = self.getSelectedIndexPath(),let cell = self.collectionView.cellForItem(at: indexPath) as? SalonSelectorCollectionViewCell else {return}switch self.scrollingState {case .idle:cell.state = .expandedcase .scrolling(_):cell.state = .collapsed}}}}func getSelectedIndexPath() -> IndexPath? {let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)if let visibleIndexPath = collectionView.indexPathForItem(at: visiblePoint) {return visibleIndexPath}return nil}

getSelectedIndex() 首先获取 collection view 当前的可视区域的 frame,然后得到它的中心点,调用 collectionView.indexPathForItem() 方法并传入这个中心点,即可知道位于该点的 cell 的 indexPath。

然后实现 scrollView 的两个代理方法:

    public func scrollViewDidScroll(_ scrollView: UIScrollView) {scrollingState = .scrolling(animateSelectionFrame: true)}public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {scrollingState = .idle}

这样,当你滚动 collection view 时,滚动到屏幕中央的 cell 会自动选中并呈现 expanded 状态:

video link: https://gitee.com/kmyhy/CollectionViewCoolAnimation/raw/master/5.mov

如果视频不能播放,可在此处下载:https://gitee.com/kmyhy/CollectionViewCoolAnimation/raw/master/5.mov

这样还不够酷,我们准备在选中的 cell 外面再添加一个类型取景框的效果:

首先,在 Main.storyboard,拖入一个 view,并为它创建一个 IBOutlet:

    @IBOutlet weak var selectionFrameView: UIView!

在 selectionFrameView 上面放入两个 image view,增加相应的约束,宽高 340*220 并让它和 collection view 中央对齐,类似成这样:

注意左下角的那张图片可以让它旋转 180 度:layer.transform.rotation.z = 3.14

类似在 State 枚举所做的,我们将 SelectionCollectionViewScrollingState 的两个状态绑定到另外两个属性:

enum SelectionCollectionViewScrollingState {...var alpha: CGFloat {switch self {case .idle:return 1case .scrolling(let animateSelectionFrame):return animateSelectionFrame ? 0 : 1}}var transform: CGAffineTransform {switch self {case .idle:return .identitycase .scrolling(let animateSelectionFrame):return animateSelectionFrame ? CGAffineTransform(scaleX: 1.5, y: 1.5) : CGAffineTransform(scaleX: 1.15, y: 1.15)}}
}

当 idle 状态时,selectionFrameView 的 alpha 将被设置为 1,切换到 .scrolling 状态后,alpha 根据 animatedSelectionFrame 而定,为 true 时 = 0,为 false 时 = 1,同时 transform 也会做相应的改变。这样,只需切换 idle/scrolling 状态,就可改变 “取景框”显示/隐藏状态和 frame 大小。

每当选中 cell 都会调用 updateSelection 方法,我们只需在 updateSelection 方法增加这 2 句:

 UIView.animate(withDuration: 0.15) { () -> Void inself.selectionFrameView.transform = self.scrollingState.transformself.selectionFrameView.alpha = self.scrollingState.alpha...}

即可让取景框自动显示,并执行一个微微放大的动画。

然后在 UICollectionViewDelegate 协议的 didSelectItem 方法中,增加

scrollingState = .scrolling(animateSelectionFrame: false)

这样当用户通过点击而非拖动选择一个 cell 时,“取景框动画”仍然播放。

然后在我们在视图一加载时默认选中第一个 cell。在 viewDidLoad() 中:

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] inself?.updateSelection()
}

因为 collection view 在 viewDidLoad 的时候很可能并没有渲染,此时 collection view 可能并没有来得及实例化任何 cell ,导致 update cell 状态失败,因此我们延迟 0.5 秒才调用 updateSelection 方法,以解决此问题。这是一个不完美的解决方案。

video link: 7.mov

如果视频不能播放,请在此处下载:https://gitee.com/kmyhy/CollectionViewCoolAnimation/raw/master/7.mov

可以发现,正如前面所说,第一个 cell 和最后一个 cell 没有滚动到屏幕中央。这可以通过让 ViewController 实现 UICollectionViewDelegateFlowLayout 协议来解决:


extension ViewController: UICollectionViewDelegateFlowLayout {public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {let padding = (collectionView.bounds.width - SalonSelectorCollectionViewCell.State.expanded.containerWidth) / 2return UIEdgeInsets(top: 10, left: padding, bottom: 10, right: padding)}
}

通过调整 cell 的左右 padding ,让 cell 自动居中显示。最终效果如下:

video link: 8.mov

如果视频不能播放,请到此处下载:https://gitee.com/kmyhy/CollectionViewCoolAnimation/raw/master/8.mov

创建酷炫的 CollectionViewCell 转换动画相关推荐

  1. 自定义动画属性java_创建酷炫动画效果的10个JavaScript库

    原标题:创建酷炫动画效果的10个JavaScript库 1) Dynamics.jsDynamics.js是设计基于物理规律的动画的重要Java库.它可以赋予生命给所有包含CSS 和SVG属性的DOM ...

  2. ztext - 简单几行代码创建酷炫 3D 特效文字的开源 JS 库

    把网页上的文字变成酷炫的 3D 风格,还能制作旋转动效,有了 ztext.js,只需要几行代码. ztext 能做什么 ztext.js 是一个能把常规的平面文字变成 3D 样式的前端开源代码库,让开 ...

  3. 情人节程序员用HTML网页表白【彩色酷炫的空间背景动画特效】 HTML5七夕情人节表白网页源码 HTML+CSS+JavaScript

    这是程序员表白系列中的100款网站表白之一,旨在让任何人都能使用并创建自己的表白网站给心爱的人看. 此波共有100个表白网站,可以任意修改和使用,很多人会希望向心爱的男孩女孩告白,生性腼腆的人即使那个 ...

  4. android 新闻应用、Xposed模块、酷炫的加载动画、下载模块、九宫格控件等源码...

    Android精选源码 灵活的ShadowView,可替代CardView使用 基于Tesseract-OCR实现自动扫描识别手机号 Android播放界面仿QQ音乐开源音乐播放器 新闻应用项目采用了 ...

  5. ae制h5文字动画_html5酷炫的文字打字动画特效

    特效描述:html5 酷炫的文字 打字动画特效.html5和css3制作键盘输入文字酷炫的打字动画特效. 代码结构 1. 引入JS 2. HTML代码 0 1 2 3 4 5 6 7 8 9 10 1 ...

  6. android 新闻应用、Xposed模块、酷炫的加载动画、下载模块、九宫格控件等源码

    Android精选源码 灵活的ShadowView,可替代CardView使用 基于Tesseract-OCR实现自动扫描识别手机号 Android播放界面仿QQ音乐开源音乐播放器 新闻应用项目采用了 ...

  7. android 新闻应用、Xposed模块、酷炫的加载动画、下载模块、九宫格控件等源码... 1

    Android精选源码 灵活的ShadowView,可替代CardView使用 基于Tesseract-OCR实现自动扫描识别手机号 Android播放界面仿QQ音乐开源音乐播放器 新闻应用项目采用了 ...

  8. html5 canvas 烟花,html5 canvas酷炫的烟花爆炸动画特效

    特效描述:html5canvas 酷炫的 烟花爆炸动画特效.html5点击页面烟花爆炸动画,3D烟花动画,酷炫的烟花特效. 代码结构 1. 引入JS 2. HTML代码 点击页面 (function( ...

  9. canvas 形状碰撞_【案例】如何用html5 制作canvas酷炫的网状图形动画特效

    点击上方[我分享我快乐]→[...]右上角→[设为星标⭐]即可第一时间获取最新设计资源 哈喽大家好,又到了每周二案例环节啦,在教学开始前先插播一条招聘信息~ 高薪招聘 杭州招聘(非技术) 公司背景: ...

最新文章

  1. 因果关系:真的存在吗?
  2. Apache开启GZIP压缩功能方法
  3. Python程序开发——第九章 异常处理
  4. python单选按钮重置_python – Tkinter单选按钮初始化错误
  5. java mvc中重复提交表单,spring mvc 防止重复提交表单的两种方法,推荐第二种
  6. 计算机网络【一】概述+OSI参考模型
  7. JAVA-SWING:生成透明JTable的改进2
  8. 矩阵的Cholesky 分解
  9. 以下内容被锁定不能编辑:mxh983 ph6527 zyt2538 zyt9276 zy2674 zyt4372 zyt8731 zyt2832求解
  10. 【0门槛】PR稿的自我修养
  11. win10的计算机用户名怎么改,win10账户名修改,教您win10怎么更改账户名称
  12. ZigBee网络数据传递流程_米家入门(设备篇):智能网关(ZigBee)
  13. android 自定义指南针,android 自定义指南针view
  14. 浏览器无法访问百度的问题
  15. 那些 996 公司的员工怎么样了?
  16. 人力资源管理案例选读【3】
  17. 集成测试最全详解,看完必须懂了
  18. 【领导力】同理心:CIO 的力量倍增器
  19. 计算机论文摘要200字模板,论文的摘要范文(论文摘要是摘抄还是自己写)
  20. Java容器之HashMap源码解析(视频讲解)

热门文章

  1. C语言中三个数排列大小,C语言三个数排列大小的实现方法
  2. IDM和迅雷哪个更好用?Internet Download Manager迅雷对比
  3. Scaling Hyperledger Fabric Using Pipelined Execution and Sparse Peers(提升fabric 6倍性能的文章翻译)
  4. Google出品的Python代码静态类型分析器:Pytype
  5. MFC使用滚动条显示大图片
  6. 肺活量测试软件原理,测测你的肺活量 肺活量测试装置小发明小实验
  7. Ubuntu18.04和win10双系统完美安装(dell笔记本电脑)
  8. OSChina 周一乱弹 —— 终于可以尝尝冷水泡面了
  9. hive on tez安装部署、配置及tez-ui配置
  10. Vue使用Pinia实现状态管理