Objective-C & Swift的 自定义的转场动画

说到动画,就得说说transform属性了。

1.所有的控件都有这个属性;
2.transform能实现控件的平移、缩放、旋转的动画效果。

下面是一个仿照push控制器效果的modal操作:
管理者简介:

.h文件:


CKPresentationManager.h.png
这个是转场动画的管理者类,这个头文件中包含了两个类:
1.CKPresentationManager : 转场动画的管理者;
2.CKPresentationController: 这个类是基于系统的 UIPresentationController 类。

.m 文件:(下面3张图片都在这个.m文件中)


图一.png
图二.png
图三.png
这个.m文件中包含了两个类的实现 , 下面就来好好解读一下。

正文:

<一>首先谈谈 UIPresentationController 这个系统提供的类:

1.UIPresentationController是提供高级视图切换的类。它让管理present ViewController的过程变得简单;
2.通过重写该类,在子类中重写 - (void)containerViewDidLayoutSubviews 方法,可以修改弹出视图的frame,同时获取到两个非常重要的对象: containerView 和 - (nullable UIView *)presentedView ;
3.containerView :是转场动画呈现视图的容器视图,所呈现的所有控件都是添加到这个容器视图上的;
4.通过 - (nullable UIView *)presentedView方法可以拿到我们眼睛很直观所需要看到的视图,并可以对其进行操作更改。

@interface CKPresentationController ()

@property (nonatomic, strong) UIButton * coverBtn;

@end

@implementation CKPresentationController

- (UIButton *)coverBtn {
    if (!_coverBtn) {
        _coverBtn = [UIButton buttonWithType:UIButtonTypeCustom];
        _coverBtn.frame = [UIScreen mainScreen].bounds;
        [_coverBtn addTarget:self action:@selector(coverBtnClickEvent) forControlEvents:UIControlEventTouchUpInside];
    }
    return _coverBtn;
}

/**
 *   重写系统方法
 *   用于布局转场动画弹出的控件,且只调一次
 */
- (void)containerViewDidLayoutSubviews {
    //  containerView :非常重要,转场动画呈现视图的容器视图,转场动画呈现的所有控件,都是添加到这个容器视图上的
    //  - (nullable UIView *)presentedView :该方法非常重要,通过该方法可以拿到弹出的视图
    self.presentedView.frame = CGRectMake(0, 20, 200, 200);
    CGPoint center = self.presentedView.center;
    center.x = [UIScreen mainScreen].bounds.size.width * 0.5;
    self.presentedView.center = center;
    
    //  添加蒙版 - 便于点击空白处让视图消失
    [self.containerView insertSubview:self.coverBtn atIndex:0];
}

- (void)coverBtnClickEvent {
    //  让菜单消失
    [self.presentedViewController dismissViewControllerAnimated:true completion:nil];
}

@end
<二>再来谈谈 CKPresentationManager这个类:

1.CKPresentationManager :这个类集成NSObject,主要是作为转场动画的代理对象,将整个转场动画封装成一个独立的类,便于下次的使用,已经代码的可读性。
2.CKPresentationManager 遵循了两个协议:
<UIViewControllerTransitioningDelegate,UIViewControllerAnimatedTransitioning>
3.UIViewControllerTransitioningDelegate协议:遵循该协议,主要是为了实现3个方法

/**
 *    该方法返回一个赋值转场动画的对象
 *    CKPresentationController:可以在该对象中控制弹出视图的尺寸等
 */
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0) {
    return [[CKPresentationController alloc] initWithPresentedViewController:presented presentingViewController:presenting];
}
/**
 *    该方法用于返回一个负责转场动画如何 出现 的对象,需要实现相对应的代理方法
 */
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    NSLog(@"呈现");
    _isPresent = YES;
    return self;
}
/**
 *    该方法用于返回一个负责转场动画如何 消失 的对象,需要实现相对应的代理方法
 */
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    NSLog(@"消失");
    _isPresent = NO;
    return self;
}

