iOS-Swift知识之 UIView基础动画详解和效果展示

2,114 阅读7分钟

1 背景

开发中,经常会遇到要做一些动画效果,对于一些简单的动画,我们可以通过UIView基础动画来实现。

UiView动画是基于高层API封装进行封装的,对UIView的属性进行修改时候所产生的动画。

2 基本动画

2.1 animate(withDuration duration: TimeInterval, animations: @escaping () -> Void)

这是最常用的一个方法,duration参数表示动画时间的时间,是TimeInterval类型,其实就是一个Double类型,而animations是一个闭包,用来修改view的一些属性,支持修改的属性有frame、bounds、center、transform、alpha、backgroundColor、contentStretch。

2.2 animate(withDuration duration: TimeInterval, animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil)

这也是最常用的方法之一,跟上面的方法类似,只是多了一个completion闭包参数,可以用来在动画完成后做一些处理。示例如下:

let view0 = UIView(frame: CGRect(x: 0, y: 70, width: 100, height: 100))
view0.backgroundColor = .blue
view.addSubview(view0)
UIView.animate(withDuration: 2) {
    view0.frame = CGRect(x: 150, y: 150, width: 100, height: 100)
} completion: { isFinish in
    if isFinish {
        view0.backgroundColor = .red
    }
}

2.3 animate(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.AnimationOptions = [], animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil)

这个方法相比之前的方法,多了两个参数,其中delay表示延迟的时间,即这个动画延迟多久后执行,而options参数表示动画的效果,其定义和注释如下

/**********************动画效果相关**********************/
/// 提交动画的时候布局子控件,表示子控件将和父控件一同动画。
static var layoutSubviews: UIView.AnimationOptions
    
/// 动画时允许用户交流,比如触摸
static var allowUserInteraction: UIView.AnimationOptions

/// 从当前状态开始动画
static var beginFromCurrentState: UIView.AnimationOptions

/// 动画无限重复
static var `repeat`: UIView.AnimationOptions

/// 执行动画回路,前提是设置动画无限重复
static var autoreverse: UIView.AnimationOptions

/// 忽略外层动画嵌套的执行时间
static var overrideInheritedDuration: UIView.AnimationOptions

/// 忽略外层动画嵌套的时间变化曲线
static var overrideInheritedCurve: UIView.AnimationOptions 

/// 通过改变属性和重绘实现动画效果,如果key没有提交动画将使用快照
static var allowAnimatedContent: UIView.AnimationOptions

/// 用显隐的方式替代添加移除图层的动画效果
static var showHideTransitionViews: UIView.AnimationOptions

/// 忽略嵌套继承的选项
static var overrideInheritedOptions: UIView.AnimationOptions

/**********************时间函数曲线相关**********************/
/// 时间曲线函数,由慢到快
static var curveEaseInOut: UIView.AnimationOptions
    
/// 时间曲线函数,由慢到特别快
static var curveEaseIn: UIView.AnimationOptions

/// 时间曲线函数,由快到慢
static var curveEaseOut: UIView.AnimationOptions

/// 时间曲线函数,匀速
static var curveLinear: UIView.AnimationOptions

/**********************转场动画相关**********************/
/// 转场从左翻转
static var transitionFlipFromLeft: UIView.AnimationOptions

/// 转场从右翻转
static var transitionFlipFromRight: UIView.AnimationOptions

/// 上卷转场
static var transitionCurlUp: UIView.AnimationOptions

/// 下卷转场
static var transitionCurlDown: UIView.AnimationOptions

/// 转场交叉消失
static var transitionCrossDissolve: UIView.AnimationOptions

/// 转场从上翻转
static var transitionFlipFromTop: UIView.AnimationOptions

/// 转场从下翻转
static var transitionFlipFromBottom: UIView.AnimationOptions

/**********************刷新频率相关**********************/
/// 指定动画刷新频率为60fps
static var preferredFramesPerSecond60: UIView.AnimationOptions

/// 指定动画刷新频率为30fps
static var preferredFramesPerSecond30: UIView.AnimationOptions

官方提供了很多动画效果,这里只展示几个,其它感兴趣的可以自己去尝试运用。

private func getLabel(frame:CGRect, text:String) -> UILabel {
    let label = UILabel(frame: frame)
    label.font = .systemFont(ofSize: 15)
    label.backgroundColor = .blue
    label.textColor = .white
    label.textAlignment = .center
    label.text = text
    return label
}

let label0 = getLabel(frame: CGRect(x: 0, y: 70, width: 130, height: 30), text: "curveEaseIn")
view.addSubview(label0)
UIView.animate(withDuration: 2, delay: 1, options: [.repeat, .curveEaseIn]) {
    label0.frame = CGRect(x: 250, y: 70, width: 130, height: 30)
}
        
