Saturday 29 February 2020

Friday 28 February 2020

夜之光

White Rabbit, Moscow,

expo image editor 5 crop

press crop button in editor

image will be stretched to full screen
crop area is highlighted
2 range sliders shrink/expand the crop area

top slider positions the x origin and width of the crop area
bottom slider position the y origin and height of the crop area
castle tower is the region of interest, press confirm

castle is cropped, only tower is left

new image is saved

//editor.js

import React, { useState, useEffect } from 'react';
import { BackHandler } from 'react-native';
import * as MediaLibrary from 'expo-media-library';
import * as Sharing from 'expo-sharing';
import * as FileSystem from 'expo-file-system';
import * as ImageManipulator from 'expo-image-manipulator';
import { Image, ImageBackground, Alert, Dimensions, ScrollView, Slider } from 'react-native'
import {
    Container, Header, Title, Content, Footer,
    FooterTab, Button, Left, Right, Body, Icon, Text,
    Accordion, Card, CardItem, Thumbnail, ListItem,
    CheckBox, DatePicker, DeckSwiper, Fab, View,
    Badge, Form, Item, Input, Label, Picker, Textarea,
    Switch, Radio, Spinner, Tab, Tabs, TabHeading,
    ScrollableTab, H1, H2, H3, Drawer, Toast
} from 'native-base';
import {
    Ionicons, MaterialIcons, Foundation,
    MaterialCommunityIcons, Octicons
} from '@expo/vector-icons';
import ReactNativeZoomableView from '@dudigital/react-native-zoomable-view/src/ReactNativeZoomableView';
import { Col, Row, Grid } from "react-native-easy-grid";
import { CustomSlider } from './multiSlider/CustomSlider'

const picWidth = Math.round(Dimensions.get('window').width) / 2 - 20;

