Note:

This is the article I wrote in Jane in 16 years, now moved here

The github code hasn’t been updated yet, but you can easily change to Objc without learning Swift

The main idea to see the animation can be realized, this is the most important

A brief introduction to UICollectionView

Before iOS6, developers used to use UITableView to display collections of all types of data. Although Apple has used a UI similar to the UICollectionView view in its photos app for a long time, it is not available to third-party developers. At the time, we could leverage third-party frameworks like Three20 to do similar things. In the iOS6 UICollectionViewController apple introduces a new controller. Provides a more elegant way to display various types of data in a view. Now,UICollectionView can be seen everywhere in various types of apps. No matter what application it is, there are always UICollectionView application scenarios, and apple has also made a better optimization of UICollectionView in iOS10. This article mainly shows UICollectionView’s commonly used animations and forced animations, and all animations will be explained in detail in this article. Look at the effect

Results 1:

Effect 2: Circle enlargement

Effect of 3:

Results 4:

Before driving

As you can see from the title, the first two effects require you to master the posture associated with the custom transition. For those of you who are not familiar with this, there are many articles on this in Jane’s book. WWDC 2013 Session Notes – iOS7 ViewController switch. Or first look at the idea of album effect realization.

Effect 1 implementation idea

Let’s talk about the implementation of the long and drag cell, this is the easiest, just implement the collectionView of the UICollectionView, okay? .moveItemAtIndexPath(NSIndexPath, toIndexPath: NSIndexPath) so we first record the indexPath before the move as lastPath, then record the indexPath of the target position as curPath according to the position of the finger, and record lastPath = curPath after the move, so we can achieve the animation effect of dragging the cell. The last step is to modify the data source, which I foolishly did at the beginning at the end of the gesture, which is not ok. Because cells have changed so much while moving, it is important to modify the data source while moving.

Add the long press gesture
        let longGest = UILongPressGestureRecognizer(target: self, action: "longGestHandle:") collectionView? .addGestureRecognizer(longGest)Copy the code
The core code
    func longGestHandle(longGest : UILongPressGestureRecognizer){
        
        switch longGest.state{
        case .Began :
            // Get the click point
            let touchP = longGest.locationInView(collectionView)
            
            // Get the indexPath corresponding to this point
            guard letindexPath = collectionView? .indexPathForItemAtPoint(touchP)else {return}
            
            / / record
            curPath = indexPath
            lastPath = indexPath
            
            // Get the cell for indexPath
            letcell = collectionView? .cellForItemAtIndexPath(indexPath)as! TheFirstCell
            self.cell = cell
            
            let imageView = UIImageView()
            imageView.frame = cell.frame
            imageView.image = cell.image
            imageView.transform = CGAffineTransformMakeScale(1.15.1.15) collectionView? .addSubview(imageView)self.imageView = imageView
            
        case .Changed: cell? .alpha =0
            
            // Get the position of the finger
            lettouchP = longGest.locationInView(collectionView) imageView? .center = touchP// Get the corresponding indexpath based on finger position
            letindexPath = collectionView? .indexPathForItemAtPoint(touchP)if(indexPath ! =nil) { curPath = indexPath collectionView? .moveItemAtIndexPath(lastPath! , toIndexPath: curPath!) }// Modify the data source
            iflastPath ! =nil{
            letlastImg = imageArr[lastPath!.item] imageArr.removeAtIndex(lastPath! .item) imageArr.insert(lastImg, atIndex: curPath! .item) lastPath = curPath }case .Ended: imageView? .removeFromSuperview() cell? .alpha =1
            
        default : break}}}Copy the code

Image browser

thinking
  • Click on thecellModal ofViewWhat kind is it?
  • How do you get ModalViewAccording tocellThe pictures inside?
  • How do I know how to clickcelltheframe?
  • How do I know after dismisscelltheframe?

The answer to the first question is clear, is certainly UICollectionView, we can click the cell indexPath properties in modalVC record, by calling the collectionView. ScrollToItemAtIndexPath (NSIndexP Ath, atScrollPosition: UICollectionViewScrollPosition, animated: Bool), it is worth noting that the animated to spread false, you know. For the third question, we can directly calculate a modalVC property to receive. There is another elegant way to do this (proxy). Fourth, since the final indexPath is known only by modalVC, it is possible to obtain the frame of the cell after the dismiss by proxy.

