Перейти к содержанию

Работа с жестами

В этом разделе мы научимся работать с жестами в Reanimated. Для этого Reanimated тесно интегрируется с React Native Gesture Handler, другой библиотекой, созданной Software Mansion.

Gesture Handler поставляется с большим количеством жестов, таких как Pinch или Fling. Сейчас мы начнем с простого и познакомимся с жестами Tap и Pan, а также с тем, как использовать функцию анимации withDecay.

Только не забудьте сначала просмотреть шаги по установке обработчика жестов и вернуться сюда, чтобы узнать, как использовать его в Reanimated.

Работа с жестами касания

Начнем с самого простого жеста - постукивания. Жест касания определяет прикосновение пальцев к экрану в течение короткого промежутка времени. С их помощью можно реализовать пользовательские кнопки или нажимаемые элементы с нуля.

В данном примере мы создадим круг, который будет увеличиваться и менять цвет при касании.

Сначала обернем наше приложение с помощью GestureHandlerRootView. Убедитесь, что GestureHandlerRootView находится как можно ближе к реальному корневому виду. Это гарантирует, что наши жесты будут работать друг с другом как положено.

1
2
3
4
5
6
7
8
9
import { GestureHandlerRootView } from 'react-native-gesture-handler';

function App() {
    return (
        <GestureHandlerRootView style={{ flex: 1 }}>
            {/* rest of the app */}
        </GestureHandlerRootView>
    );
}

Новые жесты касания определяются с помощью Gesture.Tap() в теле вашего компонента. Вы можете определить поведение жеста, наложив на него цепочку методов типа onBegin, onStart, onEnd или onFinalize. Мы будем использовать их для обновления общего значения сразу после начала жеста и возврата к исходному значению по его завершении.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import 'react-native-gesture-handler';
import React from 'react';
import { StyleSheet, View } from 'react-native';
import Animated, {
    useAnimatedStyle,
    useSharedValue,
    withTiming,
} from 'react-native-reanimated';
import {
    Gesture,
    GestureDetector,
    GestureHandlerRootView,
} from 'react-native-gesture-handler';

export default function App() {
    const pressed = useSharedValue(false);

    const tap = Gesture.Tap()
        .onBegin(() => {
            pressed.value = true;
        })
        .onFinalize(() => {
            pressed.value = false;
        });

    const animatedStyles = useAnimatedStyle(() => ({
        backgroundColor: pressed.value
            ? '#FFE04B'
            : '#B58DF1',
        transform: [
            { scale: withTiming(pressed.value ? 1.2 : 1) },
        ],
    }));

    return (
        <GestureHandlerRootView style={styles.container}>
            <View style={styles.container}>
                <GestureDetector gesture={tap}>
                    <Animated.View
                        style={[
                            styles.circle,
                            animatedStyles,
                        ]}
                    />
                </GestureDetector>
            </View>
        </GestureHandlerRootView>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        height: '100%',
    },
    circle: {
        height: 120,
        width: 120,
        borderRadius: 500,
    },
});

Вы можете спокойно обращаться к общим значениям, поскольку обратные вызовы, передаваемые жестам, автоматически workletized для вас.

Мы хотим, чтобы наш круг менял цвет с фиолетового на желтый и плавно масштабировался на 20% при касании. Определим логику анимации с помощью withTiming в useAnimatedStyle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import 'react-native-gesture-handler';
import React from 'react';
import { StyleSheet, View } from 'react-native';
import Animated, {
    useAnimatedStyle,
    useSharedValue,
    withTiming,
} from 'react-native-reanimated';
import {
    Gesture,
    GestureDetector,
    GestureHandlerRootView,
} from 'react-native-gesture-handler';

export default function App() {
    const pressed = useSharedValue(false);

    const tap = Gesture.Tap()
        .onBegin(() => {
            pressed.value = true;
        })
        .onFinalize(() => {
            pressed.value = false;
        });

    const animatedStyles = useAnimatedStyle(() => ({
        backgroundColor: pressed.value
            ? '#FFE04B'
            : '#B58DF1',
        transform: [
            { scale: withTiming(pressed.value ? 1.2 : 1) },
        ],
    }));

    return (
        <GestureHandlerRootView style={styles.container}>
            <View style={styles.container}>
                <GestureDetector gesture={tap}>
                    <Animated.View
                        style={[
                            styles.circle,
                            animatedStyles,
                        ]}
                    />
                </GestureDetector>
            </View>
        </GestureHandlerRootView>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        height: '100%',
    },
    circle: {
        height: 120,
        width: 120,
        borderRadius: 500,
    },
});

