KelvinJin / AnimatedCollectionViewLayout

A UICollectionViewLayout subclass that adds custom transitions/animations to the UICollectionView without effecting your existing code.
MIT License
4.7k stars 347 forks source link

Having trouble with Cube Animator w/ programmatic cells #61

Open GalCohen opened 3 years ago

GalCohen commented 3 years ago

Hi.

The layout looks broken when using a cell programmatically. What am I missing? What could be different between the configuration of the cell in the Sample project in the storyboard and my own?

I narrowed the problem down to cell configuration because if I copy the Sample project's storyboard and viewControllers, I am about to successfully display and scroll the collectionview. As soon as I replace the Sample cell with my own, it fails.

I tore down the cell to the bare minimum

class MyCustomCell: UICollectionViewCell {
  // nothing    
}

and still, I get something like: Screen Shot 2020-10-19 at 4 00 19 PMScreen Shot 2020-10-19 at 4 00 25 PM

I'm on v 1.10, using SwiftPM, testing on iPhone SE Simulator iOS 13.5 And 14. I'm just trying to test out a full screen collectionview exactly like the sample project, only programmatic, and using my own cell.

GalCohen commented 3 years ago

I am also able to reproduce this by taking the sample project, removing the prototype cell, and replacing it with my own cell. As soon as I use the custom cell, like the one above, the layout breaks. Any idea what could be so different between the storyboard and programmatic setup?

KelvinJin commented 3 years ago

The only thing I can think of is the clipsToBounds settings on either the cell or the contentView. Can you upload a sample project to reproduce this?

GalCohen commented 3 years ago

Hi @KelvinJin , thank you for the quick response. I've attached a project that demonstrates this. All I've done is take the sample project from the repo, remove the prototype cell from the storyboard, and modified it slightly to work programmatically instead.

programmaticCellExample.zip

kazuteru commented 3 years ago

Hi @KelvinJin I am facing a similar problem I confirmed the trouble of cube animation on iOS13.7 (iPhone11 Simulator) It seems to occur when UIButton / UIImage is placed in cell and the value is changed every time it is reused. Does not occur with UILabel I also put a sample project ("iOS Example" has been changed)

Please confirm

AnimatedCollectionViewLayout-master.zip

KelvinJin commented 3 years ago

@GalCohen @kazuteru Sorry for the late reply guys.

This issue has been giving me some hard time 😢

What I found out is that I broke this with the latest update. I can't figure out what exactly are different between programmatically created cell and the one created in Interface Builder. But my update fixed similar issue that's happening on the IB created while introduced the issue to the programmatic one 🤯

Now the proper way to fix this is to figure out what are the differences and try to workaround both cases at the same time.

At the mean time, please either

Again, thanks for reporting this issue!

/// An animator that applies a cube transition effect when you scroll.
public struct CubeAttributesAnimatorForProgrammaticCell: LayoutAttributesAnimator {
    /// The perspective that will be applied to the cells. Must be negative. -1/500 by default.
    /// Recommended range [-1/2000, -1/200].
    public var perspective: CGFloat

    /// The higher the angle is, the _steeper_ the cell would be when transforming.
    public var totalAngle: CGFloat

    public init(perspective: CGFloat = -1 / 500, totalAngle: CGFloat = .pi / 2) {
        self.perspective = perspective
        self.totalAngle = totalAngle
    }