export default function Editor(props) {
    const [gallaryPic, setgalleryPic] = useState([])
    const [currentPic, setcurrentPic] = useState(0)
    const [openGridView, setopenGridView] = useState(false)
    const [selectedPic, setselectedPic] = useState([])
    const [activeFab, setactiveFab] = useState(false)
    const [showSlider, setshowSlider] = useState(false)
    const [sliderX, setsliderX] = useState([0, 10])
    const [sliderY, setsliderY] = useState([0, 10])
    const [cropX, setcropX] = useState(0)
    const [cropY, setcropY] = useState(0)
    const [cropWidth, setcropWidth] = useState(Dimensions.get('window').width)
    const [cropHeight, setcropHeight] = useState(Dimensions.get('window').height)

    useEffect(() => {
        setcurrentPic(0)
        getPictureFromGallary()

        //if back key is pressed on the phone, close editor
        BackHandler.addEventListener('hardwareBackPress', () => {
            props.closeEditor()
        });

        //component will unmount
        return () => { BackHandler.removeEventListener('hardwareBackPress') }
    }, [])

    getPictureFromGallary = async () => {
        const pics = await MediaLibrary.getAssetsAsync({
            sortBy: MediaLibrary.SortBy.modificationTime
        })

        //await setdefaultPicture(pics.assets[0].uri)
        await setgalleryPic(pics.assets)
    }

    previousPic = () => {
        const length = gallaryPic.length
        if (length === 0) { return }

        currentPic > 0 ? setcurrentPic(currentPic - 1) : setcurrentPic(length - 1)
    }

    nextPic = () => {
        const length = gallaryPic.length
        if (length === 0) { return }

        currentPic < length - 1 ? setcurrentPic(currentPic + 1) : setcurrentPic(0)
    }

    confirmDelete = async (image) => {
        //remove picture in android directory
        await MediaLibrary.deleteAssetsAsync(image)
            .then(async () => {
                //remove picture in memory
                await setgalleryPic(gallaryPic.filter(item => item.id !== image.id))
                await setselectedPic(selectedPic.filter(id => id !== image.id))
                //correct current pic index
                previousPic()
                nextPic()
            })
            .catch(error => {
                alert(error)
            });
    }

    togglePicHighlight = (id) => {
        selectedPic.includes(id) ?
            setselectedPic(selectedPic.filter(_id => { return _id !== id })) :
            setselectedPic(selectedPic.concat(id))
    }

    picLongPress = (index) => {
        setcurrentPic(index)
        setopenGridView(false)
    }

    deletePics = () => {
        let i = 0
        selectedPic.forEach(async (id) => {
            const removePic = gallaryPic.find(item => item.id === id)
            //remove picture in android directory
            await MediaLibrary.deleteAssetsAsync(removePic)
            i++
            //all pics deleted from android storage
            if (i === selectedPic.length) {
                //remove picture in memory
                setgalleryPic(gallaryPic.filter(item => !selectedPic.includes(item.id)))

                setselectedPic([])
                setcurrentPic(0)
            }
        })
    }

    shareImage = async (image) => {
        //copy image from memory to app cache, app cache will be automatically cleaned when storage is low
        //sharing can't access 'file:///storage/emulated/0/DCIM/e3bda948-2407-47e1-b5cf-12e7705bf86e.jpg'
        //have to copy to 'file:///data/user/0/host.exp.exponent/cache/ExperienceData/%2540chuanshuoge%252Fexpo-medialibrary/e3bda948-2407-47e1-b5cf-12e7705bf86e.jpg'
        //console.log(FileSystem.cacheDirectory + image.filename)
        await FileSystem.copyAsync({
            from: image.uri,
            to: FileSystem.cacheDirectory + image.filename
        })

        await Sharing.shareAsync(FileSystem.cacheDirectory + image.filename)
    }

    rotatePic = async (image) => {
        //console.log(image)
        const newImage = await ImageManipulator.manipulateAsync(
            image.uri,
            [{ rotate: 90 }],
            {
                compress: 1,
                format: image.filename.includes('png') ? ImageManipulator.SaveFormat.PNG :
                    ImageManipulator.SaveFormat.JPEG
            }
        );
        //console.log(newImage)
        await MediaLibrary.saveToLibraryAsync(newImage.uri)
            .then(async () => {
                await getPictureFromGallary()
                await setcurrentPic(0)
            })
            .catch(error => {
                alert(error)
            });
        //console.log(gallaryPic)
    }

    mirrorPic = async (image) => {
        const newImage = await ImageManipulator.manipulateAsync(
            image.uri,
            [{ flip: ImageManipulator.FlipType.Horizontal }],
            {
                compress: 1,
                format: image.filename.includes('png') ? ImageManipulator.SaveFormat.PNG :
                    ImageManipulator.SaveFormat.JPEG
            }
        );

        await MediaLibrary.saveToLibraryAsync(newImage.uri)
            .then(async () => {
                await getPictureFromGallary()
                await setcurrentPic(0)
            })
            .catch(error => {
                alert(error)
            });
    }

    sliderXChange = (markers) => {
        setsliderX(markers)

        this.myImage.measure((fx, fy, width, height, px, py) => {
            /*console.log('Component width is: ' + width)
            console.log('Component height is: ' + height)
            console.log('X offset to frame: ' + fx)
            console.log('Y offset to frame: ' + fy)
            console.log('X offset to page: ' + px)
            console.log('Y offset to page: ' + py)*/
            setcropX(width * markers[0] / 10 + px)
            setcropWidth(width * (markers[1] - markers[0]) / 10)
        })

    }

    sliderYChange = (markers) => {
        setsliderY(markers)

        this.myImage.measure((fx, fy, width, height, px, py) => {
            setcropY(height * markers[0] / 10 + py)
            setcropHeight(height * (markers[1] - markers[0]) / 10)
        })
    }

    cropPic = async (image) => {
        const cropShape = {
            originX: image.width * sliderX[0] / 10,
            width: image.width * (sliderX[1] - sliderX[0]) / 10,
            originY: image.height * sliderY[0] / 10,
            height: image.height * (sliderY[1] - sliderY[0]) / 10,
        }

        const newImage = await ImageManipulator.manipulateAsync(
            image.uri,
            [{ crop: cropShape }],
            {
                compress: 1,
                format: image.filename.includes('png') ? ImageManipulator.SaveFormat.PNG :
                    ImageManipulator.SaveFormat.JPEG
            }
        );

        await MediaLibrary.saveToLibraryAsync(newImage.uri)
            .then(async () => {
                await getPictureFromGallary()
                await setcurrentPic(0)
                await setsliderX([0, 10])
                await setsliderY([0, 10])
                await setcropX(0)
                await setcropY(0)
                await setcropWidth(Dimensions.get('window').width)
                await setcropHeight(Dimensions.get('window').height)
            })
            .catch(error => {
                alert(error)
            });
    }

    return (
        <Container>
            {showSlider ? null :
                <Header style={{ marginTop: 25 }}>
                    <Left>
                        <Button transparent onPress={() => props.closeEditor()}>
                            <Ionicons name='ios-arrow-round-back' size={40} color="white"></Ionicons>
                        </Button>
                    </Left>
                    <Body style={{ alignItems: 'center' }}>
                        <Title>Editor</Title>
                    </Body>
                    <Right>
                        <Button transparent iconRight onPress={() => setopenGridView(true)}>
                            <Text style={{ color: 'white' }}>Gallary{' '}</Text>
                            <Ionicons name='ios-arrow-round-forward' size={40} color="white"></Ionicons>
                        </Button>
                    </Right>
                </Header>
            }
            <View style={{ flex: 1 }}>
                {gallaryPic.length > 0 ?
                    <ImageBackground source={require('./assets/background.jpg')}
                        style={{ flex: 1, resizeMode: 'stretch' }}>
                        {showSlider ? null :
                            <ReactNativeZoomableView
                                maxZoom={1.5}
                                minZoom={0.5}
                                zoomStep={0.5}
                                initialZoom={1}
                                bindToBorders={true}>

                                <Image style={{ width: '100%', height: '100%', resizeMode: 'contain' }}
                                    source={{ uri: gallaryPic[currentPic].uri }}
                                />
                            </ReactNativeZoomableView>
                        }
                        {showSlider ?
                            <Image style={{ width: '100%', height: '100%', resizeMode: 'stretch' }}
                                source={{ uri: gallaryPic[currentPic].uri }}
                                ref={ref => { this.myImage = ref }}
                            />
                            : null
                        }
                    </ImageBackground>
                    : null}

                {openGridView ? null :
                    <Fab
                        active={activeFab}
                        direction="up"
                        containerStyle={{}}
                        style={{ backgroundColor: '#5067FF' }}
                        position="bottomRight"
                        onPress={() => activeFab ? setactiveFab(false) : setactiveFab(true)}>
                        <Foundation name='social-yelp'></Foundation>

                        <Button style={{ backgroundColor: '#34A34F' }}
                            onPress={() => shareImage(gallaryPic[currentPic])}>
                            <Foundation name='share' size={30} color="white"></Foundation>
                        </Button>
                        <Button style={{ backgroundColor: 'yellow' }}
                            onPress={() => rotatePic(gallaryPic[currentPic])}>
                            <MaterialCommunityIcons name='axis-x-rotate-clockwise' size={30} />
                        </Button>
                        <Button style={{ backgroundColor: 'pink' }}
                            onPress={() => mirrorPic(gallaryPic[currentPic])}>
                            <Octicons name='mirror' size={30} />
                        </Button>
                        <Button style={{ backgroundColor: 'cyan' }}
                            onPress={() => { setshowSlider(true); setactiveFab(false) }}>
                            <Foundation name='crop' size={30} />
                        </Button>
                        <Button style={{ backgroundColor: 'purple' }}
                            onPress={() => alert('double tap on image to zoom')}>
                            <Foundation name='zoom-in' size={30} color="white"></Foundation>
                        </Button>
                        <Button style={{ backgroundColor: '#DD5144' }}
                            onPress={() =>
                                Alert.alert(
                                    'Delete current picture?',
                                    '',
                                    [{
                                        text: 'Cancel',
                                        onPress: () => { },
                                        style: 'cancel',
                                    },
                                    { text: 'OK', onPress: () => confirmDelete(gallaryPic[currentPic]) },
                                    ],
                                    { cancelable: true },
                                )
                            }>
                            <MaterialIcons name='delete' size={30} color="white" />
                        </Button>
                    </Fab>
                }
            </View>

            {openGridView || showSlider ? null :
                <Footer>
                    <FooterTab>
                        <Button active onPress={() => previousPic()}>
                            <Text>Previous</Text>
                        </Button>
                        <Button active onPress={() => nextPic()}>
                            <Text>Next</Text>
                        </Button>
                    </FooterTab>
                </Footer>
            }

            {showSlider ?
                <Footer style={{ zIndex: 8 }}>
                    <FooterTab>
                        <Button active onPress={() => {
                            cropPic(gallaryPic[currentPic]);
                            setshowSlider(false); setactiveFab(true)
                        }}>
                            <Text>confirm</Text>
                        </Button>
                        <Button active onPress={() => { setshowSlider(false); setactiveFab(true) }}>
                            <Text>Cancel</Text>
                        </Button>
                    </FooterTab>
                </Footer>
                : null}

            {openGridView ?
                <View style={{
                    position: 'absolute',
                    bottom: 0,
                    right: 0,
                    left: 0,
                    top: 0,
                    backgroundColor: 'white',
                    zIndex: 5,
                }}>
                    <Header style={{ marginTop: 25 }}>
                        <Left>
                            <Button transparent onPress={() => setopenGridView(false)}>
                                <Ionicons name='ios-arrow-round-back' size={40} color="white"></Ionicons>
                            </Button>
                        </Left>
                        <Body style={{ alignItems: 'center' }}>
                            <Title>Gallary</Title>
                        </Body>
                        <Right>
                            {selectedPic.length > 0 ?
                                <Button transparent onPress={() =>
                                    Alert.alert(
                                        'Delete selected pictures?',
                                        '',
                                        [{
                                            text: 'Cancel',
                                            onPress: () => { },
                                            style: 'cancel',
                                        },
                                        { text: 'OK', onPress: () => deletePics() },
                                        ],
                                        { cancelable: true },
                                    )}>
                                    <MaterialIcons name='delete' size={40} color="white" />
                                </Button>
                                : null}
                        </Right>
                    </Header>
                    <ScrollView>
                        {gallaryPic.length > 0 ?
                            <Grid>
                                <Col>
                                    {gallaryPic.map((item, index) => {
                                        const { id, uri } = item
                                        return (
                                            index % 2 === 0 ?
                                                <Row key={id} style={{ justifyContent: 'center', marginVertical: 5 }}>
                                                    <Button transparent
                                                        style={{
                                                            height: picWidth, width: picWidth, borderColor: 'green',
                                                            borderWidth: selectedPic.includes(id) ? 2 : 0
                                                        }}
                                                        onPress={() => togglePicHighlight(id)}
                                                        onLongPress={() => picLongPress(index)}>
                                                        <Image style={{ height: '100%', width: '100%', resizeMode: 'cover' }}
                                                            source={{ uri: uri }} />
                                                    </Button>
                                                </Row>
                                                : null)
                                    })}
                                </Col>
                                <Col>
                                    {gallaryPic.map((item, index) => {
                                        const { id, uri } = item
                                        return (
                                            index % 2 === 1 ?
                                                <Row key={id} style={{ justifyContent: 'center', marginVertical: 5 }}>
                                                    <Button transparent
                                                        style={{
                                                            height: picWidth, width: picWidth, borderColor: 'green',
                                                            borderWidth: selectedPic.includes(id) ? 2 : 0
                                                        }}
                                                        onPress={() => togglePicHighlight(id)}
                                                        onLongPress={() => picLongPress(index)}>
                                                        <Image style={{ height: '100%', width: '100%', resizeMode: 'cover' }}
                                                            source={{ uri: uri }} />
                                                    </Button>
                                                </Row>
                                                : null)
                                    })}
                                    {
                                        gallaryPic.length % 2 === 1 ?
                                            <Row style={{ justifyContent: 'center', marginVertical: 5 }}>
                                                <Button transparent
                                                    style={{
                                                        height: picWidth, width: picWidth
                                                    }}>
                                                </Button>
                                            </Row>
                                            : null
                                    }
                                </Col>
                            </Grid>
                            : null}
                    </ScrollView>
                </View> : null}

            {showSlider ?
                <View style={{
                    position: 'absolute',
                    bottom: 0,
                    right: 0,
                    left: 0,
                    top: 0,
                    backgroundColor: 'transparent',
                    zIndex: 6,
                }}>
                    <Button transparent
                        style={{
                            height: cropHeight, width: cropWidth,
                            left: cropX, top: cropY,
                            borderColor: 'red',
                            borderWidth: 5
                        }} />
                </View>
                : null
            }

            {showSlider ?
                <View style={{
                    position: 'absolute',
                    bottom: 0,
                    right: 0,
                    left: 0,
                    top: 0,
                    backgroundColor: 'transparent',
                    zIndex: 7,
                    justifyContent: "space-around"
                }}>
                    <CustomSlider
                        min={0}
                        max={10}
                        LRpadding={40}
                        callback={(e) => sliderXChange(e)}
                        single={false}
                    />

                    <CustomSlider
                        min={0}
                        max={10}
                        LRpadding={40}
                        callback={(e) => sliderYChange(e)}
                        single={false}
                    />
                </View>
                : null
            }
        </Container>
    )
}

