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 the
cell
Modal ofView
What kind is it? - How do you get Modal
View
According tocell
The pictures inside? - How do I know how to click
cell
theframe
? - How do I know after dismiss
cell
theframe
?
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