创建酷炫的 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 转换动画的主要内容,如果未能解决你的问题,请参考以下文章

创建酷炫的 CollectionViewCell 转换动画

酷炫的阴影3D进度条:CSS/Sass

酷炫的SVG 动态图标

三分钟让你也拥有一个很酷炫的GitHub展示页面(保姆级教程)

三分钟让你也拥有一个很酷炫的GitHub展示页面(保姆级教程)

Python制作这款酷炫的可视化报表,速度也太快了吧