In my React Native application, I have a 48 x 48 profile picture component that is in the header on all of the screens.
ProfilePicture.jsx
const ProfilePicture = () => {
return (
<>
<Image style={styles.profPic} source={{ uri: userObj?.avatar }} />
</>
);
};
const styles = StyleSheet.create({
profPic: {
minHeight: 48,
minWidth: 48,
borderRadius: 100,
shadowColor: "#000",
shadowOffset: {
width: 2,
height: 2,
},
shadowOpacity: 1.0,
shadowRadius: 3.84,
elevation: 5,
},
});
export default ProfilePicture;
Now, I want to use a larger version in my user settings screen where the user can change their profile image.
My thought was that by setting the style in the imported component, it would show a larger version of the image:
<ProfilePicture style={{ width: 100, height: 100 }} />
However, the image remains the same size. I know I could just create another component with the required size for this page, but since the image comes from a remote server, I don't want to call the URL multiple times.
How can I do this just using my current component?
A good way to handle this would be to pass in a style prop to your ProfilePicture component. That way any component using the ProfilePicture component would be able to overrule the default styling. Combine this with the fact that the style prop of react-native components can take in arrays as value, and the code is pretty clean
const ProfilePicture = ({ style }) => {
return (
<Image style={[styles.profPic, style]} source={{ uri: userObj?.avatar }} />
);
};
const styles = StyleSheet.create({
profPic: {
minHeight: 48,
minWidth: 48,
borderRadius: 100,
shadowColor: "#000",
shadowOffset: {
width: 2,
height: 2,
},
shadowOpacity: 1.0,
shadowRadius: 3.84,
elevation: 5,
},
});
export default ProfilePicture;
Your example code will then work as intended
<ProfilePicture style={{ width: 100, height: 100 }} />
Related
Background:
I am trying to add a tooltip to a react-native-svg chart following this tutorial.
The link to the tutorial: Link
Current Code Implementation:
import React, {useState} from 'react';
import {View, Text, Dimensions} from 'react-native';
import {LineChart} from 'react-native-chart-kit';
import {Rect, Text as TextSVG, Svg} from 'react-native-svg';
const Charts = () => {
let [tooltipPos, setTooltipPos] = useState({
x: 0,
y: 0,
visible: false,
value: 0,
});
return (
<View>
<LineChart
data={{
labels: ['January', 'February', 'March', 'April', 'May', 'June'],
datasets: [
{
data: [100, 110, 90, 130, 80, 103],
},
],
}}
width={Dimensions.get('window').width}
height={250}
yAxisLabel="$"
yAxisSuffix="k"
yAxisInterval={1}
chartConfig={{
backgroundColor: 'white',
backgroundGradientFrom: '#fbfbfb',
backgroundGradientTo: '#fbfbfb',
decimalPlaces: 2,
color: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
labelColor: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
style: {
borderRadius: 0,
},
propsForDots: {
r: '6',
strokeWidth: '0',
stroke: '#fbfbfb',
},
}}
bezier
style={{
marginVertical: 8,
borderRadius: 6,
}}
decorator={() => {
return tooltipPos.visible ? (
<View>
<Svg>
<Rect
x={tooltipPos.x - 15}
y={tooltipPos.y + 10}
width="40"
height="30"
fill="black"
/>
<MaterialCommunityIcons
name="run"
size={32}
color="rgb(67, 67, 67)"
/>
<TextSVG
x={tooltipPos.x + 5}
y={tooltipPos.y + 30}
fill="white"
fontSize="16"
fontWeight="bold"
textAnchor="middle">
{tooltipPos.value}
</TextSVG>
</Svg>
</View>
) : null;
}}
onDataPointClick={(data) => {
let isSamePoint = tooltipPos.x === data.x && tooltipPos.y === data.y;
isSamePoint
? setTooltipPos((previousState) => {
return {
...previousState,
value: data.value,
visible: !previousState.visible,
};
})
: setTooltipPos({
x: data.x,
value: data.value,
y: data.y,
visible: true,
});
}}
/>
</View>
);
};
Question:
I want to add the icon(Running icon) as seen on the image above, to be next to the tool-tip text.
The Icon, then the text inside the rectangle filled in Black. When I try to position it,, it shows up on the extreme top left for some reason. How do I position it?
You can use the ForeignObject component from react-native-svg and change your decorator to something like this:
decorator={() => {
return tooltipPos.visible ? (
<ForeignObject x={tooltipPos.x} y={tooltipPos.y}>
<View
style={{
width: 70,
flexDirection: 'row',
backgroundColor: 'black',
}}>
<MaterialCommunityIcons
name="run"
size={32}
color="rgb(67, 67, 67)"
/>
<Text
style={{
color: 'white',
fontSize: 16,
fontWeight: 'bold',
textAnchor: 'middle',
}}>
{tooltipPos.value}
</Text>
</View>
</ForeignObject>
) : null;
}}
The problem with what you had before is that the react-native-svg Text and Rect components use x and y coordinates and your icon doesn't, so the positioning will be off.
The benefit of the approach shown above is that you only have to specify the x and y props of the ForeignObject. Everything inside the ForeignObject can be regular views, positioned as you normally would (https://github.com/react-native-community/react-native-svg#foreignobject).
I've chosen the tooltipPos.x and tooltipPos.y for the x and y prop values of the ForeignObject respectively, but you could add an offset if necessary.
Be sure to import ForeignObject from react-native-svg.
I'd like to create a reusable component using Material-UI's api (not using styled-components.) I got this far - and it almost works - but the settings that use theme variable don't work (e.g, bgcolor and padding). Am I doing something wrong - or is this not possible?
const BigPanel = styled(Box)({
display: 'flex',
width: '100%',
flexgrow: 1,
bgcolor: 'background.paper',
borderRadius: 10,
boxShadow:'1',
p:{ xs: 4, md: 8 }
});
The object passed to styled is intended to be CSS properties, but you have a mixture of CSS properties and Box props (bgcolor, p). Even the ones that are valid CSS properties (display, width) are also valid Box props, so the most straightforward solution is to specify all of them as props.
One way to handle this is to use defaultProps. This makes it very easy to override some of the props when using the component by specifying them explicitly as shown in the example below.
import React from "react";
import Box from "#material-ui/core/Box";
import CssBaseline from "#material-ui/core/CssBaseline";
import { styled } from "#material-ui/core/styles";
const BigPanel = styled(Box)({});
BigPanel.defaultProps = {
display: "flex",
width: "100%",
borderRadius: 10,
flexGrow: 1,
bgcolor: "background.paper",
p: { xs: 4, md: 8 },
boxShadow: "1"
};
export default function App() {
return (
<>
<CssBaseline />
<BigPanel>Default BigPanel</BigPanel>
<BigPanel bgcolor="primary.main" color="primary.contrastText">
BigPanel with explicit props
</BigPanel>
</>
);
}
In the example above, styled isn't really serving any purpose anymore except to create a new component type. Though it isn't less code, below is an alternative way to get the same effect without using styled:
const BigPanel = React.forwardRef(function BigPanel(props, ref) {
return <Box ref={ref} {...props} />;
});
BigPanel.defaultProps = {
display: "flex",
width: "100%",
borderRadius: 10,
flexGrow: 1,
bgcolor: "background.paper",
p: { xs: 4, md: 8 },
boxShadow: "1"
};
The goal: have a dropdown view that animates the height-expansion over time. the caveat is this: once the view is expanded, it needs to be able to dynamically handle whether or not there is additional view data present. if it is present, a couple extra text components will be rendered.
The problem: as it currently is, the animation the parent's height to a fixed height. so when the additionalContent is rendered, it surpasses the bounds of the parent, whose height is fixed. I dont want to not set the height of the parent explicitly, because then I cant animate that aspect the way I want. I want to maintain the height animation as-is, as well as dynamically size the parent to contain the children when the additionalContent is present
const ListItem = (props) => {
const [checkInModal, setCheckInModal] = useState(false);
const [animatedHeight, setAnimatedHeight] = useState(new Animated.Value(0))
const [animatedOpacity] = useState(new Animated.Value(0))
const [dynamicHeight, setDynamicHeight] = useState(0);
const [expanded, setExpanded] = useState(false);
const toggleDropdown = () => {
if (expanded == true) {
// collapse dropdown
Animated.timing(animatedHeight, {
toValue: 0,
duration: 200,
}).start()
} else {
// expand dropdown
Animated.timing(animatedHeight, {
toValue: 100,
duration: 200,
}).start()
}
setExpanded(!expanded)
}
const renderAdditionalContent = () => {
setDynamicHeight(75);
if (someVariable == true) {
return (
<View> <Text> Some Content </Text> </View>
)
}
}
const interpolatedHeight = animatedHeight.interpolate({
inputRange: [0, 100],
outputRange: [75, 225]
})
const interpolatedOpacity = animatedOpacity.interpolate({
inputRange: [0, 100],
outputRange: [0.0, 1.0]
})
return (
<Animated.View
style={[styles.container, { height: interpolatedHeight + dynamicHeight }]}
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', }}>
<View style={styles.leftContainer}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={styles.title}>{props.title}</Text>
</View>
<Text style={styles.subtitle}>{time()}</Text>
</View>
<View style={styles.rightContainer}>
<TouchableOpacity onPress={() => toggleDropdown()} style={styles.toggleBtn}>
<Image source={require('../assets/img/chevron-down.png')} resizeMode={'contain'} style={styles.chevron} />
</TouchableOpacity>
</View>
</View>
{expanded == true ? (
<Animated.View style={[styles.bottomContainer, { opacity: interpolatedOpacity }]}>
<Components.BodyText text="Subject:" style={{ fontFamily: Fonts.OPENSANS_BOLD }} />
<Components.BodyText text={props.subject} />
{ renderAdditionalContent() }
</Animated.View>
) : null}
</Animated.View>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: '#fff',
borderRadius: 25,
width: width * 0.95,
marginBottom: 5,
marginHorizontal: 5,
paddingVertical: 15,
paddingHorizontal: 15
},
leftContainer: {
justifyContent: 'space-between',
},
rightContainer: {
flexDirection: 'row',
alignItems: 'center'
},
title: {
fontFamily: Fonts.OPENSANS_BOLD,
fontSize: 20,
color: '#454A66'
},
subtitle: {
color: '#454A66',
fontSize: 14
},
typeIcon: {
height: 25,
width: 25
},
chevron: {
height: 15,
width: 15
},
toggleBtn: {
borderWidth: 1,
borderColor: Colors.PRIMARY_DARK,
borderRadius: 7,
paddingTop: 4,
paddingBottom: 2.5,
paddingHorizontal: 4,
marginLeft: 10
},
bottomContainer: {
marginVertical: 20
},
buttonContainer: {
flexDirection: 'row',
width: 250,
justifyContent: 'space-between',
alignSelf: 'center',
marginVertical: 20
},
noShadow: {
elevation: 0,
shadowOffset: {
width: 0,
height: 0
},
shadowRadius: 0,
}
});
export default ListItem;
How can this be accomplished? So far ive tried creating a state variable dynamicHeight and setting it inside the function that renders additional content, but that hasn't worked.
Heres the snack: https://snack.expo.io/P6WKioG76
clarification edit: renderAdditionalContent function renders additional content (obviously), this content could be anywhere from one line of characters to multiple lines. regardless of the char count, the main parent container of the component needs to have all the children within its bounds. as it stands, if the additional-content-rendered has too much content, the content will spill over the border of the component's main parent container, which must be avoided. this can be done by simply not giving a height to the main component container, obviously. but the the idea is to have the animated height AND wrap the child content properly
Any suggestions?
EDITED : easy and simple way
You can also apply the height 100%, in that way we don't need to calculate the height of inner content it will auto adjust as per the content provided
const interpolatedHeight = animatedHeight.interpolate({
inputRange: [0, 100],
outputRange: ["0%", "100%"] // <---- HERE
})
To make this happen I have added <View> tag around the <Animated.View> to get height 100% correct and few changes in css, this looks more elegant and provides a perfect solution to your problem.
WORKING DEMO
So, you can use onLayout
This event is fired immediately once the layout has been calculated
First step:
// setHeight
const setHeight = (height) => {
setDynamicHeight(prev => prev + height);
}
<View onLayout={(event) => {
var { x, y, width, height } = event.nativeEvent.layout;
setHeight(height); // <--- get the height and add it to total height
}}>
<Text>Subject</Text>
<Text>Subject Content</Text>
{renderAdditionalContent()}
</View>
Second step:
useEffect(() => {
// trigger if only expanded
if (expanded) {
// trigger height animation , whenever there is change in height
Animated.timing(animatedHeight, {
toValue: dynamicHeight, // <--- animate to the given height
duration: 200,
}).start();
}
}, [dynamicHeight]); // <--- check of height change
WORKING DEMO (you can test it by adding remove text)
Here's a working snack of your code with a dynamic dropdown height: https://snack.expo.io/4mT5Xj6qF
You can change the height by changing the thisIsAValue constant.
Honestly you were very close with your code, you just had a few bits and pieces missing:
When you're animating height you don't need to use interpolate, you can let the <Animated.View> straight up calculate the height. The interpolation is needed if you want to animate, for example, a rotation, and you need to calculate the degrees to which the element must move.
You need to pass your dynamic height into the animation, as your to: value
I set up a simple useEffect hook checking for changes in the someVariable and dynamicHeight prop
This solution will let you set the height dynamically through a prop.
However if you want to calculate the height based on elements which are present in the View you may want to check #vivek-doshi 's answer
You can use React refs. This allows you to access a component directly.
const el = React.useRef(null);
Assign el to the ref of the container div of whatever content you have. Then hide/show the div using the visibility style:
<div style={{ visibility: !expanded && "hidden" }} ref={el}>
<View /* snip */>
{ renderAdditionalContent() }
/* -- snip -- */
</View>
</div>
Then in the toggleDropdown function you can access the height of the component via the .current.offsetHeight property:
Animated.timing(animatedHeight, {
toValue: el.current.offsetHeight,
duration: 200,
}).start()
Refs can be used on any html element and allow you to access the element's raw properties. Animations are probably one of the most common use cases for them.
Demo: https://snack.expo.io/heLsrZNpz
You can read more here: https://reactjs.org/docs/refs-and-the-dom.html
I calculate various margin's based on the images i'm displaying in my app, which range in different pixel widths each time images are selected, so my need us quite dynamic. Once i calculate the margin widths, i save them in state variables marginL and marginR.
However i can't seem to access these in StyleSheet, i just get an error message to say marginLeft is undefined.
const styles = StyleSheet.create({
container: {
flex: 1,
},
gap: {
flex: 0.5,
marginLeft: this.state.marginL,
marginRight: this.state.marginR
}
})
How do i get access to my variables?
I'm not sure about dynamically changing in StyleSheet value. But when you calculate the margin widths you can override your style something like below
<View style={[styles.gap, {marginLeft: this.state.marginL, marginRight: this.state.marginR}]} />
Check complete example
import React, { Component } from "react";
import { View, StyleSheet } from "react-native";
export default class App extends Component {
state = {
marginL: 10,
marginR: 20
};
render() {
return (
<View style={styles.container}>
<View style={[ styles.gap, { marginLeft: this.state.marginL, marginRight: this.state.marginR }]} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "red"
},
gap: {
flex: 0.5,
marginLeft: 0,
marginRight: 0,
backgroundColor: "green"
}
});
This not be the optimal solution but hope this helps you. Feel free for doubts.
I want to use <RNPickerSelect./> with a <TextInput/> in a single row. So, when I make the Parent flexDirection: row, I see only the arrow and no text. Even if I remove the <TextInput/>, I don't see any text in the Picker.
import React, { Component } from 'react';
import {
StyleSheet,
View,
} from 'react-native';
import RNPickerSelect from 'react-native-picker-select';
type Props = {}
const countryCode = [
{
label: '+91',
value: '+91',
},
{
label: '+1',
value: '+1',
},
{
label: '+2',
value: '+2',
},
];
export default class PickerTest extends Component<Props> {
constructor() {
super()
this.state = {
phoneNumber: "",
countryCode: ""
}
}
render() {
return (
<View style={{flexDirection:'row'}}>
<View paddingVertical={5}>
{/* and hiding the InputAccessoryView on iOS */}
<RNPickerSelect
placeholder={{}}
items={countryCode}
onValueChange={value => {
this.setState({
countryCode: value,
});
}}
InputAccessoryView={() => null}
style={pickerSelectStyles}
value={this.state.countryCode}
/>
</View>
</View>
);
}
}
const pickerSelectStyles = StyleSheet.create({
inputIOS: {
fontSize: 16,
paddingVertical: 12,
paddingHorizontal: 10,
borderWidth: 1,
borderColor: 'gray',
borderRadius: 4,
color: 'black',
paddingRight: 30, // to ensure the text is never behind the icon
},
inputAndroid: {
fontSize: 16,
paddingHorizontal: 10,
paddingVertical: 8,
borderWidth: 0.5,
borderColor: 'purple',
borderRadius: 8,
color: 'black',
paddingRight: 30, // to ensure the text is never behind the icon
},
});
On running the above app, I get something like this
As you can see the picker is not showing the text.
Below are the configuration I am using
react-native-picker-select version: 6.3.3
react-native version: 0.61.2
react version: 16.9.0
this is an upstream issue: https://snack.expo.io/HkygCqhsr
options:
useNativeAndroidPickerStyle prop
set width and height with inputAndroid style prop
Add the atribute "pickerProps" to the RNPickerSelect with the option overflow: 'hidden'
<RNPickerSelect style={styles.selectContainer}
...
pickerProps={{ style: { height: 214, overflow: 'hidden' } }}
...
/>