React Native - style responsiveness for different screen sizes - react-native

We are building an app which is going to run on multiple devices (iOS, Android) and therefore on many screen sizes. After reading about PixelRatio it still doesn't seem as if there is a magic solution to scale automatically all style values according to the different screen sizes.
The only solution I could think of was to override the Stylesheet class to automatically scale all sizes according to the current screen size. For example:
import {StyleSheet} from ‘react-native’;
import { Dimensions, Platform, PixelRatio } from ‘react-native’;
const {
width,
height
} = Dimensions.get(‘window’);
const scale = width / 320;
const ratioKeys = {
fontSize: true,
paddingHorizontal: true,
paddingVertical: true,
paddingTop: true,
paddingLeft: true,
paddingRight: true,
paddingBottom: true,
padding: true,
marginHorizontal: true,
marginVertical: true,
marginTop: true,
marginRight: true,
marginLeft: true,
marginBottom: true,
margin: true,
width: true,
height: true,
lineHeight: true,
}
// automatically scale specific keys (specified above) according to the screen size
const parseKey = (key, value) => {
if (ratioKeys[key]) {
value = value * scale;
}
return value;
}
export const parseStyle = (style) => {
console.log(‘style -> ’, style);
return Object.keys(style).reduce((output, key) => {
output[key] = parseKey(key, style[key]);
return output;
}, {});
}
const parseStyleSheet = (styles) => {
console.log(‘styles -> ’, styles);
return Object.keys(styles).reduce((output, key) => {
output[key] = parseStyle(styles[key]);
return output;
}, {});
}
export default {
create: (style) => {
return StyleSheet.create(parseStyleSheet(style));
}
}
It seems pretty strange that this wouldn't actually be supported out of the box, so I guess I am missing something?

I've seen this library as a solution, it supports media queries, and we may eventually use it if we feel we need more customization than flexbox. I've seen it in action and it looks like it might address some of your needs: https://github.com/vitalets/react-native-extended-stylesheet

Related

simple solution for sizing sidebar of a react-admin app

I simply want to define the width of the sidebar according to the documentation "Sidebar Customization" here: https://marmelab.com/react-admin/Theming.html
import { createMuiTheme } from '#material-ui/core/styles';
const theme = createMuiTheme({
sidebar: { // <- Error 'sidebar' does not exist in type 'ThemeOptions'
width: 300, // The default value is 240
closedWidth: 70, // The default value is 55
},
});
const App = () => (
<Admin theme={theme} dataProvider={simpleRestProvider('http://path.to.my.api')}>
// ...
</Admin>
);
I get a :
'sidebar' does not exist in type 'ThemeOptions'
which is correct, when looking into material-ui.core.styles.ThemeOptions
What is the simpliest way to simply size the sidebar ? I like to do because my MenuItemLink texts overlaps the size of the sidebar.
The createMuiTheme() call is not needed:
const darkTheme = {
sidebar: {
width: 220,
closedWidth: 55,
},
}
Only this code worked for me, because they don't export types on v3.9.4, but I am using TS.
import { ThemeOptions } from "#material-ui/core";
export interface CustomThemeOptions extends ThemeOptions {
sidebar?: {
width?: number;
closedWidth?: number;
};
}
const theme: CustomThemeOptions = {
...defaultTheme,
sidebar: {
width: 100,
closedWidth: 40,
},
};
issue https://github.com/marmelab/react-admin/issues/5426
With the current version 3.19.10 you also have to widen the menu if you don't want menu item labels to wrap at the original sidebar width of 240px. e.g.
import { defaultTheme } from "react-admin";
export const defaultAppTheme = {
...defaultTheme,
sidebar: {
width: 260,
closedWidth: 55,
},
menu: {
width: 260,
closedWidth: 55,
},
};
You can see where the menu widths are pulled from the theme and applied here in their source code

Detox: detect that element was displayed