Определенный жест необходимо передать в пропсы gesture компонента GestureDetector. Этот компонент должен обернуть представление, для которого вы хотите обрабатывать жесты. Также не забудьте передать определенные animatedStyles в представление, которое вы хотите анимировать, следующим образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import 'react-native-gesture-handler';
import React from 'react';
import { StyleSheet, View } from 'react-native';
import Animated, {
    useAnimatedStyle,
    useSharedValue,
    withTiming,
} from 'react-native-reanimated';
import {
    Gesture,
    GestureDetector,
    GestureHandlerRootView,
} from 'react-native-gesture-handler';

export default function App() {
    const pressed = useSharedValue(false);

    const tap = Gesture.Tap()
        .onBegin(() => {
            pressed.value = true;
        })
        .onFinalize(() => {
            pressed.value = false;
        });

    const animatedStyles = useAnimatedStyle(() => ({
        backgroundColor: pressed.value
            ? '#FFE04B'
            : '#B58DF1',
        transform: [
            { scale: withTiming(pressed.value ? 1.2 : 1) },
        ],
    }));

    return (
        <GestureHandlerRootView style={styles.container}>
            <View style={styles.container}>
                <GestureDetector gesture={tap}>
                    <Animated.View
                        style={[
                            styles.circle,
                            animatedStyles,
                        ]}
                    />
                </GestureDetector>
            </View>
        </GestureHandlerRootView>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        height: '100%',
    },
    circle: {
        height: 120,
        width: 120,
        borderRadius: 500,
    },
});

С помощью композиционных жестов можно реализовать более сложное поведение. Но что, если мы хотим создать что-то более интересное?

Обработка жестов панорамирования

Сделаем круг перетаскиваемым, а при отпускании он будет возвращаться в исходное положение. Сохраним также эффект выделения цветом и масштабирования, добавленный в предыдущем примере. Реализовать такое поведение простым жестом касания невозможно. Вместо этого нам необходимо использовать жест панорамирования.

К счастью, все жесты имеют схожий API, поэтому реализовать это практически просто: переименуйте жест Tap в Pan и добавьте в цепочку дополнительный метод onChange.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import 'react-native-gesture-handler';
import React from 'react';
import { StyleSheet, View } from 'react-native';
import Animated, {
    useAnimatedStyle,
    useSharedValue,
    withSpring,
    withTiming,
} from 'react-native-reanimated';
import {
    Gesture,
    GestureDetector,
    GestureHandlerRootView,
} from 'react-native-gesture-handler';

export default function App() {
    const pressed = useSharedValue(false);
    const offset = useSharedValue(0);

    const pan = Gesture.Pan()
        .onBegin(() => {
            pressed.value = true;
        })
        .onChange((event) => {
            offset.value = event.translationX;
        })
        .onFinalize(() => {
            offset.value = withSpring(0);
            pressed.value = false;
        });

    const animatedStyles = useAnimatedStyle(() => ({
        transform: [
            { translateX: offset.value },
            { scale: withTiming(pressed.value ? 1.2 : 1) },
        ],
        backgroundColor: pressed.value
            ? '#FFE04B'
            : '#b58df1',
    }));

    return (
        <GestureHandlerRootView style={styles.container}>
            <View style={styles.container}>
                <GestureDetector gesture={pan}>
                    <Animated.View
                        style={[
                            styles.circle,
                            animatedStyles,
                        ]}
                    />
                </GestureDetector>
            </View>
        </GestureHandlerRootView>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        height: '100%',
    },
    circle: {
        height: 120,
        width: 120,
        backgroundColor: '#b58df1',
        borderRadius: 500,
        cursor: 'grab',
    },
});

