Компоненты/Перемотка

Перемотка

21 августа 2026

Полоса перемотки вырастает, пока её тянут

Установка

npx astra-rn add seek-bar
pnpm dlx astra-rn add seek-bar
yarn dlx astra-rn add seek-bar
bunx --bun astra-rn add seek-bar

Файлы

SeekBar.tsx
НазадСтрока поискаДалееСегменты на жестах

Вручную

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

npm install expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets
pnpm add expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets
yarn add expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets
bun add expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets

Скопируйте эти файлы в src/shared/components/molecules/seek-bar/:

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

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

import { View, StyleSheet, Dimensions, Platform } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { StatusBar } from "expo-status-bar";
import { useEffect, useRef, useState } from "react";

import { SeekBar } from "@/shared/components/molecules/seek-bar";
import { Showcase, hexToRgb, useAutoplay, useCustomize } from "~/showcase";

const CUSTOMIZE = [
  { key: "activeColor", label: "Цвет заполнения", type: "color", default: "#ffffff" },
  { key: "inactiveColor", label: "Цвет дорожки", type: "color", default: "#ffffff" },
  { key: "inactiveOpacity", label: "Прозрачн. дорожки", type: "slider", min: 0, max: 1, step: 0.05, default: 0.3 },
  { key: "width", label: "Ширина", type: "slider", min: 160, max: 380, step: 2, unit: "px", default: 300 },
  { key: "height", label: "Толщина", type: "slider", min: 2, max: 24, step: 1, unit: "px", default: 8 },
  { key: "activeHeight", label: "Толщина при нажатии", type: "slider", min: 8, max: 60, step: 1, unit: "px", default: 40 },
  { key: "trackScale", label: "Растяжение", type: "slider", min: 1, max: 5, step: 0.1, default: 3.5 },
  { key: "showThumb", label: "Ползунок", type: "toggle", default: false },
  { key: "thumbColor", label: "Цвет ползунка", type: "color", default: "#ffffff" },
] 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 App() {
  const custom = useCustomize(CUSTOMIZE);
  const [value, setValue] = useState<number>(0);
  const [pressed, setPressed] = useState(false);
  const pointerDown = useRef(false);
  const releaseOnPointerUp = useRef(false);

  useEffect(() => {
    if (Platform.OS !== "web") return;
    const onDown = () => {
      pointerDown.current = true;
    };
    const onUp = () => {
      pointerDown.current = false;
      if (!releaseOnPointerUp.current) return;
      releaseOnPointerUp.current = false;
      setPressed(false);
    };
    window.addEventListener("pointerdown", onDown, true);
    window.addEventListener("pointerup", onUp, true);
    window.addEventListener("pointercancel", onUp, true);
    return () => {
      window.removeEventListener("pointerdown", onDown, true);
      window.removeEventListener("pointerup", onUp, true);
      window.removeEventListener("pointercancel", onUp, true);
    };
  }, []);

  useAutoplay(async ({ wait }) => {
    releaseOnPointerUp.current = false;
    const glide = async (from: number, to: number, duration: number) => {
      const steps = Math.max(1, Math.round(duration / 40));
      for (let step = 1; step <= steps; step++) {
        await wait(duration / steps);
        setValue(from + ((to - from) * step) / steps);
      }
    };
    try {
      setValue(0);
      await wait(450);
      setPressed(true);
      setValue(0.05);
      await glide(0.05, 0.4, 200);
      await glide(0.4, 0.87, 300);
      await glide(0.87, 1, 350);
      await wait(300);
      await glide(1, 0.47, 400);
      await glide(0.47, 0.03, 300);
      await wait(200);
      setPressed(false);
      await wait(500);
    } finally {
      if (pointerDown.current) releaseOnPointerUp.current = true;
      else setPressed(false);
    }
  });

  return (
    <Showcase>
      <GestureHandlerRootView style={styles.container}>
        <StatusBar style="inverted" />
        <SeekBar
          value={value}
          onValueChange={setValue}
          key={custom.height}
          width={custom.width}
          height={custom.height}
          activeColor={custom.activeColor}
          inactiveColor={rgba(custom.inactiveColor, custom.inactiveOpacity)}
          thumbColor={custom.thumbColor}
          showThumb={custom.showThumb}
          trackScale={custom.trackScale}
          activeHeight={custom.activeHeight}
          pressed={pressed}
        />
      </GestureHandlerRootView>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#0a0a0a",
    justifyContent: "center",
    alignItems: "center",
  },
  header: {
    flexDirection: "row",
    justifyContent: "center",
    alignItems: "center",
    paddingTop: 120,
  },
  title: {
    fontSize: 35,
    color: "#fff",
    letterSpacing: 2,
  },
  subtitle: {
    fontSize: 15,
    color: "#aaa",
  },
  headerRight: {
    width: 40,
    height: 40,
    borderRadius: 20,
    backgroundColor: "#1a1a1a",
    justifyContent: "center",
    alignItems: "center",
  },
  card: {
    width: "100%",
    height: 340,
    borderRadius: 24,
    overflow: "hidden",
  },
  cardImage: {
    width: 300,
    height: 400,
    resizeMode: "cover",
  },
  cardGradient: {
    ...StyleSheet.absoluteFillObject,
  },
  cardContent: {
    position: "absolute",
    bottom: 0,
    left: 0,
    right: 0,
    padding: 20,
    gap: 8,
  },
  albumName: {
    fontSize: 16,
    color: "#fff",
    letterSpacing: 1,
  },
  artistRow: {
    flexDirection: "row",
    alignItems: "center",
    gap: 6,
  },
  artistText: {
    fontSize: 13,
    color: "rgba(255,255,255,0.7)",
  },
  dot: {
    width: 3,
    height: 3,
    borderRadius: 1.5,
    backgroundColor: "rgba(255,255,255,0.4)",
  },
  yearText: {
    fontSize: 13,
    color: "rgba(255,255,255,0.5)",
  },
  footer: {
    paddingHorizontal: 24,
    marginTop: 32,
    gap: 20,
  },
  nowPlaying: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    backgroundColor: "#141414",
    padding: 12,
    borderRadius: 16,
  },
  nowPlayingLeft: {
    flexDirection: "row",
    alignItems: "center",
    gap: 12,
    flex: 1,
  },
  nowPlayingImage: {
    width: 48,
    height: 48,
    borderRadius: 10,
  },
  nowPlayingInfo: {
    flex: 1,
    gap: 2,
  },
  nowPlayingTitle: {
    fontSize: 14,
    fontWeight: "600",
    color: "#fff",
  },
  nowPlayingArtist: {
    fontSize: 12,
    color: "#666",
  },
  nowPlayingControls: {
    width: 44,
    height: 44,
    borderRadius: 22,
    backgroundColor: "#fff",
    justifyContent: "center",
    alignItems: "center",
  },
  dots: {
    flexDirection: "row",
    justifyContent: "center",
    alignItems: "center",
    gap: 6,
  },
  dotIndicator: {
    width: 6,
    height: 6,
    borderRadius: 3,
    backgroundColor: "#333",
  },
  dotIndicatorActive: {
    width: 20,
    backgroundColor: "#fff",
  },
});

Пропсы

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

Тип number

onValueChange

Тип (value: number) => void

width?

Тип number

height?

Тип number

activeHeight?

Тип number

activeColor?

Тип string

inactiveColor?

Тип string

disabled?

Тип boolean

tapToSeek?

Тип boolean

thumbSize?

Тип number

thumbColor?

Тип string

showThumb?

Тип boolean

trackScale?

Тип number

thumbScale?

Тип number

containerScale?

Тип number

pressed?

Тип boolean