master 2450ec459839 cached
24 files
55.1 KB
13.5k tokens
20 symbols
1 requests
Download .txt
Repository: antonKalinin/react-native-image-view
Branch: master
Commit: 2450ec459839
Files: 24
Total size: 55.1 KB

Directory structure:
gitextract_k_se4hxv/

├── .eslintrc
├── .flowconfig
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── example/
│   ├── .eslintrc
│   ├── .flowconfig
│   ├── .gitignore
│   ├── .watchmanconfig
│   ├── App.js
│   ├── app.json
│   ├── babel.config.js
│   ├── metro.config.js
│   └── package.json
├── package.json
└── src/
    ├── ImageView.js
    ├── controls/
    │   ├── Close.js
    │   ├── Next.js
    │   ├── Prev.js
    │   └── index.js
    ├── styles.js
    ├── types.js
    └── utils.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc
================================================
{
  "parser": "babel-eslint",
  "extends": [
    "airbnb",
    "plugin:flowtype/recommended",
    "plugin:jest/recommended",
    "prettier",
    "prettier/react"
  ],
  "plugins": [
    "flowtype",
    "jest",
    "prettier"
  ],
  "rules": {
    "prettier/prettier": "error",
    "react/jsx-filename-extension": [
      1,
      {
        "extensions": [
          ".js",
          ".jsx"
        ]
      }
    ],
    "import/extensions": "off",
    "import/no-unresolved": "off",
    "react/require-default-props": "off",
    "react/destructuring-assignment": "off",
    "global-require": "off",
    "no-restricted-properties": "off",
    "no-unused-vars": "error",
    "import/no-extraneous-dependencies": "error",
    "flowtype/no-weak-types": [
      1,
      {
        "any": true,
        "Function": true,
        "Object": false
      }
    ]
  },
  "env": {
    "es6": true
  }
}

================================================
FILE: .flowconfig
================================================
[ignore]
; We fork some components by platform
.*/*[.]android.js

; Ignore "BUCK" generated dirs
<PROJECT_ROOT>/\.buckd/

; Ignore unexpected extra "@providesModule"
.*/node_modules/.*/node_modules/fbjs/.*

; Ignore duplicate module providers
; For RN Apps installed via npm, "Libraries" folder is inside
; "node_modules/react-native" but in the source repo it is in the root
.*/Libraries/react-native/React.js

; Ignore polyfills
.*/Libraries/polyfills/.*

; Ignore metro
.*/node_modules/metro/.*

[include]

