做uniApp开发的朋友应该都遇到过这种需求:一个页面顶部是张大图或者品牌色头图,往下滑动的时候,头部背景要跟着滚动从透明渐变成纯色,标题也要慢慢浮现出来。这个效果在各类内容型App里太常见了,但真在uniApp里做起来,总会被小程序、App、H5三端差异折腾一遍。
我最近在做一个基于uniApp的内容展示页,产品给的原型就是这种“下拉渐变头部”,要求在App、微信小程序、H5三端表现一致,而且后续要在好几个页面上复用。这套东西做完之后我抽成了两个组件,一个负责展示渐变头部,另一个负责把滚动容器一起封装好。这篇文章就把实现思路、完整代码、接入步骤和踩过的坑全部整理出来,属于能直接拷贝进项目里改改用那种。
1. 方案设计:先理解渐变头部的本质
1.1 这个效果由哪几层组成
拆开看,所谓“下拉渐变头部”,视觉效果上至少包含两个独立图层:
- 背景层:顶部一块区域,通常是半透明到纯色的过渡,可能还叠加图片、渐变底色。
- 内容层:最顶部固定区域的文字、返回按钮、操作按钮,文字颜色需要随着背景明暗变化,背景越深字越白,背景透明时可能需要阴影或深色文字。
单纯有个背景渐变还不够,很多页面还要“标题渐显”:一开始只显示左右侧的图标,不显示居中标题,当滚动距离超过一定阈值后,标题再淡入出现。
这三层其实可以完全独立控制。最省事的方式是:背景层透明度由滚动距离线性映射,文字颜色由两个颜色插值过渡,标题显隐由一个阈值控制。核心就是“滚动距离 -> 透明度 / 颜色值”的换算。
1.2 为什么在uniApp里做起来比普通H5麻烦
在纯H5页面里,你可能会写一个position: sticky或者position: fixed的导航栏,监听window.scrollY,然后改背景色。这在uniApp里不能直接用,原因有三个:
- 小程序页面没有
window全局滚动对象,页面滚动靠onPageScroll生命周期,scrollTop 要在那个回调里拿。 - uniApp里每个页面默认有原生导航栏,想要头部图“延伸到屏幕最顶上”,必须把页面配置成自定义导航栏。自定义导航栏之后,状态栏高度、刘海屏安全区、小程序右上角胶囊按钮都需要手动处理。
- 不同端布局容器不一样:H5用页面滚动和小程序用
scroll-view滚动,拿滚动位置的方式完全不同。
我在组件设计最开始就定了一个原则:滚动数据由外部提供,组件只负责根据 scrollTop 计算结果并渲染样式。这样无论是页面滚动还是局部scroll-view,都能复用同一套渐变逻辑。
1.3 “拿来即用”到底要方便到什么程度
我理解的“拿来即用”,不是从网上找一段半成品,改半天还跑不通。而是直接复制两个文件,页面里注册一下,传几个参数,效果就出来了。所以我最终提取成两个组件:
GradientHeader.vue:纯头部组件,接收scrollTop和配置参数,自动计算背景透明度、文字颜色、标题显隐状态。GradientScrollPage.vue:封装了滚动容器和渐变头部,页面里只需要写 slot 内容,连滚动监听都不用自己写。
如果你的页面本来就使用scroll-view做滚动,直接用第二个组件就很舒服。如果你的页面是整页onPageScroll监听,那就用第一个组件,在页面生命周期里把 scrollTop 传进去就行。
2. 渐变计算原理:别直接用死值
2.1 透明度不是简单的“除以100”
很多新手直接写opacity = scrollTop / 100,这种写法的毛病在于:不同页面头部高度不同,滚动快慢不同,100这个常数换个地方就得改。正确做法是给一个“渐变距离”参数,表示“滚动多少px后,头部完全变成不透明”。
核心公式:
const progress = Math.min(1, Math.max(0, scrollTop / fadeDistance))- 当
scrollTop <= 0时,progress 为 0,背景完全透明。 - 当
scrollTop >= fadeDistance时,progress 为 1,背景完全不透明。 - 中间值按照线性过渡。
为什么用Math.min/max做钳制?因为iOS的橡皮筋回弹效果可能让scrollTop短暂变成负数,或者超出滚动区域。如果不钳制,rgba 的 alpha 会变成非法值,颜色计算也会出错。
fadeDistance的经验值我一般取头部高度的1.5到2倍。头部高度如果是44 + statusBarHeight,那渐变距离给个120到160比较自然。太小的话,手一滑就到底了,过渡很生硬;太大了,划到一半还是半透明,标题也看不清。
2.2 颜色插值才是文字渐变的关键
背景可以只算透明度,但文字颜色不能只调透明度。因为头部背景从透明渐变成白色时,文字如果一直是白色,那背景到透明阶段会看不清;如果一直是黑色,背景变白后又不够协调。
所以我写了一个颜色插值函数,在“起始色”和“结束色”之间按 progress 取中间色。比如:
- 初始状态:背景透明,文字用深色或带阴影的白色。
- 滚动完成:背景为
#ffffff,文字用#333333。
颜色插值原理很简单,把十六进制拆成 RGB 三个分量,然后各自差值乘以 progress:
function hexToRgb(hex) { const h = hex.replace('#', '') const full = h.length === 3 ? h.split('').map(c => c + c).join('') : h return { r: parseInt(full.substring(0, 2), 16), g: parseInt(full.substring(2, 4), 16), b: parseInt(full.substring(4, 6), 16) } } function mixColor(hex1, hex2, progress) { const c1 = hexToRgb(hex1) const c2 = hexToRgb(hex2) const r = Math.round(c1.r + (c2.r - c1.r) * progress) const g = Math.round(c1.g + (c2.g - c1.g) * progress) const b = Math.round(c1.b + (c2.b - c1.b) * progress) return `rgb(${r}, ${g}, ${b})` }背景色我就直接输出rgba,因为背景只需要透明度变化:
const bgColor = computed(() => { const c = hexToRgb(props.bgColor) return `rgba(${c.r}, ${c.g}, ${c.b}, ${progress.value})` })这里bgColor是最终完全显示的纯色,比如白色或者品牌深色。滚动的时候,这个纯色的 alpha 从0变到1,相当于纯色背景从透明渐显,效果就是“原有层逐渐被底色覆盖”,这是最稳的方案。
2.3 标题渐显不要用透明度去叠加
标题渐显如果也用透明度,会跟在头部背景的背后,当背景没完全变成纯色时,标题真的很丑。我的做法是:标题默认opacity: 0,只有当scrollTop >= titleOffset才渐显,而且让它在背景基本成形之后再出现。
titleOffset可以独立设置,一般建议设置为fadeDistance * 0.7。小于这个距离时标题完全透明,过了这个距离后,标题透明度从0线性变到1,再用一个极短的距离完成。这样视觉上就是“背景颜色已经稳定,标题慢慢浮出来”,不会出现背景还是半透明时标题就叠在上面的脏效果。
3. 组件实现拆解:两个组件完整代码
3.1 GradientHeader 组件的模板结构
这个组件的作用很纯粹:给一个滚动距离,渲染头部。
<template> <view class="gd-header" :style="{ height: headerHeight + 'px', paddingTop: statusBarHeight + 'px', backgroundColor: headerBg, color: textColor, ...(needShadow ? { boxShadow: headerShadow } : {}) }" > <slot name="left"> <view v-if="showBack" class="gd-header__back" @tap="handleBack" > <text class="gd-header__back-icon">‹</text> </view> </slot> <view class="gd-header__title" :style="{ opacity: titleOpacity }" > {{ titleText }} </view> <view class="gd-header__right"> <slot name="right"></slot> </view> </view> </template>模板里核心三块:左侧默认返回按钮插槽、居中标题、右侧插槽。头部的最外层 style 直接绑定了动态的背景、文字颜色和阴影。
注意我这里没有把状态栏高度写在headerHeight里。headerHeight指的是导航条可视高度,状态栏是单独paddingTop撑开的。这样在页面中计算定位和避让时,数值更直观。
3.2 GradientHeader 组件的完整逻辑
<script setup> import { computed, ref } from 'vue' const props = defineProps({ scrollTop: { type: Number, default: 0 }, headerHeight: { type: Number, default: 44 }, fadeDistance: { type: Number, default: 120 }, bgColor: { type: String, default: '#ffffff' }, textColor: { type: String, default: '#ffffff' }, initialTextColor: { type: String, default: '#ffffff' }, titleText: { type: String, default: '' }, titleOffset: { type: Number, default: 80 }, showBack: { type: Boolean, default: true }, needShadow: { type: Boolean, default: true } }) const emit = defineEmits(['back']) const statusBarHeight = ref(0) try { const sysInfo = uni.getSystemInfoSync() statusBarHeight.value = sysInfo.statusBarHeight || 0 } catch (e) { statusBarHeight.value = 20 } const progress = computed(() => { const v = props.scrollTop / props.fadeDistance return Math.min(1, Math.max(0, v)) }) const headerBg = computed(() => { const c = hexToRgb(props.bgColor) return `rgba(${c.r}, ${c.g}, ${c.b}, ${progress.value})` }) const textColor = computed(() => { return mixColor(props.initialTextColor, props.textColor, progress.value) }) const titleOpacity = computed(() => { if (!props.titleText) return 0 if (props.scrollTop <= props.titleOffset) return 0 const range = Math.max(1, props.fadeDistance * 0.3) return Math.min(1, (props.scrollTop - props.titleOffset) / range) }) const headerShadow = computed(() => { if (progress.value < 0.05) return 'none' return '0 2rpx 12rpx rgba(0, 0, 0, 0.06)' }) function handleBack() { emit('back') } </script>这段逻辑里几个细节需要解释:
statusBarHeight放在try/catch里读取,因为某些奇葩环境下uni.getSystemInfoSync可能抛异常。默认值给20,保证不白屏。titleOpacity不是直接和 progress 挂钩,而是要走完背景变化后才开始。props.fadeDistance * 0.3意思是标题用“余下30%的距离”完成从0到1的过渡。headerShadow用了一个极小阈值,避免刚开始滚动就出现阴影,视觉上更柔和。
3.3 GradientScrollPage 组件的设计思路
GradientHeader好用,但滚动监听还是要写。我干脆把scroll-view也封装成一个页面级组件:组件内部有一个全屏高度的scroll-view,slot 内容放在里面,头部组件浮在它上面。
<template> <view class="gd-page"> <scroll-view class="gd-page__scroll" scroll-y :style="'height:' + windowHeight + 'px;'" @scroll="handleScroll" :scroll-with-animation="false" > <view class="gd-page__content"> <slot /> </view> </scroll-view> <GradientHeader class="gd-page__header" :scroll-top="scrollTop" :header-height="headerHeight" :fade-distance="fadeDistance" :bg-color="bgColor" :text-color="textColor" :initial-text-color="initialTextColor" :title-text="titleText" :title-offset="titleOffset" :show-back="showBack" :need-shadow="needShadow" @back="$emit('back')" /> </view> </template><script setup> import { ref } from 'vue' import GradientHeader from './GradientHeader.vue' const props = defineProps({ headerHeight: { type: Number, default: 44 }, fadeDistance: { type: Number, default: 140 }, bgColor: { type: String, default: '#ffffff' }, textColor: { type: String, default: '#333333' }, initialTextColor: { type: String, default: '#ffffff' }, titleText: { type: String, default: '' }, titleOffset: { type: Number, default: 90 }, showBack: { type: Boolean, default: true }, needShadow: { type: Boolean, default: true } }) const emit = defineEmits(['back']) const windowHeight = ref(0) const scrollTop = ref(0) try { const sysInfo = uni.getSystemInfoSync() windowHeight.value = sysInfo.windowHeight || 0 if (!windowHeight.value) windowHeight.value = 600 } catch (e) { windowHeight.value = 600 } function handleScroll(e) { scrollTop.value = e.detail.scrollTop > 0 ? e.detail.scrollTop : 0 } </script>这里有几个关键点:
scroll-view高度必须写死,用windowHeight而不是100vh。因为在小程序里100vh在不同机型上可能包含或排除状态栏,容易出现底部能滚过头的偏差。- slot 内容外层加了
gd-page__content,主要用于给内容留头部避让空间。具体留多少,在样式中通过 padding 实现。 scroll-with-animation必须设为 false,否则某些Android机型上滚动会有惯性动画,滚到顶时动画过程中scrollTop会一直变动,导致渐变往复闪烁。
3.4 样式与避让细节
GradientHeader 的样式比较常规:
.gd-header { position: fixed; top: 0; left: 0; right: 0; z-index: 999; display: flex; align-items: center; box-sizing: border-box; transition: color 0.1s ease-out; } .gd-header__back { width: 60rpx; height: 60rpx; margin-left: 16rpx; display: flex; align-items: center; justify-content: center; border-radius: 50%; background-color: rgba(0, 0, 0, 0.2); } .gd-header__back-icon { font-size: 44rpx; line-height: 1; color: #fff; font-weight: bold; } .gd-header__title { flex: 1; text-align: center; font-size: 34rpx; font-weight: 500; transition: opacity 0.2s ease-out; } .gd-header__right { min-width: 60rpx; padding-right: 20rpx; display: flex; align-items: center; justify-content: flex-end; }左侧返回按钮背景用了一层半透明白色圆形,这是为了让透明背景时返回箭头看得清。如果初始文字颜色是深色,那么这个返回按钮背景就要跟着动态调整,我在模板里直接让插槽自己控制,这里给出的只是一个默认样式。实际项目中,返回箭头下面的text颜色可以绑定textColor,但为了示例简洁没有再加一层嵌套。
GradientScrollPage的样式需要注意:
.gd-page { position: relative; width: 100%; height: 100%; background-color: #f7f8fa; } .gd-page__scroll { width: 100%; } .gd-page__content { padding-top: 130px; } .gd-page__header { position: absolute; top: 0; left: 0; right: 0; }这里gd-page__content的padding-top要超过“状态栏 + 头部高度”,否则内容的第一屏会被头部遮挡。比较靠谱的做法是给它绑定一个动态内联 style:padding-top: calc(statusBarHeight + headerHeight + 10)px。但组件里我没把状态栏高度再透传出去,所以先用固定值示意。实际接项目时,我会建议在页面里对首屏内容额外做避让,因为每个页面首屏内容样式不同,不可能由组件统一处理。
4. 实操步骤:把组件接入你的项目
4.1 准备阶段:创建组件文件
在项目的components目录下新建GradientHeader和GradientScrollPage两个子目录,把上面的代码分别保存。如果你的项目使用的是Vue2版本的uniApp,注意一下写法:
- Vue2用
props和computed一样,但defineProps不支持。你需要改成export default { props: {...}, computed: {...} }的写法。 - Vue2里
slot="left"写法需要保持,其它逻辑基本一致。
我的示例代码是Vue3写法,现在 HBuilderX 创建的项目默认很多都切到Vue3了,对于还在维护老项目的同学,你只需要把defineProps部分换掉,公式和结构不用动。
4.2 整页滚动场景:页面级接入
在pages.json中,把目标页面的navigationStyle改成custom:
{ "path": "pages/index/index", "style": { "navigationStyle": "custom" } }然后在页面里使用GradientHeader:
<template> <view class="page"> <slider-header :scroll-top="scrollTop" title-text="我的主页" bg-color="#ffffff" text-color="#333333" initial-text-color="#ffffff" :show-back="true" @back="goBack" /> <view class="page__body"> <!-- 页面主体内容 --> </view> </view> </template> <script setup> import { ref } from 'vue' import { onPageScroll } from '@dcloudio/uni-app' import SliderHeader from '@/components/GradientHeader/index.vue' const scrollTop = ref(0) onPageScroll((e) => { scrollTop.value = e.scrollTop }) </script>注意onPageScroll必须从@dcloudio/uni-app导入,这是uniApp对Vue3组合式API提供的生命周期封装。如果你在<script setup>里直接写onPageScroll而不导入,编译会报未定义。
页面主体内容的第一个区块如果不是图片,而是文字块,建议给它加一个padding-top,避免被顶部固定导航遮挡。你可以把状态栏高度和导航高度都放进一个css variables里,或者干脆在页面里计算好:
const topOffset = uni.getSystemInfoSync().statusBarHeight + 44然后给page__body设置:style="'padding-top:' + (topOffset + 20) + 'px'"。
4.3 局部滚动场景:使用 GradientScrollPage
如果一个页面内部本来就是多个区块纵向排列,用scroll-view包住这些区块,那就直接用GradientScrollPage:
<template> <GradientScrollPage title-text="商品详情" bg-color="#ffffff" text-color="#333333" initial-text-color="#ffffff" fade-distance="180" > <view class="banner">这里是轮播图</view> <view class="info">这里是商品信息</view> <view class="detail">这里是详情内容</view> </GradientScrollPage> </template>所有滚动监听和渐变计算都在组件内部完成,你只需要关注slot里面的业务内容。这就是真正的“拿来即用”。
不过这种方案有局限:scroll-view内的内容高度必须超过windowHeight,否则不会滚动,渐变也一直停在初始状态。如果你的页面在某块区域已经有横向滚动或特殊手势,再包一层纵向scroll-view,手势冲突需要多测几遍。
4.4 调整渐变速率,找到合适的视觉节奏
fadeDistance是最需要调的参数。我的参考标准:
| 场景 | 建议 fadeDistance | 说明 |
|---|---|---|
| 页面顶部是轮播图 | 180-220 | 图片高度大,渐变得慢一点,不要急着露出背景 |
| 页面顶部是用户头像区 | 100-140 | 头像高度小,快速渐变,否则会显得粘腻 |
| 页面有大量文字 | 140-180 | 文字阅读时对顶部干扰要小,中等过渡速度即可 |
| 左右滑动Tab页 | 120-150 | 配合切换动画,过渡太慢会感觉卡顿 |
titleOffset单独控制标题出现时机,建议和fadeDistance配合调。如果fadeDistance是160,titleOffset给100到120,意思是背景已经渐变了大概三分之二后标题再淡入。如果titleOffset过小,标题就会在背景还是透明时出现,很违和。
4.5 在小程序端处理胶囊按钮
小程序自定义导航栏时,右上角胶囊按钮是系统渲染的,你改不了它。如果要放右侧操作按钮,就必须避开胶囊。
我在实际项目中加了一段逻辑,你可以在GradientHeader中扩展:
const capsule = ref(null) try { const menu = uni.getMenuButtonBoundingClientRect() capsule.value = menu } catch (e) { capsule.value = null }然后在右侧插槽外层根据胶囊的位置动态加一个padding-right。这个不是必须写进组件里的,因为非小程序端胶囊不存在。我的做法是组件提供一个useCapsulePadding的 prop,小程序端开,其它端关。
5. 踩坑记录:这些问题的坑我替你先踩了
5.1 scroll-view 在小程序端滚动事件不触发怎么办
如果你用GradientScrollPage,发现滚动了很多但scrollTop一直是0,先检查两点:
scroll-y是否写上。忘写的效果就是内容超过一屏,但页面就是不动,scroll事件自然也不触发。scroll-view是否设置了确定高度。在Flex布局里,height: 100%不一定生效,我给组件用的是windowHeight强指定的方式,就是为了避免这个问题。
还有一种情况是:slot 内容高度等于或小于windowHeight,那根本不会滚动。调试时可以先给内容加一个height: 1500px确认事件能触发,再排查业务内容高度。
5.2 iOS 橡皮筋回弹导致渐变突变
iOS的scroll-view和页面滚动都存在回弹。往回拉到顶部后松手,scrollTop在那一瞬间可能变成负值,然后弹回0。如果你直接用负值计算,Math.min/max会把它钳制到0,一般没什么问题。但App端的 webview 回弹更容易出现的是:scrollTop在0附近持续抖动,透明度也跟着抖。
解决办法有两个方向:
- 在计算逻辑里给
scrollTop做一个阈值器,比如scrollTop < 0.5都当作0处理。 - 给渐变计算加个一阶惯性滤波,不要让透明度跟随每一次变化:
let smoothProgress = 0 const progress = computed(() => { const target = Math.min(1, Math.max(0, props.scrollTop / props.fadeDistance)) smoothProgress = smoothProgress + (target - smoothProgress) * 0.15 return smoothProgress })注意计算属性要求纯函数,这里用模块变量会有状态残留,实际我是在watch里做平滑处理的,示例只用来表达思路。真要做到平滑,可以监听 props.scrollTop,然后手动驱动一个 rAF 循环。不过大多数场景下不做平滑也够用,iOS原生的视觉效果已经比较流畅。
5.3 状态栏高度在不同端不一样
uni.getSystemInfoSync().statusBarHeight在iOS和Android上返回的值不同。iPhone刘海屏大概是44,Android有24、25、28等,非刘海屏可能20。这只是状态栏高度,不包括胶囊按钮高度。
在小程序端,胶囊按钮底边一般和状态栏有一段距离,默认返回按钮如果只从paddingTop开始布局,左侧箭头会偏上。更好的做法是把整个头部高度设定为statusBarHeight + navbarHeight,让内容区垂直居中于statusBarHeight到statusBarHeight + navbarHeight这一段。
在GradientHeader模板里,当前是只有一个paddingTop然后align-items: center,所以内容会在statusBarHeight到statusBarHeight + headerHeight这个区间居中,状态栏区域并没有内容,这样其实是安全的。但如果左侧返回箭头想对齐胶囊按钮水平位置,需要额外处理。
5.4 页面滚动和 scroll-view 滚动不要混用
一个常见错误:页面本身设置了navigationStyle: custom,页面里又用了scroll-view,然后在onPageScroll里去监听滚动。这时候onPageScroll只在页面原生滚动时触发,而scroll-view是内部滚动,不会触发页面的onPageScroll。所以两头必须选一头:
- 页面原生滚动:用
onPageScroll获取scrollTop。 - 局部
scroll-view滚动:用scroll-view的@scroll事件获取e.detail.scrollTop。
我提供GradientScrollPage就是为了避免这种混用,让使用者不需要理解onPageScroll也能用起来。
5.5 H5端 fixed 定位和滚动容器之间的坑
H5端如果外层容器有transform动画,内部的position: fixed会失效。因为 CSS 的transform会创建新的包含块。在GradientScrollPage里,我给外层.gd-page没有加任何transform,所以不影响。但如果你在使用时给页面容器加了transform做转场动画,就必须在动画结束后移除,或者把头部设置成absolute并配合相对定位的父容器。
如果整个页面用uni.navigateTo进入,H5端转场时也会有transform,所以我的GradientHeader用的是fixed,在转场期间虽然会受包含块影响,但转场动画很短,实际上很多项目直接忽略这个问题。如果想绝对稳妥,可以用absolute配合外层height: 100%的父容器。
5.6 不要依赖 backdrop-filter 做背景模糊
很多人想给头部加毛玻璃效果:背景渐入的同时模糊掉底下内容。backdrop-filter 在 iOS上效果不错,但在很多Android webview 和小程序 webview 里不支持或表现乱掉,而且滚动时持续计算模糊非常耗性能。
稳妥方案:背景层用纯色rgba渐入,最多在纯色层上再叠加一层半透明蒙层。如果非要模糊,可以尝试用filter: blur()给内容层做模糊,但filter会影响滚动性能,在小程序端有时还会触发渲染崩溃。我的建议是官网视觉稿能接受纯色渐变,就不要去碰毛玻璃。
6. 让渐变头部更好用的几个扩展细节
6.1 背景图在渐变中如何过渡
如果头部背景是一张图,而不是纯色,经典做法是:图片层固定在顶部,纯色蒙层在滚动时逐渐覆盖图片。也就是说,必须在头部区域内再叠两层:
- 底层:背景图,随着滚动可以做一个轻微的 scale 或上移。
- 上层:
rgba纯色蒙层,透明度由 progress 控制。
我在GradientHeader基础上扩展很简单,加一个bgImageUrlprop,然后在模板里插入:
<view class="gd-header__bg-img" :style="{ backgroundImage: 'url(' + bgImageUrl + ')', opacity: 1 - progress }"></view>当 progress 为0时,图片完全显示;当 progress 为1时,图片透明度为0,露出底下的纯色背景。这样会产生“图片被白色/品牌色覆盖”的过渡效果,比单纯把图片蒙上纯色更干净。
6.2 左侧插槽和右侧插槽的动态颜色
GradientHeader里的文字颜色只作用于标题区域。左右插槽是组件外部提供的,比如返回箭头、分享按钮,它们的颜色需要开发者自己监听渐变状态。我建议给组件再暴露一个progress的 prop 或者通过作用域插槽传出当前 progress:
<slot name="left" :progress="progress"></slot>使用方拿到 progress 后,可以自己决定图标的颜色、阴影。注意作用域插槽在 Vue2 和 Vue3 叫法不同,Vue3 里直接用v-slot:left="{ progress }",Vue2 里用slot-scope。这个扩展看项目需求,不是必须。
6.3 多Tab页面切换时重置滚动位置
很多页面在scroll-view内部切换Tab,切换后scrollTop可能还停留在上一个Tab的位置。此时头部渐变状态是错的。解决办法是在Tab切换逻辑里手动把scroll-view的scroll-top置为0,并且把组件内部的scrollTop也重置为0。
scroll-view有一个天然的麻烦:直接改scroll-top属性,可能因为属性没变化而不触发滚动。所以重置时要先设0,再在下个 tick 设置成新的值。如果想让平滑滚回顶部,scroll-with-animation开启一下,结束后再关掉,避免影响后续渐变监听。
6.4 动态修改渐变结束色
如果你的页面支持暗黑模式,或者品牌色会随主题变化,bgColor和textColor应该是响应式的。组件已经用了 computed 监听 props,所以你只需要在外层动态改变传入的颜色值即可。唯一要注意的是hexToRgb只支持六位hex,如果要传#fff这种三位的,需要先扩展一下函数。我在前面的hexToRgb里已经处理了三位的情况,直接支持。
7. 给同样在做这个效果的朋友几点体会
这个渐变头部看起来简单,真正落到uniApp上,主要工作不是写渐变公式,而是处理各端滚动机制和导航栏避让。组件化的思路是从一开始就坚持的,事实证明非常值得:项目里三个页面用到了这个效果,全部只改参数,没有复制粘贴过第二次。
我个人在实际操作中的体会是:把“滚动数据”和“展示组件”拆开,收益最大。因为业务页面的滚动容器不可能完全一样,有的用页面滚动,有的用局部 scroll-view,有的用第三方组件内部的滚动。只要展示层永远只是“接收scrollTop,输出样式”,那么无论滚动来源怎么变,这个头部组件都能复用。
最后再分享一个小技巧:如果你觉得渐变的线性过渡看起来有点呆,可以把progress做一次非线性映射,比如progress = progress * progress * (3 - 2 * progress),这是一条平滑插值曲线,头尾变化慢、中间快,视觉上更柔和。我在正式项目里就是用这个公式替代了纯线性映射,效果提升很明显。代码里只需要替换一行,你可以试试看。