let label1 = getLabel(frame: CGRect(x: 0, y: 120, width: 130, height: 30), text: "autoreverse")
view.addSubview(label1)
UIView.animate(withDuration: 2, delay: 1, options: [.repeat, .autoreverse]) {
    label1.frame = CGRect(x: 250, y: 120, width: 130, height: 30)
}

let label2 = getLabel(frame: CGRect(x: 0, y: 170, width: 130, height: 30), text: "curveEaseOut")
view.addSubview(label2)
UIView.animate(withDuration: 2, delay: 1, options: [.repeat, .curveEaseOut]) {
    label2.frame = CGRect(x: 250, y: 170, width: 130, height: 30)
}

3 弹簧动画

弹簧动画的方法

@available(iOS 7.0, *)
open class func animate(withDuration duration: TimeInterval, 
                        delay: TimeInterval, 
                        usingSpringWithDamping dampingRatio: CGFloat, 
                        initialSpringVelocity velocity: CGFloat, 
                        options: UIView.AnimationOptions = [], 
                        animations: @escaping () -> Void, 
                        completion: ((Bool) -> Void)? = nil)

其中duration表示动画时间,delay表示动画延迟时间,dampingRatio表示弹簧的阻尼,如果为1动画则平稳减速动画没有振荡。 这里值为 0~1,velocity表示弹簧的速率,数值越小,动力越小,弹簧的拉伸幅度就越小。反之相反。比如:总共的动画运行距离是200 pt,你希望每秒 100pt 时,值为 0.5。示例如下

let label3 = getLabel(frame: CGRect(x: 20, y: 70, width: 120, height: 120), text: "dampingRatio = 0.1\nvelocity = 5")
view.addSubview(label3)
UIView.animate(withDuration: 3, delay: 1, usingSpringWithDamping: 0.1, initialSpringVelocity: 5, options: [.curveEaseIn]) {
    label3.frame = CGRect(x: 20, y: 370, width: 120, height: 120)
} completion: { isFinish in
     if isFinish {
         label3.backgroundColor = .red
     }
}
        
let label4 = getLabel(frame: CGRect(x: 160, y: 70, width: 120, height: 120), text: "dampingRatio = 0.3\nvelocity = 3")
view.addSubview(label4)
UIView.animate(withDuration: 3, delay: 1, usingSpringWithDamping: 0.3, initialSpringVelocity: 3, options: [.curveEaseIn]) {
    label4.frame = CGRect(x: 160, y: 370, width: 120, height: 120)
} completion: { isFinish in
     if isFinish {
         label4.backgroundColor = .red
     }
}
        
let label5 = getLabel(frame: CGRect(x: 300, y: 70, width: 120, height: 120), text: "dampingRatio = 0.5\nvelocity = 1")
view.addSubview(label5)
UIView.animate(withDuration: 3, delay: 1, usingSpringWithDamping: 0.5, initialSpringVelocity: 1, options: [.curveEaseIn]) {
    label5.frame = CGRect(x: 300, y: 370, width: 120, height: 120)
} completion: { isFinish in
     if isFinish {
         label5.backgroundColor = .red
     }
}

4 过渡动画

4.1 transition(with view: UIView, duration: TimeInterval, options: UIView.AnimationOptions = [], animations: (() -> Void)?, completion: ((Bool) -> Void)? = nil)

其中view表示要执行过渡动画的视图,示例如下

let view1 = UIView(frame: CGRect(x: 50, y: 70, width: 100, height: 100))
view1.center = view.center
view1.backgroundColor = .blue
view.addSubview(view1)
        
/// 刚刚view.addSubview(view1)后,如果不延迟后执行,options设置的动画效果无效
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
    UIView.transition(with: view1, duration: 3, options: [.transitionCurlUp]) {
        view1.backgroundColor = .yellow
    } completion: { isFinish in
         if isFinish {
             view1.backgroundColor = .red
         }
    }
}

4.2 transition(from fromView: UIView, to toView: UIView, duration: TimeInterval, options: UIView.AnimationOptions = [], completion: ((Bool) -> Void)? = nil)

其中fromView表示一开始的视图,toView表示转换后的视图,示例如下

let contentView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
contentView.backgroundColor = .brown
contentView.center = view.center
view.addSubview(contentView)

let view2 = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view2.backgroundColor = .blue
view2.center = contentView.center
contentView.addSubview(view2)

let view3 = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view3.backgroundColor = .yellow
        
/// 刚刚view.addSubview(view1)后,如果不延迟后执行,options设置的动画效果无效
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
            
    /// 动画变化的是view2的superView,这里view2是加在contentView上,所以动画变化的是contentView
    /// 如果view2是加直接加在self.view上,则整个self.view执行动画效果
    UIView.transition(from: view2, to: view3, duration: 3, options: [.transitionFlipFromLeft]) { isFinish in
           if isFinish {
               view3.backgroundColor = .red
           }
    }
}

5 关键帧动画

关键帧动画的方法

