`setCollectionViewLayout` 动画在更改集合视图框架时被破坏 [英] `setCollectionViewLayout` animation broken when also changing collection view frame

查看:24
本文介绍了`setCollectionViewLayout` 动画在更改集合视图框架时被破坏的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试在水平胶片布局和垂直堆栈布局之间转换我的收藏视图.

<头>
水平胶片条垂直/扩展堆栈

这是我目前正在做的:

  1. 使用):

    class ViewController: UIViewController {var isExpanded = falsevar verticalFlowLayout = UICollectionViewFlowLayout()var horizo​​ntalFlowLayout = UICollectionViewFlowLayout()@IBOutlet 弱变量 collectionView:UICollectionView!@IBOutlet 弱变量 collectionViewHeightConstraint:NSLayoutConstraint!@IBAction func toggleExpandPressed(_ sender: Any) {isExpanded.toggle()如果是扩展{collectionView.setCollectionViewLayout(verticalFlowLayout, animation: true)///设置垂直滚动collectionViewHeightConstraint.constant = 300///使集合视图高度更高} 别的 {collectionView.setCollectionViewLayout(horizo​​ntalFlowLayout, animation: true)///设置水平滚动collectionViewHeightConstraint.constant = 50///使集合视图高度更短}///动画集合视图的高度UIView.animate(withDuration: 1) {self.view.layoutIfNeeded()}///奖励积分:///这使动画变得更糟,但我希望能够在过渡期间滚动到特定的 IndexPath.//collectionView.scrollToItem(at: IndexPath(item: 9, section: 0), at: .bottom, animation: true)}覆盖 func viewDidLoad() {super.viewDidLoad()VerticalFlowLayout.scrollDirection = .vertical水平流布局.scrollDirection = .horizo​​ntalcollectionView.collectionViewLayout = horizo​​ntalFlowLayoutcollectionView.dataSource = selfcollectionView.delegate = self}}扩展视图控制器:UICollectionViewDelegateFlowLayout {func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) ->CG尺寸{///如果展开,单元格应该是全角的///如果没有展开,单元格的宽度应该是 50return isExpanded ?CGSize(width: collectionView.frame.width, height: 50) : CGSize(width: 100, height: 50)}func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) ->CGFloat {return 0///现在不需要间距}}///示例数据源扩展视图控制器:UICollectionViewDataSource {func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection 部分: Int) ->整数{返回 10}func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) ->UICollectionViewCell {let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ID", for: indexPath)cell.contentView.layer.borderWidth = 5cell.contentView.layer.borderColor = UIColor.red.cgColor返回单元格}}

    解决方案

    我通过使用自定义的 UICollectionViewFlowLayout 类 (

    这是我的视图控制器.我现在使用自定义闭包 sizeForListItemAtsizeForStripItemAt,而不是遵循 UICollectionViewDelegateFlowLayout.

    class ViewController: UIViewController {var isExpanded = false懒惰 var listLayout = FlowLayout(layoutType: .list)懒惰 var stripLayout = FlowLayout(layoutType: .strip)@IBOutlet 弱变量 collectionView:UICollectionView!@IBOutlet 弱变量 collectionViewHeightConstraint:NSLayoutConstraint!@IBAction func toggleExpandPressed(_ sender: Any) {isExpanded.toggle()如果是扩展{collectionView.setCollectionViewLayout(listLayout,动画:真)collectionViewHeightConstraint.constant = 300} 别的 {collectionView.setCollectionViewLayout(stripLayout,动画:真)collectionViewHeightConstraint.constant = 60}UIView.animate(withDuration: 0.6) {self.view.layoutIfNeeded()}}覆盖 func viewDidLoad() {super.viewDidLoad()collectionView.collectionViewLayout = stripLayoutcollectionView.dataSource = self///使用这些代替`UICollectionViewDelegateFlowLayout`listLayout.sizeForListItemAt = { [weak self] indexPath in返回 CGSize(width: self?.collectionView.frame.width ?? 100, height: 50)}stripLayout.sizeForStripItemAt = { indexPath in返回 CGSize(宽:100,高:50)}}}///示例数据源扩展视图控制器:UICollectionViewDataSource {func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection 部分: Int) ->整数{返回 10}func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) ->UICollectionViewCell {let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ID", for: indexPath)cell.contentView.layer.borderWidth = 5cell.contentView.layer.borderColor = UIColor.red.cgColor返回单元格}}

    然后,这是我的自定义 UICollectionViewFlowLayout 类.在 prepare 中,我手动计算和设置每个单元格的帧.我不确定为什么会这样,但是系统现在能够确定哪些单元格等于哪些单元格,甚至跨多个 FlowLayouts(listLayout>stripLayout.).

    enum LayoutType {案例清单箱条}类 FlowLayout:UICollectionViewFlowLayout {var layoutType:布局类型var sizeForListItemAt: ((IndexPath) -> CGSize)?///获取列表项的大小var sizeForStripItemAt: ((IndexPath) -> CGSize)?///获取条带项目的大小var layoutAttributes = [UICollectionViewLayoutAttributes]()///存放每个item的framevar contentSize = CGSize.zero///集合视图的可滚动内容大小override var collectionViewContentSize: CGSize { return contentSize }///将可滚动内容的大小传回集合视图override func prepare() {///配置单元格的框架超级准备()守卫让 collectionView = collectionView else { return }let itemCount = collectionView.numberOfItems(inSection: 0)///我只有 1 个部分如果 layoutType == .list {var y: CGFloat = 0///每个单元格的y位置,从0开始对于 0..UICollectionViewLayoutAttributes?{返回 layoutAttributes[indexPath.item]}覆盖 func layoutAttributesForElements(in rect: CGRect) ->[UICollectionViewLayoutAttributes]?{返回 layoutAttributes.filter { rect.intersects($0.frame) }}///用布局初始化初始化(布局类型:布局类型){self.layoutType = layoutType超级初始化()}///样板代码需要 init?(coder aDecoder: NSCoder) { fatalError(init(coder:) 尚未实现") }覆盖 func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) ->布尔 { 返回真 }覆盖 func invalidationContext(forBoundsChange newBounds: CGRect) ->UICollectionViewLayoutInvalidationContext {let context = super.invalidationContext(forBoundsChange: newBounds) as!UICollectionViewFlowLayoutInvalidationContextcontext.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size返回上下文}}

    感谢这篇精彩的文章提供的巨大帮助.

    I'm trying to transition my collection view between a horizontal film-strip layout and a vertical stack layout.

    Horizontal film strip Vertical/expanded stack

    This is what I'm currently doing:

    1. Using setCollectionViewLayout(_:animated:completion:) to transition between horizontal and vertical scroll direction
    2. Changing the height constraint of the collection view, along with UIView.animate and self.view.layoutIfNeeded()

    The animation from film-strip to vertical stack is fine. However, the animation from vertical stack to film-strip is broken. Here's what it looks like:

    If I make collectionViewHeightConstraint 300 by default, and remove these lines:

    collectionViewHeightConstraint.constant = 300
    collectionViewHeightConstraint.constant = 50
    

    ... the transition animation is fine both ways. However, there is excess spacing, and I want the film-strip layout to only be in 1 row.

    How can I make the animation be smooth both ways? Here's my code (link to the demo project):

    class ViewController: UIViewController {
    
        var isExpanded = false
        var verticalFlowLayout = UICollectionViewFlowLayout()
        var horizontalFlowLayout = UICollectionViewFlowLayout()
        
        @IBOutlet weak var collectionView: UICollectionView!
        @IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!
        @IBAction func toggleExpandPressed(_ sender: Any) {
            
            isExpanded.toggle()
            if isExpanded {
                collectionView.setCollectionViewLayout(verticalFlowLayout, animated: true) /// set vertical scroll
                collectionViewHeightConstraint.constant = 300 /// make collection view height taller
            } else {
                collectionView.setCollectionViewLayout(horizontalFlowLayout, animated: true) /// set horizontal scroll
                collectionViewHeightConstraint.constant = 50 /// make collection view height shorter
            }
            
            /// animate the collection view's height
            UIView.animate(withDuration: 1) {
                self.view.layoutIfNeeded()
            }
            
            /// Bonus points:
            /// This makes the animation way more worse, but I would like to be able to scroll to a specific IndexPath during the transition.
            //  collectionView.scrollToItem(at: IndexPath(item: 9, section: 0), at: .bottom, animated: true)
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
            verticalFlowLayout.scrollDirection = .vertical
            horizontalFlowLayout.scrollDirection = .horizontal
            
            collectionView.collectionViewLayout = horizontalFlowLayout
            collectionView.dataSource = self
            collectionView.delegate = self
        }
    }
    
    extension ViewController: UICollectionViewDelegateFlowLayout {
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            
            /// if expanded, cells should be full-width
            /// if not expanded, cells should have a width of 50
            return isExpanded ? CGSize(width: collectionView.frame.width, height: 50) : CGSize(width: 100, height: 50)
        }
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
            return 0 /// no spacing needed for now
        }
    }
    
    /// sample data source
    extension ViewController: UICollectionViewDataSource {
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return 10
        }
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ID", for: indexPath)
            cell.contentView.layer.borderWidth = 5
            cell.contentView.layer.borderColor = UIColor.red.cgColor
            return cell
        }
    }
    

    解决方案

    I got it working by using a custom UICollectionViewFlowLayout class (link to demo repo). The center cell even stays centered across both layouts (which was actually the exact purpose of the "Bonus points" part in my question)!

    Here's my view controller. Instead of conforming to UICollectionViewDelegateFlowLayout, I now use the custom closures sizeForListItemAt and sizeForStripItemAt.

    class ViewController: UIViewController {
    
        var isExpanded = false
        lazy var listLayout = FlowLayout(layoutType: .list)
        lazy var stripLayout = FlowLayout(layoutType: .strip)
        
        @IBOutlet weak var collectionView: UICollectionView!
        @IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!
        @IBAction func toggleExpandPressed(_ sender: Any) {
            
            isExpanded.toggle()
            if isExpanded {
                collectionView.setCollectionViewLayout(listLayout, animated: true)
                collectionViewHeightConstraint.constant = 300
            } else {
                collectionView.setCollectionViewLayout(stripLayout, animated: true)
                collectionViewHeightConstraint.constant = 60
            }
            UIView.animate(withDuration: 0.6) {
                self.view.layoutIfNeeded()
            }
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
            collectionView.collectionViewLayout = stripLayout
            collectionView.dataSource = self
            
            /// use these instead of `UICollectionViewDelegateFlowLayout`
            listLayout.sizeForListItemAt = { [weak self] indexPath in
                return CGSize(width: self?.collectionView.frame.width ?? 100, height: 50)
            }
            stripLayout.sizeForStripItemAt = { indexPath in
                return CGSize(width: 100, height: 50)
            }
        }
    }
    
    /// sample data source
    extension ViewController: UICollectionViewDataSource {
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return 10
        }
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ID", for: indexPath)
            cell.contentView.layer.borderWidth = 5
            cell.contentView.layer.borderColor = UIColor.red.cgColor
            return cell
        }
    }
    

    Then, here's my custom UICollectionViewFlowLayout class. Inside prepare, I manually calculate and set the frames of each cell. I'm not exactly sure why this works, but the system is now able to figure out which cells are equal to which, even across multiple FlowLayouts (listLayout and stripLayout.).

    enum LayoutType {
        case list
        case strip
    }
    
    class FlowLayout: UICollectionViewFlowLayout {
    
        var layoutType: LayoutType
        var sizeForListItemAt: ((IndexPath) -> CGSize)? /// get size for list item
        var sizeForStripItemAt: ((IndexPath) -> CGSize)? /// get size for strip item
        
        var layoutAttributes = [UICollectionViewLayoutAttributes]() /// store the frame of each item
        var contentSize = CGSize.zero /// the scrollable content size of the collection view
        override var collectionViewContentSize: CGSize { return contentSize } /// pass scrollable content size back to the collection view
        
        override func prepare() { /// configure the cells' frames
            super.prepare()
            
            guard let collectionView = collectionView else { return }
            let itemCount = collectionView.numberOfItems(inSection: 0) /// I only have 1 section
            
            if layoutType == .list {
                var y: CGFloat = 0 /// y position of each cell, start at 0
                for itemIndex in 0..<itemCount {
                    let indexPath = IndexPath(item: itemIndex, section: 0)
                    let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                    attributes.frame = CGRect(
                        x: 0,
                        y: y,
                        width: sizeForListItemAt?(indexPath).width ?? 0,
                        height: sizeForListItemAt?(indexPath).height ?? 0
                    )
                    layoutAttributes.append(attributes)
                    y += attributes.frame.height /// add height to y position, so next cell becomes offset
                }                           /// use first item's width
                contentSize = CGSize(width: sizeForStripItemAt?(IndexPath(item: 0, section: 0)).width ?? 0, height: y)
            } else {
                var x: CGFloat = 0 /// z position of each cell, start at 0
                for itemIndex in 0..<itemCount {
                    let indexPath = IndexPath(item: itemIndex, section: 0)
                    let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                    attributes.frame = CGRect(
                        x: x,
                        y: 0,
                        width: sizeForStripItemAt?(indexPath).width ?? 0,
                        height: sizeForStripItemAt?(indexPath).height ?? 0
                    )
                    layoutAttributes.append(attributes)
                    x += attributes.frame.width /// add width to z position, so next cell becomes offset
                }                              /// use first item's height
                contentSize = CGSize(width: x, height: sizeForStripItemAt?(IndexPath(item: 0, section: 0)).height ?? 0)
            }
        }
        
        /// pass attributes to the collection view flow layout
        override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
            return layoutAttributes[indexPath.item]
        }
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            return layoutAttributes.filter { rect.intersects($0.frame) }
        }
    
        /// initialize with a layout
        init(layoutType: LayoutType) {
            self.layoutType = layoutType
            super.init()
        }
        
        /// boilerplate code
        required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
        override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }
        override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
            let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
            context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
            return context
        }
    }
    

    Thanks to this amazing article for the huge help.

    这篇关于`setCollectionViewLayout` 动画在更改集合视图框架时被破坏的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