Skip to content

Commit b750df6

Browse files
authored
Support SVG image (#1226)
* Support SVG image * Remove packages from peerDependencies * Move SVG image to assets configuration * Add docs for SVG support * Add reanimated (removed in merge) * Move SVG example to ImageScreen and move return statement
1 parent 1ee09ba commit b750df6

File tree

12 files changed

+199
-34
lines changed

12 files changed

+199
-34
lines changed

demo/src/configurations.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Assets.loadAssetsGroup('images.demo', {
1414
brokenImage: require('./assets/images/placeholderMissingImage.png')
1515
});
1616

17+
Assets.loadAssetsGroup('svgs.demo', {
18+
logo: require('../../uilib-docs/src/images/newDesign/headerLogo.svg').default
19+
});
20+
1721
Typography.loadTypographies({
1822
h1: {...Typography.text40},
1923
h2: {...Typography.text50},

demo/src/screens/componentScreens/ImageScreen.tsx

Lines changed: 95 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,53 @@
11
import React, {Component} from 'react';
2+
import {ScrollView} from 'react-native';
23
import {View, Text, Image, Colors, Assets} from 'react-native-ui-lib';
34
import {renderBooleanOption, renderRadioGroup, renderSliderOption} from '../ExampleScreenPresenter';
45

5-
66
const IMAGE_URL =
77
'https://images.pexels.com/photos/748837/pexels-photo-748837.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260';
88
const BROKEN_URL = 'file:///Desktop/website/img/cupcake.jpg';
99
const DEFAULT_SIZE = 100;
1010

11+
const file = Assets.svgs.demo.logo;
12+
const xml = `
13+
<svg width="32" height="32" viewBox="0 0 32 32">
14+
<path
15+
fill-rule="evenodd"
16+
clip-rule="evenodd"
17+
fill="url(#gradient)"
18+
d="M4 0C1.79086 0 0 1.79086 0 4V28C0 30.2091 1.79086 32 4 32H28C30.2091 32 32 30.2091 32 28V4C32 1.79086 30.2091 0 28 0H4ZM17 6C17 5.44772 17.4477 5 18 5H20C20.5523 5 21 5.44772 21 6V25C21 25.5523 20.5523 26 20 26H18C17.4477 26 17 25.5523 17 25V6ZM12 11C11.4477 11 11 11.4477 11 12V25C11 25.5523 11.4477 26 12 26H14C14.5523 26 15 25.5523 15 25V12C15 11.4477 14.5523 11 14 11H12ZM6 18C5.44772 18 5 18.4477 5 19V25C5 25.5523 5.44772 26 6 26H8C8.55228 26 9 25.5523 9 25V19C9 18.4477 8.55228 18 8 18H6ZM24 14C23.4477 14 23 14.4477 23 15V25C23 25.5523 23.4477 26 24 26H26C26.5523 26 27 25.5523 27 25V15C27 14.4477 26.5523 14 26 14H24Z"
19+
/>
20+
<defs>
21+
<linearGradient
22+
id="gradient"
23+
x1="0"
24+
y1="0"
25+
x2="8.46631"
26+
y2="37.3364"
27+
gradient-units="userSpaceOnUse">
28+
<stop offset="0" stop-color="#FEA267" />
29+
<stop offset="1" stop-color="#E75A4C" />
30+
</linearGradient>
31+
</defs>
32+
</svg>
33+
`;
34+
35+
enum SizeType {
36+
None = '',
37+
Fixed = '50',
38+
Percentage = '50%'
39+
}
40+
1141
class ImageScreen extends Component {
1242
state = {
1343
cover: true,
1444
showOverlayContent: false,
1545
overlayType: 'none',
1646
margin: 0,
17-
showErrorImage: false
47+
showErrorImage: false,
48+
showSvg: false,
49+
isFile: false,
50+
sizeType: SizeType.None
1851
};
1952

2053
renderOverlayContent() {
@@ -41,34 +74,74 @@ class ImageScreen extends Component {
4174
}
4275
}
4376

44-
render() {
77+
renderImage() {
4578
const {cover, overlayType, margin, showErrorImage} = this.state;
79+
return (
80+
<Image
81+
source={{uri: showErrorImage ? BROKEN_URL : IMAGE_URL}}
82+
errorSource={Assets.images.demo.brokenImage}
83+
cover={cover}
84+
overlayType={overlayType !== 'none' ? overlayType : undefined}
85+
style={!cover && {width: DEFAULT_SIZE, height: DEFAULT_SIZE}}
86+
customOverlayContent={this.renderOverlayContent()}
87+
{...{[`margin-${margin}`]: true}}
88+
/>
89+
);
90+
}
91+
92+
renderSvgImage() {
93+
const {isFile, sizeType} = this.state;
94+
const size: any = Number(sizeType) || sizeType;
95+
return (
96+
<>
97+
{size ? (
98+
<Image source={isFile ? file : xml} width={size} height={size}/>
99+
) : (
100+
<Image source={isFile ? file : xml}/>
101+
)}
102+
</>
103+
);
104+
}
105+
106+
renderOptions() {
107+
return (
108+
<>
109+
{renderBooleanOption.call(this, 'Show as Cover Image', 'cover')}
110+
{renderBooleanOption.call(this, 'Show Overlay Content', 'showOverlayContent')}
111+
{renderBooleanOption.call(this, 'Show Error Image', 'showErrorImage')}
112+
{renderRadioGroup.call(this, 'Overlay Type', 'overlayType', {none: 'none', ...Image.overlayTypes})}
113+
{renderSliderOption.call(this, 'Margin(margin-XX)', 'margin', {step: 4, min: 0, max: 40})}
114+
</>
115+
);
116+
}
46117

118+
renderSvgOptions() {
119+
const {isFile} = this.state;
120+
return (
121+
<>
122+
{renderBooleanOption.call(this, isFile ? 'Load from file' : 'Use xml const', 'isFile')}
123+
{renderRadioGroup.call(this, 'Size Type', 'sizeType', SizeType, {isRow: true})}
124+
</>
125+
);
126+
}
127+
128+
render() {
129+
const {showSvg} = this.state;
47130
return (
48131
<View flex>
49132
<View centerH height={250}>
50-
<Image
51-
source={{uri: showErrorImage ? BROKEN_URL : IMAGE_URL}}
52-
errorSource={Assets.images.demo.brokenImage}
53-
cover={cover}
54-
overlayType={overlayType !== 'none' ? overlayType : undefined}
55-
style={!cover && {width: DEFAULT_SIZE, height: DEFAULT_SIZE}}
56-
customOverlayContent={this.renderOverlayContent()}
57-
{...{[`margin-${margin}`]: true}}
58-
/>
133+
{showSvg ? this.renderSvgImage() : this.renderImage()}
59134
</View>
60-
<View height={2} bg-grey60/>
135+
<Text margin-20 text40>
136+
Image Screen
137+
</Text>
61138
<View useSafeArea flex>
62-
<View padding-20 bottom flex>
63-
<View flex>
64-
{renderBooleanOption.call(this, 'Show as Cover Image', 'cover')}
65-
{renderBooleanOption.call(this, 'Show Overlay Content', 'showOverlayContent')}
66-
{renderBooleanOption.call(this, 'Show Error Image', 'showErrorImage')}
67-
{renderRadioGroup.call(this, 'Overlay Type', 'overlayType', {none: 'none', ...Image.overlayTypes})}
68-
{renderSliderOption.call(this, 'Margin(margin-XX)', 'margin', {step: 4, min: 0, max: 40})}
139+
<ScrollView>
140+
<View padding-20 bottom flex>
141+
{renderBooleanOption.call(this, 'Show SVG', 'showSvg')}
142+
{showSvg ? this.renderSvgOptions() : this.renderOptions()}
69143
</View>
70-
<Text text40>Image Screen</Text>
71-
</View>
144+
</ScrollView>
72145
</View>
73146
</View>
74147
);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/// <reference types="react" />
2+
export interface SvgImageProps {
3+
data: any;
4+
}
5+
declare function SvgImage(props: SvgImageProps): JSX.Element | null;
6+
declare namespace SvgImage {
7+
var displayName: string;
8+
}
9+
export default SvgImage;

generatedTypes/components/image/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ declare type State = {
5858
* @description: Image wrapper with extra functionality like source transform and assets support
5959
* @extends: Image
6060
* @extendsLink: https://facebook.github.io/react-native/docs/image.html
61+
* @notes: please note that for SVG support you need to add both
62+
* `react-native-svg` and `react-native-svg-transformer`,
63+
* and also configure them (see `metro.config.js`)
6164
*/
6265
declare class Image extends PureComponent<Props, State> {
6366
static displayName: string;
@@ -81,6 +84,8 @@ declare class Image extends PureComponent<Props, State> {
8184
getVerifiedSource(source?: ImageSourcePropType): any;
8285
getImageSource(): any;
8386
onError: (event: NativeSyntheticEvent<ImageErrorEventData>) => void;
87+
renderSvg: () => JSX.Element;
88+
renderRegularImage(): JSX.Element;
8489
render(): JSX.Element;
8590
}
8691
export { Image };
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare let SvgPackage: any;
2+
export default SvgPackage;

generatedTypes/optionalDependencies/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { default as DateTimePickerPackage } from './DateTimePickerPackage';
22
export { default as BlurViewPackage } from './BlurViewPackage';
33
export { default as NetInfoPackage } from './NetInfoPackage';
44
export { PickerPackage, CommunityPickerPackage } from './PickerPackage';
5+
export { default as SvgPackage } from './SvgPackage';

metro.config.js

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
1+
const {getDefaultConfig} = require('metro-config');
2+
13
/**
24
* Metro configuration for React Native
35
* https://github.com/facebook/react-native
46
*
57
* @format
68
*/
7-
8-
module.exports = {
9-
transformer: {
10-
getTransformOptions: async () => ({
11-
transform: {
12-
experimentalImportSupport: false,
13-
inlineRequires: false,
14-
},
15-
}),
16-
},
17-
};
9+
module.exports = (async () => {
10+
const {
11+
resolver: {sourceExts, assetExts}
12+
} = await getDefaultConfig();
13+
return {
14+
transformer: {
15+
getTransformOptions: async () => ({
16+
transform: {
17+
experimentalImportSupport: false,
18+
inlineRequires: false
19+
}
20+
}),
21+
babelTransformerPath: require.resolve('react-native-svg-transformer')
22+
},
23+
resolver: {
24+
assetExts: assetExts.filter(ext => ext !== 'svg'),
25+
sourceExts: [...sourceExts, 'svg']
26+
}
27+
};
28+
})();

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@
100100
"react-native-gesture-handler": "1.9.0",
101101
"react-native-keyboard-tracking-view": "^5.6.1",
102102
"react-native-navigation": "7.6.0",
103+
"react-native-svg": "^12.1.0",
104+
"react-native-svg-transformer": "^0.14.3",
103105
"react-native-reanimated": "2.0.0",
104106
"react-test-renderer": "^16.13.1",
105107
"shell-utils": "^1.0.10",

src/components/image/SvgImage.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react';
2+
import {SvgPackage} from '../../optionalDependencies';
3+
const SvgXml = SvgPackage?.SvgXml;
4+
// const SvgProps = SvgPackage?.SvgProps; TODO: not sure how (or if) we can use their props
5+
6+
export interface SvgImageProps {
7+
data: any; // TODO: I thought this should be string | React.ReactNode but it doesn't work properly
8+
}
9+
10+
function SvgImage(props: SvgImageProps) {
11+
const {data, ...others} = props;
12+
13+
if (!SvgXml) {
14+
// eslint-disable-next-line max-len
15+
console.error(`RNUILib Image "svg" prop requires installing "react-native-svg" and "react-native-svg-transformer" dependencies`);
16+
return null;
17+
}
18+
19+
if (typeof data === 'string') {
20+
return <SvgXml xml={data} {...others}/>;
21+
} else if (data) {
22+
const File = data; // Must be with capital letter
23+
return <File {...others}/>;
24+
}
25+
26+
return null;
27+
}
28+
29+
SvgImage.displayName = 'IGNORE';
30+
31+
export default SvgImage;

src/components/image/index.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {asBaseComponent, ForwardRefInjectedProps, BaseComponentInjectedProps, Ma
1616
// @ts-ignore
1717
import Assets from '../../assets';
1818
import Overlay, {OverlayTypeType} from '../overlay';
19+
import SvgImage from './SvgImage';
1920

2021

2122
export type ImageProps = RNImageProps & MarginModifiers & {
@@ -77,6 +78,9 @@ type State = {
7778
* @description: Image wrapper with extra functionality like source transform and assets support
7879
* @extends: Image
7980
* @extendsLink: https://facebook.github.io/react-native/docs/image.html
81+
* @notes: please note that for SVG support you need to add both
82+
* `react-native-svg` and `react-native-svg-transformer`,
83+
* and also configure them (see `metro.config.js`)
8084
*/
8185
class Image extends PureComponent<Props, State> {
8286
static displayName = 'Image';
@@ -153,7 +157,12 @@ class Image extends PureComponent<Props, State> {
153157
}
154158
}
155159

156-
render() {
160+
renderSvg = () => {
161+
const {source, ...others} = this.props;
162+
return <SvgImage data={source} {...others}/>;
163+
}
164+
165+
renderRegularImage() {
157166
const {error} = this.state;
158167
const source = error ? this.getVerifiedSource(this.props.errorSource) : this.getImageSource();
159168
const {
@@ -196,6 +205,16 @@ class Image extends PureComponent<Props, State> {
196205
</ImageView>
197206
);
198207
}
208+
209+
render() {
210+
const {source} = this.props;
211+
const isSvg = typeof source === 'string' || typeof source === 'function';
212+
if (isSvg) {
213+
return this.renderSvg();
214+
} else {
215+
return this.renderRegularImage();
216+
}
217+
}
199218
}
200219

201220
const styles = StyleSheet.create({
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
let SvgPackage: any;
2+
try {
3+
SvgPackage = require('react-native-svg');
4+
// Sadly we cannot verify with require('react-native-svg-transformer');
5+
} catch (error) {}
6+
7+
export default SvgPackage;

src/optionalDependencies/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export {default as DateTimePickerPackage} from './DateTimePickerPackage';
22
export {default as BlurViewPackage} from './BlurViewPackage';
33
export {default as NetInfoPackage} from './NetInfoPackage';
44
export {PickerPackage, CommunityPickerPackage} from './PickerPackage';
5+
export {default as SvgPackage} from './SvgPackage';

0 commit comments

Comments
 (0)