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>
123 lines
3.9 KiB
TypeScript
123 lines
3.9 KiB
TypeScript
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 };
|
||
}
|