大家好,我是Tony。今天和大家分享一下我做的AppStore里面的其中两个Animation。就是当大家点击Card是和离开Card时的Animation。
这里我先放一个我录制的Gif给大家看下效果
这只是其中一个简单的效果,当然还有很多其他的效果值得我们去挖掘。那么,这个简单的效果是怎么做成的呢。首先我要给大家看一张图
Transitioning Delegate
每一个controller都可以有自己的transitioningDelegate,也就是UIViewControllerTransitioningDelegate,每当你显示或者退出一个view controller的时候, UIKit就会需要一个transitioning delegate。当然,每个方法都会有自己的default的animation,比如presentViewController就是从下往上显示下一个ViewController。
Animation Controller
在这里面它会返回一个object。并且这个Controller会继承UIViewControllerAnimatedTransitioning,里面包括你自定义的animation。
Transitioning Context
这里是官方的描述:
Declaration
protocol UIViewControllerContextTransitioning : NSObjectProtocol
Description
Description
A set of methods that provide contextual information for transition animations between view controllers.
Do not adopt this protocol in your own classes, nor should you directly create objects that adopt this protocol. During a transition, the animator objects involved in that transition receive a fully configured context object from UIKit. Custom animator objects—objects that adopt the UIViewControllerAnimatedTransitioning or UIViewControllerInteractiveTransitioning protocol—should simply retrieve the information they need from the provided object.
A context object encapsulates information about the views and view controllers involved in the transition. It also contains details about the how to execute the transition. For interactive transitions, the interactive animator object uses the methods of this protocol to report the animation’s progress. When the animation starts, the interactive animator object must save a pointer to the context object. Based on user interactions, the animator object then calls the updateInteractiveTransition(_:), finishInteractiveTransition(), or cancelInteractiveTransition() methods to report the progress toward completing the animation. Those methods send that information to UIKit so that it can drive the timing of the animations.
Important
When defining custom animator objects, always check the value returned by the isAnimated() method to determine whether you should create animations at all. And when you do create transition animations, always call the completeTransition(_:) method from an appropriate completion block to let UIKit know when all of your animations have finished.
大意就是说transitioning context会用到UIViewControllerContextTransitioning,但是UIKit会帮你用到这个Protocol,正如图面画的那样。并且在你结束的时候需要用到completeTransition。
The Transitioning Process
接下来是presentation transition的几个步骤:
- 我们触发transition
- UIKit会向view controller要transitioning delegate,如果没有UIKit就会用内建的默认的。
- 当UIKit看到了transitioning delegate之后,会触发这个protocol的第一个方法:animationController(forPresented:presenting:source:)。如果没有,就用默认的。
- UIKit会构建一个transitioning context。
- UIkit之后会询问controller里面的transitionDuration(using:)的方法
- UIKit接着call animateTransition(using:) function。
- 最后,call completeTransition(_:)表示过程结束。
说了那么多,不如动手来做做。
首先给大家一个start project,在我的github里面可以下载
打开之后大家可以先运行程序,发现这里用的是默认的
self.present(controller, animated: true, completion: nil)
首先第一步是, 按照我们刚才说的那样,创建一个自定义的animation controller。在Controllers里面创建一个新的文件名字为PresentSectionViewController
接着import uikit, 以及创建class:
import UIKit
class PresentSectionViewController: NSObject, UIViewControllerAnimatedTransitioning {
}
当写完之后会出现报错,因为UIViewControllerAnimatedTransitioning是一个protocol,所以必须满足这个协议就要添加两个function。
第一个function就很简单了,意思就是问要多久,我们在这里就填入0.6。当然,如果你想让你的app更加灵活,就可以加入variable,然后在Controller里面定也行。
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
接下来我们需要理解的就是,这个animation不是将原来的cell移动成点进去的那样,也就是说我们没有动原来的cell,而是将进去之后的页面变成其实的cell的大小以及位置,然后变大的。所以这里我们需要cell的位置,也就是cell的frame。我们用一个variable去储存它。
var cellFrame: CGRect!
然后我们需要在animateTransition(using transitionContext: UIViewControllerContextTransitioning)加入东西了
// 1
let toVC = transitionContext.viewController(forKey: .to) as! AppDetailController
let containerView = transitionContext.containerView
let bounds = UIScreen.main.bounds
// 2
containerView.addSubview(toVC.view)
这里的1和2分别是什么意思呢?首先,第一个variable toVC的意思是我需要获取这次animation到达的controller,也就是to。当然,from对应来的controller。后面我们用as!是因为我们肯定确定去的controller是AppDetialController,所以我们可以这么做,如果不知道,千万不可以这样,不然会报错的。
接着这个containerView是指我们的animation的最底层的这个View。我给大家看一张图就明白了。
在UIWindow的上面,APPDetailController里面UIView的下面有这么一个UITransitioning View。这个view就是我们这里的containerView。所以我们之后会往上面加东西。
这个bounds就是整个手机屏幕的frame。
在第二步里我们就需要把toVC.view加到contrainerView里面,也就是把AppDetailController的View加到上面。
// 3
toVC.image.layer.cornerRadius = 14
toVC.imageHeightConstraint.constant = 460
toVC.viewTopConstraint.constant = cellFrame.origin.y - 20
toVC.viewLeftConstraint.constant = cellFrame.origin.x
toVC.viewRightConstraint.constant = -(bounds.width - cellFrame.origin.x - cellFrame.width)
toVC.viewBottomConstraint.constant = -(bounds.height - cellFrame.origin.y - cellFrame.height)
toVC.view.layoutIfNeeded()
在第三步里面,有很多东西我们没有在之前看到,就是这些constraint。这些constraint是auto constraint的variable。在AppDetailController里面,我们用的是anchor的function,不妨打开Extension里面的Anchor,其实就是用的auto constraint。如果不知道这是什么可以在评论里面问我。但是由于我们用的这个constraint在这个PresentSectionViewControllerl里面不可以用到,所有我们需要再AppDetailController创建这些variable。
var viewTopConstraint: NSLayoutConstraint!
var viewLeftConstraint: NSLayoutConstraint!
var viewRightConstraint: NSLayoutConstraint!
var viewBottomConstraint: NSLayoutConstraint!
其次我们需要添加一个UIView, 名字为background,然后修改image。background是为了把image放在这个上面,并且加入它的constaint:
let background: UIView = {
let view = UIView()
view.backgroundColor = .white
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let image: UIImageView = {
let image = UIImageView()
image.image = #imageLiteral(resourceName: "flBackground")
image.contentMode = .scaleAspectFill
image.clipsToBounds = true
image.isUserInteractionEnabled = true
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
fileprivate func setupViews() {
view.backgroundColor = .white
view.addSubview(background)
background.addSubview(image)
image.addSubview(cancelButton)
image.addSubview(icon)
image.addSubview(getButton)
viewTopConstraint = background.topAnchor.constraint(equalTo: view.topAnchor)
viewLeftConstraint = background.leftAnchor.constraint(equalTo: view.leftAnchor)
viewRightConstraint = background.rightAnchor.constraint(equalTo: view.rightAnchor)
viewBottomConstraint = background.bottomAnchor.constraint(equalTo: view.bottomAnchor)
NSLayoutConstraint.activate([
viewTopConstraint,
viewLeftConstraint,
viewRightConstraint,
viewBottomConstraint
])
image.anchor(top: background.topAnchor, left: background.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 550)
cancelButton.anchor(top: image.topAnchor, left: nil, bottom: nil, right: image.rightAnchor, paddingTop: 14, paddingLeft: 0, paddingBottom: 0, paddingRight: 14, width: 28, height: 28)
cancelButton.addTarget(self, action: #selector(handleCancel), for: .touchUpInside)
icon.anchor(top: image.topAnchor, left: image.leftAnchor, bottom: nil, right: nil, paddingTop: 14, paddingLeft: 14, paddingBottom: 0, paddingRight: 0, width: 80, height: 80)
getButton.anchor(top: nil, left: nil, bottom: image.bottomAnchor, right: image.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 20, paddingRight: 14, width: 80, height: 28)
}
接着我们需要加入imageHeightConstraint。把image的anchor改为:
imageHeightConstraint = image.heightAnchor.constraint(equalToConstant: 550)
NSLayoutConstraint.activate([
image.topAnchor.constraint(equalTo: background.topAnchor),
image.leftAnchor.constraint(equalTo: background.leftAnchor),
image.rightAnchor.constraint(equalTo: background.rightAnchor),
imageHeightConstraint
])
对于下面的逻辑也是很简单的。
比如说toVC.viewTopConstraint.constant需要等于cell的y轴减去20(status bar)的高度,需要距离上面这么多也就正好是cell的顶部了。其他同理。不明白可以评论问我。
// 3
toVC.image.layer.cornerRadius = 14
toVC.imageHeightConstraint.constant = 460
toVC.viewTopConstraint.constant = cellFrame.origin.y - 20
toVC.viewLeftConstraint.constant = cellFrame.origin.x
toVC.viewRightConstraint.constant = -(bounds.width - cellFrame.origin.x - cellFrame.width)
toVC.viewBottomConstraint.constant = -(bounds.height - cellFrame.origin.y - cellFrame.height)
toVC.view.layoutIfNeeded()
再就是animation的部分了
// 4
let animator = UIViewPropertyAnimator(duration: 0.6, dampingRatio: 0.7) {
toVC.viewTopConstraint.constant = 0
toVC.viewLeftConstraint.constant = 0
toVC.viewRightConstraint.constant = 0
toVC.viewBottomConstraint.constant = 0
toVC.image.layer.cornerRadius = 0
toVC.imageHeightConstraint.constant = 550
toVC.view.layoutIfNeeded()
}
animator.startAnimation()
// 5
animator.addCompletion { (finished) in
transitionContext.completeTransition(true)
}
第四部分就是animation结束时需要的状态,也就是距离上下左右都是0
第五部分就是告诉UIKit我们结束了
这里我们结束了AnimationController的code部分,接下来我们需要再ViewController里面用到这个协议:
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return presentSectionViewController
}
}
并且修改didselect的function
fileprivate var presentSectionViewController = PresentSectionViewController()
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let controller = AppDetailController()
controller.transitioningDelegate = self
let attributes = collectionView.layoutAttributesForItem(at: indexPath)!
let cellFrame = collectionView.convert(attributes.frame, to: view)
presentSectionViewController.cellFrame = cellFrame
self.present(controller, animated: true, completion: nil)
}
这下就好了,效果呢还是很接近的了。希望大家喜欢。