------------------------------------
//multiSlider/customerSlider.js

import React, { Component } from 'react';
import { StyleSheet, Text, View, Dimensions } from 'react-native';
import MultiSlider from '@ptomasroos/react-native-multi-slider';
import { CustomMarker } from './CustomMarker';
import { Item } from './Item';

export class CustomSlider extends Component {

    constructor(props) {
        super(props);
        this.state = {
            multiSliderValue: [this.props.min, this.props.max],
            first: this.props.min,
            second: this.props.max,
        }
    }

    render() {
        return (
            <View>
                <View style={[styles.column, { marginLeft: this.props.LRpadding, marginRight: this.props.LRpadding }]}>
                    {this.renderScale()}
                </View>
                <View style={styles.container}>

                    <MultiSlider
                        trackStyle={{ backgroundColor: '#bdc3c7' }}
                        selectedStyle={{ backgroundColor: "#5e5e5e" }}
                        values={this.props.single ? [this.state.multiSliderValue[1]] : [this.state.multiSliderValue[0], this.state.multiSliderValue[1]]}
                        sliderLength={Dimensions.get('window').width - this.props.LRpadding * 2}
                        onValuesChange={this.multiSliderValuesChange}
                        min={this.props.min}
                        max={this.props.max}
                        step={1}
                        allowOverlap={false}
                        customMarker={CustomMarker}
                        snapped={true}
                    />
                </View>
            </View>
        );
    }

