import { ComponentType, PureComponent, ReactElement, ReactNode, RefObject } from 'react'; import { LayoutChangeEvent, View, ViewProps } from 'react-native'; import { IOContext, IOContextValue } from './IOContext'; import { ObserverInstance } from './IOManager'; import { Element } from './IntersectionObserver'; export interface RenderProps { inView: boolean; onChange: (inView: boolean) => void; } export interface Props { [key: string]: any; } export type InViewProps = T & { children: ReactNode | ((fields: RenderProps) => ReactElement); as?: ComponentType; triggerOnce?: boolean; onLayout?: (event: LayoutChangeEvent) => void; onChange?: (inView: boolean, areaThreshold: number) => void; }; export type InViewWrapper = ComponentType<{ ref?: RefObject | ((ref: any) => void); onLayout?: (event: LayoutChangeEvent) => void; }>; /** * @public * @category 화면 제어 * @name InView * @description * `InView` 컴포넌트는 화면에 요소가 보이기 시작하거나 화면에서 사라지는 것을 감지하는 컴포넌트예요. * 요소가 화면에 조금이라도 보이기 시작하면 `onChanged` 핸들러가 호출되고 첫 번째 인자로 `true` 값이 전달돼요. 반대로 요소가 화면에서 사라지면 `false` 값이 전달돼요. * `onChanged` 핸들러의 두 번째 인자로 요소의 화면 노출 비율이 전달돼요. 노출 비율 값은 `0`에서 `1.0` 사이예요. 예를 들어 `0.2`가 전달되면 컴포넌트가 20%만큼 화면에 노출된 상태라는 의미예요. * ::: warning 유의하세요 `InView`는 반드시 `IOContext`가 포함된 [IOScrollView](/reference/react-native-bedrock/화면%20제어/InView.md) 또는 [IOFlatList](/reference/react-native-bedrock/화면%20제어/IOFlatList.md) 내부에서 사용해야 해요. 만약 `IOContext` 외부에서 사용하면 `IOProviderMissingError`가 발생해요. ::: * @param {Object} props - 컴포넌트에 전달되는 props 객체예요. * @param {React.ReactNode} props.children - 컴포넌트 하위에 렌더링될 자식 컴포넌트들이에요. * @param {React.ComponentType} [prop.as=View] - 실제 렌더링할 컴포넌트를 지정해요. 기본값은 [View](https://reactnative.dev/docs/view) 컴포넌트예요. * @param {boolean} [triggerOnce=false] - 요소가 화면에 처음 보일 때 한 번만 `onChange` 콜백을 호출하려면 이 옵션을 사용해요. * @param {(event: LayoutChangeEvent) => void} [onLayout] - 레이아웃에 변경이 생겼을 때 호출되는 콜백 함수예요. * @param {(inView: boolean, areaThreshold: number) => void} [onChange] - 요소가 화면에 나타나거나 사라질 때 호출되는 콜백 함수예요. 첫 번째 인자로 노출 여부가, 두 번째 인자로 노출 비율이 전달돼요. * * @example * * ### `InView`컴포넌트로 요소의 `10%` 지점을 감지하기 * * ```tsx * import { LayoutChangeEvent, View, Text, Dimensions } from 'react-native'; * import { InView, IOScrollView } from 'react-native-bedrock'; * * export function InViewExample() { * const handleLayout = (event: LayoutChangeEvent) => { * console.log('레이아웃 변경됨', event.nativeEvent.layout); * }; * * const handleChange = (inView: boolean, areaThreshold: number) => { * if (inView) { * console.log(`${areaThreshold * 100}% 비율만큼 화면에 보이는 상태`); * } else { * console.log('화면에 보이지 않는 상태'); * } * }; * * return ( * * * 스크롤을 내려주세요 * * * * * 10% 지점 * * * * * ); * } * ``` */ export class InView extends PureComponent> { static contextType = IOContext; static defaultProps: Partial = { triggerOnce: false, as: View, }; context: undefined | IOContextValue = undefined; mounted = false; protected element: Element; protected instance: undefined | ObserverInstance; protected view: any; constructor(props: InViewProps) { super(props); this.element = { inView: false, intersectionRatio: 0, layout: { x: 0, y: 0, width: 0, height: 0, }, measureLayout: this.measureLayout, }; } componentDidMount() { this.mounted = true; if (this.context?.manager) { this.instance = this.context.manager.observe(this.element, this.handleChange); } } componentWillUnmount() { this.mounted = false; if (this.context?.manager && this.instance) { this.context.manager.unobserve(this.element); } } protected handleChange = (inView: boolean, areaThreshold: number) => { if (this.mounted) { const { triggerOnce, onChange } = this.props; if (inView && triggerOnce) { if (this.context?.manager) { this.context?.manager.unobserve(this.element); } } if (onChange) { onChange(inView, areaThreshold); } } }; protected handleRef = (ref: any) => { this.view = ref; }; protected handleLayout = (event: LayoutChangeEvent) => { const { nativeEvent: { layout }, } = event; if (layout.width !== this.element.layout.width || layout.height !== this.element.layout.height) { if (this.element.onLayout) { this.element.onLayout(); } } const { onLayout } = this.props; if (onLayout) { onLayout(event); } }; measure = (...args: any) => { this.view.measure(...args); }; measureInWindow = (...args: any) => { this.view.measureInWindow(...args); }; measureLayout = (...args: any) => { this.view.measureLayout(...args); }; setNativeProps = (...args: any) => { this.view.setNativeProps(...args); }; focus = (...args: any) => { this.view.focus(...args); }; blur = (...args: any) => { this.view.blur(...args); }; render() { const { as, children, ...props } = this.props; if (typeof children === 'function') { return null; } const ViewComponent: InViewWrapper = (as || View) as InViewWrapper; return ( {children} ); } }