может кому пригодиться без реаниматора v4
реаниматор v4 тупит без expo или я не смог настроить его нормально, но тупило жоска на последней версии RN.
реаниматор v3 не пробовал. надо старую версию RN накатывать
этот вариант очень простой через PanResponder
багов не увидел.
import React, { useEffect, useRef, useState } from 'react';
import {
View, Text, StyleSheet, Animated, Dimensions,
PanResponder, ScrollView
} from 'react-native';
const { height: screenHeight } = Dimensions.get('window');
const MIN_HEIGHT = 70;
const MAX_HEIGHT = 3400; // потолок, выше которого панель не открывать, даже если контент больше
const INITIAL_TRANSLATE_Y = screenHeight - MIN_HEIGHT;
const CLOSED_POSITION = INITIAL_TRANSLATE_Y;
const ScreenDemo1 = ({ navigation }) => {
const slideAnim = useRef(new Animated.Value(INITIAL_TRANSLATE_Y)).current;
const currentPosition = useRef(INITIAL_TRANSLATE_Y);
const [btmCoo, setBtmCoo] = useState(100);
const [isAtTop, setIsAtTop] = useState(false);
const [scrollY, setScrollY] = useState(0);
const [heightBottom, setHeightBottom] = useState(0);
// ref с тем же значением — он НЕ застывает в замыканиях PanResponder
const contentHeightRef = useRef(0);
const measuredRef = useRef(false); // флаг "высота уже посчитана"
// актуальный верхний предел открытия панели
const getMaxOpenPosition = () => {
const measuredHeight = measuredRef.current
? contentHeightRef.current // + MIN_HEIGHT + 70 на хэндл сверху контента
: MAX_HEIGHT; // пока высота не измерена — используем потолок как дефолт
const h = Math.min(measuredHeight, MAX_HEIGHT);
return screenHeight - h;
};
const updateIsAtTop = (value) => {
const newIsAtTop = Math.abs(value - getMaxOpenPosition()) < 15;
if (newIsAtTop !== isAtTop) {
setIsAtTop(newIsAtTop);
}
};
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (_, gestureState) => {
if (isAtTop && gestureState.dy > 0 && scrollY <= 5) {
return true;
}
return Math.abs(gestureState.dy) > 10;
},
onPanResponderGrant: () => {
slideAnim.stopAnimation();
},
onPanResponderMove: (_, gestureState) => {
let newPosition = currentPosition.current + gestureState.dy;
const maxOpenPosition = getMaxOpenPosition(); // читаем актуальное значение из ref
newPosition = Math.max(
maxOpenPosition,
Math.min(newPosition, CLOSED_POSITION)
);
setBtmCoo(newPosition);
//console.log('translateY:', newPosition);
slideAnim.setValue(newPosition);
updateIsAtTop(newPosition);
},
onPanResponderRelease: () => {
const finalPosition = slideAnim._value;
currentPosition.current = finalPosition;
updateIsAtTop(finalPosition);
},
})
).current;
useEffect(() => {
slideAnim.setValue(screenHeight);
Animated.timing(slideAnim, {
toValue: INITIAL_TRANSLATE_Y,
duration: 350,
useNativeDriver: true,
}).start(() => {
updateIsAtTop(INITIAL_TRANSLATE_Y);
});
}, []);
return (
<View style={styles.container}>
<View style={styles.backgroundContent}>
<Text style={styles.demoTitle}>Основной экран</Text>
<Text style={{ marginBottom: 240 }}>1 материалы</Text>
<Text style={{ marginBottom: 240 }}>2 материалы</Text>
<Text style={{ marginBottom: 240 }}>3 материалы</Text>
<Text style={{ marginBottom: 240 }}>4 материалы</Text>
<Text style={{ marginBottom: 240 }}>5 материалы</Text>
<Text style={{ }}>3 материалы</Text>
</View>
{btmCoo < 0 && (
<View style={{backgroundColor: '#529b00',position: 'absolute',top: 0,left: 0,right: 0,zIndex: 1999, }}>
<Text style={[styles.title, { color: '#fff' }]}>
Панель {btmCoo}
</Text>
</View>
)}
<Animated.View
style={[
styles.overlay,
{
backgroundColor: '#000',
transform: [{ translateY: slideAnim }],
},
]}
{...panResponder.panHandlers}
>
<View
onLayout={(e) => {
const { height } = e.nativeEvent.layout;
contentHeightRef.current = height; // используется внутри PanResponder
measuredRef.current = true;
setHeightBottom(height); // для рендера/отображения, если нужно
console.log('CONTENT HEIGHT:', height);
}}
style={{ backgroundColor: '#000' }}>
{btmCoo > 0 && (
<View style={{backgroundColor: '#580597',position: 'absolute',top: 0,left: 0,right: 0,zIndex: 1999, }}>
<Text style={[styles.title, { color: '#fff' }]}>
Панель {btmCoo}
</Text>
</View>
)}
<View style={{paddingBottom:20}}>
<Text style={{ marginBottom: 240, color: '#fff' }}>Вытягиваемый контент</Text>
<Text style={{ color: '#fff' }}>1 материалы</Text>
<Text style={{ color: '#fff',marginBottom: 240 }}>3 материалы</Text>
<Text style={{ color: '#fff',marginBottom: 240 }}>4 материалы</Text>
<Text style={{ color: '#fff',marginBottom: 240 }}>5 материалы</Text>
<Text style={{ color: '#fff',marginBottom: 240 }}>3 материалы</Text>
<Text style={{ color: '#fff',marginBottom: 240 }}>4 материалы</Text>
<Text style={{ color: '#fff',marginBottom: 240 }}>5 материалы</Text>
</View>
</View>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f0f2f5' },
backgroundContent: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 40,
},
demoTitle: { fontSize: 28, fontWeight: 'bold', color: '#333', marginBottom: 8 },
overlay: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
height: screenHeight,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: -6 },
shadowOpacity: 0.3,
shadowRadius: 16,
elevation: 25,
zIndex: 1000,
},
handleContainer: { alignItems: 'center', paddingVertical: 12 },
handle: {
width: 42,
height: 5,
backgroundColor: '#ccc',
borderRadius: 999,
},
title: { fontSize: 24, fontWeight: '700', marginBottom: 24 },
});
export default ScreenDemo1;