├── .babelrc ├── .editorconfig ├── .eslintrc ├── .flowconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── demo └── react-native-loading-placeholder.gif ├── package-lock.json ├── package.json └── src ├── Placeholder.js ├── PlaceholderContainer.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # we recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true 8 | }, 9 | 10 | "parserOptions": { 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true 14 | } 15 | }, 16 | 17 | "extends": [ 18 | "prettier", 19 | "prettier/flowtype", 20 | "prettier/react" 21 | ], 22 | 23 | "plugins": [ 24 | "babel", 25 | "flowtype", 26 | "import", 27 | "react", 28 | "react-native", 29 | "prettier", 30 | ], 31 | 32 | "rules": { 33 | "constructor-super": "error", 34 | "no-case-declarations": "error", 35 | "no-class-assign": "error", 36 | "no-cond-assign": "error", 37 | "no-const-assign": "error", 38 | "no-constant-condition": "error", 39 | "no-control-regex": "error", 40 | "no-delete-var": "error", 41 | "no-dupe-args": "error", 42 | "no-dupe-class-members": "error", 43 | "no-dupe-keys": "error", 44 | "no-duplicate-case": "error", 45 | "no-empty": "error", 46 | "no-empty-character-class": "error", 47 | "no-empty-pattern": "error", 48 | "no-ex-assign": "error", 49 | "no-extra-boolean-cast": "error", 50 | "no-extra-semi": "error", 51 | "no-fallthrough": "error", 52 | "no-func-assign": "error", 53 | "no-global-assign": "error", 54 | "no-inner-declarations": "error", 55 | "no-invalid-regexp": "error", 56 | "no-new-symbol": "error", 57 | "no-obj-calls": "error", 58 | "no-octal": "error", 59 | "no-redeclare": "error", 60 | "no-regex-spaces": "error", 61 | "no-self-assign": "error", 62 | "no-sparse-arrays": "error", 63 | "no-this-before-super": "error", 64 | "no-undef": "error", 65 | "no-unexpected-multiline": "error", 66 | "no-unreachable": "error", 67 | "no-unsafe-finally": "error", 68 | "no-unsafe-negation": "error", 69 | "no-unused-labels": "error", 70 | "no-unused-vars": "error", 71 | "require-yield": "error", 72 | "use-isnan": "error", 73 | "valid-typeof": "error", 74 | 75 | "babel/new-cap": "off", 76 | "babel/object-curly-spacing": "off", 77 | "babel/arrow-parens": "off", 78 | 79 | "flowtype/boolean-style": ["error", "boolean"], 80 | "flowtype/define-flow-type": "error", 81 | "flowtype/no-dupe-keys": "error", 82 | "flowtype/no-primitive-constructor-types": "error", 83 | "flowtype/no-weak-types": "off", 84 | "flowtype/require-parameter-type": "off", 85 | "flowtype/require-return-type": "off", 86 | "flowtype/require-valid-file-annotation": "error", 87 | "flowtype/require-variable-type": "off", 88 | "flowtype/sort-keys": "off", 89 | "flowtype/type-id-match": "off", 90 | "flowtype/use-flow-type": "error", 91 | "flowtype/valid-syntax": "error", 92 | 93 | "import/no-unresolved": "error", 94 | "import/named": "error", 95 | "import/default": "off", 96 | "import/namespace": "off", 97 | "import/export": "error", 98 | "import/no-named-as-default": "off", 99 | "import/no-named-as-default-member": "off", 100 | "import/no-deprecated": "off", 101 | "import/no-extraneous-dependencies": "off", 102 | "import/no-commonjs": "error", 103 | "import/no-amd": "error", 104 | "import/no-nodejs-modules": "off", 105 | "import/imports-first": "error", 106 | "import/no-duplicates": "error", 107 | "import/no-namespace": "off", 108 | "import/extensions": ["error", { "js": "never", "json": "always" }], 109 | "import/order": "off", 110 | 111 | "react/display-name": "off", 112 | "react/forbid-prop-types": "off", 113 | "react/no-danger": "error", 114 | "react/no-deprecated": "error", 115 | "react/no-did-mount-set-state": "error", 116 | "react/no-did-update-set-state": "error", 117 | "react/no-direct-mutation-state": "error", 118 | "react/no-is-mounted": "error", 119 | "react/no-multi-comp": "off", 120 | "react/no-set-state": "off", 121 | "react/no-string-refs": "error", 122 | "react/no-unknown-property": "error", 123 | "react/prefer-es6-class": "error", 124 | "react/prop-types": "error", 125 | "react/react-in-jsx-scope": "error", 126 | "react/require-render-return": "error", 127 | "react/self-closing-comp": "error", 128 | "react/sort-comp": "error", 129 | "react/sort-prop-types": "off", 130 | "react/jsx-boolean-value": ["error", "never"], 131 | "react/jsx-handler-names": "off", 132 | "react/jsx-key": "error", 133 | "react/jsx-no-bind": "off", 134 | "react/jsx-no-duplicate-props": "error", 135 | "react/jsx-no-literals": "off", 136 | "react/jsx-no-undef": "error", 137 | "react/jsx-pascal-case": "error", 138 | "react/jsx-sort-props": "off", 139 | "react/jsx-uses-react": "error", 140 | "react/jsx-uses-vars": "error", 141 | 142 | "react-native/no-unused-styles": "error", 143 | "react-native/split-platform-components": "off", 144 | 145 | "prettier/prettier": ["error", {"trailingComma": "all", "singleQuote": true}], 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore templates for 'react-native init' 6 | .*/local-cli/templates/.* 7 | 8 | ; Ignore the website subdir 9 | /website/.* 10 | 11 | ; Ignore "BUCK" generated dirs 12 | /\.buckd/ 13 | 14 | ; Ignore unexpected extra "@providesModule" 15 | .*/node_modules/.*/node_modules/fbjs/.* 16 | +.*/node_modules/react-dom/.* 17 | 18 | ; Ignore duplicate module providers 19 | ; For RN Apps installed via npm, "Libraries" folder is inside 20 | ; "node_modules/react-native" but in the source repo it is in the root 21 | .*/Libraries/react-native/React.js 22 | .*/Libraries/react-native/ReactNative.js 23 | 24 | ; Ignore duplicate modules under example/ 25 | .*/example/node_modules/fbjs 26 | .*/example/node_modules/fbemitter 27 | .*/example/node_modules/react 28 | .*/example/node_modules/react-native 29 | .*/example/node_modules/expo 30 | .*/example/\.buckd/ 31 | 32 | [include] 33 | 34 | [libs] 35 | node_modules/react-native/Libraries/react-native/react-native-interface.js 36 | node_modules/react-native/flow 37 | 38 | [options] 39 | emoji=true 40 | 41 | module.system=haste 42 | 43 | experimental.strict_type_args=true 44 | 45 | munge_underscores=true 46 | 47 | module.name_mapper='^expo$' -> 'emptyObject' 48 | 49 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 50 | 51 | suppress_type=$FlowIssue 52 | suppress_type=$FlowFixMe 53 | suppress_type=$FixMe 54 | 55 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(4[0-0]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\) 56 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(4[0-0]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\)?:? #[0-9]+ 57 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 58 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 59 | 60 | unsafe.enable_getters_and_setters=true 61 | 62 | [version] 63 | ^0.38.0 64 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | tsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .idea 35 | .gradle 36 | local.properties 37 | 38 | # node.js 39 | # 40 | node_modules/ 41 | npm-debug.log 42 | 43 | # BUCK 44 | buck-out/ 45 | \.buckd/ 46 | android/app/libs 47 | android/keystores/debug.keystore 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Zeljko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Loading Placeholder [![npm version](https://img.shields.io/npm/v/react-native-loading-placeholder.svg?style=flat)](https://www.npmjs.com/package/react-native-loading-placeholder) 2 | 3 | A customizable loading placeholder component for React Native. 4 | 5 | ## Features 6 | 7 | - Highly customizable design 8 | - Async feature to resolve whole PlaceholderContainer content or just Placeholder elements. 9 | 10 | 11 | ## Demo 12 | 13 | 14 | 15 | 16 | ## Installation 17 | 18 | ```sh 19 | npm install react-native-loading-placeholder 20 | ``` 21 | 22 | 23 | ## Example 24 | 25 | ```js 26 | 27 | import React, { Component } from 'react'; 28 | import { AppRegistry, StyleSheet, Text, View } from 'react-native'; 29 | import { 30 | PlaceholderContainer, 31 | Placeholder 32 | } from 'react-native-loading-placeholder'; 33 | import LinearGradient from 'react-native-linear-gradient'; 34 | 35 | export default class Test extends Component { 36 | loadingComponent: Promise>; 37 | loadingComponent1: Promise<*>; 38 | constructor(props) { 39 | super(props); 40 | } 41 | componentWillMount(): void { 42 | this.loadingComponent = new Promise(resolve => { 43 | setTimeout(() => { 44 | resolve( 45 | 48 | Resolved 49 | 50 | ); 51 | }, 6000); 52 | }); 53 | this.loadingComponent1 = new Promise(resolve => { 54 | setTimeout(() => { 55 | resolve(); 56 | }, 8000); 57 | }); 58 | } 59 | render() { 60 | return ( 61 | 62 | 63 | 64 | 65 | ); 66 | } 67 | } 68 | 69 | const Gradient = (): React.Element<*> => { 70 | return ( 71 | 80 | ); 81 | }; 82 | 83 | const PlaceholderExample = ({ 84 | loader 85 | }: { 86 | loader: Promise<*> 87 | }): React.Element<*> => { 88 | return ( 89 | } 92 | duration={1000} 93 | delay={1000} 94 | loader={loader} 95 | > 96 | 97 | 98 | 106 | 115 | 124 | 125 | 126 | 127 | 130 | 131 | 132 | 133 | ); 134 | }; 135 | 136 | const PlaceholderExample1 = ({ 137 | loader 138 | }: { 139 | loader: Promise<*> 140 | }): React.Element<*> => { 141 | return ( 142 | } 145 | duration={1000} 146 | delay={1000} 147 | loader={loader} 148 | replace={true} 149 | > 150 | 151 | 152 | Name 153 | 162 | John Doe 163 | 164 | 165 | 166 | 167 | 168 | 169 | Age 170 | 179 | 47 180 | 181 | 182 | 183 | 184 | 185 | ); 186 | }; 187 | 188 | const styles = StyleSheet.create({ 189 | container: { 190 | flex: 1, 191 | alignItems: 'center', 192 | paddingTop: 25, 193 | backgroundColor: '#f6f7f8' 194 | }, 195 | placeholderContainer: { 196 | width: '90%', 197 | backgroundColor: '#fff', 198 | height: 200 199 | }, 200 | placeholder: { 201 | height: 8, 202 | marginTop: 6, 203 | marginLeft: 15, 204 | alignSelf: 'flex-start', 205 | justifyContent: 'center', 206 | backgroundColor: '#eeeeee' 207 | }, 208 | row: { 209 | flexDirection: 'row', 210 | width: '100%' 211 | } 212 | }); 213 | ``` 214 | 215 | 216 | ## API 217 | 218 | The package exposes the following components, 219 | 220 | ### `` 221 | 222 | Container component responsible for orchestrating animations in placeholder components. 223 | 224 | #### Props 225 | 226 | - `duration` - Animated timing 'speed' 227 | - `delay` - Delay before starting next placeholder animation 228 | - `style` - Container style, 229 | - `animatedComponent` - Animated component (example: gradient component) 230 | - `loader` - Promise that resolves to React Component that is going to be displayed instead placeholders. Note: If replace props is set to true loader just need to resolve. 231 | - `replace` - Flag to indicate if placeholder elements are going to be replaced with its child elements on loader status resolved 232 | 233 | 234 | ### `` 235 | 236 | Component that displays animated component 237 | 238 | #### Props 239 | 240 | - `style` - Object 241 | -------------------------------------------------------------------------------- /demo/react-native-loading-placeholder.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeljkoX/react-native-loading-placeholder/8440af62311199f413eb0c6ce7574dee7d49f4c6/demo/react-native-loading-placeholder.gif -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-loading-placeholder", 3 | "version": "0.0.5", 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "asap": { 7 | "version": "2.0.5", 8 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz", 9 | "integrity": "sha1-UidltQw1EEkOUtfc/ghe+bqWlY8=" 10 | }, 11 | "core-js": { 12 | "version": "1.2.7", 13 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", 14 | "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" 15 | }, 16 | "encoding": { 17 | "version": "0.1.12", 18 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", 19 | "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=" 20 | }, 21 | "fbjs": { 22 | "version": "0.8.12", 23 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz", 24 | "integrity": "sha1-ELXZL3bUVXX9Y6IX1OoCvqL47QQ=" 25 | }, 26 | "iconv-lite": { 27 | "version": "0.4.17", 28 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.17.tgz", 29 | "integrity": "sha1-T9qjs4rLwsAxsEXQ7c3+HsqxjI0=" 30 | }, 31 | "is-stream": { 32 | "version": "1.1.0", 33 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 34 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 35 | }, 36 | "isomorphic-fetch": { 37 | "version": "2.2.1", 38 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", 39 | "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=" 40 | }, 41 | "js-tokens": { 42 | "version": "3.0.1", 43 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz", 44 | "integrity": "sha1-COnxMkhKLEWjCQfp3E1VZ7fxFNc=" 45 | }, 46 | "loose-envify": { 47 | "version": "1.3.1", 48 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", 49 | "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=" 50 | }, 51 | "node-fetch": { 52 | "version": "1.7.0", 53 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.0.tgz", 54 | "integrity": "sha1-P/bFZUT5t/sAaCM4u1Xub1SooO8=" 55 | }, 56 | "object-assign": { 57 | "version": "4.1.1", 58 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 59 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 60 | }, 61 | "promise": { 62 | "version": "7.1.1", 63 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz", 64 | "integrity": "sha1-SJZUxpJha4qlWwck+oCbt9tJxb8=" 65 | }, 66 | "prop-types": { 67 | "version": "15.5.10", 68 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", 69 | "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=" 70 | }, 71 | "setimmediate": { 72 | "version": "1.0.5", 73 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", 74 | "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" 75 | }, 76 | "ua-parser-js": { 77 | "version": "0.7.12", 78 | "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.12.tgz", 79 | "integrity": "sha1-BMgamb3V3FImPqKdJMa/jUgYpLs=" 80 | }, 81 | "whatwg-fetch": { 82 | "version": "2.0.3", 83 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", 84 | "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-loading-placeholder", 3 | "version": "0.0.6", 4 | "description": "Placeholder component for React Native", 5 | "main": "src/index.js", 6 | "keywords": ["react-native", "react-component", "react-native", "ios", "android", "placeholder", "loading"], 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/zeljkoX/react-native-loading-placeholder.git" 10 | }, 11 | "author": "Zeljko Markovic (https://github.com/zeljkoX/)", 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/zeljkoX/react-native-loading-placeholder/issues" 15 | }, 16 | "homepage": "https://github.com/zeljkoX/react-native-loading-placeholder#readme", 17 | "dependencies": { 18 | "prop-types": "^15.5.10" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Placeholder.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @flow 3 | */ 4 | 5 | import React, { Component } from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import { 8 | View, 9 | StyleSheet, 10 | Animated, 11 | } from 'react-native'; 12 | 13 | type PlaceholderProps = { 14 | style: Object, 15 | animatedComponent: React.Element<*> 16 | }; 17 | 18 | type PlaceholderState = { 19 | x: number, 20 | width: number, 21 | isMeasured: boolean, 22 | resolved: boolean 23 | }; 24 | 25 | export default class Placeholder extends Component { 26 | state: PlaceholderState; 27 | props: PlaceholderProps; 28 | constructor(props: PlaceholderProps) { 29 | super(props); 30 | this.state = { 31 | x: 0, 32 | width: 0, 33 | isMeasured: false, 34 | resolved: false 35 | }; 36 | } 37 | 38 | componentDidMount(): void { 39 | this.context.registerPlaceholder(this); 40 | } 41 | 42 | render(): React.Element<*> { 43 | const { style, children } = this.props; 44 | const { x, isMeasured, resolved } = this.state; 45 | const { animatedComponent, position } = this.context; 46 | 47 | if (resolved) { 48 | return children 49 | } 50 | const animatedStyle = { 51 | height: '100%', 52 | width: '100%', 53 | transform: [{ translateX: position }], 54 | left: -x 55 | }; 56 | return ( 57 | { 59 | this.testRef = ref; 60 | }} 61 | onLayout={this._setDimensions} 62 | style={[style, styles.overflow]} 63 | > 64 | {isMeasured && 65 | 66 | {animatedComponent} 67 | } 68 | 69 | ); 70 | } 71 | _resolve = () => { 72 | this.setState(() => ({ resolved: true })); 73 | }; 74 | 75 | _setDimensions = (event): void => { 76 | const { x } = event.nativeEvent.layout; 77 | this.setState(() => ({ x, isMeasured: true })); 78 | }; 79 | } 80 | 81 | Placeholder.contextTypes = { 82 | position: PropTypes.object.isRequired, 83 | animatedComponent: PropTypes.object.isRequired, 84 | registerPlaceholder: PropTypes.func.isRequired 85 | } 86 | 87 | 88 | const styles = StyleSheet.create({ 89 | overflow: { 90 | overflow: 'hidden' 91 | } 92 | }); 93 | -------------------------------------------------------------------------------- /src/PlaceholderContainer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @flow 3 | */ 4 | 5 | import React, { Component } from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import { 8 | Text, 9 | View, 10 | StyleSheet, 11 | Animated, 12 | Dimensions, 13 | Platform 14 | } from 'react-native'; 15 | 16 | const screenWidth = Dimensions.get('window').width; 17 | 18 | type PlaceholderContainerProps = { 19 | duration: number, 20 | delay: number, 21 | style: Object, 22 | animatedComponent: React.Element<*>, 23 | loader: Function, 24 | replace: boolean 25 | }; 26 | 27 | type PlaceholderContainerState = { 28 | containerComponent: Object, 29 | animatedComponent: Object, 30 | startPosition: number, 31 | stopPosition: number, 32 | isContainerMeasured: boolean, 33 | isAnimatedComponentMeasured: boolean, 34 | Component: React.Element<*> 35 | }; 36 | 37 | export default class PlaceholderContainer extends Component { 38 | props: PlaceholderContainerProps; 39 | state: PlaceholderContainerState; 40 | position: Animated.Value; 41 | placeholders: Array>; 42 | constructor(props: PlaceholderContainerProps) { 43 | super(props); 44 | this.state = { 45 | ContainerComponent: { x: 0, y: 0, width: 0, height: 0 }, 46 | AnimatedComponent: { x: 0, y: 0, width: 0, height: 0 }, 47 | startPosition: 0, 48 | stopPosition: 0, 49 | isContainerComponentMeasured: false, 50 | isAnimatedComponentMeasured: false, 51 | Component: null 52 | }; 53 | this.position = new Animated.Value(0); 54 | this.placeholders = []; 55 | this._measureContainerComponent = this._measureView.bind( 56 | null, 57 | 'ContainerComponent' 58 | ); 59 | this._measureAnimatedComponent = this._measureView.bind( 60 | null, 61 | 'AnimatedComponent' 62 | ); 63 | } 64 | getChildContext(): Object { 65 | return { 66 | position: this.position, 67 | animatedComponent: this.props.animatedComponent, 68 | registerPlaceholder: this._registerPlaceholder 69 | }; 70 | } 71 | componentDidMount(): void { 72 | const { loader, replace } = this.props; 73 | loader && 74 | Promise.resolve(loader).then(Component => { 75 | !replace ? this.setState({ Component }) : this._replacePlaceholders(); 76 | }); 77 | } 78 | 79 | componentWillUnmount(): void { 80 | this.position.stopAnimation(); 81 | } 82 | 83 | render(): React.Element<*> { 84 | const { style, elements, animatedComponent, children } = this.props; 85 | const { Component, isAnimatedComponentMeasured } = this.state; 86 | return ( 87 | { 89 | this.containerRef = ref; 90 | }} 91 | onLayout={this._measureContainerComponent} 92 | style={style} 93 | > 94 | {!isAnimatedComponentMeasured && 95 | { 97 | this.componentRef = ref; 98 | }} 99 | onLayout={this._measureAnimatedComponent} 100 | style={styles.offscreen} 101 | > 102 | {animatedComponent} 103 | } 104 | {(Component && Component) || children} 105 | 106 | ); 107 | } 108 | 109 | _triggerAnimation = (): void => { 110 | const { duration, delay } = this.props; 111 | const { startPosition, stopPosition } = this.state; 112 | Animated.loop(Animated.sequence([ 113 | Animated.timing(this.position, { 114 | toValue: stopPosition || screenWidth, 115 | duration: duration, 116 | useNativeDriver: true, 117 | isInteraction: false 118 | }), 119 | Animated.timing(this.position, { 120 | toValue: startPosition || 0, 121 | duration: 0, 122 | delay: delay || 0, 123 | useNativeDriver: true, 124 | isInteraction: false 125 | }) 126 | ])).start(); 127 | }; 128 | 129 | _startAndRepeat = (): void => { 130 | const { Component } = this.state; 131 | !Component && this._triggerAnimation(); 132 | }; 133 | 134 | _measureView = (viewName: string, event: Object): void => { 135 | const { x, y, height, width } = event.nativeEvent.layout; 136 | this.setState( 137 | () => ({ 138 | [viewName]: { 139 | x, 140 | y, 141 | height, 142 | width 143 | }, 144 | [`is${viewName}Measured`]: true 145 | }), 146 | () => { 147 | this._setAnimationPositions(); 148 | } 149 | ); 150 | }; 151 | 152 | _setAnimationPositions = (): void => { 153 | const { 154 | ContainerComponent, 155 | AnimatedComponent, 156 | isContainerComponentMeasured, 157 | isAnimatedComponentMeasured 158 | } = this.state; 159 | if (!isContainerComponentMeasured || !isAnimatedComponentMeasured) { 160 | return; 161 | } 162 | const startPosition = -(ContainerComponent.x + AnimatedComponent.width); 163 | const stopPosition = 164 | ContainerComponent.x + ContainerComponent.width + AnimatedComponent.width; 165 | this.setState( 166 | () => ({ 167 | startPosition, 168 | stopPosition 169 | }), 170 | () => { 171 | this.position.setValue(startPosition); 172 | this._startAndRepeat(); 173 | } 174 | ); 175 | }; 176 | 177 | _registerPlaceholder = (placeholder: React.Element<*>): void => { 178 | const { replace } = this.props; 179 | if (!replace) { 180 | return; 181 | } 182 | this.placeholders.push(placeholder); 183 | }; 184 | 185 | _replacePlaceholders = (): void => { 186 | try { 187 | this.placeholders.forEach(placeholder => { 188 | placeholder._resolve(); 189 | }); 190 | } catch (e) { 191 | console.log('Something went wrong'); 192 | } 193 | }; 194 | } 195 | 196 | PlaceholderContainer.childContextTypes = { 197 | position: PropTypes.object.isRequired, 198 | animatedComponent: PropTypes.object.isRequired, 199 | registerPlaceholder: PropTypes.func.isRequired 200 | }; 201 | 202 | const styles = StyleSheet.create({ 203 | offscreen: { 204 | position: 'absolute', 205 | left: -1000 206 | } 207 | }); 208 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint-disable import/no-commonjs */ 3 | 4 | module.exports = { 5 | get PlaceholderContainer() { 6 | return require('./PlaceholderContainer').default; 7 | }, 8 | get Placeholder() { 9 | return require('./Placeholder').default; 10 | } 11 | }; 12 | --------------------------------------------------------------------------------