I'm new to react so please be nice,
I'm trying to animate my compass, so that every time the userLocation is updated, the arrow (in my code the png of the animated image) is rotated at the given angle (here rotation) so that it points at another location. For some reason, it seems like the rotation passed to the Animated.Image remains 0, because the image never rotates. Can someone land me a hand real quick.
Here's my code:
import {
Alert,
Animated,
Easing,
Linking,
StyleSheet,
Text,
View,
} from "react-native";
import React, { useEffect, useRef, useState } from "react";
import * as Location from "expo-location";
import * as geolib from "geolib";
import { COLORS } from "../../assets/Colors/Colors";
export default function DateFinder() {
const [hasForegroundPermissions, setHasForegroundPermissions] =
useState(null);
const [userLocation, setUserLocation] = useState(null);
const [userHeading, setUserHeading] = useState(null);
const [angle, setAngle] = useState(0);
const rotation = useRef(new Animated.Value(0)).current;
useEffect(() => {
const AccessLocation = async () => {
function appSettings() {
console.warn("Open settigs pressed");
if (Platform.OS === "ios") {
Linking.openURL("app-settings:");
} else RNAndroidOpenSettings.appDetailsSettings();
}
const appSettingsALert = () => {
Alert.alert(
"Allow Wassupp to Use your Location",
"Open your app settings to allow Wassupp to access your current position. Without it, you won't be able to use the love compass",
[
{
text: "Cancel",
onPress: () => console.warn("Cancel pressed"),
},
{ text: "Open settings", onPress: appSettings },
]
);
};
const foregroundPermissions =
await Location.requestForegroundPermissionsAsync();
if (
foregroundPermissions.canAskAgain == false ||
foregroundPermissions.status == "denied"
) {
appSettingsALert();
}
setHasForegroundPermissions(foregroundPermissions.status === "granted");
if (foregroundPermissions.status == "granted") {
const location = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.BestForNavigation,
activityType: Location.ActivityType.Fitness,
distanceInterval: 0,
},
(location) => {
setUserLocation(location);
}
);
const heading = await Location.watchHeadingAsync((heading) => {
setUserHeading(heading.trueHeading);
});
}
};
AccessLocation().catch(console.error);
}, []);
useEffect(() => {
if (userLocation != null) {
setAngle(getBearing() - userHeading);
rotateImage(angle); // Here's the call to the rotateImage function that should cause the value of rotation to be animated
}
}, [userLocation]);
const textPosition = JSON.stringify(userLocation);
const getBearing = () => {
const bearing = geolib.getGreatCircleBearing(
{
latitude: userLocation.coords.latitude,
longitude: userLocation.coords.longitude,
},
{
latitude: 45.47307231766645,
longitude: -73.86611198944459,
}
);
return bearing;
};
const rotateImage = (angle) => {
Animated.timing(rotation, {
toValue: angle,
duration: 1000,
easing: Easing.bounce,
useNativeDriver: true,
}).start();
};
return (
<View style={styles.background}>
<Text>{textPosition}</Text>
<Animated.Image
source={require("../../assets/Compass/Arrow_up.png")}
style={[styles.image, { transform: [{ rotate: `${rotation}deg` }] }]} // this is where it should get rotated but it doesn't for some reason
/>
</View>
);
}
const styles = StyleSheet.create({
background: {
backgroundColor: COLORS.background_Pale,
flex: 1,
// justifyContent: "flex-start",
//alignItems: "center",
},
image: {
flex: 1,
// height: null,
// width: null,
//alignItems: "center",
},
scrollView: {
backgroundColor: COLORS.background_Pale,
},
});
Your error is here
useEffect(() => {
if (userLocation != null) {
setAngle(getBearing() - userHeading);
rotateImage(angle); // Here's the call to the rotateImage function that should cause the value of rotation to be animated
}
}, [userLocation]);
The angle will be updated on the next render, so the rotation you do will always be a render behind. You could either store the result of getBearing and setAngle to that value as well as provide that value to rotateImage:
useEffect(() => {
if (userLocation != null) {
const a = getBearing() -userHeading;
setAngle(a);
rotateImage(a); // Here's the call to the rotateImage function that should cause the value of rotation to be animated
}
}, [userLocation]);
or you could use useEffect and listen for angle changes:
useEffect(() => {
rotateImage(angle)
}, [angle]);
Related
I have a <TextInput> which, when the user enters anything into, will have a button appear from behind it.
This works fine using the Animated library from react, but each time I input text into the area, the animation "rubber bands" up and down.
My question is: How can I keep the button in place after the initial animation happens?
I still need to monitor whether the text box is empty in order to decide whether to show or hide the button.
My code:
import { View, TextInput, StyleSheet, Animated, TouchableOpacity } from "react-native";
import { useState } from "react";
import CurrencyInput from 'react-native-currency-input';
import { Ionicons } from "#expo/vector-icons";
type expenseItemData = {
item:string;
costAndSign:string;
cost:number | null;
}
type Props = {
sendItem: ({ item, cost, costAndSign }: expenseItemData) => void;
}
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
const AddAndConfirmExpense: React.FC<Props> = ({sendItem}) => {
const [animation] = useState(new Animated.Value(-58));
const [expenseValue, setExpenseValue] = useState<number>(0.00);
const [expenseItem, setExpenseItem] = useState<string>("");
const [expenseValueAndSign, SetExpenseValueAndSign] = useState<string>("");
const [buttonLayoutPersistence, setButtonLayoutPersistence] = useState({
bottom:0
});
const [validExpenseItem, setValidExpenseItem] = useState<boolean>(false);
const onChangeExpenseItem = (text: string) => {
setExpenseItem(text);
if (text.trim().length === 0) {
setValidExpenseItem(false);
hideButton();
setButtonLayoutPersistence({bottom:0})
return;
}
if (validExpenseItem) {
setButtonLayoutPersistence({bottom:-48})
return
};
showButton();
setValidExpenseItem(true);
};
const onButtonPress = () => {
const newData:expenseItemData = {
item:expenseItem,
costAndSign:expenseValueAndSign,
cost:expenseValue
}
sendItem(newData);
};
const setAreaDynamicStyling = () => {
if (validExpenseItem) {
return {
borderTopRightRadius:5,
borderTopLeftRadius:5,
backgroundColor:"#f5f5f5"
}
}
return {borderRadius:5};
};
const setButtonDynamicStyling = () => {
if (!validExpenseItem) return {borderRadius:5}
return {borderBottomLeftRadius: 5,borderBottomRightRadius:5}
};
const animatedStyle = {transform: [{translateY:animation}],};
const showButton = () => {
Animated.timing(animation, {
toValue: -10,
duration: 1000,
useNativeDriver: true,
}).start();
}
const hideButton = () => {
Animated.timing(animation, {
toValue: -58,
duration: 500,
useNativeDriver: true,
}).start();
}
return (
<View>
<View style={validExpenseItem ? [styles.inputsContainer, setAreaDynamicStyling()] : [styles.inputsContainer,styles.shadowProp,setAreaDynamicStyling()]}>
<TextInput
style={styles.textInputArea}
placeholder='Item'
placeholderTextColor="#aaaaaa"
onChangeText={(text) => {onChangeExpenseItem(text)}}
value={expenseItem}
underlineColorAndroid="transparent"
autoCapitalize="none"
/>
<View style={styles.verticalLine}/>
<CurrencyInput
style={styles.currencyInputArea}
value={expenseValue}
onChangeValue={setExpenseValue}
prefix="£"
delimiter=","
separator="."
precision={2}
minValue={0}
onChangeText={(formattedValue) => {
SetExpenseValueAndSign(formattedValue)
}}
/>
</View>
<AnimatedTouchable onPress={()=>{onButtonPress()}} style={[{flex:1, zIndex:-1},animatedStyle]}>
<View style={[styles.confirmInputContainer, setButtonDynamicStyling(), buttonLayoutPersistence]}>
<Ionicons name="checkmark-circle-outline" size={24} color="white" />
</View>
</AnimatedTouchable>
</View>
)
}
const styles = StyleSheet.create({
confirmInputContainer:{
backgroundColor:"#7f96ff",
height: 48,
flexDirection:"row",
paddingVertical:10,
justifyContent:"center",
},
inputsContainer:{
backgroundColor:"white",
height: 48,
flexDirection:"row",
paddingVertical:10,
marginVertical:10,
justifyContent:"space-between",
},
shadowProp: {
shadowColor: '#353935',
shadowOffset: {width: -2, height: 4},
shadowOpacity: 0.2,
shadowRadius: 4,
},
textInputArea:{
width:"60%",
maxWidth:"60%",
marginLeft:20,
},
verticalLine:{
height: "100%",
width: 1,
backgroundColor: "#909090",
marginHorizontal:5,
},
currencyInputArea:{
maxWidth:"20%",
width:"20%",
marginRight:20
},
})
export default AddAndConfirmExpense;
EDIT:
I have added:
const [animationActive, setAnimationActive] = useState(false);
useEffect(() => {
if (!animationActive && validExpenseItem) {
setButtonLayoutPersistence({bottom:-48})
};
if (!animationActive && !validExpenseItem) {
setButtonLayoutPersistence({bottom:0})
}
},[animationActive])
And changed my show and hide functions to the following:
const showButton = () => {
if (animationActive) return;
setAnimationActive(true)
Animated.timing(animation, {
toValue: -10,
duration: 500,
useNativeDriver: true,
}).start(
() => {
setAnimationActive(false)
}
);
}
const hideButton = () => {
if (animationActive) return;
setAnimationActive(true)
Animated.timing(animation, {
toValue: -58,
duration: 500,
useNativeDriver: true,
}).start(
() => {
setAnimationActive(false)
}
);
}
I have also changed onChangeExpenseItem to :
const onChangeExpenseItem = (text: string) => {
setExpenseItem(text);
if (text.trim().length === 0) {
setValidExpenseItem(false);
hideButton();
setButtonLayoutPersistence({bottom:0})
return;
}
if (!validExpenseItem && !animationActive) {
setValidExpenseItem(true);
showButton();
return
};
};
It is slightly better now, but a better solution is still needed.
I have a question of something that looks pretty obvious but It's getting hard for me. I know that for fetching data that will get actually rendered in a component you need to use reacthooks and useState. However I am having a problem because I need to fetch some data and then store it in a variable that it's not part of the component rendering. This is my current code.
import React from 'react'
import { GoogleMap, useJsApiLoader } from '#react-google-maps/api';
import { GoogleMapsOverlay } from "#deck.gl/google-maps";
import {GeoJsonLayer, ArcLayer} from '#deck.gl/layers';
import axios from 'axios';
import {useState} from 'react';
const hasWindow = typeof window !== 'undefined';
function getWindowDimensions() {
const width = hasWindow ? window.innerWidth : null;
const height = hasWindow ? window.innerHeight : null;
return {
width,
height,
};
}
const center = {
lat: 51.509865,
lng: -0.118092
};
const deckOverlay = new GoogleMapsOverlay({
layers: [
new GeoJsonLayer({
id: "airports",
data: markers,
filled: true,
pointRadiusMinPixels: 2,
opacity: 1,
pointRadiusScale: 2000,
getRadius: f => 11 - f.properties.scalerank,
getFillColor: [200, 0, 80, 180],
pickable: true,
autoHighlight: true
}),
new ArcLayer({
id: "arcs",
data: markers,
dataTransform: d => d.features.filter(f => f.properties.scalerank < 4),
getSourcePosition: f => [-0.4531566, 51.4709959], // London
getTargetPosition: f => f.geometry.coordinates,
getSourceColor: [0, 128, 200],
getTargetColor: [200, 0, 80],
getWidth: 1
})
]
});
export default function Map() {
const { isLoaded } = useJsApiLoader({
id: 'lensmap',
googleMapsApiKey: "YOUR_API_KEY"
})
const onLoad = React.useCallback(function callback(map) {
deckOverlay.setMap(map)
}, [])
const onUnmount = React.useCallback(function callback(map) {
}, [])
return isLoaded ? (
<GoogleMap
mapContainerStyle={getWindowDimensions()}
center={center}
zoom={10}
onLoad={onLoad}
onUnmount={onUnmount}
>
<></>
</GoogleMap>
) : <></>
}
As you can see GoogleMapsOverlay receives a markers object in it's constructor, here I would get my markers doing a call to an API using axios but everything that I've tested ends in a 500 code when loading the page.
I assume that you're asking for a way to fetch the markers and make everything load in the correct order. I think you could store the deckOverlay instance in a ref, fetch the markers in a useEffect hook, update the layers with the markers data, and set a flag to hold from rendering the map until the layers are updated.
import React, { useState, useRef, useEffect, useCallback } from "react";
import { GoogleMap, useJsApiLoader } from "#react-google-maps/api";
import { GoogleMapsOverlay } from "#deck.gl/google-maps";
import { GeoJsonLayer, ArcLayer } from "#deck.gl/layers";
import axios from "axios";
const hasWindow = typeof window !== "undefined";
function getWindowDimensions() {
const width = hasWindow ? window.innerWidth : null;
const height = hasWindow ? window.innerHeight : null;
return {
width,
height,
};
}
const center = {
lat: 51.509865,
lng: -0.118092,
};
export default function Map() {
const { isLoaded } = useJsApiLoader({
id: "lensmap",
googleMapsApiKey: "AIzaSyBmSBtlYQLH8jvAxrdgZErUdtdWLEs40gk",
});
const [markersLoaded, setMarkersLoaded] = useState(false);
const deckOverlay = useRef(new GoogleMapsOverlay({ layers: [] }));
const fecthMarkers = useCallback(async () => {
try {
const response = await axios.get(`someapi.com/markers`);
// assuming API response will have a markers field
const markers = response.data.markers;
deckOverlay.current.setProps({
layers: [
new GeoJsonLayer({
id: "airports",
data: markers,
filled: true,
pointRadiusMinPixels: 2,
opacity: 1,
pointRadiusScale: 2000,
getRadius: (f) => 11 - f.properties.scalerank,
getFillColor: [200, 0, 80, 180],
pickable: true,
autoHighlight: true,
}),
new ArcLayer({
id: "arcs",
data: markers,
dataTransform: (d) =>
d.features.filter((f) => f.properties.scalerank < 4),
getSourcePosition: (f) => [-0.4531566, 51.4709959], // London
getTargetPosition: (f) => f.geometry.coordinates,
getSourceColor: [0, 128, 200],
getTargetColor: [200, 0, 80],
getWidth: 1,
}),
],
});
setMarkersLoaded(true);
} catch (e) {
// TODO: show some err UI
console.log(e);
}
}, []);
useEffect(() => {
fecthMarkers();
},[]);
const onLoad = React.useCallback(function callback(map) {
deckOverlay.current?.setMap(map);
}, []);
const onUnmount = React.useCallback(function callback(map) {
deckOverlay.current?.finalize();
}, []);
return markersLoaded && isLoaded ? (
<GoogleMap
mapContainerStyle={getWindowDimensions()}
center={center}
zoom={10}
onLoad={onLoad}
onUnmount={onUnmount}
>
<></>
</GoogleMap>
) : (
<></>
);
}
While it's a good idea to use a ref in most cases, it's not technically needed in this case, if there's just 1 instance of the component on the page. The important part is that you use an effect, which can run any JS and interact with any function / variable that is in scope.
Also important to know is that you need to add setMarkersLoaded(true); at the end to ensure a new render happens, if you want one to happen. If you don't need a render to happen (e.g. here if the map was already displayed regardless of whether the markers loaded), you can remove this part.
diedu's answer uses useCallback to create the async handler (fetchMarkers) used in useEffect, however you don't need to use this hook here. The function is written to ever be called just once, and is not passed to any component. useCallback is only for when you find a new function being created causes a component to re-render that otherwise wouldn't.
It's better to define the data fetching function outside of the component, so that you can keep the effect code simple and readable. You can even map it to layers in that function, and so remove another large chunk of logic out of your Map component.
useEffect(() => {
(async () {
const layers = await fetchMarkerLayers();
deckOverlay.current.setProps({layers});
setMarkersLoaded(true);
})();
},[]);
Because the argument of useEffect can not be an async function, you need put a self invoking async function inside. If you don't like that syntax, you could also chain promises with .then. Both syntaxes are a bit hairy, but because we extracted the complex logic out of the component, it's still readable.
Full code
I kept some parts of diedu's snippet, like how the ref is used, as they didn't need changes.
import React, { useState, useRef, useEffect, useCallback } from "react";
import { GoogleMap, useJsApiLoader } from "#react-google-maps/api";
import { GoogleMapsOverlay } from "#deck.gl/google-maps";
import { GeoJsonLayer, ArcLayer } from "#deck.gl/layers";
import axios from "axios";
const hasWindow = typeof window !== "undefined";
function getWindowDimensions() {
const width = hasWindow ? window.innerWidth : null;
const height = hasWindow ? window.innerHeight : null;
return {
width,
height,
};
}
const center = {
lat: 51.509865,
lng: -0.118092,
};
const fetchMarkerLayers = async () => {
try {
const response = await axios.get(`someapi.com/markers`);
// assuming API response will have a markers field
const { markers } = response.data;
return [
new GeoJsonLayer({
id: "airports",
data: markers,
filled: true,
pointRadiusMinPixels: 2,
opacity: 1,
pointRadiusScale: 2000,
getRadius: (f) => 11 - f.properties.scalerank,
getFillColor: [200, 0, 80, 180],
pickable: true,
autoHighlight: true,
}),
new ArcLayer({
id: "arcs",
data: markers,
dataTransform: (d) =>
d.features.filter((f) => f.properties.scalerank < 4),
getSourcePosition: (f) => [-0.4531566, 51.4709959], // London
getTargetPosition: (f) => f.geometry.coordinates,
getSourceColor: [0, 128, 200],
getTargetColor: [200, 0, 80],
getWidth: 1,
}),
]
} catch (e) {
// TODO: show some err UI
console.log(e);
}
};
export default function Map() {
const { isLoaded } = useJsApiLoader({
id: "lensmap",
googleMapsApiKey: "AIzaSyBmSBtlYQLH8jvAxrdgZErUdtdWLEs40gk",
});
const [markersLoaded, setMarkersLoaded] = useState(false);
const deckOverlay = useRef(new GoogleMapsOverlay({ layers: [] }));
useEffect(() => {
// Use a self invoking async function because useEffect's argument function cannot be async.
// Alternatively you can chain a regular Promise with `.then(layers => ...)`.
(async () {
const layers = await fetchMarkerLayers();
deckOverlay.current.setProps({layers});
setMarkersLoaded(true);
})();
},[]);
const onLoad = React.useCallback(function callback(map) {
deckOverlay.current?.setMap(map);
}, []);
const onUnmount = React.useCallback(function callback(map) {
deckOverlay.current?.finalize();
}, []);
return markersLoaded && isLoaded ? (
<GoogleMap
mapContainerStyle={getWindowDimensions()}
center={center}
zoom={10}
onLoad={onLoad}
onUnmount={onUnmount}
>
<></>
</GoogleMap>
) : (
<></>
);
}
I want to switch back and forth between two React components as shown below.
Every time the Photo Component is invoked and the user has taken a picture. I want the Game component to mount again (thus passing condition counter state variable as props, to be changed) before the photo component unmounts.
React is yelling at me for Warning: Can't perform a React state update on an unmounted component.
I understand the problem that my Photo component unmounts first before the state has updated.
I have looked into several similar questions and try to adopt their solutions e.g. having an additional variable to check if the component is mounted or not and few others.
Seems like none helped me.
Game.js
import React, { useState, useEffect } from 'react';
import { TextInput, Platform, Text, View, StyleSheet, Button, Alert } from 'react-native';
import Constants from 'expo-constants';
import * as Location from 'expo-location';
import * as Permissions from 'expo-permissions';
import MapView, { Marker, Circle, Polyline } from 'react-native-maps';
import Header from '../Components/Header';
import Footer from '../Components/Footer';
import PhotoHandler from './Photo';
import * as geolib from 'geolib';
import { event } from 'react-native-reanimated';
export default function GameArea() {
const [isFetching, setIsFetching] = useState(false);
const [generateTargets, setGenerateTargets] = useState(true);
const [targets, setTargets] = useState([]);
const [distances, setDistances] = useState([]);
const [index, setIndex] = useState(null);
const [gesture, setGesture] = useState({});
const [draggingMap, setdraggingMap] = useState(false);
const [numofTargets, setNumofTargets] = useState(null);
const [conditionCounter, setConditionCounter] = useState(false);
const [photoTaken, setPhotoTaken] = useState(false);
const [pickedLocation, setPickedLocation] = useState({
latitude: 123,
longitude: 123
});
const [errorMsg, setErrorMsg] = useState(null);
const [region, setRegion] = useState({
latitude: 123,
longitude: 123,
latitudeDelta: 0.001,
longitudeDelta: 0.001
});
const [mapBounds, setMapBounds] = useState({
n: null,
s: null,
e: null,
w: null
});
const [mapRef, updateMapRef] = useState(null);
useEffect(() => {
const verifyPermissions = async () => {
const result = await Permissions.askAsync(Permissions.LOCATION);
if (result.status !== 'granted') {
Alert.alert(
'Insufficient permissions!',
'You need to grant location permissions to use this app.',
[{ text: 'Okay' }]
);
return false;
}
return true;
};
(async () => {
const hasPermission = await verifyPermissions();
if (!hasPermission) {
return;
}
try {
setIsFetching(true);
const location = await Location.getCurrentPositionAsync({
});
setPickedLocation({
latitude: location.coords.latitude,
longitude: location.coords.longitude
});
setRegion({
latitude: location.coords.latitude,
longitude: location.coords.longitude,
latitudeDelta: 0.003,
longitudeDelta: 0.003
});
} catch (err) {
Alert.alert(
'Could not fetch location!',
'Please try again.',
[{ text: 'Okay' }]
);
}
setIsFetching(false);
})();
}, []);
const getBoundaries = () => {
if (mapRef === null) {
return;
}
mapRef
.getMapBoundaries()
.then((res) => {
setMapBounds({
n: res.northEast.latitude,
e: res.northEast.longitude,
s: res.southWest.latitude,
w: res.southWest.longitude
});
})
.catch((err) => console.log(err));
};
const onRegionChangeComplete = (region) => {
if (draggingMap === true && targets.length == 0) {
getBoundaries();
setRegion({
latitude: region.latitude,
longitude: region.longitude,
latitudeDelta: region.latitudeDelta,
longitudeDelta: region.longitudeDelta
})
setdraggingMap(false);
}
else {
return;
}
}
function handleChange(newValue) {
setConditionCounter(newValue);
}
const ondraggingMap = () => {
setdraggingMap(true);
}
async function generatedTargets() {
var url = `https://xxx.yy.zz`;
if (numofTargets == null) {
Alert.alert(
"Missing",
"Enter Number of targets first",
[
{ text: "OK" }
]
);
}
else {
try {
let response = await fetch(
url,
);
let responseJson = await response.json();
console.log("Targets generated");
console.log(responseJson);
setTargets(responseJson);
setGenerateTargets(false);
} catch (error) {
Alert.alert(
"Failed: Network Error",
"Try again",
[
{ text: "OK" }
]
);
console.error(error);
}
}
}
useEffect(() => {
console.log('inside useEffect hook for measuring targets distances');
var lat1 = pickedLocation.latitude;
var lng1 = pickedLocation.longitude;
console.log("targets distance");
const R = 6371;
let targets_distance = [];
for (i = 0; i < targets.length; i++) {
const φ1 = lat1 * Math.PI / 180;
const φ2 = targets[i].lat * Math.PI / 180;
const Δφ = (targets[i].lat - lat1) * Math.PI / 180;
const Δλ = (targets[i].lng - lng1) * Math.PI / 180;
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c * 1000;
console.log('distance is'+ distance);
if(distance <=20){
setConditionCounter(true); //switch to photo component
targets.splice(i, 1);
console.log("targets after removal");
console.log(targets);
}
else{
targets_distance.push(distance);
}
}
console.log(targets_distance);
var index = 0;
var value = targets_distance[0];
for (var i = 1; i < targets_distance.length; i++) {
if (targets_distance[i] < value) {
value = targets_distance[i];
index = i;
}
}
console.log("smallest element is " + value + " at index " + index);
setIndex(index);
setDistances(targets_distance);
}, [pickedLocation]);
useEffect(() => {
console.log("Inside UseEffect watch position");
(async () => {
try {
await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.BestForNavigation,
distanceInterval: 10,
timeInterval: 3000,
},
(loc) => {
setPickedLocation({
latitude: loc.coords.latitude,
longitude: loc.coords.longitude
});
}
);
} catch (e) {
Alert.alert("Error");
}
})();
}, []);
const setNumberOfTargets = (numofTargets) =>{
setNumofTargets(numofTargets);
}
return (
<View style={styles.container}>
<Header title="Crowdsourcing" />
{conditionCounter?
<PhotoHandler conditionCounter={setConditionCounter} onChange={handleChange}/>:
<MapView style={styles.mapContainer}
region={{
latitude: region.latitude,
longitude: region.longitude,
latitudeDelta: region.latitudeDelta,
longitudeDelta: region.longitudeDelta
}}
showsUserLocation={true}
followUserLocation={true}
ref={(ref) => updateMapRef(ref)}
onPanDrag={ondraggingMap}
onRegionChangeComplete={onRegionChangeComplete}
>
{
targets.map((target, index) => (
<MapView.Marker
key={index}
coordinate={{
latitude: target.lat,
longitude: target.lng
}}
title={target.name}
/>
))}
</MapView>}
<Footer
Targets = {generateTargets}
numberOfTargets = {numofTargets}
setNumTargets = {setNumberOfTargets}
osmTargets = {generatedTargets}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
width: '100%',
height: '100%'
},
mapContainer: {
width: '100%',
height: 500,
marginBottom: 20
},
footer: {
width: '100%',
height: 70,
backgroundColor: '#FF7F50',
alignItems: 'center',
justifyContent: 'center'
},
buttonContainer: {
flexDirection: 'row',
width: '100%',
justifyContent: 'space-evenly'
}
});
Photo.js
import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View, Alert, Button, Image } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import * as Permissions from 'expo-permissions';
import * as Location from 'expo-location';
export default function PhotoHandler(props) {
const [pickedLocation, setPickedLocation] = useState({
latitude: null,
longitude: null
});
const [pickedImage, setPickedImage] = useState(null);
const verifyPermissions = async () =>{
const result = await Permissions.askAsync(Permissions.CAMERA_ROLL);
if(result.status != 'granted'){
Alert.alert(
'Insufficient Permissions!',
'You need to grant camera permissions to use this app',
[{text: 'Okay'}]
);
return false;
}
return true;
};
const imagetakenHandler = async () =>{
const hasPermission = await verifyPermissions();
if(!hasPermission){
return;
}
const image = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 1,
aspect: [1, 1],
base64: true,
exif: true
});
setPickedImage(image.base64);
};
useEffect(() => {
const requestOptions = {
method: 'POST',
headers: 'Content-type: application/x-www-form-urlencoded',
};
if(pickedImage != null){
props.onChange(false); // to switch back to Game component
let response = fetch(`https://xxx.yy.zz`, requestOptions)
.then(response => response.json())
.then(console.log('SUCCESS: '+response))
.catch(e => {
console.log(e);
});
}
}, [pickedImage])
return (
<View style={styles.container}>
{
Alert.alert(
"Success",
"Take photo",
[
{text: "Open Camera",
onPress: imagetakenHandler
}
]
)
}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 30
}
});
this is my react map component:
import 'mapbox-gl/dist/mapbox-gl.css';
import './switcher/switcher.css';
import mapboxgl from 'mapbox-gl';
import React, { useRef, useLayoutEffect, useEffect, useState } from 'react';
import { deviceCategories } from '../common/deviceCategories';
import { loadIcon, loadImage } from './mapUtil';
import { styleCarto} from './mapStyles';
import { useAttributePreference } from '../common/preferences';
const element = document.createElement('div');
element.style.width = '100%';
element.style.height = '100%';
export const map = new mapboxgl.Map({
container: element,
style: styleCarto(),
center: [80.379370, 23.846870],
zoom: 4.8
});
let ready = false;
const readyListeners = new Set();
const addReadyListener = listener => {
readyListeners.add(listener);
listener(ready);
};
const removeReadyListener = listener => {
readyListeners.delete(listener);
};
const updateReadyValue = value => {
ready = value;
readyListeners.forEach(listener => listener(value));
};
const initMap = async () => {
const background = await loadImage('images/background.svg');
await Promise.all(deviceCategories.map(async category => {
if (!map.hasImage(category)) {
const imageData = await loadIcon(category, background, `images/icon/car.png`);
map.addImage(category, imageData, { pixelRatio: window.devicePixelRatio });
}
}));
updateReadyValue(true);
};
map.on('load', initMap);
map.addControl(new mapboxgl.NavigationControl({
showCompass: false,
}));
const Map = ({ children }) => {
const containerEl = useRef(null);
const [mapReady, setMapReady] = useState(false);
const mapboxAccessToken = useAttributePreference('mapboxAccessToken');
useEffect(() => {
mapboxgl.accessToken = mapboxAccessToken;
}, [mapboxAccessToken]);
useEffect(() => {
const listener = ready => setMapReady(ready);
addReadyListener(listener);
return () => {
removeReadyListener(listener);
};
}, []);
useLayoutEffect(() => {
const currentEl = containerEl.current;
currentEl.appendChild(element);
if (map) {
map.resize();
}
return () => {
currentEl.removeChild(element);
};
}, [containerEl]);
return (
<div style={{ width: '100%', height: '100%' }} ref={containerEl}>
{mapReady && children}
</div>
);
};
export default Map;
I am fetching coordinates from api endpoint using socket controller, there is redux store that handle the changes in the data, however the position of the icons changes but its not smooth ,i have been trying to make it done since 5 days but i dont find any way how to do it, i am not finding mapbox documentation helpful
Below is the position map component , here the positions are being refreshed and updated to new cordinates, but i want to animate the changes on screen like movement of car on uber/ola app.
import React, { useCallback, useEffect } from 'react';
import ReactDOM from 'react-dom';
import mapboxgl from 'mapbox-gl';
import { Provider, useSelector } from 'react-redux';
import { map } from './Map';
import store from '../store';
import { useHistory } from 'react-router-dom';
import StatusView from './StatusView';
const PositionsMap = ({ positions }) => {
const id = 'positions';
const history = useHistory();
const devices = useSelector(state => state.devices.items);
const createFeature = (devices, position) => {
const device = devices[position.deviceId] || null;
return {
deviceId: position.deviceId,
name: device ? device.name : '',
category: device && (device.category || 'default'),
}
};
const onMouseEnter = () => map.getCanvas().style.cursor = 'pointer';
const onMouseLeave = () => map.getCanvas().style.cursor = '';
const onClickCallback = useCallback(event => {
const feature = event.features[0];
let coordinates = feature.geometry.coordinates.slice();
while (Math.abs(event.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += event.lngLat.lng > coordinates[0] ? 360 : -360;
}
const placeholder = document.createElement('div');
ReactDOM.render(
<Provider store={store}>
<StatusView deviceId={feature.properties.deviceId} onShowDetails={positionId => history.push(`/position/${positionId}`)} />
</Provider>,
placeholder
);
new mapboxgl.Popup({
offset: 20,
anchor: 'bottom-left',
closeButton: false,
className: 'popup'
})
.setDOMContent(placeholder)
.setLngLat(coordinates)
.addTo(map);
}, [history]);
useEffect(() => {
map.addSource(id, {
'type': 'geojson',
'data': {
type: 'FeatureCollection',
features: [],
}
});
map.addLayer({
'id': id,
'type': 'symbol',
'source': id,
'layout': {
'icon-image': '{category}',
'icon-allow-overlap': true,
'text-field': '{name}',
'text-allow-overlap': true,
'text-anchor': 'bottom',
'text-offset': [0, -2],
'text-font': ['Roboto Regular'],
'text-size': 12,
}
});
map.on('mouseenter', id, onMouseEnter);
map.on('mouseleave', id, onMouseLeave);
map.on('click', id, onClickCallback);
return () => {
Array.from(map.getContainer().getElementsByClassName('mapboxgl-popup')).forEach(el => el.remove());
map.off('mouseenter', id, onMouseEnter);
map.off('mouseleave', id, onMouseLeave);
map.off('click', id, onClickCallback);
map.removeLayer(id);
map.removeSource(id);
};
}, [onClickCallback]);
useEffect(() => {
map.getSource(id).setData({
type: 'FeatureCollection',
features: positions.map(position => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [position.longitude, position.latitude]
},
properties: createFeature(devices, position),
}))
});
}, [devices, positions]);
return null;
}
export default PositionsMap;
can any body help on thin to figure what i have been missing
I managed to build a simple music player using react-native-track-player following a tutorial as part of my learning curve in react native. Now, instead of just playing/streaming songs from provided url tracks in my array of songs, I want to stream the songs from API (though it can beb any API - but I have registered for Napster API) which I feel is not limited by just few songs. But, I can't put together how to implement or call the Napster API to fetch songs.
Please any help/guide I will appreciate so much.
Below is my code:
I have data.js, my array of songs:
const songs = [
{
title: "death bed",
artist: "Powfu",
artwork: require("../assets/album-arts/death-bed.jpg"),
url: "https://github.com/ShivamJoker/sample-songs/raw/master/death%20bed.mp3",
id: "1",
},
{
title: "bad liar",
artist: "Imagine Dragons",
artwork: require("../assets/album-arts/bad-liar.jpg"),
url: "https://github.com/ShivamJoker/sample-songs/raw/master/Bad%20Liar.mp3",
id: "2",
},
{
title: "faded",
artist: "Alan Walker",
artwork: require("../assets/album-arts/faded.jpg"),
url: "https://github.com/ShivamJoker/sample-songs/raw/master/Faded.mp3",
id: "3",
},
];
export default songs;
And here's my playerScreen.js :
import React, {useRef, useEffect, useState} from 'react';
import {
View,
SafeAreaView,
Text,
Image,
FlatList,
Dimensions,
Animated,
StyleSheet,
} from 'react-native';
import TrackPlayer, {
Capability,
useTrackPlayerEvents,
usePlaybackState,
TrackPlayerEvents,
STATE_PLAYING,
Event,
} from 'react-native-track-player';
import songs from './data';
import Controller from './Controller';
import SliderComp from './SliderComp';
const {width, height} = Dimensions.get('window');
// const events = [
// TrackPlayerEvents.PLAYBACK_STATE,
// TrackPlayerEvents.PLAYBACK_ERROR
// ];
export default function PlayerScreen() {
const scrollX = useRef(new Animated.Value(0)).current;
const slider = useRef(null);
const isPlayerReady = useRef(false);
const index = useRef(0);
const [songIndex, setSongIndex] = useState(0);
const isItFromUser = useRef(true);
// for tranlating the album art
const position = useRef(Animated.divide(scrollX, width)).current;
const playbackState = usePlaybackState();
useEffect(() => {
// position.addListener(({ value }) => {
// console.log(value);
// });
scrollX.addListener(({value}) => {
const val = Math.round(value / width);
setSongIndex(val);
});
TrackPlayer.setupPlayer().then(async () => {
// The player is ready to be used
console.log('Player ready');
// add the array of songs in the playlist
await TrackPlayer.reset();
await TrackPlayer.add(songs);
TrackPlayer.play();
isPlayerReady.current = true;
await TrackPlayer.updateOptions({
stopWithApp: false,
alwaysPauseOnInterruption: true,
capabilities: [
Capability.Play,
Capability.Pause,
Capability.SkipToNext,
Capability.SkipToPrevious,
],
});
//add listener on track change
TrackPlayer.addEventListener(Event.PlaybackTrackChanged, async (e) => {
console.log('song ended', e);
const trackId = (await TrackPlayer.getCurrentTrack()) - 1; //get the current id
console.log('track id', trackId, 'index', index.current);
if (trackId !== index.current) {
setSongIndex(trackId);
isItFromUser.current = false;
if (trackId > index.current) {
goNext();
} else {
goPrv();
}
setTimeout(() => {
isItFromUser.current = true;
}, 200);
}
// isPlayerReady.current = true;
});
//monitor intterupt when other apps start playing music
TrackPlayer.addEventListener(Event.RemoteDuck, (e) => {
// console.log(e);
if (e.paused) {
// if pause true we need to pause the music
TrackPlayer.pause();
} else {
TrackPlayer.play();
}
});
});
return () => {
scrollX.removeAllListeners();
TrackPlayer.destroy();
// exitPlayer();
};
}, []);
// change the song when index changes
useEffect(() => {
if (isPlayerReady.current && isItFromUser.current) {
TrackPlayer.skip(songs[songIndex].id)
.then((_) => {
console.log('changed track');
})
.catch((e) => console.log('error in changing track ', e));
}
index.current = songIndex;
}, [songIndex]);
const exitPlayer = async () => {
try {
await TrackPlayer.stop();
} catch (error) {
console.error('exitPlayer', error);
}
};
const goNext = async () => {
slider.current.scrollToOffset({
offset: (index.current + 1) * width,
});
await TrackPlayer.play();
};
const goPrv = async () => {
slider.current.scrollToOffset({
offset: (index.current - 1) * width,
});
await TrackPlayer.play();
};
const renderItem = ({index, item}) => {
return (
<Animated.View
style={{
alignItems: 'center',
width: width,
transform: [
{
translateX: Animated.multiply(
Animated.add(position, -index),
-100,
),
},
],
}}>
<Animated.Image
source={item.artwork}
style={{width: 320, height: 320, borderRadius: 5}}
/>
</Animated.View>
);
};
return (
<SafeAreaView style={styles.container}>
<SafeAreaView style={{height: 320}}>
<Animated.FlatList
ref={slider}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
scrollEventThrottle={16}
data={songs}
renderItem={renderItem}
keyExtractor={(item) => item.id}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {x: scrollX}}}],
{useNativeDriver: true},
)}
/>
</SafeAreaView>
<View>
<Text style={styles.title}>{songs[songIndex].title}</Text>
<Text style={styles.artist}>{songs[songIndex].artist}</Text>
</View>
<SliderComp />
<Controller onNext={goNext} onPrv={goPrv} />
</SafeAreaView>
);
}
I would pass the songs into your current PlayerScreen through props. You can use a separate component to load tracks from the Napster API and then render a PlayerScreen with those props.
The only part I'm not sure about is what path to pass to the player as the url. The Napster data contains a property previewURL which is an mp3 but it's not the whole song. I believe that the href is the streamable URL. It requires authentication to load the full track though.
The API path that I'm using here is for the most popular tracks.
export default function TopTracks() {
const [songs, setSongs] = useState([]);
useEffect(() => {
const loadData = async () => {
try {
const url = `http://api.napster.com/v2.2/tracks/top?apikey=${API_KEY}&limit=5`;
const res = await axios.get(url);
setSongs(
res.data.tracks.map((track) => ({
duration: track.playbackSeconds,
title: track.name,
artist: track.artistName,
album: track.albumName,
id: track.id,
url: track.href // or track.previewURL?
}))
);
} catch (error) {
console.error(error);
}
};
loadData();
}, []);
return <PlayerScreen songs={songs} />;
}