Обратный вызов, переданный в onChange, поставляется с некоторыми данными события, которые имеют кучу удобных свойств. Одно из них - translationX, которое показывает, насколько объект переместился по оси X. Мы сохранили это значение в общем виде, чтобы соответствующим образом переместить окружность. Чтобы вернуть окружность в исходное положение, достаточно сбросить значение offset.value в методе onFinalize. Мы можем использовать функции withSpring или withTiming для анимации возврата.

Осталось только настроить логику в useAnimatedStyle для работы со смещением.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import 'react-native-gesture-handler';
import React from 'react';
import { StyleSheet, View } from 'react-native';
import Animated, {
    useAnimatedStyle,
    useSharedValue,
    withSpring,
    withTiming,
} from 'react-native-reanimated';
import {
    Gesture,
    GestureDetector,
    GestureHandlerRootView,
} from 'react-native-gesture-handler';

export default function App() {
    const pressed = useSharedValue(false);
    const offset = useSharedValue(0);

    const pan = Gesture.Pan()
        .onBegin(() => {
            pressed.value = true;
        })
        .onChange((event) => {
            offset.value = event.translationX;
        })
        .onFinalize(() => {
            offset.value = withSpring(0);
            pressed.value = false;
        });

    const animatedStyles = useAnimatedStyle(() => ({
        transform: [
            { translateX: offset.value },
            { scale: withTiming(pressed.value ? 1.2 : 1) },
        ],
        backgroundColor: pressed.value
            ? '#FFE04B'
            : '#b58df1',
    }));

    return (
        <GestureHandlerRootView style={styles.container}>
            <View style={styles.container}>
                <GestureDetector gesture={pan}>
                    <Animated.View
                        style={[
                            styles.circle,
                            animatedStyles,
                        ]}
                    />
                </GestureDetector>
            </View>
        </GestureHandlerRootView>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        height: '100%',
    },
    circle: {
        height: 120,
        width: 120,
        backgroundColor: '#b58df1',
        borderRadius: 500,
        cursor: 'grab',
    },
});

Использование withDecay

Помните, некоторое время назад мы говорили, что еще вернемся к withDecay? Сейчас самое время!

withDecay позволяет сохранять скорость жеста и выполнять анимацию с некоторым замедлением. Это означает, что, отпустив захваченный объект с некоторой скоростью, можно медленно довести его до остановки. Звучит сложно, но на самом деле это не так!

Просто передайте конечную скорость в методе onFinalize свойству velocity функции withDecay и позвольте Reanimated сделать это за вас. Для сохранения нового положения объекта обновите изменение по оси X в методе onChange следующим образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const pan = Gesture.Pan()
    .onChange((event) => {
        offset.value += event.changeX;
    })
    .onFinalize((event) => {
        offset.value = withDecay({
            velocity: event.velocityX,
            rubberBandEffect: true,
            clamp: [
                -(width.value / 2) + SIZE / 2,
                width.value / 2 - SIZE / 2,
            ],
        });
    });

Остальная часть кода нужна только для того, чтобы квадрат оставался внутри экрана.

Обязательно ознакомьтесь с полным справочником withDecay API reference, чтобы получить представление об остальных параметрах настройки.

Резюме

В этом разделе мы рассмотрели основы работы с жестами с помощью Reanimated и Gesture Handler. Мы узнали о жестах Tap и Pan, а также о функции withDecay. Подведем итоги:

  • Reanimated интегрируется с другим пакетом под названием React Native Gesture Handler для обеспечения бесшовного взаимодействия.
  • Мы создаем новые жесты, такие как Gesture.Pan() или Gesture.Tap(), и передаем их в GestureDetector, который должен обернуть элемент, с которым мы хотим осуществлять взаимодействие.
  • Вы можете получать доступ к общим значениям и изменять их внутри обратных вызовов жестов без дополнительных шаблонов.
  • Функция withDecay позволяет создавать замедляющуюся анимацию на основе скорости, поступающей от жеста.

Что дальше?

В этой статье мы лишь поверхностно рассмотрели возможности использования жестов в Reanimated. Помимо жестов Tap и Pan, обработчик жестов содержит множество других, например, Pinch или Fling. Мы приглашаем вас погрузиться в раздел Quick start документации по React Native Gesture Handler и изучить все возможности, которые предоставляет эта библиотека.

В следующем разделе мы узнаем больше об анимации цветов.

Комментарии