Задать вопрос
@User782
Кратко о себе

Как сделать беспрерывный свайп?

Это демо экрана полностью рабочее:

import React, { useEffect, useRef } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  Animated,
  Dimensions,
  Image,
  PanResponder,
} from 'react-native';

const { height: screenHeight } = Dimensions.get('window');

const MIN_HEIGHT = 100;
const INITIAL_TRANSLATE_Y = screenHeight - MIN_HEIGHT;

const ScreenDemo = ({ navigation, theme }) => {
  const slideAnim = useRef(new Animated.Value(INITIAL_TRANSLATE_Y)).current;
  const currentPosition = useRef(INITIAL_TRANSLATE_Y);
  const isVisible = useRef(true);

  const CLOSED_POSITION = INITIAL_TRANSLATE_Y; // уровень открытия экрана

  
  const panResponder = useRef(
    PanResponder.create({
      onMoveShouldSetPanResponder: (_, gestureState) => {
        return Math.abs(gestureState.dy) > 10; 
      },

      onPanResponderGrant: () => {
        slideAnim.stopAnimation(); // Останавливаем любую анимацию при касании
      },

      onPanResponderMove: (_, gestureState) => {
  const newPosition = Math.max(
    0,
    Math.min(
      currentPosition.current + gestureState.dy,
      CLOSED_POSITION
    )
  );

  slideAnim.setValue(newPosition);
},

      onPanResponderRelease: (_, gestureState) => {
    
        currentPosition.current = slideAnim._value;

       
      },
    })
  ).current;

  // Начальное появление
  useEffect(() => {
    slideAnim.setValue(screenHeight);
    Animated.timing(slideAnim, {
      toValue: INITIAL_TRANSLATE_Y,
      duration: 350,
      useNativeDriver: true,
    }).start();
  }, []);
  return (
    <View style={styles.container}>
      <View style={styles.backgroundContent}>
        <Text style={styles.demoTitle}>Основной экран</Text>      
         <Text style={{marginBottom:440}}>1 материалы</Text>
             <Text style={{marginBottom:440}}>2 материалы</Text>
              <Text style={{marginBottom:440}}>4 материалы</Text>
               <Text style={{marginBottom:440}}>5 материалы</Text> 
      </View>

      <Animated.View
        style={[
          styles.overlay,
          {
            backgroundColor: theme === 'dark' ? '#1d2632' : '#ffffff',
            transform: [{ translateY: slideAnim }],
          },
        ]}
        {...panResponder.panHandlers}
      >
        <View style={styles.handleContainer}>
          <View style={styles.handle} />
        </View>     

        <View style={styles.content}>
          <Text style={[styles.title, theme === 'dark' && styles.textDark]}>
            Панель
          </Text>        
            <Text style={{marginBottom:440}}>1 материалы</Text>
             <Text style={{marginBottom:440}}>2 материалы</Text>
              <Text style={{marginBottom:440}}>4 материалы</Text>
               <Text style={{marginBottom:440}}>5 материалы</Text>
             
        </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 },
  demoSubtitle: { fontSize: 16, color: '#666', textAlign: 'center' },  
  handleContainer: { alignItems: 'center', paddingVertical: 12 },
  handle: {
    width: 42,
    height: 5,
    backgroundColor: '#ccc',
    borderRadius: 999,
  },  
  title: { fontSize: 24, fontWeight: '700', marginBottom: 24, color: '#222' },
  section: { marginBottom: 12, padding: 18, borderRadius: 12 },
  sectionText: { fontSize: 16, fontWeight: '600', color: '#333' },  
  infoText: { fontSize: 13, color: '#666', textAlign: 'center' },
  textDark: { color: '#fff' },
});

export default ScreenDemo;

Сейчас на экране есть основной контент длинный, который требует скрола, чтобы весь просмотреть.
А также есть язычок, приоткрытый снизу. Если тянуть язычок, он будет следовать за пальцем, раскрывая содержимое.
Язычок тоже содержит контент, который требует скрола, чтобы весь просмотреть.

Задача следующая:

1. Если свайпить основной контент, то он должен просто скролится свайпиться.
2. Если вытянуть язычок до верхнего предела, то должен включиться скрол-свайп содержимого язычка.
То есть, свайп вверх продолжается, но так как язык на верхнем пределе, то свайпится содержимое языка.
3. Если начать свайпить вниз содержимое язычка, то доходя до передела включается свайп языка вниз.
4. Переключения свайпов без поднятия пальца от экрана.
  • Вопрос задан
  • 113 просмотров
Подписаться 1 Сложный Комментировать
Помогут разобраться в теме Все курсы
  • Stepik
    React для современных веб-приложений
    1 месяц
    Далее
  • beONmax
    Курс JavaScript – полный курс с нуля до результата
    1 месяц
    Далее
  • Stepik
    Redux для управления состоянием React-приложений
    1 месяц
    Далее
Пригласить эксперта
Ответы на вопрос 2
opium
@opium
Просто люблю качественно работать
PanResponder не даст сделать нормальный handoff — он захватывает жест и ScrollView внутри уже не включится без нового касания. Это задача для @gorhom/bottom-sheet: внутри шита подмени ScrollView на BottomSheetScrollView, и он сам разруливает когда тянуть панель, а когда скроллить содержимое. Без отрыва пальца.
Ответ написан
@User782 Автор вопроса
Кратко о себе
может кому пригодиться без реаниматора 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;
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы