How to take full page screenshot of webview in react native? Already tried "react-native-view-shot " Link but it only takes screenshot of the visible area.
Someone please help.
Thanks
For future readers:
Below is the final script after many attempts, which takes full page screenshot from Webview. I have used ProgressWebView instead of Webview. You can use Webview if you want.
This code works in functional components.
Note: When the page is fully loaded then click on the Take Screenshot button
import React, { useState } from 'react';
import { View, Button } from 'react-native';
import { captureRef } from "react-native-view-shot";
import ProgressWebView from "react-native-progress-webview";
import json5 from 'json5'
import { Dimensions } from 'react-native';
const Report = () => {
const [componentHeight, setComponentHeight] = useState(0)
const [globalComponentHeight, setGlobalComponentHeight] = useState(0)
const [componentHeightFlex, setComponentHeightFlex] = useState(1)
let url = 'https://stackoverflow.com/questions/63708244/webview-full-page-screenshot-in-react-native'
let webview = null;
let count = 0
const injectJS = _ => {
const script = `
let method${count} = _ => {
let documentHeight = document.body.scrollHeight
let data = {componentHeight: documentHeight}
window.ReactNativeWebView.postMessage(JSON.stringify(data))
}
method${count}()`
webview.injectJavaScript(script)
count++
}
const takeScreenshot = _ => {
console.log(globalComponentHeight)
const {height} = Dimensions.get("window")
console.log(height)
if(globalComponentHeight <= height) setComponentHeight(height)
else setComponentHeight(globalComponentHeight)
setComponentHeightFlex(null)
setTimeout(_ => {
captureRef(webview, {
format: "png",
quality: 0.9,
result: "base64"
}).then(
_screenshot => {
console.log(_screenshot)
//First save your screenshot from _screenshot(base64 string). You can send base64 string to your server and save
//Then make the component default as below
setComponentHeight(0)
setComponentHeightFlex(1)
},
error => console.error("Oops, screenshot failed", error)
);
}, 100)
}
return (
<View style={{ marginTop: 40, flex: 1, display: 'flex' }}>
<Button mode='contained' onPress={takeScreenshot} title="Take Screenshot"/>
<View
style={{
height: componentHeight,
flex: componentHeightFlex
}}
>
<ProgressWebView
ref={ref => {
if (ref != null)
webview = ref
}}
bounces={false}
style={{ position: 'relative' }}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={true}
source={{ uri: url }}
startInLoadingState={true}
onLoad={e => injectJS()}
onMessage={e => {
let data = json5.parse(e.nativeEvent.data)
// console.log(data)
setGlobalComponentHeight(parseInt(data.componentHeight))
}}
></ProgressWebView>
</View>
</View>
);
}
export default Report
Related
import { useState } from 'react';
const AddNote = ({ handleAddNote }) => {
const [noteText, setNoteText] = useState('');
const [jobLink, setJobLink] = useState('');
const characterLimit = 200;
const handleDescChange = (event) => {
if (characterLimit - event.target.value.length >= 0) {
setNoteText(event.target.value);
}
};
const handleJobLinkChange = (event) => {
if (event.target.value.length >= 0) {
setJobLink(event.target.value);
console.log(event.target.value);
}
};
const handleSaveClick = () => {
if (noteText.trim().length > 0) {
handleAddNote(noteText);
setNoteText('');
setJobLink('');
}
};
return (
<div className='note new'>
<textarea
rows='8'
cols='10'
placeholder='Type to add a note...'
value={noteText}
onChange={handleDescChange}
></textarea>
<input style={{
paddingTop: "1.5%",
outline: "none",
color: "black",
marginRight: "0px",
marginLeft: "0px",
paddingRight: "0px",
backgroundColor: "white"
}}
className="form-control"
id="link"
name="link"
placeholder="Link to Job Posting"
value={jobLink}
type="link"
onChange={handleJobLinkChange}
/>
<div className='note-footer'>
<small>
{characterLimit - noteText.length} Remaining
</small>
<button className='save' onClick={handleSaveClick}>
Save
</button>
</div>
</div>
);
};
export default AddNote;
import { MdDeleteForever } from 'react-icons/md';
import AddNote from './AddNote';
const Note = ({ id, link, text, date, handleDeleteNote,}) => {
const go = e => {
link = "https://www.google.com";
e.preventDefault();
window.location.href = link;
}
return (
<div className='note'>
<span>{text}</span>
<div className='note-footer'>
<small>{date}</small>
<div className='note-btn'>
<button onClick={go} style={{ backgroundColor: "#001E49", borderRadius: "10px"
, borderColor: "none", margin: "auto", padding: "8px", marginLeft: "0px"
, marginRight: "0px", width: "fit-content"
, borderColor: "none"}}>Apply Here</button>
</div>
</div>
</div>
);
};
export default Note;
import Note from './Note';
import AddNote from './AddNote';
const NotesList = ({
notes,
handleAddNote,
handleDeleteNote,
}) => {
return (
<div className='notes-list'>
{notes.map((note) => (
<Note
id={note.id}
text={note.text}
date={note.date}
handleDeleteNote={handleDeleteNote}
/>
))}
<AddNote handleAddNote={handleAddNote} />
</div>
);
};
export default NotesList;
import React from 'react'
import './Internships.css'
import { useState, useEffect } from 'react';
import { nanoid } from 'nanoid';
import NotesList from '../components/NotesList';
import Search from '../components/Search';
import Header from '../components/Header';
const Internships = () => {
const [notes, setNotes] = useState([
]);
const [searchText, setSearchText] = useState('');
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
const savedNotes = JSON.parse(
localStorage.getItem('react-notes-app-data')
);
if (savedNotes) {
setNotes(savedNotes);
}
}, []);
useEffect(() => {
localStorage.setItem(
'react-notes-app-data',
JSON.stringify(notes)
);
}, [notes]);
const addNote = (text, link) => {
const date = new Date();
const newNote = {
id: nanoid(),
text: text,
date: date.toLocaleDateString(),
link: link,
};
const newNotes = [...notes, newNote];
setNotes(newNotes);
};
const deleteNote = (id) => {
const newNotes = notes.filter((note) => note.id !== id);
setNotes(newNotes);
};
return (
<div className={`${darkMode && 'dark-mode'}`}>
<div className='container'>
<Header />
<Search handleSearchNote={setSearchText} />
<NotesList
notes={notes.filter((note) =>
note.text.toLowerCase().includes(searchText)
)}
handleAddNote={addNote}
handleDeleteNote={deleteNote}
/>
</div>
</div>
);
};
export default Internships
for the 'link= "www.google.com"'(it is a placeholder) the button redirects me to google.com but I want there to be a user inputted website and it redirects me there. I am not able to pass joblink into note.js and it is not reading it when I did inspect element on the button that was created. I don't know how to fix trying to get an input from the user and passing it into note.js.
I am a little bit confused by how the code was written out on the forum, but it seems like you could set a state for the target value in your input. So do something like
const[site, setSite] = useState("https://www.google.com");
<input style={{
paddingTop: "1.5%",
outline: "none",
color: "black",
marginRight: "0px",
marginLeft: "0px",
paddingRight: "0px",
backgroundColor: "white"
}}
className="form-control"
id="link"
name="link"
placeholder="Link to Job Posting"
value={jobLink}
type="link"
onChange={() => setSite(e.target.value)}
/>
And then use callback props and send it to your Note component, then replace your 'go' link to
link = site
I apologize if that made no sense lol.
From my understanding, you have the component AddNote where the user is able to generate a new job post, and these values saved here are needed in the Note component
Here not only the states are saved in the incorrect component, but when they hit save (handleSaveClick) this happens:
setNoteText('');
setJobLink('');
What I recommend is to have a Notes component, including a notes state array where you'll save a json with noteText and jobLink, and then map those to Note components, passing them as props.
And this Notes component can also contain the AddNote Component so this way you can pass the setNotes hook and efficiently work with dynamic job posts:
const [notes, setNotes] = useState([])
{notes?.length > 0 && notes.map(elem => (<Note noteText={elem.noteText} jobLink={elem.jobLink} />))}
(preferably pass the 'elem' as a whole, maybe you'll add new properties to it and the props quantity will grow)
and with the use of useRef hook:
const handleSaveClick = () => {
handleAddNote(prev => [...prev, {noteLink: noteLinkRef.current.value, jobLink: jobLinkRef.current.value}])
}
Hope this helps to understand, if you feel that I need to expand a little bit more, let me know!
The following component CapturePhoto is used to take a photo using react-native Image Picker, once a photo is taken, I copy the photo file to a specific path that I pass as prop to this component from its parent ( a gallery of images that is a list of the CapturePhoto component) The reason I am copying the photo is that I cannot find another way to specify the path to Image picker as option.
The code works for taking a photo and copying the file image to the specific path but the photo is not being displayed until I take a second photo (delay)
I am learning react-native and Javascript at the same time, I know it's not the good way but I don't have time to learn Javascript first!
Any guidance provided would be much appreciated!
import React, { useState } from 'react';
import { TouchableOpacity, View, Image, StyleSheet, Text } from 'react-native';
import * as ImagePicker from 'react-native-image-picker';
import placeHolder from "./placeHolder.png"
export default function CapturePhoto({ photoPath }) {
let uri;
let decodedURI;
const [pickerResponse, setPickerResponse] = useState(null);
var RNFS = require('react-native-fs');
const copyPhoto = (source, destination) => {
if (source === undefined || destination === undefined) {
return;
} else {
RNFS.copyFile(source, destination)
.then((result) => {
console.log("\n\n>>>>>>>>>>>>The photo has been copied ")
}).catch((error) => {
console.log("\n\n>>>>>>>>>>>>Copy photo failed: ", error)
})
}
}
const onCameraPress = async () => {
const options = {
saveToPhotos: true,
mediaType: 'photo',
includeBase64: false,
};
ImagePicker.launchCamera(options, setPickerResponse);
uri = pickerResponse?.assets && pickerResponse.assets[0].uri;
console.log("\n\n>>>>>>>>>>>>destination photo: ", photoPath);
decodedURI = decodeURIComponent(uri)
await uri ? copyPhoto(decodedURI, photoPath) : console.log("\n\n>>>>>>>>>>>>The photo has not been copied ")
}
return (
<View style={{
justifyContent: 'center',
alignItems: 'center',
marginTop: 10
}}>
<TouchableOpacity onPress={() => onCameraPress()}>
<Image
style={styles.avatarImage}
source={uri ? { uri } : placeHolder}
/>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
avatarImage: {
height: 260,
width: 260,
overflow: 'hidden',
margin: 10
}
});
You don't actually need any states for saving image picker response. If you want to save the uri of the image which was clicked you can have a state for it and add the code for it inside copyPhoto() after file has been copied.
var RNFS = require('react-native-fs'); This statement should not be inside function. Everytime the component re-renders it will require it again. Add it like you've added import statements.
You should change your functions like this.
const copyPhoto = (source, destination) => {
if (source === undefined || destination === undefined) {
return;
} else {
RNFS.copyFile(source, destination)
.then((result) => {
console.log("\n\n>>>>>>>>>>>>The photo has been copied ");
})
.catch((error) => {
console.log("\n\n>>>>>>>>>>>>Copy photo failed: ", error);
});
}
};
const onCameraPress = async () => {
const options = {
saveToPhotos: true,
mediaType: "photo",
includeBase64: false
};
const result = await ImagePicker.launchCamera(options);
//if the user cancelled the process
if (result.didCancel) {
return alert("You cancelled the process");
}
const {assets} = result;
if (assets.length > 0) {
const decodedUri = decodeURIComponent(assets[0].uri);
copyPhoto(decodedUri, photoPath);
}
};
I have been trying to achieve hiding (or slide up) my react native startup app top bar after 3 seconds, then have a button to press to show the top bar, but could not. I've searched online how to do it, but have not seen good solution for it. Please I need help here, the below is snippet code of what I tried doing but it did not work.
<HeaderHomeComponent /> is the top header I created and imported it in my Home screen
const Home = () => {
const camera = useRef();
const [isRecording, setIsRecording] = useState(false);
const [flashMode, setFlashMode] = useState(RNCamera.Constants.FlashMode.off);
const [cameraType, setCameraType] = useState(RNCamera.Constants.Type.front);
const [showHeader, setShowHeader] = useState(true);
const onRecord = async () => {
if (isRecording) {
camera.current.stopRecording();
} else {
setTimeout(() => setIsRecording && camera.current.stopRecording(), 23*1000);
const data = await camera.current.recordAsync();
}
};
setTimeout(()=> setShowHeader(false),3000);
const DisplayHeader = () => {
if(showHeader == true) {
return <HeaderHomeComponent />
} else {
return null;
}
}
// useEffect(() => {
// DisplayHeader();
// }, [])
return (
<View style={styles.container}>
<RNCamera
ref={camera}
type={cameraType}
flashMode={flashMode}
onRecordingStart={() => setIsRecording(true)}
onRecordingEnd={() => setIsRecording(false)}
style={styles.preview}
/>
<TouchableOpacity
style={styles.showHeaderButton}
onPress={() => {
setShowHeader(!showHeader);
}}>
<Button title='Show' />
</TouchableOpacity>
<HeaderHomeComponent />
You were really close.
This is how it should be done:
useEffect(() => {
setTimeout(toggleHeader,3000);
}, []);
const toggleHeader = () => setShowHeader(prev => !prev);
Then inside the "return":
{showHeader && <HeaderHomeComponent />}
As simple as that.
This should help you get started in the right direction, you can use animation based on your preference to this code.
import React, {useState, useEffect} from 'react';
import { Text, View, StyleSheet, Button } from 'react-native';
import Constants from 'expo-constants';
export default function App()
{
const [showStatusbar, setShowStatusbar] = useState(true)
useEffect(() =>
{
setTimeout(() =>
{
setShowStatusbar(false)
}, 3000)
}, [])
return (
<View style={styles.container}>
{
showStatusbar
? <View style = {styles.statusBar}>
<Text>Status Bar</Text>
</View>
: null
}
<Button title = "Show Status bar" onPress = {() => setShowStatusbar(true)}/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ecf0f1',
},
statusBar:
{
height: 50,
backgroundColor:'lightblue',
justifyContent:'center',
alignItems:'center'
}
});
Instead of setTimeout use Animation.
or
Check this lib : https://www.npmjs.com/package/react-native-animated-hide-view
or
check below answers which might help you
Hide and Show View With Animation in React Native v0.46.
I am trying to build a simple stopwatch app in react-native. I'm using AsyncStorage to store the time recorded data into local storage, along with that I would like to display a table that shows all the recorded times. The core idea is that when a person presses and holds a LottieView animation, it will start a timer, when they press out, the timer stops, records in AsyncStorage and then updates the table.
After 10 elements, my FlatList (inside TimeTable.jsx) becomes extremely slow and I am not sure why. The component that is causing this error is I believe TimeTable.jsx but I am not quite sure why.
src/components/Timer/TimeTable.jsx
import React, {useState, useEffect} from 'react'
import { StyleSheet, FlatList } from "react-native";
import { Divider, List, ListItem } from '#ui-kitten/components'
import AsyncStorage from '#react-native-async-storage/async-storage';
const getRecordedEventsTable = async (dbKey) => {
try {
let currentDataArray = await AsyncStorage.getItem(dbKey);
return currentDataArray ? JSON.parse(currentDataArray) : [];
} catch (err) {
console.log(err);
}
};
const renderItem = ({ item, index }) => (
<ListItem
title={`${item.timeRecorded / 1000} ${index + 1}`}
description={`${new Date(item.timestamp)} ${index + 1}`}
/>
)
export const TimeTable = ({storageKey, timerOn}) => {
const [timeArr, setTimeArr] = useState([]);
useEffect(() => {
getRecordedEventsTable(storageKey).then((res) => {
setTimeArr(res)
})
}, [timerOn])
return (
<FlatList
style={styles.container}
data={timeArr}
ItemSeparatorComponent={Divider}
renderItem={renderItem}
keyExtractor={item => item.timestamp.toString()}
/>
);
};
const styles = StyleSheet.create({
container: {
maxHeight: 200,
},
});
src/components/Timer/Timer.jsx
import React, {useState, useEffect, useRef} from 'react'
import {
View,
StyleSheet,
Pressable,
} from 'react-native';
import {Layout, Button, Text} from '#ui-kitten/components';
import LottieView from 'lottie-react-native'
import AsyncStorage from '#react-native-async-storage/async-storage';
import {TimeTable} from './TimeTable'
const STORAGE_KEY = 'dataArray'
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#E8EDFF"
},
seconds: {
fontSize: 40,
paddingBottom: 50,
}
})
const getRecordedEventsTable = async () => {
try {
let currentDataArray = await AsyncStorage.getItem(STORAGE_KEY)
return currentDataArray ? JSON.parse(currentDataArray) : []
} catch (err) {
console.log(err)
}
}
const addToRecordedEventsTable = async (item) => {
try {
let dataArray = await getRecordedEventsTable()
dataArray.push(item)
await AsyncStorage.setItem(
STORAGE_KEY,
JSON.stringify(dataArray)
)
} catch (err) {
console.log(err)
}
}
// ...
const Timer = () => {
const [isTimerOn, setTimerOn] = useState(false)
const [runningTime, setRunningTime] = useState(0)
const animation = useRef(null);
const handleOnPressOut = () => {
setTimerOn(false)
addToRecordedEventsTable({
timestamp: Date.now(),
timeRecorded: runningTime
})
setRunningTime(0)
}
useEffect(() => {
let timer = null
if(isTimerOn) {
animation.current.play()
const startTime = Date.now() - runningTime
timer = setInterval(() => {
setRunningTime(Date.now() - startTime)
})
} else if(!isTimerOn) {
animation.current.reset()
clearInterval(timer)
}
return () => clearInterval(timer)
}, [isTimerOn])
return (
<View>
<Pressable onPressIn={() => setTimerOn(true)} onPressOut={handleOnPressOut}>
<LottieView ref={animation} style={{width: 300, height: 300}} source={require('../../../assets/record.json')} speed={1.5}/>
</Pressable>
<Text style={styles.seconds}>{runningTime/1000}</Text>
<TimeTable storageKey={STORAGE_KEY} timerOn={isTimerOn} />
<Button onPress={resetAsyncStorage}>Reset Async</Button>
</View>
)
}
export default Timer
Any help, appreciated. Thanks.
EDIT:
Received the following warning in console:
VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc. Object {
"contentLength": 1362.5,
"dt": 25161,
"prevDt": 368776,
EDIT:
In Timer.jsx, I have a Text View in the render function as follows:
<Text style={styles.seconds}>{runningTime/1000}</Text>, this part is supposed to show the stopwatch value and update with the timer.
As the FlatList gets bigger, this is the part that becomes extremely laggy.
My suspicion is that as this is trying to re-render constantly, the children component TimeTable.jsx is also re-rendering constantly?
Looks to me like you have a loop here:
useEffect(() => {
getRecordedEventsTable(storageKey).then((res) => {
setTimeArr(res)
})
}, [timeArr, timerOn])
useEffect will get called every time timeArr is updated. Then, inside you call your async getRecordedEventsTable, and every time that finishes, it'll call setTimeArr, which will set timeArr, triggering the sequence to start again.
For optimizing the FlatList you can use different parameters that are available. You can read this https://reactnative.dev/docs/optimizing-flatlist-configuration.
Also you might consider using useCallback hook for renderItems function.
I would recommend reading this https://medium.com/swlh/how-to-use-flatlist-with-hooks-in-react-native-and-some-optimization-configs-7bf4d02c59a0
I was able to solve this problem.
The main culprit for the slowness was that in the parent component Timer.jsx because the timerOn props is changing everytime the user presses the button, the whole children component is trying to re-render and that AsyncStorage call is being called everytime. This is the reason that the {runningTime/1000} is rendering very slowly. Because everytime the timerOn component changes all child components have been queued to re-render.
The solution for this was to render the Table component from a parent of Timer and not inside the Timer component and maintain a state in Timer which is passed back to the parent and then passed to the Table component.
This is what my parent component looks like now:
const [timerStateChanged, setTimerStateChanged] = useState(false);
return (
<View style={styles.container}>
<Timer setTimerStateChanged={setTimerStateChanged} />
<View
style={{
borderBottomColor: "grey",
borderBottomWidth: 1,
}}
/>
<TimeTable timerOn={timerStateChanged} />
</View>
);
};
A better way would be to use something like React context or Redux.
Thanks for all the help.
I am working on camera in app, where I have took a picture and have convert into base64 now I want to display an image but did not able to display an image . Could someone please help me how to achieve this goal .
Thanks
takePicture = async () => {
if (this.camera) {
let photo = await this.camera.takePictureAsync({
base64: true,
});
this.props.cameraToggle(false);
this.props.image(photo)
console.log('### take picutre', photo)
}
}
Image Code that I want to render
<Image source={{uri:`data:image/png;base64,${cameraImage}`}} style={{width:100,height:100}}/>
I assume that you're using class-based components. You can render the image which is captured from camera by setting the response photo to a local state and conditionally rendering it.
import React from "react";
import { Image } from "react-native";
import { Camera } from "expo-camera";
import Constants from "expo-constants";
import * as ImagePicker from "expo-image-picker";
import * as Permissions from "expo-permissions";
class Cameras extends React.Component(props) {
state = {
captures: "",
};
takePicture = async () => {
if (this.camera) {
let photo = await this.camera.takePictureAsync({
base64: true,
});
this.props.cameraToggle(false);
this.setState({ captures: photo.uri });
}
};
render() {
const { captures } = this.state;
return (
<View flex={1}>
{this.state.captures ? (
<Image source={{ uri: captures }} style={{width:50,height:50}} />
) : (
<Camera {...pass in props} />
)}
</View>
);
}
}
export default Cameras;
Note:
A more clean approach would be to pass the state captures as a route param by navigating a different screen and rendering the image on that screen.
I am assuming you are storing value of photo in cameraImage.
I am sharing a working solution with you using expo image picker, give it a try
Your component
import * as ImagePicker from 'expo-image-picker';
import * as Permissions from 'expo-permissions';
<TouchableHighlight onPress={this._openCamera}>
<Text>Open Camera</Text>
</TouchableHighlight>
<Image source={this.state.mainImageUrl} />
Your function
_openCamera = async () => {
await Permissions.askAsync(Permissions.CAMERA);
try {
let result: any = await ImagePicker.launchCameraAsync({
base64: true,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.cancelled) {
this.setState({
mainImageUrl: { uri: `data:image/jpg;base64,${result.base64}` }
});
}
} catch (E) {
console.warn(E);
}
}
Your code:
let photo = await this.camera.takePictureAsync({
base64: true,
});
this.props.image(photo)
The takePictureAsync return an object, so your base64 data is in the field photo.base64.
Solution 2
You can store the URI into the state without base64.
const [photoUri, setPhotoUri] = React.useState(null);
const cameraRef = React.useRef(null);
const takePhoto = async () => {
setPhotoUri(null);
const camera = cameraRef.current;
if(!camera) return;
try {
const data = await camera.takePictureAsync();
setPhotoUri(data.uri);
} catch (error) {
console.log('error: ', error);
}
}
Then use it.
return (
<Image source={{uri: photoUri}} />
);
Hope it helps.