Definition of protocol and proxy methods
protocol PresentedProtocol : class{
    func getImageView(indexPath : NSIndexPath) -> UIImageView
    func getStartRect(indexPath : NSIndexPath) -> CGRect
    func getEndRect(indexPath : NSIndexPath) -> CGRect
    func getEndCell(indexPath : NSIndexPath) -> TheFirstCell?
}

protocol dismissProtocol : class{
    func getImageView(a) -> UIImageView
    func getEndRect(a) -> NSIndexPath
}
Copy the code

Implementation of proxy methods

Presented some
extension TheFirstViewController : PresentedProtocol{
    
    func getImageView(indexPath: NSIndexPath) -> UIImageView {
        
        let imageView = UIImageView()
        imageView.contentMode = .ScaleAspectFill
        imageView.clipsToBounds = true
        
        letcell = collectionView? .cellForItemAtIndexPath(indexPath)as! TheFirstCell
        imageView.image = cell.imageView.image
        
        return imageView
    }
    
    func getStartRect(indexPath: NSIndexPath) -> CGRect {
        
        letcell = collectionView? .cellForItemAtIndexPath(indexPath)as? TheFirstCell
        
        if cell == nil{
            return CGRectZero
        }
        
        letstartRect = collectionView! .convertRect(cell! .frame, toCoordinateSpace:UIApplication.sharedApplication().keyWindow!)
        
        return startRect
    }
    
    func getEndRect(indexPath: NSIndexPath) -> CGRect {
        letcell = collectionView? .cellForItemAtIndexPath(indexPath)as! TheFirstCell
        return calculateWithImage(cell.imageView.image!)
    }
    
    func getEndCell(indexPath: NSIndexPath) -> TheFirstCell? {
        
        varcell = collectionView? .cellForItemAtIndexPath(indexPath)as? TheFirstCell
        
        if cell == nil{ collectionView? .scrollToItemAtIndexPath(indexPath, atScrollPosition: .Right, animated: false) cell = collectionView? .cellForItemAtIndexPath(indexPath)as? TheFirstCell
            return cell
        }
        
        returncell! }}Copy the code
Dismiss part
// MARK: -dismiss proxy method
extension YJBrowserViewController : dismissProtocol{
    func getImageView(a) -> UIImageView {
        // Get the currently displayed cell
        let cell =  collectionView.visibleCells().first as! YJBrowserCell
        
        let imageView = UIImageView()
        imageView.image = cell.imageView.image
        imageView.contentMode = .ScaleToFill
        imageView.clipsToBounds = true
        imageView.frame = cell.imageView.frame
        
        return imageView
    }

    func getEndRect(a) -> NSIndexPath {
        // Get the currently displayed cell
        let cell =  collectionView.visibleCells().first as! YJBrowserCell
        
        returncollectionView.indexPathForCell(cell)! }}Copy the code
Animation core code
extension YJBrowserAnimator{
    
    func presentAnimate(transitionContext : UIViewControllerContextTransitioning){
        
        // Get the "stage" for the transition animation
        letcontainerView = transitionContext.containerView() containerView? .backgroundColor =UIColor.blackColor()
        
        // Get the modal View
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey) toView? .alpha =0containerView? .addSubview(toView!)// Get the imageView delegate
        guard let presentDelegate = presentDelegate else{
            return
        }
        
        letimageView = presentDelegate.getImageView(indexPath!) imageView.frame = presentDelegate.getStartRect(indexPath!) containerView? .addSubview(imageView)UIView .animateWithDuration(transitionDuration(transitionContext), animations: { () -> Void in
            imageView.frame = presentDelegate.getEndRect(self.indexPath!) {}) (_) - >Void intoView? .alpha =1
                imageView.removeFromSuperview()
                
                // Be sure to call this method after the animation is complete, otherwise there will be many unexpected bugs
                transitionContext.completeTransition(true)}}}Copy the code

Effect 2 implementation ideas

First of all, to achieve this effect, CALayer’s mask property is used. The mask property is very easy to understand. It is a mask. A mask is also a CALayer, but CALayer doesn’t do that, so we can use its subclass CAShapeLayer. This subclass has a property called PATH that allows you to draw various shapes. When a cell is clicked, its center is taken as the center of the circle. The next problem is to find the radius of the circle. There are two ways to find the radius.

