Files
danyang-match/miniapp/utils/nav-metrics.js
T

160 lines
6.2 KiB
JavaScript
Raw Normal View History

/**
* 顶部安全区几何(状态栏 + 右上角胶囊按钮)——全站唯一事实源
*
* 为什么需要它(2026-09-23 真实缺陷):
* 五个自定义导航页(index / discover / messages / activity / mine)此前用
* `padding-top: env(safe-area-inset-top)` 顶开状态栏。这个值在微信里**只反映设备的
* 物理安全区**(刘海/挖孔),且安卓上常为 0,更关键的是它**完全不反映右上角胶囊按钮
* (「••• ⊙」)的位置**。于是出现两类重合:
* ① 竖向:导航行落在胶囊所在的横带里(消息页四个快捷入口、活动页分类胶囊被压住);
* ② 横向:右对齐元素(首页「消息」、找对象页搜索、活动页日历、我的页「编辑资料」、
* 聊天页「⋯」)直接压在胶囊下面,点不到。
* 正确做法是用 `wx.getMenuButtonBoundingClientRect()` 实测胶囊矩形,反推:
* - navBarHeight:与胶囊同高并垂直居中的导航行高度 = 胶囊高 + 上下间距 ×2
* - reserve :屏幕右边缘到胶囊左边缘的距离 + 余量,即右对齐元素必须让出的宽度
* - navTop :状态栏 + 导航行高度,即页面内容「可以开始」的 y 坐标
*
* 用法(页面):
* const { navSafeData } = require('../../utils/nav-metrics')
* Page({ data: { navVars: '' }, onLoad() { this.setData(navSafeData()) } })
* <view class="page" style="{{navVars}}"> <!-- wxss 里用 var(--nav-top) 等 -->
*
* 单位一律 px(inline style 与 rpx 混用会算错),rpx 换算交给 wxss 侧的 var()。
*/
/** 胶囊左侧再留出的余量,避免右对齐文字与胶囊贴边(px) */
var CAPSULE_GAP_PX = 8;
/** 导航行高度合理区间:过小会压字,过大会把内容推太远(px) */
var MIN_NAV_BAR_HEIGHT = 40;
var MAX_NAV_BAR_HEIGHT = 56;
/** 拿不到胶囊信息时的兜底(按带刘海的 iPhone 取,宁可多留不可少留) */
var FALLBACK = {
windowWidth: 375,
statusBarHeight: 20,
navBarHeight: 44,
reserve: 102,
navTop: 64,
};
var cache = null;
function toPositiveNumber(value, fallback) {
var n = Number(value);
if (!isFinite(n) || n <= 0) return fallback;
return n;
}
function clamp(value, min, max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
/**
* 纯函数:只做几何计算,不碰 wx。便于在 Node 侧用多机型样本做断言。
* @param {{windowWidth?:number, statusBarHeight?:number, capsule?:{top:number,left:number,width:number,height:number,right?:number}}} input
*/
function computeNavMetrics(input) {
var source = input || {};
var windowWidth = toPositiveNumber(source.windowWidth, FALLBACK.windowWidth);
var statusBarHeight = toPositiveNumber(source.statusBarHeight, FALLBACK.statusBarHeight);
var capsule = source.capsule;
var hasCapsule = !!(capsule && Number(capsule.width) > 0 && Number(capsule.height) > 0);
var navBarHeight = FALLBACK.navBarHeight;
var reserve = FALLBACK.reserve;
if (hasCapsule) {
// 胶囊在状态栏下方通常还有一点间距,导航行取「胶囊高 + 上下对称间距」,视觉上正好居中对齐
var gap = Math.max(Number(capsule.top || 0) - statusBarHeight, 0);
navBarHeight = clamp(
Number(capsule.height) + gap * 2,
MIN_NAV_BAR_HEIGHT,
MAX_NAV_BAR_HEIGHT,
);
// 右对齐元素要整体避开胶囊:从右边缘到胶囊左边 + 余量,且不超过半屏(避免极端机型把内容挤没)
reserve = clamp(windowWidth - Number(capsule.left || 0) + CAPSULE_GAP_PX, 0, windowWidth * 0.5);
}
navBarHeight = Math.round(navBarHeight);
reserve = Math.round(reserve);
return {
windowWidth: windowWidth,
statusBarHeight: Math.round(statusBarHeight),
navBarHeight: navBarHeight,
reserve: reserve,
navTop: Math.round(statusBarHeight) + navBarHeight,
source: hasCapsule ? 'capsule' : 'fallback',
};
}
function readWindowInfo() {
try {
if (typeof wx !== 'undefined' && typeof wx.getWindowInfo === 'function') {
return wx.getWindowInfo() || {};
}
} catch (e) { /* 低版本基础库无此接口 */ }
try {
if (typeof wx !== 'undefined' && typeof wx.getSystemInfoSync === 'function') {
return wx.getSystemInfoSync() || {};
}
} catch (e) { /* 接口异常时走兜底 */ }
return {};
}
function readCapsuleRect() {
try {
if (typeof wx !== 'undefined' && typeof wx.getMenuButtonBoundingClientRect === 'function') {
return wx.getMenuButtonBoundingClientRect();
}
} catch (e) { /* 同上 */ }
return null;
}
/**
* 带缓存的取值:同一进程内胶囊几何不会变,避免每页 onLoad 重复调用系统接口。
* @param {boolean} [force] 强制重新读取(机型/窗口变化时可用)
*/
function getNavMetrics(force) {
if (cache && !force) return cache;
var win = readWindowInfo();
cache = computeNavMetrics({
windowWidth: win.windowWidth,
statusBarHeight: win.statusBarHeight,
capsule: readCapsuleRect(),
});
return cache;
}
/**
* 页面 data 片段:同时给出 CSS 变量串(给 wxml 根节点 inline style)与 px 数值。
* wxss 侧约定(四个变量各司其职,别混用):
* --nav-status 状态栏高度:把「导航行」顶到胶囊所在横带的起点
* --nav-h 导航行高度:与胶囊垂直居中对齐,导航行高就写它
* --nav-right 右对齐元素必须让出的宽度(胶囊安全区)
* --nav-top 状态栏 + 导航行 = 内容真正可以开始的 y(导航行之后的第一个区块用)
*/
function navSafeData(force) {
var m = getNavMetrics(force);
return {
navVars: '--nav-status:' + m.statusBarHeight + 'px;'
+ '--nav-h:' + m.navBarHeight + 'px;'
+ '--nav-right:' + m.reserve + 'px;'
+ '--nav-top:' + m.navTop + 'px;',
navTopPx: m.navTop,
navStatusPx: m.statusBarHeight,
navBarHeightPx: m.navBarHeight,
capsuleReservePx: m.reserve,
};
}
/** 仅测试用:清空缓存 */
function clearNavMetricsCache() {
cache = null;
}
module.exports = {
computeNavMetrics: computeNavMetrics,
getNavMetrics: getNavMetrics,
navSafeData: navSafeData,
clearNavMetricsCache: clearNavMetricsCache,
FALLBACK: FALLBACK,
};