4.UIViewControllerAnimatedTransitioning协议:遵循该协议,主要是为了实现2个方法

/**
 *    该方法用于返回展示和消失动画的时长
 */
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
    return 0.5;
}
/**
 *    专门用于管理modal如何展示和消失,而且无论展现或者消失都会调用这个方法;
 *    注意:
 *       1.只有实现了这个代理方法,系统才不会使用默认的动画了;
 *       2.也就是说系统不会再添加从下至上的动画,所有的动画操作都需要我们自己去实现,包括需要展示的视图,也需要我们自己添加到 容器视图(containerView)上;
 *       3.transitionContext : 所有动画需要的东西都保存在这个上下文中,可以通过 transitionContext 获取我们想要的东西。
 */
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    
    if (_isPresent) {
        //  展示
        //  获取需要弹出的视图
        UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
        //  将需要弹出的视图添加到容器视图上
        [transitionContext.containerView addSubview:toView];
        //  执行动画
        //toView.transform = CGAffineTransformMakeScale(1.0, 0.0);
        //  MakeTranslation : 基于最开始的位置形变,每一次形变都会把之前的形变清空,重新从最开始的位置形变
        toView.transform = CGAffineTransformMakeTranslation(-WIDTH, 0);
        //  动画是添加layer上的,所以涉及到需要设置锚点的概念,否则就是从展示的中间向两边展示,锚点默认为(0.5,0.5)
        toView.layer.anchorPoint = CGPointMake(0.5, 0.0);
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            //  清空transform属性
            toView.transform = CGAffineTransformIdentity;
        } completion:^(BOOL finished) {
            //  注意:自定义的转场动画,在执行完毕后,一定要手动告诉系统
            [transitionContext completeTransition:true];
        }];
    } else {
        //  消失
        //  获取需要消失的视图
        UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
        //  执行动画让视图消失
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            //
            //fromView.transform = CGAffineTransformMakeScale(1.0, 1.0);
            fromView.transform = CGAffineTransformMakeTranslation(WIDTH, 0);
        } completion:^(BOOL finished) {
            //  注意:自定义的转场动画,在执行完毕后,一定要手动告诉系统
            [transitionContext completeTransition:true];
        }];
    }
}

其中值得注意的地方是,动画是添加到layer上,这就需要引入一个概念 - 锚点 :
锚点:( anchorPoint)
1.anchorPoint点(锚点)的值是用相对bounds的比例值来确定的. 例如(0,0), (1,1)分别表示左上角、右下角,依此类推;
2.锚点都是对于自身来讲的. 确定自身的锚点,通常用于做相对的tranform变换.当然也可以用来确定位置;
3.锚点属性的类型为CGPoint,其默认值为(0.5, 0.5)。

最后,就是这个类的使用了:
ViewController.m.png

在使用过程中,需要注意的是:
1.在实例化CKPresentationManager管理者时,必须要强引用该类,否则就可能造成该管理者对象的提前释放;
2.需要modal的控制器的modalPresentationStyle属性必须设置为UIModalPresentationCustom,否则就不是自定义的转场动画,且modal出控制器后,就会将原有的控制器移除。

--- OC End ---

=============================================================================
--- Swift Start ---
思路跟OC是一样的,直接上代码:
CKPresentationManager管理者类:

import UIKit

class CKPresentationManager: NSObject, UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning
{
    //  定义标记记录当前是否是展现
    var isPresent = false
    