Radius idea 1

Note: if you click on cell0, you will not be able to reach the bottom left corner from the center of cell0, because a circle drawn with this radius will not cover the entire screen, so you will need to reach the bottom right corner. X or x = collectionView.width – cell.center.x. Instead of complicated conditional statements, we can use the mathematical function Max () to get the x value

Radius idea 2

Taking the center of the screen as the center of the circle, draw a circle based on the radius of the screen width and height, which can also be achieved. I used this method when I was dismissed. So here’s the code

Sections of the Core Chicago code
extension YJBrowserAnimator{
    
    func maskPresentAnimate(transitionContext : UIViewControllerContextTransitioning){
        
        self.transitionContext = transitionContext
        
        let containerView = transitionContext.containerView()
        
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey) transitionContext.containerView()! .addSubview(toView!)guard let presentDelegate = presentDelegate else{return}
        
        guard let indexPath = indexPath else{return}
        
        let imageView = presentDelegate.getImageView(indexPath)
        imageView.frame = presentDelegate.getStartRect(indexPath)
        
        let startCircle = UIBezierPath(ovalInRect: presentDelegate.getStartRect(indexPath))
        
        // Calculate the radius
        let x = max(imageView.center.x, UIScreen.mainScreen().bounds.width - imageView.center.x)
        let y = max(imageView.center.y, CGRectGetHeight(UIScreen.mainScreen().bounds) - imageView.center.y)
        
        let startRadius = sqrt(pow(x,2) + pow(y,2))
        
        let endPath = UIBezierPath(ovalInRect: CGRectInset(imageView.frame, -startRadius, -startRadius))
        
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = endPath.CGPathtoView? .layer.mask = shapeLayer// Core animation
        let animation = CABasicAnimation(keyPath: "path")
        animation.fromValue = startCircle.CGPath
        animation.toValue = endPath.CGPath
        animation.duration = transitionDuration(transitionContext)
        animation.delegate = self
        shapeLayer.addAnimation(animation, forKey: "")}override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
        ifisMask{ transitionContext? .completeTransition(true) transitionContext? .viewControllerForKey(UITransitionContextFromViewControllerKey)? .view.layer.mask =niltransitionContext? .viewForKey(UITransitionContextFromViewKey)? .removeFromSuperview() } } }Copy the code
Dismiss part core code
extension YJBrowserAnimator {
    
    func maskDismissAnimate(transitionContext : UIViewControllerContextTransitioning){
        
        self.transitionContext = transitionContext
        
        let containerView = transitionContext.containerView()
        
        let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
        
        // Get the size to return
        letstartRect = presentDelegate? .getStartRect((dismissDelegate? .getEndRect())!)let endPath = UIBezierPath(ovalInRect: startRect!)
        
        letradius = sqrt(pow((containerView? .frame.size.height)! .2) + pow((containerView? .frame.size.width)! .2)) / 2
        
        let startPath = UIBezierPath(arcCenter: containerView! .center, radius: radius, startAngle:0, endAngle: CGFloat(M_PI * 2), clockwise: true)
        
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = endPath.CGPath
        shapeLayer.backgroundColor = UIColor.clearColor().CGColorfromView! .layer.mask = shapeLayerlet animate = CABasicAnimation(keyPath: "path")
        animate.fromValue = startPath.CGPath
        animate.toValue = endPath.CGPath
        animate.duration = transitionDuration(transitionContext)
        animate.delegate = self
        shapeLayer.addAnimation(animate, forKey: "")}}Copy the code

Effect 3 Animation idea

First, we’ll introduce some methods of UICollectionViewLayout, which we’ll need to override in this case.

func layoutAttributesForElementsInRect(_ rect: CGRect)- > [UICollectionViewLayoutAttributes]?
Copy the code

This method returns an array of layoutAttributes for the cell in the specified rect. Default returns nil, this method is just drag UICollectionView is invoked when we look at UICollectionViewLayoutAttributes what header file attributes