We have a toast component in our app that is adding considerable flakiness to our tests. The toast component displays an animated View for 4s and then disappears. In a lot of tests I need to check what the message content is in order to continue with the test.
The toast component is implemented with the following code:
// #flow
import * as React from "react"
import { StyleSheet, View, Animated, Dimensions, Text } from "react-native"
import type {
TextStyle,
ViewStyle,
} from "react-native/Libraries/StyleSheet/StyleSheet"
import type AnimatedValue from "react-native/Libraries/Animated/src/nodes/AnimatedValue"
import type { CompositeAnimation } from "react-native/Libraries/Animated/src/AnimatedImplementation"
import { AnimationConstants } from "constants/animations"
const styles = StyleSheet.create({
container: {
position: "absolute",
left: 0,
right: 0,
elevation: 999,
alignItems: "center",
zIndex: 10000,
},
content: {
backgroundColor: "black",
borderRadius: 5,
padding: 10,
},
text: {
color: "white",
},
})
type Props = {
style: ViewStyle,
position: "top" | "center" | "bottom",
textStyle: TextStyle,
positionValue: number,
fadeInDuration: number,
fadeOutDuration: number,
opacity: number,
}
type State = {
isShown: boolean,
text: string | React.Node,
opacityValue: AnimatedValue,
}
export const DURATION = AnimationConstants.durationShort
const { height } = Dimensions.get("window")
export default class Toast extends React.PureComponent<Props, State> {
static defaultProps = {
position: "bottom",
textStyle: styles.text,
positionValue: 120,
fadeInDuration: AnimationConstants.fadeInDuration,
fadeOutDuration: AnimationConstants.fadeOutDuration,
opacity: 1,
}
isShown: boolean
duration: number
callback: Function
animation: CompositeAnimation
timer: TimeoutID
constructor(props: Props) {
super(props)
this.state = {
isShown: false,
text: "",
opacityValue: new Animated.Value(this.props.opacity),
}
}
show(text: string | React.Node, duration: number, callback: Function) {
this.duration = typeof duration === "number" ? duration : DURATION
this.callback = callback
this.setState({
isShown: true,
text: text,
})
this.animation = Animated.timing(this.state.opacityValue, {
toValue: this.props.opacity,
duration: this.props.fadeInDuration,
useNativeDriver: true,
})
this.animation.start(() => {
this.isShown = true
this.close()
})
}
close(duration?: number) {
const delay = typeof duration === "undefined" ? this.duration : duration
if (!this.isShown && !this.state.isShown) return
this.timer && clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.animation = Animated.timing(this.state.opacityValue, {
toValue: 0.0,
duration: this.props.fadeOutDuration,
useNativeDriver: true,
})
this.animation.start(() => {
this.setState({
isShown: false,
})
this.isShown = false
if (typeof this.callback === "function") {
this.callback()
}
})
}, delay)
}
componentWillUnmount() {
this.animation && this.animation.stop()
this.timer && clearTimeout(this.timer)
}
render() {
const { isShown, text, opacityValue } = this.state
const { position, positionValue } = this.props
const pos = {
top: positionValue,
center: height / 2,
bottom: height - positionValue,
}[position]
if (isShown) {
return (
<View style={[styles.container, { top: pos }]}>
<Animated.View
style={[
styles.content,
{ opacity: opacityValue },
this.props.style,
]}
>
{React.isValidElement(text) ? (
text
) : (
<Text style={this.props.textStyle}>{text}</Text>
)}
</Animated.View>
</View>
)
}
return null
}
}
Normally we display the toast message for 4s, but I decided to display it in e2e tests for 1.5s in order to make some what faster.
I'm testing for the presence of the toast like this:
await expect(element(by.text(text))).toBeVisible()
await waitFor(element(by.text(text))).toBeNotVisible().withTimeout(2000)
However it happens often that detox fails at "toBeVisible". I can see the message on the screen, but for some reason detox is missing it.
What is the minimum time I should keep the message on the screen for detox to detect it?
On .circleCI we record videos of failing tests. When a test fails with "cannot find element" and I watch the video I clearly see the toast appearing on the screen, but detox fails to find it.
I'm still not sure if there is a better way, but I found a way that currently works for us.
Instead of automatically hiding the toast in e2e test, we mock the modal component to display and stay visible until tapped on.
Once detox sees the element we tap on it, close it and continue with our test.
I also had exactly the same problem in my project and the the solution that we found was to disable detox synchronization around the toast.
As an example, this is how the code would look like:
await device.disableSynchronization();
await element(by.id(showToastButtonId)).tap();
await waitFor(element(by.text('Toast Message')))
.toExist()
.withTimeout(TIMEOUT_MS);
await device.enableSynchronization();
Reference: https://github.com/wix/Detox/blob/master/docs/Troubleshooting.Synchronization.md#switching-to-manual-synchronization-as-a-workaround

