With Canvas, we have one flattened graphic. It’s not multiple elements displayed on top of each other, like it would be if we just used HTML and CSS. Since it’s one thing, we never have to worry about it looking any different than it does right now. And -this part is pretty exciting- there’s a way to save this graphic to be used later.
reference:
node node_modules/react-scripts/scripts/start.js
https://github.com/facebook/create-react-app/issues/8221
css filter
https://codepen.io/joshuajcollinsworth/pen/bKXoRN
react canvas
https://blog.cloudboost.io/using-html5-canvas-with-react-ff7d93f5dc76
canvas filter
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter
canvas scale
https://stackoverflow.com/questions/23104582/scaling-an-image-to-fit-on-canvas
canvas clear
https://stackoverflow.com/questions/2142535/how-to-clear-the-canvas-for-redrawing
file-saver
https://www.npmjs.com/package/file-saver
http://chuanshuoge2.blogspot.com/2019/01/node-upload-to-cloud-download-from-cloud.html
file upload temporary store
https://medium.com/@650egor/react-30-day-challenge-day-2-image-upload-preview-2d534f8eaaa
https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
Saturday, 29 February 2020
Friday, 28 February 2020
expo image editor 5 crop
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
Thursday, 27 February 2020
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
reference:
https://exchangeratesapi.io/
https://www.coindesk.com/coindesk-api
{"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":"$","rate":"8,981.1483","description":"United States Dollar","rate_float":8981.1483},"GBP":{"code":"GBP","symbol":"£","rate":"6,943.3258","description":"British Pound Sterling","rate_float":6943.3258},"EUR":{"code":"EUR","symbol":"€","rate":"8,265.7909","description":"Euro","rate_float":8265.7909}}}
reference:
https://exchangeratesapi.io/
https://www.coindesk.com/coindesk-api
Tuesday, 25 February 2020
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
Subscribe to:
Posts (Atom)