import { Ionicons } from "@expo/vector-icons"; import React, { useEffect, useState } from "react"; import { Pressable, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View, } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue, withSpring, withTiming, } from "react-native-reanimated"; interface DraggablePanelProps { minHeightPct?: number; maxHeightPct?: number; initialState?: "min" | "max"; onExpandedChange?: (expanded: boolean) => void; children?: React.ReactNode; } export default function DraggablePanel({ minHeightPct = 0.1, maxHeightPct = 0.6, initialState = "min", onExpandedChange, children, }: DraggablePanelProps) { const { height: screenHeight } = useWindowDimensions(); // Thêm chiều cao của bottom tab bar vào tính toán const bottomOffset = 80; // 50 là chiều cao mặc định của tab bar const minHeight = screenHeight * minHeightPct; const maxHeight = screenHeight * maxHeightPct; // State để quản lý icon const [iconName, setIconName] = useState<"chevron-down" | "chevron-up">( initialState === "max" ? "chevron-down" : "chevron-up" ); // Sử dụng translateY để điều khiển vị trí const translateY = useSharedValue( initialState === "min" ? screenHeight - minHeight - bottomOffset : screenHeight - maxHeight - bottomOffset ); const isExpanded = useSharedValue(initialState === "max"); // Update khi screen height thay đổi (xoay màn hình) useEffect(() => { const currentHeight = isExpanded.value ? maxHeight : minHeight; translateY.value = screenHeight - currentHeight - bottomOffset; }, [screenHeight, minHeight, maxHeight, bottomOffset]); const notifyExpandedChange = (expanded: boolean) => { if (onExpandedChange) { onExpandedChange(expanded); } }; const togglePanel = () => { const newExpanded = !isExpanded.value; isExpanded.value = newExpanded; const targetY = newExpanded ? screenHeight - maxHeight - bottomOffset : screenHeight - minHeight - bottomOffset; translateY.value = withTiming(targetY, { duration: 500, }); notifyExpandedChange(newExpanded); }; const startY = useSharedValue(0); const panGesture = Gesture.Pan() .onStart(() => { startY.value = translateY.value; }) .onUpdate((event) => { // Cập nhật translateY theo gesture const newY = startY.value + event.translationY; // Clamp giá trị trong khoảng [screenHeight - maxHeight - bottomOffset, screenHeight - minHeight - bottomOffset] const minY = screenHeight - maxHeight - bottomOffset; const maxY = screenHeight - minHeight - bottomOffset; if (newY >= minY && newY <= maxY) { translateY.value = newY; } else if (newY < minY) { translateY.value = minY; } else if (newY > maxY) { translateY.value = maxY; } }) .onEnd((event) => { // Tính toán vị trí để snap const currentHeight = screenHeight - translateY.value - bottomOffset; const midHeight = (minHeight + maxHeight) / 2; // Kiểm tra velocity để quyết định snap const snapToMax = event.velocityY < -100 || currentHeight > midHeight; const targetY = snapToMax ? screenHeight - maxHeight - bottomOffset + 40 : screenHeight - minHeight - bottomOffset; translateY.value = withSpring( targetY, { damping: 20, stiffness: 200, }, () => { "worklet"; isExpanded.value = snapToMax; } ); }); const animatedStyle = useAnimatedStyle(() => { return { transform: [{ translateY: translateY.value }], }; }); // Sử dụng useAnimatedReaction để cập nhật icon dựa trên chiều cao useAnimatedReaction( () => { const currentHeight = screenHeight - translateY.value - bottomOffset; return currentHeight > minHeight + 10; }, (isCurrentlyExpanded) => { const newIcon = isCurrentlyExpanded ? "chevron-down" : "chevron-up"; runOnJS(setIconName)(newIcon); } ); return ( {/* Header với drag handle và nút toggle */} {/* Nội dung */} {children || ( Draggable Panel Click hoặc kéo để mở rộng panel này Min: {(minHeightPct * 100).toFixed(0)}% | Max:{" "} {(maxHeightPct * 100).toFixed(0)}% )} ); } const styles = StyleSheet.create({ panelContainer: { position: "absolute", left: 0, right: 0, bottom: 0, height: "100%", pointerEvents: "box-none", }, panel: { position: "absolute", top: 0, left: 0, right: 0, backgroundColor: "white", borderTopLeftRadius: 20, borderTopRightRadius: 20, shadowColor: "#000", shadowOffset: { width: 0, height: -2, }, shadowOpacity: 0.25, shadowRadius: 8, elevation: 10, }, header: { paddingTop: 12, paddingBottom: 8, paddingHorizontal: 16, alignItems: "center", justifyContent: "center", }, dragHandle: { width: 40, height: 4, backgroundColor: "#D1D5DB", borderRadius: 2, marginBottom: 8, }, toggleButton: { position: "absolute", right: 16, top: 1, padding: 4, }, content: { flex: 1, paddingHorizontal: 16, // paddingBottom: 16, }, placeholderContent: { alignItems: "center", paddingTop: 20, }, placeholderText: { fontSize: 18, fontWeight: "600", color: "#333", marginBottom: 8, }, placeholderSubtext: { fontSize: 14, color: "#666", textAlign: "center", marginTop: 4, }, });