Компоненты/Барабанный счётчик

Барабанный счётчик

31 января 2025

Счётчик, где каждая цифра пружинно уезжает вверх

Установка

npx astra-rn add rolling-counter
pnpm dlx astra-rn add rolling-counter
yarn dlx astra-rn add rolling-counter
bunx --bun astra-rn add rolling-counter

Файлы

index.tsx
НазадРетро-плеерДалееПоворотная ручка

Вручную

Поставьте зависимости:

npm install @react-native-masked-view/masked-view expo-blur expo-linear-gradient react-native-reanimated react-native-worklets
pnpm add @react-native-masked-view/masked-view expo-blur expo-linear-gradient react-native-reanimated react-native-worklets
yarn add @react-native-masked-view/masked-view expo-blur expo-linear-gradient react-native-reanimated react-native-worklets
bun add @react-native-masked-view/masked-view expo-blur expo-linear-gradient react-native-reanimated react-native-worklets

Скопируйте эти файлы в src/shared/components/organisms/rolling-counter/:

Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.

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

import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { useSharedValue, Easing } from "react-native-reanimated";

import { RollingCounter } from "@/shared/components/organisms/rolling-counter";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";

const CUSTOMIZE = [
  { key: "color", label: "Цвет цифр", type: "color", default: "#ffffff" },
  { key: "fontSize", label: "Размер шрифта", type: "slider", min: 20, max: 96, step: 1, unit: "px", default: 58 },
  { key: "height", label: "Высота цифры", type: "slider", min: 30, max: 120, step: 1, unit: "px", default: 72 },
  { key: "width", label: "Ширина цифры", type: "slider", min: 20, max: 80, step: 1, unit: "px", default: 46 },
  { key: "duration", label: "Длительность", type: "slider", min: 100, max: 2500, step: 50, unit: "ms", default: 900 },
  { key: "stagger", label: "Задержка разрядов", type: "slider", min: 0, max: 150, step: 1, unit: "ms", default: 38 },
  { key: "fadeEdges", label: "Затухание краёв", type: "toggle", default: true },
  { key: "motionBlur", label: "Размытие движения", type: "toggle", default: true },
] as const;

const AUTOPLAY_STEPS: [(value: number) => number, number][] = [
  [(value) => value + 1, 230],
  [(value) => value + 1, 230],
  [(value) => value + 1, 230],
  [(value) => value + 1, 230],
  [(value) => value + 1, 230],
  [(value) => value + 1, 330],
  [(value) => value + 111, 680],
  [() => 3562, 560],
  [() => 2145, 300],
  [() => 1933, 620],
  [(value) => value + 1, 250],
  [(value) => value + 1, 250],
  [(value) => value + 1, 250],
  [(value) => value + 1, 250],
];

export default function RollingCounterScreen() {
  const custom = useCustomize(CUSTOMIZE);
  const counter = useSharedValue<number>(1234);

  useAutoplay(async ({ wait }) => {
    if (counter.value !== 1234) {
      counter.value = 1234;
      await wait(1100);
    }
    await wait(300);
    for (const [next, hold] of AUTOPLAY_STEPS) {
      counter.value = next(counter.value);
      await wait(hold);
    }
    await wait(1500);
  });

  return (
    <Showcase>
      <View
        style={[
          styles.stage,
          {
            justifyContent: "center",
            alignItems: "center",
            flex: 1,
          },
        ]}
      >
        <RollingCounter
          value={counter}
          height={custom.height}
          width={custom.width}
          stagger={custom.stagger}
          timingConfig={{
            duration: custom.duration,
            easing: Easing.bezier(0.2, 1, 0.48, 1),
          }}
          fontSize={custom.fontSize}
          fadeEdges={custom.fadeEdges}
          motionBlur={custom.motionBlur}
          color={custom.color}
        />

        <View style={styles.row}>
          <Pressable
            style={styles.button}
            onPress={() => (counter.value = counter.value - 1)}
          >
            <Text style={styles.buttonText}>-1</Text>
          </Pressable>
          <Pressable
            style={styles.button}
            onPress={() => (counter.value = counter.value + 1)}
          >
            <Text style={styles.buttonText}>+1</Text>
          </Pressable>
          <Pressable
            style={styles.button}
            onPress={() => (counter.value = counter.value + 111)}
          >
            <Text style={styles.buttonText}>+111</Text>
          </Pressable>
          <Pressable
            style={styles.button}
            onPress={() => (counter.value = Math.floor(Math.random() * 5000))}
          >
            <Text style={styles.buttonText}>random</Text>
          </Pressable>
        </View>
      </View>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  stage: { alignItems: "center", gap: 32 },
  row: { flexDirection: "row", gap: 12 },
  button: {
    paddingHorizontal: 16,
    paddingVertical: 10,
    borderRadius: 12,
    backgroundColor: "#1c1c1e",
  },
  buttonText: { color: "#fff", fontSize: 14, fontWeight: "600" },
});

Пропсы

Пропс
Описание
value

Тип number | SharedValue<number>

height?

Тип number

width?

Тип number

digitStyle?

Тип StyleProp<TextStyle>

style?

Тип StyleProp<ViewStyle>

springConfig?

Spring driver. Omit to use the smoother `timingConfig` roll (default).

Тип Partial<WithSpringConfig>

timingConfig?

Timing driver used when `springConfig` is not supplied.

Тип WithTimingConfig

stagger?

Delay in ms added per place, cascading right to left. `0` disables it.

Тип number

fadeEdges?

Soften the top and bottom of every digit window with a gradient mask.

Тип boolean

motionBlur?

Blur and squash each digit in proportion to how fast it is travelling.

Тип boolean

fontSize?

Тип number

color?

Тип string