设计思路
微信小程序自带导航栏,导航栏有胶囊,且胶囊位置和大小不可改变。
- 自定义导航栏的时候,取消默认导航栏。
- 用一个容器替代取消的默认导航栏。
- 通过小程序胶囊(...和退出)的位置和大小计算出自定义导航栏的宽度高度和位置。
- 抽取出自定义导航栏初始化和关键信息的hook
- 自定义容器预留出胶囊大小,根据胶囊大小和位置信息,写出自定义容器的CSS,设计自定义导航栏API。
1. 取消默认导航栏
每个页面都会在index.config.ts
中配置页面信息,如果需要使用自定义导航栏,则配置导航栏为custom
:
export default definePageConfig({
navigationStyle: 'custom',
})
2. 用容器替代取消的默认导航栏
容器指的是整个自定义导航栏,在这里我们把胶囊也算在自定义导航栏里面,因此容器的宽度是视口宽度。
设计上我们就像modal
组件一样,分为container
和paper
,以及胶囊,称为menuButton
。
大致如下:
3. 计算导航栏位置
通过API获取到系统信息和胶囊的位置大小:
// 获取系统信息
const systemInfo = getSystemInfoSync()
// 获取胶囊信息
const menuButtonInfo = getMenuButtonBoundingClientRect()
而手机还有状态栏,在自定义导航栏的情况下,小程序会顶到手机最上面。因此整个自定义导航栏的高度是需要加上状态栏高度的:
// 状态栏高度 获取不到的情况给通用的44 图中的1
const statusBarHeight = systemInfo.statusBarHeight ?? 44
// 状态栏到胶囊的间距 图中的2
const menuButtonStatusBarGap = menuButtonInfo.top - statusBarHeight
// 导航栏高度 = 状态栏到胶囊的间距(胶囊距上距离-状态栏高度) * 2 + 胶囊高度 + 状态栏高度 1+ 2 + 2 + 3
const navBarHeight = menuButtonStatusBarGap * 2 + menuButtonInfo.height + statusBarHeight
// 左右两个的padding
const paddingX = systemInfo.screenWidth - menuButtonInfo.right
4. 抽离位置信息的公共逻辑
import { getSystemInfoSync, getMenuButtonBoundingClientRect } from '@tarojs/taro'
interface NavigateBarInfoProps {
navBar: {
height: number
top: number
py: number
px: number
}
menuInfo: ReturnType<typeof getMenuButtonBoundingClientRect> | null
}
let navigateBarInfo: NavigateBarInfoProps = {
navBar: { top: 0, height: 0, py: 0, px: 0 },
menuInfo: null
}
export const useNavigationBar = () => {
const initNavigationBar = () => {
const systemInfo = getSystemInfoSync()
const menuButtonInfo = getMenuButtonBoundingClientRect()
// 导航栏高度 = 状态栏到胶囊的间距(胶囊距上距离-状态栏高度) * 2 + 胶囊高度 + 状态栏高度
const statusBarHeight = systemInfo.statusBarHeight ?? 44
// 状态栏到胶囊的间距
const menuButtonStatusBarGap = menuButtonInfo.top - statusBarHeight
const navBarHeight = menuButtonStatusBarGap * 2 + menuButtonInfo.height + statusBarHeight
const navBarTop = statusBarHeight
const paddingX = systemInfo.screenWidth - menuButtonInfo.right
navigateBarInfo = {
navBar: {
height: navBarHeight,
top: navBarTop,
py: menuButtonStatusBarGap,
px: paddingX
},
menuInfo: menuButtonInfo
}
}
return {
navigateBarInfo,
initNavigationBar
}
}
在小程序启动的时候,去初始化:
useLaunch(async options => {
initNavigationBar()
})
5. 导航栏组件实现
import { View, Text, Button } from '@tarojs/components'
import { CSSProperties, FC, PropsWithChildren, useMemo, useState } from 'react'
import C from 'classnames'
import {
usePageScroll,
PageScrollObject,
navigateBack,
switchTab,
getCurrentPages
} from '@tarojs/taro'
import { getNavigatePath } from '@/utils/route'
import { throttle } from 'lodash'
import { useNavigationBar } from '@/hooks/useNavigationBar'
interface NavigationBarProps {
containerClassName?: string // 导航栏样式
containerStyle?: CSSProperties // 导航栏样式
className?: string // 导航栏menu内容样式
style?: CSSProperties // 导航栏menu内容样式
gradient?: boolean // 是否开启渐变 默认true
back?: boolean // 是否有返回按钮 默认true
beforeBack?: () => boolean | void // 返回前的钩子 返回false则阻断 返回true或者不返回值则继续
title?: string
}
const NavigationBar: FC<PropsWithChildren<NavigationBarProps>> = props => {
const {
children,
containerClassName,
containerStyle,
className,
style,
gradient = true,
back = true,
beforeBack,
title = ''
} = props
const { navigateBarInfo } = useNavigationBar()
const { navBar, menuInfo } = navigateBarInfo
const [opacity, setOpacity] = useState(0)
// 滑动的时候触发计算渐变度
const scrollSetOpacity = useMemo(
() =>
throttle((e: PageScrollObject) => {
let opacity = Math.min(1, Math.max(0, (e.scrollTop * 2) / navBar.height))
setOpacity(opacity)
}, 30),
[navBar.height]
)
usePageScroll(scrollSetOpacity)
const navigationBarContainerStyle = useMemo(() => {
return Object.assign(
{
height: `${navBar.height}px`
},
gradient ? { backgroundColor: `rgba(255,255,255,${opacity})` } : { backgroundColor: '#fff' },
containerStyle
)
}, [opacity, navBar.height, gradient, containerStyle])
const onBack = () => {
if (beforeBack) {
const result = beforeBack()
if (result === false) return
}
const pages = getCurrentPages()
if (pages.length >= 2) {
navigateBack()
} else {
// TODO 跳转到主页
}
}
return (
<View
className={C('fixed w-screen top-0 left-0 z-[2000]', containerClassName)}
style={navigationBarContainerStyle}
>
<View
className={C('absolute left-0 bottom-0 w-full box-border flex items-center', className)}
style={{
top: `${navBar.top}px`,
padding: `${navBar.py}px ${navBar.px}px`,
...style
}}
>
<View
onClick={onBack}
style={{
flexBasis: `${menuInfo?.width}px`
}}
>
{back && <IconFont name="Back" size={20} />}
</View>
<View className="flex-1">
{!children && title && (
<View className="text-center">
<Text className="text-primary text-base font-medium">{title}</Text>
</View>
)}
{children}
</View>
<View
style={{
flexBasis: `${menuInfo?.width}px`
}}
></View>
</View>
</View>
)
}
export default NavigationBar