diff --git a/packages/griffith/src/contexts/MessageContext.tsx b/packages/griffith/src/contexts/MessageContext.tsx index 158537b3..deec97e2 100644 --- a/packages/griffith/src/contexts/MessageContext.tsx +++ b/packages/griffith/src/contexts/MessageContext.tsx @@ -6,6 +6,7 @@ import { EventParamsMap, createMessageHelper, } from 'griffith-message' +import useHandler from '../hooks/useHandler' const EVENT_TYPE = 'event' const ACTION_TYPE = 'action' @@ -66,13 +67,6 @@ type MessageProviderProps = { type MessageHelper = ReturnType -const useHandler = any>(handler: T) => { - const handlerRef = useRef(handler) - handlerRef.current = handler - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return useRef(((...args: any[]) => handlerRef.current(...args)) as T).current -} - /** * * Retrieve `MessageContext` from outside of Player diff --git a/packages/griffith/src/contexts/PositionProvider.tsx b/packages/griffith/src/contexts/PositionProvider.tsx index 899d2bff..c072e133 100644 --- a/packages/griffith/src/contexts/PositionProvider.tsx +++ b/packages/griffith/src/contexts/PositionProvider.tsx @@ -1,6 +1,8 @@ -import React from 'react' +import React, {useRef, useEffect, useState, useMemo} from 'react' import {css} from 'aphrodite/no-important' import {reduce} from 'griffith-utils' +import useHandler from '../hooks/useHandler' +import usePrevious from '../hooks/usePrevious' import listenResize from '../utils/listenResize' import PositionContext from './PositionContext' import styles from './PositionProvider.styles' @@ -9,122 +11,86 @@ type Props = { shouldObserveResize?: boolean } -type State = { +type VideoSize = { videoWidth: number videoHeight: number - isFullWidth: boolean - helperImageSrc?: string | null } -export default class PositionProvider extends React.PureComponent< - Props, - State -> { - unlistenResize_?: () => void +// Create a placeholder image to preserve the original aspect ratio +const createHolderImageSrc = (width: number, height: number) => + `data:image/svg+xml;utf8,` - state = { +const PositionProvider: React.FC = ({children}) => { + const ref = useRef(null) + const [helperImageSrc, setHelperImageSrc] = useState(null) + const [isFullWidth, setIsFullWidth] = useState(false) + const [videoSize, setVideoSize] = useState({ videoWidth: 0, videoHeight: 0, - isFullWidth: false, - helperImageSrc: null, - } + }) - ref = React.createRef() - - componentDidMount() { - if (this.props.shouldObserveResize) { - this.startObservingResize() - } - this.updateHelperImageSrc() - } - - componentDidUpdate(prevProps: Props, prevState: State) { - const {shouldObserveResize: prevShouldObserve} = prevProps - const {videoWidth: prevWidth, videoHeight: prevHeight} = prevState - const {shouldObserveResize} = this.props - const {videoWidth, videoHeight} = this.state - - if (prevWidth !== videoWidth || prevHeight !== videoHeight) { - this.triggerUpdateIsFullWidth() - this.updateHelperImageSrc() - } - - if (!prevShouldObserve && shouldObserveResize) { - this.startObservingResize() - } - if (prevShouldObserve && !shouldObserveResize) { - this.stopObservingResize() - } - } - - componentWillUnmount() { - this.stopObservingResize() - } - - startObservingResize = () => { - const root = this.ref.current - if (root) { - this.unlistenResize_ = listenResize(root, this.updateIsFullWidth) - } - } - - stopObservingResize() { - if (this.unlistenResize_) { - this.unlistenResize_() - } - } - - updateHelperImageSrc = () => { - const {videoWidth, videoHeight} = this.state + const updateHelperImageSrc = useHandler(() => { + const {videoWidth, videoHeight} = videoSize if (!videoWidth || !videoHeight) { return } const [width, height] = reduce(videoWidth, videoHeight) - const canvas = document.createElement('canvas') - canvas.width = width - canvas.height = height - const helperImageSrc = canvas.toDataURL() - this.setState({helperImageSrc}) - } + setHelperImageSrc(createHolderImageSrc(width, height)) + }) - updateIsFullWidth = () => { - const {videoWidth, videoHeight} = this.state + const updateIsFullWidth = useHandler(() => { + const {videoWidth, videoHeight} = videoSize if (!videoWidth || !videoHeight) { return } - const root = this.ref.current - if (!root) return + if (!ref.current) { + return + } - const {width, height} = root.getBoundingClientRect() + const {width, height} = ref.current.getBoundingClientRect() // 因为视频缩放后,长宽可能不严格相等,所以认为差值小于等于 0.01 的就算相等。 // 比如 1280x720 (1.777777778) 和 848x478 (1.774058577),认为相等。 - const isFullWidth = width / height - videoWidth / videoHeight <= 0.01 - if (isFullWidth !== this.state.isFullWidth) { - this.setState({isFullWidth}) + const isFullWidthNew = width / height - videoWidth / videoHeight <= 0.01 + if (isFullWidthNew !== isFullWidth) { + setIsFullWidth(isFullWidthNew) } - } + }) - triggerUpdateIsFullWidth = () => requestAnimationFrame(this.updateIsFullWidth) + useEffect(() => { + if (ref.current) { + return listenResize(ref.current, updateIsFullWidth) + } + }, [ref, updateIsFullWidth]) - updateVideoSize = ({videoWidth, videoHeight}: any) => { - this.setState({videoWidth, videoHeight}) - } + useEffect(() => { + updateHelperImageSrc() + }, [updateHelperImageSrc]) - render() { - const {children} = this.props - const {isFullWidth, helperImageSrc} = this.state - return ( - -
- {children} -
-
- ) - } + const prevVideoSize = usePrevious(videoSize) + useEffect(() => { + if (!prevVideoSize) { + return + } + const {videoWidth: prevWidth, videoHeight: prevHeight} = prevVideoSize + const {videoWidth, videoHeight} = videoSize + if (prevWidth !== videoWidth || prevHeight !== videoHeight) { + requestAnimationFrame(updateIsFullWidth) + updateHelperImageSrc() + } + }, [prevVideoSize, updateHelperImageSrc, updateIsFullWidth, videoSize]) + + const contextValue = useMemo( + () => ({isFullWidth, helperImageSrc, updateVideoSize: setVideoSize}), + [helperImageSrc, isFullWidth] + ) + + return ( + +
+ {children} +
+
+ ) } + +export default PositionProvider diff --git a/packages/griffith/src/hooks/useHandler.ts b/packages/griffith/src/hooks/useHandler.ts new file mode 100644 index 00000000..be9e6bb8 --- /dev/null +++ b/packages/griffith/src/hooks/useHandler.ts @@ -0,0 +1,13 @@ +import {useRef} from 'react' + +/** + * Create immutable function ref + */ +const useHandler = any>(handler: T) => { + const handlerRef = useRef(handler) + handlerRef.current = handler + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return useRef(((...args: any[]) => handlerRef.current(...args)) as T).current +} + +export default useHandler diff --git a/packages/griffith/src/hooks/usePrevious.ts b/packages/griffith/src/hooks/usePrevious.ts new file mode 100644 index 00000000..e6d4a874 --- /dev/null +++ b/packages/griffith/src/hooks/usePrevious.ts @@ -0,0 +1,18 @@ +import {useRef, useEffect} from 'react' + +/** + * Get previous rendered value + * + * @see https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state + */ +function usePrevious(value: T) { + const ref = useRef() + + useEffect(() => { + ref.current = value + }, [value]) + + return ref.current +} + +export default usePrevious