🌟让你的uniapp应用拥有更现代的交互体验,一个支持滚动渐变透明的导航栏组件🌟

uni-app 自适应透明导航栏组件实现

一个支持滚动渐变透明的 uni-app 导航栏组件,让你的小程序拥有更现代的交互体验

📖 前言

在开发小程序时,我们经常会看到这样的效果:页面顶部有张大图,导航栏初始是透明的,随着页面向下滚动,导航栏逐渐变得不透明。这种设计既美观又实用,今天就来分享如何实现这个效果。

✨ 核心特性

  • 🎨 自动透明渐变:滚动时导航栏背景从透明到不透明平滑过渡
  • 🎯 精准控制:基于 IntersectionObserver 实现,性能优异
  • 🔧 灵活配置:支持自定义背景色、标题、返回按钮等
  • 📱 完美适配:自动适配不同机型的状态栏高度

🎬 效果演示

当用户向下滚动页面时,导航栏会从完全透明逐渐变为设定的背景色,整个过渡非常丝滑自然。

🔍 实现原理

核心思路

  1. 占位元素:在页面顶部放置一个与导航栏等高的透明占位元素
  2. 交叉观察 :使用 IntersectionObserver 监听占位元素与视口的交叉情况
  3. 透明度计算:根据交叉比例动态计算导航栏背景的透明度
  4. 实时更新:通过响应式数据驱动样式更新

关键技术点

  • IntersectionObserver:性能优于传统的 scroll 事件监听
  • RGBA 动态计算:保持颜色不变,只改变透明度通道
  • 临界值优化:处理真机环境下交叉比例不精确的问题

💻 代码实现

1. 组件主体 (kl-navbar/index.vue)

vue 复制代码
<script lang="ts" setup>
const { 
  title = '', 
  placeholder = false,
  leftArrow = false,
  backgroundColor = '#fff',
  autoTransparent = false 
} = defineProps<{
  title?: string
  placeholder?: boolean
  leftArrow?: boolean
  backgroundColor?: string
  /** 滚动时标题栏透明渐变 ( tip : placeholder = true时无效 ) */
  autoTransparent?: boolean
}>()

// 只有在不使用 placeholder 模式时才启用自动透明
const canIUseAutoTransparent = computed(() => autoTransparent && !placeholder)

const { statusBarHeight, headerHeight, navbarHeight } = useGlobalStore()

// 按需启用透明度计算
const { r, g, b, a } = (!canIUseAutoTransparent.value)
  ? {}
  : useAutoTransparent(backgroundColor)
</script>

<script lang="ts">
export default {
  options: {
    addGlobalClass: true,
    virtualHost: true,
    styleIsolation: 'shared',
  },
}
</script>

<template>
  <view>
    <!-- 导航栏主体 -->
    <view
      class="fixed left-0 top-0 z-996 grid grid-cols-3 w-100vw items-center"
      :class="[canIUseAutoTransparent && 'transition-background-color duration-100 ease-out']"
      :style="{
        height: `${navbarHeight}px`,
        paddingTop: `${statusBarHeight}px`,
        lineHeight: `${navbarHeight}px`,
        backgroundColor: canIUseAutoTransparent 
          ? `rgba(${r},${g},${b},${a})` 
          : `${backgroundColor}`,
      }"
    >
      <!-- 返回按钮 -->
      <view 
        v-if="leftArrow" 
        class="i-line-md:chevron-small-left p-x-12Px text-24Px" 
        @tap="navigateBack" 
      />
      <!-- 标题 -->
      <text class="col-start-2 text-center">
        {{ title }}
      </text>
    </view>
    
    <!-- 占位模式:推开后续内容 -->
    <view v-if="placeholder" :style="{ height: `${headerHeight}px` }" />
    
    <!-- 自动透明模式:用于观察的目标元素 -->
    <view
      v-else-if="autoTransparent"
      class="_auto-transparent__observer-target pointer-events-none absolute w-full"
      :style="{ height: `${headerHeight}px` }"
    />
  </view>
</template>

设计要点:

  • 导航栏使用 fixed 定位,始终固定在顶部
  • 动态计算状态栏高度,适配不同机型
  • 根据模式渲染不同的占位/观察元素

2. 透明度逻辑 (use-auto-transparent.ts)

typescript 复制代码
import { convertToRGBA } from '@/utils'

