Как реагировать на события клавиатуры в React Native Reanimated? - PullRequest
1 голос
/ 16 октября 2019

Что я пытаюсь сделать

Я пытаюсь создать анимированный элемент пробела / отступа, который изменяет высоту, когда клавиатура отображается или скрывается, чтобы гарантировать, что TextInput не покрыт клавиатурой иликнопкой, которая избегает клавиатуры, используя KeyboardAvoidingView. Я хочу, чтобы это анимированное пространство изменяло высоту только в том случае, если кнопка будет покрывать ввод, иначе я не хочу, чтобы интервал изменял высоту. Это проектное требование.

Мое текущее решение

Ранее я мог достичь этого, используя Animated API из react-native, однако я хотел использовать react-native-reanimated, чтобы получитьвыигрыш в производительности запуска всего в потоке пользовательского интерфейса. На самом деле у меня есть рабочее решение, однако поток пользовательского интерфейса падает до середины 50 кадров в секунду во время анимации, поэтому я предполагаю, что я делаю что-то не так.

Как вы увидите из кода ниже, яЯ рассчитываю высоту всех элементов, чтобы выяснить, перекрывается ли кнопка, прикрепленная к верхней части клавиатуры, TextInput. Если это так, я вычитаю величину перекрытия из высоты интервала над текстом (animHeaderHeight). Вы должны быть в состоянии скопировать, вставить этот код и запустить его. Если вы включите профилировщик и посмотрите поток пользовательского интерфейса, переключите анимацию, сфокусировав ввод и нажав клавишу возврата, чтобы закрыть его. Анимация работает, но она заставляет поток пользовательского интерфейса работать со скоростью ниже 60 кадров в секунду.

Воспроизводимый код

Я загрузил проект с expo init. Вот версии пакета:

"expo": "^35.0.0",
"expo-constants": "~7.0.0",
"react": "16.8.3",
"react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz",
"react-native-gesture-handler": "~1.3.0",
"react-native-reanimated": "~1.2.0",

Вот код.

import React, { useEffect, useRef } from "react";
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  Keyboard,
  KeyboardEvent,
  KeyboardEventName,
  Platform,
  TextInput,
  SafeAreaView,
  KeyboardAvoidingView,
  LayoutChangeEvent,
  Dimensions
} from "react-native";
import Animated, { Easing } from "react-native-reanimated";
import Constants from "expo-constants";

const DEVICE_HEIGHT = Dimensions.get("screen").height;
const STATUS_BAR_HEIGHT = Constants.statusBarHeight;
const HEADER_HEIGHT = 100;
const MAX_ANIMATED_HEIGHT = 75;
const BOTTOM_BUTTON_HEIGHT = 60;
const KEYBOARD_EASING = Easing.bezier(0.38, 0.7, 0.125, 1.0);

const {
  Value,
  Clock,
  set,
  block,
  cond,
  eq,
  and,
  neq,
  add,
  sub,
  max,
  startClock,
  stopClock,
  timing,
  interpolate
} = Animated;

export default App = () => {
  // These refs are used so the height calculations are only called once and don't cause re-renders
  const wasKeyboardMeasured = useRef(false);
  const wasContentMeasured = useRef(false);

  const clock = new Clock();
  const keyboardShown = new Value(-1);
  const animKeyboardHeight = new Value(0);
  const animContentHeight = new Value(0);

  function handleLayout(e) {
    if (!wasContentMeasured.current) {
      // Set animated value and set ref measured flag true
      const height = Math.floor(e.nativeEvent.layout.height);
      wasContentMeasured.current = true;
      animContentHeight.setValue(height);
    }
  }

  useEffect(() => {
    const handleKbdShow = (e: KeyboardEvent) => {
      if (!wasKeyboardMeasured.current) {
        // Set animated value and set ref measured flag true
        const kbdHeight = Math.floor(e.endCoordinates.height);
        wasKeyboardMeasured.current = true;
        animKeyboardHeight.setValue(kbdHeight);
      }
      keyboardShown.setValue(1);
    };
    const handleKbdHide = () => {
      keyboardShown.setValue(0
);
    };

    const kbdWillOrDid = Platform.select({ ios: "Will", android: "Did" });
    const showEventName = `keyboard${kbdWillOrDid}Show`;
    const hideEventName = `keyboard${kbdWillOrDid}Hide`;

    Keyboard.addListener(showEventName, handleKbdShow);
    Keyboard.addListener(hideEventName, handleKbdHide);

    return () => {
      Keyboard.removeListener(showEventName, handleKbdShow);
      Keyboard.removeListener(hideEventName, handleKbdHide);
    };
  }, []);

  const animHeaderHeight = runTiming(
    clock,
    keyboardShown,
    animContentHeight,
    animKeyboardHeight
  );

  return (
    <SafeAreaView style={styles.container}>
      <KeyboardAvoidingView style={styles.container} behavior="padding">
        <View style={styles.header}>
          <Text style={styles.headerText}>Header</Text>
        </View>
        <Animated.View
          style={[styles.animatedSpace, { height: animHeaderHeight }]}
        />
        <View onLayout={handleLayout}>
          <View style={styles.heading}>
            <Text style={styles.headingText}>
              Note: CHANGE THIS TEXT CONTENT TO WHATEVER LENGTH MAKES THE BOTTOM
              BUTTON OVERLAP THE TEXT INPUT WHEN THE KEYBOARD IS SHOWN! Lorem
              ipsum dolor sit amet, consectetur adipiscing elit.
            </Text>
          </View>
          <View style={styles.textInputContainer}>
            <TextInput style={styles.textInput} autoFocus={true} />
          </View>
        </View>
        <TouchableOpacity style={styles.bottomButton} />
      </KeyboardAvoidingView>
    </SafeAreaView>
  );
};

