No Description

index.android.js 9.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. 'use strict';
  2. import React, { PureComponent } from 'react';
  3. import {
  4. findNodeHandle,
  5. requireNativeComponent,
  6. Animated,
  7. DeviceEventEmitter,
  8. Easing,
  9. StyleSheet,
  10. Platform,
  11. UIManager,
  12. ViewPropTypes,
  13. WebView
  14. } from 'react-native';
  15. import PropTypes from 'prop-types';
  16. import {
  17. isEqual,
  18. setState,
  19. isSizeChanged,
  20. handleSizeUpdated,
  21. getWidth,
  22. getScript,
  23. domMutationObserveScript,
  24. getCurrentSize,
  25. getRenderSize
  26. } from './common.js';
  27. const RCTAutoHeightWebView = requireNativeComponent('RCTAutoHeightWebView', AutoHeightWebView, {
  28. nativeOnly: {
  29. nativeOnly: {
  30. onLoadingStart: true,
  31. onLoadingError: true,
  32. onLoadingFinish: true,
  33. messagingEnabled: PropTypes.bool
  34. }
  35. }
  36. });
  37. import momoize from './momoize';
  38. const baseUrl = 'file:///android_asset/web/';
  39. const getUpdatedState = momoize(setState, isEqual);
  40. export default class AutoHeightWebView extends PureComponent {
  41. static propTypes = {
  42. onNavigationStateChange: PropTypes.func,
  43. onMessage: PropTypes.func,
  44. scrollEnabled: PropTypes.bool,
  45. source: WebView.propTypes.source,
  46. customScript: PropTypes.string,
  47. customStyle: PropTypes.string,
  48. enableAnimation: PropTypes.bool,
  49. // either height or width updated will trigger this
  50. onSizeUpdated: PropTypes.func,
  51. // if set to false may cause some layout issues (width of container will be than width of screen)
  52. scalesPageToFit: PropTypes.bool,
  53. // only works on enable animation
  54. animationDuration: PropTypes.number,
  55. // only on android
  56. animationEasing: PropTypes.func,
  57. // offset of rn webView margin
  58. heightOffset: PropTypes.number,
  59. // baseUrl not work in android 4.3 or below version
  60. enableBaseUrl: PropTypes.bool,
  61. style: ViewPropTypes.style,
  62. // rn WebView callback
  63. onError: PropTypes.func,
  64. onLoad: PropTypes.func,
  65. onLoadStart: PropTypes.func,
  66. onLoadEnd: PropTypes.func,
  67. // works if set enableBaseUrl to true; add web/files... to android/app/src/assets/
  68. files: PropTypes.arrayOf(
  69. PropTypes.shape({
  70. href: PropTypes.string,
  71. type: PropTypes.string,
  72. rel: PropTypes.string
  73. })
  74. )
  75. };
  76. static defaultProps = {
  77. scalesPageToFit: true,
  78. enableBaseUrl: false,
  79. enableAnimation: true,
  80. animationDuration: 255,
  81. heightOffset: 20,
  82. animationEasing: Easing.out(Easing.quad)
  83. };
  84. constructor(props) {
  85. super(props);
  86. const { enableAnimation, style, source, enableBaseUrl, heightOffset } = props;
  87. isBelowKitKat && DeviceEventEmitter.addListener('webViewBridgeMessage', this.listenWebViewBridgeMessage);
  88. this.finishInterval = true;
  89. const initWidth = getWidth(style);
  90. const initHeight = style ? (style.height ? style.height : 0) : 0;
  91. let state = {
  92. isSizeChanged: false,
  93. isSizeMayChange: false,
  94. height: initHeight,
  95. width: initWidth,
  96. script: getScript(props, getBaseScript),
  97. source: enableBaseUrl ? Object.assign({}, source, { baseUrl }) : source
  98. };
  99. if (enableAnimation) {
  100. Object.assign(state, {
  101. heightValue: new Animated.Value(initHeight ? initHeight + heightOffset : 0),
  102. widthValue: new Animated.Value(initWidth)
  103. });
  104. }
  105. this.state = state;
  106. }
  107. componentDidMount() {
  108. this.startInterval();
  109. }
  110. static getDerivedStateFromProps(props, state) {
  111. const { height: oldHeight, width: oldWidth, source: prevSource, script: prevScript } = state;
  112. const { style, enableBaseUrl } = props;
  113. const { source, script } = getUpdatedState(props, enableBaseUrl ? baseUrl : null, getBaseScript);
  114. const height = style ? style.height : null;
  115. const width = style ? style.width : null;
  116. if (source.html !== prevSource.html || source.uri !== prevSource.uri || script !== prevScript) {
  117. return {
  118. source,
  119. script,
  120. isSizeMayChange: true
  121. };
  122. }
  123. if (isSizeChanged(height, oldHeight, width, oldWidth)) {
  124. return {
  125. height,
  126. width,
  127. isSizeChanged: true
  128. };
  129. }
  130. return null;
  131. }
  132. componentDidUpdate() {
  133. const { height, width, isSizeChanged, isSizeMayChange, heightValue, widthValue } = this.state;
  134. if (isSizeMayChange) {
  135. this.startInterval();
  136. this.setState({ isSizeMayChange: false });
  137. }
  138. if (isSizeChanged) {
  139. const { enableAnimation, animationDuration, animationEasing, onSizeUpdated, heightOffset } = this.props;
  140. if (enableAnimation) {
  141. Animated.parallel([
  142. Animated.timing(heightValue, {
  143. toValue: height ? height + heightOffset : 0,
  144. easing: animationEasing,
  145. duration: animationDuration
  146. }),
  147. Animated.timing(widthValue, {
  148. toValue: width,
  149. easing: animationEasing,
  150. duration: animationDuration
  151. })
  152. ]).start(() => {
  153. handleSizeUpdated(height, width, onSizeUpdated);
  154. });
  155. } else {
  156. handleSizeUpdated(height, width, onSizeUpdated);
  157. }
  158. this.setState({ isSizeChanged: false });
  159. }
  160. }
  161. componentWillUnmount() {
  162. this.stopInterval();
  163. isBelowKitKat && DeviceEventEmitter.removeListener('webViewBridgeMessage', this.listenWebViewBridgeMessage);
  164. }
  165. // below kitkat
  166. listenWebViewBridgeMessage = body => this.onMessage(body.message);
  167. // below kitkat
  168. sendToWebView(message) {
  169. UIManager.dispatchViewManagerCommand(
  170. findNodeHandle(this.webView),
  171. UIManager.RCTAutoHeightWebView.Commands.sendToWebView,
  172. [String(message)]
  173. );
  174. }
  175. postMessage(data) {
  176. UIManager.dispatchViewManagerCommand(
  177. findNodeHandle(this.webView),
  178. UIManager.RCTAutoHeightWebView.Commands.postMessage,
  179. [String(data)]
  180. );
  181. }
  182. startInterval() {
  183. if (this.finishInterval === false) {
  184. return;
  185. }
  186. this.finishInterval = false;
  187. this.interval = setInterval(() => {
  188. if (!this.finishInterval) {
  189. isBelowKitKat ? this.sendToWebView('getBodyHeight') : this.postMessage('getBodyHeight');
  190. }
  191. }, 205);
  192. }
  193. stopInterval() {
  194. this.finishInterval = true;
  195. clearInterval(this.interval);
  196. }
  197. onMessage = e => {
  198. if (!e.nativeEvent) {
  199. return;
  200. }
  201. const { height, width } = JSON.parse(isBelowKitKat ? e.nativeEvent.message : e.nativeEvent.data);
  202. const { height: oldHeight, width: oldWidth } = this.state;
  203. if (isSizeChanged(height, oldHeight, width, oldWidth)) {
  204. this.stopInterval();
  205. this.setState({
  206. isSizeChanged: true,
  207. height,
  208. width
  209. });
  210. }
  211. const { onMessage } = this.props;
  212. onMessage && onMessage(e);
  213. };
  214. onLoadingStart = event => {
  215. const { onLoadStart, onNavigationStateChange } = this.props;
  216. onLoadStart && onLoadStart(event);
  217. onNavigationStateChange && onNavigationStateChange(event.nativeEvent);
  218. };
  219. onLoadingError = event => {
  220. const { onError, onLoadEnd } = this.props;
  221. onError && onError(event);
  222. onLoadEnd && onLoadEnd(event);
  223. console.warn('Encountered an error loading page', event.nativeEvent);
  224. };
  225. onLoadingFinish = event => {
  226. const { onLoad, onLoadEnd, onNavigationStateChange } = this.props;
  227. onLoad && onLoad(event);
  228. onLoadEnd && onLoadEnd(event);
  229. onNavigationStateChange && onNavigationStateChange(event.nativeEvent);
  230. };
  231. stopLoading() {
  232. UIManager.dispatchViewManagerCommand(
  233. findNodeHandle(this.webView),
  234. UIManager.RCTAutoHeightWebView.Commands.stopLoading,
  235. null
  236. );
  237. }
  238. getWebView = webView => (this.webView = webView);
  239. render() {
  240. const { height, width, script, source, heightValue, widthValue } = this.state;
  241. const { scalesPageToFit, style, scrollEnabled, heightOffset, enableAnimation } = this.props;
  242. return (
  243. <Animated.View
  244. style={[
  245. styles.container,
  246. getRenderSize(enableAnimation, height, width, heightOffset, heightValue, widthValue),
  247. style
  248. ]}
  249. >
  250. <RCTAutoHeightWebView
  251. onLoadingStart={this.onLoadingStart}
  252. onLoadingFinish={this.onLoadingFinish}
  253. onLoadingError={this.onLoadingError}
  254. originWhitelist={['.*']}
  255. ref={this.getWebView}
  256. style={styles.webView}
  257. javaScriptEnabled={true}
  258. injectedJavaScript={script}
  259. scalesPageToFit={scalesPageToFit}
  260. scrollEnabled={!!scrollEnabled}
  261. source={source}
  262. onMessage={this.onMessage}
  263. messagingEnabled={true}
  264. // below kitkat
  265. onChange={this.onMessage}
  266. />
  267. </Animated.View>
  268. );
  269. }
  270. }
  271. const isBelowKitKat = Platform.Version < 19;
  272. const styles = StyleSheet.create({
  273. container: {
  274. backgroundColor: 'transparent'
  275. },
  276. webView: {
  277. flex: 1,
  278. backgroundColor: 'transparent'
  279. }
  280. });
  281. const commonScript = `
  282. ${getCurrentSize}
  283. var wrapper = document.createElement("div");
  284. wrapper.id = "wrapper";
  285. while (document.body.firstChild instanceof Node) {
  286. wrapper.appendChild(document.body.firstChild);
  287. }
  288. document.body.appendChild(wrapper);
  289. var height = 0;
  290. `;
  291. const getBaseScript = isBelowKitKat
  292. ? function(style) {
  293. return `
  294. ;
  295. ${commonScript}
  296. var width = ${getWidth(style)};
  297. function updateSize() {
  298. var size = getSize(document.body.firstChild);
  299. height = size.height;
  300. width = size.width;
  301. AutoHeightWebView.send(JSON.stringify({ width, height }));
  302. }
  303. (function () {
  304. AutoHeightWebView.onMessage = updateSize;
  305. ${domMutationObserveScript}
  306. } ());
  307. `;
  308. }
  309. : function(style) {
  310. return `
  311. ;
  312. ${commonScript}
  313. var width = ${getWidth(style)};
  314. function updateSize() {
  315. var size = getSize(document.body.firstChild);
  316. height = size.height;
  317. width = size.width;
  318. window.postMessage(JSON.stringify({ width, height }), '*');
  319. }
  320. (function () {
  321. document.addEventListener("message", updateSize);
  322. ${domMutationObserveScript}
  323. } ());
  324. `;
  325. };