[
  {
    "path": ".eslintrc",
    "content": "{\n  \"parser\": \"babel-eslint\",\n  \"extends\": [\n    \"airbnb\",\n    \"plugin:flowtype/recommended\",\n    \"plugin:jest/recommended\",\n    \"prettier\",\n    \"prettier/react\"\n  ],\n  \"plugins\": [\n    \"flowtype\",\n    \"jest\",\n    \"prettier\"\n  ],\n  \"rules\": {\n    \"prettier/prettier\": \"error\",\n    \"react/jsx-filename-extension\": [\n      1,\n      {\n        \"extensions\": [\n          \".js\",\n          \".jsx\"\n        ]\n      }\n    ],\n    \"import/extensions\": \"off\",\n    \"import/no-unresolved\": \"off\",\n    \"react/require-default-props\": \"off\",\n    \"react/destructuring-assignment\": \"off\",\n    \"global-require\": \"off\",\n    \"no-restricted-properties\": \"off\",\n    \"no-unused-vars\": \"error\",\n    \"import/no-extraneous-dependencies\": \"error\",\n    \"flowtype/no-weak-types\": [\n      1,\n      {\n        \"any\": true,\n        \"Function\": true,\n        \"Object\": false\n      }\n    ]\n  },\n  \"env\": {\n    \"es6\": true\n  }\n}"
  },
  {
    "path": ".flowconfig",
    "content": "[ignore]\n; We fork some components by platform\n.*/*[.]android.js\n\n; Ignore \"BUCK\" generated dirs\n<PROJECT_ROOT>/\\.buckd/\n\n; Ignore unexpected extra \"@providesModule\"\n.*/node_modules/.*/node_modules/fbjs/.*\n\n; Ignore duplicate module providers\n; For RN Apps installed via npm, \"Libraries\" folder is inside\n; \"node_modules/react-native\" but in the source repo it is in the root\n.*/Libraries/react-native/React.js\n\n; Ignore polyfills\n.*/Libraries/polyfills/.*\n\n; Ignore metro\n.*/node_modules/metro/.*\n\n[include]\n\n[libs]\nnode_modules/react-native/Libraries/react-native/react-native-interface.js\nnode_modules/react-native/flow/\nnode_modules/react-native/flow-github/\n\n[options]\nemoji=true\n\nmodule.system=haste\n\nmunge_underscores=true\n\nmodule.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'\n\nmodule.file_ext=.js\nmodule.file_ext=.jsx\nmodule.file_ext=.json\nmodule.file_ext=.native.js\n\nsuppress_type=$FlowIssue\nsuppress_type=$FlowFixMe\nsuppress_type=$FlowFixMeProps\nsuppress_type=$FlowFixMeState\n\nsuppress_comment=\\\\(.\\\\|\\n\\\\)*\\\\$FlowFixMe\\\\($\\\\|[^(]\\\\|(\\\\(<VERSION>\\\\)? *\\\\(site=[a-z,_]*react_native[a-z,_]*\\\\)?)\\\\)\nsuppress_comment=\\\\(.\\\\|\\n\\\\)*\\\\$FlowIssue\\\\((\\\\(<VERSION>\\\\)? *\\\\(site=[a-z,_]*react_native[a-z,_]*\\\\)?)\\\\)?:? #[0-9]+\nsuppress_comment=\\\\(.\\\\|\\n\\\\)*\\\\$FlowFixedInNextDeploy\nsuppress_comment=\\\\(.\\\\|\\n\\\\)*\\\\$FlowExpectedError\n\n[version]\n^0.71.0"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\n.idea\n.expo"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"bracketSpacing\": false,\n  \"parser\": \"flow\",\n  \"printWidth\": 80,\n  \"singleQuote\": true,\n  \"tabWidth\": 4,\n  \"trailingComma\": \"es5\"\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Anton Kalinin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "## If you are using React Native >= 0.59.0 it's recommended to use similar package https://github.com/jobtoday/react-native-image-viewing as improved and better supported version!\n\n[![npm version](https://badge.fury.io/js/react-native-image-view.svg)](https://badge.fury.io/js/react-native-image-view)\n\nReact Native modal image view with pinch zoom and carousel.\n\nTry with expo: https://expo.io/@antonkalinin/react-native-image-view\n\n#### Warning: Breaking changes since v2.0.0:\n\n- instead of prop `source` => `images`\n- no title prop for footer, please use `renderFooter` instead\n\n## Installation\n\n```bash\nyarn add react-native-image-view\n```\n\nor\n\n```bash\nnpm install --save react-native-image-view\n```\n\n## Demo\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/antonKalinin/react-native-image-view/master/static/demoV2.gif\" height=\"400\" />\n</p>\n\n## Usage\n```jsx\nimport ImageView from 'react-native-image-view';\n\nconst images = [\n    {\n        source: {\n            uri: 'https://cdn.pixabay.com/photo/2017/08/17/10/47/paris-2650808_960_720.jpg',\n        },\n        title: 'Paris',\n        width: 806,\n        height: 720,\n    },\n];\n\n<ImageView\n    images={images}\n    imageIndex={0}\n    isVisible={this.state.isImageViewVisible}\n    renderFooter={(currentImage) => (<View><Text>My footer</Text></View>)}\n/>\n```\n\n#### [See example for better understanding](https://github.com/antonKalinin/react-native-image-view/blob/master/example/App.js)\n\n## Props\n\nProp name           | Description   | Type      | Default value | Platform |\n--------------------|---------------|-----------|---------------|----------|\n`animationType` | Type of animation modal presented with | \"none\", \"fade\", \"slide\" | \"none\" |\n`backgroundColor` | Background color of the modal in HEX (#0099CC) | string | null |\n`controls` | Config of available controls (see below) | Object | {close: true} |\n`glideAlways`  | Emulates ScrollView glide animation if built-in was not triggered  | boolean | false | Android\n`glideAlwaysDelay`  | Defines delay in milliseconds for glideAlways  | number | 75 | Android\n`images` | Array of images to display, see below image item description | array | [] |\n`imageIndex` | Current index of image to display | number | 0 |\n`isVisible` | Is modal shown or not | boolean | false |\n`isTapZoomEnabled` | Zoom image when double tapped | boolean | true |\n`isPinchZoomEnabled` | Zoom image with pinch gesture | boolean | true |\n`isSwipeCloseEnabled` | Close modal with swipe up or down | boolean | true |\n`onClose` | Function called on modal closed | function | none |\n`onImageChange` | Function called when image is changed | function | none |\n`renderFooter` | Function returns a footer element | function | none |\n\n#### Image item:\n\n```js\n{\n  source: any, // Image Component source object\n  width: ?number, // Width of full screen image (optional but recommended)\n  height: ?number, // Height of full screen image (optional but recommended)\n  // any other props you need to render your footer\n}\n```\n\nIt's recommended to specify width and height to speed up rendering, overwise component needs to fetch images sizes and cache them in images objects passed as props.\n\n#### controls prop:\n\n```js\ntype ControlType = React.Component<{onPress: () => void}> | null | boolean,\n\n{\n  close: ControlType // Component for close button in up right corner, as onPress prop accepts function to close modal\n  next: ControlType, // Component for next image button, as onPress prop accepts function to scroll to next image\n  prev: ControlType, // Component for previous image button, as onPress prop accepts function to scroll to previous image\n}\n```\n\nTo use default components just set `{next: true, prev: true}`, close is showing by default. To create custom controls check src/controls.\n\n### License\n  [MIT](LICENSE)\n"
  },
  {
    "path": "example/.eslintrc",
    "content": "{\n    \"extends\": [\n      \"airbnb\",\n      \"prettier\",\n      \"prettier/flowtype\",\n      \"prettier/react\"\n    ],\n    \"rules\": {\n        \"indent\": [\"error\", 4],\n        \"comma-dangle\": [\"error\", {\n            \"arrays\": \"always-multiline\",\n            \"objects\": \"always-multiline\",\n            \"imports\": \"always-multiline\",\n            \"exports\": \"always-multiline\",\n            \"functions\": \"ignore\"\n        }],\n        \"no-console\": 0,\n        \"no-restricted-properties\": 0,\n        \"object-curly-spacing\": [\"error\", \"never\"],\n        \"prettier/prettier\": \"error\",\n        \"react/prop-types\": [\"error\", {\"customValidators\": [\"skipUndeclared\"]}],\n        \"react/jsx-indent-props\": [\"error\", 4],\n        \"react/jsx-indent\": [\"error\", 4],\n        \"react/jsx-filename-extension\": [1, { \"extensions\": [\".js\", \".jsx\"] }]\n    },\n    \"root\": true,\n    \"plugins\": [\n        \"react\",\n        \"flowtype\",\n        \"prettier\"\n    ],\n    \"settings\": {\n        \"flowtype\": {\n            \"onlyFilesWithFlowAnnotation\": true\n        }\n    },\n    \"parser\": \"babel-eslint\",\n    \"parserOptions\": {\n        \"ecmaFeatures\": {\n            \"experimentalObjectRestSpread\": true\n        }\n    }\n}\n"
  },
  {
    "path": "example/.flowconfig",
    "content": "[ignore]\n; We fork some components by platform\n.*/*[.]android.js\n\n; Ignore \"BUCK\" generated dirs\n<PROJECT_ROOT>/\\.buckd/\n\n; Ignore unexpected extra \"@providesModule\"\n.*/node_modules/.*/node_modules/fbjs/.*\n\n; Ignore duplicate module providers\n; For RN Apps installed via npm, \"Libraries\" folder is inside\n; \"node_modules/react-native\" but in the source repo it is in the root\n.*/Libraries/react-native/React.js\n\n; Ignore polyfills\n.*/Libraries/polyfills/.*\n\n; Ignore metro\n.*/node_modules/metro/.*\n\n[include]\n\n[libs]\nnode_modules/react-native/Libraries/react-native/react-native-interface.js\nnode_modules/react-native/flow/\nnode_modules/react-native/flow-github/\n\n[options]\nemoji=true\n\nmodule.system=haste\n\nmunge_underscores=true\n\nmodule.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'\n\nmodule.file_ext=.js\nmodule.file_ext=.jsx\nmodule.file_ext=.json\nmodule.file_ext=.native.js\n\nsuppress_type=$FlowIssue\nsuppress_type=$FlowFixMe\nsuppress_type=$FlowFixMeProps\nsuppress_type=$FlowFixMeState\n\nsuppress_comment=\\\\(.\\\\|\\n\\\\)*\\\\$FlowFixMe\\\\($\\\\|[^(]\\\\|(\\\\(<VERSION>\\\\)? *\\\\(site=[a-z,_]*react_native[a-z,_]*\\\\)?)\\\\)\nsuppress_comment=\\\\(.\\\\|\\n\\\\)*\\\\$FlowIssue\\\\((\\\\(<VERSION>\\\\)? *\\\\(site=[a-z,_]*react_native[a-z,_]*\\\\)?)\\\\)?:? #[0-9]+\nsuppress_comment=\\\\(.\\\\|\\n\\\\)*\\\\$FlowFixedInNextDeploy\nsuppress_comment=\\\\(.\\\\|\\n\\\\)*\\\\$FlowExpectedError\n\n[version]\n^0.71.0"
  },
  {
    "path": "example/.gitignore",
    "content": "node_modules/**/*\n.expo/*\nnpm-debug.*\nyarn.lock\nImageView.js\ntypes.js\n"
  },
  {
    "path": "example/.watchmanconfig",
    "content": "{}\n"
  },
  {
    "path": "example/App.js",
    "content": "import React, {Component} from 'react';\nimport {\n    Text,\n    View,\n    Image,\n    TouchableOpacity,\n    StyleSheet,\n    Dimensions,\n    Platform,\n} from 'react-native';\n\n// import ImageView from '../src/ImageView';\nimport ImageView from 'react-native-image-view';\n\nconst {width} = Dimensions.get('window');\n\nconst cities = [\n    {\n        source: {\n            uri:\n                'https://avatars.mds.yandex.net/get-pdb/49816/d9152cc6-bf48-4e44-b2d5-de73b2e94454/s800',\n        },\n        title: 'London',\n    },\n    {\n        // eslint-disable-next-line\n        source: require('./assets/spb.jpg'),\n        title: 'St-Petersburg',\n        width: 1200,\n        height: 800,\n    },\n    {\n        source: {\n            uri:\n                'https://cdn.pixabay.com/photo/2017/08/17/10/47/paris-2650808_960_720.jpg',\n        },\n        title: 'Paris',\n        width: 806,\n        height: 720,\n    },\n];\n\nconst nature = [\n    {\n        source: {\n            uri:\n                'https://images.fineartamerica.com/images/artworkimages/mediumlarge/1/1-forest-in-fog-russian-nature-forest-mist-dmitry-ilyshev.jpg',\n        },\n        title: 'Switzerland',\n    },\n\n    {\n        source: {\n            uri:\n                'https://i.pinimg.com/564x/a5/1b/63/a51b63c13c7c41fa333b302fc7938f06.jpg',\n        },\n        title: 'USA',\n        width: 400,\n        height: 800,\n    },\n    {\n        source: {\n            uri:\n                'https://guidetoiceland.imgix.net/4935/x/0/top-10-beautiful-waterfalls-of-iceland-8?auto=compress%2Cformat&ch=Width%2CDPR&dpr=1&ixlib=php-2.1.1&w=883&s=1fb8e5e1906e1d18fc6b08108a9dde8d',\n        },\n        title: 'Iceland',\n        width: 880,\n        height: 590,\n    },\n];\n\nconst tabs = [\n    {title: 'Cities', images: cities},\n    {title: 'Nature', images: nature},\n];\n\nconst styles = StyleSheet.create({\n    container: {\n        flex: 1,\n        flexDirection: 'column',\n        alignItems: 'center',\n        justifyContent: 'center',\n        backgroundColor: '#000',\n        paddingTop: Platform.select({ios: 0, android: 10}),\n    },\n    tabs: {\n        flexDirection: 'row',\n    },\n    tab: {\n        flex: 1,\n        height: 30,\n        alignItems: 'center',\n        justifyContent: 'flex-end',\n    },\n    tabTitle: {\n        color: '#EEE',\n    },\n    tabTitleActive: {\n        fontWeight: '700',\n        color: '#FFF',\n    },\n    footer: {\n        width,\n        height: 50,\n        flexDirection: 'row',\n        alignItems: 'center',\n        justifyContent: 'center',\n        backgroundColor: 'rgba(0, 0, 0, 0.4)',\n        paddingHorizontal: 10,\n        paddingVertical: 5,\n    },\n    footerButton: {\n        flexDirection: 'row',\n        marginLeft: 15,\n    },\n    footerText: {\n        fontSize: 16,\n        color: '#FFF',\n        textAlign: 'center',\n    },\n});\n\nexport default class App extends Component {\n    constructor(props) {\n        super(props);\n\n        this.state = {\n            activeTab: 0,\n            imageIndex: 0,\n            isImageViewVisible: false,\n            likes: [...cities, ...nature].reduce((acc, image) => {\n                acc[image.title] = 0;\n\n                return acc;\n            }, {}),\n        };\n\n        this.renderFooter = this.renderFooter.bind(this);\n    }\n\n    renderFooter({title}) {\n        const {likes} = this.state;\n\n        return (\n            <View style={styles.footer}>\n                <Text style={styles.footerText}>{title}</Text>\n                <TouchableOpacity\n                    style={styles.footerButton}\n                    onPress={() => {\n                        const imageLikes = likes[title] + 1;\n                        this.setState({likes: {...likes, [title]: imageLikes}});\n                    }}\n                >\n                    <Text style={styles.footerText}>♥</Text>\n                    <Text style={[styles.footerText, {marginLeft: 7}]}>\n                        {likes[title]}\n                    </Text>\n                </TouchableOpacity>\n            </View>\n        );\n    }\n\n    render() {\n        const {isImageViewVisible, activeTab, imageIndex} = this.state;\n        const images = tabs[activeTab].images || [];\n\n        return (\n            <View style={styles.container}>\n                <View>\n                    {images.map((image, index) => (\n                        <TouchableOpacity\n                            key={image.title}\n                            onPress={() => {\n                                this.setState({\n                                    imageIndex: index,\n                                    isImageViewVisible: true,\n                                });\n                            }}\n                        >\n                            <Image\n                                style={{width, height: 200}}\n                                source={image.source}\n                                resizeMode=\"cover\"\n                            />\n                        </TouchableOpacity>\n                    ))}\n                </View>\n                <View style={styles.tabs}>\n                    {tabs.map(({title}, index) => (\n                        <TouchableOpacity\n                            style={styles.tab}\n                            key={title}\n                            onPress={() => {\n                                this.setState({\n                                    activeTab: index,\n                                });\n                            }}\n                        >\n                            <Text\n                                style={[\n                                    styles.tabTitle,\n                                    index === activeTab &&\n                                        styles.tabTitleActive,\n                                ]}\n                            >\n                                {title}\n                            </Text>\n                        </TouchableOpacity>\n                    ))}\n                </View>\n                <ImageView\n                    glideAlways\n                    images={images}\n                    imageIndex={imageIndex}\n                    animationType=\"fade\"\n                    isVisible={isImageViewVisible}\n                    renderFooter={this.renderFooter}\n                    onClose={() => this.setState({isImageViewVisible: false})}\n                    onImageChange={index => {\n                        console.log(index);\n                    }}\n                />\n            </View>\n        );\n    }\n}\n"
  },
  {
    "path": "example/app.json",
    "content": "{\n  \"expo\": {\n    \"name\": \"react-native-image-view\",\n    \"description\": \"React Native modal image view with pinch zoom\",\n    \"slug\": \"react-native-image-view\",\n    \"privacy\": \"public\",\n    \"sdkVersion\": \"35.0.0\",\n    \"version\": \"1.0.0\",\n    \"orientation\": \"portrait\",\n    \"primaryColor\": \"#cccccc\",\n    \"icon\": \"./assets/icon.png\",\n    \"splash\": {\n      \"image\": \"./assets/splash.png\",\n      \"resizeMode\": \"contain\",\n      \"backgroundColor\": \"#ffffff\"\n    },\n    \"ios\": {\n      \"supportsTablet\": true\n    }\n  }\n}\n"
  },
  {
    "path": "example/babel.config.js",
    "content": "module.exports = function(api) {\n    api.cache(true);\n    return {\n        presets: ['babel-preset-expo'],\n    };\n};\n"
  },
  {
    "path": "example/metro.config.js",
    "content": "const path = require('path');\n\nmodule.exports = {\n    resolver: {\n        extraNodeModules: new Proxy(\n            {},\n            {\n                get: (target, name) =>\n                    path.join(process.cwd(), `node_modules/${name}`),\n            }\n        ),\n    },\n    projectRoot: [path.resolve(__dirname)],\n    watchFolders: [path.resolve(__dirname, '../src')],\n};\n"
  },
  {
    "path": "example/package.json",
    "content": "{\n  \"main\": \"node_modules/expo/AppEntry.js\",\n  \"private\": true,\n  \"dependencies\": {\n    \"expo\": \"^35.0.0\",\n    \"react\": \"16.8.6\",\n    \"react-native\": \"https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz\",\n    \"react-native-image-view\": \"^2.1.6\"\n  },\n  \"devDependencies\": {\n    \"babel-eslint\": \"10.0.1\",\n    \"babel-preset-expo\": \"7.0.0\",\n    \"eslint\": \"5.16.0\",\n    \"eslint-config-airbnb\": \"17.1.0\",\n    \"eslint-config-prettier\": \"4.3.0\",\n    \"eslint-plugin-flowtype\": \"3.9.1\",\n    \"eslint-plugin-import\": \"2.17.3\",\n    \"eslint-plugin-jsx-a11y\": \"6.2.1\",\n    \"eslint-plugin-prettier\": \"3.1.0\",\n    \"eslint-plugin-react\": \"7.13.0\",\n    \"flow-bin\": \"0.100.0\",\n    \"prettier\": \"1.18.2\"\n  },\n  \"scripts\": {\n    \"eslint-check\": \"eslint --print-config .eslintrc.js | eslint-config-prettier-check\"\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"react-native-image-view\",\n  \"version\": \"2.1.9\",\n  \"description\": \"React Native modal image view with pinch zoom\",\n  \"main\": \"src/ImageView\",\n  \"scripts\": {\n    \"flow\": \"flow\",\n    \"eslint-check\": \"eslint --print-config .eslintrc.js | eslint-config-prettier-check\",\n    \"postversion\": \"git push origin master && git push --tags origin master\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/antonKalinin/react-native-image-view.git\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=15.2.0\",\n    \"react-native\": \">=0.44.0\"\n  },\n  \"devDependencies\": {\n    \"babel-eslint\": \"10.0.1\",\n    \"eslint\": \"5.16.0\",\n    \"eslint-config-airbnb\": \"17.1.0\",\n    \"eslint-config-prettier\": \"4.3.0\",\n    \"eslint-plugin-flowtype\": \"3.9.1\",\n    \"eslint-plugin-import\": \"2.17.3\",\n    \"eslint-plugin-jest\": \"^21.17.0\",\n    \"eslint-plugin-jsx-a11y\": \"6.2.1\",\n    \"eslint-plugin-prettier\": \"3.1.0\",\n    \"eslint-plugin-react\": \"7.13.0\",\n    \"flow-bin\": \"0.100.0\",\n    \"prettier\": \"1.18.2\"\n  },\n  \"keywords\": [\n    \"react-native\",\n    \"image\",\n    \"zoom\",\n    \"preview\",\n    \"modal\",\n    \"pinch\",\n    \"component\"\n  ],\n  \"files\": [\n    \"package.json\",\n    \"readme.md\",\n    \"src\"\n  ],\n  \"author\": \"Anton Kalinin\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/antonKalinin/react-native-image-view/issues\"\n  },\n  \"homepage\": \"https://github.com/antonKalinin/react-native-image-view#readme\"\n}\n"
  },
  {
    "path": "src/ImageView.js",
    "content": "// @flow\n\nimport React, {Component, type Node, type ComponentType} from 'react';\nimport {\n    ActivityIndicator,\n    Animated,\n    Dimensions,\n    FlatList,\n    Modal,\n    Platform,\n    View,\n    SafeAreaView,\n} from 'react-native';\n\nimport {\n    type ControlType,\n    type ControlsType,\n    type DimensionsType,\n    type EventType,\n    type ImageType,\n    type ImageSizeType,\n    type GestureState,\n    type NativeEventType,\n    type TouchType,\n    type TransitionType,\n    type TranslateType,\n} from './types';\n\nimport {\n    addIndexesToImages,\n    calculateInitialTranslate,\n    fetchImageSize,\n    generatePanHandlers,\n    getImagesWithoutSize,\n    getScale,\n    getDistance,\n    getInitialParams,\n    hexToRgb,\n    isHex,\n    scalesAreEqual,\n} from './utils';\n\nimport createStyles from './styles';\nimport {Close, Prev, Next} from './controls';\n\nconst IMAGE_SPEED_FOR_CLOSE = 1.1;\nconst SCALE_MAXIMUM = 5;\nconst HEADER_HEIGHT = 60;\nconst SCALE_MAX_MULTIPLIER = 3;\nconst FREEZE_SCROLL_DISTANCE = 15;\nconst BACKGROUND_OPACITY_MULTIPLIER = 0.003;\nconst defaultBackgroundColor = [0, 0, 0];\n\nconst getScreenDimensions = () => ({\n    screenWidth: Dimensions.get('window').width,\n    screenHeight: Dimensions.get('window').height,\n});\n\nlet styles = createStyles(getScreenDimensions());\n\ntype PropsType = {\n    animationType: 'none' | 'fade' | 'slide',\n    backgroundColor?: string,\n    glideAlways?: boolean,\n    glideAlwaysDelay?: number,\n    images: ImageType[],\n    imageIndex: number,\n    isVisible: boolean,\n    isTapZoomEnabled: boolean,\n    isPinchZoomEnabled: boolean,\n    isSwipeCloseEnabled: boolean,\n    onClose: () => {},\n    onImageChange: number => {},\n    renderFooter: ImageType => {},\n    controls: {\n        close?: ComponentType<ControlType> | boolean,\n        next?: ComponentType<ControlType> | boolean,\n        prev?: ComponentType<ControlType> | boolean,\n    },\n};\n\nexport type StateType = {\n    images: ImageType[],\n    isVisible: boolean,\n    imageIndex: number,\n    imageScale: number,\n    imageTranslate: {x: number, y: number},\n    scrollEnabled: boolean,\n    panelsVisible: boolean,\n    isFlatListRerendered: boolean,\n    screenDimensions: {screenWidth: number, screenHeight: number},\n};\n\nexport default class ImageView extends Component<PropsType, StateType> {\n    static defaultProps = {\n        backgroundColor: null,\n        images: [],\n        imageIndex: 0,\n        isTapZoomEnabled: true,\n        isPinchZoomEnabled: true,\n        isSwipeCloseEnabled: true,\n        glideAlways: false,\n        glideAlwaysDelay: 75,\n        controls: {prev: null, next: null},\n    };\n\n    constructor(props: PropsType) {\n        super(props);\n\n        // calculate initial scale and translate for images\n        const initialScreenDimensions = getScreenDimensions();\n        this.imageInitialParams = props.images.map(image =>\n            getInitialParams(image, initialScreenDimensions)\n        );\n\n        this.state = {\n            images: props.images,\n            isVisible: props.isVisible,\n            imageIndex: props.imageIndex,\n            imageScale: 1,\n            imageTranslate: {x: 0, y: 0},\n            scrollEnabled: true,\n            panelsVisible: true,\n            isFlatListRerendered: false,\n            screenDimensions: initialScreenDimensions,\n        };\n        this.glideAlwaysTimer = null;\n        this.listRef = null;\n        this.isScrolling = false;\n        this.footerHeight = 0;\n        this.initialTouches = [];\n        this.currentTouchesNum = 0;\n        this.doubleTapTimer = null;\n        this.modalAnimation = new Animated.Value(0);\n        this.modalBackgroundOpacity = new Animated.Value(0);\n\n        this.headerTranslateValue = new Animated.ValueXY();\n        this.footerTranslateValue = new Animated.ValueXY();\n\n        this.imageScaleValue = new Animated.Value(this.getInitialScale());\n        const {x, y} = this.getInitialTranslate();\n        this.imageTranslateValue = new Animated.ValueXY({x, y});\n\n        this.panResponder = generatePanHandlers(\n            (event: EventType): void => this.onGestureStart(event.nativeEvent),\n            (event: EventType, gestureState: GestureState): void =>\n                this.onGestureMove(event.nativeEvent, gestureState),\n            (event: EventType, gestureState: GestureState): void =>\n                this.onGestureRelease(event.nativeEvent, gestureState)\n        );\n\n        const imagesWithoutSize = getImagesWithoutSize(\n            addIndexesToImages(props.images)\n        );\n\n        if (imagesWithoutSize.length) {\n            Promise.all(fetchImageSize(imagesWithoutSize)).then(\n                this.setSizeForImages\n            );\n        }\n    }\n\n    componentDidMount() {\n        styles = createStyles(this.state.screenDimensions);\n        Dimensions.addEventListener('change', this.onChangeDimension);\n    }\n\n    componentDidUpdate() {\n        const {images, imageIndex, isVisible} = this.state;\n\n        if (\n            typeof this.props.isVisible !== 'undefined' &&\n            this.props.isVisible !== isVisible\n        ) {\n            this.onNextImagesReceived(this.props.images, this.props.imageIndex);\n\n            if (\n                images !== this.props.images ||\n                imageIndex !== this.props.imageIndex\n            ) {\n                const imagesWithoutSize = getImagesWithoutSize(\n                    addIndexesToImages(this.props.images)\n                );\n\n                if (imagesWithoutSize.length) {\n                    Promise.all(fetchImageSize(imagesWithoutSize)).then(\n                        updatedImages =>\n                            this.onNextImagesReceived(\n                                this.setSizeForImages(updatedImages),\n                                this.props.imageIndex\n                            )\n                    );\n                }\n            }\n\n            this.setState({\n                isVisible: this.props.isVisible,\n                isFlatListRerendered: false,\n            });\n\n            this.modalBackgroundOpacity.setValue(0);\n\n            if (this.props.isVisible) {\n                Animated.timing(this.modalAnimation, {\n                    duration: 400,\n                    toValue: 1,\n                }).start();\n            }\n        }\n    }\n\n    componentWillUnmount() {\n        Dimensions.removeEventListener('change', this.onChangeDimension);\n\n        if (this.glideAlwaysTimer) {\n            clearTimeout(this.glideAlwaysTimer);\n        }\n    }\n\n    onChangeDimension = ({window}: {window: DimensionsType}) => {\n        const screenDimensions = {\n            screenWidth: window.width,\n            screenHeight: window.height,\n        };\n\n        this.setState({screenDimensions});\n        styles = createStyles(screenDimensions);\n\n        this.onNextImagesReceived(this.props.images, this.state.imageIndex);\n    };\n\n    onNextImagesReceived(images: Array<ImageType>, imageIndex: number = 0) {\n        this.imageInitialParams = images.map(image =>\n            getInitialParams(image, this.state.screenDimensions)\n        );\n        const {scale, translate} = this.imageInitialParams[imageIndex] || {\n            scale: 1,\n            translate: {},\n        };\n\n        this.setState({\n            images,\n            imageIndex,\n            imageScale: scale,\n            imageTranslate: translate,\n            isFlatListRerendered: false,\n        });\n\n        this.imageScaleValue.setValue(scale);\n        this.imageTranslateValue.setValue(translate);\n    }\n\n    // $FlowFixMe\n    onFlatListRender = flatListRef => {\n        const {images, imageIndex, isFlatListRerendered} = this.state;\n\n        if (flatListRef && !isFlatListRerendered) {\n            this.listRef = flatListRef;\n            this.setState({\n                isFlatListRerendered: true,\n            });\n\n            // Fix for android https://github.com/facebook/react-native/issues/13202\n            if (images.length > 0) {\n                const nextTick = new Promise(resolve => setTimeout(resolve, 0));\n                nextTick.then(() => {\n                    flatListRef.scrollToIndex({\n                        index: imageIndex,\n                        animated: false,\n                    });\n                });\n            }\n        }\n    };\n\n    onNextImage = (event: EventType) => {\n        const {imageIndex} = this.state;\n        const {x} = event.nativeEvent.contentOffset || {x: 0};\n\n        const nextImageIndex = Math.round(\n            x / this.state.screenDimensions.screenWidth\n        );\n\n        this.isScrolling =\n            Math.ceil(x) % this.state.screenDimensions.screenWidth > 10;\n\n        if (imageIndex !== nextImageIndex && nextImageIndex >= 0) {\n            const nextImageScale = this.getInitialScale(nextImageIndex);\n            const nextImageTranslate = this.getInitialTranslate(nextImageIndex);\n\n            this.setState({\n                imageIndex: nextImageIndex,\n                imageScale: nextImageScale,\n                imageTranslate: nextImageTranslate,\n            });\n\n            this.imageScaleValue.setValue(nextImageScale);\n            this.imageTranslateValue.setValue(nextImageTranslate);\n\n            if (typeof this.props.onImageChange === 'function') {\n                this.props.onImageChange(nextImageIndex);\n            }\n        }\n    };\n\n    onGestureStart(event: NativeEventType) {\n        this.initialTouches = event.touches;\n        this.currentTouchesNum = event.touches.length;\n    }\n\n    /**\n     * If image is moved from its original position\n     * then disable scroll (for ScrollView)\n     */\n    onGestureMove(event: NativeEventType, gestureState: GestureState) {\n        if (this.isScrolling && this.state.scrollEnabled) {\n            return;\n        }\n\n        if (this.currentTouchesNum === 1 && event.touches.length === 2) {\n            this.initialTouches = event.touches;\n        }\n\n        const {isSwipeCloseEnabled, isPinchZoomEnabled} = this.props;\n\n        const {\n            images,\n            imageIndex,\n            imageScale,\n            imageTranslate,\n            screenDimensions,\n        } = this.state;\n        const {screenHeight} = screenDimensions;\n        const {touches} = event;\n        const {x, y} = imageTranslate;\n        const {dx, dy} = gestureState;\n        const imageInitialScale = this.getInitialScale();\n        const {height} = images[imageIndex];\n\n        if (imageScale !== imageInitialScale) {\n            this.imageTranslateValue.x.setValue(x + dx);\n        }\n\n        // Do not allow to move image vertically until it fits to the screen\n        if (imageScale * height > screenHeight) {\n            this.imageTranslateValue.y.setValue(y + dy);\n        }\n\n        // if image not scaled and fits to the screen\n        if (\n            isSwipeCloseEnabled &&\n            scalesAreEqual(imageScale, imageInitialScale) &&\n            height * imageInitialScale < screenHeight\n        ) {\n            const backgroundOpacity = Math.abs(\n                dy * BACKGROUND_OPACITY_MULTIPLIER\n            );\n\n            this.imageTranslateValue.y.setValue(y + dy);\n            this.modalBackgroundOpacity.setValue(\n                backgroundOpacity > 1 ? 1 : backgroundOpacity\n            );\n        }\n\n        const currentDistance = getDistance(touches);\n        const initialDistance = getDistance(this.initialTouches);\n\n        const scrollEnabled = Math.abs(dy) < FREEZE_SCROLL_DISTANCE;\n        this.setState({scrollEnabled});\n\n        if (!initialDistance) {\n            return;\n        }\n\n        if (!isPinchZoomEnabled || touches.length < 2) {\n            return;\n        }\n\n        let nextScale = getScale(currentDistance, initialDistance) * imageScale;\n\n        if (nextScale < imageInitialScale) {\n            nextScale = imageInitialScale;\n        } else if (nextScale > SCALE_MAXIMUM) {\n            nextScale = SCALE_MAXIMUM;\n        }\n\n        this.imageScaleValue.setValue(nextScale);\n        this.currentTouchesNum = event.touches.length;\n    }\n\n    onGestureRelease(event: NativeEventType, gestureState: GestureState) {\n        if (this.glideAlwaysTimer) {\n            clearTimeout(this.glideAlwaysTimer);\n        }\n\n        if (this.props.glideAlways && Platform.OS === 'android') {\n            this.glideAlwaysTimer = setTimeout(() => {\n                this.glideAlwaysTimer = null;\n                // If standard glide is not triggered then emulate it\n                // $FlowFixMe\n                if (this.listRef && this.listRef.scrollToIndex) {\n                    this.listRef.scrollToIndex({\n                        index: this.state.imageIndex,\n                        animated: true,\n                    });\n                }\n            }, this.props.glideAlwaysDelay);\n        }\n\n        if (this.isScrolling) {\n            return;\n        }\n\n        const {imageScale} = this.state;\n        const {isSwipeCloseEnabled, isTapZoomEnabled} = this.props;\n\n        let {_value: scale} = this.imageScaleValue;\n        const {_value: modalBackgroundOpacity} = this.modalBackgroundOpacity;\n\n        const {dx, dy, vy} = gestureState;\n        const imageInitialScale = this.getInitialScale();\n        const imageInitialTranslate = this.getInitialTranslate();\n\n        // Position haven't changed, so it just tap\n        if (event && !dx && !dy && scalesAreEqual(imageScale, scale)) {\n            // Double tap timer is launched, its double tap\n\n            if (isTapZoomEnabled && this.doubleTapTimer) {\n                clearTimeout(this.doubleTapTimer);\n                this.doubleTapTimer = null;\n\n                scale = scalesAreEqual(imageInitialScale, scale)\n                    ? scale * SCALE_MAX_MULTIPLIER\n                    : imageInitialScale;\n\n                Animated.timing(this.imageScaleValue, {\n                    toValue: scale,\n                    duration: 300,\n                }).start();\n\n                this.togglePanels(scale === imageInitialScale);\n            } else {\n                this.doubleTapTimer = setTimeout(() => {\n                    this.togglePanels();\n                    this.doubleTapTimer = null;\n                }, 200);\n            }\n        }\n\n        const {x, y} = this.calculateNextTranslate(dx, dy, scale);\n        const scrollEnabled =\n            scale === this.getInitialScale() &&\n            x === imageInitialTranslate.x &&\n            y === imageInitialTranslate.y;\n\n        Animated.parallel(\n            [\n                modalBackgroundOpacity > 0\n                    ? Animated.timing(this.modalBackgroundOpacity, {\n                          toValue: 0,\n                          duration: 100,\n                      })\n                    : null,\n                Animated.timing(this.imageTranslateValue.x, {\n                    toValue: x,\n                    duration: 100,\n                }),\n                Animated.timing(this.imageTranslateValue.y, {\n                    toValue: y,\n                    duration: 100,\n                }),\n            ].filter(Boolean)\n        ).start();\n\n        // Close modal with animation if image not scaled and high vertical gesture speed\n        if (\n            isSwipeCloseEnabled &&\n            scale === imageInitialScale &&\n            Math.abs(vy) >= IMAGE_SPEED_FOR_CLOSE\n        ) {\n            Animated.timing(this.imageTranslateValue.y, {\n                toValue: y + 400 * vy,\n                duration: 150,\n            }).start(this.close);\n        }\n\n        this.setState({\n            imageScale: scale,\n            imageTranslate: {x, y},\n            scrollEnabled,\n        });\n    }\n\n    onImageLoaded(index: number) {\n        const {images} = this.state;\n\n        images[index] = {...images[index], loaded: true};\n\n        this.setState({images});\n    }\n\n    onMomentumScrollBegin = () => {\n        this.isScrolling = true;\n        if (this.glideAlwaysTimer) {\n            // If FlatList started gliding then prevent glideAlways scrolling\n            clearTimeout(this.glideAlwaysTimer);\n        }\n    };\n\n    onMomentumScrollEnd = () => {\n        this.isScrolling = false;\n    };\n\n    getItemLayout = (_: *, index: number): Object => {\n        const {screenWidth} = this.state.screenDimensions;\n\n        return {length: screenWidth, offset: screenWidth * index, index};\n    };\n\n    getInitialScale(index?: number): number {\n        const imageIndex = index !== undefined ? index : this.state.imageIndex;\n        const imageParams = this.imageInitialParams[imageIndex];\n\n        return imageParams ? imageParams.scale : 1;\n    }\n\n    getInitialTranslate(index?: number): TranslateType {\n        const imageIndex = index !== undefined ? index : this.state.imageIndex;\n        const imageParams = this.imageInitialParams[imageIndex];\n\n        return imageParams ? imageParams.translate : {x: 0, y: 0};\n    }\n\n    getImageStyle(\n        image: ImageType,\n        index: number\n    ): {width?: number, height?: number, transform?: any, opacity?: number} {\n        const {imageIndex, screenDimensions} = this.state;\n        const {width, height} = image;\n\n        if (!width || !height) {\n            return {opacity: 0};\n        }\n\n        // very strange caching, fix it with changing size to 1 pixel\n        const {x, y} = calculateInitialTranslate(\n            width,\n            height + 1,\n            screenDimensions\n        );\n        const translateValue = new Animated.ValueXY({x, y});\n\n        const transform =\n            index === imageIndex\n                ? this.imageTranslateValue.getTranslateTransform()\n                : translateValue.getTranslateTransform();\n\n        const scale =\n            index === imageIndex\n                ? this.imageScaleValue\n                : this.getInitialScale(index);\n        // $FlowFixMe\n        transform.push({scale});\n\n        return {width, height, transform};\n    }\n\n    getControls = (): ControlsType => {\n        const {close, prev, next} = this.props.controls;\n        const controls = {close: Close, prev: undefined, next: undefined};\n\n        if (close === null) {\n            controls.close = null;\n        }\n\n        if (close) {\n            controls.close = close === true ? Close : close;\n        }\n\n        if (prev) {\n            controls.prev = prev === true ? Prev : prev;\n        }\n\n        if (next) {\n            controls.next = next === true ? Next : next;\n        }\n\n        return controls;\n    };\n\n    setSizeForImages = (nextImages: Array<ImageSizeType>): Array<ImageType> => {\n        if (nextImages.length === 0) {\n            return [];\n        }\n\n        const {images} = this.state;\n\n        return images.map((image, index) => {\n            const nextImageSize = nextImages.find(\n                nextImage => nextImage.index === index\n            );\n\n            /* eslint-disable */\n            if (nextImageSize) {\n                image.width = nextImageSize.width;\n                image.height = nextImageSize.height;\n            }\n            /* eslint-enable */\n\n            return image;\n        });\n    };\n\n    scrollToNext = () => {\n        if (this.listRef && typeof this.listRef.scrollToIndex === 'function') {\n            this.listRef.scrollToIndex({\n                index: this.state.imageIndex + 1,\n                animated: true,\n            });\n        }\n    };\n\n    scrollToPrev = () => {\n        if (this.listRef && typeof this.listRef.scrollToIndex === 'function') {\n            this.listRef.scrollToIndex({\n                index: this.state.imageIndex - 1,\n                animated: true,\n            });\n        }\n    };\n\n    imageInitialParams: TransitionType[];\n    glideAlwaysTimer: ?TimeoutID;\n    listRef: *;\n    isScrolling: boolean;\n    footerHeight: number;\n    initialTouches: TouchType[];\n    currentTouchesNum: number;\n    doubleTapTimer: ?TimeoutID;\n    modalAnimation: *;\n    modalBackgroundOpacity: *;\n    headerTranslateValue: *;\n    footerTranslateValue: *;\n    imageScaleValue: *;\n    imageTranslateValue: *;\n    panResponder: *;\n\n    calculateNextTranslate(\n        dx: number,\n        dy: number,\n        scale: number\n    ): {x: number, y: number} {\n        const {\n            images,\n            imageIndex,\n            imageTranslate,\n            screenDimensions,\n        } = this.state;\n        const {x, y} = imageTranslate;\n        const {screenWidth, screenHeight} = screenDimensions;\n        const {width, height} = images[imageIndex];\n        const imageInitialScale = this.getInitialScale();\n\n        const getTranslate = (axis: string): number => {\n            const imageSize = axis === 'x' ? width : height;\n            const screenSize = axis === 'x' ? screenWidth : screenHeight;\n            const leftLimit = (scale * imageSize - imageSize) / 2;\n            const rightLimit = screenSize - imageSize - leftLimit;\n\n            let nextTranslate = axis === 'x' ? x + dx : y + dy;\n\n            // Less than the screen\n            if (screenSize > scale * imageSize) {\n                if (width >= height) {\n                    nextTranslate = (screenSize - imageSize) / 2;\n                } else {\n                    nextTranslate =\n                        screenSize / 2 -\n                        (imageSize * (scale / imageInitialScale)) / 2;\n                }\n\n                return nextTranslate;\n            }\n\n            if (nextTranslate > leftLimit) {\n                nextTranslate = leftLimit;\n            }\n\n            if (nextTranslate < rightLimit) {\n                nextTranslate = rightLimit;\n            }\n\n            return nextTranslate;\n        };\n\n        return {x: getTranslate('x'), y: getTranslate('y')};\n    }\n\n    togglePanels(isVisible?: boolean) {\n        const panelsVisible =\n            typeof isVisible !== 'undefined'\n                ? isVisible\n                : !this.state.panelsVisible;\n        // toggle footer and header\n        this.setState({panelsVisible});\n\n        Animated.timing(this.headerTranslateValue.y, {\n            toValue: !panelsVisible ? -(HEADER_HEIGHT + 44) : 0,\n            duration: 200,\n            useNativeDriver: true,\n        }).start();\n\n        if (this.footerHeight > 0) {\n            Animated.timing(this.footerTranslateValue.y, {\n                toValue: !panelsVisible ? this.footerHeight : 0,\n                duration: 200,\n                useNativeDriver: true,\n            }).start();\n        }\n    }\n\n    listKeyExtractor = (image: ImageType): string =>\n        this.state.images.indexOf(image).toString();\n\n    close = () => {\n        this.setState({isVisible: false});\n\n        if (typeof this.props.onClose === 'function') {\n            this.props.onClose();\n        }\n    };\n\n    renderImage = ({item: image, index}: {item: *, index: number}): * => {\n        const loaded = image.loaded && image.width && image.height;\n\n        return (\n            <View\n                style={styles.imageContainer}\n                onStartShouldSetResponder={(): boolean => true}\n            >\n                <Animated.Image\n                    resizeMode=\"cover\"\n                    source={image.source}\n                    style={this.getImageStyle(image, index)}\n                    onLoad={(): void => this.onImageLoaded(index)}\n                    {...this.panResponder.panHandlers}\n                />\n                {!loaded && <ActivityIndicator style={styles.loading} />}\n            </View>\n        );\n    };\n\n    render(): Node {\n        const {animationType, renderFooter, backgroundColor} = this.props;\n        const {\n            images,\n            imageIndex,\n            imageScale,\n            isVisible,\n            scrollEnabled,\n        } = this.state;\n\n        const {close, prev, next} = this.getControls();\n        const imageInitialScale = this.getInitialScale();\n        const headerTranslate = this.headerTranslateValue.getTranslateTransform();\n        const footerTranslate = this.footerTranslateValue.getTranslateTransform();\n        const rgbBackgroundColor =\n            backgroundColor && isHex(backgroundColor)\n                ? hexToRgb(backgroundColor)\n                : defaultBackgroundColor;\n        const rgb = rgbBackgroundColor.join(',');\n        const animatedBackgroundColor = this.modalBackgroundOpacity.interpolate(\n            {\n                inputRange: [0, 1],\n                outputRange: [`rgba(${rgb}, 0.9)`, `rgba(${rgb}, 0.2)`],\n            }\n        );\n\n        const isPrevVisible =\n            imageScale === imageInitialScale && imageIndex > 0;\n        const isNextVisible =\n            imageScale === imageInitialScale && imageIndex < images.length - 1;\n\n        return (\n            <Modal\n                transparent\n                visible={isVisible}\n                animationType={animationType}\n                onRequestClose={this.close}\n                supportedOrientations={['portrait', 'landscape']}\n            >\n                <Animated.View\n                    style={[\n                        {backgroundColor: animatedBackgroundColor},\n                        styles.underlay,\n                    ]}\n                />\n                <Animated.View\n                    style={[\n                        styles.header,\n                        {\n                            transform: headerTranslate,\n                        },\n                    ]}\n                >\n                    <SafeAreaView style={{flex: 1}}>\n                        {!!close &&\n                            React.createElement(close, {onPress: this.close})}\n                    </SafeAreaView>\n                </Animated.View>\n                <FlatList\n                    horizontal\n                    pagingEnabled\n                    data={images}\n                    scrollEnabled={scrollEnabled}\n                    scrollEventThrottle={16}\n                    style={styles.container}\n                    ref={this.onFlatListRender}\n                    renderSeparator={() => null}\n                    keyExtractor={this.listKeyExtractor}\n                    onScroll={this.onNextImage}\n                    renderItem={this.renderImage}\n                    getItemLayout={this.getItemLayout}\n                    onMomentumScrollBegin={this.onMomentumScrollBegin}\n                    onMomentumScrollEnd={this.onMomentumScrollEnd}\n                />\n                {prev &&\n                    isPrevVisible &&\n                    React.createElement(prev, {onPress: this.scrollToPrev})}\n                {next &&\n                    isNextVisible &&\n                    React.createElement(next, {onPress: this.scrollToNext})}\n                {renderFooter && (\n                    <Animated.View\n                        style={[styles.footer, {transform: footerTranslate}]}\n                        onLayout={event => {\n                            this.footerHeight = event.nativeEvent.layout.height;\n                        }}\n                    >\n                        {typeof renderFooter === 'function' &&\n                            images[imageIndex] &&\n                            renderFooter(images[imageIndex])}\n                    </Animated.View>\n                )}\n            </Modal>\n        );\n    }\n}\n"
  },
  {
    "path": "src/controls/Close.js",
    "content": "// @flow\nimport React from 'react';\nimport {StyleSheet, Text, TouchableOpacity} from 'react-native';\n\nconst HIT_SLOP = {top: 15, left: 15, right: 15, bottom: 15};\n\nconst styles = StyleSheet.create({\n    closeButton: {\n        alignSelf: 'flex-end',\n        height: 24,\n        width: 24,\n        borderRadius: 12,\n        backgroundColor: 'rgba(0,0,0,0.2)',\n        alignItems: 'center',\n        justifyContent: 'center',\n        marginTop: 25,\n        marginRight: 15,\n    },\n    closeButton__text: {\n        backgroundColor: 'transparent',\n        fontSize: 25,\n        lineHeight: 25,\n        color: '#FFF',\n        textAlign: 'center',\n    },\n});\n\nexport default ({onPress}: {onPress: () => *}) => (\n    <TouchableOpacity\n        hitSlop={HIT_SLOP}\n        style={styles.closeButton}\n        onPress={onPress}\n    >\n        <Text style={styles.closeButton__text}>×</Text>\n    </TouchableOpacity>\n);\n"
  },
  {
    "path": "src/controls/Next.js",
    "content": "// @flow\nimport React from 'react';\nimport {StyleSheet, Text, TouchableOpacity} from 'react-native';\n\nconst HIT_SLOP = {top: 15, left: 15, right: 15, bottom: 15};\n\nconst styles = StyleSheet.create({\n    nextButton: {\n        position: 'absolute',\n        zIndex: 100,\n        right: 10,\n        top: '50%',\n        height: 32,\n        width: 32,\n        borderRadius: 16,\n        backgroundColor: 'rgba(0,0,0,0.3)',\n        alignItems: 'center',\n        justifyContent: 'center',\n    },\n    nextButton__text: {\n        backgroundColor: 'transparent',\n        fontSize: 25,\n        lineHeight: 25,\n        color: '#FFF',\n        textAlign: 'center',\n    },\n});\n\nexport default ({onPress}: {onPress: () => *}) => (\n    <TouchableOpacity\n        hitSlop={HIT_SLOP}\n        style={styles.nextButton}\n        onPress={onPress}\n    >\n        <Text style={styles.nextButton__text}>›</Text>\n    </TouchableOpacity>\n);\n"
  },
  {
    "path": "src/controls/Prev.js",
    "content": "// @flow\nimport React from 'react';\nimport {StyleSheet, Text, TouchableOpacity} from 'react-native';\n\nconst HIT_SLOP = {top: 15, left: 15, right: 15, bottom: 15};\n\nconst styles = StyleSheet.create({\n    prevButton: {\n        position: 'absolute',\n        zIndex: 100,\n        left: 10,\n        top: '50%',\n        height: 32,\n        width: 32,\n        borderRadius: 16,\n        backgroundColor: 'rgba(0,0,0,0.3)',\n        alignItems: 'center',\n        justifyContent: 'center',\n    },\n    prevButton__text: {\n        backgroundColor: 'transparent',\n        fontSize: 25,\n        lineHeight: 25,\n        color: '#FFF',\n        textAlign: 'center',\n    },\n});\n\nexport default ({onPress}: {onPress: () => *}) => (\n    <TouchableOpacity\n        hitSlop={HIT_SLOP}\n        style={styles.prevButton}\n        onPress={onPress}\n    >\n        <Text style={styles.prevButton__text}>‹</Text>\n    </TouchableOpacity>\n);\n"
  },
  {
    "path": "src/controls/index.js",
    "content": "// @flow\nexport {default as Close} from './Close';\nexport {default as Prev} from './Prev';\nexport {default as Next} from './Next';\n"
  },
  {
    "path": "src/styles.js",
    "content": "import {StyleSheet} from 'react-native';\n\nconst HEADER_HEIGHT = 60;\n\nexport default function createStyles({screenWidth, screenHeight}) {\n    return StyleSheet.create({\n        underlay: {\n            position: 'absolute',\n            top: 0,\n            left: 0,\n            right: 0,\n            bottom: 0,\n        },\n        container: {\n            width: screenWidth,\n            height: screenHeight,\n        },\n        header: {\n            position: 'absolute',\n            top: 0,\n            left: 0,\n            zIndex: 100,\n            height: HEADER_HEIGHT,\n            width: screenWidth,\n        },\n        imageContainer: {\n            width: screenWidth,\n            height: screenHeight,\n            overflow: 'hidden',\n        },\n        loading: {\n            position: 'absolute',\n            top: screenHeight / 2 - 20,\n            alignSelf: 'center',\n        },\n        footer: {\n            position: 'absolute',\n            bottom: 0,\n            left: 0,\n            right: 0,\n            zIndex: 100,\n        },\n    });\n}\n"
  },
  {
    "path": "src/types.js",
    "content": "// @flow\nimport {type ComponentType} from 'react';\n\nexport type ControlType = {\n    onPress: () => void,\n};\n\nexport type ControlsType = {\n    close?: ?ComponentType<ControlType>,\n    next?: ComponentType<ControlType>,\n    prev?: ComponentType<ControlType>,\n};\n\nexport type TouchType = {\n    pageX: number,\n    pageY: number,\n};\n\nexport type NativeEventType = {\n    touches: Array<TouchType>,\n    contentOffset: {x: number, y: number},\n};\n\nexport type EventType = {nativeEvent: NativeEventType};\n\nexport type ImageType = {\n    source: any,\n    width: number,\n    height: number,\n    title: ?string,\n    index: number,\n};\n\nexport type TranslateType = {\n    x: number,\n    y: number,\n};\n\nexport type GestureState = {\n    dx: number,\n    dy: number,\n    vx: number,\n    vy: number,\n};\n\nexport type DimensionsType = {width: number, height: number};\nexport type ScreenDimensionsType = {screenWidth: number, screenHeight: number};\n\nexport type ImageSizeType = DimensionsType & {index: number};\n\nexport type TransitionType = {scale: number, translate: TranslateType};\n"
  },
  {
    "path": "src/utils.js",
    "content": "// @flow\nimport {Image, PanResponder} from 'react-native';\n\nimport {\n    type EventType,\n    type GestureState,\n    type ImageType,\n    type TouchType,\n    type TranslateType,\n    type TransitionType,\n    type DimensionsType,\n    type ScreenDimensionsType,\n} from './types';\n\nconst SCALE_EPSILON = 0.01;\nconst SCALE_MULTIPLIER = 1.2;\n\nexport const generatePanHandlers = (\n    onStart: (EventType, GestureState) => *,\n    onMove: (EventType, GestureState) => *,\n    onRelease: (EventType, GestureState) => *\n): * =>\n    PanResponder.create({\n        onStartShouldSetPanResponder: (): boolean => true,\n        onStartShouldSetPanResponderCapture: (): boolean => true,\n        onMoveShouldSetPanResponder: (): boolean => true,\n        onMoveShouldSetPanResponderCapture: (): boolean => true,\n        onPanResponderGrant: onStart,\n        onPanResponderMove: onMove,\n        onPanResponderRelease: onRelease,\n        onPanResponderTerminate: onRelease,\n        onPanResponderTerminationRequest: (): void => {},\n        onShouldBlockNativeResponder: () => false,\n    });\n\nexport const getScale = (\n    currentDistance: number,\n    initialDistance: number\n): number => (currentDistance / initialDistance) * SCALE_MULTIPLIER;\n\nexport const getDistance = (touches: Array<TouchType>): number => {\n    const [a, b] = touches;\n\n    if (a == null || b == null) {\n        return 0;\n    }\n\n    return Math.sqrt(\n        Math.pow(a.pageX - b.pageX, 2) + Math.pow(a.pageY - b.pageY, 2)\n    );\n};\n\nexport const calculateInitialScale = (\n    imageWidth: number = 0,\n    imageHeight: number = 0,\n    {screenWidth, screenHeight}: ScreenDimensionsType\n): number => {\n    const screenRatio = screenHeight / screenWidth;\n    const imageRatio = imageHeight / imageWidth;\n\n    if (imageWidth > screenWidth || imageHeight > screenHeight) {\n        if (screenRatio > imageRatio) {\n            return screenWidth / imageWidth;\n        }\n\n        return screenHeight / imageHeight;\n    }\n\n    return 1;\n};\n\nexport const calculateInitialTranslate = (\n    imageWidth: number = 0,\n    imageHeight: number = 0,\n    {screenWidth, screenHeight}: ScreenDimensionsType\n): TranslateType => {\n    const getTranslate = (axis: string): number => {\n        const imageSize = axis === 'x' ? imageWidth : imageHeight;\n        const screenSize = axis === 'x' ? screenWidth : screenHeight;\n\n        if (imageWidth >= imageHeight) {\n            return (screenSize - imageSize) / 2;\n        }\n\n        return screenSize / 2 - imageSize / 2;\n    };\n\n    return {\n        x: getTranslate('x'),\n        y: getTranslate('y'),\n    };\n};\n\nexport const getInitialParams = (\n    {width, height}: DimensionsType,\n    screenDimensions: Object\n): TransitionType => ({\n    scale: calculateInitialScale(width, height, screenDimensions),\n    translate: calculateInitialTranslate(width, height, screenDimensions),\n});\n\nexport function fetchImageSize(images: Array<ImageType> = []) {\n    return images.reduce((acc, image) => {\n        if (\n            image.source &&\n            image.source.uri &&\n            (!image.width || !image.height)\n        ) {\n            const imageSize = new Promise((resolve, reject) => {\n                Image.getSize(\n                    image.source.uri,\n                    (width, height) =>\n                        resolve({\n                            width,\n                            height,\n                            index: image.index,\n                        }),\n                    reject\n                );\n            });\n\n            acc.push(imageSize);\n        }\n\n        return acc;\n    }, []);\n}\n\nconst shortHexRegex = /^#?([a-f\\d])([a-f\\d])([a-f\\d])$/i;\nconst fullHexRegex = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i;\n\nexport const isHex = (color: string): boolean =>\n    fullHexRegex.test(color) || shortHexRegex.test(color);\n\nexport const hexToRgb = (hex: string): number[] => {\n    // Expand shorthand form (e.g. \"03F\") to full form (e.g. \"0033FF\")\n    const input = hex.replace(\n        shortHexRegex,\n        (m, r, g, b) => `${r}${r}${g}${g}${b}${b}`\n    );\n\n    const [match, r, g, b] = [].concat(fullHexRegex.exec(input));\n\n    if (!match) {\n        return [];\n    }\n\n    return [parseInt(r, 16), parseInt(g, 16), parseInt(b, 16)];\n};\n\nexport const addIndexesToImages = (images: ImageType[]): ImageType[] =>\n    images.map((image, index) => ({...image, index}));\n\nexport const getImagesWithoutSize = (images: ImageType[]) =>\n    images.filter(({width, height}) => !width || !height);\n\nexport const scalesAreEqual = (scaleA: number, scaleB: number): boolean =>\n    Math.abs(scaleA - scaleB) < SCALE_EPSILON;\n"
  }
]