🌈

react-native-svg でグラデーションを表示

2021/12/08に公開

この記事は React Native Advent Calendar 2021 の8日目の記事です。

React Native でグラデーションを表示したい

React Native でグラデーションの方法を検索すると react-native-linear-gradient ライブラリがよく紹介されています。
使い勝手がよく便利なライブラリです。

https://github.com/react-native-linear-gradient/react-native-linear-gradient

ただ、同じ機能とまではいかなくてもグラデーション表示のみであれば、アイコン表示のためにすでにインストール済みであろう react-native-svg ライブラリ(個人的感想)を使用すれば表示できるのでは?と考え代替手段として作ってみたいと思います。

react-native-svg のインストール

以下のページを参考にインストールします。
https://github.com/react-native-svg/react-native-svg#installation

動作イメージ

今回このようなグラデーション背景のボタンを作りました。

https://snack.expo.dev/@tadaedo/gradient-example1

実装

Gradient.js
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 ・・・ グラデーションを縦方向にします。
App.js
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

グラデーションの色情報を定義します。
offset0% から 100% までを指定してグラデーションのバランスを調整します。
もしくは 0,1 で指定することも可能です。
今回は colors で渡された色の数分、パーセントを均等に設定されるようにしています。

Rect

四角形を描画します。
塗りつぶす色は fill で指定します。
今回は定義済みのグラデーションで塗りつぶすため fill="url(#gradient)" を指定します。

色のウェイトを指定できるように拡張

現在の実装だと全ての色が等間隔で表示されるため、影のような効果を表現したい場合同じ色を何度も追加する必要があるため、ウェイトを指定できるようにしたいと思います。

Gradient.js
 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 とサイズを合わせる必要がある。
App.js
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>

これで強調したいカラーを個別に指定できるようになりました。

https://snack.expo.dev/@tadaedo/gradient-example2

ハマったところ

iOS で動作確認していたのですが、Android でも動かしてみたところグラデーション表示が行われず、単色で表示されてしまいました。
原因を調査したところ LinearGradient の x2,y2vertical をそのまま渡していたのですが、Android ではちゃんと文字列型で渡さないと動作しないみたいでした。
(Expo Go クライアント、react-native アプリ(Hermes)でいずれも同じ結果でした)

以下のように修正。

Gradient.js
-   <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"}>

最終版

Gradient.js
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