I have a long list of areas loaded from a SQLite database into react-native-maps. Each area has a particular status, and a fillColor that gets calculated based on its status. As some areas end up having hundreds of Polygons, the maps gets slower the more areas are calculated. Is there a way to detect if a Polygon is within the MapView to hide it when is not within the observable map?
So far I have a memoized component that gets the coordinates and a couple of helper function to get the color based on the area. The looped component gets then injected inside the MapView.
const Areas = useMemo(
() =>
areas &&
areas.map((area: Area) => {
console.log('rendering');
const coordinates = getCoordinates(area.geometry);
const areaColor = getAreaColor(area.status);
return (
<View key={area.CODE}>
<Polyline
strokeWidth={2}
fillColor={'transparent'}
coordinates={coordinates}
strokeColor={areaColor}
tappable={true}
onPress={() => handlePressArea(area)}
/>
</View>
);
}),
[areas]
);
const getCoordinates = (coords: string): LatLng[] => {
const geometry = JSON.parse(coords);
const output = geometry[0].map((item: Point[]) => ({
latitude: item[1],
longitude: item[0],
}));
return output;
};
const getAreaColor = (status: Area['status']): string => {
if (status === STATUS.ONE) {
return 'rgba(54, 155, 247, 0.6)';
} else if (status === STATUS.TWO) {
return 'rgba(240, 54, 0, 0.6)';
} else {
return 'rgba(235, 149, 50, 0.6)';
}
};
Eventually I mount the component like this:
<MapView
initialRegion={region}
provider={PROVIDER_DEFAULT}
rotateEnabled={false}
style={styles.map}
showsUserLocation={true}
onRegionChangeComplete={handleRegionChange}
customMapStyle={mapStyle}
>
{AreasView}
</MapView>
I tried to get a ref for each Polygon but the number of callbacks is so high that makes the app crash. I also tried with ObservableAPI but with no success. Simplifying the polygons is not an option either. Is there any way to detect if each polygon is within the observable map to hide them when out of view? I been stuck for some days so far, I hope someone could help.
Thank you!
UPDATE
I have implemented a function that checks if a polygon is within boundaries on region change as such:
<MapView
...props
ref={mapRef}
onRegionChangeComplete={getMapBounds}
/>
const mapRef = useRef<MapView>(null);
const getMapBounds = async () => {
await mapRef?.current?.getMapBoundaries().then(res => setMapBounds(res));
};
This basically stores inside the state the northEast and southWest boundaries of the map every time the user changes view.
Then to check if is in view:
const isInView = useMemo(
() => (point: LatLng) => {
if (mapBounds) {
const { northEast, southWest } = mapBounds;
const x = point.longitude;
const y = point.latitude;
return (
northEast.latitude >= y &&
y >= southWest.latitude &&
northEast.longitude >= x &&
x >= southWest.longitude
);
}
},
[mapBounds]
);
Gets a portion of the polygon and checks is the latitude and longitude of the point are within those boundaries
And so this leaves to:
const AreasView = useMemo(
() =>
areas &&
areas.map((area: Area) => {
const coordinates = getCoordinates(area.geometry);
const areaColor = getAreaColor(area.status);
const isVisible = isInView(coordinates[0]);
if (isVisible) {
return (
<View key={area.CODE}>
<Polygon
strokeWidth={2}
fillColor={areaColor}
coordinates={coordinates}
strokeColor={areaColor}
tappable={true}
onPress={() => handlePressArea(area)}
/>
</View>
);
}
}),
[areas, isInView]
);
It's still quite slow...shall I use useCallback instead, or is it any function for any library that is more optimised than that?
Cheers
Related
I'm making a game for university project. I could not figure out why it's not re-render the screen when I use setState() for status, but it does when I use for fool. If I use console.log(status) It was altered after the setStatus.
const [status, setStatus] = React.useState({ astrounauts: 0, research: 0, money: 0, publicity: 0 });
const [fool, setFool] = React.useState(false);
const acceptCard = (index) => {
let statusAux = status;
statusAux.astrounauts += cards[index].acceptCardStatus.astrounauts;
statusAux.research += cards[index].acceptCardStatus.research;
statusAux.money += cards[index].acceptCardStatus.money;
statusAux.publicity += cards[index].acceptCardStatus.publicity;
//no re-render... =C
setStatus(() => statusAux);
//it does trigger re-render, not working without that.. this is dumb
setFool(() => !fool);
}
...
<View style={styles.cardsOptions}>
<Swiper
cards={cards}
renderCard={(card =>
<Card card={card} />
)}
onSwipedRight={(cardIndex) => acceptCard(cardIndex)}
onSwipedLeft={(cardIndex) => rejectCard(cardIndex)}
// onSwiped={(cardIndex) => { console.log(cardIndex) }}
onSwipedAll={() => { console.log('onSwipedAll') }}
disableBottomSwipe={true}
disableTopSwipe={true}
outputRotationRange={["-10deg", "0deg", "10deg"]}
cardIndex={0}
backgroundColor={'#18191A'}
stackSize={100}
>
</Swiper>
</View>
You are setting your state to a function. the setState callback accepts the value itself, not a callback that returns the value. You need to change:
setStatus(() => statusAux);
To:
setStatus(statusAux);
In addition to that... you are mutating the original object, you need to create a new object for React to detect the change. You can do using the spread operator
like this:
let statusAux = {...status};
Just try to do this, Don`t need to use "()=>"
const acceptCard = (index) => {
let statusAux = status;
statusAux.astrounauts += cards[index].acceptCardStatus.astrounauts;
statusAux.research += cards[index].acceptCardStatus.research;
statusAux.money += cards[index].acceptCardStatus.money;
statusAux.publicity += cards[index].acceptCardStatus.publicity;
//no re-render... =C
setStatus(statusAux);
//it does trigger re-render, not working without that.. this is dumb
setFool(!fool);
}
I need to perform different action on single and double tap on a view. On double tap I need to like the image just like Instagram double tap experience. On single tap I need to open a modal.
For double tap I have used TapGestureHandler which works perfect
<TapGestureHandler
ref={this.doubleTapRef}
maxDelayMs={200}
onHandlerStateChange={this.onProductImageDoubleTap}
numberOfTaps={2}
>
<SomeChildComponent ...
But when I add any Touchable to detect single tap in the
<TouchableWithoutFeedback onPress={this.imageTapped}>
on double tapping the this.imageTapped function is called twice along with this.onProductImageDoubleTap. Is there any way to cancel tap on touchable when two taps are done is quick succession
The best solution is not using state, since setting state is asynchronous.Works like a charm for me on android !
let lastPress = 0;
const functionalComp = () => {
const onDoublePress = () => {
const time = new Date().getTime();
const delta = time - lastPress;
const DOUBLE_PRESS_DELAY = 400;
if (delta < DOUBLE_PRESS_DELAY) {
// Success double press
console.log('double press');
}
lastPress = time;
};
return <View
onStartShouldSetResponder =
{(evt) => onDoublePress()}>
</View>
}
2022 update
This is a performant native solution without any JS thread blocking calculation!
Many more tips here
const tap = Gesture.Tap()
.numberOfTaps(2)
.onStart(() => {
console.log('Yay, double tap!');
});
return (
<GestureDetector gesture={tap}>
{children}
</GestureDetector>
);
The best solution is use react-native-gesture-handler
https://github.com/software-mansion/react-native-gesture-handler
Here is my solution -
import {State, TapGestureHandler} from 'react-native-gesture-handler';
export const DoubleTap = ({children}: any) => {
const doubleTapRef = useRef(null);
const onSingleTapEvent = (event: any) => {
if (event.nativeEvent.state === State.ACTIVE) {
console.log("single tap 1");
}
};
const onDoubleTapEvent = (event: any) => {
if (event.nativeEvent.state === State.ACTIVE) {
console.log("double tap 1");
}
};
return (
<TapGestureHandler
onHandlerStateChange={onSingleTapEvent}
waitFor={doubleTapRef}>
<TapGestureHandler
ref={doubleTapRef}
onHandlerStateChange={onDoubleTapEvent}
numberOfTaps={2}>
{children}
</TapGestureHandler>
</TapGestureHandler>
);
};
Now we will wrap the component where we need to detect the double and single tap : -
<DoubleTap>
<View>
...some view and text
</View>
</DoubleTap>
The package react-native-double-tap seems to be what you are looking for.
since you are asking on handling one tap and double tap, here's a simple code i think should covered your issue
Untested
first defined clickCount:0 in state:
state={clickCount:0, //another state}
then create a function with setTimeout to handling if user tapping once or two times:
handlingTap(){
this.state.clickCount==1 ?
//user tap twice so run this.onProductImageDoubleTap()
this.onProductImageDoubleTap :
//user tap once so run this.imageTapped with setTimeout and setState
//i set 1 sec for double tap, so if user tap twice more than 1 sec, it's count as one tap
this.setState({clickCount:1}, ()=>{
setTimeout(()=>{
this.setState({clickCount:0})
this.imageTapped()
}, 1000)
})
}
<TouchableWithoutFeedback onPress={this.handlingTap()}/>
just used TouchableWithoutFeedback instead of TapGestureHandler
With hooks:
const [lastPressed, setLastPressed] = useState(0);
const handlePress = useCallback(() => {
const time = new Date().getTime();
const delta = time - lastPressed;
setLastPressed(time);
if (lastPressed) {
if (delta < DOUBLE_PRESS_DELAY) {
console.log('double press');
} else {
console.log('single press');
}
}
}, [lastPressed]);
I have modified flix's answer into this. By this way, you can catch one and double click separately. I also changed debounce into 300ms which is fairly well for one and double click.
state={clickCount:0, //another state}
Binding context into the handlingTap method
constructor(props){
super(props)
this.handlingTap = this.handlingTap.bind(this)
}
With this function you can catch them separately
handlingTap() {
this.state.clickCount === 1
? this.doubleClick() // This catches double click
: this.setState(state => ({ clickCount: state.clickCount + 1 }), () => {
setTimeout(() => {
if (this.state.clickCount !== 2) {
this.oneClick() // this catches one click
}
this.setState({ clickCount: 0 })
}, 300)
})
}
In the button you can use this way
<TouchableWithoutFeedback onPress={this.handlingTap}></TouchableWithoutFeedback>
I am trying to build my sub-component in function _buildComponent, and put result into render(), just have a look at my code below
the problem I met was the AsyncStorage.getItem() is running async, causing it render nothing there in render() method
...react
_buildComponent = async (key) => {
let val = await AsyncStorage.getItem(key)
console.log(key + ' : ' +val);
debugger;//code will run to here after ScrollableTabView finish rendering. but I need to build Arr first.
if(val == 1) return <PopularView tabLabel={key}>{key}</PopularView>
}
render() {
let Arr = Constants.TABS.map(item =
return this._buildComponent(item).done();
})
debugger;//code will run into here directly without waiting building Arr above, making Arr was null when rendering ScrollableTabView
return (
<ScrollableTabView
tabBarBackgroundColor='#2196F3'
tabBarInactiveTextColor='mintcream'
tabBarTextStyle={{marginTop:27}}
initialPage={0}
renderTabBar={() => <ScrollableTabBar/>}
>
{Arr}// Arr is null here because the _buildComponent method was not finish yet.
{/* <PopularView tabLabel='Java'>Java</PopularView>
<PopularView tabLabel='IOS'>IOS</PopularView>
<PopularView tabLabel='Android'>Android</PopularView>
<PopularView tabLabel='Javascript'>Javascript</PopularView> */}
</ScrollableTabView>
)
}
...
I have explain my issue in comment, please check it, thanks guys. I do not know what's the best practise to prepare variable before running render().
Try this solution which dynamically add tabs (Updated)
_buildComponent = async () => {
let tabsData = [];
await AsyncStorage.multiGet(Constants.TABS).then(response => {
Constants.TABS.forEach((item,index) =>
{
if(response[index][1] == 1) {
tabsData.push(response[index][0]); // key
}
}
);
// This only render once
this.setState({ tabLabels: tabsData })
})
}
componentDidMount() {
this._buildComponent()
}
render() {
const tabLabelList = this.state.tabLabels.map((key) => {
return (
<PopularView tabLabel={key}>{key}</PopularView>
)
})
return (
<ScrollableTabView
tabBarBackgroundColor='#2196F3'
tabBarInactiveTextColor='mintcream'
tabBarTextStyle={{marginTop:27}}
initialPage={0}
renderTabBar={() => <ScrollableTabBar/>}
>
{tabLabelList}
</ScrollableTabView>
)
}
Sorry im posting this here
I want to save the info of each line i drew on canvas(the save action would be called onChange)
so i can retrieve this data and draw it on canvas again in case the user change screen or something.
I'm using expo-pixi to draw a image and sketch over it
onChangeAsync = async (param) => {
// here i want on change code i get the line informantion and store it
}
onLayout = async ({
nativeEvent: {
layout: { width, height },
},
}) => {
this.setState({
layoutWidth: width,
layoutHeight: height,
})
this.onReady();
}
onReady = async () => {
const { layoutWidth, layoutHeight, points } = this.state;
this.sketch.graphics = new PIXI.Graphics();
if (this.sketch.stage) {
if (layoutWidth && layoutHeight) {
const background = await PIXI.Sprite.fromExpoAsync(this.props.image);
background.width = layoutWidth * scaleR;
background.height = layoutHeight * scaleR;
this.sketch.stage.addChild(background);
this.sketch.renderer._update();
}
}
};
// The sketch component is pretty much as the example which comes with the lib
<Sketch
ref={ref => (this.sketch = ref)}
style={styles.sketch}
strokeColor={this.state.strokeColor}
strokeWidth={this.state.strokeWidth}
strokeAlpha={1}
onChange={this.onChangeAsync}
onReady={this.onReady}
/>
Does anyone have any clue? im kind of desperate
Thanks
Delete this question please, someone has helped me on another thread.
I am trying to generate markers at certain times in my app so that it will show a blank map at launch, but then sometime later, markers can show up and disappear at certain times.
My state variables are set up like this:
class Map extends Component {
state = {
markers: [{
key: null,
contactName: null,
location: null,
}]
}
The code I am trying to generate the markers with is as follows:
renderMarker = ({ key, contactName, location }) => {
console.log('renderMarker: ' + key + ', ' + contactName + ', ' + location);
return this.state.markers.map(Marker => (
<MapView.Marker
key = { key }
coordinate = {{
latitude: location[0],
longitude: location[1]
}}
//image = { carIcon }
title = { contactName } />
))
}
The console.log in there is generating the correct data, so it is returning for example:
renderMarker: 389djhJHDjdkh392sdk, Steve, -30.498767387,120.4398732
My render function looks like this:
render() {
return (
<MapContainer>
<MapView
style = { styles.map }
region = { this.state.mapRegion }
showsUserLocation = { true }
followUserLocation = { true }
onRegionChangeComplete = { this.onRegionChangeComplete.bind(this) }>
</MapView>
</MapContainer>
)
}
But nothing shows up on the map. I have been scouring the interwebs but I can't find a followable example. Do I need to add something to my render() function to make this work? My thoughts were I won't need to because renderMarker is doing that work.
Any ideas guys?