创建酷炫的 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
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanhfhaaek
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01 -
怎样阻止微信小程序自动打开
PHP中文网 06-13