React Native - How to convert image to Base64 and Visa versa in android

I have created an app which takes photo and uploads to AWS s3. Once I captured those, I need to hash those. There I used Base64 hash method and the captured photos get encrypted. But I am not able to open that and I am not sure whether I have done it in a proper way or not.
Before uploading, I want to un-hash those. That is, they must be stored as a hashed image in the gallery. But, I want to convert them as the original image before uploading. So, I can store the real image in the cloud.
My code is,
import React, {Component} from 'react';
import {Platform, StyleSheet,Alert, Text,TouchableOpacity, View,Picker,Animated,Easing,Image, NetInfo,
Dimensions,Button,ScrollView } from 'react-native';
import ImagePicker from 'react-native-image-picker';
import RNFS from 'react-native-fs';
class SecondScreen extends React.Component {
takePic = () => {
if(this.state.connection_Status==="Online"){
this.getServerTime();
try{
this.setState({capturedTime:this.state.serverTime.currentFileTime+'_'+time},
() => console.log(this.state.serverTime.currentFileTime)
);
} catch (err) {
var date = new Date();
var time = date.getTime();
this.setState({capturedTime:time});
console.log("localtime")
}
}
const options = {
quality: 1.0,
maxWidth: 75,
maxHeight: 75,
base64: true,
skipProcessing: true
}
ImagePicker.launchCamera(options,(responce)=>{
this.state.testImage.push({ uri: responce.uri });
const file ={
uri : responce.uri,
name :responce.fileName,
method: 'POST',
width : 50,
height : 50,
path : responce.path,
type : responce.type,
notification: {
enabled: true
}
}
this.setState(prevState => {
// get the previous state values for the arrays
let saveImages = prevState.saveImages;
// add the values to the arrays like before
saveImages.push(file);
// return the new state
return {
saveImages
}
});
const base64 = RNFS.writeFile(responce.path, 'base64');
return base64;
})
}
_upload=()=>{
if(this.state.connection_Status==="Online"){
const config ={
keyPrefix :aws_keyPrefix,
bucket : aws_bucketName,
region :aws_region,
accessKey:aws_accessKey,
secretKey :aws_secretKey,
successActionStatus :201
}
//store captured images in an array
this.state.saveImages.map((image) => {
RNS3.put(image,config)
.then((responce) => {
console.log(image);
});
});
if (this.state.saveImages && this.state.saveImages.length) {
Alert.alert("Successfully, uploaded");
//reset the arrays
this.setState({saveImages:''});
this.setState({testImage:''});
} else {
Alert.alert('No images captured');
}
} else {
Alert.alert('Upload failed. User is in offline');
}
}
render() {
return (
<View style={styles.Camera}>
<TouchableOpacity onPress={this.takePic.bind(this)}>
<Text>Take Picture</Text>
</TouchableOpacity>
<View style={styles.Send}>
<TouchableOpacity onPress={() => this._upload()}>
<Text>Send</Text>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
Camera :{
justifyContent: 'center',
alignItems: 'center',
marginTop : 20,
backgroundColor : '#48a4ff',
alignItems : 'center',
padding : 1,
borderWidth : 1,
borderColor : '#48a4ff',
},
Send :{
justifyContent: 'center',
alignItems: 'center',
marginTop : 20,
backgroundColor : '#48a4ff',
alignItems : 'center',
padding : 3,
borderWidth : 1,
borderColor : '#48a4ff',
}
});
export default SecondScreen;
This code I am using to hash the image,
const base64 = RNFS.writeFile(responce.path, 'base64');
return base64;
After hashing image properties will be as below,
How to convert it into the real image before uploading?
Can anyone assist me, since a day I am trying. I did not any document as such to implete this functionality.
I have done this using a react-native-fs library. Image hashing I am doing in client side i.e in react-native and unhashing I am doing in server side.( I store images in Amezon s3, before downing we unhash those images using python.)
My image hashing code is,
takePic = () => {
/* other sets of codes
.
.*/
const base64 = RNFS.writeFile(responce.uri, responce.data);
return base64;
}

What is the point of StyleSheet.create

I'm reading the React Native docs / tutorial, and I'm wondering what the point of the StyleSheet.create function is.
For example, the tutorial has the following code:
const styles = StyleSheet.create({
bigblue: {
color: 'blue',
fontWeight: 'bold',
fontSize: 30,
},
red: {
color: 'red',
},
});
But I don't understand the difference between that and:
const styles = {
bigblue: {
color: 'blue',
fontWeight: 'bold',
fontSize: 30,
},
red: {
color: 'red',
},
};
TL;DR Always use StyleSheet.create() when you can.
The answer by Nico is correct, but there is more to it.
To summarize:
It validates the styles as mentioned by Nico
As mentioned in the documentation:
Making a stylesheet from a style object makes it possible to refer to it by ID instead of creating a new style object every time.
Also mentioned in the documentation:
It also allows to send the style only once through the bridge. All subsequent uses are going to refer an id (not implemented yet).
As you might know, sending the data across the bridge is a very costly operation that has significant impact on the performance of the application. So, using StyleSheet.create() you reduce the strain on the bridge.
StyleSheet.create does not add performance gains anymore.
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/29265#issuecomment-430783289
quoting the github comment:
#alloy I understand what docs says, but can prove that code:
const myStyle: ViewStyle = { flex: 1 } export const FlexView:
React.SFC = (props) => <View style={myStyle}>{props.children}</View>
has almost same performance (even slightly faster) compared to
const s = StyleSheet.create({ flex: { flex: 1 } })
export const FlexView: React.SFC = (props) => <View style={s.flex}>{props.children}</View>
because if you look at sources, you discover that latest chunk effectively extracted to this (see:
https://github.com/facebook/react-native/blob/0.57-stable/Libraries/StyleSheet/StyleSheet.js#L373):
const s = { flex: { flex: 1 } }
export const FlexView = (props) => <View style={s.flex}>{props.children}</View>
And yes, in previous
versions of RN it was global registry of styles, but it was even more
slow, because it never crossed bridge border actually (proof from 0.55
branch) 😀
Here is there source code of create.
create<T: Object, U>(obj: T): {[key:$Keys<T>]: number} {
var result: T = (({}: any): T);
for (var key in obj) {
StyleSheetValidation.validateStyle(key, obj);
result[key] = ReactNativePropRegistry.register(obj[key]);
}
return result;
}
I am not an expert of React in any. I actually never used it but here are my insights. It seems that create does some kind of validation over your keys and register them to React.
I think you could skip the validation by simply not calling create but I'm not sure what ReactNativePropRegistry.register does exactly.
Reference to the source
As #Mentor pointed out in the comments:
.create still only validates in development and does nothing else. In production it just returns the object. See source code in repository.
source code
create<+S: ____Styles_Internal>(obj: S): $ObjMap<S, (Object) => any> {
if (__DEV__) {
for (const key in obj) {
StyleSheetValidation.validateStyle(key, obj);
if (obj[key]) {
Object.freeze(obj[key]);
}
}
}
return obj;
}
I think this comment deserves to be more noticeable. So I post it as an answer.
Additionally, I'd like to point out that validation - is a good thing, but there is another, better way to validate - Typescript:
const styles = StyleSheet.create({
someViewStyle: { ... },
someTextStyle: { ... },
})
can be replaced with
import { ..., ViewStyle, TextStyle } from 'react-native';
interface Styles {
someViewStyle: ViewStyle,
someTextStyle: TextStyle,
}
const styles = {
someViewStyle: { ... },
someTextStyle: { ... },
}
And it's not just static-time check, it also allows to discriminate between ViewStyle and TextStyle.
But there are more lines of code. So, personally, I prefer to go without styles object, if possible:
const someViewStyle: ViewStyle = { ... },
const someTextStyle: TextStyle = { ... },

Animate listview items when they are added/removed from datasource

Can someone give me an idea of how this can be done, e.g. animate the height from 0 when added and back to 0 when removed?
Animation when added is easy, just use Animated in componentDidMount with your listRow , for example:
componentDidMount = ()=> {
Animated.timing(this.state._rowOpacity, {
toValue: 1,
duration: 250,
}).start()
}
Animate a component before unmount is much harder in react-native. You should set a handler for ListView. When dataSource changed, diff the data, start Animated to hide removed row, and set new dataSource for ListView.
Here you can get full working example for opacity animation:
import React from 'react-native';
export default class Cell extends React.Component {
constructor(props) {
super(props);
this.state = {
opacity: new React.Animated.Value(0)
};
}
componentDidMount() {
React.Animated.timing(this.state.opacity, {
toValue: 1,
duration: 250,
}).start();
}
render() {
return (
<React.Animated.View style={[styles.wrapper, {opacity: this.state.opacity}]}>
<React.Image source={{uri: 'http://placehold.it/150x150'}} style={styles.image}/>
<React.Text style={styles.text}>
Text
</React.Text>
</React.Animated.View>
);
}
}
const styles = React.StyleSheet.create({
wrapper: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
},
image: {
height: 40,
width: 40,
marginRight: 16,
backgroundColor: '#C9D5E6'
},
text: {
fontSize: 20
}
});
In case you need for removing an item from the list, here's how to do the ListRow component:
class DynamicListRow extends Component {
// these values will need to be fixed either within the component or sent through props
_defaultHeightValue = 60;
_defaultTransition = 500;
state = {
_rowHeight : new Animated.Value(this._defaultHeightValue),
_rowOpacity : new Animated.Value(0)
};
componentDidMount() {
Animated.timing(this.state._rowOpacity, {
toValue : 1,
duration : this._defaultTransition
}).start()
}
componentWillReceiveProps(nextProps) {
if (nextProps.remove) {
this.onRemoving(nextProps.onRemoving);
} else {
// we need this for iOS because iOS does not reset list row style properties
this.resetHeight()
}
}
onRemoving(callback) {
Animated.timing(this.state._rowHeight, {
toValue : 0,
duration : this._defaultTransition
}).start(callback);
}
resetHeight() {
Animated.timing(this.state._rowHeight, {
toValue : this._defaultHeightValue,
duration : 0
}).start();
}
render() {
return (
<Animated.View
style={{height: this.state._rowHeight, opacity: this.state._rowOpacity}}>
{this.props.children}
</Animated.View>
);
}
}
i've posted a complete tutorial to this question in this blog post. And it's explaining step by step what you need to do to accomplish both adding and removing an item and animate this process.
For adding is pretty straight forward, but for removing looks like it's a little bit more complex.
http://moduscreate.com/react-native-dynamic-animated-lists/
Here's a full example for height and opacity animation. It supports both adding and removing an element. The key point is that you need to reset the height and opacity after the disappearing animation completes. Then you immediately delete the item from the source.
export const ListItem = (props: ListItemProps) => {
// Start the opacity at 0
const [fadeAnim] = useState(new Animated.Value(0));
// Start the height at 0
const [heightAnim] = useState(new Animated.Value(0));
/**
* Helper function for animating the item
* #param appear - whether the animation should cause the item to appear or disappear
* #param delay - how long the animation should last (ms)
* #param callback - callback to be called when the animation finishes
*/
const _animateItem = (appear: boolean = true, delay: number = 300, callback: () => void = () => null) => {
Animated.parallel([
Animated.timing(
fadeAnim,
{
toValue: appear ? 1 : 0,
duration: delay,
}
),
Animated.timing(
heightAnim,
{
toValue: appear ? 100 : 0,
duration: delay,
}
),
]).start(callback);
};
// Animate the appearance of the item appearing the first time it loads
// Empty array in useEffect results in this only occuring on the first render
React.useEffect(() => {
_animateItem();
}, []);
// Reset an item to its original height and opacity
// Takes a callback to be called once the reset finishes
// The reset will take 0 seconds and then immediately call the callback.
const _reset = (callback: () => void) => {
_animateItem(true,0, callback);
}
// Deletes an item from the list. Follows the following order:
// 1) Animate the item disappearing. On completion:
// 2) Reset the item to its original display height (in 0 seconds). On completion:
// 3) Call the parent to let it know to remove the item from the list
const _delete = () => {
_animateItem(false, 200, () => _reset(props.delete));
};
return (
<Animated.View
style={{height: heightAnim, opacity: fadeAnim, flexDirection: 'row'}}>
<Text>{props.text}</Text>
<Button onPress={() => _delete()}><Text>Delete</Text></Button>
</Animated.View>
);
}