Files
jrshikoku/lib/useKeyboardAvoid.ts
harukin-expo-dev-env a2912d77ae キーボード回避ロジックをuseKeyboardAvoid hookに共通化
3コンポーネントに重複していたキーボード処理を lib/useKeyboardAvoid.ts に集約:
- Androidの偽イベント(height<100)ガード+キャッシュ
- hide→show高速切替のデバウンス(100ms)
- Android measure()の150ms遅延
- LayoutAnimation easeInEaseOut

対象: AllTrainDiagramView, SearchUnitBox, StationDiagramView

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 09:56:51 +00:00

123 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from "react";
import { Keyboard, LayoutAnimation, Platform } from "react-native";
interface UseKeyboardAvoidOptions {
/** measure()対象のViewのref。指定するとrefの画面座標からオフセットを精密計算する */
measureRef?: React.RefObject<any>;
/** iOS用のタブバー高さfallback計算時に減算 */
tabBarHeight?: number;
}
interface UseKeyboardAvoidResult {
/** キーボードが表示中か */
keyboardVisible: boolean;
/** キーボードの生の高さ(キャッシュ済み) */
keyboardHeight: number;
/** measure()またはfallbackで計算されたオフセット値paddingBottom/bottomに使う */
measuredOffset: number;
}
const LAYOUT_ANIM_CONFIG = {
duration: 250,
update: { type: LayoutAnimation.Types.easeInEaseOut },
};
/**
* キーボード回避の共通hook。
* - Androidの偽イベントheight<100をガードキャッシュで対応
* - hide→show高速切替時のデバウンス100ms
* - Android measure()の150ms遅延
*/
export function useKeyboardAvoid(
options: UseKeyboardAvoidOptions = {}
): UseKeyboardAvoidResult {
const { measureRef, tabBarHeight = 0 } = options;
const [keyboardVisible, setKeyboardVisible] = useState(false);
const [keyboardHeight, setKeyboardHeight] = useState(0);
const [measuredOffset, setMeasuredOffset] = useState(0);
const showTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const lastValidKbRef = useRef<{
height: number;
screenY: number;
} | null>(null);
useEffect(() => {
const doMeasure = (kbScreenY: number, kbHeight: number) => {
if (measureRef?.current) {
(measureRef.current as any).measure(
(
_x: number,
_y: number,
_w: number,
h: number,
_pageX: number,
pageY: number
) => {
const bottomY = pageY + h;
const offset = Math.max(0, bottomY - kbScreenY);
LayoutAnimation.configureNext(LAYOUT_ANIM_CONFIG);
setMeasuredOffset(offset);
}
);
} else {
LayoutAnimation.configureNext(LAYOUT_ANIM_CONFIG);
setMeasuredOffset(
Platform.OS === "ios" ? kbHeight - tabBarHeight : kbHeight
);
}
};
const showSubscription = Keyboard.addListener("keyboardDidShow", (e) => {
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current);
hideTimerRef.current = null;
}
if (showTimerRef.current) clearTimeout(showTimerRef.current);
const isValid = e.endCoordinates.height >= 100;
const kbInfo = isValid
? {
height: e.endCoordinates.height,
screenY: e.endCoordinates.screenY,
}
: lastValidKbRef.current;
if (!kbInfo) return;
if (isValid) lastValidKbRef.current = kbInfo;
setKeyboardVisible(true);
setKeyboardHeight(kbInfo.height);
if (Platform.OS === "android") {
showTimerRef.current = setTimeout(
() => doMeasure(kbInfo.screenY, kbInfo.height),
150
);
} else {
doMeasure(kbInfo.screenY, kbInfo.height);
}
});
const hideSubscription = Keyboard.addListener("keyboardDidHide", () => {
if (showTimerRef.current) clearTimeout(showTimerRef.current);
hideTimerRef.current = setTimeout(() => {
LayoutAnimation.configureNext(LAYOUT_ANIM_CONFIG);
setKeyboardVisible(false);
setKeyboardHeight(0);
setMeasuredOffset(0);
}, 100);
});
return () => {
if (showTimerRef.current) clearTimeout(showTimerRef.current);
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
showSubscription.remove();
hideSubscription.remove();
};
}, [measureRef, tabBarHeight]);
return { keyboardVisible, keyboardHeight, measuredOffset };
}