Gesture Handler — surish, tortish, swipe-to-delete
Muammodan boshlaylik
Mobil foydalanuvchi faqat bosmaydi — u suradi, tortadi, chimchilaydi, ikki marta bosadi:
- Xatni chapga surib o'chirish (Gmail, Telegram)
- Rasmni ikki barmoq bilan kattalashtirish
- Kartani surib tashlash (Tinder)
- Bottom sheet ni barmoq bilan yuqoriga tortish
Pressable faqat bosishni biladi. Murakkab imo-ishoralar uchun — React Native Gesture Handler, Reanimated bilan birga.
O'rnatish
Expo shablonida bor. Bo'lmasa:
npx expo install react-native-gesture-handler
Ildizni o'rash (Expo Router da odatda allaqachon):
// app/_layout.tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack />
</GestureHandlerRootView>
);
}
Asosiy tuzilma
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
export function SuriladiganQuti() {
const x = useSharedValue(0);
const y = useSharedValue(0);
const surish = Gesture.Pan()
.onUpdate((e) => {
x.value = e.translationX;
y.value = e.translationY;
})
.onEnd(() => {
x.value = withSpring(0); // qo'yib yuborilganda — joyiga qaytadi
y.value = withSpring(0);
});
const stil = useAnimatedStyle(() => ({
transform: [{ translateX: x.value }, { translateY: y.value }],
}));
return (
<GestureDetector gesture={surish}>
<Animated.View style={[{ width: 100, height: 100, backgroundColor: '#0284c7', borderRadius: 16 }, stil]} />
</GestureDetector>
);
}
Gesture.Pan()— surish imo-ishorasi.onUpdate— barmoq harakatlanayotganda (worklet — UI thread da, 45-dars).onEnd— barmoq qo'yib yuborilgandaGestureDetector— imo-ishorani elementga ulaydi
Quti barmoq orqasidan kechikishsiz ergashadi — chunki hammasi UI thread da.
Imo-ishora turlari
| Gesture | Nima | Hodisa ma'lumoti |
|---|---|---|
Gesture.Tap() |
Bosish; .numberOfTaps(2) — ikki marta |
— |
Gesture.LongPress() |
Uzoq bosish | duration |
Gesture.Pan() |
Surish, tortish | translationX/Y, velocityX/Y |
Gesture.Pinch() |
Ikki barmoq bilan masshtab | scale |
Gesture.Rotation() |
Ikki barmoq bilan burish | rotation |
Gesture.Fling() |
Tez "otish" | yo'nalish |
Birlashtirish
const ikkiMarta = Gesture.Tap().numberOfTaps(2).onEnd(() => { masshtab.value = withSpring(2); });
const birMarta = Gesture.Tap().onEnd(() => { runOnJS(ochish)(); });
// Ikki marta bosish ustun; u bo'lmasa — bir marta
const bosish = Gesture.Exclusive(ikkiMarta, birMarta);
// Pinch va Pan bir vaqtda — rasmni kattalashtirib, surish
const rasm = Gesture.Simultaneous(pinch, pan);
Swipe-to-delete
Eng ko'p so'raladigan imo-ishora:
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle, withTiming, runOnJS, interpolate, FadeOut, LinearTransition } from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
const CHEGARA = -100;
export function SuriladiganBand({ matn, ochir }: { matn: string; ochir: () => void }) {
const x = useSharedValue(0);
const surish = Gesture.Pan()
.activeOffsetX([-10, 10]) // gorizontal 10 dan ko'p surilganda boshlansin
.failOffsetY([-10, 10]) // vertikal surish — ro'yxat scroll i uchun qoldirilsin
.onUpdate((e) => {
x.value = Math.min(0, e.translationX); // faqat chapga
})
.onEnd(() => {
if (x.value < CHEGARA) {
x.value = withTiming(-500, { duration: 200 }, () => runOnJS(ochir)());
runOnJS(Haptics.impactAsync)(Haptics.ImpactFeedbackStyle.Medium);
} else {
x.value = withTiming(0);
}
});
const bandStil = useAnimatedStyle(() => ({ transform: [{ translateX: x.value }] }));
const ikonkaStil = useAnimatedStyle(() => ({
opacity: interpolate(x.value, [0, CHEGARA], [0, 1]),
transform: [{ scale: interpolate(x.value, [0, CHEGARA], [0.5, 1], 'clamp') }],
}));
return (
<Animated.View exiting={FadeOut} layout={LinearTransition}>
{/* Orqadagi qizil fon */}
<View style={{ ...StyleSheet.absoluteFillObject, backgroundColor: '#dc2626', justifyContent: 'center', alignItems: 'flex-end', paddingRight: 24 }}>
<Animated.View style={ikonkaStil}>
<Ionicons name="trash" size={24} color="white" />
</Animated.View>
</View>
{/* Suriladigan qism */}
<GestureDetector gesture={surish}>
<Animated.View style={[{ backgroundColor: 'white', padding: 18 }, bandStil]}>
<Text style={{ fontSize: 16 }}>{matn}</Text>
</Animated.View>
</GestureDetector>
</Animated.View>
);
}
Muhim jihatlar:
activeOffsetX/failOffsetY— eng nozik joy. Busiz band ro'yxatning vertikal scroll ini "o'g'irlaydi" va ro'yxat aylanmaydi. Gorizontal harakat — surish, vertikal — scroll- Chegara — 100 birlikdan ko'p surilsa o'chadi, kam bo'lsa — qaytadi
- Ikonka asta paydo bo'ladi va kattalashadi — foydalanuvchi qancha surish kerakligini his qiladi
runOnJS(ochir)— holatni o'zgartirish JS daexiting+layout— o'chirilgan band yo'qoladi, qolganlar silliq yuqoriga siljiydi
[!TIP] Tayyor variant:
react-native-gesture-handlerdagiReanimatedSwipeablekomponenti — chap/o'ng amallar tugmalari bilan. O'z dizayningiz kerak bo'lmasa — shuni ishlating.
Pinch-to-zoom — rasm
const masshtab = useSharedValue(1);
const saqlangan = useSharedValue(1);
const pinch = Gesture.Pinch()
.onUpdate((e) => { masshtab.value = saqlangan.value * e.scale; })
.onEnd(() => {
if (masshtab.value < 1) masshtab.value = withSpring(1);
else if (masshtab.value > 4) masshtab.value = withSpring(4);
saqlangan.value = Math.min(Math.max(masshtab.value, 1), 4);
});
const ikkiMarta = Gesture.Tap().numberOfTaps(2).onEnd(() => {
const yangi = masshtab.value > 1 ? 1 : 2.5;
masshtab.value = withSpring(yangi);
saqlangan.value = yangi;
});
const stil = useAnimatedStyle(() => ({ transform: [{ scale: masshtab.value }] }));
<GestureDetector gesture={Gesture.Simultaneous(pinch, ikkiMarta)}>
<Animated.Image source={{ uri }} style={[{ width: '100%', aspectRatio: 1 }, stil]} />
</GestureDetector>
saqlangan — oldingi pinch natijasi: ikkinchi pinch birinchisi to'xtagan joydan davom etadi.
Bottom sheet
O'z bottom sheet ingizni yasash — chegaralar, tezlik (velocityY) bo'yicha yopish, ichidagi scroll bilan nizolar... Bu murakkab. Tayyor kutubxona — @gorhom/bottom-sheet (Reanimated + Gesture Handler ustida):
<BottomSheet snapPoints={['25%', '60%', '90%']} enablePanDownToClose>
<BottomSheetView>{/* kontent */}</BottomSheetView>
</BottomSheet>
Uch balandlikda "yopishadi", pastga tortib yopiladi.
UX qoidalari
- Imo-ishora — qo'shimcha yo'l. Har bir amal uchun ko'rinadigan muqobil bo'lsin (o'chirish — uzoq bosish menyusida ham). Imo-ishoralar yashirin, hamma ularni bilmaydi
- Tizim imo-ishoralari bilan to'qnashmang: ekranning chap cheti — iOS da "orqaga", pastki chet — uy
- Har bir harakatga javob: element barmoq orqasidan ergashsin, chegara o'tilganda — tebranish
- Tezlikni hisobga oling: tez "otish" (
velocityX) — qisqa masofada ham amalni bajarsin
Nima uchun shunday?
Nima uchun React Native ning o'z imo-ishora tizimi (PanResponder) emas, alohida kutubxona?
PanResponder — JavaScript thread da ishlaydi: har bir barmoq harakati native dan JS ga yuboriladi, JS hisoblaydi va natijani qaytaradi. JS band bo'lsa — element barmoqdan orqada qoladi, va bu darhol seziladi. Gesture Handler esa imo-ishoralarni native darajada, platformaning o'z mexanizmlari bilan taniydi va Reanimated bilan birga hammasini UI thread da qayta ishlaydi. Natija — native ilovalardagi kabi barmoqqa "yopishgan" harakat va scroll bilan to'g'ri ishlash.
Xulosa
Eslab qoling:
GestureHandlerRootView— ildizda;GestureDetector gesture={...}— elementdaGesture.Pan / Tap / LongPress / Pinch / Fling;.onUpdate,.onEnd— worklet lar- Reanimated
useSharedValuebilan — barmoq orqasidan kechikishsiz Exclusive,Simultaneous— birlashtirish- Swipe-to-delete:
activeOffsetX+failOffsetY— scroll bilan nizosiz; chegara;runOnJS - Tayyor:
ReanimatedSwipeable,@gorhom/bottom-sheet - Imo-ishora — qo'shimcha yo'l; ko'rinadigan muqobil bo'lsin
Keyingi dars
47-dars: UX va tezlik — skeleton, memo, rasm optimallash