    //// MARK: - UIViewControllerTransitioningDelegate
    //  该方法用于返回一个负责转场动画的对象
    //  可以在该对象中控制弹出视图的尺寸等
    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        /// 自定义的UIPresentationController
        return CKPresentationController(presentedViewController: presented, presenting: presenting)
    }
    //  该方法用于返回一个负责转场如何出现的对象
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        NJLog(message: "展现")
        isPresent = true
        return self
    }
    //  该方法用于返回一个负责转场如何消失的对象
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        NJLog(message: "消失")
        isPresent = false
        return self
    }

    //// MARK: - UIViewControllerAnimatedTransitioning
    //  告诉系统展现和消失的动画时长 , 暂时用不上
    public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5
    }
    
    //  专门用于管理modal如何展现和消失的,无论是展现还是消失都会调用该方法
    //  注意: 只要我们实现了这个代理方法,那么系统就不会再有默认的动画了;也就是说默认的modal从下自上的移动,系统也不会再帮我们添加了,所有的动画操作都需要我们自己去实现,包括需要展现的视图,也需要我们自己添加到容器视图上(containerView)
    //  transitionContext: 所有的动画需要的东西都保存在这个上下文中,换而言之就是可以通过transitionContext获取到我们想要的东西
    public func animateTransition(using transitionContext:UIViewControllerContextTransitioning) {
        
        //  判断当前是展现还是消失
        if isPresent {
            //  获取需要弹出的视图
            //        let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
            //        let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
            
            //  通过ToViewKey取出的就是toVC对应的view
            guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else {
                return
            }
            
            //  将需要弹出的视图添加到containerView上
            transitionContext.containerView.addSubview(toView)
            
            //  执行动画
            toView.transform = CGAffineTransform(scaleX: 1.0, y: 0.0)
            //  设置锚点,如果不设置就是从view的中心点,上下展开
            toView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
            UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
                () -> Void in
                //  清空transform属性
                toView.transform = CGAffineTransform.identity
                //  用下划线当作参数时,表示忽略这个值
            }) { (_) -> Void in
                //  注意:自定义转场动画,在执行完动画之后,一定要告诉系统动画执行完毕了
                transitionContext.completeTransition(true)
            }
        } else {
            //  通过FromViewKey取出的就是fromVC对应的view
            guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else {
                return
            }
            //  执行动画让视图消失
            UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
                () -> Void in
                //  containerView突然消失,是因为CGFloat不准确,导致无法执行动画,遇到这样的问题只需要将CGFloat的值设置为一个很小的值即可
                fromView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
            }, completion: { (_) -> Void in
                transitionContext.completeTransition(true)
            })
        }
    }
}

class CKPresentationController: UIPresentationController {
    
    //  用于布局转场动画弹出的控件,只调一次
    override func containerViewDidLayoutSubviews() {
        //  containerView : 非常重要,转场动画呈现视图的容器view,所有的modal出来的视图都是添加到containerView上
        //  presentedView()  : 非常重要,通过该方法能够拿到弹出的视图
        presentedView?.frame = CGRect(x: 0, y: 55, width: 200, height: 200)
        presentedView?.center.x = UIScreen.main.bounds.width * 0.5
        
        //  添加蒙版view
        containerView?.insertSubview(coverbtn, at: 0)
        coverbtn.addTarget(self, action: #selector(coverBtnClick), for: UIControlEvents.touchUpInside)
    }
    
    //  MARK: - 懒加载
    private lazy var coverbtn: UIButton = {
        let btn = UIButton()
        btn.frame = UIScreen.main.bounds
        return btn
    }()
    
    //  btn点击事件
    @objc private func coverBtnClick() {
        //  让菜单消失
        presentedViewController.dismiss(animated: true, completion: nil)
    }
}

该管理者类在控制器中的使用:

//  对转场动画manager的懒加载
    private lazy var animatorManager = CKPresentationManager()
//  自定义转场动画
//  自定义的转场动画时,modal出控制器后,不会把后面的控制器移除
//  如果不自定义转场动画时,modal出来的控制器,会把原有的控制器移除
//  如果不自定义转场动画时,modal出来的控制器的尺寸与屏幕一样
//  如果自定义转场动画时,modal出来的控制器的尺寸,可以在自己的containerViewDidLayoutSubviews()方法中控制
//  设置转场代理
    menuView.transitioningDelegate = animatorManager
 //  设置转场动画的样式
    menuView.modalPresentationStyle = UIModalPresentationStyle.custom
 //  弹出菜单
    present(menuView, animated: true, completion: nil)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容