在我的Expo应用程序中的特定屏幕上,您可以拍摄图像或使用expo API从相机胶卷中进行选择,然后进行预览。在预览中,我有一个叠加层,用户可以在其上触摸图像上的一点,在相应的X,Y轴上显示点/标记。
我希望能够放大图像并更精确地放置标记。
到目前为止,我已经尝试过:
Scrollview-有点用,但是保存图像并再次查看后,标记不再位于正确的位置。
https://kmagiera.github.io/react-native-gesture-handler/docs/handler-pan.html:这可以更好地缩放并保持准确性,但是一旦缩放,我将无法平移缩放后的图像。
3。https://github.com/ascoders/react-native-image-zoom:当您尝试平移并失去标记位置时,它会出现小故障。
理想情况下,我正在考虑使用选项2,但需要进行更改,以便可以平移图像。
有人在工作中有类似的例子或建议吗?
import React from "react";
import {
Container,
Text,
Button,
View,
Icon,
StyleProvider,
} from "native-base";
import {
ImageBackground,
Image,
TouchableOpacity,
TextInput,
Animated,
StyleSheet,
Dimensions,
} from "react-native";
import {
PinchGestureHandler,
PanGestureHandler,
ScrollView,
} from "react-native-gesture-handler";
import styles from "./ShotLoggerStyles";
import texts from "../../Style/texts";
import getTheme from "../../native-base-theme/components";
import marksmenOne from "../../native-base-theme/variables/marksmenOne";
import HeaderComponent from "../../Components/Header/Header";
import FooterComponent from "../../Components/Footer/FooterComponent";
import ChangeWeapon from "../../Components/ChangeWeapon";
import Loader from "../../Components/Loader/Loader";
const mainBG = require("../../assets/Backgrounds/metal.jpg");
const contentBG = require("../../assets/Backgrounds/training.jpg");
const windowWidth = Dimensions.get("window").width;
const ShotLoggerView = props => {
const {
leftPress,
log,
addShot,
removeShot,
updateShots,
updateScore,
changeWeaponHandler,
_takeImageHandler,
saveLog,
weaponModalVisible,
toggleWeaponModal,
loading,
displayHeight,
pinchRef,
rotationRef,
_onPinchGestureEvent,
_onPinchHandlerStateChange,
_scale,
panRef,
_onTiltGestureEvent,
_onTiltGestureStateChange,
minDist,
minPointers,
maxPointers,
avgTouches,
_tiltStr,
} = props;
return (
<Container>
<StyleProvider style={getTheme(marksmenOne)}>
<ImageBackground source={mainBG} style={styles.mainBG}>
<Loader visible={loading} />
<HeaderComponent
headerTitle="Logging Shots"
leftPress={leftPress}
leftAction="close"
/>
<View style={styles.content}>
{!log.image ? (
<>
<ChangeWeapon
weaponModalVisible={weaponModalVisible}
onChangeWeapon={changeWeaponHandler}
toggleWeaponModal={toggleWeaponModal}
/>
<ImageBackground style={styles.noImage} source={contentBG}>
<Text style={texts.headerTitle}>
Take a picture of your target
</Text>
<Button
icon
style={styles.buttonPrimary}
onPress={() => _takeImageHandler()}
>
<Icon style={styles.buttonIcon} name="camera" />
</Button>
</ImageBackground>
</>
) : (
<>
<View style={styles.details}>
<View style={styles.detailsItem}>
<Text style={texts.bodyWhite}>Shots</Text>
<TextInput
style={styles.input}
defaultValue={log.shots.toString()}
editable={false}
/>
</View>
<View style={styles.detailsItem}>
<Text style={texts.bodyWhite}>Score</Text>
<TextInput
style={styles.input}
onChangeText={value => updateScore(value)}
clearTextOnFocus
keyboardType="number-pad"
returnKeyType="done"
/>
</View>
</View>
<ScrollView>
<PanGestureHandler
ref={panRef}
onGestureEvent={_onTiltGestureEvent}
onHandlerStateChange={_onTiltGestureStateChange}
// onGestureEvent={this._onDragGestureEvent}
// onHandlerStateChange={this._onDragHandlerStateChange}
minDist={1}
minPointers={2}
maxPointers={2}
avgTouches
>
<Animated.View
style={[
styles.style2,
{
transform: [
{ perspective: 200 },
{ scale: _scale },
{ rotateX: _tiltStr },
],
},
]}
collapsable={false}
>
<PinchGestureHandler
ref={pinchRef}
simultaneousHandlers={rotationRef}
onGestureEvent={_onPinchGestureEvent}
onHandlerStateChange={_onPinchHandlerStateChange}
>
<Animated.View
style={[
styles.style2,
{
transform: [
{ perspective: 200 },
{ scale: _scale },
{ rotateX: _tiltStr },
],
},
]}
collapsable={false}
>
<TouchableOpacity
activeOpacity={1}
style={styles.pointer}
onPress={evt => addShot(evt.nativeEvent)}
>
<View
style={[
styles.instructions,
log.heatmap.length > 0
? { zIndex: -5 }
: { zIndex: 20 },
]}
>
<Text style={texts.headerBody}>
Tap the picture to add & remove shots.
</Text>
</View>
<Animated.Image
style={{
width: "100%",
minHeight: props.displayHeight,
height: "auto",
}}
source={{
uri: `data:image/jpg;base64,${log.image}`,
}}
/>
{log.heatmap.length > 0 &&
log.heatmap.map((shot, index) => {
return (
<TouchableOpacity
key={index}
onPress={() => removeShot(index)}
style={[
styles.shot,
{ left: shot.x - 10, top: shot.y - 10 },
]}
/>
);
})}
</TouchableOpacity>
</Animated.View>
</PinchGestureHandler>
</Animated.View>
</PanGestureHandler>
</ScrollView>
</>
)}
</View>
<FooterComponent
rightAction={saveLog}
rightActionText="Save shots"
rightActionDisable={!log.image}
/>
</ImageBackground>
</StyleProvider>
</Container>
);
};
export default ShotLoggerView;
const styles2 = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
backgroundColor: "black",
overflow: "hidden",
alignItems: "center",
flex: 1,
justifyContent: "center",
},
pinchableImage: {
width: 250,
height: 250,
},
wrapper: {
flex: 1,
},
});
和
/* eslint-disable no-underscore-dangle */
import React, { Component } from "react";
import { Alert, Dimensions, Animated, PanResponder } from "react-native";
import * as ImagePicker from "expo-image-picker";
import Constants from "expo-constants";
import { State } from "react-native-gesture-handler";
import * as Permissions from "expo-permissions";
import * as ImageManipulator from "expo-image-manipulator";
import * as Segment from "expo-analytics-segment";
import ShotLoggerView from "../ShotLoggerView";
import ENV from "../../../env";
import { USE_NATIVE_DRIVER } from "../../../config";
const windowWidth = Dimensions.get("window").width;
const circleRadius = 30;
Segment.initialize({
androidWriteKey: ENV.SEGMENT_ANDROID_KEY,
iosWriteKey: ENV.SEGMENT_IOS_KEY,
});
class ShotLoggerContainer extends Component {
_panResponder = {};
_previousLeft = 0;
_previousTop = 0;
_circleStyles = {};
panRef = React.createRef();
pinchRef = React.createRef();
tapRef = React.createRef();
constructor(props) {
super(props);
/* Panning */
this._touchX = new Animated.Value(windowWidth / 2 - circleRadius);
this._translateX = Animated.add(
this._touchX,
new Animated.Value(-circleRadius)
);
this._onPanGestureEvent = Animated.event(
[
{
nativeEvent: {
x: this._touchX,
},
},
],
{ useNativeDriver: USE_NATIVE_DRIVER }
);
/* Pinching */
this._baseScale = new Animated.Value(1);
this._pinchScale = new Animated.Value(1);
this._scale = Animated.multiply(this._baseScale, this._pinchScale);
this._lastScale = 1;
this._onPinchGestureEvent = Animated.event(
[{ nativeEvent: { scale: this._pinchScale } }],
{ useNativeDriver: USE_NATIVE_DRIVER }
);
/* Tilt */
this._tilt = new Animated.Value(0);
this._tiltStr = this._tilt.interpolate({
inputRange: [-501, -500, 0, 1],
outputRange: ["1rad", "1rad", "0rad", "0rad"],
});
this._lastTilt = 0;
this._onTiltGestureEvent = Animated.event(
[{ nativeEvent: { translationY: this._tilt } }],
{ useNativeDriver: USE_NATIVE_DRIVER }
);
this.state = {
weaponModalVisible: false,
log: {
heatmap: [],
shots: 0,
score: 0,
weapon_id: 0,
diary_id: 0,
image: undefined,
imageSize: {
width: 768,
height: 1024,
},
},
loading: false,
displayHeight: 1024,
};
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd,
onPanResponderTerminate: this._handlePanResponderEnd,
});
this._previousLeft = 20;
this._previousTop = 84;
this._circleStyles = {
style: {
left: this._previousLeft,
top: this._previousTop,
backgroundColor: "green",
},
};
}
componentDidMount() {
this.getPermissionAsync();
this.state.log.weapon_id = this.props.shootingSession.currentWeapon.id;
this.state.log.diary_id = this.props.shootingSession.id;
this._updateNativeStyles();
}
getPermissionAsync = async () => {
if (Constants.platform.ios) {
const { status } = await Permissions.askAsync(
Permissions.CAMERA_ROLL,
Permissions.CAMERA
);
if (status !== "granted") {
Alert.alert(
"Sorry, we need camera and camera roll permissions to make this work!"
);
}
}
};
_onTapHandlerStateChange = ({ nativeEvent }) => {
if (nativeEvent.oldState === State.ACTIVE) {
// Once tap happened we set the position of the circle under the tapped spot
this._touchX.setValue(nativeEvent.x);
}
};
_onPinchHandlerStateChange = event => {
if (event.nativeEvent.oldState === State.ACTIVE) {
this._lastScale *= event.nativeEvent.scale;
this._baseScale.setValue(this._lastScale);
this._pinchScale.setValue(1);
}
};
_onTiltGestureStateChange = event => {
if (event.nativeEvent.oldState === State.ACTIVE) {
this._lastTilt += event.nativeEvent.translationY;
this._tilt.setOffset(this._lastTilt);
this._tilt.setValue(0);
}
};
changeWeaponHandler = () => {
this.setState({
weaponModalVisible: true,
});
};
toggleWeaponModal = () => {
this.setState({
weaponModalVisible: false,
});
};
_takeImageHandler = async () => {
this.setState({
loading: true,
});
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: false,
aspect: [4, 3],
quality: 1,
exif: true,
});
if (!result.cancelled) {
const manipResult = await ImageManipulator.manipulateAsync(
result.uri,
[{ resize: { height: 1024 } }],
{
compress: 0.9,
format: ImageManipulator.SaveFormat.JPEG,
base64: true,
}
);
this.state.log.image = manipResult.base64;
// Calculate resized width
this.state.log.imageSize.width = (1024 * result.width) / result.height;
const deviceWidth = Dimensions.get("window").width;
const displayHeight = (deviceWidth * result.height) / result.width;
this.setState({
loading: false,
displayHeight,
});
this.forceUpdate();
}
};
saveLog = () => {
Alert.alert("Save Log", "Are you sure you want to save this shot log?", [
{
text: "Cancel",
style: "cancel",
},
{
text: "CONFIRM",
onPress: () => {
this.realSave();
},
},
]);
};
realSave = async () => {
this.setState({
loading: true,
});
this.props.shootingSession.logs.push(this.state.log);
this.props.shootingSession.score =
Number(this.props.shootingSession.score) + Number(this.state.log.score);
this.props.shootingSession.shots =
this.props.shootingSession.shots + this.state.log.shots;
this.props
.addDiaryLog(this.props.user.auth, this.state.log)
.then(result => {
Segment.trackWithProperties("Shots logging saved", {
version: "Beta",
});
this.goBack();
})
.catch(error => {
this.setState({
loading: false,
});
console.log("error", error);
});
};
addShot = event => {
const logA = JSON.parse(JSON.stringify(this.state.log));
logA.heatmap.push({ x: event.locationX, y: event.locationY, value: 85 });
logA.shots++;
this.setState({ log: logA });
};
removeShot = index => {
const logR = JSON.parse(JSON.stringify(this.state.log));
logR.heatmap.splice(index, 1);
logR.shots--;
this.setState({ log: logR });
};
updateShots = value => {
this.state.log.shots = value;
};
updateScore = value => {
this.state.log.score = value;
};
goBack = () => {
this.props.navigation.goBack();
};
leftPress = () => {
Alert.alert("Abort drill?", "Image and data will be lost.", [
{
text: "Cancel",
style: "cancel",
},
{
text: "Abort",
onPress: () => {
Segment.trackWithProperties("Shots logging cancel", {
version: "Beta",
});
this.goBack();
},
},
]);
};
_highlight = () => {
this._circleStyles.style.backgroundColor = "blue";
this._updateNativeStyles();
};
_unHighlight = () => {
this._circleStyles.style.backgroundColor = "green";
this._updateNativeStyles();
};
_updateNativeStyles = () => {
this.circle && this.circle.setNativeProps(this._circleStyles);
};
_handleStartShouldSetPanResponder = (e, gestureState) => {
// Should we become active when the user presses down on the circle?
return true;
};
_handleMoveShouldSetPanResponder = (e, gestureState) => {
// Should we become active when the user moves a touch over the circle?
return true;
};
_handlePanResponderGrant = (e, gestureState) => {
this._highlight();
};
_handlePanResponderMove = (e, gestureState) => {
this._circleStyles.style.left =
this._previousLeft + gestureState.dx * (I18nManager.isRTL ? -1 : 1);
this._circleStyles.style.top = this._previousTop + gestureState.dy;
this._updateNativeStyles();
};
_handlePanResponderEnd = (e, gestureState) => {
this._unHighlight();
this._previousLeft += gestureState.dx * (I18nManager.isRTL ? -1 : 1);
this._previousTop += gestureState.dy;
};
render() {
const { weaponModalVisible } = this.state;
Segment.screenWithProperties("Shotlogger", {
version: "Beta",
});
return (
<ShotLoggerView
setState={data => this.setState(data)}
goBack={this.goBack}
log={this.state.log}
leftPress={this.leftPress}
addShot={event => this.addShot(event)}
removeShot={event => this.removeShot(event)}
updateShots={value => this.updateShots(value)}
updateScore={value => this.updateScore(value)}
_takeImageHandler={this._takeImageHandler}
saveLog={() => this.saveLog()}
changeWeaponHandler={this.changeWeaponHandler}
weaponModalVisible={weaponModalVisible}
toggleWeaponModal={this.toggleWeaponModal}
loading={this.state.loading}
displayHeight={this.state.displayHeight}
_onPinchHandlerStateChange={event =>
this._onPinchHandlerStateChange(event)
}
pinchRef={this.pinchRef}
rotationRef={this.rotationRef}
_onPinchGestureEvent={this._onPinchGestureEvent}
_scale={this._scale}
_tiltStr={this._tiltStr}
panRef={this.panRef}
_onTiltGestureEvent={this._onTiltGestureEvent}
_onTiltGestureStateChange={this._onTiltGestureStateChange}
minDist={this.minDist}
minPointers={this.minPointers}
maxPointers={this.maxPointers}
avgTouches={this.avgTouches}
tapRef={this.tapRef}
panRef={this.panRef}
/>
);
}
}
export default ShotLoggerContainer;
Expo CLI 3.9.1 environment info:
System:
OS: macOS 10.15.1
Shell: 5.7.1 - /bin/zsh
Binaries:
Node: 12.8.0 - ~/.nvm/versions/node/v12.8.0/bin/node
Yarn: 1.19.1 - /usr/local/bin/yarn
npm: 6.11.3 - ~/.nvm/versions/node/v12.8.0/bin/npm
Watchman: 4.9.0 - /usr/local/bin/watchman
IDEs:
Android Studio: 3.5 AI-191.8026.42.35.5791312
Xcode: 11.2/11B41 - /usr/bin/xcodebuild
npmPackages:
@types/react-native: ^0.60.22 => 0.60.22
expo: ^35.0.1 => 35.0.1
react: 16.8.3 => 16.8.3
react-native: https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz => 0.59.8
react-navigation: ^3.13.0 => 3.13.0
npmGlobalPackages:
expo-cli: 3.9.1