【iOS】实现可自定义模糊度的毛玻璃及动画效果

7,099 阅读4分钟

在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
}

只要对UIVisualEffectVieweffect属性的setter方法包裹一层动画,就可以实现毛玻璃动画了:

effect.gif

控制毛玻璃动画进度

仔细观察,毛玻璃效果从无到有的这个过程,不就是模糊度从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

这样,通过修改animatorfractionComplete属性从而实现可自定义模糊度的毛玻璃效果了:

JPBlurView_1.gif

实现可自定义模糊度动画

虽然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")

这样就能实现可自定义模糊度动画了:

JPBlurAnimationView_1.gif

需要注意的地方

  1. 一旦使用UIViewPropertyAnimator暂停了动画(没有开启或还没结束),必须在退出页面前让动画结束,否则会崩溃!
animator.stopAnimation(true)
  1. 同样,如果使用UIViewPropertyAnimator暂停了动画,App一旦进入后台模式就会失效(挂起时不会,即双击Home键/上滑到App列表的时候),返回前台时需要重新设置一下:
@objc func willEnterForegroundHandle() {
    guard animator.state != .active else { return }
    animator.stopAnimation(true)
    resetAnimator()
}
  • 📢:经网友指点,在iOS 11之后可以设置animatorpausesOnCompletiontrue,使其在完成时自动暂停而不是转换到非活动状态,可以防止动画结束后被清理(App进入后台模式时会清理),这样就不需要返回前台时重置animator了。

实现可交互的毛玻璃背景

结合上面的做法,就可以在浏览大图的背景实现这种可交互的毛玻璃效果:

JPBlurAnimationView_2.gif

  • 通过手指进行交互,根据手指滑动距离修改模糊度;
  • 松开手指还原/关闭,动画实现模糊度从「0.x」到「1.0 / 0.0」的过程。

已经应用在项目的用户相册背景:

jp_gif_file 5.GIF

Demo地址:JPBlurView

在复用列表中的使用

经网友反馈,应用在UITableViewCell/UICollectionViewCell上模糊效果不稳定:反复滑动会有毛玻璃消失的情况。

经调试,貌似是苹果的复用机制会让UIViewPropertyAnimator直接失效。目前还没找到完美解决的方法,只能提供一个重置模糊效果的方法resetEffect,在复用Cell时对其进行重置:

// 需要在下一个runloop才能重置,否则会受到系统动画的影响(collectionView.reloadSections)导致失效
DispatchQueue.main.async {
    self.blurView.resetEffect()
}

JPBlurView_2.gif

  • 具体重置操作可以参考Demo中CustomBlurCellsetModel方法。

不过如果是应用到UITableViewCell/UICollectionViewCell上或其他性能要求高的地方,建议使用VisualEffectView