diff --git a/src/hooks/use-keyboard-height.ts b/src/hooks/use-keyboard-height.ts index 5bb6eec0..1b3e28d1 100644 --- a/src/hooks/use-keyboard-height.ts +++ b/src/hooks/use-keyboard-height.ts @@ -8,7 +8,8 @@ * 1. 持续写入 --app-viewport-height,聊天页高度不再只依赖 100dvh。 * 2. 优先使用 visualViewport 计算键盘高度。 * 3. 输入框聚焦后测量目标元素是否越过可见底部,计算额外抬升。 - * 4. 小米系 Android + Facebook IAB 若 viewport 未可靠变化,启用保守 fallback 抬升。 + * 4. 小米系 Android + Facebook IAB 若 viewport 未可靠变化,启用保守 fallback 抬升; + * 键盘收起后若焦点仍停留在 textarea,也要主动释放 fallback。 */ import { type RefObject, useEffect, useRef } from "react"; @@ -21,6 +22,7 @@ const FOCUS_GAP_PX = 14; const FALLBACK_KEYBOARD_RATIO = 0.42; const FALLBACK_KEYBOARD_MIN = 260; const FALLBACK_KEYBOARD_MAX = 380; +const FALLBACK_CLOSE_GRACE_MS = 900; const RECHECK_DELAYS_MS = [0, 50, 120, 240, 420, 700]; const RECHECK_EVENT = "cozsweet:keyboard-recheck"; @@ -36,9 +38,13 @@ export function useKeyboardHeight({ active = false, }: UseKeyboardHeightOptions = {}): void { const activeRef = useRef(active); + const fallbackAllowedRef = useRef(false); + const focusedAtRef = useRef(0); useEffect(() => { activeRef.current = active; + fallbackAllowedRef.current = active; + focusedAtRef.current = active ? performance.now() : 0; window.dispatchEvent(new Event(RECHECK_EVENT)); }, [active]); @@ -50,14 +56,27 @@ export function useKeyboardHeight({ let lastKeyboardHeight = -1; let lastViewportHeight = -1; let lastInputLift = -1; + let lastMayBeKeyboardClose = false; const apply = () => { rafId = null; const metrics = readViewportMetrics(); + if ( + activeRef.current && + fallbackAllowedRef.current && + lastMayBeKeyboardClose && + shouldReleaseFallback(metrics, focusedAtRef.current) + ) { + fallbackAllowedRef.current = false; + } + lastMayBeKeyboardClose = false; + const keyboardHeight = Math.round(metrics.keyboardHeight); const viewportHeight = Math.round(metrics.visibleHeight); - const fallbackLift = computeFallbackLift(metrics); + const fallbackLift = fallbackAllowedRef.current + ? computeFallbackLift(metrics) + : 0; const inputLift = Math.round( activeRef.current ? Math.max( @@ -95,7 +114,9 @@ export function useKeyboardHeight({ viewportHeight, inputLift, fallbackLift, + fallbackAllowed: fallbackAllowedRef.current, visualViewport: metrics.hasVisualViewport, + virtualKeyboard: metrics.hasVirtualKeyboard, }); } }; @@ -105,7 +126,10 @@ export function useKeyboardHeight({ rafId = window.requestAnimationFrame(apply); }; - const scheduleRechecks = () => { + const scheduleRechecks = (event?: Event) => { + if (event && isPossibleKeyboardCloseEvent(event)) { + lastMayBeKeyboardClose = true; + } timeoutIds.forEach((id) => window.clearTimeout(id)); timeoutIds = RECHECK_DELAYS_MS.map((delay) => window.setTimeout(schedule, delay), @@ -117,6 +141,8 @@ export function useKeyboardHeight({ vv.addEventListener("resize", scheduleRechecks); vv.addEventListener("scroll", scheduleRechecks); } + const virtualKeyboard = getVirtualKeyboard(); + virtualKeyboard?.addEventListener("geometrychange", scheduleRechecks); window.addEventListener("resize", scheduleRechecks); window.addEventListener("orientationchange", scheduleRechecks); window.addEventListener("focusin", scheduleRechecks); @@ -132,6 +158,7 @@ export function useKeyboardHeight({ vv.removeEventListener("resize", scheduleRechecks); vv.removeEventListener("scroll", scheduleRechecks); } + virtualKeyboard?.removeEventListener("geometrychange", scheduleRechecks); window.removeEventListener("resize", scheduleRechecks); window.removeEventListener("orientationchange", scheduleRechecks); window.removeEventListener("focusin", scheduleRechecks); @@ -144,6 +171,7 @@ export function useKeyboardHeight({ interface ViewportMetrics { hasVisualViewport: boolean; + hasVirtualKeyboard: boolean; layoutHeight: number; visibleHeight: number; visibleBottom: number; @@ -153,24 +181,36 @@ interface ViewportMetrics { function readViewportMetrics(): ViewportMetrics { const layoutHeight = window.innerHeight; const vv = window.visualViewport; + const virtualKeyboard = getVirtualKeyboard(); + const virtualKeyboardRect = virtualKeyboard?.boundingRect ?? null; + const virtualKeyboardHeight = virtualKeyboardRect?.height ?? 0; if (!vv) { + const visibleBottom = + virtualKeyboardHeight > 0 ? layoutHeight - virtualKeyboardHeight : layoutHeight; return { hasVisualViewport: false, + hasVirtualKeyboard: virtualKeyboard != null, layoutHeight, - visibleHeight: layoutHeight, - visibleBottom: layoutHeight, - keyboardHeight: 0, + visibleHeight: visibleBottom, + visibleBottom, + keyboardHeight: virtualKeyboardHeight, }; } - const visibleHeight = vv.height; - const visibleBottom = vv.offsetTop + vv.height; + const visualBottom = vv.offsetTop + vv.height; + const keyboardTop = + virtualKeyboardHeight > 0 + ? Math.min(virtualKeyboardRect?.y ?? layoutHeight, layoutHeight - virtualKeyboardHeight) + : layoutHeight; + const visibleBottom = Math.min(visualBottom, keyboardTop); + const visibleHeight = Math.min(vv.height, visibleBottom); return { hasVisualViewport: true, + hasVirtualKeyboard: virtualKeyboard != null, layoutHeight, visibleHeight, visibleBottom, - keyboardHeight: Math.max(0, layoutHeight - visibleBottom), + keyboardHeight: Math.max(0, layoutHeight - visibleBottom, virtualKeyboardHeight), }; } @@ -185,6 +225,7 @@ function computeTargetOverlap( function computeFallbackLift(metrics: ViewportMetrics): number { if (!shouldUseInAppKeyboardFallback()) return 0; + if (metrics.hasVirtualKeyboard && metrics.keyboardHeight <= 0) return 0; const viewportAlreadyShrank = metrics.visibleHeight < metrics.layoutHeight * 0.82; const keyboardAlreadyDetected = metrics.keyboardHeight > 80; @@ -197,6 +238,26 @@ function computeFallbackLift(metrics: ViewportMetrics): number { ); } +function shouldReleaseFallback( + metrics: ViewportMetrics, + focusedAt: number, +): boolean { + if (focusedAt <= 0) return false; + if (performance.now() - focusedAt < FALLBACK_CLOSE_GRACE_MS) return false; + return !hasDetectedKeyboard(metrics); +} + +function hasDetectedKeyboard(metrics: ViewportMetrics): boolean { + return ( + metrics.keyboardHeight > 80 || + metrics.visibleHeight < metrics.layoutHeight * 0.82 + ); +} + +function isPossibleKeyboardCloseEvent(event: Event): boolean { + return event.type === "resize" || event.type === "geometrychange"; +} + function shouldUseInAppKeyboardFallback(): boolean { if (typeof navigator === "undefined") return false; const ua = navigator.userAgent; @@ -216,3 +277,14 @@ function resetVars(): void { document.documentElement.style.setProperty(VIEWPORT_VAR, "100dvh"); document.documentElement.style.setProperty(INPUT_LIFT_VAR, "0px"); } + +interface VirtualKeyboardLike extends EventTarget { + readonly boundingRect: DOMRectReadOnly; +} + +function getVirtualKeyboard(): VirtualKeyboardLike | null { + const nav = navigator as Navigator & { + virtualKeyboard?: VirtualKeyboardLike; + }; + return nav.virtualKeyboard ?? null; +}