React Native — попытка изменить 2 высоты просмотра с помощью GestureHandler

Я пытаюсь создать представления с регулируемой высотой с помощью React Native для приложения, которое я создаю. Я продолжаю застревать на этом одном аспекте. Я пытаюсь создать два сложенных представления с линией между ними, чтобы они регулировались по высоте при перетаскивании строки вверх или вниз, а также настраивали содержимое в ней. Изображение ниже является представлением того, что я пытаюсь сделать. «Вариант дома 2» — это состояние по умолчанию, «Вариант дома 1.3» — когда ползунок перетаскивается вниз, а «Вариант дома 1.2» — наоборот — ползунок перетаскивается вверх.

С панелью приложений внизу. (у меня еще не готово)

Любые мысли или помощь приветствуются!

Вот мой код для App.tsx

import * as React from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import BottomSheet, { BottomSheetRefProps } from './components/BottomSheet';
import { useCallback, useRef } from 'react';
import MapView, { Marker, Geojson } from "react-native-maps";
import { PROVIDER_GOOGLE } from "react-native-maps";




export default function App() {
  const ref = useRef<BottomSheetRefProps>(null);
  const [topViewHeight, setTopViewHeight] = React.useState(0);

  const onPress = useCallback(() => {
    const isActive = ref?.current?.isActive();
    if (isActive) {
      ref?.current?.scrollTo(0);
    } else {
      ref?.current?.scrollTo(-200);
    }
  }, []);

  return (
    <GestureHandlerRootView style = {{ flex: 1 }}>
      <View style = {styles.mapViewContainer}>
        <MapView
          provider = {PROVIDER_GOOGLE}
          showsUserLocation = {true}
          style = {styles.mapView}
          initialRegion = {{
            latitude: 00.00 ,
            longitude: -00.00 ,
            latitudeDelta: 00.00 ,
            longitudeDelta: 00.00 ,
          }}
        >
          <Marker coordinate = {{ latitude: 00.00, longitude: 00.00 }} />
        </MapView>
      </View>
      <View style = {styles.container}>
        <StatusBar style = "light" />
        <TouchableOpacity style = {styles.button} onPress = {onPress} />
        <BottomSheet ref = {ref} {...{setTopViewHeight, topViewHeight}}>
          <View style = {{ flex: 1, backgroundColor: 'orange' }} />
        </BottomSheet>
      </View>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#111',
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    height: 50,
    borderRadius: 25,
    aspectRatio: 1,
    backgroundColor: 'white',
    opacity: 0.6,
  },
  mapViewContainer: {
    height: "50%",
    width: "95%",
    overflow: "hidden",
    background: "transparent",
    borderRadius: 13,
  },
  mapView: {
    height: "100%",
    width: "100%",
  },
});

Код для BottomSheet.tsx (который я использовал в качестве эталона для идеального UX)

import { Dimensions, StyleSheet, Text, View } from 'react-native';
import React, { useCallback, useEffect, useImperativeHandle } from 'react';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  Extrapolate,
  interpolate,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
} from 'react-native-reanimated';

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

const TOP_VIEW_HEIGHT = 50;

const VIEW_RESIZE = 2.5;
const MAX_TRANSLATE_Y = -SCREEN_HEIGHT / VIEW_RESIZE;

type BottomSheetProps = {
  children?: React.ReactNode;
  setTopViewHeight: (height: number) => void;
  topViewHeight: number;
};

export type BottomSheetRefProps = {
  scrollTo: (destination: number) => void;
  isActive: () => boolean;
};

const BottomSheet = React.forwardRef<BottomSheetRefProps, BottomSheetProps>(
  ({ children }, ref) => {
    const translateY = useSharedValue(0);
    const active = useSharedValue(false);

    const scrollTo = useCallback((destination: number) => {
      'worklet';
      active.value = destination !== 0;

      translateY.value = withSpring(destination, { damping: 50 });
    }, []);

    const isActive = useCallback(() => {
      return active.value;
    }, []);

    useImperativeHandle(ref, () => ({ scrollTo, isActive }), [
      scrollTo,
      isActive,
    ]);

    const context = useSharedValue({ y: 0 });
    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);
        console.info(translateY.value);
      })
      .onEnd(() => {
        if (translateY.value > -SCREEN_HEIGHT / 3) {
          scrollTo(0);
        } else if (translateY.value < -SCREEN_HEIGHT / 1.5) {
          scrollTo(MAX_TRANSLATE_Y);
        }
        console.info('end: ' + translateY.value)
      });

    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 }],
        maxHeight: 500,
      };
    });

 

    return (
      <GestureDetector gesture = {gesture}>
        <Animated.View style = {[styles.bottomSheetContainer, rBottomSheetStyle] }>
          <View style = {styles.line} />
          {children}
        </Animated.View>
      </GestureDetector>
    );
  }
);

