如何通过UIViewPropertyAnimator实现导航栏大标题模式的交互式滚动切换动画?
实现UIScrollView滚动时导航栏大标题的交互式切换动画
嘿,我看了你这段用UIViewPropertyAnimator实现导航栏滚动切换的代码,问题出在没有真正利用到UIViewPropertyAnimator的交互式驱动能力,而且直接修改largeTitleDisplayMode的方式也没法产生平滑的过渡动画。我来给你拆解问题,再给出修改后的实现:
现有代码的核心问题
- 每次滚动触发就添加动画并立即启动,本质还是瞬间切换,没有实现跟着滚动进度实时变化的交互式效果
largeTitleDisplayMode本身不是可动画属性,直接在动画块里修改它不会产生平滑过渡,layoutIfNeeded()也救不了这个问题- 重复复用同一个animator但每次都添加新动画,会导致动画队列混乱,出现跳变
正确的实现思路
我们需要用UIViewPropertyAnimator的暂停+进度驱动特性,把scrollView的滚动偏移量转化为动画的完成进度(fractionComplete),让导航栏的切换完全跟着滚动手势走,实现真正的交互式动画。
修改后的完整代码
首先定义几个辅助变量:
// 专门用于导航栏切换的动画器 private var navBarAnimator: UIViewPropertyAnimator? // 记录上次滚动偏移量,用来判断滚动方向 private var lastContentOffset: CGFloat = 0 // 滚动超过这个阈值才开始触发过渡动画 private let transitionThreshold: CGFloat = 20
然后修改scrollViewDidScroll方法:
func scrollViewDidScroll(_ scrollView: UIScrollView) { let currentOffset = scrollView.contentOffset.y let scrollDelta = currentOffset - lastContentOffset lastContentOffset = currentOffset // 初始化动画器(首次滚动或重置后创建) if navBarAnimator == nil { navBarAnimator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) { [weak self] in guard let self = self else { return } // 根据滚动方向设置最终状态 if scrollDelta > 0 { // 向上滚动:切换到小标题模式 self.navigationItem.largeTitleDisplayMode = .never } else { // 向下滚动到顶部:切换到大标题模式 self.navigationItem.largeTitleDisplayMode = .always } // 强制导航栏更新布局 self.navigationController?.navigationBar.layoutIfNeeded() } // 先暂停动画,准备用滚动进度驱动 navBarAnimator?.pauseAnimation() } // 计算动画完成进度 var animationFraction: CGFloat = 0 if scrollDelta > 0 { // 向上滚动:进度从0到1(从不切换到完全切换为小标题) animationFraction = min(1.0, max(0.0, (currentOffset - transitionThreshold) / transitionThreshold)) } else { // 向下滚动:进度从1到0(从小标题切回大标题) animationFraction = max(0.0, min(1.0, 1.0 - (abs(currentOffset) + transitionThreshold) / transitionThreshold)) } // 把滚动进度同步给动画器,实现实时过渡 navBarAnimator?.fractionComplete = animationFraction // 当滚动停止/超出阈值时,自动完成剩余动画,避免停在中间状态 if abs(scrollDelta) > transitionThreshold || !scrollView.isDragging || !scrollView.isDecelerating { let finalFraction: CGFloat = scrollDelta > 0 ? 1.0 : 0.0 navBarAnimator?.continueAnimation(withTimingParameters: nil, durationFactor: 0) navBarAnimator?.fractionComplete = finalFraction // 重置动画器,下次滚动重新创建 navBarAnimator = nil } }
额外注意事项
- 记得在
viewDidLoad里初始化基础设置:override func viewDidLoad() { super.viewDidLoad() navigationController?.navigationBar.prefersLargeTitles = true navigationItem.largeTitleDisplayMode = .always } - 可以调整
transitionThreshold的值来控制触发动画的灵敏度,数值越小,滚动一点就开始切换;数值越大,需要滚动更多才会触发 - 用
[weak self]避免循环引用,防止内存泄漏
内容的提问来源于stack exchange,提问作者Denis
相关产品推荐
相关产品推荐

