code link: https://github.com/chuanshuoge6/Expo-camera
project link: https://expo.io/@chuanshuoge/expo-medialibrary
standalone apk: https://github.com/chuanshuoge6/Expo-camera/blob/master/expo-medialibrary-fc5ce44f999d4f2c948ad85908942c92-signed.apk
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