如何在uniapp中实现自定义顶部导航栏并适配不同机型?

6,833 阅读5分钟

背景

在uniapp中,原生的头部不适合我们,就需要自定义头部满足场景需求。

本文就从0到1封装一个自定义头部,对应的组件已发布插件市场cos-header,可以下载进行体验。

1. 不同平台对自定义头部的表现形式

如何开启自定义头部,在pages.json中将对应页面的style中添加"navigationStyle": "custom"即可。

不同平台设置后的表现形式也不同,下面主要说明微信、支付宝、h5、app的差别:

  • 微信小程序:设置后,胶囊存在,需要考虑胶囊的宽度
  • h5、App:顶部没有显示
  • 支付宝小程序:进行自定义头部,顶部的导航栏还是存在,如果要设置动态的标题,可调用uni.setNavigationBarTitle进行设置。

2. 哪些应用场景

先看几张图片:

  • 需要设置字体颜色

    image.png

  • 只需返回按钮

    image.png

  • 需要自定义输入框

    image.png

  • 滚动渐变效果

image.png

还可能需要自定义头部背景等等。

3. 具体实现

对于h5和app我们不需要考虑胶囊的大小,支付宝不支持就隐藏,只要说明下微信小程序的实现。

先看一张图:

image.png

头部组成主要有: 状态栏 + 导航栏,导航栏分为左边+中间+右边,左右基本设置成一致,那么我们就只需要得到胶囊大小,就能确定布局。

3.1 获取宽高信息

那我们需要获取状态栏高度、胶囊高度信息

getRectInfo() {
    // 获取状态栏高度
    const sysInfo = uni.getSystemInfoSync()
    this.statusBarHeight = sysInfo.statusBarHeight
    // 默认高度
    this.height = this.statusBarHeight + this.defaultNavBarheight
    // #ifndef APP || H5
    // 判断获取微信小程序胶囊API是否可用 H5出现为true情况无法使用
    if (uni.canIUse('getMenuButtonBoundingClientRect')) {
            // 获取微信小程序胶囊布局位置信息
            this.menuButtonRect = uni.getMenuButtonBoundingClientRect()
            // (胶囊上部高度-状态栏高度)*2 + 胶囊高度 = 导航栏高度(不包含状态栏)
            // 以此保证胶囊位于中间位置,多机型适配
            this.navBarHeight = (this.menuButtonRect.top - sysInfo.statusBarHeight) * 2 + this.menuButtonRect
                    .height
            // 状态栏高度 + 导航栏高度 = 自定义导航栏高度总和
            this.height = this.statusBarHeight + this.navBarHeight
    }
    // #endif
},

3.2 当前页是否首页

如果当前页面为第一个,展示的返回按钮为主页,不是返回按钮

const pages = getCurrentPages()
this.isFirstPage = pages.length === 1

首次进入页面:

image.png

其他页面跳转:

image.png

其余这里不做过多说明,请看具体整体代码实现和文档cos-header

3.3 整体代码实现

<template>
	<!-- #ifndef MP-ALIPAY -->
	<view class="cos-header" :class="{ 'is_fixed': fixed }"
		:style="{ height: height + 'px', backgroundColor: backgroundColor, zIndex: zIndex, color: fontColor }">
		<image :src="backgroundImage" class="nav-bg" mode="scaleToFill" :style="{ height: height + 'px' }"></image>
		<!-- 状态栏占位 -->
		<div :style="{ height: statusBarHeight + 'px' }"></div>
		<!-- 导航栏 -->
		<view class="nav-wrapper" :style="{ height: navBarHeight + 'px' }">
			<!-- 返回按钮 -->
			<view class="nav-back" v-if="isShowLeft" :style="{ width: menuButtonRect.width + 'px' }" @click="handleBack">
				<slot name="left">
					<!-- 支付宝自定义还是有返回按钮 -->
					<template v-if="isShowBack">
						<image v-if="isFirstPage" :src="homeIcon || '../../static/home.svg'" class="img">
						</image>
						<image v-else :src="backIcon || '../../static/prev.svg'" class="img"></image>
					</template>
				</slot>
			</view>
			<view class="nav-title">
				<slot>
					{{ title }}
				</slot>
			</view>
			<!-- 胶囊位置 -->
			<view class="nav-menu" v-if="isShowRight" :style="{ width: menuButtonRect.width + 'px' }">
				<slot name="right"></slot>
			</view>
		</view>
	</view>
	<!-- #endif -->
</template>
<script>
/**
 * NavBar 自定义导航栏
 * @description 导航栏组件,主要用于头部导航
 * @property {String} title 标题文字
 * @property {String} homeUrl 左侧点击返回主页地址
 * @property {String} homeIcon 左侧主页图标
 * @property {String} backUrl 左侧点击返回地址
 * @property {String} backIcon 左侧返回图标
 * @property {Boolean} fixed = [true|false] 是否固定顶部
 * @property {Number} zIndex = 显示层级
 * @property {String} backgroundColor 导航栏背景颜色
 * @property {String} backgroundImage 导航栏背景图片
 * @property {String} fontColor 图标和文字颜色
 * @property {Boolean} isShowBack 是否显示返回图标
 * @property {Boolean} isShowLeft 是否显示左侧
 * @property {Boolean} isShowRight 是否显示右侧
 * @property {Number} defaultNavBarheight 默认导航栏高度
 * @property {Number} defaultMenuWidth 默认菜单宽度
 * @property {Object} jumpMap 主页跳转方法映射
 */
