Animate集合视图边界更改自定义布局动画

时间:2017-08-24 20:34:04

标签: ios swift animation uicollectionview

我有一个集合视图,我希望它有两种状态:折叠和展开。

以下是两种状态:

collapsed expanded

集合视图的背景为红色,单元格为紫色背景,集合视图的包含视图为灰色背景。

我正在使用UICollectionViewFlowLayout的自定义子类来处理单元格的分页,缩放和不透明度。布局应该确保其中一个单元始终居中。

我想要实现的目标

两个状态之间的平滑动画,其中单元格通过集合视图简单地增长/缩小。

我正在经历

由于单元格的宽度在两种状态之间变化,我不仅会看到单元格之间的默认淡入淡出动画,而且还会看到以一种状态为中心的单元格不再居中。

current animation

我尝试了什么

我尝试按照UPCarouselLayout进行初始布局。我尝试了this objc.io post关于动画的想法,特别是有关设备轮换的部分。我无法使用他们的方法,因为layoutAttributesForElement(at indexPath: IndexPath)返回的帧不正确。

布局代码

protocol TrayCarouselFlowLayoutDelegate: class {
    func sizeForItemInCarousel(_ collectionView: UICollectionView, _ layout: TrayCarouselFlowLayout) -> CGSize
}

class TrayCarouselFlowLayout: UICollectionViewFlowLayout {
    fileprivate var collectionViewSize: CGSize = .zero

    fileprivate var peekItemScale: CGFloat = 0.95
    fileprivate var peekItemAlpha: CGFloat = 0.65
    fileprivate var peekItemShift: CGFloat = 0.0

    var spacing = 16

    weak var delegate: TrayCarouselFlowLayoutDelegate!

    fileprivate var indexPathsToAnimate: [IndexPath] = []

    override func prepare() {
        super.prepare()
        guard let size = collectionView?.bounds.size, collectionViewSize != size else { return }

        setUpCollectionView()
        updateLayout()
        collectionViewSize = size
    }

    fileprivate func setUpCollectionView() {
        guard let collectionView = collectionView else { return }
        if collectionView.decelerationRate != UIScrollViewDecelerationRateFast {
            collectionView.decelerationRate = UIScrollViewDecelerationRateFast
        }
    }

    fileprivate func updateLayout() {
        guard let collectionView = collectionView else { return }

        itemSize = delegate.sizeForItemInCarousel(collectionView, self)

        let yInset = (collectionView.bounds.height - itemSize.height) / 2
        let xInset = (collectionView.bounds.width - itemSize.width) / 2
        sectionInset = UIEdgeInsets(top: yInset, left: xInset, bottom: yInset, right: xInset)

        let side = itemSize.width
        let scaledItemOffset = (side - side * peekItemScale) / 2

        minimumLineSpacing = spacing - scaledItemOffset
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        let oldBounds = collectionView?.bounds ?? .zero

        if oldBounds != newBounds {
            return true
        }

        return false
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let superAttributes = super.layoutAttributesForElements(in: rect),
            let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes]
            else { return nil }
        return attributes.map { self.transformLayoutAttributes($0) }
    }

    fileprivate func transformLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        guard let collectionView = collectionView else { return attributes }

        let collectionCenter = collectionView.frame.size.width / 2
        let offset = collectionView.contentOffset.x
        let normalizedCenter = attributes.center.x - offset

        let maxDistance = itemSize.width + minimumLineSpacing
        let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
        let ratio = (maxDistance - distance) / maxDistance

        let alpha = ratio * (1 - peekItemAlpha) + peekItemAlpha
        let scale = ratio * (1 - peekItemScale) + peekItemScale
        let shift = (1 - ratio) * peekItemShift

        attributes.alpha = alpha
        attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
        attributes.zIndex = Int(alpha * 10)

        attributes.center.y += shift

        return attributes
    }

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint,
                                      withScrollingVelocity velocity: CGPoint) -> CGPoint {
        guard let collectionView = collectionView, !collectionView.isPagingEnabled,
            let layoutAttributes = self.layoutAttributesForElements(in: collectionView.bounds)
            else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }

        let midSide: CGFloat = collectionViewSize.width / 2
        let proposedContentOffsetCenterOrigin = proposedContentOffset.x + midSide

        let closest = layoutAttributes
            .sorted { abs($0.center.x - proposedContentOffsetCenterOrigin) < abs($1.center.x - proposedContentOffsetCenterOrigin) }
            .first ?? UICollectionViewLayoutAttributes()
        let targetContentOffset = CGPoint(x: floor(closest.center.x - midSide), y: proposedContentOffset.y)

        return targetContentOffset
    }
}

我需要帮助

我的目标是简单地增加集合视图和单元格。由于我在代码中更改了部分插入和单元格的大小,因此最具挑战性的部分是在生长/收缩期间保持细胞居中。任何帮助将不胜感激!

编辑:如果在展开/折叠时我没有更改单元格的宽度,则表明我的行为几乎是我想要的。仍然有一个淡入淡出的动画,但如果它是唯一的标记,那将是可以接受的。出于某种原因,在布局单元格增长时重新定义宽度是导致GIF中出现问题的原因。

这是宽度为静态的GIF:

no size change

0 个答案:

没有答案