Компоненты/Линейка

Линейка

31 января 2025

Плавно перетаскиваемая линейка с анимированными делениями

Установка

npx astra-rn add ruler
pnpm dlx astra-rn add ruler
yarn dlx astra-rn add ruler
bunx --bun astra-rn add ruler

Файлы

index.tsx
НазадВращение квадратовДалееКарусель-масштаб

Вручную

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

npm install @shopify/react-native-skia expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets
pnpm add @shopify/react-native-skia expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets
yarn add @shopify/react-native-skia expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets
bun add @shopify/react-native-skia expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets

Скопируйте эти файлы в src/shared/components/base/ruler/:

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

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

import React, { useRef, useState } from "react";
import { StyleSheet, Text, View, useWindowDimensions } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Ruler from "@/shared/components/base/ruler";
import type { IRulerHandle } from "@/shared/components/base/ruler/types";
import { Showcase, hexToRgb, useAutoplay, useCustomize } from "~/showcase";

const CUSTOMIZE = [
  { key: "label", label: "Подпись", type: "text", default: "Weight", maxLength: 20 },
  { key: "unit", label: "Единица", type: "text", default: "kg", maxLength: 8 },
  { key: "valueColor", label: "Цвет значения", type: "color", default: "#ffffff" },
  { key: "activeTickColor", label: "Активное деление", type: "color", default: "#b56c1d" },
  { key: "tickColor", label: "Цвет делений", type: "color", default: "#ffffff" },
  { key: "tickOpacity", label: "Прозрачн. делений", type: "slider", min: 0, max: 1, step: 0.05, default: 0.3 },
  { key: "step", label: "Шаг делений", type: "slider", min: 8, max: 40, step: 1, unit: "px", default: 18 },
  { key: "notchHeight", label: "Высота деления", type: "slider", min: 10, max: 100, step: 1, unit: "px", default: 40 },
  { key: "notchWidth", label: "Толщина деления", type: "slider", min: 1, max: 8, step: 0.5, unit: "px", default: 3 },
] as const;

function rgba(hex: string, alpha: number) {
  const { r, g, b } = hexToRgb(hex);
  return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${alpha})`;
}

export default function RulerScreen() {
  const custom = useCustomize(CUSTOMIZE);
  const { width: screenWidth } = useWindowDimensions();
  const rulerWidth = Math.min(400, Math.floor(screenWidth));
  const [value, setValue] = useState<number>(0);
  const rulerRef = useRef<IRulerHandle>(null);

  useAutoplay(async ({ wait }) => {
    rulerRef.current?.scrollTo(5, 1100);
    await wait(1500);
    rulerRef.current?.scrollTo(30, 2100);
    await wait(2600);
    rulerRef.current?.scrollTo(43, 950);
    await wait(2200);
  });

  return (
    <Showcase>
      <SafeAreaView style={styles.container}>
        <View style={styles.content}>
          <Text style={styles.label}>{custom.label}</Text>
          <View style={styles.valueRow}>
            <Text style={[styles.value, { color: custom.valueColor }]}>{value}</Text>
            <Text style={styles.unit}>{custom.unit}</Text>
          </View>
        </View>
        <View style={styles.rulerRow}>
          <Ruler
            key={`${custom.step}-${rulerWidth}`}
            ref={rulerRef}
            height={150}
            width={rulerWidth}
            minValue={5}
            maxValue={50}
            step={custom.step}
            tickColor={rgba(custom.tickColor, custom.tickOpacity)}
            activeTickColor={custom.activeTickColor}
            notchHeight={custom.notchHeight}
            notchWidth={custom.notchWidth}
            onValueChange={setValue}
          />
        </View>
      </SafeAreaView>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: "center", backgroundColor: "#0a0a0a" },
  content: { alignItems: "center", gap: 9 },
  rulerRow: { alignItems: "center" },
  label: {
    fontSize: 15,
    color: "rgba(255,255,255,0.5)",
    letterSpacing: 1,
    textTransform: "uppercase",
  },
  valueRow: { flexDirection: "row", alignItems: "baseline", gap: 6 },
  value: { fontSize: 72, color: "#fff", fontWeight: "600" },
  unit: { fontSize: 24, color: "rgba(255,255,255,0.5)" },
});

Прокрутка из кода

Необязательный ref с типом IRulerHandle даёт метод scrollTo(value, duration?): линейка плавно доезжает до значения value (в пределах minValue–maxValue) за duration мс, по умолчанию 900. Без ref линейка работает только от жестов, как и раньше.

const rulerRef = useRef<IRulerHandle>(null);

rulerRef.current?.scrollTo(30, 2000);

Пропсы

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

Тип number

width

Тип number

minValue

Тип number

maxValue

Тип number

step

Тип number

onScroll?

Тип (value: number) => void

onValueChange?

Тип (value: number) => void

tickColor?

Тип string

activeTickColor?

Тип string

backgroundColor?

Тип string

notchHeight?

Тип number

notchWidth?

Тип number

enableHaptics?

Тип boolean

animateOnMount?

Тип boolean

ref?

Тип Ref<IRulerHandle>

ITick

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

Тип number

tickX

Тип number

xCenter

Тип number

yCenter

Тип number

translateX

Тип SharedValue<number>

mountAnimation

Тип SharedValue<number>

notchHeight

Тип number

notchWidth

Тип number

tickColor

Тип string

activeTickColor

Тип string

step

Тип number