export default {
	name: "CosHeader",
	props: {
		title: {
			type: String,
			default: ''
		},
		homeUrl: {
			type: String,
			default: '/pages/index/index'
		},
		homeIcon: {
			type: String,
			default: ''
		},
		backUrl: {
			type: String,
			default: ''
		},
		backIcon: {
			type: String,
			default: ''
		},
		fixed: {
			type: Boolean,
			default: false
		},
		zIndex: {
			type: Number,
			default: 99
		},
		backgroundColor: {
			type: String,
			default: '#fff'
		},
		backgroundImage: {
			type: String,
			default: ''
		},
		fontColor: {
			type: String,
			default: '#000'
		},
		isShowBack: {
			type: Boolean,
			default: true
		},
		isShowLeft: {
			type: Boolean,
			default: true
		},
		isShowRight: {
			type: Boolean,
			default: true
		},
		defaultNavBarheight: {
			type: Number,
			default: 40
		},
		defaultMenuWidth: {
			type: Number,
			default: 40
		},
		jumpMap: {
			type: Object,
			default: () => ({
				home: '',
				back: ''
			})
		}
	},
	data() {
		return {
			// 状态栏
			statusBarHeight: 0,
			// 默认导航栏高度设置40,针对app和h5情况
			navBarHeight: this.defaultNavBarheight,
			// 胶囊信息
			menuButtonRect: {
				width: this.defaultMenuWidth
			},
			height: this.defaultNavBarheight,
			isFirstPage: true,
		}
	},
	mounted() {
		this.init()
	},
	methods: {
		init() {
			this.getRectInfo()
			this.getPageInfo()
			this.setTitle()
		},
		setTitle() {
			// 针对支付宝进行动态设置
			// #ifdef MP-ALIPAY
			uni.setNavigationBarTitle({
				title: this.title
			});
			// #endif
			// #ifdef H5
			document.title = this.title
			// #endif
		},
		getRectInfo() {
			// 获取状态栏高度
			const sysInfo = uni.getSystemInfoSync()
			this.statusBarHeight = sysInfo.statusBarHeight
			// 默认高度
			this.height = this.statusBarHeight + this.defaultNavBarheight
			// #ifndef APP || H5
			// 判断获取微信小程序胶囊API是否可用 H5出现为true情况无法使用
			if (uni.canIUse('getMenuButtonBoundingClientRect')) {
				// 获取微信小程序胶囊布局位置信息
				this.menuButtonRect = uni.getMenuButtonBoundingClientRect()
				// (胶囊上部高度-状态栏高度)*2 + 胶囊高度 = 导航栏高度(不包含状态栏)
				// 以此保证胶囊位于中间位置,多机型适配
				this.navBarHeight = (this.menuButtonRect.top - sysInfo.statusBarHeight) * 2 + this.menuButtonRect
					.height
				// 状态栏高度 + 导航栏高度 = 自定义导航栏高度总和
				this.height = this.statusBarHeight + this.navBarHeight
			}
			// #endif
		},
		getPageInfo() {
			const pages = getCurrentPages()
			this.isFirstPage = pages.length === 1
		},
		handleBack() {
			if (!this.isShowBack) return
			if (!this.isFirstPage && !this.backUrl) {
				uni.navigateBack({
					delta: 1
				})
				return
			}
			const url = this.isFirstPage ? this.homeUrl : this.backUrl
			if (!url) return
			const type = this.isFirstPage ? (this.jumpMap.home || '') : (this.jumpMap.back || '')
			if (type) {
				uni[type]({
					url
				})
				return
			}
			try {
				uni.navigateTo({
					url
				})
			} catch (error) {
				uni.switchTab({
					url
				})
			}
		}
	}
}
</script>
<style>
.cos-header {
	position: relative;
	width: 100vw;
}

.nav-bg {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	width: 100vw;
}


.is_fixed {
	position: fixed;
	top: 0;
	right: 0;
	left: 0;
	z-index: 99;
}

.nav-wrapper {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: 0 8px;
}

.img {
	width: 20px;
	height: 20px;
	color: #fff;
}

.nav-back {
	flex-shrink: 0;
}

.nav-title {
	text-align: center;
	white-space: nowrap;
	overflow: hidden;
	word-break: break-all;
	text-overflow: ellipsis;
}

.nav-menu {
	flex-shrink: 0;
}
</style>

3.4 如何实现滚动渐变

滚动渐变本质上就是修改背景色和字体颜色,可以配置uniapp中的onPageScroll轻松实现。

代码如下:

<template>
	<view class="content">
		<cos-header title="导航栏组件" :background-color="backgroundColor" :font-color="fontColor" :fixed="fixed">
		</cos-header>
		<image src="/static/sync_bg.jpg" mode="" style="width: 100vw;"></image>
		<div v-for="item in 100" :key="item">
			{{ item }}
		</div>
	</view>
</template>

<script>
export default {
    data() {
            return {
                    backgroundColor: '',
                    fontColor: '#fff',
                    fixed: true,
            }
    },
    onPageScroll(e) {
        if (e.scrollTop < 5) {
                this.fontColor = '#fff'
                this.backgroundColor = ''
        } else if (e.scrollTop < 50) {
                const rate = e.scrollTop / 50
                this.backgroundColor = `rgba(255,255,255, ${rate})`
                this.fontColor = `rgba(0,0,0, ${rate})`
        } else {
                this.backgroundColor = 'rgba(255,255,255)'
                this.fontColor = 'rgba(0,0,0)'
        }
    }
}
</script>

4. 效果展示

image.png

header-ezgif.com-video-to-gif-converter.gif

具体文档请查看cos-header

体验预览效果:

image.png

5. 总结

通过这次封装,主要学习不同平台的表现,学习对应的应用场景。希望能对你有所帮助,如有错误,请指正O^O!