I have the react-native map running with google for both IOS and Android.
Is there a way to have the mapType layer switcher like that of google maps app enabled in react-native expo app? So that user can switch mapTypes (standard, satellite, ....)
a simplified version of the code
constructor(props) {
super(props);
this.state = {
mapRegion: null,
markers: [],
mapType: null
};
}
switchMapType() {
console.log('Changing');
this.state.mapType = 'satellite'
}
render() {
return (
<MapView
provider="google"
mapType={this.state.mapType}
>
<Icon
onPress={this.switchMapType}
/>
</MapView>
);
}
I get an undefined error when inside state switchMapType().
Looking at the documentation it can be as simple as passing the correct style to the mapType prop
https://github.com/react-native-community/react-native-maps/blob/master/docs/mapview.md
The map type to be displayed.
standard: standard road map (default)
none: no map
satellite: satellite view
hybrid: satellite view with roads and points of interest overlayed
terrain: (Android only) topographic view
mutedStandard: more subtle, makes markers/lines pop more (iOS 11.0+ only)
Binding you function
You are getting that error because probably need to bind your function so that it knows that value of this to use. You can do it in your constructor by putting the following in your constructor
constructor(props) {
...
this.switchMapType = this.switchMapType.bind(this);
...
}
or you could convert switchMapType to an arrow function by changing its declaration to
switchMapType = () => {
...
}
or you could bind the function when you call it
<Icon
onPress={this.switchMapType.bind(this}
/>
You can see this article for more details https://medium.freecodecamp.org/react-binding-patterns-5-approaches-for-handling-this-92c651b5af56
I prefer to use arrow functions myself.
Setting State
I also notice that there is an error in your function switchMapType with how you are setting state. You are calling this.state.mapType = 'satellite' You should not manipulate state like this. Changing state like this will not force a re-render (which is what you want) and it can lead to unexpected consequences. See this article for more on setting state https://medium.com/#baphemot/understanding-reactjs-setstate-a4640451865b
If you want to change the state you should use this.setState({ key1: value1, key2, value2 });
So if you update your switchMapType function to be the following it should work
switchMapType = () => {
console.log('changing');
this.setState({ mapType: 'satellite' });
}
If you want to be able to toggle between the satellite and standard versions you could do something like this. This uses a ternary statement to handle the if/else https://codeburst.io/javascript-the-conditional-ternary-operator-explained-cac7218beeff
switchMapType = () => {
console.log('changing');
this.setState({ mapType: this.state.mapType === 'satellite' ? 'standard' : 'satellite' });
}
Related
i have an app that renders a webpage inside a webview. this actually has a bunch of crypto addresses.
I want them to be automatically made clickable. and when they are clicked - i want to show a popup (some information about the addresses).
can this be done ?
im very unsure if this thing about changing the UI is possible...but in desktop web world, there are extensions that do this. if there are any examples of flutter webview codebases that do this, that would be helpful
the second point - communicating back and forth with the webpage is even more confusing. can this be done at all ? can i receive the data of the click back to main flutter app and then do something ?
Here's a working example
This is achieved as I said by Injecting Js when the webpage loads, as for communication with Flutter, I used the flutter Webview plugin provided JavascriptChannel
The Javascript code looks for a specific element firstly on Page load and secondly while scrolling the webpage (to account for newly created dynamic elements)
Here's how the flow works
JS: assigns the element a new css style (Or in your case make it look like a button) or even create a button and insert it into the webpage
JS: assign on click to the element to call the Flutter JS Channel.
Flutter: Receive message Display a snackbar - you can deeplink or do whatever you want.
As the comments on the JS code say. the scrolling behavior calls every time which is not always ideal, you can use another function make it only trigger on a specific scroll distance
Full working example
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
dynamic ctrl;
// if you have multiple elements, just use querySelector All and loop
const jsCodeToForAnElement = """
// Choose the element
watch();
// I would add dealy to calculate delta between scrolls
// Meaning this code wont watch on every scroll
window.onscroll = watch();
function watch(){
let elm = document.querySelector("main h1");
try{
// Style it
elm.style.cssText = "background: red";
// Add on click
elm.onclick = (event)=> {
var walletHTMLElement = event.target;
// Use native API to communicate with Flutter
jsMessager.postMessage('Wallet clicked: ' + walletHTMLElement.innerHTML);
};
}catch(e){
jsMessager.postMessage('Error: ' + e);
}
}
""";
void main() {
runApp(
MaterialApp(
home: WebViewExample(),
),
);
}
class WebViewExample extends StatefulWidget {
#override
WebViewExampleState createState() => WebViewExampleState();
}
class WebViewExampleState extends State<WebViewExample> {
#override
void initState() {
super.initState();
// Enable virtual display.
if (Platform.isAndroid) WebView.platform = AndroidWebView();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: WebView(
initialUrl: 'https://flutter.dev/',
debuggingEnabled: true,
onWebViewCreated: (WebViewController webViewController) {
ctrl = webViewController;
},
javascriptMode: JavascriptMode.unrestricted,
javascriptChannels: <JavascriptChannel>{
JavascriptChannel(
name: 'jsMessager',
onMessageReceived: (jsMessager) async {
if (jsMessager.message.contains("Wallet")) {
var snackBar = SnackBar(
content: Text(jsMessager.message),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
// Do Other Thing like deeplinking to crypto app
// Deeplink to wallet LaunchApp(wallet) <-- clean wallet string first
}
}),
},
onPageStarted: (String url) async {
ctrl.runJavascript(jsCodeToForAnElement);
},
onPageFinished: (String url) async {},
),
);
}
}
Original Answer
This would be possible by Injecting Javascript into Webviews (This is one idea)
1 - I would wait for the page to load
2 - Modify the HTML content using Javascript
Should be pretty straight forward.
Refer to this Answer to see how it is done in Flutter.
https://stackoverflow.com/a/73240357/6151564
Here is my sample React component:
const OwnerView = () => {
const [monthlyCharge, setMonthlyCharge] = useState(0)
useEffect(() => {
getPerMonthCharges(ownerPhoneNumber, vehicles.length)
}, [])
async function getPerMonthCharges(ownerPhoneNumber, noOfCars) {
console.log(`inside getPerMonthCharges`);
try {
const serviceProviderChargesDoc = await firestore().collection(`${serviceProviderId}_charges`).doc(`${ownerPhoneNumber}`).get()
if (serviceProviderChargesDoc?.data()?.chargesPerMonth > 0) {
setMonthlyCharge(serviceProviderChargesDoc?.data()?.chargesPerMonth)
return
}
} catch (error) {
console.log(`Error while fetching monthly charge ${error}`);
}
setMonthlyCharge(noOfCars * perMonthGeneralCharge)
console.log(`done with getPerMonthCharges`);
}
}
There is a possibility that OwnerView gets unmounted even before getPerMonthCharges() completes its execution. Therefore in case OwnerView gets unmounted I receive a warning that am doing state update on an unmounted component and this is a non-op. Can someone please highlight what is your observation and right way to write this piece of code?
There are many ways to address this
You can check if the component is still Mounted, a bit ugly approach I agree, but quite a standard one (I would just use something like useAsync from react-use, which essentially does the same, but hides the ugliness)
Move loading logic outside of UI and make part of the global state (Redux, MobX, Apollo, or any other state management library), it would be in lines of separation of concerns and should make your code more readable.
The worst would be to prevent your user from any actions, while content is loading - making your app seem clunky, but React would not complain anymore.
The closest to the right way would be 2, but this can sparkle religious debates and some witch-burning, which I'm not a fan of.
You can refer to this: https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup
You can have a variable to keep track whether your component has unmount, let isMounted = true inside useEffect and set it to false as soon as the component is unmounted.
The code will be:
useEffect(() => {
let isMounted = true;
async function getPerMonthCharges(ownerPhoneNumber, noOfCars) {
console.log(`inside getPerMonthCharges`);
try {
const serviceProviderChargesDoc = await firestore().collection(`${serviceProviderId}_charges`).doc(`${ownerPhoneNumber}`).get()
if (serviceProviderChargesDoc?.data()?.chargesPerMonth > 0 && isMounted) { // add conditional check
setMonthlyCharge(serviceProviderChargesDoc?.data()?.chargesPerMonth)
return
}
} catch (error) {
console.log(`Error while fetching monthly charge ${error}`);
}
if (isMounted) setMonthlyCharge(noOfCars * perMonthGeneralCharge) // add conditional check
console.log(`done with getPerMonthCharges`);
}
getPerMonthCharges(ownerPhoneNumber, vehicles.length)
return () => { isMounted = false }; // cleanup toggles value, if unmounted
}, []);
This is my MapView code,
<MapView
style={styles.map}
initialRegion={
this.state.region
}
region={this.state.region}
onRegionChangeComplete={this.onRegionChange}
showsMyLocationButton
showsCompass
showsUserLocation
zoomControlEnabled
loadingEnabled
onMapReady={this.state.placesread ? this.getBreakfast : null}
>
{marker}
</MapView>
My problem is when user moves the map position the map starts randomly moving around without stopping, and it cannot be stopped without closing the map window.
when the map loads i get markers and load on the map and similar to google maps i have a search this area button on the map, i am using the onRegionChangeComplete to store the new region to state.
my onRegionChangeComplete code is,
onRegionChange = async region => {
await this.setState({ region });
await this.setState({ regionChange: true });
};
I guess the problem is that you are using onRegionChangeComplete callback to store the new region to state. i.e this.state.region. and that region prop is passed to MapView Component.
Try removing region={this.state.region} from MapView component.
Update
Also one more thing using await with setState is totally redundant(await this.setState({ region })) because setSate does not return promise but undefined.
setState is asynchronous function. If you want to await it what you can do is you can put it into a promise and resolve in callback/second argument.
For Example whenever you want setState to behave as synchronous call promisedSetState like so.
promisedSetState = (newState) => new Promise((resolve) => {
this.setState(newState, () => {
resolve();
});
});
//await the promise to get resolved
await promisedSetState({newState: 'whatever it is'});
ScrollToEnd() (on a FlatList) appears to have no effect in Android (or in the iOS simulator). The refs appear to be correct, and my own scrollEnd function is being called (I was desperate) but nothing changes on the screen. The effects of all the scroll functions appear to be really inconsistent - I can get scrollToOffset to work on iOS but not Android. I read that this may be because Android doesn't know the height of the items in the flatlist, but it still doesn't work with getItemLayout implemented.
There's no feedback/errors I can see which would explain why this wouldn't work. Note that I am developing with Redux, using Android 7.0 to test and am trying this in Debugging mode (using react-native run-android). The FlatList is inside a normal View (not a ScrollView).
The logic in the code is correct as far as I can tell, but calling scrollToEnd on the FlatList has no visible effect.
My render() function:
<View style={styles.container}>
<View style={styles.inner}>
<FlatList
ref={(ref) => { this.listRef = ref; }}
data = {this.getConversation().messages || []}
renderItem = {this.renderRow}
keyExtractor = {(item) => item.hash + ''}
numColumns={1}
initialNumToRender={1000}
onContentSizeChange={() => {
this.scrollToEnd();
}}
getItemLayout={(data, index) => (
{length: 50, offset: 50 * index, index}
)}
onContentSizeChange={this.scrollToEnd()}
onLayout={ this.scrollToEnd()}
onScroll={ this.scrollToEnd()}
/>
</View>
</View>
this.scrollToEnd():
scrollToEnd = () => {
console.log("scrolling func"); // This is printed
const wait = new Promise((resolve) => setTimeout(resolve, 0));
wait.then( () => {
console.log("scrolling"); // This is also printed
this.listRef.scrollToEnd(); // Throws no errors, has no effect?
});
};
Thanks so much.
Your code seems correct to me, but which version of React are you using? Starting from v16.3 the recommended way to use refs is through React.createRef(), so your code would be change to:
// constructor
constructor(props) {
super(props)
this.flatlistRef = React.createRef();
}
// inside render
<FlatList
ref={this.flatlistRef}
...
/>
// scrollToEnd
scrollToEnd = () => {
this.flatlistRef.current.scrollToEnd(); // ref.current refers to component
};
Note that this might not solve your problem, since the older usage of ref should still be valid
I'm currently fiddling around with react-native-maps to simulate various moving objects around a map space (simulating real time tracking of items) with a callout showing each object's name beside the marker denoting it.
I'm currently able to show the callout for each marker whenever I press it. However, what I intend to do is create a button which toggles on or off the callouts for every marker on the map.
I'm currently using the react-native-maps library for my map.
What I've done are as such:
/* this.props.trackedObjects is an array of objects containing
coordinate information retrieved from database with the following format
trackedObjects=[{
coordinate:{
latitude,
longitude
},
userName
}, ...]
*/
/* inside render() */
{this.props.trackedObjects.map((eachObject) => (
<View>
<MapView.Marker
ref={ref => {this.marker = ref;}}
key={eachObject.userName}
coordinate={eachObject.coordinate}
>
/*Custom Callout that displays eachObject.userName as a <Text>*/
</MapView.Marker>
</View>
))}
/* Button onPress method */
onPress(){
if(toggledOn){
this.marker.showCallout();
}else{
this.marker.hideCallout();
}
}
It seems that when I render a single Marker component, this method works. However, I can't quite crack my head to get around using showCallout() to show the callouts for an entire group of markers.
Would anyone be able to shed some light on how to go about doing this?
1. Wrap the component MapView.Marker into a custom Marker:
class Marker extends React.Component {
marker
render () {
return (
<MapView.Marker {...this.props} ref={_marker => {this.marker = _marker}} />
)
}
componentDidUpdate (prevProps) {
if (prevProps.calloutVisible !== this.props.calloutVisible) {
this.updateCallout();
}
}
updateCallout () {
if (this.props.calloutVisible) {
this.marker.showCallout()
} else {
this.marker.hideCallout()
}
}
}
2. Update your higher level component accordingly in order to provide the callout visibility filter via prop calloutVisible:
/* inside render() */
{this.props.trackedObjects.map((eachObject) => (
<View>
<Marker
key={eachObject.userName}
coordinate={eachObject.coordinate}
calloutVisible={eachObject.calloutVisible} // visibility filter here
>
/*Custom Callout that displays eachObject.userName as a <Text>*/
</MapView.Marker>
</View>
))}