export function useAutoTransparent(backgroundColor: string) {
  // 将背景色转换为 RGB 值
  const { r, g, b } = convertToRGBA(backgroundColor)
  const a = ref(0)  // 透明度通道,0 表示完全透明

  let observer: UniNamespace.IntersectionObserver
  
  onMounted(() => {
    const instance = getCurrentInstance()
    
    // 创建交叉观察器,设置 51 个观察阈值(0%, 2%, 4%...100%)
    observer = uni.createIntersectionObserver(
      instance?.proxy, 
      { thresholds: Array.from({ length: 51 }, (_, i) => (i / 50)) }
    )
    
    // 相对于视口顶部进行观察
    observer
      .relativeToViewport({ top: 0 })
      .observe('._auto-transparent__observer-target', ({ intersectionRatio }) => {
        // 处理临界值:真机环境下可能不会精确等于 0 或 1
        // >= 0.95 视为完全可见(透明)
        // <= 0.05 视为完全不可见(不透明)
        a.value = intersectionRatio >= 0.95 
          ? 0 
          : intersectionRatio <= 0.05 
          ? 1 
          : 1 - intersectionRatio
      })
  })
  
  onUnmounted(() => observer.disconnect())

  return { r, g, b, a }
}

核心逻辑:

  1. 观察阈值:设置 51 个阈值点,确保过渡足够平滑
  2. 交叉比例intersectionRatio 表示目标元素有多少比例与视口交叉
    • 1 表示完全在视口内 → 导航栏透明
    • 0 表示完全不在视口内 → 导航栏不透明
  3. 临界值处理:处理精度问题,避免无法完全透明/不透明

3. 颜色转换工具 (utils/index.ts)

typescript 复制代码
/** 返回合法颜色值的 r, g, b 值 */
export function convertToRGBA(color: string) {
  // 处理 HEX 格式:#fff 或 #ffffff
  if (color.startsWith('#')) {
    const hex = color.slice(1).replace(/^([0-9A-F]{3})$/i, '$1$1')
    const r = Number.parseInt(hex.substring(0, 2), 16)
    const g = Number.parseInt(hex.substring(2, 4), 16)
    const b = Number.parseInt(hex.substring(4, 6), 16)
    return { r, g, b }
  }
  // 处理 RGB 格式:rgb(255, 255, 255)
  else if (color.startsWith('rgb')) {
    const parts = color.match(/(\d+),\s*(\d+),\s*(\d+)/)
    if (parts) {
      const [_, r, g, b] = parts
      return { r, g, b }
    }
  }
  throw new Error('Invalid color format')
}

支持格式:

  • HEX:#fff#ffffff
  • RGB:rgb(255, 255, 255)

4. 全局状态管理 (store/global.ts)

typescript 复制代码
export const useGlobalStore = defineStore('global', () => {
  const systemInfo = uni.getSystemInfoSync()

  // 高度相关常量(单位:px)
  const navbarHeight = 44  // 导航栏高度
  const statusBarHeight = systemInfo.statusBarHeight || 0  // 状态栏高度
  const headerHeight = statusBarHeight + navbarHeight  // 总头部高度

  const tabbarHeight = 50
  const whiteBarHeight = systemInfo.safeAreaInsets?.bottom || 0
  const footerHeight = tabbarHeight + whiteBarHeight

  return {
    systemInfo,
    statusBarHeight,
    navbarHeight,
    headerHeight,
    tabbarHeight,
    whiteBarHeight,
    footerHeight,
  }
})

全局常量:

  • 统一管理各种高度值
  • 自动适配不同设备的状态栏高度

📝 使用示例

vue 复制代码
<template>
  <view>
    <!-- 基础用法:固定背景色 -->
    <kl-navbar 
      title="页面标题" 
      :left-arrow="true" 
      background-color="#ffffff"
    />

    <!-- 占位模式:推开页面内容 -->
    <kl-navbar 
      title="页面标题" 
      :placeholder="true"
      background-color="#ffffff"
    />

    <!-- 自动透明模式:滚动渐变 -->
    <kl-navbar 
      title="页面标题" 
      :left-arrow="true"
      :auto-transparent="true"
      background-color="#ffffff"
    />
    
    <!-- 页面内容 -->
    <view class="content">
      <!-- 这里通常会放一张大图或其他内容 -->
    </view>
  </view>
</template>

Props 说明

