/** * 响应式设计 Hook * 提供屏幕尺寸、断点、方向、平台检测等响应式信息 */ import { useState, useEffect, useMemo, useCallback } from 'react'; import { Dimensions, ScaledSize, Platform } from 'react-native'; // ==================== 断点定义 ==================== export const BREAKPOINTS = { mobile: 0, // 手机 tablet: 768, // 平板竖屏 desktop: 1024, // 平板横屏/桌面 wide: 1440, // 宽屏桌面 } as const; export type BreakpointKey = keyof typeof BREAKPOINTS; export type BreakpointValue = typeof BREAKPOINTS[BreakpointKey]; // ==================== 更细粒度的断点定义 ==================== export const FINE_BREAKPOINTS = { xs: 0, // 超小屏手机 sm: 375, // 小屏手机 (iPhone SE, 小屏安卓) md: 414, // 中屏手机 (iPhone Pro Max, 大屏安卓) lg: 768, // 平板竖屏 / 大折叠屏手机展开 xl: 1024, // 平板横屏 / 小桌面 '2xl': 1280, // 桌面 '3xl': 1440, // 大桌面 '4xl': 1920, // 超大屏 } as const; export type FineBreakpointKey = keyof typeof FINE_BREAKPOINTS; // ==================== 响应式值类型 ==================== export type ResponsiveValue = T | Partial>; // ==================== 返回值类型 ==================== export interface ResponsiveInfo { // 基础尺寸 width: number; height: number; // 基础断点 breakpoint: BreakpointKey; isMobile: boolean; isTablet: boolean; isDesktop: boolean; isWide: boolean; isWideScreen: boolean; // 细粒度断点 fineBreakpoint: FineBreakpointKey; isXS: boolean; isSM: boolean; isMD: boolean; isLG: boolean; isXL: boolean; is2XL: boolean; is3XL: boolean; is4XL: boolean; // 方向 orientation: 'portrait' | 'landscape'; isPortrait: boolean; isLandscape: boolean; // 平台检测 platform: { OS: 'ios' | 'android' | 'windows' | 'macos' | 'web'; isWeb: boolean; isIOS: boolean; isAndroid: boolean; isNative: boolean; }; } // ==================== 获取当前断点 ==================== function getBreakpoint(width: number): BreakpointKey { if (width >= BREAKPOINTS.wide) { return 'wide'; } if (width >= BREAKPOINTS.desktop) { return 'desktop'; } if (width >= BREAKPOINTS.tablet) { return 'tablet'; } return 'mobile'; } // ==================== 获取细粒度断点 ==================== function getFineBreakpoint(width: number): FineBreakpointKey { if (width >= FINE_BREAKPOINTS['4xl']) return '4xl'; if (width >= FINE_BREAKPOINTS['3xl']) return '3xl'; if (width >= FINE_BREAKPOINTS['2xl']) return '2xl'; if (width >= FINE_BREAKPOINTS.xl) return 'xl'; if (width >= FINE_BREAKPOINTS.lg) return 'lg'; if (width >= FINE_BREAKPOINTS.md) return 'md'; if (width >= FINE_BREAKPOINTS.sm) return 'sm'; return 'xs'; } // ==================== 获取屏幕方向 ==================== function getOrientation(width: number, height: number): 'portrait' | 'landscape' { return width > height ? 'landscape' : 'portrait'; } // ==================== useWindowDimensions Hook ==================== /** * 获取窗口尺寸,支持屏幕旋转响应 * 替代 Dimensions.get('window'),提供实时尺寸更新 */ function useWindowDimensions(): ScaledSize { const [dimensions, setDimensions] = useState(() => Dimensions.get('window')); useEffect(() => { const subscription = Dimensions.addEventListener('change', ({ window }) => { setDimensions(window); }); return () => { subscription.remove(); }; }, []); return dimensions; } // ==================== useResponsive Hook ==================== /** * 响应式设计 Hook * 提供屏幕尺寸、断点、方向、平台检测等响应式信息 * * @returns ResponsiveInfo 响应式信息对象 * * @example * const { * width, height, * breakpoint, isMobile, isTablet, isDesktop, isWide, * fineBreakpoint, isXS, isSM, isMD, isLG, isXL, * orientation, isPortrait, isLandscape, * platform: { isWeb, isIOS, isAndroid } * } = useResponsive(); */ export function useResponsive(): ResponsiveInfo { const windowDimensions = useWindowDimensions(); const { width, height } = windowDimensions; const breakpoint = getBreakpoint(width); const fineBreakpoint = getFineBreakpoint(width); const orientation = getOrientation(width, height); const isMobile = breakpoint === 'mobile'; const isTablet = breakpoint === 'tablet'; const isDesktop = breakpoint === 'desktop'; const isWide = breakpoint === 'wide'; const isWideScreen = isTablet || isDesktop || isWide; const isXS = fineBreakpoint === 'xs'; const isSM = fineBreakpoint === 'sm'; const isMD = fineBreakpoint === 'md'; const isLG = fineBreakpoint === 'lg'; const isXL = fineBreakpoint === 'xl'; const is2XL = fineBreakpoint === '2xl'; const is3XL = fineBreakpoint === '3xl'; const is4XL = fineBreakpoint === '4xl'; const isPortrait = orientation === 'portrait'; const isLandscape = orientation === 'landscape'; const platform = useMemo(() => ({ OS: Platform.OS, isWeb: Platform.OS === 'web', isIOS: Platform.OS === 'ios', isAndroid: Platform.OS === 'android', isNative: Platform.OS !== 'web', }), []); return { width, height, breakpoint, isMobile, isTablet, isDesktop, isWide, isWideScreen, fineBreakpoint, isXS, isSM, isMD, isLG, isXL, is2XL, is3XL, is4XL, orientation, isPortrait, isLandscape, platform, }; } // ==================== 响应式值选择器 ==================== /** * 根据当前断点从响应式值对象中选择合适的值 * * @param value - 响应式值,可以是单一值或断点映射对象 * @param currentBreakpoint - 当前细粒度断点 * @returns 选中的值 * * @example * const padding = useResponsiveValue({ xs: 8, md: 16, lg: 24 }); * // 在 xs 屏幕返回 8,md 屏幕返回 16,lg 及以上返回 24 */ export function useResponsiveValue(value: ResponsiveValue): T { const { fineBreakpoint } = useResponsive(); return useMemo(() => { // 如果不是对象,直接返回 if (typeof value !== 'object' || value === null || Array.isArray(value)) { return value as T; } const breakpointOrder: FineBreakpointKey[] = ['4xl', '3xl', '2xl', 'xl', 'lg', 'md', 'sm', 'xs']; const valueMap = value as Partial>; // 从当前断点开始向下查找 const currentIndex = breakpointOrder.indexOf(fineBreakpoint); for (let i = currentIndex; i < breakpointOrder.length; i++) { const bp = breakpointOrder[i]; if (bp in valueMap) { return valueMap[bp]!; } } // 如果没找到,返回 xs 的值或第一个值 return (valueMap.xs ?? Object.values(valueMap)[0]) as T; }, [value, fineBreakpoint]); } // ==================== 响应式样式生成器 ==================== /** * 根据断点生成响应式样式 * * @param styles - 响应式样式对象 * @returns 当前断点对应的样式 * * @example * const containerStyle = useResponsiveStyle({ * padding: { xs: 8, md: 16, lg: 24 }, * fontSize: { xs: 14, lg: 16 } * }); */ export function useResponsiveStyle>>( styles: T ): { [K in keyof T]: T[K] extends ResponsiveValue ? V : never } { const { fineBreakpoint } = useResponsive(); return useMemo(() => { const result = {} as { [K in keyof T]: T[K] extends ResponsiveValue ? V : never }; for (const key in styles) { const value = styles[key]; if (typeof value !== 'object' || value === null || Array.isArray(value)) { (result as Record)[key] = value; } else { const valueMap = value as Partial>; const breakpointOrder: FineBreakpointKey[] = ['4xl', '3xl', '2xl', 'xl', 'lg', 'md', 'sm', 'xs']; const currentIndex = breakpointOrder.indexOf(fineBreakpoint); let selectedValue: unknown = undefined; for (let i = currentIndex; i < breakpointOrder.length; i++) { const bp = breakpointOrder[i]; if (bp in valueMap) { selectedValue = valueMap[bp]; break; } } (result as Record)[key] = selectedValue ?? valueMap.xs ?? Object.values(valueMap)[0]; } } return result; }, [styles, fineBreakpoint]); } // ==================== 断点比较工具 ==================== /** * 检查当前断点是否大于等于目标断点 * * @param current - 当前断点 * @param target - 目标断点 * @returns 是否满足条件 */ export function isBreakpointGTE( current: FineBreakpointKey, target: FineBreakpointKey ): boolean { const order: FineBreakpointKey[] = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl']; return order.indexOf(current) >= order.indexOf(target); } /** * 检查当前断点是否小于目标断点 * * @param current - 当前断点 * @param target - 目标断点 * @returns 是否满足条件 */ export function isBreakpointLT( current: FineBreakpointKey, target: FineBreakpointKey ): boolean { return !isBreakpointGTE(current, target); } // ==================== 断点范围检查 Hook ==================== /** * 检查当前是否在指定断点范围内 * * @param options - 断点范围选项 * @returns 是否在范围内 * * @example * const isMediumUp = useBreakpointGTE('md'); * const isMobileOnly = useBreakpointLT('lg'); */ export function useBreakpointGTE(target: FineBreakpointKey): boolean { const { fineBreakpoint } = useResponsive(); return isBreakpointGTE(fineBreakpoint, target); } export function useBreakpointLT(target: FineBreakpointKey): boolean { const { fineBreakpoint } = useResponsive(); return isBreakpointLT(fineBreakpoint, target); } export function useBreakpointBetween( min: FineBreakpointKey, max: FineBreakpointKey ): boolean { const { fineBreakpoint } = useResponsive(); return isBreakpointGTE(fineBreakpoint, min) && !isBreakpointGTE(fineBreakpoint, max); } // ==================== 平台检测 Hook ==================== /** * 平台检测 Hook * 提供便捷的平台检测方法 * * @example * const { isWeb, isIOS, isAndroid, isNative } = usePlatform(); */ export function usePlatform() { const { platform } = useResponsive(); return platform; } // ==================== 媒体查询模拟 ==================== /** * 模拟 CSS 媒体查询 * * @param query - 查询条件 * @returns 是否匹配 * * @example * const isMinWidth768 = useMediaQuery({ minWidth: 768 }); * const isMaxWidth1024 = useMediaQuery({ maxWidth: 1024 }); * const isPortrait = useMediaQuery({ orientation: 'portrait' }); */ interface MediaQueryOptions { minWidth?: number; maxWidth?: number; minHeight?: number; maxHeight?: number; orientation?: 'portrait' | 'landscape'; } export function useMediaQuery(query: MediaQueryOptions): boolean { const { width, height, orientation: currentOrientation } = useResponsive(); return useMemo(() => { if (query.minWidth !== undefined && width < query.minWidth) return false; if (query.maxWidth !== undefined && width > query.maxWidth) return false; if (query.minHeight !== undefined && height < query.minHeight) return false; if (query.maxHeight !== undefined && height > query.maxHeight) return false; if (query.orientation !== undefined && currentOrientation !== query.orientation) return false; return true; }, [width, height, currentOrientation, query]); } // ==================== 列数计算工具 ==================== /** * 根据容器宽度计算合适的列数 * * @param containerWidth - 容器宽度 * @param options - 配置选项 * @returns 列数 * * @example * const columns = useColumnCount({ * xs: 1, * sm: 2, * md: 3, * lg: 4 * }); */ export function useColumnCount( columnConfig: Partial> = {} ): number { const { fineBreakpoint } = useResponsive(); const defaultConfig: Record = { xs: 1, sm: 1, md: 2, lg: 3, xl: 4, '2xl': 4, '3xl': 5, '4xl': 6, }; return useMemo(() => { const config = { ...defaultConfig, ...columnConfig }; const breakpointOrder: FineBreakpointKey[] = ['4xl', '3xl', '2xl', 'xl', 'lg', 'md', 'sm', 'xs']; const currentIndex = breakpointOrder.indexOf(fineBreakpoint); for (let i = currentIndex; i < breakpointOrder.length; i++) { const bp = breakpointOrder[i]; if (config[bp] !== undefined) { return config[bp]; } } return 1; }, [fineBreakpoint, columnConfig]); } // ==================== 间距计算工具 ==================== /** * 响应式间距 Hook * * @param spacingConfig - 间距配置 * @returns 当前断点对应的间距值 * * @example * const gap = useResponsiveSpacing({ xs: 8, md: 16, lg: 24 }); */ export function useResponsiveSpacing( spacingConfig: Partial> = {} ): number { const { fineBreakpoint } = useResponsive(); const defaultConfig: Record = { xs: 8, sm: 8, md: 12, lg: 16, xl: 20, '2xl': 24, '3xl': 32, '4xl': 40, }; return useMemo(() => { const config = { ...defaultConfig, ...spacingConfig }; const breakpointOrder: FineBreakpointKey[] = ['4xl', '3xl', '2xl', 'xl', 'lg', 'md', 'sm', 'xs']; const currentIndex = breakpointOrder.indexOf(fineBreakpoint); for (let i = currentIndex; i < breakpointOrder.length; i++) { const bp = breakpointOrder[i]; if (config[bp] !== undefined) { return config[bp]; } } return defaultConfig.xs; }, [fineBreakpoint, spacingConfig]); } export default useResponsive;