刚开始从 android 转到 ios 写应用时,发现 iOS 的界面之间切换真方便,什么都不用写都有不错的转场动画,除了简单的平移、底部弹出,甚至还有 3D 旋转和翻页效果,而 Android 那系统默认转场是屏幕闪一下。
尽管系统提供的方式已经满足大部分场景了,但也没法挡住审美疲劳啊,所以就需要自定义转场动画了。
Present&Dismiss Transition
先看个效果图:
刚开始写 iOS 时看到这种会动会弹的效果一直都是懵的,心里觉得这一定非常复杂,等到真正沉下心去学时,发现只要搞清楚每个步骤,这一点都不难。
1、实现 UIViewControllerTransitioningDelegate
比如我的第一个页面叫 FirstViewController,单击底部任意一个按钮都会以 present 方式打开 SecondViewController。点击按钮触发的代码如下:
1 2 3 4 5 6 7 8
| self.tappedButton = button let sb = UIStoryboard(name: "Main", bundle: nil) let vc = sb.instantiateViewController(withIdentifier: "SBSecond") vc.view.backgroundColor = button.backgroundColor vc.transitioningDelegate = self self.present(vc, animated: true, completion: nil)
|
实现 UIViewControllerTransitioningDelegate 的两个方法
1 2 3 4 5 6 7 8 9 10 11
| extension : UIViewControllerTransitioningDelegate { func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return nil } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return nil } }
|
暂时给 present 和 dismiss 都返回 nil
2、编写具体的转场动画
先分析效果图的中的打开动画,SecondViewController 是从当前点击按钮的位置和大小,放大的同时改变中心点,达到了最终的全屏显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| class FirstPresentTransition: NSObject, UIViewControllerAnimatedTransitioning { let duration = 1.0 var fromFrame = CGRect.zero func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return duration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let containerView = transitionContext.containerView let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)! let toFrame = toView.frame let scaleX = fromFrame.size.width / toFrame.size.width let scaleY = fromFrame.size.height / toFrame.size.height toView.transform = toView.transform.scaledBy(x: scaleX, y: scaleY) toView.center = CGPoint(x: fromFrame.midX, y: fromFrame.midY) containerView.addSubview(toView) UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0, options: [], animations: { toView.transform = CGAffineTransform.identity toView.center = CGPoint(x: toFrame.midX, y: toFrame.midY) }) { (_) in transitionContext.completeTransition(true) } } }
|
再看 dismiss 动画。SecondViewController 由全屏缩小一半到屏幕中心,再向上平移到屏幕外。
1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class SecondDismissTransition: NSObject, UIViewControllerAnimatedTransitioning { let duration = 1.0 func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return duration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let containerView = transitionContext.containerView let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)! let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)! containerView.insertSubview(toView, belowSubview: fromView) UIView.animate(withDuration: duration / 2, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 10, options: [], animations: { fromView.transform = fromView.transform.scaledBy(x: 0.5, y: 0.5) }) { (_) in UIView.animate(withDuration: self.duration / 2, animations: { fromView.frame.origin.y -= containerView.frame.size.height }, completion: { (_) in transitionContext.completeTransition(true) }) } } }
|
present 和 dismiss 的转场动画写完了,就该把开始 return nil 的地方替换掉了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| extension : UIViewControllerTransitioningDelegate { func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { let transition = FirstPresentTransition() transition.fromFrame = self.tappedButton.frame return transition } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return SecondDismissTransition() } }
|
我添加的 3 个按钮都是固定位置的,并没有滚动条,所以可以直接使用。但如果是在 UIScrollView、UITableView 或 UICollectionView 中,那就需要转换一下 frame 了。
Push&Pop Transition
既然 Present 和 Dismiss 的转场可以自定义,那 UINavigationController 的 Push 和 Pop 肯定也是可以的,实现同样的效果,动画代码可以用同一份,只需设置不同的 delegate 即可。
给 FirstViewController 套一个 UINavigationController,按钮触发的代码修改为:
1 2 3 4 5 6 7
| self.tappedButton = button let sb = UIStoryboard(name: "Main", bundle: nil) let vc = sb.instantiateViewController(withIdentifier: "SBSecond") vc.view.backgroundColor = button.backgroundColor self.navigationController?.delegate = self self.navigationController?.pushViewController(vc, animated: true)
|
实现 UINavigationControllerDelegate:
1 2 3 4 5 6 7 8 9 10 11 12 13
| extension : UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { if operation == .push { let transition = FirstPresentTransition() transition.fromFrame = self.tappedButton.frame return transition } else { return SecondDismissTransition() } } }
|
就这么简单
以上是关于iOS的主要内容,如果未能解决你的问题,请参考以下文章
iOS xcode 代码片段
iOS常用于显示几小时前/几天前/几月前/几年前的代码片段
java内存流:java.io.ByteArrayInputStreamjava.io.ByteArrayOutputStreamjava.io.CharArrayReaderjava.io(代码片段
iOS开发CGRectGetMidX. CGRectGetMidY.CGRectGetMinY. CGRectGetMaxY. CGRectGetMinX. CGRectGetMaxX的使用(代码片段
iOS开发CGRectGetMidX. CGRectGetMidY.CGRectGetMinY. CGRectGetMaxY. CGRectGetMinX. CGRectGetMaxX的使用(代码片段
java缓冲字符字节输入输出流:java.io.BufferedReaderjava.io.BufferedWriterjava.io.BufferedInputStreamjava.io.(代码片段