参数 类型 默认值 说明
title string '' 导航栏标题
placeholder boolean false 是否占位模式(推开内容)
leftArrow boolean false 是否显示返回箭头
backgroundColor string '#fff' 背景颜色
autoTransparent boolean false 是否启用自动透明渐变

⚠️ 注意: autoTransparentplaceholder 不能同时使用,当 placeholder=true 时,autoTransparent 会被忽略。


🎯 技术亮点

1. 性能优化

使用 IntersectionObserver 而非 scroll 事件监听,优势:

  • 浏览器原生 API,性能更好
  • 自动节流,避免频繁计算
  • 更精确的元素可见性判断

2. 边界处理

typescript 复制代码
a.value = intersectionRatio >= 0.95 
  ? 0 
  : intersectionRatio <= 0.05 
  ? 1 
  : 1 - intersectionRatio

在真机测试中发现,intersectionRatio 在接近 0 或 1 时可能出现微小误差(如 0.9999 或 0.0001),导致导航栏永远无法完全透明或不透明。通过设置 5% 的容差范围,确保视觉效果完美。

3. 灵活的颜色支持

通过 convertToRGBA 工具函数,支持多种颜色格式输入,最终转换为 RGBA 格式,只改变透明度通道,保持颜色不变。

4. 响应式设计

利用 Vue 3 的响应式系统,透明度 a 的变化会自动触发样式更新,无需手动操作 DOM。


🤔 常见问题

Q1: 为什么要设置 51 个观察阈值?

A: 阈值越多,过渡越平滑。51 个阈值意味着每 2% 的变化就会触发一次回调,在性能和流畅度之间取得平衡。

Q2: 占位模式和透明模式有什么区别?

A:

  • 占位模式:导航栏下方有一个等高的空白占位,页面内容被推到导航栏下方
  • 透明模式 :导航栏使用 fixed 定位悬浮在页面上方,页面内容从屏幕顶部开始

Q3: 能否自定义渐变速度?

A: 当前实现中渐变速度与滚动速度成正比。如需自定义,可以在计算透明度时添加缓动函数。


🚀 扩展思路

  1. 支持渐变色背景:当前只支持纯色,可以扩展支持渐变背景
  2. 标题颜色联动:背景变化时,标题颜色也跟随变化(黑 ↔ 白)
  3. 自定义阈值:将阈值数量作为 prop 暴露,让用户自定义平滑度
  4. 支持其他样式:除了透明度,还可以支持模糊效果(backdrop-filter)

📚 总结

这个导航栏组件的实现虽然代码量不大,但涉及了多个技术点:

  • ✅ IntersectionObserver API 的使用
  • ✅ Vue 3 Composition API 的实践
  • ✅ 跨平台适配(状态栏高度)
  • ✅ 性能优化(避免频繁计算)
  • ✅ 边界情况处理

希望这篇文章能帮助你理解并实现类似的效果。如果你有更好的实现思路,欢迎交流讨论!

相关推荐
2501_916007472 小时前
iOS 应用性能测试的工程化流程,构建从指标采集到问题归因的多工具协同测试体系
android·ios·小程序·https·uni-app·iphone·webview
00后程序员张5 小时前
iOS 抓不到包怎么办?从 HTTPS 解密、QUIC 排查到 TCP 数据流分析的完整解决方案
android·tcp/ip·ios·小程序·https·uni-app·iphone
前端互助会5 小时前
UNI-APP开发APP避坑指南:这些关键事项你必须掌握
uni-app
游戏开发爱好者81 天前
iOS 商店上架全流程解析 从工程准备到审核通过的系统化实践指南
android·macos·ios·小程序·uni-app·cocoa·iphone
toooooop81 天前
Vuex 中 state、mutations 和 actions 的原理和写法
前端·javascript·uni-app
林_xi1 天前
uniapp使用@uni-ku/root插件实现全局组件
前端·uni-app
计算机毕设定制辅导-无忧学长1 天前
基于uni-app的“民族风韵”特色购物小程序
uni-app
一个处女座的程序猿O(∩_∩)O1 天前
UniApp 生命周期全解析:从应用到页面,再到组件的完美协奏曲
前端·uni-app
你听得到111 天前
Web前端们!我用三年亲身经历,说说从 uniapp 到 Flutter怎么转型的,这条路我爬过,坑我踩过
前端·flutter·uni-app