    multiSliderValuesChange = values => {
        if (this.props.single) {
            this.setState({
                second: values[0],
            })
        } else {
            this.setState({
                multiSliderValue: values,
                first: values[0],
                second: values[1],
            })
        }
        this.props.callback(values)
    }

    renderScale = () => {
        const items = [];
        for (let i = this.props.min; i <= this.props.max; i++) {
            items.push(
                <Item
                    key={'num' + i}
                    value={i}
                    first={this.state.first}
                    second={this.state.second}
                />
            );
        }
        return items;
    }
}

const styles = StyleSheet.create({
    container: {
        justifyContent: 'center',
        alignItems: 'center',
    },
    column: {
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'space-between',
        bottom: -20,
    },
    active: {
        textAlign: 'center',
        fontSize: 20,
        color: '#5e5e5e',
    },
    inactive: {
        textAlign: 'center',
        fontWeight: 'normal',
        color: '#bdc3c7',
    },
    line: {
        textAlign: 'center',
    }
});

-----------------------------------------------
//multiSlider/customerMarker.js

import React, { Component } from 'react';
import { StyleSheet, View, Image } from 'react-native';

export class CustomMarker extends Component {
  render() {
    return (
      <Image
        style={styles.image}
        source={require('./slider-button.png')}
        resizeMode="contain"
      />
    );
  }
}

