• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

创建酷炫的 CollectionViewCell 转换动画

武飞扬头像
颐和园
帮助4

新建 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
系列文章
更多 icon
同类精品
更多 icon
继续加载