14/02/2026
- Skab en Fængslende Brugeroplevelse med Dynamiske BottomSheets i React Native
- Hvad er en BottomSheet?
- Hvorfor Bygge en Fra Bunden?
- Vores Værktøjskasse: Reanimated og Gesture Handler
- Opsætning af Projektet
- Oprettelse af BottomSheet Komponentens Grundstruktur
- Implementering af Pan Gestus
- Tilføjelse af Dynamisk Border Radius
- Tilføjelse af Ekstern Kontrol (Programmatisk Styring)
- Tilføjelse af en Backdrop
- Konklusion
Skab en Fængslende Brugeroplevelse med Dynamiske BottomSheets i React Native
I en verden af mobilapplikationer er brugergrænsefladen (UI) kongen. En veludført UI kan transformere en god app til en fremragende en. Blandt de mange UI-mønstre, der er tilgængelige for udviklere, skiller bottom sheets sig ud som et utroligt alsidigt og brugervenligt element. Disse pop-up-paneler, der glider op fra bunden af skærmen, er ideelle til at præsentere yderligere indhold eller handlinger uden at afbryde den primære brugerflow. I denne dybdegående guide vil vi dykke ned i, hvordan man bygger en sådan dynamisk og responsiv bottom sheet fra bunden i React Native ved hjælp af de kraftfulde pakker, React Native Reanimated og React Native Gesture Handler.

