Компоненты/Строка поиска

Строка поиска

31 января 2025

Строка поиска с плавными переходами по фокусу

Установка

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

Файлы

SearchBar.tsx
НазадПоиск потягомДалееПеремотка

Вручную

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

npm install @expo/vector-icons expo-blur expo-symbols react-native-reanimated react-native-worklets
pnpm add @expo/vector-icons expo-blur expo-symbols react-native-reanimated react-native-worklets
yarn add @expo/vector-icons expo-blur expo-symbols react-native-reanimated react-native-worklets
bun add @expo/vector-icons expo-blur expo-symbols react-native-reanimated react-native-worklets

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

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

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

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

import { SearchBar } from "@/shared/components/molecules/search-bar";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";

const CUSTOMIZE = [
  { key: "placeholder", label: "Подсказка", type: "text", default: "Search", maxLength: 30 },
  { key: "tint", label: "Акцент", type: "color", default: "#ffffff" },
  { key: "containerWidth", label: "Ширина", type: "slider", min: 220, max: 380, step: 2, unit: "px", default: 350 },
  { key: "cancelButtonWidth", label: "Ширина «Отмена»", type: "slider", min: 50, max: 110, step: 1, unit: "px", default: 68 },
  { key: "centerWhenUnfocused", label: "По центру", type: "toggle", default: true },
  { key: "enableWidthAnimation", label: "Анимация ширины", type: "toggle", default: true },
] as const;

export default function App() {
  const custom = useCustomize(CUSTOMIZE);
  const { width: screenWidth } = useWindowDimensions();
  const containerWidth = Math.min(custom.containerWidth, Math.floor(screenWidth - 32));
  const hostRef = useRef<View>(null);

  const findInput = () =>
    (hostRef.current as unknown as HTMLElement | null)?.querySelector("input");

  useAutoplay(async ({ wait }) => {
    await wait(500);
    findInput()?.focus({ preventScroll: true });
    await wait(1000);
    findInput()?.blur();
    await wait(450);
  });

  return (
    <Showcase>
      <StatusBar style="inverted" />
      <GestureHandlerRootView style={styles.container}>
        <View ref={hostRef}>
          <SearchBar
            key={`${containerWidth}-${custom.cancelButtonWidth}-${custom.centerWhenUnfocused}-${custom.enableWidthAnimation}`}
            placeholder={custom.placeholder}
            containerWidth={containerWidth}
            cancelButtonWidth={custom.cancelButtonWidth}
            centerWhenUnfocused={custom.centerWhenUnfocused}
            enableWidthAnimation={custom.enableWidthAnimation}
            tint={custom.tint}
            textCenterOffset={2.2}
            iconCenterOffset={2.2}
            inputStyle={Platform.OS === "web" ? { minHeight: 34 } : undefined}
          />
        </View>
      </GestureHandlerRootView>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#0a0a0a",
    justifyContent: "center",
    alignItems: "center",
    overflow: "hidden",
  },
  header: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    paddingHorizontal: 24,
    paddingTop: 70,
  },
  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",
  },
});

Пропсы

Пропс
Описание
placeholder?

Placeholder text for the search input

Тип string·По умолчанию "Search..."

onSearch?

Callback when search text changes

Тип (query: string) => void

onClear?

Callback when search is cleared

Тип () => void

style?

Additional style for the container

Тип ViewStyle

inputStyle?

Style for the input

Тип TextStyle

width?

Width of the search bar

Тип DimensionValue·По умолчанию "100%"

maxWidth?

Maximum width of the search bar

Тип number·По умолчанию screenWidth - 32

parentHeight?

Height of the parent container

Тип number | 40·По умолчанию 40

tint?

Tint color for the search

Тип string

iconPadding?

Тип number

renderTrailingIcons?

Тип () => React.ReactNode

renderLeadingIcons?

Тип () => React.ReactNode

onSearchDone?

Тип () => void

onSearchMount?

Тип () => void

containerWidth?

Тип number

focusedWidth?

Тип number

cancelButtonWidth?

Тип number

iconStyle?

Тип StyleProp<ViewStyle>

enableWidthAnimation?

Тип boolean

centerWhenUnfocused?

Тип boolean

textCenterOffset?

Тип number

iconCenterOffset?

Тип number