説明なし

index.js 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. //@flow
  2. import React, { Component } from "react";
  3. import { View, NativeModules, Platform, findNodeHandle } from "react-native";
  4. const { RNViewShot } = NativeModules;
  5. import type { Element, ElementRef, ElementType, Ref } from 'react';
  6. import type { ViewStyleProp } from 'StyleSheet';
  7. import type { LayoutEvent } from 'CoreEventTypes';
  8. const neverEndingPromise = new Promise(() => {});
  9. type Options = {
  10. width?: number,
  11. height?: number,
  12. format: "png" | "jpg" | "webm" | "raw",
  13. quality: number,
  14. result: "tmpfile" | "base64" | "data-uri" | "zip-base64",
  15. snapshotContentContainer: boolean
  16. };
  17. if (!RNViewShot) {
  18. console.warn(
  19. "NativeModules.RNViewShot is undefined. Make sure the library is linked on the native side."
  20. );
  21. }
  22. const acceptedFormats = ["png", "jpg"].concat(
  23. Platform.OS === "android" ? ["webm", "raw"] : []
  24. );
  25. const acceptedResults = ["tmpfile", "base64", "data-uri"].concat(
  26. Platform.OS === "android" ? ["zip-base64"] : []
  27. );
  28. const defaultOptions = {
  29. format: "png",
  30. quality: 1,
  31. result: "tmpfile",
  32. snapshotContentContainer: false
  33. };
  34. // validate and coerce options
  35. function validateOptions(
  36. options: ?Object
  37. ): { options: Options, errors: Array<string> } {
  38. options = {
  39. ...defaultOptions,
  40. ...options
  41. };
  42. const errors = [];
  43. if (
  44. "width" in options &&
  45. (typeof options.width !== "number" || options.width <= 0)
  46. ) {
  47. errors.push("option width should be a positive number");
  48. delete options.width;
  49. }
  50. if (
  51. "height" in options &&
  52. (typeof options.height !== "number" || options.height <= 0)
  53. ) {
  54. errors.push("option height should be a positive number");
  55. delete options.height;
  56. }
  57. if (
  58. typeof options.quality !== "number" ||
  59. options.quality < 0 ||
  60. options.quality > 1
  61. ) {
  62. errors.push("option quality should be a number between 0.0 and 1.0");
  63. options.quality = defaultOptions.quality;
  64. }
  65. if (typeof options.snapshotContentContainer !== "boolean") {
  66. errors.push("option snapshotContentContainer should be a boolean");
  67. }
  68. if (acceptedFormats.indexOf(options.format) === -1) {
  69. options.format = defaultOptions.format;
  70. errors.push(
  71. "option format '" + options.format + "' is not in valid formats: " + acceptedFormats.join(" | ")
  72. );
  73. }
  74. if (acceptedResults.indexOf(options.result) === -1) {
  75. options.result = defaultOptions.result;
  76. errors.push(
  77. "option result '" + options.result + "' is not in valid formats: " + acceptedResults.join(" | ")
  78. );
  79. }
  80. return { options, errors };
  81. }
  82. export function captureRef<T: ElementType>(
  83. view: number | ?View | Ref<T>,
  84. optionsObject?: Object
  85. ): Promise<string> {
  86. if (view && typeof view === "object" && "current" in view && view.current) { // React.RefObject
  87. view = view.current;
  88. }
  89. if (typeof view !== "number") {
  90. const node = findNodeHandle(view);
  91. if (!node)
  92. return Promise.reject(
  93. new Error("findNodeHandle failed to resolve view=" + String(view))
  94. );
  95. view = node;
  96. }
  97. const { options, errors } = validateOptions(optionsObject);
  98. if (__DEV__ && errors.length > 0) {
  99. console.warn(
  100. "react-native-view-shot: bad options:\n" +
  101. errors.map(e => `- ${e}`).join("\n")
  102. );
  103. }
  104. return RNViewShot.captureRef(view, options);
  105. }
  106. export function releaseCapture(uri: string): void {
  107. if (typeof uri !== "string") {
  108. if (__DEV__) {
  109. console.warn("Invalid argument to releaseCapture. Got: " + uri);
  110. }
  111. } else {
  112. RNViewShot.releaseCapture(uri);
  113. }
  114. }
  115. export function captureScreen(
  116. optionsObject?: Options
  117. ): Promise<string> {
  118. const { options, errors } = validateOptions(optionsObject);
  119. if (__DEV__ && errors.length > 0) {
  120. console.warn(
  121. "react-native-view-shot: bad options:\n" +
  122. errors.map(e => `- ${e}`).join("\n")
  123. );
  124. }
  125. return RNViewShot.captureScreen(options);
  126. }
  127. type Props = {
  128. options?: Object,
  129. captureMode?: "mount" | "continuous" | "update",
  130. children: Element<*>,
  131. onLayout?: (e: *) => void,
  132. onCapture?: (uri: string) => void,
  133. onCaptureFailure?: (e: Error) => void,
  134. style?: ViewStyleProp
  135. };
  136. function checkCompatibleProps(props: Props) {
  137. if (!props.captureMode && props.onCapture) {
  138. console.warn(
  139. "react-native-view-shot: a captureMode prop must be provided for `onCapture`"
  140. );
  141. } else if (props.captureMode && !props.onCapture) {
  142. console.warn(
  143. "react-native-view-shot: captureMode prop is defined but onCapture prop callback is missing"
  144. );
  145. } else if (
  146. (props.captureMode === "continuous" || props.captureMode === "update") &&
  147. props.options &&
  148. props.options.result &&
  149. props.options.result !== "tmpfile"
  150. ) {
  151. console.warn(
  152. "react-native-view-shot: result=tmpfile is recommended for captureMode=" +
  153. props.captureMode
  154. );
  155. }
  156. }
  157. export default class ViewShot extends Component<Props> {
  158. static captureRef = captureRef;
  159. static releaseCapture = releaseCapture;
  160. root: ?View;
  161. _raf: *;
  162. lastCapturedURI: ?string;
  163. resolveFirstLayout: (layout: Object) => void;
  164. firstLayoutPromise = new Promise<void>(resolve => {
  165. this.resolveFirstLayout = resolve;
  166. });
  167. capture = (): Promise<string> =>
  168. this.firstLayoutPromise
  169. .then(() => {
  170. const { root } = this;
  171. if (!root) return neverEndingPromise; // component is unmounted, you never want to hear back from the promise
  172. return captureRef(root, this.props.options);
  173. })
  174. .then(
  175. (uri: string) => {
  176. this.onCapture(uri);
  177. return uri;
  178. },
  179. (e: Error) => {
  180. this.onCaptureFailure(e);
  181. throw e;
  182. }
  183. );
  184. onCapture = (uri: string) => {
  185. if (!this.root) return;
  186. if (this.lastCapturedURI) {
  187. // schedule releasing the previous capture
  188. setTimeout(releaseCapture, 500, this.lastCapturedURI);
  189. }
  190. this.lastCapturedURI = uri;
  191. const { onCapture } = this.props;
  192. if (onCapture) onCapture(uri);
  193. };
  194. onCaptureFailure = (e: Error) => {
  195. if (!this.root) return;
  196. const { onCaptureFailure } = this.props;
  197. if (onCaptureFailure) onCaptureFailure(e);
  198. };
  199. syncCaptureLoop = (captureMode: ?string) => {
  200. cancelAnimationFrame(this._raf);
  201. if (captureMode === "continuous") {
  202. let previousCaptureURI = "-"; // needs to capture at least once at first, so we use "-" arbitrary string
  203. const loop = () => {
  204. this._raf = requestAnimationFrame(loop);
  205. if (previousCaptureURI === this.lastCapturedURI) return; // previous capture has not finished, don't capture yet
  206. previousCaptureURI = this.lastCapturedURI;
  207. this.capture();
  208. };
  209. this._raf = requestAnimationFrame(loop);
  210. }
  211. };
  212. onRef = (ref: ElementRef<*>) => {
  213. this.root = ref;
  214. };
  215. onLayout = (e: LayoutEvent) => {
  216. const { onLayout } = this.props;
  217. this.resolveFirstLayout(e.nativeEvent.layout);
  218. if (onLayout) onLayout(e);
  219. };
  220. componentDidMount() {
  221. if (__DEV__) checkCompatibleProps(this.props);
  222. if (this.props.captureMode === "mount") {
  223. this.capture();
  224. } else {
  225. this.syncCaptureLoop(this.props.captureMode);
  226. }
  227. }
  228. componentWillReceiveProps(nextProps: Props) {
  229. if (nextProps.captureMode !== this.props.captureMode) {
  230. this.syncCaptureLoop(nextProps.captureMode);
  231. }
  232. }
  233. componentDidUpdate() {
  234. if (this.props.captureMode === "update") {
  235. this.capture();
  236. }
  237. }
  238. componentWillUnmount() {
  239. this.syncCaptureLoop(null);
  240. }
  241. render() {
  242. const { children } = this.props;
  243. return (
  244. <View ref={this.onRef} collapsable={false} onLayout={this.onLayout} style={this.props.style}>
  245. {children}
  246. </View>
  247. );
  248. }
  249. }