在iOS中,通常是使用UIVisualEffectView+UIBlurEffect实现毛玻璃效果,但是苹果并没有提供修改模糊度的API。网上很多都是修改其alpha值实现所谓“模糊度改变”的效果,这种只是自欺欺人的做法,并非真的修改了模糊度。
Demo地址:JPBlurView
实现毛玻璃动画
虽说没有修改模糊度的API,但是有毛玻璃的展示和隐藏动画:
let effectView = UIVisualEffectView(effect: nil)
// 毛玻璃展示动画
UIView.animate(withDuration: 0.3) {
effectView.effect = UIBlurEffect(style: systemThinMaterialDark)
}
// 毛玻璃隐藏动画
UIView.animate(withDuration: 0.3) {
effectView.effect = nil
}
只要对UIVisualEffectView的effect属性的setter方法包裹一层动画,就可以实现毛玻璃动画了:
控制毛玻璃动画进度
仔细观察,毛玻璃效果从无到有的这个过程,不就是模糊度从0到1吗?那么只要能控制这个毛玻璃动画的进度,就相当于自定义模糊度了。这种情况就是UIViewPropertyAnimator登场的时候了。
UIViewPropertyAnimator是iOS中的一个动画类,用于创建和管理视图动画。它提供了一种强大且灵活的方式来创建自定义的交互式动画:可以定义动画的起始状态、结束状态和持续时间,还可以通过控制动画的进度来实现交互性。
更多的介绍这里就不赘述了,网上很多资料可以参考,这里简单实现一下:
let effectView = UIVisualEffectView(effect: nil)
animator = UIViewPropertyAnimator(duration: 0, curve: .linear, animations: { [weak effectView] in
effectView?.effect = UIBlurEffect(style: systemThinMaterialDark)
})
animator.fractionComplete = 0
这样,通过修改animator的fractionComplete属性从而实现可自定义模糊度的毛玻璃效果了:
实现可自定义模糊度动画
虽然UIViewPropertyAnimator可以控制动画进度,但是并不能实现自定义模糊度的动画。
例如如果想通过UIViewPropertyAnimator实现模糊度0.2~0.7的动画是不行的,一旦动画开始了就会直达1.0,不能执行到某个值自动停下,除非对fractionComplete属性进行监听,我个人觉得这种做法很麻烦。
有没有一种模拟动画并且能返回动画进度的方法呢?这样我就能通过这个实时动画进度更新fractionComplete属性就可以了。
很幸运,有!那就是pop!
POPPropertyAnimation有个POPAnimatableProperty属性,可以实时监听动画的进度,而且是带有动画曲线的进度,并且不需要真的对某个视图添加动画,简单来说就是能模拟一个动画,十分好用。
我这里选择使用POPBasicAnimation来实现,因为POPBasicAnimation能自定义动画时长和动画曲线,能满足基本的动画效果:
// 创建一个基类专门来监听进度
let animObserver = NSObject()
let animation = POPBasicAnimation()
animation.fromValue = 0.2
animation.toValue = 0.7
animation.duration = 0.3
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
// 监听实时动画进度
animation.property = POPAnimatableProperty.property(withName: "JPProperty") { [weak animator] prop in
prop?.writeBlock = { (_, values) in
guard let values = values else { return }
let value = values[0]
// 刷新模糊度
animator?.fractionComplete = value
}
} as? POPAnimatableProperty
// 开始(模拟)动画
animObserver.pop_add(animation, forKey: "JPAnimation")
这样就能实现可自定义模糊度动画了:
需要注意的地方
- 一旦使用
UIViewPropertyAnimator暂停了动画(没有开启或还没结束),必须在退出页面前让动画结束,否则会崩溃!
animator.stopAnimation(true)
- 同样,如果使用
UIViewPropertyAnimator暂停了动画,App一旦进入后台模式就会失效(挂起时不会,即双击Home键/上滑到App列表的时候),返回前台时需要重新设置一下:
@objc func willEnterForegroundHandle() {
guard animator.state != .active else { return }
animator.stopAnimation(true)
resetAnimator()
}
- 📢:经网友指点,在
iOS 11之后可以设置animator的pausesOnCompletion为true,使其在完成时自动暂停而不是转换到非活动状态,可以防止动画结束后被清理(App进入后台模式时会清理),这样就不需要返回前台时重置animator了。
实现可交互的毛玻璃背景
结合上面的做法,就可以在浏览大图的背景实现这种可交互的毛玻璃效果:
- 通过手指进行交互,根据手指滑动距离修改模糊度;
- 松开手指还原/关闭,动画实现模糊度从「0.x」到「1.0 / 0.0」的过程。
已经应用在项目的用户相册背景:
Demo地址:JPBlurView
在复用列表中的使用
经网友反馈,应用在UITableViewCell/UICollectionViewCell上模糊效果不稳定:反复滑动会有毛玻璃消失的情况。
经调试,貌似是苹果的复用机制会让UIViewPropertyAnimator直接失效。目前还没找到完美解决的方法,只能提供一个重置模糊效果的方法resetEffect,在复用Cell时对其进行重置:
// 需要在下一个runloop才能重置,否则会受到系统动画的影响(collectionView.reloadSections)导致失效
DispatchQueue.main.async {
self.blurView.resetEffect()
}
- 具体重置操作可以参考Demo中
CustomBlurCell的setModel方法。
不过如果是应用到UITableViewCell/UICollectionViewCell上或其他性能要求高的地方,建议使用VisualEffectView。