    public func animate(collectionView: UICollectionView, attributes: AnimatedCollectionViewLayoutAttributes) {
        let position = attributes.middleOffset
        if abs(position) >= 1 {
            attributes.contentView?.layer.transform = CATransform3DIdentity
            attributes.contentView?.keepCenterAndApplyAnchorPoint(CGPoint(x: 0.5, y: 0.5))
        } else if attributes.scrollDirection == .horizontal {
            let rotateAngle = totalAngle * position
            var transform = CATransform3DIdentity
            transform.m34 = perspective
            transform = CATransform3DRotate(transform, rotateAngle, 0, 1, 0)

            attributes.contentView?.layer.transform = transform
            attributes.contentView?.keepCenterAndApplyAnchorPoint(CGPoint(x: position > 0 ? 0 : 1, y: 0.5))
        } else {
            let rotateAngle = totalAngle * position
            var transform = CATransform3DIdentity
            transform.m34 = perspective
            transform = CATransform3DRotate(transform, rotateAngle, -1, 0, 0)

            attributes.contentView?.layer.transform = transform
            attributes.contentView?.keepCenterAndApplyAnchorPoint(CGPoint(x: 0.5, y: position > 0 ? 0 : 1))
        }
    }
}

extension UIView {
     func keepCenterAndApplyAnchorPoint(_ point: CGPoint) {

         guard layer.anchorPoint != point else { return }

         var newPoint = CGPoint(x: bounds.size.width * point.x, y: bounds.size.height * point.y)
         var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y)

         newPoint = newPoint.applying(transform)
         oldPoint = oldPoint.applying(transform)

         var c = layer.position
         c.x -= oldPoint.x
         c.x += newPoint.x

         c.y -= oldPoint.y
         c.y += newPoint.y

         layer.position = c
         layer.anchorPoint = point
     }
 }
GalCohen commented 3 years ago

I spent a few hours on this with no luck. Thanks for taking the time to investigate and suggesting some fixes. Really appreciate the work!

HimmaHorde commented 3 years ago

@GalCohen @KelvinJin

use autoLayout

    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.translatesAutoresizingMaskIntoConstraints = false
        contentView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        contentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        contentView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
        contentView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
    }
HimmaHorde commented 3 years ago

@KelvinJin iOS 14 programmatically created cells will reset Origin (changed by anchor) You can try to optimize your code in the following ways

else if attributes.scrollDirection == .horizontal {
            let rotateAngle = totalAngle * position
            let anchorPoint = CGPoint(x: position > 0 ? 0 : 1, y: 0.5)

            // As soon as we changed anchor point, we'll need to either update frame/position
            // or transform to offset the position change. frame doesn't work for iOS 14 any
            // more so we'll use transform.
            let anchorPointOffsetValue = contentView.layer.bounds.width / 2
            let anchorPointOffset = position > 0 ? -anchorPointOffsetValue : anchorPointOffsetValue
            var transform = CATransform3DMakeTranslation(anchorPointOffset, 0, 0)
            contentView.layer.anchorPoint = anchorPoint

            if #available(iOS 14, *) {
                if contentView.translatesAutoresizingMaskIntoConstraints == true {
                    // not use transformX/Y
                    transform = CATransform3DMakeTranslation(0, 0, 0)
                    // reset origin
                    var frame = attributes.frame
                    frame.origin = .zero
                    contentView.frame = frame
                }
            }
            transform.m34 = perspective

            transform = CATransform3DRotate(transform, rotateAngle, 0, 1, 0)
            contentView.layer.transform = transform

        }
rome753 commented 1 year ago

extension UIView { func keepCenterAndApplyAnchorPoint(_ point: CGPoint) {

     guard layer.anchorPoint != point else { return }

     var newPoint = CGPoint(x: bounds.size.width * point.x, y: bounds.size.height * point.y)
     var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y)

     newPoint = newPoint.applying(transform)
     oldPoint = oldPoint.applying(transform)

     var c = layer.position
     c.x -= oldPoint.x
     c.x += newPoint.x

     c.y -= oldPoint.y
     c.y += newPoint.y

     layer.position = c
     layer.anchorPoint = point
 }

}

Works for most devices, but not work for iPhone 7 Plus(iOS 13.6), or iPhone 11 sometimes.

VladimirMoor commented 7 months ago

try to add checking for rotateAngle in this place:

if abs(position) >= 1 || rotateAngle == 0 { contentView.layer.transform = CATransform3DIdentity contentView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) } else { ...