Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-apk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:

- name: Build release APK
working-directory: example/android
run: ./gradlew assembleRelease --no-daemon --stacktrace
run: ./gradlew assembleRelease --no-daemon --stacktrace -PnewArchEnabled=true

- name: Upload APK
uses: actions/upload-artifact@v4
Expand Down
10 changes: 1 addition & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,7 @@
"type": "module",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"react-native": {
"windows": {
"sourceDir": "windows",
"solutionFile": "RNViewShot.sln",
"project": {
"projectFile": "RNViewShot\\RNViewShot.csproj"
}
}
},
"react-native": "src/index.tsx",
"keywords": [
"react-native",
"screenshot",
Expand Down
2 changes: 1 addition & 1 deletion react-native.config.js → react-native.config.cjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default {
module.exports = {
dependency: {
platforms: {
ios: {},
Expand Down
236 changes: 142 additions & 94 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import React, {Component, ReactNode, RefObject} from "react";
import React, {
ReactNode,
RefObject,
useRef,
useCallback,
useEffect,
useMemo,
useImperativeHandle,
forwardRef,
} from "react";
import {
View,
Platform,
Expand Down Expand Up @@ -262,115 +271,154 @@ function checkCompatibleProps(props: ViewShotProperties): void {
}
}

export default class ViewShot extends Component<ViewShotProperties> {
static captureRef = captureRef;
static releaseCapture = releaseCapture;

root: any = null;
_raf: number | null = null;
lastCapturedURI: string | null = null;
/**
* Ref handle exposed by ViewShot component.
*/
export type ViewShotRef = {
capture: () => Promise<string>;
};

resolveFirstLayout: ((layout: any) => void) | null = null;
firstLayoutPromise: Promise<any> = new Promise(resolve => {
this.resolveFirstLayout = resolve;
});
const ViewShotComponent = forwardRef<ViewShotRef, ViewShotProperties>(
function ViewShot(props, ref) {
const {
children,
options,
captureMode,
onCapture,
onCaptureFailure,
onLayout,
style,
} = props;

capture = (): Promise<string> =>
this.firstLayoutPromise
.then(() => {
const {root} = this;
if (!root) return neverEndingPromise as Promise<never>; // component is unmounted, you never want to hear back from the promise
return captureRef(root, this.props.options);
})
.then(
uri => {
this.onCapture(uri);
return uri;
},
e => {
this.onCaptureFailure(e);
throw e;
},
) as Promise<string>;
const rootRef = useRef<View>(null);
const rafRef = useRef<number | null>(null);
const lastCapturedURIRef = useRef<string | null>(null);
const resolveFirstLayoutRef = useRef<((layout: any) => void) | null>(null);

onCapture = (uri: string): void => {
if (!this.root) return;
if (this.lastCapturedURI) {
// schedule releasing the previous capture
setTimeout(releaseCapture, 500, this.lastCapturedURI);
}
this.lastCapturedURI = uri;
const {onCapture} = this.props;
if (onCapture) onCapture(uri);
};
const firstLayoutPromise = useMemo(
() =>
new Promise<any>(resolve => {
resolveFirstLayoutRef.current = resolve;
}),
[],
);

onCaptureFailure = (e: Error): void => {
if (!this.root) return;
const {onCaptureFailure} = this.props;
if (onCaptureFailure) onCaptureFailure(e);
};
// Keep latest props in refs so stable callbacks always see fresh values
const onCaptureRef = useRef(onCapture);
onCaptureRef.current = onCapture;
const onCaptureFailureRef = useRef(onCaptureFailure);
onCaptureFailureRef.current = onCaptureFailure;
const optionsRef = useRef(options);
optionsRef.current = options;

syncCaptureLoop = (captureMode: string | null | undefined): void => {
cancelAnimationFrame(this._raf as number);
if (captureMode === "continuous") {
let previousCaptureURI: string | null = "-"; // needs to capture at least once at first, so we use "-" arbitrary string
const loop = (): void => {
this._raf = requestAnimationFrame(loop);
if (previousCaptureURI === this.lastCapturedURI) return; // previous capture has not finished, don't capture yet
previousCaptureURI = this.lastCapturedURI;
this.capture();
};
this._raf = requestAnimationFrame(loop);
}
};
const capture = useCallback(
(): Promise<string> =>
firstLayoutPromise
.then(() => {
if (!rootRef.current) return neverEndingPromise as Promise<never>;
return captureRef(rootRef.current, optionsRef.current);
})
.then(
uri => {
if (!rootRef.current) return uri;
if (lastCapturedURIRef.current) {
setTimeout(releaseCapture, 500, lastCapturedURIRef.current);
}
lastCapturedURIRef.current = uri;
if (onCaptureRef.current) onCaptureRef.current(uri);
return uri;
},
e => {
if (!rootRef.current) throw e;
if (onCaptureFailureRef.current) onCaptureFailureRef.current(e);
throw e;
},
) as Promise<string>,
[firstLayoutPromise],
);

onRef = (ref: any): void => {
this.root = ref;
};
useImperativeHandle(ref, () => ({capture}), [capture]);

onLayout = (e: LayoutChangeEvent): void => {
const {onLayout} = this.props;
if (this.resolveFirstLayout) {
this.resolveFirstLayout(e.nativeEvent.layout);
}
if (onLayout) onLayout(e);
};
const syncCaptureLoop = useCallback(
(mode: string | null | undefined): void => {
cancelAnimationFrame(rafRef.current as number);
if (mode === "continuous") {
let previousCaptureURI: string | null = "-";
const loop = (): void => {
rafRef.current = requestAnimationFrame(loop);
if (previousCaptureURI === lastCapturedURIRef.current) return;
previousCaptureURI = lastCapturedURIRef.current;
capture();
};
rafRef.current = requestAnimationFrame(loop);
}
},
[capture],
);

componentDidMount(): void {
if (__DEV__) checkCompatibleProps(this.props);
if (this.props.captureMode === "mount") {
this.capture();
} else {
this.syncCaptureLoop(this.props.captureMode);
}
}
// Mount + unmount lifecycle (equivalent to componentDidMount / componentWillUnmount)
useEffect(() => {
if (__DEV__) checkCompatibleProps(props);
if (captureMode === "mount") {
capture();
} else {
syncCaptureLoop(captureMode);
}
return () => syncCaptureLoop(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

componentDidUpdate(prevProps: ViewShotProperties): void {
if (this.props.captureMode !== undefined) {
if (this.props.captureMode !== prevProps.captureMode) {
this.syncCaptureLoop(this.props.captureMode);
// Update lifecycle (equivalent to componentDidUpdate)
const prevCaptureModeRef = useRef(captureMode);
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
prevCaptureModeRef.current = captureMode;
return;
}
}
if (this.props.captureMode === "update") {
this.capture();
}
}
if (
captureMode !== undefined &&
captureMode !== prevCaptureModeRef.current
) {
syncCaptureLoop(captureMode);
}
prevCaptureModeRef.current = captureMode;
if (captureMode === "update") {
capture();
}
});

componentWillUnmount(): void {
this.syncCaptureLoop(null);
}
const onLayoutHandler = useCallback(
(e: LayoutChangeEvent): void => {
if (resolveFirstLayoutRef.current) {
resolveFirstLayoutRef.current(e.nativeEvent.layout);
resolveFirstLayoutRef.current = null;
}
if (onLayout) onLayout(e);
},
[onLayout],
);

render(): ReactNode {
const {children} = this.props;
return (
<View
ref={this.onRef}
ref={rootRef}
collapsable={false}
onLayout={this.onLayout}
style={this.props.style}
onLayout={onLayoutHandler}
style={style}
>
{children}
</View>
);
}
}
},
);

// Attach static methods for backwards compatibility (ViewShot.captureRef, ViewShot.releaseCapture)
const ViewShot = ViewShotComponent as typeof ViewShotComponent & {
captureRef: typeof captureRef;
releaseCapture: typeof releaseCapture;
};
ViewShot.captureRef = captureRef;
ViewShot.releaseCapture = releaseCapture;

export default ViewShot;
2 changes: 1 addition & 1 deletion src/specs/NativeRNViewShot.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {TurboModule} from "react-native";
import {TurboModuleRegistry, NativeModules, Platform} from "react-native";
import {TurboModuleRegistry, NativeModules} from "react-native";
import {Int32, WithDefault} from "react-native/Libraries/Types/CodegenTypes";

export interface Spec extends TurboModule {
Expand Down
Loading