1、需求场景
页面顶部有一张带渐变背景的产品图,导航栏初始透明 叠在图上(白字白箭头)。 随着用户上滑、内容向上滚出可视区,当主体图的顶部刚要被导航栏遮住时,希望导航栏自动切换为:
- 背景:白色
- 标题文字:深色 (
#333) - 返回箭头:深色
下滑回顶部后再平滑还原为透明态。整个切换要带过渡,不能突变。
2、实现思路
核心三步:
- 用页面级生命周期
onPageScroll(e)拿到scrollTop; - 用一个布尔状态
isNavSolid控制"透明态 / 实色态",过阈值就置true,否则false; - 模板上根据这个状态切换 class,CSS 用
transition让背景色和文字色平滑过渡。
返回箭头是 u-icon 这种第三方组件,它的颜色是通过 prop 传入的,不能直接 CSS transition ,所以要把 color 改成动态绑定。
3、完整示例代码
Template
vue
<template>
<view class="page-container">
<!-- 自定义透明导航栏,过阈值后切换为白底深字 -->
<view
class="custom-nav"
:class="{ 'is-solid': isNavSolid }"
:style="{ height: navbarHeight + 'px' }"
>
<view
class="nav-title-content"
:style="{ height: statusBarHeight + 'px' }"
@click="goBack"
>
<u-icon name="arrow-left" size="40" :color="navIconColor"></u-icon>
<text class="nav-title">页面标题</text>
</view>
</view>
<!-- 顶部产品图区(渐变背景) -->
<view class="top-banner" :style="{ paddingTop: navbarHeight + 'px' }">
<image class="banner-image" :src="imageUrl" mode="widthFix" />
</view>
<!-- 其余内容... -->
</view>
</template>
Script
js
export default {
data() {
return {
statusBarHeight: 0,
navbarHeight: 0,
isNavSolid: false,
imageUrl: '',
};
},
computed: {
navIconColor() {
return this.isNavSolid ? '#333' : '#fff';
},
},
onLoad() {
const sysInfo = uni.getSystemInfoSync();
this.statusBarHeight = sysInfo.statusBarHeight;
this.navbarHeight = sysInfo.statusBarHeight + 44;
},
onPageScroll(e) {
const threshold = 30;
this.isNavSolid = e.scrollTop >= threshold;
},
methods: {
goBack() {
uni.navigateBack({ delta: 1 });
},
},
};
Style
scss
.custom-nav {
width: 100%;
background: transparent;
display: flex;
align-items: flex-end;
justify-content: center;
position: fixed;
top: 0;
left: 0;
z-index: 999;
transition: background-color 0.2s ease;
.nav-title-content {
display: flex;
align-items: center;
width: 100%;
padding: 0 32rpx;
}
.nav-title {
color: #fff;
font-size: 32rpx;
font-weight: 500;
margin-left: 10rpx;
transition: color 0.2s ease;
}
&.is-solid {
background: #fff;
.nav-title {
color: #333;
}
}
}
4、关键点解析
4.1. 为什么用 onPageScroll 而不是 scroll-view + @scroll?
onPageScroll是小程序 / uni-app 的页面级生命周期,监听的是整个页面的原生滚动;- 不需要套
<scroll-view>,整张页面用普通<view>就行,写起来简单、性能也更好; - 平台底层已经做了节流(默认 ~16ms 一次),不需要自己再写
throttle。
4.2. 为什么过渡要分开写在 .custom-nav 和 .nav-title 上?
- 背景色变化是父元素
.custom-nav自己的事; - 文字颜色变化在子元素
.nav-title上; - 两者各自加
transition,互不影响;统一写在父元素上(用all)会污染所有子元素的属性变化、性能也差。
4.3. u-icon 颜色为什么要动态绑定?
u-icon 把 color prop 转成内部样式(通常是 <text> 的 color 或 SVG 的 fill),但它不会自动被外部 CSS transition 接管。所以:
- ❌
<u-icon color="#fff" class="dynamic-color">+ 外面写.is-solid .dynamic-color { color: #333 }------ 不生效; - ✅
<u-icon :color="navIconColor">+ 计算属性返回不同颜色 ------ 生效,但箭头颜色是瞬切(无补间)。
如果要箭头颜色也带补间动画,可以叠两层 u-icon(一白一黑),用 opacity 切换,外面给 opacity 加 transition。一般场景下瞬切已经够用。
4.4. 阈值(threshold)怎么选?
凭手感不靠谱,我做了个粗略的几何推导:
设:
- 导航栏总高
H_nav = statusBarHeight + 44(一般 64--88px) - 产品图视觉顶部到页面顶部的距离
Y_img(例如布局 top + transform 缩放偏移) - 标题文字垂直中线在
Y_title ≈ statusBarHeight + 22
期望触发条件:视觉图顶达到标题文字位置
Y_img - scrollTop = Y_title => threshold = Y_img - Y_title
实测代入数字(Y_img ≈ 100px,Y_title ≈ 66px),threshold ≈ 30--35px,就用 30 落地。最终值实机微调即可:
- 太晚(图都进导航栏一半才变色)→ 调小到 15--20;
- 太早(一滑就变色,没"过渡感")→ 调大到 40--50。
5、注意事项 / 踩坑
-
状态栏高度不能写死 44 :iOS 异形屏 / 安卓不同设备状态栏高度差很多,必须用
uni.getSystemInfoSync().statusBarHeight动态取。 -
onPageScroll一定要写在页面 (pages/xxx) 上 ,写在components/里组件里不会被触发。 -
不要在
onPageScroll里做重操作:每次滚动都会触发,里面只做"对比 + 赋值"这种 O(1) 操作;DOM 查询、网络请求绝对不要放进来。 -
位置稳定性 :如果触发的赋值会引发
isNavSolid在阈值附近反复横跳,可以加滞后区(hysteresis):上滑 ≥ 30 触发实色,下滑 ≤ 20 才回到透明,避免频繁切换。
6、总结
| 步骤 | 关键 API / 技巧 |
|---|---|
| 监听滚动 | onPageScroll(e) |
| 状态控制 | 一个布尔 isNavSolid + 模板 :class 切换 |
| 平滑过渡 | CSS transition: background-color, color |
| 第三方组件颜色 | 用 :color 动态绑定计算属性 |
| 阈值估算 | 视觉图顶位置 - 标题文字位置 |
整个交互逻辑 不到 20 行代码,但用户感受到的"质感提升"非常明显,是 ROI 很高的一类细节。
如果喜欢我的文章,欢迎点赞、转发:)