You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 19:42:54