function runTiming(
  clock,
  keyboardShown,
  animContentHeight,
  animKeyboardHeight
) {
  const state = {
    finished: new Value(0),
    position: new Value(0),
    time: new Value(0),
    frameTime: new Value(0)
  };

  const config = {
    duration: 300,
    toValue: new Value(-1),
    easing: KEYBOARD_EASING
  };

  const upperContentHeightNode = add(
    STATUS_BAR_HEIGHT,
    HEADER_HEIGHT,
    MAX_ANIMATED_HEIGHT,
    animContentHeight
  );
  const keyboardContentHeightNode = add(
    BOTTOM_BUTTON_HEIGHT,
    animKeyboardHeight
  );
  const overlap = max(
    sub(add(upperContentHeightNode, keyboardContentHeightNode), DEVICE_HEIGHT),
    0
  );
  const headerMinHeightNode = max(sub(MAX_ANIMATED_HEIGHT, overlap), 0);

  return block([
    cond(and(eq(keyboardShown, 1), neq(config.toValue, 1)), [
      set(state.finished, 0),
      set(state.time, 0),
      set(state.frameTime, 0),
      set(config.toValue, 1),
      startClock(clock)
    ]),
    cond(and(eq(keyboardShown, 0), neq(config.toValue, 0)), [
      set(state.finished, 0),
      set(state.time, 0),
      set(state.frameTime, 0),
      set(config.toValue, 0),
      startClock(clock)
    ]),
    timing(clock, state, config),
    cond(state.finished, stopClock(clock)),
    interpolate(state.position, {
      inputRange: [0, 1],
      outputRange: [MAX_ANIMATED_HEIGHT, headerMinHeightNode]
    })
  ]);
}

// Coloring below is used just to easily see the different components
const styles = StyleSheet.create({
  container: {
    flex: 1
  },
  header: {
    height: HEADER_HEIGHT,
    width: "100%",
    backgroundColor: "teal",
    justifyContent: "center",
    alignItems: "center"
  },
  headerText: {
    color: "white"
  },
  heading: {
    alignItems: "center",
    marginBottom: 15,
    paddingHorizontal: 30
  },
  headingText: {
    fontSize: 28,
    fontWeight: "600",
    textAlign: "center"
  },
  animatedSpace: {
    backgroundColor: "pink",
    width: "100%"
  },
  textInputContainer: {
    alignItems: "center",
    paddingHorizontal: 40,
    width: "100%",
    height: 60
  },
  textInput: {
    backgroundColor: "lightgray",
    width: "100%",
    height: 60
  },
  bottomButton: {
    marginTop: "auto",
    height: BOTTOM_BUTTON_HEIGHT,
    backgroundColor: "orange",
    paddingHorizontal: 20
  }
});

Заключительные мысли

Я ожидал, что fps пользовательского интерфейса останется равным 60, однакочто-то в том, как я настроил вещи, вызывает падение кадров. Мне интересно, связано ли это с тем фактом, что моя анимация react-native-reanimated зависит от состояния клавиатуры (т.е. зависит от информации из потока JS). Мне интересно, можно ли вообще обойтись без постоянной связи между потоками JS и UI по мосту. Любая помощь или направление будет принята с благодарностью.

1 Ответ

0 голосов
/ 17 октября 2019

Только для безопасности, не могли бы вы включить вызов runTiming в сценарий использования с соответствующими зависимостями? [клавиатура показана и т. д.]. В вашем фрагменте кода есть много побочных эффектов, которые могут вызвать проблемы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...