[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
node_modules/react-native/flow-github/

[options]
emoji=true

module.system=haste

munge_underscores=true

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'

module.file_ext=.js
module.file_ext=.jsx
module.file_ext=.json
module.file_ext=.native.js

suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState

suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError

[version]
^0.71.0

================================================
FILE: .gitignore
================================================
node_modules/
.idea
.expo

================================================
FILE: .prettierrc
================================================
{
  "bracketSpacing": false,
  "parser": "flow",
  "printWidth": 80,
  "singleQuote": true,
  "tabWidth": 4,
  "trailingComma": "es5"
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2017 Anton Kalinin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
## 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!

[![npm version](https://badge.fury.io/js/react-native-image-view.svg)](https://badge.fury.io/js/react-native-image-view)

React Native modal image view with pinch zoom and carousel.

Try with expo: https://expo.io/@antonkalinin/react-native-image-view

#### Warning: Breaking changes since v2.0.0:

- instead of prop `source` => `images`
- no title prop for footer, please use `renderFooter` instead

## Installation

```bash
yarn add react-native-image-view
```

or

```bash
npm install --save react-native-image-view
```

## Demo

<p align="center">
  <img src="https://raw.githubusercontent.com/antonKalinin/react-native-image-view/master/static/demoV2.gif" height="400" />
</p>

## Usage
```jsx
import ImageView from 'react-native-image-view';

const images = [
    {
        source: {
            uri: 'https://cdn.pixabay.com/photo/2017/08/17/10/47/paris-2650808_960_720.jpg',
        },
        title: 'Paris',
        width: 806,
        height: 720,
    },
];

<ImageView
    images={images}
    imageIndex={0}
    isVisible={this.state.isImageViewVisible}
    renderFooter={(currentImage) => (<View><Text>My footer</Text></View>)}
/>
```

#### [See example for better understanding](https://github.com/antonKalinin/react-native-image-view/blob/master/example/App.js)

## Props

Prop name           | Description   | Type      | Default value | Platform |
--------------------|---------------|-----------|---------------|----------|
`animationType` | Type of animation modal presented with | "none", "fade", "slide" | "none" |
`backgroundColor` | Background color of the modal in HEX (#0099CC) | string | null |
`controls` | Config of available controls (see below) | Object | {close: true} |
`glideAlways`  | Emulates ScrollView glide animation if built-in was not triggered  | boolean | false | Android
`glideAlwaysDelay`  | Defines delay in milliseconds for glideAlways  | number | 75 | Android
`images` | Array of images to display, see below image item description | array | [] |
`imageIndex` | Current index of image to display | number | 0 |
`isVisible` | Is modal shown or not | boolean | false |
`isTapZoomEnabled` | Zoom image when double tapped | boolean | true |
`isPinchZoomEnabled` | Zoom image with pinch gesture | boolean | true |
`isSwipeCloseEnabled` | Close modal with swipe up or down | boolean | true |
`onClose` | Function called on modal closed | function | none |
`onImageChange` | Function called when image is changed | function | none |
`renderFooter` | Function returns a footer element | function | none |

#### Image item:

```js
{
  source: any, // Image Component source object
  width: ?number, // Width of full screen image (optional but recommended)
  height: ?number, // Height of full screen image (optional but recommended)
  // any other props you need to render your footer
}
```

It'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.

#### controls prop:

```js
type ControlType = React.Component<{onPress: () => void}> | null | boolean,

{
  close: ControlType // Component for close button in up right corner, as onPress prop accepts function to close modal
  next: ControlType, // Component for next image button, as onPress prop accepts function to scroll to next image
  prev: ControlType, // Component for previous image button, as onPress prop accepts function to scroll to previous image
}
```

To use default components just set `{next: true, prev: true}`, close is showing by default. To create custom controls check src/controls.

### License
  [MIT](LICENSE)


================================================
FILE: example/.eslintrc
================================================
{
    "extends": [
      "airbnb",
      "prettier",
      "prettier/flowtype",
      "prettier/react"
    ],
    "rules": {
        "indent": ["error", 4],
        "comma-dangle": ["error", {
            "arrays": "always-multiline",
            "objects": "always-multiline",
            "imports": "always-multiline",
            "exports": "always-multiline",
            "functions": "ignore"
        }],
        "no-console": 0,
        "no-restricted-properties": 0,
        "object-curly-spacing": ["error", "never"],
        "prettier/prettier": "error",
        "react/prop-types": ["error", {"customValidators": ["skipUndeclared"]}],
        "react/jsx-indent-props": ["error", 4],
        "react/jsx-indent": ["error", 4],
        "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }]
    },
    "root": true,
    "plugins": [
        "react",
        "flowtype",
        "prettier"
    ],
    "settings": {
        "flowtype": {
            "onlyFilesWithFlowAnnotation": true
        }
    },
    "parser": "babel-eslint",
    "parserOptions": {
        "ecmaFeatures": {
            "experimentalObjectRestSpread": true
        }
    }
}


================================================
FILE: example/.flowconfig
================================================
[ignore]
; We fork some components by platform
.*/*[.]android.js

; Ignore "BUCK" generated dirs
<PROJECT_ROOT>/\.buckd/

; Ignore unexpected extra "@providesModule"
.*/node_modules/.*/node_modules/fbjs/.*

; Ignore duplicate module providers
; For RN Apps installed via npm, "Libraries" folder is inside
; "node_modules/react-native" but in the source repo it is in the root
.*/Libraries/react-native/React.js

; Ignore polyfills
.*/Libraries/polyfills/.*

; Ignore metro
.*/node_modules/metro/.*

[include]

[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
node_modules/react-native/flow-github/

[options]
emoji=true

module.system=haste

munge_underscores=true

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'

module.file_ext=.js
module.file_ext=.jsx
module.file_ext=.json
module.file_ext=.native.js

suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState

suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError

[version]
^0.71.0

================================================
FILE: example/.gitignore
================================================
node_modules/**/*
.expo/*
npm-debug.*
yarn.lock
ImageView.js
types.js


================================================
FILE: example/.watchmanconfig
================================================
{}


================================================
FILE: example/App.js
================================================
import React, {Component} from 'react';
import {
    Text,
    View,
    Image,
    TouchableOpacity,
    StyleSheet,
    Dimensions,
    Platform,
} from 'react-native';

// import ImageView from '../src/ImageView';
import ImageView from 'react-native-image-view';

const {width} = Dimensions.get('window');

const cities = [
    {
        source: {
            uri:
                'https://avatars.mds.yandex.net/get-pdb/49816/d9152cc6-bf48-4e44-b2d5-de73b2e94454/s800',
        },
        title: 'London',
    },
    {
        // eslint-disable-next-line
        source: require('./assets/spb.jpg'),
        title: 'St-Petersburg',
        width: 1200,
        height: 800,
    },
    {
        source: {
            uri:
                'https://cdn.pixabay.com/photo/2017/08/17/10/47/paris-2650808_960_720.jpg',
        },
        title: 'Paris',
        width: 806,
        height: 720,
    },
];

const nature = [
    {
        source: {
            uri:
                'https://images.fineartamerica.com/images/artworkimages/mediumlarge/1/1-forest-in-fog-russian-nature-forest-mist-dmitry-ilyshev.jpg',
        },
        title: 'Switzerland',
    },

    {
        source: {
            uri:
                'https://i.pinimg.com/564x/a5/1b/63/a51b63c13c7c41fa333b302fc7938f06.jpg',
        },
        title: 'USA',
        width: 400,
        height: 800,
    },
    {
        source: {
            uri:
                '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',
        },
        title: 'Iceland',
        width: 880,
        height: 590,
    },
];

const tabs = [
    {title: 'Cities', images: cities},
    {title: 'Nature', images: nature},
];

const styles = StyleSheet.create({
    container: {
        flex: 1,
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: '#000',
        paddingTop: Platform.select({ios: 0, android: 10}),
    },
    tabs: {
        flexDirection: 'row',
    },
    tab: {
        flex: 1,
        height: 30,
        alignItems: 'center',
        justifyContent: 'flex-end',
    },
    tabTitle: {
        color: '#EEE',
    },
    tabTitleActive: {
        fontWeight: '700',
        color: '#FFF',
    },
    footer: {
        width,
        height: 50,
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'rgba(0, 0, 0, 0.4)',
        paddingHorizontal: 10,
        paddingVertical: 5,
    },
    footerButton: {
        flexDirection: 'row',
        marginLeft: 15,
    },
    footerText: {
        fontSize: 16,
        color: '#FFF',
        textAlign: 'center',
    },
});

export default class App extends Component {
    constructor(props) {
        super(props);

        this.state = {
            activeTab: 0,
            imageIndex: 0,
            isImageViewVisible: false,
            likes: [...cities, ...nature].reduce((acc, image) => {
                acc[image.title] = 0;

                return acc;
            }, {}),
        };

        this.renderFooter = this.renderFooter.bind(this);
    }

    renderFooter({title}) {
        const {likes} = this.state;

        return (
            <View style={styles.footer}>
                <Text style={styles.footerText}>{title}</Text>
                <TouchableOpacity
                    style={styles.footerButton}
                    onPress={() => {
                        const imageLikes = likes[title] + 1;
                        this.setState({likes: {...likes, [title]: imageLikes}});
                    }}
                >
                    <Text style={styles.footerText}>♥</Text>
                    <Text style={[styles.footerText, {marginLeft: 7}]}>
                        {likes[title]}
                    </Text>
                </TouchableOpacity>
            </View>
        );
    }

    render() {
        const {isImageViewVisible, activeTab, imageIndex} = this.state;
        const images = tabs[activeTab].images || [];

        return (
            <View style={styles.container}>
                <View>
                    {images.map((image, index) => (
                        <TouchableOpacity
                            key={image.title}
                            onPress={() => {
                                this.setState({
                                    imageIndex: index,
                                    isImageViewVisible: true,
                                });
                            }}
                        >
                            <Image
                                style={{width, height: 200}}
                                source={image.source}
                                resizeMode="cover"
                            />
                        </TouchableOpacity>
                    ))}
                </View>
                <View style={styles.tabs}>
                    {tabs.map(({title}, index) => (
                        <TouchableOpacity
                            style={styles.tab}
                            key={title}
                            onPress={() => {
                                this.setState({
                                    activeTab: index,
                                });
                            }}
                        >
                            <Text
                                style={[
                                    styles.tabTitle,
                                    index === activeTab &&
                                        styles.tabTitleActive,
                                ]}
                            >
                                {title}
                            </Text>
                        </TouchableOpacity>
                    ))}
                </View>
                <ImageView
                    glideAlways
                    images={images}
                    imageIndex={imageIndex}
                    animationType="fade"
                    isVisible={isImageViewVisible}
                    renderFooter={this.renderFooter}
                    onClose={() => this.setState({isImageViewVisible: false})}
                    onImageChange={index => {
                        console.log(index);
                    }}
                />
            </View>
        );
    }
}


================================================
FILE: example/app.json
================================================
{
  "expo": {
    "name": "react-native-image-view",
    "description": "React Native modal image view with pinch zoom",
    "slug": "react-native-image-view",
    "privacy": "public",
    "sdkVersion": "35.0.0",
    "version": "1.0.0",
    "orientation": "portrait",
    "primaryColor": "#cccccc",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "ios": {
      "supportsTablet": true
    }
  }
}


================================================
FILE: example/babel.config.js
================================================
module.exports = function(api) {
    api.cache(true);
    return {
        presets: ['babel-preset-expo'],
    };
};


================================================
FILE: example/metro.config.js
================================================
const path = require('path');

module.exports = {
    resolver: {
        extraNodeModules: new Proxy(
            {},
            {
                get: (target, name) =>
                    path.join(process.cwd(), `node_modules/${name}`),
            }
        ),
    },
    projectRoot: [path.resolve(__dirname)],
    watchFolders: [path.resolve(__dirname, '../src')],
};


================================================
FILE: example/package.json
================================================
{
  "main": "node_modules/expo/AppEntry.js",
  "private": true,
  "dependencies": {
    "expo": "^35.0.0",
    "react": "16.8.6",
    "react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz",
    "react-native-image-view": "^2.1.6"
  },
  "devDependencies": {
    "babel-eslint": "10.0.1",
    "babel-preset-expo": "7.0.0",
    "eslint": "5.16.0",
    "eslint-config-airbnb": "17.1.0",
    "eslint-config-prettier": "4.3.0",
    "eslint-plugin-flowtype": "3.9.1",
    "eslint-plugin-import": "2.17.3",
    "eslint-plugin-jsx-a11y": "6.2.1",
    "eslint-plugin-prettier": "3.1.0",
    "eslint-plugin-react": "7.13.0",
    "flow-bin": "0.100.0",
    "prettier": "1.18.2"
  },
  "scripts": {
    "eslint-check": "eslint --print-config .eslintrc.js | eslint-config-prettier-check"
  }
}


================================================
FILE: package.json
================================================
{
  "name": "react-native-image-view",
  "version": "2.1.9",
  "description": "React Native modal image view with pinch zoom",
  "main": "src/ImageView",
  "scripts": {
    "flow": "flow",
    "eslint-check": "eslint --print-config .eslintrc.js | eslint-config-prettier-check",
    "postversion": "git push origin master && git push --tags origin master"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/antonKalinin/react-native-image-view.git"
  },
  "peerDependencies": {
    "react": ">=15.2.0",
    "react-native": ">=0.44.0"
  },
  "devDependencies": {
    "babel-eslint": "10.0.1",
    "eslint": "5.16.0",
    "eslint-config-airbnb": "17.1.0",
    "eslint-config-prettier": "4.3.0",
    "eslint-plugin-flowtype": "3.9.1",
    "eslint-plugin-import": "2.17.3",
    "eslint-plugin-jest": "^21.17.0",
    "eslint-plugin-jsx-a11y": "6.2.1",
    "eslint-plugin-prettier": "3.1.0",
    "eslint-plugin-react": "7.13.0",
    "flow-bin": "0.100.0",
    "prettier": "1.18.2"
  },
  "keywords": [
    "react-native",
    "image",
    "zoom",
    "preview",
    "modal",
    "pinch",
    "component"
  ],
  "files": [
    "package.json",
    "readme.md",
    "src"
  ],
  "author": "Anton Kalinin",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/antonKalinin/react-native-image-view/issues"
  },
  "homepage": "https://github.com/antonKalinin/react-native-image-view#readme"
}


================================================
FILE: src/ImageView.js
================================================
// @flow

import React, {Component, type Node, type ComponentType} from 'react';
import {
    ActivityIndicator,
    Animated,
    Dimensions,
    FlatList,
    Modal,
    Platform,
    View,
    SafeAreaView,
} from 'react-native';

import {
    type ControlType,
    type ControlsType,
    type DimensionsType,
    type EventType,
    type ImageType,
    type ImageSizeType,
    type GestureState,
    type NativeEventType,
    type TouchType,
    type TransitionType,
    type TranslateType,
} from './types';

import {
    addIndexesToImages,
    calculateInitialTranslate,
    fetchImageSize,
    generatePanHandlers,
    getImagesWithoutSize,
    getScale,
    getDistance,
    getInitialParams,
    hexToRgb,
    isHex,
    scalesAreEqual,
} from './utils';

import createStyles from './styles';
import {Close, Prev, Next} from './controls';

const IMAGE_SPEED_FOR_CLOSE = 1.1;
const SCALE_MAXIMUM = 5;
const HEADER_HEIGHT = 60;
const SCALE_MAX_MULTIPLIER = 3;
const FREEZE_SCROLL_DISTANCE = 15;
const BACKGROUND_OPACITY_MULTIPLIER = 0.003;
const defaultBackgroundColor = [0, 0, 0];

const getScreenDimensions = () => ({
    screenWidth: Dimensions.get('window').width,
    screenHeight: Dimensions.get('window').height,
});

let styles = createStyles(getScreenDimensions());

type PropsType = {
    animationType: 'none' | 'fade' | 'slide',
    backgroundColor?: string,
    glideAlways?: boolean,
    glideAlwaysDelay?: number,
    images: ImageType[],
    imageIndex: number,
    isVisible: boolean,
    isTapZoomEnabled: boolean,
    isPinchZoomEnabled: boolean,
    isSwipeCloseEnabled: boolean,
    onClose: () => {},
    onImageChange: number => {},
    renderFooter: ImageType => {},
    controls: {
        close?: ComponentType<ControlType> | boolean,
        next?: ComponentType<ControlType> | boolean,
        prev?: ComponentType<ControlType> | boolean,
    },
};

export type StateType = {
    images: ImageType[],
    isVisible: boolean,
    imageIndex: number,
    imageScale: number,
    imageTranslate: {x: number, y: number},
    scrollEnabled: boolean,
    panelsVisible: boolean,
    isFlatListRerendered: boolean,
    screenDimensions: {screenWidth: number, screenHeight: number},
};

export default class ImageView extends Component<PropsType, StateType> {
    static defaultProps = {
        backgroundColor: null,
        images: [],
        imageIndex: 0,
        isTapZoomEnabled: true,
        isPinchZoomEnabled: true,
        isSwipeCloseEnabled: true,
        glideAlways: false,
        glideAlwaysDelay: 75,
        controls: {prev: null, next: null},
    };

    constructor(props: PropsType) {
        super(props);

        // calculate initial scale and translate for images
        const initialScreenDimensions = getScreenDimensions();
        this.imageInitialParams = props.images.map(image =>
            getInitialParams(image, initialScreenDimensions)
        );

        this.state = {
            images: props.images,
            isVisible: props.isVisible,
            imageIndex: props.imageIndex,
            imageScale: 1,
            imageTranslate: {x: 0, y: 0},
            scrollEnabled: true,
            panelsVisible: true,
            isFlatListRerendered: false,
            screenDimensions: initialScreenDimensions,
        };
        this.glideAlwaysTimer = null;
        this.listRef = null;
        this.isScrolling = false;
        this.footerHeight = 0;
        this.initialTouches = [];
        this.currentTouchesNum = 0;
        this.doubleTapTimer = null;
        this.modalAnimation = new Animated.Value(0);
        this.modalBackgroundOpacity = new Animated.Value(0);

        this.headerTranslateValue = new Animated.ValueXY();
        this.footerTranslateValue = new Animated.ValueXY();

        this.imageScaleValue = new Animated.Value(this.getInitialScale());
        const {x, y} = this.getInitialTranslate();
        this.imageTranslateValue = new Animated.ValueXY({x, y});

        this.panResponder = generatePanHandlers(
            (event: EventType): void => this.onGestureStart(event.nativeEvent),
            (event: EventType, gestureState: GestureState): void =>
                this.onGestureMove(event.nativeEvent, gestureState),
            (event: EventType, gestureState: GestureState): void =>
                this.onGestureRelease(event.nativeEvent, gestureState)
        );

        const imagesWithoutSize = getImagesWithoutSize(
            addIndexesToImages(props.images)
        );

        if (imagesWithoutSize.length) {
            Promise.all(fetchImageSize(imagesWithoutSize)).then(
                this.setSizeForImages
            );
        }
    }

    componentDidMount() {
        styles = createStyles(this.state.screenDimensions);
        Dimensions.addEventListener('change', this.onChangeDimension);
    }

    componentDidUpdate() {
        const {images, imageIndex, isVisible} = this.state;

        if (
            typeof this.props.isVisible !== 'undefined' &&
            this.props.isVisible !== isVisible
        ) {
            this.onNextImagesReceived(this.props.images, this.props.imageIndex);

            if (
                images !== this.props.images ||
                imageIndex !== this.props.imageIndex
            ) {
                const imagesWithoutSize = getImagesWithoutSize(
                    addIndexesToImages(this.props.images)
                );

                if (imagesWithoutSize.length) {
                    Promise.all(fetchImageSize(imagesWithoutSize)).then(
                        updatedImages =>
                            this.onNextImagesReceived(
                                this.setSizeForImages(updatedImages),
                                this.props.imageIndex
                            )
                    );
                }
            }

            this.setState({
                isVisible: this.props.isVisible,
                isFlatListRerendered: false,
            });

            this.modalBackgroundOpacity.setValue(0);

            if (this.props.isVisible) {
                Animated.timing(this.modalAnimation, {
                    duration: 400,
                    toValue: 1,
                }).start();
            }
        }
    }

    componentWillUnmount() {
        Dimensions.removeEventListener('change', this.onChangeDimension);

        if (this.glideAlwaysTimer) {
            clearTimeout(this.glideAlwaysTimer);
        }
    }

    onChangeDimension = ({window}: {window: DimensionsType}) => {
        const screenDimensions = {
            screenWidth: window.width,
            screenHeight: window.height,
        };

        this.setState({screenDimensions});
        styles = createStyles(screenDimensions);

        this.onNextImagesReceived(this.props.images, this.state.imageIndex);
    };

    onNextImagesReceived(images: Array<ImageType>, imageIndex: number = 0) {
        this.imageInitialParams = images.map(image =>
            getInitialParams(image, this.state.screenDimensions)
        );
        const {scale, translate} = this.imageInitialParams[imageIndex] || {
            scale: 1,
            translate: {},
        };

        this.setState({
            images,
            imageIndex,
            imageScale: scale,
            imageTranslate: translate,
            isFlatListRerendered: false,
        });

        this.imageScaleValue.setValue(scale);
        this.imageTranslateValue.setValue(translate);
    }

    // $FlowFixMe
    onFlatListRender = flatListRef => {
        const {images, imageIndex, isFlatListRerendered} = this.state;

        if (flatListRef && !isFlatListRerendered) {
            this.listRef = flatListRef;
            this.setState({
                isFlatListRerendered: true,
            });

            // Fix for android https://github.com/facebook/react-native/issues/13202
            if (images.length > 0) {
                const nextTick = new Promise(resolve => setTimeout(resolve, 0));
                nextTick.then(() => {
                    flatListRef.scrollToIndex({
                        index: imageIndex,
                        animated: false,
                    });
                });
            }
        }
    };

    onNextImage = (event: EventType) => {
        const {imageIndex} = this.state;
        const {x} = event.nativeEvent.contentOffset || {x: 0};

        const nextImageIndex = Math.round(
            x / this.state.screenDimensions.screenWidth
        );

        this.isScrolling =
            Math.ceil(x) % this.state.screenDimensions.screenWidth > 10;

        if (imageIndex !== nextImageIndex && nextImageIndex >= 0) {
            const nextImageScale = this.getInitialScale(nextImageIndex);
            const nextImageTranslate = this.getInitialTranslate(nextImageIndex);

            this.setState({
                imageIndex: nextImageIndex,
                imageScale: nextImageScale,
                imageTranslate: nextImageTranslate,
            });

            this.imageScaleValue.setValue(nextImageScale);
            this.imageTranslateValue.setValue(nextImageTranslate);

            if (typeof this.props.onImageChange === 'function') {
                this.props.onImageChange(nextImageIndex);
            }
        }
    };

    onGestureStart(event: NativeEventType) {
        this.initialTouches = event.touches;
        this.currentTouchesNum = event.touches.length;
    }

    /**
     * If image is moved from its original position
     * then disable scroll (for ScrollView)
     */
    onGestureMove(event: NativeEventType, gestureState: GestureState) {
        if (this.isScrolling && this.state.scrollEnabled) {
            return;
        }

        if (this.currentTouchesNum === 1 && event.touches.length === 2) {
            this.initialTouches = event.touches;
        }

        const {isSwipeCloseEnabled, isPinchZoomEnabled} = this.props;

        const {
            images,
            imageIndex,
            imageScale,
            imageTranslate,
            screenDimensions,
        } = this.state;
        const {screenHeight} = screenDimensions;
        const {touches} = event;
        const {x, y} = imageTranslate;
        const {dx, dy} = gestureState;
        const imageInitialScale = this.getInitialScale();
        const {height} = images[imageIndex];

        if (imageScale !== imageInitialScale) {
            this.imageTranslateValue.x.setValue(x + dx);
        }

        // Do not allow to move image vertically until it fits to the screen
        if (imageScale * height > screenHeight) {
            this.imageTranslateValue.y.setValue(y + dy);
        }

        // if image not scaled and fits to the screen
        if (
            isSwipeCloseEnabled &&
            scalesAreEqual(imageScale, imageInitialScale) &&
            height * imageInitialScale < screenHeight
        ) {
            const backgroundOpacity = Math.abs(
                dy * BACKGROUND_OPACITY_MULTIPLIER
            );

            this.imageTranslateValue.y.setValue(y + dy);
            this.modalBackgroundOpacity.setValue(
                backgroundOpacity > 1 ? 1 : backgroundOpacity
            );
        }

        const currentDistance = getDistance(touches);
        const initialDistance = getDistance(this.initialTouches);

        const scrollEnabled = Math.abs(dy) < FREEZE_SCROLL_DISTANCE;
        this.setState({scrollEnabled});

        if (!initialDistance) {
            return;
        }

        if (!isPinchZoomEnabled || touches.length < 2) {
            return;
        }

        let nextScale = getScale(currentDistance, initialDistance) * imageScale;

        if (nextScale < imageInitialScale) {
            nextScale = imageInitialScale;
        } else if (nextScale > SCALE_MAXIMUM) {
            nextScale = SCALE_MAXIMUM;
        }

        this.imageScaleValue.setValue(nextScale);
        this.currentTouchesNum = event.touches.length;
    }

    onGestureRelease(event: NativeEventType, gestureState: GestureState) {
        if (this.glideAlwaysTimer) {
            clearTimeout(this.glideAlwaysTimer);
        }

        if (this.props.glideAlways && Platform.OS === 'android') {
            this.glideAlwaysTimer = setTimeout(() => {
                this.glideAlwaysTimer = null;
                // If standard glide is not triggered then emulate it
                // $FlowFixMe
                if (this.listRef && this.listRef.scrollToIndex) {
                    this.listRef.scrollToIndex({
                        index: this.state.imageIndex,
                        animated: true,
                    });
                }
            }, this.props.glideAlwaysDelay);
        }

        if (this.isScrolling) {
            return;
        }

        const {imageScale} = this.state;
        const {isSwipeCloseEnabled, isTapZoomEnabled} = this.props;

        let {_value: scale} = this.imageScaleValue;
        const {_value: modalBackgroundOpacity} = this.modalBackgroundOpacity;

        const {dx, dy, vy} = gestureState;
        const imageInitialScale = this.getInitialScale();
        const imageInitialTranslate = this.getInitialTranslate();

        // Position haven't changed, so it just tap
        if (event && !dx && !dy && scalesAreEqual(imageScale, scale)) {
            // Double tap timer is launched, its double tap

            if (isTapZoomEnabled && this.doubleTapTimer) {
                clearTimeout(this.doubleTapTimer);
                this.doubleTapTimer = null;

                scale = scalesAreEqual(imageInitialScale, scale)
                    ? scale * SCALE_MAX_MULTIPLIER
                    : imageInitialScale;

                Animated.timing(this.imageScaleValue, {
                    toValue: scale,
                    duration: 300,
                }).start();

                this.togglePanels(scale === imageInitialScale);
            } else {
                this.doubleTapTimer = setTimeout(() => {
                    this.togglePanels();
                    this.doubleTapTimer = null;
                }, 200);
            }
        }

        const {x, y} = this.calculateNextTranslate(dx, dy, scale);
        const scrollEnabled =
            scale === this.getInitialScale() &&
            x === imageInitialTranslate.x &&
            y === imageInitialTranslate.y;

        Animated.parallel(
            [
                modalBackgroundOpacity > 0
                    ? Animated.timing(this.modalBackgroundOpacity, {
                          toValue: 0,
                          duration: 100,
                      })
                    : null,
                Animated.timing(this.imageTranslateValue.x, {
                    toValue: x,
                    duration: 100,
                }),
                Animated.timing(this.imageTranslateValue.y, {
                    toValue: y,
                    duration: 100,
                }),
            ].filter(Boolean)
        ).start();

        // Close modal with animation if image not scaled and high vertical gesture speed
        if (
            isSwipeCloseEnabled &&
            scale === imageInitialScale &&
            Math.abs(vy) >= IMAGE_SPEED_FOR_CLOSE
        ) {
            Animated.timing(this.imageTranslateValue.y, {
                toValue: y + 400 * vy,
                duration: 150,
            }).start(this.close);
        }

        this.setState({
            imageScale: scale,
            imageTranslate: {x, y},
            scrollEnabled,
        });
    }

    onImageLoaded(index: number) {
        const {images} = this.state;

        images[index] = {...images[index], loaded: true};

        this.setState({images});
    }

    onMomentumScrollBegin = () => {
        this.isScrolling = true;
        if (this.glideAlwaysTimer) {
            // If FlatList started gliding then prevent glideAlways scrolling
            clearTimeout(this.glideAlwaysTimer);
        }
    };

    onMomentumScrollEnd = () => {
        this.isScrolling = false;
    };

    getItemLayout = (_: *, index: number): Object => {
        const {screenWidth} = this.state.screenDimensions;

        return {length: screenWidth, offset: screenWidth * index, index};
    };

    getInitialScale(index?: number): number {
        const imageIndex = index !== undefined ? index : this.state.imageIndex;
        const imageParams = this.imageInitialParams[imageIndex];

        return imageParams ? imageParams.scale : 1;
    }

    getInitialTranslate(index?: number): TranslateType {
        const imageIndex = index !== undefined ? index : this.state.imageIndex;
        const imageParams = this.imageInitialParams[imageIndex];

        return imageParams ? imageParams.translate : {x: 0, y: 0};
    }

    getImageStyle(
        image: ImageType,
        index: number
    ): {width?: number, height?: number, transform?: any, opacity?: number} {
        const {imageIndex, screenDimensions} = this.state;
        const {width, height} = image;

        if (!width || !height) {
            return {opacity: 0};
        }

        // very strange caching, fix it with changing size to 1 pixel
        const {x, y} = calculateInitialTranslate(
            width,
            height + 1,
            screenDimensions
        );
        const translateValue = new Animated.ValueXY({x, y});

        const transform =
            index === imageIndex
                ? this.imageTranslateValue.getTranslateTransform()
                : translateValue.getTranslateTransform();

        const scale =
            index === imageIndex
                ? this.imageScaleValue
                : this.getInitialScale(index);
        // $FlowFixMe
        transform.push({scale});

        return {width, height, transform};
    }

    getControls = (): ControlsType => {
        const {close, prev, next} = this.props.controls;
        const controls = {close: Close, prev: undefined, next: undefined};

        if (close === null) {
            controls.close = null;
        }

        if (close) {
            controls.close = close === true ? Close : close;
        }

        if (prev) {
            controls.prev = prev === true ? Prev : prev;
        }

        if (next) {
            controls.next = next === true ? Next : next;
        }

        return controls;
    };

    setSizeForImages = (nextImages: Array<ImageSizeType>): Array<ImageType> => {
        if (nextImages.length === 0) {
            return [];
        }

        const {images} = this.state;

        return images.map((image, index) => {
            const nextImageSize = nextImages.find(
                nextImage => nextImage.index === index
            );

            /* eslint-disable */
            if (nextImageSize) {
                image.width = nextImageSize.width;
                image.height = nextImageSize.height;
            }
            /* eslint-enable */

            return image;
        });
    };

    scrollToNext = () => {
        if (this.listRef && typeof this.listRef.scrollToIndex === 'function') {
            this.listRef.scrollToIndex({
                index: this.state.imageIndex + 1,
                animated: true,
            });
        }
    };

    scrollToPrev = () => {
        if (this.listRef && typeof this.listRef.scrollToIndex === 'function') {
            this.listRef.scrollToIndex({
                index: this.state.imageIndex - 1,
                animated: true,
            });
        }
    };

    imageInitialParams: TransitionType[];
    glideAlwaysTimer: ?TimeoutID;
    listRef: *;
    isScrolling: boolean;
    footerHeight: number;
    initialTouches: TouchType[];
    currentTouchesNum: number;
    doubleTapTimer: ?TimeoutID;
    modalAnimation: *;
    modalBackgroundOpacity: *;
    headerTranslateValue: *;
    footerTranslateValue: *;
    imageScaleValue: *;
    imageTranslateValue: *;
    panResponder: *;

    calculateNextTranslate(
        dx: number,
        dy: number,
        scale: number
    ): {x: number, y: number} {
        const {
            images,
            imageIndex,
            imageTranslate,
            screenDimensions,
        } = this.state;
        const {x, y} = imageTranslate;
        const {screenWidth, screenHeight} = screenDimensions;
        const {width, height} = images[imageIndex];
        const imageInitialScale = this.getInitialScale();

        const getTranslate = (axis: string): number => {
            const imageSize = axis === 'x' ? width : height;
            const screenSize = axis === 'x' ? screenWidth : screenHeight;
            const leftLimit = (scale * imageSize - imageSize) / 2;
            const rightLimit = screenSize - imageSize - leftLimit;

            let nextTranslate = axis === 'x' ? x + dx : y + dy;

            // Less than the screen
            if (screenSize > scale * imageSize) {
                if (width >= height) {
                    nextTranslate = (screenSize - imageSize) / 2;
                } else {
                    nextTranslate =
                        screenSize / 2 -
                        (imageSize * (scale / imageInitialScale)) / 2;
                }

                return nextTranslate;
            }

            if (nextTranslate > leftLimit) {
                nextTranslate = leftLimit;
            }

            if (nextTranslate < rightLimit) {
                nextTranslate = rightLimit;
            }

            return nextTranslate;
        };

        return {x: getTranslate('x'), y: getTranslate('y')};
    }

    togglePanels(isVisible?: boolean) {
        const panelsVisible =
            typeof isVisible !== 'undefined'
                ? isVisible
                : !this.state.panelsVisible;
        // toggle footer and header
        this.setState({panelsVisible});

        Animated.timing(this.headerTranslateValue.y, {
            toValue: !panelsVisible ? -(HEADER_HEIGHT + 44) : 0,
            duration: 200,
            useNativeDriver: true,
        }).start();

        if (this.footerHeight > 0) {
            Animated.timing(this.footerTranslateValue.y, {
                toValue: !panelsVisible ? this.footerHeight : 0,
                duration: 200,
                useNativeDriver: true,
            }).start();
        }
    }

    listKeyExtractor = (image: ImageType): string =>
        this.state.images.indexOf(image).toString();

    close = () => {
        this.setState({isVisible: false});

        if (typeof this.props.onClose === 'function') {
            this.props.onClose();
        }
    };

    renderImage = ({item: image, index}: {item: *, index: number}): * => {
        const loaded = image.loaded && image.width && image.height;

        return (
            <View
                style={styles.imageContainer}
                onStartShouldSetResponder={(): boolean => true}
            >
                <Animated.Image
                    resizeMode="cover"
                    source={image.source}
                    style={this.getImageStyle(image, index)}
                    onLoad={(): void => this.onImageLoaded(index)}
                    {...this.panResponder.panHandlers}
                />
                {!loaded && <ActivityIndicator style={styles.loading} />}
            </View>
        );
    };

    render(): Node {
        const {animationType, renderFooter, backgroundColor} = this.props;
        const {
            images,
            imageIndex,
            imageScale,
            isVisible,
            scrollEnabled,
        } = this.state;

        const {close, prev, next} = this.getControls();
        const imageInitialScale = this.getInitialScale();
        const headerTranslate = this.headerTranslateValue.getTranslateTransform();
        const footerTranslate = this.footerTranslateValue.getTranslateTransform();
        const rgbBackgroundColor =
            backgroundColor && isHex(backgroundColor)
                ? hexToRgb(backgroundColor)
                : defaultBackgroundColor;
        const rgb = rgbBackgroundColor.join(',');
        const animatedBackgroundColor = this.modalBackgroundOpacity.interpolate(
            {
                inputRange: [0, 1],
                outputRange: [`rgba(${rgb}, 0.9)`, `rgba(${rgb}, 0.2)`],
            }
        );

        const isPrevVisible =
            imageScale === imageInitialScale && imageIndex > 0;
        const isNextVisible =
            imageScale === imageInitialScale && imageIndex < images.length - 1;

        return (
            <Modal
                transparent
                visible={isVisible}
                animationType={animationType}
                onRequestClose={this.close}
                supportedOrientations={['portrait', 'landscape']}
            >
                <Animated.View
                    style={[
                        {backgroundColor: animatedBackgroundColor},
                        styles.underlay,
                    ]}
                />
                <Animated.View
                    style={[
                        styles.header,
                        {
                            transform: headerTranslate,
                        },
                    ]}
                >
                    <SafeAreaView style={{flex: 1}}>
                        {!!close &&
                            React.createElement(close, {onPress: this.close})}
                    </SafeAreaView>
                </Animated.View>
                <FlatList
                    horizontal
                    pagingEnabled
                    data={images}
                    scrollEnabled={scrollEnabled}
                    scrollEventThrottle={16}
                    style={styles.container}
                    ref={this.onFlatListRender}
                    renderSeparator={() => null}
                    keyExtractor={this.listKeyExtractor}
                    onScroll={this.onNextImage}
                    renderItem={this.renderImage}
                    getItemLayout={this.getItemLayout}
                    onMomentumScrollBegin={this.onMomentumScrollBegin}
                    onMomentumScrollEnd={this.onMomentumScrollEnd}
                />
                {prev &&
                    isPrevVisible &&
                    React.createElement(prev, {onPress: this.scrollToPrev})}
                {next &&
                    isNextVisible &&
                    React.createElement(next, {onPress: this.scrollToNext})}
                {renderFooter && (
                    <Animated.View
                        style={[styles.footer, {transform: footerTranslate}]}
                        onLayout={event => {
                            this.footerHeight = event.nativeEvent.layout.height;
                        }}
                    >
                        {typeof renderFooter === 'function' &&
                            images[imageIndex] &&
                            renderFooter(images[imageIndex])}
                    </Animated.View>
                )}
            </Modal>
        );
    }
}


================================================
FILE: src/controls/Close.js
================================================
// @flow
import React from 'react';
import {StyleSheet, Text, TouchableOpacity} from 'react-native';

const HIT_SLOP = {top: 15, left: 15, right: 15, bottom: 15};

const styles = StyleSheet.create({
    closeButton: {
        alignSelf: 'flex-end',
        height: 24,
        width: 24,
        borderRadius: 12,
        backgroundColor: 'rgba(0,0,0,0.2)',
        alignItems: 'center',
        justifyContent: 'center',
        marginTop: 25,
        marginRight: 15,
    },
    closeButton__text: {
        backgroundColor: 'transparent',
        fontSize: 25,
        lineHeight: 25,
        color: '#FFF',
        textAlign: 'center',
    },
});

export default ({onPress}: {onPress: () => *}) => (
    <TouchableOpacity
        hitSlop={HIT_SLOP}
        style={styles.closeButton}
        onPress={onPress}
    >
        <Text style={styles.closeButton__text}>×</Text>
    </TouchableOpacity>
);


================================================
FILE: src/controls/Next.js
================================================
// @flow
import React from 'react';
import {StyleSheet, Text, TouchableOpacity} from 'react-native';

const HIT_SLOP = {top: 15, left: 15, right: 15, bottom: 15};

const styles = StyleSheet.create({
    nextButton: {
        position: 'absolute',
        zIndex: 100,
        right: 10,
        top: '50%',
        height: 32,
        width: 32,
        borderRadius: 16,
        backgroundColor: 'rgba(0,0,0,0.3)',
        alignItems: 'center',
        justifyContent: 'center',
    },
    nextButton__text: {
        backgroundColor: 'transparent',
        fontSize: 25,
        lineHeight: 25,
        color: '#FFF',
        textAlign: 'center',
    },
});

export default ({onPress}: {onPress: () => *}) => (
    <TouchableOpacity
        hitSlop={HIT_SLOP}
        style={styles.nextButton}
        onPress={onPress}
    >
        <Text style={styles.nextButton__text}>›</Text>
    </TouchableOpacity>
);


================================================
FILE: src/controls/Prev.js
================================================
// @flow
import React from 'react';
import {StyleSheet, Text, TouchableOpacity} from 'react-native';

const HIT_SLOP = {top: 15, left: 15, right: 15, bottom: 15};

const styles = StyleSheet.create({
    prevButton: {
        position: 'absolute',
        zIndex: 100,
        left: 10,
        top: '50%',
        height: 32,
        width: 32,
        borderRadius: 16,
        backgroundColor: 'rgba(0,0,0,0.3)',
        alignItems: 'center',
        justifyContent: 'center',
    },
    prevButton__text: {
        backgroundColor: 'transparent',
        fontSize: 25,
        lineHeight: 25,
        color: '#FFF',
        textAlign: 'center',
    },
});

export default ({onPress}: {onPress: () => *}) => (
    <TouchableOpacity
        hitSlop={HIT_SLOP}
        style={styles.prevButton}
        onPress={onPress}
    >
        <Text style={styles.prevButton__text}>‹</Text>
    </TouchableOpacity>
);


================================================
FILE: src/controls/index.js
================================================
// @flow
export {default as Close} from './Close';
export {default as Prev} from './Prev';
export {default as Next} from './Next';


================================================
FILE: src/styles.js
================================================
import {StyleSheet} from 'react-native';

const HEADER_HEIGHT = 60;

export default function createStyles({screenWidth, screenHeight}) {
    return StyleSheet.create({
        underlay: {
            position: 'absolute',
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
        },
        container: {
            width: screenWidth,
            height: screenHeight,
        },
        header: {
            position: 'absolute',
            top: 0,
            left: 0,
            zIndex: 100,
            height: HEADER_HEIGHT,
            width: screenWidth,
        },
        imageContainer: {
            width: screenWidth,
            height: screenHeight,
            overflow: 'hidden',
        },
        loading: {
            position: 'absolute',
            top: screenHeight / 2 - 20,
            alignSelf: 'center',
        },
        footer: {
            position: 'absolute',
            bottom: 0,
            left: 0,
            right: 0,
            zIndex: 100,
        },
    });
}


================================================
FILE: src/types.js
================================================
// @flow
import {type ComponentType} from 'react';

export type ControlType = {
    onPress: () => void,
};

export type ControlsType = {
    close?: ?ComponentType<ControlType>,
    next?: ComponentType<ControlType>,
    prev?: ComponentType<ControlType>,
};

export type TouchType = {
    pageX: number,
    pageY: number,
};

export type NativeEventType = {
    touches: Array<TouchType>,
    contentOffset: {x: number, y: number},
};

export type EventType = {nativeEvent: NativeEventType};

export type ImageType = {
    source: any,
    width: number,
    height: number,
    title: ?string,
    index: number,
};

export type TranslateType = {
    x: number,
    y: number,
};

export type GestureState = {
    dx: number,
    dy: number,
    vx: number,
    vy: number,
};

export type DimensionsType = {width: number, height: number};
export type ScreenDimensionsType = {screenWidth: number, screenHeight: number};

export type ImageSizeType = DimensionsType & {index: number};

export type TransitionType = {scale: number, translate: TranslateType};


================================================
FILE: src/utils.js
================================================
// @flow
import {Image, PanResponder} from 'react-native';

import {
    type EventType,
    type GestureState,
    type ImageType,
    type TouchType,
    type TranslateType,
    type TransitionType,
    type DimensionsType,
    type ScreenDimensionsType,
} from './types';

const SCALE_EPSILON = 0.01;
const SCALE_MULTIPLIER = 1.2;

export const generatePanHandlers = (
    onStart: (EventType, GestureState) => *,
    onMove: (EventType, GestureState) => *,
    onRelease: (EventType, GestureState) => *
): * =>
    PanResponder.create({
        onStartShouldSetPanResponder: (): boolean => true,
        onStartShouldSetPanResponderCapture: (): boolean => true,
        onMoveShouldSetPanResponder: (): boolean => true,
        onMoveShouldSetPanResponderCapture: (): boolean => true,
        onPanResponderGrant: onStart,
        onPanResponderMove: onMove,
        onPanResponderRelease: onRelease,
        onPanResponderTerminate: onRelease,
        onPanResponderTerminationRequest: (): void => {},
        onShouldBlockNativeResponder: () => false,
    });

export const getScale = (
    currentDistance: number,
    initialDistance: number
): number => (currentDistance / initialDistance) * SCALE_MULTIPLIER;

export const getDistance = (touches: Array<TouchType>): number => {
    const [a, b] = touches;

    if (a == null || b == null) {
        return 0;
    }

    return Math.sqrt(
        Math.pow(a.pageX - b.pageX, 2) + Math.pow(a.pageY - b.pageY, 2)
    );
};

export const calculateInitialScale = (
    imageWidth: number = 0,
    imageHeight: number = 0,
    {screenWidth, screenHeight}: ScreenDimensionsType
): number => {
    const screenRatio = screenHeight / screenWidth;
    const imageRatio = imageHeight / imageWidth;

    if (imageWidth > screenWidth || imageHeight > screenHeight) {
        if (screenRatio > imageRatio) {
            return screenWidth / imageWidth;
        }

        return screenHeight / imageHeight;
    }

    return 1;
};

export const calculateInitialTranslate = (
    imageWidth: number = 0,
    imageHeight: number = 0,
    {screenWidth, screenHeight}: ScreenDimensionsType
): TranslateType => {
    const getTranslate = (axis: string): number => {
        const imageSize = axis === 'x' ? imageWidth : imageHeight;
        const screenSize = axis === 'x' ? screenWidth : screenHeight;

        if (imageWidth >= imageHeight) {
            return (screenSize - imageSize) / 2;
        }

        return screenSize / 2 - imageSize / 2;
    };

    return {
        x: getTranslate('x'),
        y: getTranslate('y'),
    };
};

export const getInitialParams = (
    {width, height}: DimensionsType,
    screenDimensions: Object
): TransitionType => ({
    scale: calculateInitialScale(width, height, screenDimensions),
    translate: calculateInitialTranslate(width, height, screenDimensions),
});

export function fetchImageSize(images: Array<ImageType> = []) {
    return images.reduce((acc, image) => {
        if (
            image.source &&
            image.source.uri &&
            (!image.width || !image.height)
        ) {
            const imageSize = new Promise((resolve, reject) => {
                Image.getSize(
                    image.source.uri,
                    (width, height) =>
                        resolve({
                            width,
                            height,
                            index: image.index,
                        }),
                    reject
                );
            });

            acc.push(imageSize);
        }

        return acc;
    }, []);
}

const shortHexRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
const fullHexRegex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;

export const isHex = (color: string): boolean =>
    fullHexRegex.test(color) || shortHexRegex.test(color);

export const hexToRgb = (hex: string): number[] => {
    // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
    const input = hex.replace(
        shortHexRegex,
        (m, r, g, b) => `${r}${r}${g}${g}${b}${b}`
    );

    const [match, r, g, b] = [].concat(fullHexRegex.exec(input));

    if (!match) {
        return [];
    }

    return [parseInt(r, 16), parseInt(g, 16), parseInt(b, 16)];
};

export const addIndexesToImages = (images: ImageType[]): ImageType[] =>
    images.map((image, index) => ({...image, index}));

export const getImagesWithoutSize = (images: ImageType[]) =>
    images.filter(({width, height}) => !width || !height);

export const scalesAreEqual = (scaleA: number, scaleB: number): boolean =>
    Math.abs(scaleA - scaleB) < SCALE_EPSILON;
Download .txt
gitextract_k_se4hxv/

├── .eslintrc
├── .flowconfig
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── example/
│   ├── .eslintrc
│   ├── .flowconfig
│   ├── .gitignore
│   ├── .watchmanconfig
│   ├── App.js
│   ├── app.json
│   ├── babel.config.js
│   ├── metro.config.js
│   └── package.json
├── package.json
└── src/
    ├── ImageView.js
    ├── controls/
    │   ├── Close.js
    │   ├── Next.js
    │   ├── Prev.js
    │   └── index.js
    ├── styles.js
    ├── types.js
    └── utils.js
Download .txt
SYMBOL INDEX (20 symbols across 7 files)

FILE: example/App.js
  class App (line 123) | class App extends Component {
    method constructor (line 124) | constructor(props) {
    method renderFooter (line 141) | renderFooter({title}) {
    method render (line 163) | render() {

FILE: src/ImageView.js
  constant IMAGE_SPEED_FOR_CLOSE (line 46) | const IMAGE_SPEED_FOR_CLOSE = 1.1;
  constant SCALE_MAXIMUM (line 47) | const SCALE_MAXIMUM = 5;
  constant HEADER_HEIGHT (line 48) | const HEADER_HEIGHT = 60;
  constant SCALE_MAX_MULTIPLIER (line 49) | const SCALE_MAX_MULTIPLIER = 3;
  constant FREEZE_SCROLL_DISTANCE (line 50) | const FREEZE_SCROLL_DISTANCE = 15;
  constant BACKGROUND_OPACITY_MULTIPLIER (line 51) | const BACKGROUND_OPACITY_MULTIPLIER = 0.003;
  class ImageView (line 94) | class ImageView extends Component<PropsType, StateType> {
  method if (line 592) | if (nextImages.length === 0) {

FILE: src/controls/Close.js
  constant HIT_SLOP (line 5) | const HIT_SLOP = {top: 15, left: 15, right: 15, bottom: 15};

FILE: src/controls/Next.js
  constant HIT_SLOP (line 5) | const HIT_SLOP = {top: 15, left: 15, right: 15, bottom: 15};

FILE: src/controls/Prev.js
  constant HIT_SLOP (line 5) | const HIT_SLOP = {top: 15, left: 15, right: 15, bottom: 15};

FILE: src/styles.js
  constant HEADER_HEIGHT (line 3) | const HEADER_HEIGHT = 60;
  function createStyles (line 5) | function createStyles({screenWidth, screenHeight}) {

FILE: src/utils.js
  constant SCALE_EPSILON (line 15) | const SCALE_EPSILON = 0.01;
  constant SCALE_MULTIPLIER (line 16) | const SCALE_MULTIPLIER = 1.2;
  function fetchImageSize (line 102) | function fetchImageSize(images: Array<ImageType> = []) {
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (60K chars).
[
  {
    "path": ".eslintrc",
    "chars": 889,
    "preview": "{\n  \"parser\": \"babel-eslint\",\n  \"extends\": [\n    \"airbnb\",\n    \"plugin:flowtype/recommended\",\n    \"plugin:jest/recommend"
  },
  {
    "path": ".flowconfig",
    "chars": 1482,
    "preview": "[ignore]\n; We fork some components by platform\n.*/*[.]android.js\n\n; Ignore \"BUCK\" generated dirs\n<PROJECT_ROOT>/\\.buckd/"
  },
  {
    "path": ".gitignore",
    "chars": 25,
    "preview": "node_modules/\n.idea\n.expo"
  },
  {
    "path": ".prettierrc",
    "chars": 136,
    "preview": "{\n  \"bracketSpacing\": false,\n  \"parser\": \"flow\",\n  \"printWidth\": 80,\n  \"singleQuote\": true,\n  \"tabWidth\": 4,\n  \"trailing"
  },
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) 2017 Anton Kalinin\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "README.md",
    "chars": 3813,
    "preview": "## If you are using React Native >= 0.59.0 it's recommended to use similar package https://github.com/jobtoday/react-nat"
  },
  {
    "path": "example/.eslintrc",
    "chars": 1170,
    "preview": "{\n    \"extends\": [\n      \"airbnb\",\n      \"prettier\",\n      \"prettier/flowtype\",\n      \"prettier/react\"\n    ],\n    \"rules"
  },
  {
    "path": "example/.flowconfig",
    "chars": 1482,
    "preview": "[ignore]\n; We fork some components by platform\n.*/*[.]android.js\n\n; Ignore \"BUCK\" generated dirs\n<PROJECT_ROOT>/\\.buckd/"
  },
  {
    "path": "example/.gitignore",
    "chars": 70,
    "preview": "node_modules/**/*\n.expo/*\nnpm-debug.*\nyarn.lock\nImageView.js\ntypes.js\n"
  },
  {
    "path": "example/.watchmanconfig",
    "chars": 3,
    "preview": "{}\n"
  },
  {
    "path": "example/App.js",
    "chars": 6481,
    "preview": "import React, {Component} from 'react';\nimport {\n    Text,\n    View,\n    Image,\n    TouchableOpacity,\n    StyleSheet,\n  "
  },
  {
    "path": "example/app.json",
    "chars": 513,
    "preview": "{\n  \"expo\": {\n    \"name\": \"react-native-image-view\",\n    \"description\": \"React Native modal image view with pinch zoom\","
  },
  {
    "path": "example/babel.config.js",
    "chars": 117,
    "preview": "module.exports = function(api) {\n    api.cache(true);\n    return {\n        presets: ['babel-preset-expo'],\n    };\n};\n"
  },
  {
    "path": "example/metro.config.js",
    "chars": 376,
    "preview": "const path = require('path');\n\nmodule.exports = {\n    resolver: {\n        extraNodeModules: new Proxy(\n            {},\n "
  },
  {
    "path": "example/package.json",
    "chars": 807,
    "preview": "{\n  \"main\": \"node_modules/expo/AppEntry.js\",\n  \"private\": true,\n  \"dependencies\": {\n    \"expo\": \"^35.0.0\",\n    \"react\": "
  },
  {
    "path": "package.json",
    "chars": 1416,
    "preview": "{\n  \"name\": \"react-native-image-view\",\n  \"version\": \"2.1.9\",\n  \"description\": \"React Native modal image view with pinch "
  },
  {
    "path": "src/ImageView.js",
    "chars": 27038,
    "preview": "// @flow\n\nimport React, {Component, type Node, type ComponentType} from 'react';\nimport {\n    ActivityIndicator,\n    Ani"
  },
  {
    "path": "src/controls/Close.js",
    "chars": 903,
    "preview": "// @flow\nimport React from 'react';\nimport {StyleSheet, Text, TouchableOpacity} from 'react-native';\n\nconst HIT_SLOP = {"
  },
  {
    "path": "src/controls/Next.js",
    "chars": 910,
    "preview": "// @flow\nimport React from 'react';\nimport {StyleSheet, Text, TouchableOpacity} from 'react-native';\n\nconst HIT_SLOP = {"
  },
  {
    "path": "src/controls/Prev.js",
    "chars": 909,
    "preview": "// @flow\nimport React from 'react';\nimport {StyleSheet, Text, TouchableOpacity} from 'react-native';\n\nconst HIT_SLOP = {"
  },
  {
    "path": "src/controls/index.js",
    "chars": 131,
    "preview": "// @flow\nexport {default as Close} from './Close';\nexport {default as Prev} from './Prev';\nexport {default as Next} from"
  },
  {
    "path": "src/styles.js",
    "chars": 1049,
    "preview": "import {StyleSheet} from 'react-native';\n\nconst HEADER_HEIGHT = 60;\n\nexport default function createStyles({screenWidth, "
  },
  {
    "path": "src/types.js",
    "chars": 1060,
    "preview": "// @flow\nimport {type ComponentType} from 'react';\n\nexport type ControlType = {\n    onPress: () => void,\n};\n\nexport type"
  },
  {
    "path": "src/utils.js",
    "chars": 4609,
    "preview": "// @flow\nimport {Image, PanResponder} from 'react-native';\n\nimport {\n    type EventType,\n    type GestureState,\n    type"
  }
]

About this extraction

This page contains the full source code of the antonKalinin/react-native-image-view GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (55.1 KB), approximately 13.5k tokens, and a symbol index with 20 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!