@available(iOS 6.0*),public class UICollectionViewLayoutAttributes : NSObject.NSCopying.UIDynamicItem {
    
    public var frame: CGRect
    public var center: CGPoint
    public var size: CGSize
    public var transform3D: CATransform3D
    @available(iOS 7.0*),public var bounds: CGRect
    @available(iOS 7.0*),public var transform: CGAffineTransform
    public var alpha: CGFloat
    public var zIndex: Int // default is 0
    public var hidden: Bool // As an optimization, UICollectionView might not create a view for items whose hidden attribute is YES
    public var indexPath: NSIndexPath
    
    public var representedElementCategory: UICollectionElementCategory { get }
    public var representedElementKind: String? { get } // nil when representedElementCategory is UICollectionElementCategoryCell
    
    public convenience init(forCellWithIndexPath indexPath: NSIndexPath)
    public convenience init(forSupplementaryViewOfKind elementKind: String, withIndexPath indexPath: NSIndexPath)
    public convenience init(forDecorationViewOfKind decorationViewKind: String, withIndexPath indexPath: NSIndexPath)}Copy the code

This object can obtain the cell’s frame, center, size, transform3D and other properties, all of which are readWrite. We can use this method to change the cell’s transform in real time to achieve the desired effect

The size of the collectionView increases as the cells are closer to the center of the collectionView. The size of the collectionView increases when the centers of the collectionView overlap. So we need to figure out the distance between the center of the cell and the center of the collectionView.

The drawing is not good, so let’s just have a look. Once you’ve calculated the distance, the next step is to calculate the scale, which you can do on your own. My solution is: when the center of the cell is collectionView.width * 0.5 from the center of the collectionView, I scale by 3/4

The core code
override func layoutAttributesForElementsInRect(rect: CGRect)- > [UICollectionViewLayoutAttributes]? {
        guard let arr = super.layoutAttributesForElementsInRect(collectionView! .bounds)else{return nil}
        
        let cellAttrs = NSArray.init(array: arr, copyItems: true) as! [UICollectionViewLayoutAttributes]
        
        for cellAttr in cellAttrs {
            
            letoffsetX = collectionView! .contentOffset.xletcellDistance = fabs(cellAttr.center.x - ((collectionView? .bounds.width)! *0.5 + offsetX))
            
            let scale = 1- cellDistance / ((collectionView? .bounds.width)! *0.5) * 0.25
            
            cellAttr.transform = CGAffineTransformMakeScale(scale, scale)
            
        }
        return cellAttrs
}
Copy the code
Think further

Is it possible to calculate the size of the cell when I let go, so that the center of the larger cell is aligned with the center of the collectionView? You can use this method

func targetContentOffsetForProposedContentOffset(_ proposedContentOffset: CGPoint,
                           withScrollingVelocity velocity: CGPoint) -> CGPoint
Copy the code

This method is called when we let go, and the default return value is proposedContentOffset

// Parameter description(for example, 🌰, the position at which football stops when it is kicked out of the air without being stoppedCopy the code
The core code
    override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        
        guard let arr = super.layoutAttributesForElementsInRect(CGRect(origin: CGPoint(x: proposedContentOffset.x, y: 0), size: collectionView! .bounds.size))else {return CGPointZero}
        
        var lesserDis = CGFloat(MAXFLOAT)
        
        var proposedContentOffsetX = proposedContentOffset.x
        
        for cellAttr in arr {
            
            letcellDistance = cellAttr.center.x - (collectionView! .bounds.width *0.5 + proposedContentOffset.x)
            
            if fabs(cellDistance) < fabs(lesserDis) {
                lesserDis = cellDistance
            }
        }
        
        proposedContentOffsetX += lesserDis
        
        if proposedContentOffsetX < 0{
            proposedContentOffsetX = 0
        }
        
        return CGPoint(x: proposedContentOffsetX, y: proposedContentOffset.y)
    }
Copy the code
Effect 4 Train of thought

Exactly the same as effect 3.

For additional

In Effect 3, CAShapeLayer and mask were used. If you still don’t understand these two, I recommend some blogs to you to get a better understanding

About CAShapeLayer

Use UIBezierPath and CAShapeLayer to draw all kinds of shapes

About the mask

Some tips on using masks in CALayer

You can make cool animations with good masks, like

For details, see this article -> How Facebook Shimmer works

WWDC2016 Session Notes – iOS10 UICollectionView new features

All of the above animation code has been uploaded to Github. If you want to see the source code, you can download it here