202 lines
6.0 KiB
TypeScript
202 lines
6.0 KiB
TypeScript
"use client";
|
||
/**
|
||
* 特定 Android 设备 + Facebook IAB 专用输入框避让 hook。
|
||
*
|
||
* 这不是通用软键盘方案。它只解决一个已知 WebView 问题:
|
||
* 输入框 focus 后键盘弹出,但 Facebook IAB 没有稳定调整页面 viewport,
|
||
* 导致底部输入框被键盘遮挡。
|
||
*
|
||
* 策略:
|
||
* - 非目标 Android Facebook IAB 设备:完全不生效。
|
||
* - focus 后短期多次测量输入框是否被 visibleViewport 遮挡。
|
||
* - 若 viewport 没有可靠变化,则使用固定 fallback 抬升。
|
||
* - 聚焦期间轻量轮询,持续保持输入框避让。
|
||
*/
|
||
import { useEffect } from "react";
|
||
|
||
import { Logger } from "@/utils";
|
||
|
||
import {
|
||
FALLBACK_INPUT_LIFT_PX,
|
||
FOCUS_GAP_PX,
|
||
KEYBOARD_MEASURE_DELAYS_MS,
|
||
KEYBOARD_POLL_INTERVAL_MS,
|
||
KEYBOARD_VISIBLE_RATIO,
|
||
KEYBOARD_VISIBLE_THRESHOLD_PX,
|
||
} from "./constants";
|
||
import {
|
||
resetInputLiftVar,
|
||
setInputLiftVar,
|
||
} from "./css-vars";
|
||
import {
|
||
getKeyboardAdaptEnvironment,
|
||
shouldEnableKeyboardAdaptation,
|
||
} from "./environment";
|
||
import { isKeyboardTargetActive } from "./keyboard-target";
|
||
import type { UseKeyboardHeightOptions } from "./types";
|
||
|
||
const log = new Logger("HooksUseKeyboardHeight");
|
||
|
||
interface KeyboardSnapshot {
|
||
layoutHeight: number;
|
||
visibleHeight: number;
|
||
visibleBottom: number;
|
||
keyboardHeight: number;
|
||
keyboardVisible: boolean;
|
||
}
|
||
|
||
export function useKeyboardHeight({
|
||
targetRef,
|
||
active = false,
|
||
}: UseKeyboardHeightOptions = {}): void {
|
||
useEffect(() => {
|
||
if (typeof window === "undefined") return;
|
||
|
||
const environment = getKeyboardAdaptEnvironment();
|
||
if (!shouldEnableKeyboardAdaptation(environment)) {
|
||
resetInputLiftVar();
|
||
log.debug("[keyboard] disabled", {
|
||
reason: "not-android-facebook-fallback-device",
|
||
environment,
|
||
});
|
||
return;
|
||
}
|
||
|
||
let rafId: number | null = null;
|
||
let pollId: number | null = null;
|
||
let timeoutIds: number[] = [];
|
||
let lastInputLift = -1;
|
||
|
||
log.debug("[keyboard] environment", environment);
|
||
|
||
const clearTimers = () => {
|
||
timeoutIds.forEach((id) => window.clearTimeout(id));
|
||
timeoutIds = [];
|
||
if (pollId != null) {
|
||
window.clearInterval(pollId);
|
||
pollId = null;
|
||
}
|
||
};
|
||
|
||
const applyLift = (lift: number, reason: string) => {
|
||
const nextLift = Math.max(0, Math.round(lift));
|
||
if (nextLift === lastInputLift) return;
|
||
|
||
lastInputLift = nextLift;
|
||
setInputLiftVar(nextLift);
|
||
log.debug("[keyboard] apply", {
|
||
reason,
|
||
inputLift: nextLift,
|
||
fallback: nextLift === FALLBACK_INPUT_LIFT_PX,
|
||
snapshot: formatSnapshot(readKeyboardSnapshot()),
|
||
});
|
||
};
|
||
|
||
const measure = (reason: string) => {
|
||
rafId = null;
|
||
|
||
const target = targetRef?.current;
|
||
const activeNow = isKeyboardTargetActive(target, active);
|
||
const snapshot = readKeyboardSnapshot();
|
||
|
||
if (!activeNow) {
|
||
log.debug("[keyboard] skip inactive target", {
|
||
reason,
|
||
snapshot: formatSnapshot(snapshot),
|
||
});
|
||
return;
|
||
}
|
||
|
||
const overlapLift = computeTargetOverlap(target, snapshot.visibleBottom);
|
||
const fallbackLift = snapshot.keyboardVisible ? 0 : FALLBACK_INPUT_LIFT_PX;
|
||
applyLift(Math.max(overlapLift, fallbackLift), reason);
|
||
};
|
||
|
||
const scheduleMeasure = (reason: string) => {
|
||
if (rafId != null) return;
|
||
rafId = window.requestAnimationFrame(() => measure(reason));
|
||
};
|
||
|
||
const scheduleInitialMeasures = () => {
|
||
timeoutIds = KEYBOARD_MEASURE_DELAYS_MS.map((delay) =>
|
||
window.setTimeout(() => scheduleMeasure(`focus-${delay}`), delay),
|
||
);
|
||
};
|
||
|
||
const handleViewportSignal = () => {
|
||
scheduleMeasure("viewport-signal");
|
||
};
|
||
|
||
if (!active) {
|
||
return;
|
||
}
|
||
|
||
const visualViewport = window.visualViewport;
|
||
visualViewport?.addEventListener("resize", handleViewportSignal);
|
||
visualViewport?.addEventListener("scroll", handleViewportSignal);
|
||
window.addEventListener("resize", handleViewportSignal);
|
||
window.addEventListener("orientationchange", handleViewportSignal);
|
||
document.addEventListener("visibilitychange", handleViewportSignal);
|
||
|
||
scheduleInitialMeasures();
|
||
pollId = window.setInterval(
|
||
() => scheduleMeasure("poll"),
|
||
KEYBOARD_POLL_INTERVAL_MS,
|
||
);
|
||
|
||
return () => {
|
||
if (rafId != null) window.cancelAnimationFrame(rafId);
|
||
clearTimers();
|
||
visualViewport?.removeEventListener("resize", handleViewportSignal);
|
||
visualViewport?.removeEventListener("scroll", handleViewportSignal);
|
||
window.removeEventListener("resize", handleViewportSignal);
|
||
window.removeEventListener("orientationchange", handleViewportSignal);
|
||
document.removeEventListener("visibilitychange", handleViewportSignal);
|
||
resetInputLiftVar();
|
||
};
|
||
}, [active, targetRef]);
|
||
}
|
||
|
||
function readKeyboardSnapshot(): KeyboardSnapshot {
|
||
const layoutHeight = window.innerHeight;
|
||
const visualViewport = window.visualViewport;
|
||
const visibleHeight = visualViewport?.height ?? layoutHeight;
|
||
const visibleBottom = visualViewport
|
||
? visualViewport.offsetTop + visualViewport.height
|
||
: layoutHeight;
|
||
const keyboardHeight = Math.max(0, layoutHeight - visibleBottom);
|
||
const keyboardVisible =
|
||
keyboardHeight > KEYBOARD_VISIBLE_THRESHOLD_PX ||
|
||
visibleHeight < layoutHeight * KEYBOARD_VISIBLE_RATIO;
|
||
|
||
return {
|
||
layoutHeight,
|
||
visibleHeight,
|
||
visibleBottom,
|
||
keyboardHeight,
|
||
keyboardVisible,
|
||
};
|
||
}
|
||
|
||
function computeTargetOverlap(
|
||
target: HTMLElement | null | undefined,
|
||
visibleBottom: number,
|
||
): number {
|
||
if (!target) return 0;
|
||
|
||
const rect = target.getBoundingClientRect();
|
||
return Math.max(0, rect.bottom + FOCUS_GAP_PX - visibleBottom);
|
||
}
|
||
|
||
function formatSnapshot(snapshot: KeyboardSnapshot) {
|
||
return {
|
||
layoutHeight: Math.round(snapshot.layoutHeight),
|
||
visibleHeight: Math.round(snapshot.visibleHeight),
|
||
visibleBottom: Math.round(snapshot.visibleBottom),
|
||
keyboardHeight: Math.round(snapshot.keyboardHeight),
|
||
keyboardVisible: snapshot.keyboardVisible,
|
||
};
|
||
}
|
||
|
||
export type { UseKeyboardHeightOptions };
|