const styles = StyleSheet.create({
  circle1: {
    width: 30,
    height: 30,
    borderRadius: 15,
    backgroundColor: 'white',
    borderColor: 'gray',
    borderWidth: 1,
    position: 'absolute'
  },
  circle2: {
    width: 14,
    height: 14,
    borderRadius: 7,
    backgroundColor: '#4eaa37',
    position: 'absolute',
    justifyContent: 'center',
    alignSelf: 'center',
    zIndex: 1
  },
  container: {
    justifyContent: 'center',
    alignItems: 'center'
  },
  image: {
    width: 30, height: 30
  }
});

----------------------------------
//multiSlider/Item.js

import React, { Component } from 'react';
import { StyleSheet, View, Text } from 'react-native';

export class Item extends Component {
    render() {
        return (
            <View>
                <Text style={[this.checkActive() ? styles.active : styles.inactive]}>{this.props.value}</Text>
                <Text style={[this.checkActive() ? styles.line : {}]}> {this.checkActive() ? '|' : ''}</Text>
            </View>
        );
    }

    checkActive = () => {
        if (this.props.value >= this.props.first && this.props.value <= this.props.second)
            return true
        else
            return false
    }
}

const styles = StyleSheet.create({
    active: {
        textAlign: 'center',
        fontSize: 20,
        bottom: 10,
        color: 'red',
    },
    inactive: {
        flex: 1,
        textAlignVertical: 'center',
        textAlign: 'center',
        fontWeight: 'normal',
        color: '#bdc3c7',
    },
    line: {
        fontSize: 10,
        textAlign: 'center',
    }
});

reference:
http://chuanshuoge2.blogspot.com/2020/02/expo-image-editor-5-crop.html

Wednesday 26 February 2020

expo image editor 5 crop reference


reference:
http://chuanshuoge2.blogspot.com/2020/02/expo-image-editor-4-rotate-flip.html

multi slider
https://github.com/ptomasroos/react-native-multi-slider
https://medium.com/@KPS250/custom-scale-slider-in-react-native-cb3e6ff786bd
https://github.com/KPS250/React-Native-Scale-Slider

image position
https://stackoverflow.com/questions/30096038/react-native-getting-the-position-of-an-element

dynamic ref
https://medium.com/@jalexmayer/react-refs-with-dynamic-names-d2262ab0a0b0

Free Foreign exchange rates API

https://api.exchangeratesapi.io/latest?symbols=USD,CNY
{"rates":{"CNY":7.6045,"USD":1.084},"base":"EUR","date":"2020-02-25"}

https://api.coindesk.com/v1/bpi/currentprice.json
{"time":{"updated":"Feb 26, 2020 14:33:00 UTC","updatedISO":"2020-02-26T14:33:00+00:00","updateduk":"Feb 26, 2020 at 14:33 GMT"},"disclaimer":"This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org","chartName":"Bitcoin","bpi":{"USD":{"code":"USD","symbol":"&#36;","rate":"8,981.1483","description":"United States Dollar","rate_float":8981.1483},"GBP":{"code":"GBP","symbol":"&pound;","rate":"6,943.3258","description":"British Pound Sterling","rate_float":6943.3258},"EUR":{"code":"EUR","symbol":"&euro;","rate":"8,265.7909","description":"Euro","rate_float":8265.7909}}}

reference:
https://exchangeratesapi.io/
https://www.coindesk.com/coindesk-api

Monday 24 February 2020