const styles = StyleSheet.create({
  bottomSheetContainer: {
    minHeight: SCREEN_HEIGHT - 50,
    width: '100%',
    backgroundColor: 'white',
    position: 'relative',
    top: SCREEN_HEIGHT - 500,
    borderRadius: 25,
  },
  line: {
    width: 75,
    height: 4,
    backgroundColor: 'grey',
    alignSelf: 'center',
    marginVertical: 15,
    borderRadius: 2,
  },
});

export default BottomSheet;
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
0
53
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Компонент Bar будет иметь привязанный к нему GestureHandler. Интерполируйте yTranslation в значение от 0 до 1. SharedValue компонента Bar передается как свойство, чтобы другие компоненты в его родительском содержании использовали его:

import {
  StyleSheet,
  ViewStyle,
  Dimensions,
  View,
  useWindowDimensions,
} from 'react-native';
import Animated, {
  SharedValue,
  useAnimatedStyle,
  interpolate,
  withTiming,
} from 'react-native-reanimated';
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
type Props = {
  anim: SharedValue<number>;
  style?: ViewStyle;
};

const snapPoints = [0.2, 0.5, 0.8];

export default function Bar({ anim, style }: Props) {
  const { height } = useWindowDimensions();
  const gesture = Gesture.Pan()
    .onUpdate((e) => {
      // interpolate yTranslation to a value that snapPoints can work with
      anim.value = interpolate(
        e.translationY,
        [-height * 0.5, height * 0.5],
        [0, 1]
      );
    })
    // snap to nearest point
    .onEnd(() => {
      const snapPoint = snapPoints.reduce((prev, curr) => {
        const prevDist = Math.abs(prev - anim.value);
        const currDist = Math.abs(curr - anim.value);
        return prevDist < currDist ? prev : curr;
      }, snapPoints[0]);

      console.info('snapping to ', snapPoint);
      // animate snapping to snapPoint
      anim.value = withTiming(snapPoint);
    });

  return (
    <GestureDetector gesture = {gesture}>
      <View style = {styles.barContainer}>
        <View style = {styles.bar} />
      </View>
    </GestureDetector>
  );
}

const styles = StyleSheet.create({
  barContainer: {
    backgroundColor: 'transparent',
    width: '100%',
    //padding to make bar easier to press
    padding: 10,
    justifyContent: 'center',
  },
  bar: {
    backgroundColor: '#c4c4c4',
    width: '80%',
    height: 7,
    alignSelf: 'center',
    borderRadius: 25,
  },
});

Теперь, когда translationY представляет собой процент, его можно использовать для определения степени гибкости каждого представления:

import React from 'react';
import {
  View,
  StyleSheet,
} from 'react-native';
import Constants from 'expo-constants';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
} from 'react-native-reanimated';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import SliderBar from './SliderBar';
import View1 from './View1';
import View2 from './View2';
import { footerHeight, ScreenWidth, ScreenHeight, MAX_FLEX } from './Constants';

export default function App() {
  const barValue = useSharedValue(0.5);
  const view1Style = useAnimatedStyle(() => {
    return {
      flex: barValue.value * MAX_FLEX,
    };
  });
  const view2Style = useAnimatedStyle(() => {
    return {
      flex: Math.abs(barValue.value - 1) * MAX_FLEX,
    };
  });
  return (
    <GestureHandlerRootView
      style = {{ width: ScreenWidth, height: ScreenHeight }}>
      <View style = {styles.container}>
        <Animated.View style = {[styles.viewStyle, view1Style]}>
          <View1 />
        </Animated.View>
        <SliderBar anim = {barValue} />
        <Animated.View style = {[styles.viewStyle, view2Style]}>
          <View2 />
        </Animated.View>
        <View style = {styles.footer} />
      </View>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingTop: Constants.statusBarHeight,
    padding: 8,
    margin: 5,
  },

  viewStyle: {
    backgroundColor: '#c4c4c4',
    flex: 1,
    marginVertical: 10,
    borderRadius: 10,
  },
  footer: {
    backgroundColor: '#6f6f6f',
    height: footerHeight,
    borderRadius: 10,
  },
});

Демо

Отлично! Однако у меня есть вопрос: я заметил, что когда я нажимаю/перетаскиваю линию после того, как переместил ее в положение привязки, линия возвращается к середине. Есть мысли, почему?

emartindev 10.02.2023 17:07

Я предполагаю, что это должна быть интерполяция. Ожидается, что перетаскивание будет между половиной screenHeight (положительное и отрицательное) и сопоставляет его со значением от 0 до 1. Поэтому, когда yTranslation действительно мало, оно будет интерполировано до значения, близкого к 0,5. Я думаю, что смещение, как они делают здесь, может исправить это

PhantomSpooks 10.02.2023 18:12

Другие вопросы по теме