How can I automatically scale an SVG element within a React Native View? - react-native

I am trying to put a react-native-svg element inside of a View such that it's rendered with a certain, fixed aspect ratio, but then scaled to be as large as possible, within the confines of the containing view.
The Svg element (from react-native-svg) seems to only accept absolute width and height attributes (I've tried using percentages, but nothing renders, and debugging confirms that percent values are NSNull by the time they get to the native view). I'm not sure how to achieve the desired effect. Here's what I've tried so far:
// I have a component defined like this:
export default class MySvgCircle extends React.Component {
render() {
return (
<View style={[this.props.style, {alignItems: 'center', justifyContent: 'center'}]} ref="containingView">
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center', aspectRatio: 1.0}}>
<Svg.Svg width="100" height="100">
<Svg.Circle cx="50" cy="50" r="40" stroke="blue" strokeWidth="1.0" fill="transparent" />
<Svg.Circle cx="50" cy="50" r="37" stroke="red" strokeWidth="6.0" fill="transparent" />
</Svg.Svg>
</View>
</View>
);
}
}
// And then consumed like this:
<MySvgCircle style={{height: 200, width: 200, backgroundColor: "powderblue"}}/>
And this is what I see when it renders.
I want the red and blue circles to be scaled up to fill the 200x200 area (staying circular if the containing view is rectangular and not square), without having foreknowledge of the size desired by the consumer/user of the component.
As mentioned, I tried using percentages, like this (the rest is the same):
<Svg.Svg width="100%" height="100%">
But then the SVG part doesn't draw at all. Like this:
There are no error messages, or other indications of why this doesn't work, in the console logs.
The methods for measuring UI elements after layout in RN appears to be asynchronous, which seems like a poor match to what I'm trying to do. Is there some sort of scaling or transform magic that I could use?
The desired output would look like this (obtained by hardcoding values):
And when the containing view isn't a perfect square I'd like it to work like this:

Here is a component that behaves like your images:
import React from 'react';
import { View } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
const WrappedSvg = () =>
(
<View style={{ aspectRatio: 1, backgroundColor: 'blue' }}>
<Svg height="100%" width="100%" viewBox="0 0 100 100">
<Circle r="50" cx="50" cy="50" fill="red" />
</Svg>
</View>
);
In context:
const WrappedSvgTest = () => (
<View>
<View style={{
width: '100%',
height: 140,
alignItems: 'center',
backgroundColor: '#eeeeee'
}}
>
<WrappedSvg />
</View>
{/* spacer */}
<View style={{ height: 100 }} />
<View style={{
width: 120,
height: 280,
justifyContent: 'space-around',
backgroundColor: '#eeeeee'
}}
>
<WrappedSvg />
</View>
</View>
);
The trick is to wrap the SVG element in a view that preserves its aspect ratio, then set the SVG sizing to 100% width and height.
I believe there is some complex interaction between the SVG element size and the viewbox size that makes the SVG render smaller than you would expect, or in some cases not render at all. You can avoid this by keeping your <View> tags at a fixed aspect ratio and setting the <Svg> tags to 100% width and height, so the viewbox aspect ratio always matches the element ratio.
Be sure to set aspectRatio to viewbox.width / viewbox.height.

the trick in
preserveAspectRatio="xMinYMin slice"
you should do that
<Svg
height="100%"
preserveAspectRatio="xMinYMin slice"
width="100%"
viewBox="0 0 100 100"
>

You have to play with the width and height together with the viewBox. Usually the viewBox you have to place the original dimensions of your desired shape. And by defining the width/height based on your needs your shape will be down/up scaled properly.
Please have a look to this tutorial where this concepts have been explained pretty clear.
https://www.sarasoueidan.com/blog/svg-coordinate-systems/

For my SVG, I was using those provided at https://material.io/resources/icons
What fixed it for me, was to make sure you don't mess with the viewBox or given values in the Paths (like I did) but only change the height and width to fill and then use the containers like the other answers:
<View style={{
height: 100, display: 'flex',
}}>
<TouchableOpacity style={{
display: 'flex', justifyContent: 'center',
alignItems: 'center', aspectRatio: 1,
}}>
<Svg fill="white" height="100%"
width="100%" viewBox="0 0 24 24">
<Path d="M0 0h24v24H0z" fill="none"/>
<Path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</Svg>
</TouchableOpacity>
</View>

i'm using react-native-svg-transformer without using react-native-svg which i found very heavy in term of size,
so i can resize and change the stroke color also the fill color, but just instead of passing a fill prop, just pass color as seen below, it works perfectly
import React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import Logo from "../../assets/profile.svg";
function FirstScreen(props) {
return (
<View style={styles.container}>
<TouchableOpacity
onPress={() => { props.navigation.navigate('SecondScreen'); }}
>
<Text>Welcome</Text>
<View style={{ aspectRatio: 1,justifyContent:"center",alignItems:"center", backgroundColor: 'blue',width:200,height:200 }}>
<Logo color="white" stroke="black" height={50} width={50} />
</View>
</TouchableOpacity>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
export default FirstScreen;
the svg code
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><title>profile</title><g fill="currentColor" class="nc-icon-wrapper"><path d="M38,37H26A19.021,19.021,0,0,0,7,56a1,1,0,0,0,.594.914C7.97,57.081,16.961,61,32,61s24.03-3.919,24.406-4.086A1,1,0,0,0,57,56,19.021,19.021,0,0,0,38,37Z"></path><path data-color="color-2" d="M32,32c8.013,0,14-8.412,14-15.933a14,14,0,1,0-28,0C18,23.588,23.987,32,32,32Z"></path></g></svg>
dependencies
"dependencies": {
"#expo/webpack-config": "~0.16.2",
"#react-navigation/native": "^6.0.10",
"#react-navigation/native-stack": "^6.6.2",
"expo": "~45.0.0",
"expo-font": "^10.1.0",
"expo-status-bar": "~1.3.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-native": "0.68.2",
"react-native-svg-transformer": "^1.0.0",
},
metro.config.js file to add in the root
const { getDefaultConfig } = require('expo/metro-config');
module.exports = (() => {
const config = getDefaultConfig(__dirname);
const { transformer, resolver } = config;
config.transformer = {
...transformer,
babelTransformerPath: require.resolve('react-native-svg-transformer'),
};
config.resolver = {
...resolver,
assetExts: resolver.assetExts.filter((ext) => ext !== 'svg'),
sourceExts: [...resolver.sourceExts, 'svg'],
};
return config;
})();

I put this whole thing into an example Snack, maybe it helps.
SNACK:
https://snack.expo.dev/#changnoi69/fbf937
When you change the marginLeft and marginRight of that view that is wrapped around the SVG-Component the SVG resizes according to it.
<View style={{marginLeft:"20%", marginRight:"20%", backgroundColor: "pink"}}>
<NoInternetConnectionSVG />
</View>
Original Stackoverflow post is here:
https://stackoverflow.com/a/73511233/12647753

You will need this variables
const originalWidth = 744;
const originalHeight = 539.286;
const aspectRatio = originalWidth / originalHeight;
Wrap your svg in a view with this properties:
<View style={{ width: '100%', aspectRatio }}></View>
or
<View style={{ width: Dimensions.get('window').width, aspectRatio }}>
</View>
Use the svg inside, with this properties:
<Svg
width='100%'
height='100%'
viewBox={`0 0 ${originalWidth} ${originalHeight}`}
>
And you should be ok!

In my case, I had to scale a SVG icon based on the device size and it was using <G> and <Path> for drawing the icon. After hours of trial and error method, I found a solution - give a dynamic scale value (based on the device size) to the inner component of Svg component. Here, the inner component is <G>
<Svg width={RfH(24)} height={RfH(24)} style={{backgroundColor: 'salmon'}}>
<G
scale={RfH(1)} // Scaling added to the inner component
fill="none"
fillRule="evenodd">
<G
stroke={props?.isFocused ? '#302F4C' : '#8B8B88'}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}>
<Path
d="M9.393 2.792 3.63 7.022c-.9.7-1.63 2.19-1.63 3.32v7.41c0 2.32 1.89 4.22 4.21 4.22h11.58c2.32 0 4.21-1.9 4.21-4.21v-7.28c0-1.21-.81-2.76-1.8-3.45l-5.807-4.36c-1.4-.98-3.65-.93-5 .12Z"
fill={props?.isFocused ? '#7BBDFF' : 'none'}
fillRule="nonzero"
/>
<Path fill="#FFF" d="M12 17.993v-2.924" />
</G>
</G>
- iPad home icon with scaling
- iPad home icon without scaling
- iPhone home icon with scaling
- iPhone home icon without scaling
Rfh just converts an input value to the current device equivalent.
import {Dimensions} from 'react-native';
const STANDARD_SCREEN_DIMENSIONS = {height: 812, width: 375};
const RfH = (value) => {
const dim = Dimensions.get('window');
return dim.height * (value / STANDARD_SCREEN_DIMENSIONS.height);
};

Related

Svg height doesnt match with parent View height

I have an Svg here with a Line. I gave Line stroke width as 10. The svg is wrapped in View with height same as stroke width 10. But somehow the Line doesn't align with the Parent height? What is causing the height difference?
import * as React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import {Svg, Line} from 'react-native-svg';
export default function App() {
const width = 100;
const height = 10;
return (
<View style={styles.container}>
<View style={{ width, height, borderWidth: 1 }}>
<Svg width={width} height={height}>
<Line x1="0" y1="0" x2={width} y2="0" stroke="red" strokeWidth={height} />
</Svg>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
backgroundColor: '#ecf0f1',
padding: 8,
},
});
expo link: https://snack.expo.dev/#lcherukuri/insane-yogurt
The top of the line is getting cut off by the view because the line is placed centered along the top line. You will want to position the line in the middle of the viewport so that the entire line is visible. The position y1 and y2 are in the center axis of the line.
<Svg width={width} height={height}>
<Line x1="0" y1={height/2} x2={width} y2={height/2} stroke="red" strokeWidth={height} />
</Svg>

SVG getting cut in react native

I am using react-native-svg with react-native-svg-transformer for rendering SVGs in our app. All SVGs are rendering correctly except this one SVG which just cuts off at its right side. This is the rendered SVG (The right side of the svg is cutoff):
and this is the original svg which I want to render as it is:
I don't know what I am doing wrong, but I also tried using the viewBox prop, still no effect. How can I rectify this?
Code:
import YourEldercarePartner from '../../Assets/Images/WelcomeScreenSVGImages/Your-Eldercare-Partner.svg';
const data = [
...
{
heading: "Welcome to Emoha!",
svg: {
image: YourEldercarePartner,
width: hp(40),
height: hp(40)
},
message: "India’s largest virtual community of Elders. This app is your one-stop solution for everything Elders need to live a healthy and energized life in the comfort of home."
},
...
];
return (
<Swiper
ref={swiper}
loop={false}
onIndexChanged={index => setIndex(index)}
showsButtons={false}
showsPagination={true}
renderPagination={handlePagination}
>
{
data.map((datum, idx) => <Screen datum={datum} key={idx}/> )
}
</Swiper>
)
const Screen = ({ datum }) => (
<View style={styles.container}>
<View style={styles.main}>
<Text style={styles.heading}>
{datum.heading}
</Text>
<View style={{ marginTop: 10, flex: 10, justifyContent: 'flex-start' }}>
<datum.svg.image
width={datum.svg.width}
height={datum.svg.height}
/>
</View>
<Text style={styles.message}>
{datum.message}
</Text>
</View>
</View>
)
Kind of late, but had similar problem and it was because svg width was actually smaller than mask width inside svg.
For example:
<svg width="261" height="251" viewBox="0 0 261 251" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask1_11005_24077" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="7" y="0" width="251" height="251">
Here mask width was accidentally 251 and after changing to 261 it was fixed and image not cut anymore. Don't see your svg anymore so I don't know if this was case for you, but was for me with multiple images.

Dynamic height for react native rn_bottom_drawer

I am using rn-bottom-drawer for drawer implementation for my app. I tried several ways like PixelRatio, ModerateScale, If-else for range of screen height but i am unsuccessful in setting such a containerHeight that it works perfectly with all device screens and there is no space between my drawer and bottom of my screen.
My Code:
<BottomDrawer
ref={"_drawer"}
containerHeight={moderateScale(270)}
startUp={false}
backgroundColor={null}
downDisplay={moderateScale(200)}
onExpanded={() => this.setState({ isRecentSearchesExpanded: true })}
onCollapsed={() => this.setState({ isRecentSearchesExpanded: false })}
>
<View style={{
width: screenWidth,
}}>
<ImageBackground source={require('../../assets/tabBkgd.png')} style={{ height: "100%", width: screenWidth, justifyContent: "center", backgroundColor: "transparent" }} resizeMode="stretch">
{/* some views here */}
</ImageBackground>
</View>
</BottomDrawer>
This is the package i use for dynamic heights and widths :
rn-responsive.
Basically what you have to do is check in your phone what height suits well. Suppose you get 80 and your deviceHeight is 640 , then all you need to do is calculate (80/640)*100 i.e 12.5 so now
Just do :
import {widthPercentageToDP as wp, heightPercentageToDP as hp} from 'react-native-responsive-screen';
<BottomDrawer
ref={"_drawer"}
containerHeight={hp("12.5%")}
and its fixed. Hope it helps. feel free for doubts

hide the background from overlapping svgs in react native

I am trying to create shape of two circles overlapping on each other and i have done it successfully just overlapping part is showing background image instead of image which is set inside circles and quite new in react-native-svg area so please help me. Here is my code:
<ImageBackground source={require('../assets/images/20.png')} style={styles.background}>
<Svg height={height} width="100%">
<Defs>
<ClipPath id="clip">
<Circle cx="320" cy="230" r="250" />
<Circle cx="80" cy="150" r="180" />
</ClipPath>
</Defs>
<SvgImg
href={require('../assets/images/18.jpg')}
width={500}
height={500}
preserveAspectRatio="xMidYMid slice"
clipPath="url(#clip)"
/>
</Svg>
</ImageBackground>
const styles = StyleSheet.create({
background: {
width: 'auto',
height: '100%',
},
})
This is a common problem in ImageBackground
Because style needs to be defined for the internal image as well
try set imageStyle
<ImageBackground
imageStyle={{
resizeMode: 'contain',//or try anohter style
overflow: 'visible',
}}
>

React Native - NativeBase components not taking up full width on iOS

In the iOS version of my react native app built with NativeBase, everything is too skinny, unless a specific width is given. See images below. I have given the header and footer a width of 100% so it is fine, but I haven't done that for the inputs and they are too skinny. The header and footer are that skinny when not given a width too.
code:
import React from 'react'
import {
Container,
Header,
Form,
Item,
Input,
Label,
Content,
Footer,
FooterTab,
Button,
Left,
Right,
Body
} from 'native-base'
import { Text, Image } from 'react-native'
export const Volcalc = () => {
return (
<Container style={styles.container}>
<Header style={styles.header}>
<Left>
<Image resizeMode={Image.resizeMode.contain} style={styles.thumbnail} source={require('./img/logo_red_nowords.png')} />
</Left>
<Body>
</Body>
<Right />
</Header>
<Content>
<Form>
<Item stackedLabel bordered >
<Label>height</Label>
<Input />
</Item>
<Item stackedLabel >
<Label>width</Label>
<Input />
</Item>
</Form>
</Content>
<Footer >
<FooterTab style={styles.footer}>
<Button full>
<Text>Footer 1</Text>
</Button>
</FooterTab>
</Footer>
</Container>
)
}
const $mainColor = '#00d1b2'
const styles = {
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: $mainColor
},
header: {
width: '100%',
backgroundColor: $mainColor
},
footer: {
width: '100%',
backgroundColor: $mainColor
},
thumbnail: {
width: 35,
height: 35
}
}
I'm pretty sure I'm supposed to be able to add inputs and header, without specifying width, and it should take up the full width like Android does when not specifying. What could be wrong with my project that is causing this?
width: '100%' is supported in react native, it will take 100% of its parent width.
I think your problem is the the justifyContent:'center' and alignItems:'center' in the styles.container, they are causing all elements to be in the center of the component. You can try adding a distinctive background color to your Content component to see where it is and how much width it has.
Try adding this style:
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: $mainColor,
flexDirection: 'row' // <--- Add this line
}
Apart from above suggestion, you can use "Toggle Inspector" to get the style attributes. But I also found react native apps during development sometimes gets stuck in previous states. Try to uninstall and reinstall it.
Try using Dimensions.
import { Dimensions } from 'react-native';
var {width} = Dimension.get('window');
then you can use like
<Form style={{width:width}}>