react-native-svg でグラデーションを表示
この記事は React Native Advent Calendar 2021 の8日目の記事です。
React Native でグラデーションを表示したい
React Native でグラデーションの方法を検索すると react-native-linear-gradient ライブラリがよく紹介されています。
使い勝手がよく便利なライブラリです。
ただ、同じ機能とまではいかなくてもグラデーション表示のみであれば、アイコン表示のためにすでにインストール済みであろう react-native-svg ライブラリ(個人的感想)を使用すれば表示できるのでは?と考え代替手段として作ってみたいと思います。
react-native-svg のインストール
以下のページを参考にインストールします。
動作イメージ
今回このようなグラデーション背景のボタンを作りました。
実装
import React from 'react';
import { View } from 'react-native';
import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
const Gradient = (props) => {
const { style, colors, vertical, ...otherProps } = props;
return (
<View style={[{ overflow: 'hidden' }, style]} {...otherProps}>
<Svg width="100%" height="100%">
<Defs>
<LinearGradient id="gradient" x1="0" y1="0" x2={vertical ? "0" : "1"} y2={vertical ? "1" : "0"}>
{colors.map((color, index) => (
<Stop key={index} offset={100 / (colors.length - 1) * index + "%"} stopColor={color} stopOpacity="1" />
))}
</LinearGradient>
</Defs>
<Rect width="100%" height="100%" fill="url(#gradient)" />
</Svg>
<View style={{ position: 'absolute', width: "100%", height: "100%" }}>
{props.children}
</View>
</View>
);
}
export default Gradient;
使い方
-
colors
・・・ 色の配列です。 -
vertical
・・・ グラデーションを縦方向にします。
import React from 'react';
import { SafeAreaView, StyleSheet, Text } from 'react-native';
import Gradient from './Gradient';
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Gradient
style={styles.button}
colors={['#6495ed', '#3b5998', '#192f6a']}>
<Text style={styles.buttonText}>Submit</Text>
</Gradient>
<Gradient
style={styles.button}
colors={['rgb(0, 191, 255)', 'rgb(135, 206, 235)', 'rgb(30, 144, 255)']}
vertical>
<Text style={styles.buttonText}>Login</Text>
</Gradient>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
justifyContent: 'center',
alignItems: 'center',
},
button: {
width: 300,
height: 70,
borderRadius: 12,
marginTop: 24,
},
buttonText: {
lineHeight: 70,
color: 'white',
fontSize: 34,
fontWeight: 'bold',
textAlign: 'center',
}
});
使用したタグについて
Svg
SVG の表示領域です。
Defs
SVG の中で使い回す図形や装飾を定義します。
他のタグから参照する場合は id
を指定して使用します。
LinearGradient
グラデーションを定義します。LinearGradient は Defs の中に書く必要があります。
x1,y1,x2,y2
でグラデーションの方向を決定します。
Stop
グラデーションの色情報を定義します。
offset
は 0%
から 100%
までを指定してグラデーションのバランスを調整します。
もしくは 0,1
で指定することも可能です。
今回は colors
で渡された色の数分、パーセントを均等に設定されるようにしています。
Rect
四角形を描画します。
塗りつぶす色は fill
で指定します。
今回は定義済みのグラデーションで塗りつぶすため fill="url(#gradient)"
を指定します。
色のウェイトを指定できるように拡張
現在の実装だと全ての色が等間隔で表示されるため、影のような効果を表現したい場合同じ色を何度も追加する必要があるため、ウェイトを指定できるようにしたいと思います。
const Gradient = (props) => {
- const { style, colors, vertical, ...otherProps } = props;
+ const { style, colors, vertical, weight, ...otherProps } = props;
+
+ let stops = colors;
+ if (weight && weight.length === colors.length) {
+ stops = weight.map((val, index) => new Array(val).fill(colors[index])).flat();
+ }
return (
<View style={[{ overflow: 'hidden' }, style]} {...otherProps}>
<Svg width="100%" height="100%">
<Defs>
<LinearGradient id="gradient" x1="0" y1="0" x2={!vertical} y2={vertical}>
- {colors.map((color, index) => (
- <Stop key={index} offset={100 / (colors.length - 1) * index + "%"} stopColor={color} stopOpacity="1" />
+ {stops.map((color, index) => (
+ <Stop key={index} offset={100 / (stops.length - 1) * index + "%"} stopColor={color} stopOpacity="1" />
))}
</LinearGradient>
</Defs>
Stop の offset
の計算方法を変更したくなかったため weight
で指定された数分 colors
をコピーします。
使い方2
-
weight
・・・ 色のウェイトを指定。colors
とサイズを合わせる必要がある。
export default function App() {
<SafeAreaView style={styles.container}>
<Gradient
style={styles.button}
- colors={['#4c669f', '#3b5998', '#192f6a']}>
+ colors={['#4c669f', '#3b5998', '#192f6a']}
+ weight={[6, 1, 1]}>
<Text style={styles.buttonText}>Submit</Text>
</Gradient>
<Gradient
style={styles.button}
colors={['rgb(0, 191, 255)', 'rgb(135, 206, 235)', 'rgb(30, 144, 255)']}
+ weight={[2, 5, 3]}
vertical>
<Text style={styles.buttonText}>Login</Text>
</Gradient>
これで強調したいカラーを個別に指定できるようになりました。
ハマったところ
iOS で動作確認していたのですが、Android でも動かしてみたところグラデーション表示が行われず、単色で表示されてしまいました。
原因を調査したところ LinearGradient の x2,y2
に vertical
をそのまま渡していたのですが、Android ではちゃんと文字列型で渡さないと動作しないみたいでした。
(Expo Go クライアント、react-native アプリ(Hermes)でいずれも同じ結果でした)
以下のように修正。
- <LinearGradient id="gradient" x1="0" y1="0" x2={!vertical} y2={vertical}>
+ <LinearGradient id="gradient" x1="0" y1="0" x2={vertical ? "0" : "1"} y2={vertical ? "1" : "0"}>
最終版
import React from 'react';
import { View } from 'react-native';
import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
const Gradient = (props) => {
const { style, colors, vertical, weight, ...otherProps } = props;
let stops = colors;
if (weight && weight.length === colors.length) {
stops = weight.map((val, index) => new Array(val).fill(colors[index])).flat();
}
return (
<View style={[{ overflow: 'hidden' }, style]} {...otherProps}>
<Svg width="100%" height="100%">
<Defs>
<LinearGradient id="gradient" x1="0" y1="0" x2={vertical ? "0" : "1"} y2={vertical ? "1" : "0"}>
{stops.map((color, index) => (
<Stop key={index} offset={100 / (stops.length - 1) * index + "%"} stopColor={color} stopOpacity="1" />
))}
</LinearGradient>
</Defs>
<Rect width="100%" height="100%" fill="url(#gradient)" />
</Svg>
<View style={{ position: 'absolute', width: "100%", height: "100%" }}>
{props.children}
</View>
</View>
);
}
export default Gradient;
Discussion