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