expo image editor 4 rotate flip

open gallery

long press on castle image to enter editor, open fab

press rotate

press rotate again

press rotate once more

go back to gallery, all rotations are saved

tap on the intermediate rotations and press delete

long press on the vertical castle image

enter editor, press the mirror button

cattle is mirrored

back to gallery, 2 vertical castles are symmetric

//original image

Object {
  "albumId": "-2075821635",
  "creationTime": 1582682347060,
  "duration": 0,
  "filename": "fbf57b4e-9bbc-4626-a25c-c4299c5c1563.jpg",
  "height": 4096,
  "id": "289",
  "mediaType": "photo",
  "modificationTime": 1582682347000,
  "uri": "file:///storage/emulated/0/DCIM/fbf57b4e-9bbc-4626-a25c-c4299c5c1563.jpg",
  "width": 2304,
}

------------------------------
//rotated image
//width and height are swapped

Object {
  "height": 2304,
  "uri": "file:///data/user/0/host.exp.exponent/cache/ExperienceData/%2540chuanshuoge%252Fexpo-medialibrary/ImageManipulator/3da8a50b-4461-463c-b85f-0cdffd3240bc.jpg",
  "width": 4096,
}

-------------------------
//saved rotated image
//saved image has no creation time, but has modification time

Object {
    "albumId": "540528482",
    "creationTime": 0,
    "duration": 0,
    "filename": "5a4c80c817a17b44b3d5529b738e563b.jpg",
    "height": 710,
    "id": "14",
    "mediaType": "photo",
    "modificationTime": 1582173281000,
    "uri": "file:///storage/emulated/0/Download/5a4c80c817a17b44b3d5529b738e563b.jpg",
    "width": 1136,
  },

------------------------------
//editor.js

import React, { useState, useEffect } from 'react';
import { BackHandler } from 'react-native';
import * as MediaLibrary from 'expo-media-library';
import * as Sharing from 'expo-sharing';
import * as FileSystem from 'expo-file-system';
import * as ImageManipulator from 'expo-image-manipulator';
import { Image, ImageBackground, Alert, Dimensions, ScrollView } from 'react-native'
import {
    Container, Header, Title, Content, Footer,
    FooterTab, Button, Left, Right, Body, Icon, Text,
    Accordion, Card, CardItem, Thumbnail, ListItem,
    CheckBox, DatePicker, DeckSwiper, Fab, View,
    Badge, Form, Item, Input, Label, Picker, Textarea,
    Switch, Radio, Spinner, Tab, Tabs, TabHeading,
    ScrollableTab, H1, H2, H3, Drawer, Toast
} from 'native-base';
import {
    Ionicons, MaterialIcons, Foundation,
    MaterialCommunityIcons, Octicons
} from '@expo/vector-icons';
import ReactNativeZoomableView from '@dudigital/react-native-zoomable-view/src/ReactNativeZoomableView';
import { Col, Row, Grid } from "react-native-easy-grid";

const picWidth = Math.round(Dimensions.get('window').width) / 2 - 20;