Hvad er en BottomSheet?
En bottom sheet er et UI-komponent, der typisk vises fra bunden af skærmen for at afsløre mere information eller give adgang til relaterede funktioner. Tænk på det som en udvidelse af den eksisterende visning, der giver en organisk måde at introducere nyt indhold på. De bruges ofte til ting som:
- Visning af valgmuligheder i en liste
- Præsentation af detaljerede oplysninger
- Formularer til indtastning af data
- Kontrolpaneler med forskellige funktioner
Det smukke ved bottom sheets er deres evne til at bevare brugerens kontekst. Brugeren kan stadig se den underliggende skærm, hvilket gør det nemmere at vende tilbage til den oprindelige opgave, når de er færdige med interaktionen i bottom sheet.
Hvorfor Bygge en Fra Bunden?
Selvom der findes mange tredjepartsbiblioteker, der tilbyder bottom sheet-komponenter, er der betydelige fordele ved at bygge din egen. At mestre disse kerneteknologier giver dig fuld kontrol over udseendet, følelsen og funktionaliteten af din bottom sheet. Du kan skræddersy animationer, gestus og interaktioner præcist til dine behov, hvilket resulterer i en mere poleret og unik brugeroplevelse. Desuden giver det en dybere forståelse af React Native's animationssystem og gestus-håndtering.
Vores Værktøjskasse: Reanimated og Gesture Handler
For at opnå en flydende og naturlig brugeroplevelse vil vi anvende to essentielle pakker:
- React Native Reanimated: Dette bibliotek giver os mulighed for at skabe komplekse og performante animationer direkte på UI-tråden. Det er afgørende for at opnå glatte overgange og responstid, der føles naturlig for brugeren.
- React Native Gesture Handler: Denne pakke giver os mulighed for at opfange og håndtere forskellige brugergestus, såsom træk (pan), klem (pinch) og tryk (tap), på en robust og effektiv måde.
Ved at kombinere disse to kraftfulde værktøjer kan vi skabe en bottom sheet, der ikke kun ser godt ud, men også føles utroligt responsiv og intuitiv at interagere med.
Opsætning af Projektet
Lad os starte med at sætte et nyt React Native-projekt op med Expo og TypeScript. Dette giver os en hurtig og problemfri udviklingsoplevelse.
npx create-expo-app BottomSheetAnimation -t expo-template-blank-typescript cd BottomSheetAnimation Installer derefter de nødvendige afhængigheder:
npx expo install react-native-reanimated react-native-gesture-handler Åbn nu din App.tsx-fil og konfigurer den med en mørk baggrund og korrekt statuslinje-konfiguration:
import { StatusBar } from 'expo-status-bar' ; import { StyleSheet, View } from 'react-native' ; import { gestureHandlerRootHOC } from 'react-native-gesture-handler' ; function App ( ) { return ( < View style = { styles . container } > < StatusBar style = "light" /> {/* Her vil vi senere tilføje vores BottomSheet komponent */} < / View > ) ; } const styles = StyleSheet . create ( { container: { flex: 1, backgroundColor: '#111', }, } ) ; export default gestureHandlerRootHOC ( App ) ; Oprettelse af BottomSheet Komponentens Grundstruktur
Nu er det tid til at skabe selve BottomSheet-komponenten. Vi starter med en grundlæggende struktur, der definerer dens udseende og position på skærmen.
import { Dimensions, StyleSheet, View } from 'react-native' ; import React from 'react' ; import { GestureDetector } from 'react-native-gesture-handler' ; import Animated from 'react-native-reanimated' ; const { height: SCREEN_HEIGHT } = Dimensions . get ( 'window' ) ; type BottomSheetProps = { children ?: React . ReactNode ; } const BottomSheet: React . FC < BottomSheetProps > = ( { children } ) => { // Vi vil senere tilføje gestus-håndtering her const gesture = Gesture . Pan ( ) ; return ( < GestureDetector gesture = { gesture } > { children } ); } const styles = StyleSheet . create ( { bottomSheetContainer: { height: SCREEN_HEIGHT, width: '100%', backgroundColor: 'white', position: 'absolute', top: SCREEN_HEIGHT, // Start skjult nederst på skærmen borderRadius: 25, }, line: { width: 75, height: 4, backgroundColor: 'grey', alignSelf: 'center', marginVertical: 15, borderRadius: 2, }, } ) export default BottomSheet ; For at bruge denne komponent skal vi importere den i App.tsx og placere den under StatusBar:
import { StatusBar } from 'expo-status-bar' ; import { StyleSheet, View } from 'react-native' ; import { gestureHandlerRootHOC } from 'react-native-gesture-handler' ; import BottomSheet from './BottomSheet' // Antag at BottomSheet.tsx er i samme mappe function App ( ) { return ( < View style = { styles . container } > < StatusBar style = "light" /> {/* Indhold til din BottomSheet kan placeres her */} < / View > ) ; } const styles = StyleSheet . create ( { container: { flex: 1, backgroundColor: '#111', }, } ) ; export default gestureHandlerRootHOC ( App ) ; Implementering af Pan Gestus
Nu gør vi vores bottom sheet interaktiv. Vi vil bruge Reanimated's useSharedValue til at holde styr på dens position og Gesture Handler til at håndtere træk-gestus.
Opdater din BottomSheet.tsx som følger:
import { Dimensions, StyleSheet, View } from 'react-native' ; import React, { useCallback } from 'react' ; import { Gesture, // Importer Gesture direkte GestureDetector } from 'react-native-gesture-handler' ; import Animated, { useAnimatedStyle , useSharedValue , withSpring , runOnJS // Tilføjet for at kalde funktioner på JS-tråden } from 'react-native-reanimated' ; const { height: SCREEN_HEIGHT } = Dimensions . get ( 'window' ) ; // Vi definerer en maksimal forskydning for at lukke bottom sheet const MAX_TRANSLATE_Y = - SCREEN_HEIGHT + 50 ; type BottomSheetProps = { children ?: React . ReactNode ; // Callback til at informere forælderen om ændringer i aktiv tilstand onAnimate ?: ( isActive: boolean ) => void; } const BottomSheet: React . FC < BottomSheetProps > = ( { children, onAnimate } ) => { const translateY = useSharedValue ( 0 ) ; const context = useSharedValue ( { y: 0 } ) ; const active = useSharedValue ( false ); // Til at holde styr på om sheet er aktivt const gesture = Gesture . Pan ( ) . onStart ( ( ) => { context . value = { y: translateY . value } ; } ) . onUpdate ( ( event ) => { // Opdater translateY baseret på gestus og bevar den tidligere position translateY . value = event . translationY + context . value . y ; // Sørg for at bottom sheet ikke trækkes for langt op translateY . value = Math . max ( translateY . value, MAX_TRANSLATE_Y ) ; // Sørg for at bottom sheet ikke trækkes for langt ned translateY . value = Math . min ( translateY . value, 0 ); }) . onEnd ( ( ) => { // Bestem om bottom sheet skal lukkes eller åbnes helt baseret på position if ( translateY . value > - SCREEN_HEIGHT / 3 ) { translateY . value = withSpring ( 0 ) ; active.value = false; if (onAnimate) { runOnJS(onAnimate)(false); } } else if ( translateY . value < - SCREEN_HEIGHT / 1.5 ) { translateY . value = withSpring ( MAX_TRANSLATE_Y ) ; active.value = true; if (onAnimate) { runOnJS(onAnimate)(true); } } else { // Hvis den er i midten, luk den translateY . value = withSpring ( 0 ) ; active.value = false; if (onAnimate) { runOnJS(onAnimate)(false); } } }) // Animeret stil for bottom sheet const rBottomSheetStyle = useAnimatedStyle ( ( ) => { return { transform: [ { translateY: translateY . value } ] , } } ) ; return ( { children } ); } const styles = StyleSheet . create ( { bottomSheetContainer: { height: SCREEN_HEIGHT, width: '100%', backgroundColor: 'white', position: 'absolute', top: SCREEN_HEIGHT, // Starter skjult borderRadius: 25, }, line: { width: 75, height: 4, backgroundColor: 'grey', alignSelf: 'center', marginVertical: 15, borderRadius: 2, }, } ) export default BottomSheet ; Tilføjelse af Dynamisk Border Radius
For at gøre vores bottom sheet endnu mere visuelt tiltalende, kan vi tilføje en animeret border radius, der ændrer sig, når sheet'et bevæger sig. Dette giver en flot visuel effekt, der understreger bevægelsen.
Opdater rBottomSheetStyle i BottomSheet.tsx:
import { Extrapolate , interpolate , useAnimatedStyle , useSharedValue , withSpring , } from 'react-native-reanimated' ; // ... resten af din BottomSheet-komponent ... const rBottomSheetStyle = useAnimatedStyle ( ( ) => { const borderRadius = interpolate ( translateY . value , [ MAX_TRANSLATE_Y + 50, MAX_TRANSLATE_Y ], // Input-værdier for interpolation [ 25, 5 ], // Output-værdier for border radius Extrapolate . CLAMP // Sikrer at værdien forbliver inden for defineret område ) ; return { borderRadius , transform: [ { translateY: translateY . value } ] , } } ) // ... resten af din BottomSheet-komponent ... Tilføjelse af Ekstern Kontrol (Programmatisk Styring)
Ofte vil du have brug for at kunne styre bottom sheet'et programmatisk, f.eks. ved at åbne eller lukke det via en knap. Vi kan opnå dette ved at bruge forwardRef og useImperativeHandle.
Opdater din BottomSheet.tsx:
import React, { useCallback, useImperativeHandle, forwardRef } from 'react' ; import { Gesture, GestureDetector } from 'react-native-gesture-handler' ; import Animated, { useAnimatedStyle , useSharedValue , withSpring , runOnJS } from 'react-native-reanimated' ; // ... SCREEN_HEIGHT og MAX_TRANSLATE_Y definitioner ... export type BottomSheetRefProps = { scrollTo: ( destination: number ) => void ; isActive: ( ) => boolean ; } // Brug forwardRef til at give adgang til metoder på komponenten const BottomSheet = forwardRef < BottomSheetRefProps, BottomSheetProps > ( ( { children, onAnimate }, ref ) => { const translateY = useSharedValue ( 0 ) ; const context = useSharedValue ( { y: 0 } ) ; const active = useSharedValue ( false ); // Metoder til at styre bottom sheet const scrollTo = useCallback ( ( destination: number ) => { 'worklet' ; active.value = destination !== 0; translateY . value = withSpring ( destination, { damping: 50 } ) ; if (onAnimate) { runOnJS(onAnimate)(active.value); } }, [ onAnimate ] // Afhængighed af onAnimate callback ) ; const isActive = useCallback ( ( ) => { return active . value ; }, [ ] ) ; // Eksponer metoderne til forældrekomponenten useImperativeHandle ( ref, () => ({ scrollTo, isActive }), [ scrollTo, isActive] ); const gesture = Gesture . Pan ( ) . onStart ( ( ) => { context . value = { y: translateY . value } ; } ) . onUpdate ( ( event ) => { translateY . value = event . translationY + context . value . y ; translateY . value = Math . max ( translateY . value, MAX_TRANSLATE_Y ) ; translateY . value = Math . min ( translateY . value, 0 ); }) . onEnd ( ( ) => { if ( translateY . value > - SCREEN_HEIGHT / 3 ) { translateY . value = withSpring ( 0 ) ; active.value = false; if (onAnimate) { runOnJS(onAnimate)(false); } } else if ( translateY . value < - SCREEN_HEIGHT / 1.5 ) { translateY . value = withSpring ( MAX_TRANSLATE_Y ) ; active.value = true; if (onAnimate) { runOnJS(onAnimate)(true); } } else { translateY . value = withSpring ( 0 ) ; active.value = false; if (onAnimate) { runOnJS(onAnimate)(false); } } }) const rBottomSheetStyle = useAnimatedStyle ( ( ) => { const borderRadius = interpolate ( translateY . value , [ MAX_TRANSLATE_Y + 50, MAX_TRANSLATE_Y ] , [ 25, 5 ] , Extrapolate . CLAMP ) ; return { borderRadius , transform: [ { translateY: translateY . value } ] , } } ) ; return ( { children } ); } ); const styles = StyleSheet . create ( { bottomSheetContainer: { height: SCREEN_HEIGHT, width: '100%', backgroundColor: 'white', position: 'absolute', top: SCREEN_HEIGHT, // Starter skjult borderRadius: 25, }, line: { width: 75, height: 4, backgroundColor: 'grey', alignSelf: 'center', marginVertical: 15, borderRadius: 2, }, } ) export default BottomSheet ; Nu kan vi bruge dette i App.tsx:
import { StatusBar } from 'expo-status-bar' ; import { StyleSheet, View, Button } from 'react-native' ; import { gestureHandlerRootHOC } from 'react-native-gesture-handler' ; import BottomSheet, { BottomSheetRefProps } from './BottomSheet' ; import React, { useRef } from 'react' ; function App ( ) { const bottomSheetRef = useRef < BottomSheetRefProps > ( null ) ; const toggleBottomSheet = ( ) => { if ( ! bottomSheetRef . current ) return; const isActive = bottomSheetRef.current.isActive(); bottomSheetRef.current.scrollTo(isActive ? 0: -SCREEN_HEIGHT / 2); // Åbner/lukker til midten } return ( < View style = { styles . container } > < StatusBar style = "light" /> {/* Indhold til din BottomSheet */} Indhold i Bottom Sheet < / View > ) ; } const styles = StyleSheet . create ( { container: { flex: 1, backgroundColor: '#111', justifyContent: 'flex-end', // Juster for at placere knappen paddingBottom: 50, // Lidt plads til knappen }, } ) ; export default gestureHandlerRootHOC ( App ) ; Tilføjelse af en Backdrop
En typisk bottom sheet har en semi-transparent baggrundsfarve (backdrop), der dæmper den underliggende visning og giver visuel feedback, når sheet'et åbnes eller lukkes. Vi kan også animere denne backdrop.
Tilføj denne komponent til din BottomSheet.tsx:
import { SharedValue , useAnimatedStyle , withTiming , } from 'react-native-reanimated' ; type AnimatedBackdropProps = { active: SharedValue < boolean > ; onTap: ( ) => void ; } const AnimatedBackdrop: React . FC < AnimatedBackdropProps > = ( { active, onTap } ) => { const rBackdropStyle = useAnimatedStyle ( ( ) => { return { opacity: withTiming ( active . value ? 1: 0, { duration: 200 } ) , // pointerEvents styrer om elementet kan interagere med brugeren pointerEvents: active . value ? 'auto': 'none' , } } ) ; return ( ) } // Tilføj denne stil til din eksisterende StyleSheet const styles = StyleSheet . create ( { // ... eksisterende styles ... backdropContainer: { backgroundColor: 'rgba(0, 0, 0, 0.5)', // Mørk, semi-transparent baggrund }, } ) Og integrer den i din BottomSheet-komponent:
// ... imports ... const BottomSheet = forwardRef < BottomSheetRefProps, BottomSheetProps > ( ( { children, onAnimate }, ref ) => { // ... eksisterende state og ref opsætning ... return ( <> {/* Tilføj Backdrop-komponenten */} { scrollTo ( 0 ) ; } } /> { children } ); } ); // ... resten af koden ... Husk at importere AnimatedBackdrop og inkludere den i din return-statement.
Konklusion
Vi har nu succesfuldt bygget en dynamisk og interaktiv bottom sheet-komponent i React Native. Gennem brugen af React Native Reanimated og React Native Gesture Handler har vi opnået flydende animationer, intuitiv gestus-håndtering og mulighed for programmatisk kontrol. Denne tilgang giver dig den ultimative fleksibilitet til at designe brugeroplevelser, der passer perfekt til din applikations behov.
Tabel: Sammenligning af Gestus-håndtering
| Gestus | Formål | Relevante Reanimated Funktioner |
|---|---|---|
| Pan (Træk) | Åbne/lukke BottomSheet, scroll | useSharedValue, onUpdate, onEnd, withSpring |
| Tap (Tryk) | Lukke BottomSheet via Backdrop | Gesture.Tap(), onPress |
Ofte Stillede Spørgsmål (FAQ)
- Hvad er den primære fordel ved at bruge Reanimated og Gesture Handler?
De muliggør højtydende, native-lignende animationer og gestus-håndtering direkte på UI-tråden, hvilket giver en markant bedre brugeroplevelse sammenlignet med JavaScript-baserede animationer. - Kan jeg tilføje flere "snap points" til min BottomSheet?
Ja, ved at justere logikken ionEnd-metoden kan du definere flere destinationer (snap points), som bottom sheet'et kan glide til. - Hvordan håndterer jeg forskellige skærmstørrelser?
Ved at brugeDimensions.get('window')sikrer du, at dimensionerne er dynamiske og tilpasser sig enhedens skærmstørrelse. - Er denne komponent kompatibel med Expo?
Ja, med korrekt opsætning og installation af pakkerne er den fuldt kompatibel med Expo.
Dette er et solidt fundament for en avanceret bottom sheet-komponent. For produktionsklare applikationer kan du overveje at tilføje flere funktioner som f.eks. brugerdefinerede ikoner, fade-effekter på indholdet, når det bliver synligt, eller endda muligheden for at trække indholdet ind i bottom sheet'et.
Hvis du vil læse andre artikler, der ligner Byg en dynamisk BottomSheet i React Native, kan du besøge kategorien Mobilapps.
