Friday, 28 February 2020

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

No comments:

Post a Comment