export default function Editor(props) {
    const [gallaryPic, setgalleryPic] = useState([])
    const [currentPic, setcurrentPic] = useState(0)
    const [openGridView, setopenGridView] = useState(false)
    const [selectedPic, setselectedPic] = useState([])
    const [activeFab, setactiveFab] = useState(false)

    useEffect(() => {
        setcurrentPic(0)
        getPictureFromGallary()

        //if back key is pressed on the phone, close editor
        BackHandler.addEventListener('hardwareBackPress', () => {
            props.closeEditor()
        });

        //component will unmount
        return () => { BackHandler.removeEventListener('hardwareBackPress') }
    }, [])

    getPictureFromGallary = async () => {
        const pics = await MediaLibrary.getAssetsAsync({
            sortBy: MediaLibrary.SortBy.modificationTime
        })

        //await setdefaultPicture(pics.assets[0].uri)
        await setgalleryPic(pics.assets)
    }

    previousPic = () => {
        const length = gallaryPic.length
        if (length === 0) { return }

        currentPic > 0 ? setcurrentPic(currentPic - 1) : setcurrentPic(length - 1)
    }

    nextPic = () => {
        const length = gallaryPic.length
        if (length === 0) { return }

        currentPic < length - 1 ? setcurrentPic(currentPic + 1) : setcurrentPic(0)
    }

    confirmDelete = async (image) => {
        //remove picture in android directory
        await MediaLibrary.deleteAssetsAsync(image)
            .then(async () => {
                //remove picture in memory
                await setgalleryPic(gallaryPic.filter(item => item.id !== image.id))
                await setselectedPic(selectedPic.filter(id => id !== image.id))
                //correct current pic index
                previousPic()
                nextPic()
            })
            .catch(error => {
                alert(error)
            });
    }

    togglePicHighlight = (id) => {
        selectedPic.includes(id) ?
            setselectedPic(selectedPic.filter(_id => { return _id !== id })) :
            setselectedPic(selectedPic.concat(id))
    }

    picLongPress = (index) => {
        setcurrentPic(index)
        setopenGridView(false)
    }

    deletePics = () => {
        let i = 0
        selectedPic.forEach(async (id) => {
            const removePic = gallaryPic.find(item => item.id === id)
            //remove picture in android directory
            await MediaLibrary.deleteAssetsAsync(removePic)
            i++
            //all pics deleted from android storage
            if (i === selectedPic.length) {
                //remove picture in memory
                setgalleryPic(gallaryPic.filter(item => !selectedPic.includes(item.id)))

                setselectedPic([])
                setcurrentPic(0)
            }
        })
    }

    shareImage = async (image) => {
        //copy image from memory to app cache, app cache will be automatically cleaned when storage is low
        //sharing can't access 'file:///storage/emulated/0/DCIM/e3bda948-2407-47e1-b5cf-12e7705bf86e.jpg'
        //have to copy to 'file:///data/user/0/host.exp.exponent/cache/ExperienceData/%2540chuanshuoge%252Fexpo-medialibrary/e3bda948-2407-47e1-b5cf-12e7705bf86e.jpg'
        //console.log(FileSystem.cacheDirectory + image.filename)
        await FileSystem.copyAsync({
            from: image.uri,
            to: FileSystem.cacheDirectory + image.filename
        })

        await Sharing.shareAsync(FileSystem.cacheDirectory + image.filename)
    }

    rotatePic = async (image) => {
        //console.log(image)
        const newImage = await ImageManipulator.manipulateAsync(
            image.uri,
            [{ rotate: 90 }],
            {
                compress: 1,
                format: image.filename.includes('png') ? ImageManipulator.SaveFormat.PNG :
                    ImageManipulator.SaveFormat.JPEG
            }
        );
        //console.log(newImage)
        await MediaLibrary.saveToLibraryAsync(newImage.uri)
            .then(async () => {
                //display most recent modified image
                await getPictureFromGallary()
                await setcurrentPic(0)
            })
            .catch(error => {
                alert(error)
            });
        //console.log(gallaryPic)
    }

    mirrorPic = async (image) => {
        const newImage = await ImageManipulator.manipulateAsync(
            image.uri,
            [{ flip: ImageManipulator.FlipType.Horizontal }],
            {
                compress: 1,
                format: image.filename.includes('png') ? ImageManipulator.SaveFormat.PNG :
                    ImageManipulator.SaveFormat.JPEG
            }
        );

        await MediaLibrary.saveToLibraryAsync(newImage.uri)
            .then(async () => {
                await getPictureFromGallary()
                await setcurrentPic(0)
            })
            .catch(error => {
                alert(error)
            });
    }

    return (
        <Container>
            <Header style={{ marginTop: 25 }}>
                <Left>
                    <Button transparent onPress={() => props.closeEditor()}>
                        <Ionicons name='ios-arrow-round-back' size={40} color="white"></Ionicons>
                    </Button>
                </Left>
                <Body style={{ alignItems: 'center' }}>
                    <Title>Editor</Title>
                </Body>
                <Right>
                    <Button transparent iconRight onPress={() => setopenGridView(true)}>
                        <Text style={{ color: 'white' }}>Gallary{' '}</Text>
                        <Ionicons name='ios-arrow-round-forward' size={40} color="white"></Ionicons>
                    </Button>
                </Right>
            </Header>
            <View style={{ flex: 1 }}>
                {gallaryPic.length > 0 ?
                    <ImageBackground source={require('./assets/background.jpg')}
                        style={{ flex: 1, resizeMode: 'stretch' }}>
                        <ReactNativeZoomableView
                            maxZoom={1.5}
                            minZoom={0.5}
                            zoomStep={0.5}
                            initialZoom={1}
                            bindToBorders={true}>

                            <Image style={{ width: '100%', height: '100%', resizeMode: 'contain' }}
                                source={{ uri: gallaryPic[currentPic].uri }} />

                        </ReactNativeZoomableView>
                    </ImageBackground>
                    : null}

                {openGridView ? null :
                    <Fab
                        active={activeFab}
                        direction="up"
                        containerStyle={{}}
                        style={{ backgroundColor: '#5067FF' }}
                        position="bottomRight"
                        onPress={() => activeFab ? setactiveFab(false) : setactiveFab(true)}>
                        <Foundation name='social-yelp'></Foundation>

                        <Button style={{ backgroundColor: '#34A34F' }}
                            onPress={() => shareImage(gallaryPic[currentPic])}>
                            <Foundation name='share' size={30} color="white"></Foundation>
                        </Button>
                        <Button style={{ backgroundColor: 'yellow' }}
                            onPress={() => rotatePic(gallaryPic[currentPic])}>
                            <MaterialCommunityIcons name='axis-x-rotate-clockwise' size={30} />
                        </Button>
                        <Button style={{ backgroundColor: 'pink' }}
                            onPress={() => mirrorPic(gallaryPic[currentPic])}>
                            <Octicons name='mirror' size={30} />
                        </Button>
                        <Button style={{ backgroundColor: 'purple' }}
                            onPress={() => alert('double tap on image to zoom')}>
                            <Foundation name='zoom-in' size={30} color="white"></Foundation>
                        </Button>
                        <Button style={{ backgroundColor: '#DD5144' }}
                            onPress={() =>
                                Alert.alert(
                                    'Delete current picture?',
                                    '',
                                    [{
                                        text: 'Cancel',
                                        onPress: () => { },
                                        style: 'cancel',
                                    },
                                    { text: 'OK', onPress: () => confirmDelete(gallaryPic[currentPic]) },
                                    ],
                                    { cancelable: true },
                                )
                            }>
                            <MaterialIcons name='delete' size={30} color="white" />
                        </Button>
                    </Fab>
                }
            </View>

            {openGridView ? null :
                <Footer>
                    <FooterTab>
                        <Button active onPress={() => previousPic()}>
                            <Text>Previous</Text>
                        </Button>
                        <Button active onPress={() => nextPic()}>
                            <Text>Next</Text>
                        </Button>
                    </FooterTab>
                </Footer>
            }

            {openGridView ?
                <View style={{
                    position: 'absolute',
                    bottom: 0,
                    right: 0,
                    left: 0,
                    top: 0,
                    backgroundColor: 'white',
                    zIndex: 5,
                }}>
                    <Header style={{ marginTop: 25 }}>
                        <Left>
                            <Button transparent onPress={() => setopenGridView(false)}>
                                <Ionicons name='ios-arrow-round-back' size={40} color="white"></Ionicons>
                            </Button>
                        </Left>
                        <Body style={{ alignItems: 'center' }}>
                            <Title>Gallary</Title>
                        </Body>
                        <Right>
                            {selectedPic.length > 0 ?
                                <Button transparent onPress={() =>
                                    Alert.alert(
                                        'Delete selected pictures?',
                                        '',
                                        [{
                                            text: 'Cancel',
                                            onPress: () => { },
                                            style: 'cancel',
                                        },
                                        { text: 'OK', onPress: () => deletePics() },
                                        ],
                                        { cancelable: true },
                                    )}>
                                    <MaterialIcons name='delete' size={40} color="white" />
                                </Button>
                                : null}
                        </Right>
                    </Header>
                    <ScrollView>
                        {gallaryPic.length > 0 ?
                            <Grid>
                                <Col>
                                    {gallaryPic.map((item, index) => {
                                        const { id, uri } = item
                                        return (
                                            index % 2 === 0 ?
                                                <Row key={id} style={{ justifyContent: 'center', marginVertical: 5 }}>
                                                    <Button transparent
                                                        style={{
                                                            height: picWidth, width: picWidth, borderColor: 'green',
                                                            borderWidth: selectedPic.includes(id) ? 2 : 0
                                                        }}
                                                        onPress={() => togglePicHighlight(id)}
                                                        onLongPress={() => picLongPress(index)}>
                                                        <Image style={{ height: '100%', width: '100%', resizeMode: 'cover' }}
                                                            source={{ uri: uri }} />
                                                    </Button>
                                                </Row>
                                                : null)
                                    })}
                                </Col>
                                <Col>
                                    {gallaryPic.map((item, index) => {
                                        const { id, uri } = item
                                        return (
                                            index % 2 === 1 ?
                                                <Row key={id} style={{ justifyContent: 'center', marginVertical: 5 }}>
                                                    <Button transparent
                                                        style={{
                                                            height: picWidth, width: picWidth, borderColor: 'green',
                                                            borderWidth: selectedPic.includes(id) ? 2 : 0
                                                        }}
                                                        onPress={() => togglePicHighlight(id)}
                                                        onLongPress={() => picLongPress(index)}>
                                                        <Image style={{ height: '100%', width: '100%', resizeMode: 'cover' }}
                                                            source={{ uri: uri }} />
                                                    </Button>
                                                </Row>
                                                : null)
                                    })}
                                </Col>
                            </Grid>
                            : null}
                    </ScrollView>
                </View> : null}
        </Container>
    )
}

reference:
https://docs.expo.io/versions/latest/sdk/imagemanipulator/
https://docs.expo.io/versions/latest/sdk/media-library/
http://chuanshuoge2.blogspot.com/2020/02/expo-image-editor-3-share.html