[Refactoring] Sticker View: Use Intersection Observer to detect shared canvas visibility
This commit is contained in:
parent
dba6963c34
commit
0ab0c13d87
@ -15,6 +15,8 @@ import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../hooks/
|
||||
import useBackgroundMode, { isBackgroundModeActive } from '../../hooks/useBackgroundMode';
|
||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||
import { useStateRef } from '../../hooks/useStateRef';
|
||||
import useSharedIntersectionObserver from '../../hooks/useSharedIntersectionObserver';
|
||||
import useThrottledCallback from '../../hooks/useThrottledCallback';
|
||||
|
||||
export type OwnProps = {
|
||||
ref?: RefObject<HTMLDivElement>;
|
||||
@ -46,8 +48,8 @@ let RLottie: RLottieClass;
|
||||
|
||||
// Time for the main interface to completely load
|
||||
const LOTTIE_LOAD_DELAY = 3000;
|
||||
const THROTTLE_MS = 150;
|
||||
const ID_STORE = {};
|
||||
const ANIMATION_END_TIMEOUT = 500;
|
||||
|
||||
async function ensureLottie() {
|
||||
if (!lottiePromise) {
|
||||
@ -98,76 +100,73 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
const playRef = useStateRef(play);
|
||||
const playSegmentRef = useStateRef(playSegment);
|
||||
|
||||
const isUnmountedRef = useRef();
|
||||
const isUnmountedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isUnmountedRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const init = useCallback(() => {
|
||||
if (
|
||||
animationRef.current
|
||||
|| isUnmountedRef.current
|
||||
|| !tgsUrl
|
||||
|| (sharedCanvas && (!sharedCanvasCoords || !sharedCanvas.offsetWidth || !sharedCanvas.offsetHeight))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = containerRef.current || sharedCanvas;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newAnimation = RLottie.init(
|
||||
tgsUrl,
|
||||
container,
|
||||
renderId || generateIdFor(ID_STORE, true),
|
||||
viewId,
|
||||
{
|
||||
noLoop,
|
||||
size,
|
||||
quality,
|
||||
isLowPriority,
|
||||
coords: sharedCanvasCoords,
|
||||
},
|
||||
color,
|
||||
onLoad,
|
||||
onEnded,
|
||||
onLoop,
|
||||
);
|
||||
|
||||
if (speed) {
|
||||
newAnimation.setSpeed(speed);
|
||||
}
|
||||
|
||||
setAnimation(newAnimation);
|
||||
animationRef.current = newAnimation;
|
||||
}, [
|
||||
color, isLowPriority, noLoop, onEnded, onLoad, onLoop, quality,
|
||||
renderId, sharedCanvas, sharedCanvasCoords, size, speed, tgsUrl, viewId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (animation || !tgsUrl || (sharedCanvas && !sharedCanvasCoords)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exec = () => {
|
||||
if (isUnmountedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = containerRef.current || sharedCanvas;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until element is properly mounted
|
||||
if (sharedCanvas && !sharedCanvas.offsetParent) {
|
||||
// `requestMeasure` is useful as timeouts are run in parallel with image loadings and thus causing reflow
|
||||
setTimeout(() => requestMeasure(exec), ANIMATION_END_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
|
||||
const newAnimation = RLottie.init(
|
||||
tgsUrl,
|
||||
container,
|
||||
renderId || generateIdFor(ID_STORE, true),
|
||||
viewId,
|
||||
{
|
||||
noLoop,
|
||||
size,
|
||||
quality,
|
||||
isLowPriority,
|
||||
coords: sharedCanvasCoords,
|
||||
},
|
||||
color,
|
||||
onLoad,
|
||||
onEnded,
|
||||
onLoop,
|
||||
);
|
||||
|
||||
if (speed) {
|
||||
newAnimation.setSpeed(speed);
|
||||
}
|
||||
|
||||
setAnimation(newAnimation);
|
||||
animationRef.current = newAnimation;
|
||||
};
|
||||
|
||||
if (RLottie) {
|
||||
exec();
|
||||
init();
|
||||
} else {
|
||||
ensureLottie().then(() => {
|
||||
requestMeasure(() => {
|
||||
if (containerRef.current) {
|
||||
exec();
|
||||
}
|
||||
});
|
||||
requestMeasure(init);
|
||||
});
|
||||
}
|
||||
}, [
|
||||
animation, renderId, tgsUrl, color, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop, viewId,
|
||||
sharedCanvas, sharedCanvasCoords,
|
||||
]);
|
||||
}, [animation, init, sharedCanvas, sharedCanvasCoords, tgsUrl]);
|
||||
|
||||
const throttledInit = useThrottledCallback(init, [init], THROTTLE_MS);
|
||||
useSharedIntersectionObserver(sharedCanvas, throttledInit);
|
||||
|
||||
useEffect(() => {
|
||||
if (!animation) return;
|
||||
|
||||
@ -691,7 +691,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const throttledResize = useThrottledCallback(handleResize, [handleResize], THROTTLE_MS, false);
|
||||
|
||||
useResizeObserver(shouldFocusOnResize ? ref : undefined, throttledResize);
|
||||
useResizeObserver(ref, throttledResize, !shouldFocusOnResize);
|
||||
|
||||
useEffect(() => {
|
||||
const bottomMarker = bottomMarkerRef.current;
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import {
|
||||
useCallback, useEffect, useMemo, useState,
|
||||
} from '../lib/teact/teact';
|
||||
import { requestMeasure } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
import { round } from '../util/math';
|
||||
|
||||
import useResizeObserver from './useResizeObserver';
|
||||
import useThrottledCallback from './useThrottledCallback';
|
||||
import useSharedIntersectionObserver from './useSharedIntersectionObserver';
|
||||
|
||||
const ANIMATION_END_TIMEOUT = 500;
|
||||
const THROTTLE_MS = 300;
|
||||
const THROTTLE_MS = 150;
|
||||
|
||||
export default function useBoundsInSharedCanvas(
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
@ -27,9 +26,7 @@ export default function useBoundsInSharedCanvas(
|
||||
}
|
||||
|
||||
// Wait until elements are properly mounted
|
||||
if (!container.offsetParent || !canvas.offsetParent) {
|
||||
// `requestMeasure` is useful as timeouts are run in parallel with image loadings and thus causing reflow
|
||||
setTimeout(() => requestMeasure(recalculate), ANIMATION_END_TIMEOUT);
|
||||
if (!canvas.offsetWidth || !canvas.offsetHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -49,6 +46,7 @@ export default function useBoundsInSharedCanvas(
|
||||
|
||||
const throttledRecalculate = useThrottledCallback(recalculate, [recalculate], THROTTLE_MS);
|
||||
useResizeObserver(sharedCanvasRef, throttledRecalculate);
|
||||
useSharedIntersectionObserver(sharedCanvasRef, throttledRecalculate);
|
||||
|
||||
const coords = useMemo(() => (x !== undefined && y !== undefined ? { x, y } : undefined), [x, y]);
|
||||
|
||||
|
||||
@ -12,12 +12,12 @@ import useSyncEffect from './useSyncEffect';
|
||||
const TRANSITION_PROPERTY = 'color';
|
||||
const TRANSITION_STYLE = `60ms ${TRANSITION_PROPERTY} linear`;
|
||||
|
||||
export default function useDynamicColorListener(ref?: React.RefObject<HTMLElement>, isDisabled?: boolean) {
|
||||
export default function useDynamicColorListener(ref: React.RefObject<HTMLElement>, isDisabled?: boolean) {
|
||||
const [hexColor, setHexColor] = useState<string | undefined>();
|
||||
const rgbColorRef = useRef<[number, number, number] | undefined>();
|
||||
|
||||
const updateColor = useCallback(() => {
|
||||
if (!ref?.current || isDisabled) {
|
||||
if (!ref.current || isDisabled) {
|
||||
setHexColor(undefined);
|
||||
return;
|
||||
}
|
||||
@ -28,7 +28,7 @@ export default function useDynamicColorListener(ref?: React.RefObject<HTMLElemen
|
||||
|
||||
// Element does not receive `transitionend` event if parent has `display: none`.
|
||||
// We will receive `resize` event when parent is shown again.
|
||||
useResizeObserver(!isDisabled ? ref : undefined, updateColor);
|
||||
useResizeObserver(ref, updateColor, isDisabled);
|
||||
|
||||
// Update RGB color only when hex color changes
|
||||
useSyncEffect(() => {
|
||||
@ -42,7 +42,7 @@ export default function useDynamicColorListener(ref?: React.RefObject<HTMLElemen
|
||||
}, [hexColor]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = ref?.current;
|
||||
const el = ref.current;
|
||||
if (!el || isDisabled) {
|
||||
return undefined;
|
||||
}
|
||||
@ -55,7 +55,7 @@ export default function useDynamicColorListener(ref?: React.RefObject<HTMLElemen
|
||||
}, [isDisabled, ref]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref?.current;
|
||||
const el = ref.current;
|
||||
if (!el) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -3,25 +3,30 @@ import { useEffect } from '../lib/teact/teact';
|
||||
import type { CallbackManager } from '../util/callbacks';
|
||||
|
||||
import { createCallbackManager } from '../util/callbacks';
|
||||
import { useStateRef } from './useStateRef';
|
||||
|
||||
const elementObserverMap = new Map<HTMLElement, [ResizeObserver, CallbackManager]>();
|
||||
|
||||
export default function useResizeObserver(
|
||||
ref: React.RefObject<HTMLElement> | undefined,
|
||||
onResize: (entry: ResizeObserverEntry) => void,
|
||||
isDisabled = false,
|
||||
) {
|
||||
const onResizeRef = useStateRef(onResize);
|
||||
|
||||
useEffect(() => {
|
||||
if (!('ResizeObserver' in window) || !ref?.current) {
|
||||
const el = ref?.current;
|
||||
if (!el || isDisabled) {
|
||||
return undefined;
|
||||
}
|
||||
const el = ref.current;
|
||||
|
||||
const callback: ResizeObserverCallback = ([entry]) => {
|
||||
// Ignore updates when element is not properly mounted (`display: none`)
|
||||
if (entry.contentRect.width === 0 && entry.contentRect.height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
onResize(entry);
|
||||
onResizeRef.current(entry);
|
||||
};
|
||||
|
||||
let [observer, callbackManager] = elementObserverMap.get(el) || [undefined, undefined];
|
||||
@ -41,5 +46,5 @@ export default function useResizeObserver(
|
||||
elementObserverMap.delete(el);
|
||||
}
|
||||
};
|
||||
}, [onResize, ref]);
|
||||
}, [isDisabled, onResizeRef, ref]);
|
||||
}
|
||||
|
||||
50
src/hooks/useSharedIntersectionObserver.ts
Normal file
50
src/hooks/useSharedIntersectionObserver.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { useEffect } from '../lib/teact/teact';
|
||||
|
||||
import type { CallbackManager } from '../util/callbacks';
|
||||
|
||||
import { createCallbackManager } from '../util/callbacks';
|
||||
import { useStateRef } from './useStateRef';
|
||||
|
||||
const elementObserverMap = new Map<HTMLElement, [IntersectionObserver, CallbackManager]>();
|
||||
|
||||
export default function useSharedIntersectionObserver(
|
||||
refOrElement: React.RefObject<HTMLElement> | HTMLElement | undefined,
|
||||
onIntersectionChange: (entry: IntersectionObserverEntry) => void,
|
||||
isDisabled = false,
|
||||
) {
|
||||
const onIntersectionChangeRef = useStateRef(onIntersectionChange);
|
||||
|
||||
useEffect(() => {
|
||||
const el = refOrElement && 'current' in refOrElement ? refOrElement.current : refOrElement;
|
||||
if (!el || isDisabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const callback: IntersectionObserverCallback = ([entry]) => {
|
||||
// Ignore updates when element is not properly mounted (`display: none`)
|
||||
if (!(entry.target as HTMLElement).offsetWidth || !(entry.target as HTMLElement).offsetHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
onIntersectionChangeRef.current(entry);
|
||||
};
|
||||
|
||||
let [observer, callbackManager] = elementObserverMap.get(el) || [undefined, undefined];
|
||||
if (!observer) {
|
||||
callbackManager = createCallbackManager();
|
||||
observer = new IntersectionObserver(callbackManager.runCallbacks);
|
||||
elementObserverMap.set(el, [observer, callbackManager]);
|
||||
observer.observe(el);
|
||||
}
|
||||
callbackManager!.addCallback(callback);
|
||||
|
||||
return () => {
|
||||
callbackManager!.removeCallback(callback);
|
||||
if (!callbackManager!.hasCallbacks()) {
|
||||
observer!.unobserve(el);
|
||||
observer!.disconnect();
|
||||
elementObserverMap.delete(el);
|
||||
}
|
||||
};
|
||||
}, [isDisabled, onIntersectionChangeRef, refOrElement]);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user