@available(iOS 7.0, *)
open class func animateKeyframes(withDuration duration: TimeInterval, 
                                 delay: TimeInterval, 
                                 options: UIView.KeyframeAnimationOptions = [], 
                                 animations: @escaping () -> Void, 
                                 completion: ((Bool) -> Void)? = nil)

@available(iOS 7.0, *)
open class func addKeyframe(withRelativeStartTime frameStartTime: Double, 
                            relativeDuration frameDuration: Double, 
                            animations: @escaping () -> Void)

其中frameStartTime表示相对开始时间,frameDuration表示相对持续时间,options是枚举,定义和内容如下

/// 提交动画的时候布局子控件,表示子控件将和父控件一同动画。
static var layoutSubviews: UIView.KeyframeAnimationOptions

/// 动画时允许用户交流,比如触摸
static var allowUserInteraction: UIView.KeyframeAnimationOptions

/// 从当前状态开始动画
static var beginFromCurrentState: UIView.KeyframeAnimationOptions

/// 动画无限重复
static var `repeat`: UIView.KeyframeAnimationOptions

/// 执行动画回路,前提是设置动画无限重复
static var autoreverse: UIView.KeyframeAnimationOptions

/// 忽略外层动画嵌套的执行时间
static var overrideInheritedDuration: UIView.KeyframeAnimationOptions

/// 忽略嵌套继承的�选项
static var overrideInheritedOptions: UIView.KeyframeAnimationOptions

/**********************关键帧动画独有**********************/ 
/// 选择使用一个简单的线性插值计算的时候关键帧之间的值。   
static var calculationModeLinear: UIView.KeyframeAnimationOptions

/// 选择不插入关键帧之间的值,而是直接跳到每个新的关键帧的值。
static var calculationModeDiscrete: UIView.KeyframeAnimationOptions

/// 选择计算中间帧数值算法使用一个简单的节奏。这个选项的结果在一个均匀的动画。
static var calculationModePaced: UIView.KeyframeAnimationOptions

/// 选择计算中间帧使用默认卡特莫尔罗花键,通过关键帧的值。你不能调整该算法的参数。 这个动画好像会更圆滑一些..
static var calculationModeCubic: UIView.KeyframeAnimationOptions

/// 选择计算中间帧使用立方计划而忽略的时间属性动画。相反,时间参数计算隐式地给动画一个恒定的速度。
static var calculationModeCubicPaced: UIView.KeyframeAnimationOptions

示例如下

let view4 = UIView(frame: CGRect(x: 20, y: 70, width: 50, height: 50))
view4.backgroundColor = .orange
view.addSubview(view4)
UIView.animateKeyframes(withDuration: 3, delay: 3, options: [.repeat, .autoreverse]) {
    UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.3) {
        view4.frame = CGRect(x: 20, y: 100, width: 100, height: 100)
    }
    UIView.addKeyframe(withRelativeStartTime: 0.3, relativeDuration: 0.5) {
        view4.frame = CGRect(x: 120, y: 100, width: 150, height: 150)
    }
    UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.6) {
        view4.frame = CGRect(x: 150, y: 150, width: 200, height: 200)
    }
    UIView.addKeyframe(withRelativeStartTime: 0.6, relativeDuration: 0.8) {
        view4.frame = CGRect(x: 180, y: 200, width: 180, height: 180)
    }
    UIView.addKeyframe(withRelativeStartTime: 0.8, relativeDuration: 0.7) {
        view4.frame = CGRect(x: 200, y: 240, width: 90, height: 90)
    }
} completion: { isFinish in
     if isFinish {
         view4.backgroundColor = .red
     }
}

如果options使用了.calculationModePaced,即options: [.repeat, .autoreverse, .calculationModePaced],效果如下

可以看出,整个动画的过程更加均匀流畅,其他效果,可以自己尝试。

6 移除动画

移除动画的方法

open class func perform(_ animation: UIView.SystemAnimation, on views: [UIView], options: UIView.AnimationOptions = [], animations parallelAnimations: (() -> Void)?, completion: ((Bool) -> Void)? = nil)

其中UIView.SystemAnimation是个枚举,只有一个删除的值

public enum SystemAnimation : UInt, @unchecked Sendable {
    
    case delete = 0 // removes the views from the hierarchy when complete
}

views操作的view,这会让那个视图变模糊、收缩和褪色,之后再给它发送removeFromSuperview方法。

示例如下

let view5 = UIView(frame: CGRect(x: 20, y: 70, width: 50, height: 50))
view5.backgroundColor = .orange
let view6 = UIView(frame: CGRect(x: 40, y: 90, width: 200, height: 200))
view6.backgroundColor = .brown
view.addSubview(view5)
view.addSubview(view6)
UIView.perform(.delete, on: [view5, view6], options: .curveEaseInOut) {
    view5.backgroundColor = .blue
    view6.backgroundColor = .yellow
}