创建酷炫的 CollectionViewCell 转换动画
Posted 颐和园
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了创建酷炫的 CollectionViewCell 转换动画相关的知识,希望对你有一定的参考价值。
新建 ios App 项目,打开 Main.storyboad,拖入一个 CollectionView,为其创建布局约束如下:
为 CollectionView 创建一个 IBOutlet 连接:
@IBOutlet weak var collectionView: UICollectionView!
新建 swift 文件,充当我们的 model ,这就是我们要渲染在 cell 上的数据:
public struct SalonEntity
// MARK: - Variables
/// Name
public internal(set) var name: String?
/// Address
public internal(set) var address: String?
// MARK: - Init
/// Convenience init
public init(name: String, address: String)
self.name = name
self.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.name
salonAddressLabel.text = salon.address
override func prepareForReuse()
super.prepareForReuse()
salonNameLabel.text = nil
salonAddressLabel.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 = self
collectionView.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 collapsed
case expanded
var backgroundColor: UIColor
switch self
case .collapsed:
return UIColor.lightGray
case .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 6
case .expanded:
return 56
var separatorWidth: CGFloat
switch self
case .collapsed:
return 0
case .expanded:
return 240
var separatorHeight: CGFloat
switch self
case .collapsed:
return 0
case .expanded:
return 2
var salonNameLeadingConstant: CGFloat
switch self
case .collapsed:
return 20
case .expanded:
return 40
var salonAddressLeadingConstant: CGFloat
switch self
case .collapsed:
return 60
case .expanded:
return 80
var containerWidth: CGFloat
switch self
case .collapsed:
return 250
case .expanded:
return 320
var containerHeight: CGFloat
switch self
case .collapsed:
return 150
case .expanded:
return 200
然后为 SalonSelecotrCollectionViewCell 增加一个属性:
var state: State = .collapsed
didSet
guard oldValue != state else return
updateViewConstraints()
然后在 updateViewConstraints 方法中,根据不同状态去修改约束常量:
private func updateViewConstraints()
containerView.backgroundColor = state.backgroundColor
containerViewWidthConstraint.constant = state.containerWidth
containerViewHeightConstraint.constant = state.containerHeight
salonNameLeadingConstraint.constant = state.salonNameLeadingConstant
salonAddressLeadingConstraint.constant = state.salonAddressLeadingConstant
interLabelsPaddingConstraint.constant = state.interLabelPadding
separatorLineWidthConstraint.constant = state.separatorWidth
separatorLineHeightConstraint.constant = state.separatorHeight
layoutIfNeeded()
当然,默认情况下 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 = .lightGray
UIView.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.greatestFiniteMagnitude
let horizontalCenter = targetContentOffset.pointee.x + collectionView.bounds.width / 2
let 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.x
if 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 idle
case 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 true
case (.scrolling(_), .scrolling(_)):
return true
default:
return false
当 scrollingState 发生改变时,调用 updateSelection 去修改 cell 的状态:
func updateSelection()
func updateSelection()
UIView.animate(withDuration: 0.15) () -> Void in
guard let indexPath = self.getSelectedIndexPath(),
let cell = self.collectionView.cellForItem(at: indexPath) as? SalonSelectorCollectionViewCell else
return
switch self.scrollingState
case .idle:
cell.state = .expanded
case .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 1
case .scrolling(let animateSelectionFrame):
return animateSelectionFrame ? 0 : 1
var transform: CGAffineTransform
switch self
case .idle:
return .identity
case .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 in
self.selectionFrameView.transform = self.scrollingState.transform
self.selectionFrameView.alpha = self.scrollingState.alpha
...
即可让取景框自动显示,并执行一个微微放大的动画。
然后在 UICollectionViewDelegate 协议的 didSelectItem 方法中,增加
scrollingState = .scrolling(animateSelectionFrame: false)
这样当用户通过点击而非拖动选择一个 cell 时,“取景框动画”仍然播放。
然后在我们在视图一加载时默认选中第一个 cell。在 viewDidLoad() 中:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) [weak self] in
self?.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) / 2
return 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 转换动画的主要内容,如果未能解决你的问题,请参考以下文章
三分钟让你也拥有一个很酷炫的GitHub展示页面(保姆级教程)