Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail. GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift. - KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding - react-native-keyboard-controller installiert + KeyboardProvider in Root - Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO) - Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn - DPad-Buttons 60→48, more bg, no scale - useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates - dm.tsx: resolveAvatar wrap (iron.png-Warning) - Mail-error-humanizer + locales Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { Animated, Easing, Platform } from 'react-native';
|
|
import { useKeyboardHeight } from './useKeyboardHeight';
|
|
|
|
/**
|
|
* App-weite Composable für Sheets/Modals mit TextInput.
|
|
*
|
|
* Liefert ein **kombiniertes Animated.Value** für `transform: [{ translateY }]`,
|
|
* das gleichzeitig:
|
|
* - die Slide-In/Out-Animation des Sheets ausführt (von unten reinkommend)
|
|
* - das Sheet automatisch über die Tastatur lifted wenn TextInput fokussiert
|
|
*
|
|
* Beide Animationen laufen im **native driver** (Performance + smoother als
|
|
* height-Animationen). Kein Driver-Mix, kein Bouncing.
|
|
*
|
|
* Pattern (verifiziert auf EditMailAccountSheet + GameOverScreen):
|
|
* ```tsx
|
|
* const sheetH = SCREEN_HEIGHT * 0.5;
|
|
* const lift = useSheetKeyboardLift({ visible, offscreenY: sheetH });
|
|
*
|
|
* <Modal visible={visible}>
|
|
* <Animated.View
|
|
* style={{
|
|
* position: 'absolute',
|
|
* bottom: 0,
|
|
* height: sheetH,
|
|
* transform: [{ translateY: lift.translateY }],
|
|
* }}
|
|
* >
|
|
* {form content with TextInput}
|
|
* </Animated.View>
|
|
* </Modal>
|
|
* ```
|
|
*
|
|
* Anti-Pattern (was schief ging): `height: animatedValue` + `transform: animatedValue`
|
|
* auf demselben Animated.View → native-animated-module-Crash. Stattdessen feste
|
|
* height + nur translateY animieren.
|
|
*
|
|
* Anti-Pattern 2: `marginBottom: keyboardHeight` als JS-style + native transform
|
|
* im selben View → Bouncing weil zwei verschiedene Threads layouten.
|
|
*
|
|
* Für FlatList-basierte Sheets (PostCommentsSheet) ist das Pattern anders:
|
|
* dort wächst die Sheet-Höhe selbst weil eine variable Liste drin ist. Diese
|
|
* Composable ist für FIXED-HEIGHT-Form-Sheets gedacht.
|
|
*/
|
|
export interface SheetKeyboardLiftOptions {
|
|
/** Ob das Sheet aktuell sichtbar ist. Nur dann läuft Slide-In an. */
|
|
visible: boolean;
|
|
/** Y-Offset des Sheets im verborgenen Zustand (typischerweise = SHEET_HEIGHT). */
|
|
offscreenY: number;
|
|
/** Slide-Dauer in ms. Default 280. */
|
|
slideDurationMs?: number;
|
|
}
|
|
|
|
export function useSheetKeyboardLift({
|
|
visible,
|
|
offscreenY,
|
|
slideDurationMs = 280,
|
|
}: SheetKeyboardLiftOptions) {
|
|
const keyboardHeight = useKeyboardHeight();
|
|
const slideY = useRef(new Animated.Value(offscreenY)).current;
|
|
const keyboardLift = useRef(new Animated.Value(0)).current;
|
|
|
|
// Slide-In bei visible-Wechsel
|
|
useEffect(() => {
|
|
if (visible) {
|
|
slideY.setValue(offscreenY);
|
|
Animated.timing(slideY, {
|
|
toValue: 0,
|
|
duration: slideDurationMs,
|
|
easing: Easing.out(Easing.cubic),
|
|
useNativeDriver: true,
|
|
}).start();
|
|
}
|
|
}, [visible, offscreenY, slideDurationMs, slideY]);
|
|
|
|
// Keyboard-Lift (iOS only — Android adjustResize macht das im Manifest)
|
|
useEffect(() => {
|
|
if (Platform.OS !== 'ios') return;
|
|
Animated.timing(keyboardLift, {
|
|
toValue: keyboardHeight,
|
|
duration: 220,
|
|
easing: Easing.out(Easing.cubic),
|
|
useNativeDriver: true,
|
|
}).start();
|
|
}, [keyboardHeight, keyboardLift]);
|
|
|
|
return {
|
|
/** Direkt in `transform: [{ translateY }]` einsetzen. */
|
|
translateY: Animated.subtract(slideY, keyboardLift),
|
|
/** Manuelle Slide-Out-Animation (z.B. beim Close-Tap statt nur visible=false). */
|
|
slideOut: (cb?: () => void) =>
|
|
Animated.timing(slideY, {
|
|
toValue: offscreenY,
|
|
duration: 220,
|
|
useNativeDriver: true,
|
|
}).start(() => cb?.()),
|
|
/** Live keyboard-Höhe für extra Layout-Berechnungen wenn nötig. */
|
|
keyboardHeight,
|
|
};
|
|
}
|