Вот что мы собираемся создать в этой статье:

Это переключатель, который я разработал, работая над Above. Точнее, мы разработали его для одного из их клиентов. Выше - шведское агентство дизайна и инноваций, расположенное в красивом городе Мальмё в Швеции. Они занимаются множеством веселых и безумных проектов. Будьте уверены, чтобы проверить их!

Перво-наперво

Давайте начнем. Первое, что нам понадобится, это функциональный компонент React. Этот компонент будет принимать два аргумента , или props, чтобы говорить на React.

  1. Выход. Обратная связь, когда пользователь нажимает на переключатель.
  2. Вход. Начальное состояние тумблера.

Фрагменты в этой статье написаны на Typescript. Вот как выглядит функция, определяющая наш компонент:

const Toggle = ({ onPress, initialState }: Props) => {
   ...
}
...
export default Toggle;

Крючки

Второе, что мы хотим сделать, это добавить несколько перехватчиков React для создания состояний. Чтобы это работало, нам нужно отслеживать три значения. Они есть:

  1. Animated.Value для анимации субкомпонентов внутри Toggle. Как например перемещение ручки, изменение фона и т. д.
  2. Состояние, позволяющее отслеживать, включен или выключен переключатель
  3. Ширина контейнера. Мы могли жестко запрограммировать ширину переключателя. Тем не менее, компонент будет более гибким, если его ширина будет определяться свойствами родительского макета.

Вот как хуки выглядят в коде:

const animation = useRef(new Animated.Value(!!initialState ? 1 : 0))
.current;
const [toggled, setToggled] = useState(!!initialState);
const [containerWidth, setContainerWidth] = useState(0);

Мы используем хук useRef выше для создания объекта анимации. Согласно справочнику API хуков, он будет сохраняться в течение всего времени жизни компонента. Это означает, что React создаст переданное значение только один раз. Более поздние вызовы, обновляющие компонент, не воссоздают значение анимации. Это важно, поскольку мы хотим, чтобы внешний вид был синхронизирован с нашим включенным состоянием.

Значение анимации изменится от 0 до 1. Диапазон здесь - произвольный. Мы можем выбрать любые значения, которые захотим. Система анимации React Native (RN) поддерживает отображение между разными диапазонами. По сути, это означает, что мы можем преобразовать любой набор входных значений в любой выходной диапазон, который нам нужен. Я считаю весьма практичным использовать диапазон ввода от 0 до 1. Крайние значения здесь работают как метафоры для переключения. 0 означает, что переключатель выключен, а 1 означает, что он включен.

Итак, насколько широка эта штука?

Мы начинаем с нулевой ширины, как вы могли заметить в приведенном выше фрагменте кода. Это может показаться немного странным. Но сначала невозможно определить ширину компонента. Чтобы это выяснить, нам нужно дождаться рендеринга компонента. В RN есть обратный вызов под названием onLayout. Он позволяет нам узнать пропорции любого компонента, к которому он применяется. Среди прочего, он сообщает нам, насколько широк компонент. Вот как выглядит обратный вызов при использовании в коде:

onLayout={({
  nativeEvent: {
    layout: { width },
  },
}: LayoutChangeEvent) => setContainerWidth(width)}

Взаимодействие

Далее идет обработка событий. Чтобы зафиксировать любое взаимодействие с пользователем, мы будем использовать один из сенсорных компонентов RN. Для переключения мы будем использовать компонент TouchableWithoutFeedback. Мы используем обработчик без обратной связи, поскольку создаем анимацию самостоятельно. Мы собираемся обернуть все это в этот компонент. Здесь мы хотим, чтобы вся область, которую занимает компонент, реагировала на прикосновения. Когда пользователь нажимает внутри компонента, он переходит в свое контр-состояние. В коде:

<TouchableWithoutFeedback
  onPress={() => {
    setToggled(!toggled);
    Animated.timing(animation, {
      duration: 300,
      toValue: +!marked,
      easing: easing.materialUIStandard,
    }).start();
    onPress();
  }}
>
  ...
</TouchableWithoutFeedback>

Когда пользователь нажимает на переключатель, внутри обработчика onPress происходят три вещи:

  1. Сразу меняем переключенное состояние. Мы делаем это, даже если в данный момент есть анимация. Это сделано намеренно и особенно важно для пользователей, которые довольны триггером. Анимация будет непрерывной независимо от того, что. Это работает, потому что можно прервать функцию Animated.timing. Это по дизайну. Анимация всегда будет перезапускаться с того места, где она находится, при вызове функции.
  2. Мы запускаем собственно анимацию и устанавливаем свойства анимации. Конечное значение, продолжительность и замедление.
  3. Чтобы внешний мир узнал, что пользователь нажал на переключатель, мы вызываем предоставленный обратный вызов onPress. Это позволит любым компонентам-предкам обновлять состояния, запускать другие эффекты и т. Д.

Свет, камера, мотор

Теперь мы готовы приступить к анимации. Давайте разберем анимацию на более мелкие части и обсудим их отдельно. Сначала посмотрим на верхний контейнер. Вот как выглядят контейнер, фон переключателя и его стиль:

<TouchableWithoutFeedback ...>
  <Animated.View
    style={dynamicStyles.container(animation)}
    onLayout={...}>
    ...
  </Animated.View>
</TouchableWithoutFeedback>
...
const dynamicStyles = {
  container: (animation: Animated.Value): ViewStyle => ({
    alignItems: 'center',
    backgroundColor: animation.interpolate({
      inputRange: [0, 1],
      outputRange: [colors.coolGray50, colors.blueLight2],
    }) as any,
    borderRadius: 25,
    height: 50,
    justifyContent: 'center',
    paddingHorizontal: 57,
  }),
  ...
};

Для анимации фона нам понадобится ‹Animated.View› вместо обычного ‹View›. Использование анимированных аналогов активных элементов анимационной системы RN. Это позволяет нам напрямую управлять правилами стилизации компонента. Приведенная выше функция динамического стиля принимает в качестве аргумента переменную анимации. Затем внутри функции стилизации мы можем анимировать любое свойство, которое захотим. Вот здесь-то и вступает в игру сопоставление диапазонов, как обсуждалось ранее. Сопоставление значений позволяет анимировать правила стилизации независимо от их типа. Примерами таких свойств являются шестиугольные цвета, углы, расстояния и многие другие свойства. В этом конкретном случае мы преобразуем диапазон ввода в шестнадцатеричное значение цвета. Это значение мы, в свою очередь, устанавливаем равным backgroundColor. Фон будет переходить от серого к синему.

Вот он в действии:

При создании быстрой анимации следует помнить о двух важных вещах.

  1. Продолжительность. Установить продолжительность всегда сложно, так как это зависит от того, где и как мы используем компонент. Как правило, я использую следующий подход: анимация должна быть достаточно быстрой, чтобы пользователь мог уловить изменение, и достаточно короткой, чтобы не быть вялой. В этом случае мы будем использовать 300 мс.
  2. Ослабление. Я обычно использую ослабление пользовательского интерфейса материала, поскольку RN поддерживает настройку пользовательских кривых Безье для ослабления. Это дает нам немного более резкую анимацию по сравнению со стандартными кривыми плавности.

Текст

Следующим шагом будет анимация текста. Текстовая анимация состоит из двух отдельных компонентов Text, между которыми мы переключаемся. Мы более или менее используем ту же формулу для анимации текста, что и при анимации цвета фона. Мы снова используем версию компонента Animated вместе с передачей объекта анимации его функции стилизации. Разница в том, что мы анимируем непрозрачность вместо цвета фона. Анимация непрозрачности от 0 до 1 исчезнет в текстовом узле. Он изменится от полностью прозрачного до полностью непрозрачного.

Инвертирование интерполяции - простой способ отменить анимацию. Если мы применим обратную версию к другому текстовому узлу, он исчезнет, ​​а не появится. Затем, запустив две анимации одновременно, мы получим эффект x-fade.

<Animated.Text style={[fonts.P1, dynamicStyles.text(true, animation)]}>
  Mark as read
</Animated.Text>
<Animated.Text
  style={[
    fonts.P1,
    dynamicStyles.text(false, animation),
    styles.texttoggled,
  ]}
>
  Marked as read
</Animated.Text>
...
const dynamicStyles = {
  ...
  text: (
    inverted: boolean,
    animation: Animated.Value,
  ): TextStyle => ({
    opacity: animation.interpolate({
      inputRange: [0, 1],
      outputRange: inverted ? [1, 0] : [0, 1],
    }) as any,
    transform: [{ translateY: inverted ? 13 : -13 }],
  }),
  ...
};

И вот x-fade в действии

Ручка

Последняя часть создания полной анимации - добавление ручки. Глазурь на торте. Чтобы ручка подходила к контейнеру, нам сначала нужно освободить для нее место. Мы делаем это, заполняя обе стороны верхнего контейнера. Это позволит разместить ручку на обеих сторонах текста. Ручка состоит из 3 частей. Это вид на основание ручки и два изображения, между которыми мы перемещаемся по оси X. Для ручки мы создаем в стиле круг с тенью. С радиусом границы, цветом фона и падающими тенями у нас есть все, что нужно для создания стильной ручки.

<Animated.View ... >
  <Animated.Image ... />
  <Animated.Image ... />
</Animated.View>

Вот как выглядит слайдер в действии:

Воспользуемся тем же рецептом еще раз. Анимированные версии компонентов и манипуляции со стилем. Чтобы ручка двигалась, мы используем перемещение по оси x. Мы делаем это с помощью правила transform вместе со свойством translationX. В довершение всего мы добавляем еще один эффект x-fade к изображениям регуляторов. Синтаксис преобразования немного отличается от других анимаций. Давайте посмотрим, как мы используем свойство transform в коде:

<Animated.View
  style={[
    styles.knob,
    {
      transform: [
        {
          translateX: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [0, containerWidth - 50],
          }),
        },
      ],
    } as any,
  ]}
>

Все вместе сейчас

И последнее, но не менее важное: давайте объединим текст и анимацию ручки. Вуаля, теперь у вас есть полная анимация.

Учить больше