Closed8

React NativeでMaterial SymbolsをIcon Componentとして使えるようにする

mrtrymrtry

やったこと

  • variable fontからfilled/outlinedのfontをつくる
  • codepointをglyphMapに変換
  • createIconSet() して、componentをつくる
mrtrymrtry

variable fontからfilled/outlinedのfontをつくる

これを参考にvariable fontでFILL軸をいじったフォントファイルをつくる
https://medium.com/timeless/adding-custom-variable-fonts-in-react-native-47e0d062bcfc

Sliceをインストールする
https://slice-gui.netlify.app/docs/install/#homebrew-installation

以下から好きなフォントをダウンロードする
https://github.com/google/material-design-icons/tree/master/variablefont

自分はMaterialSymbolsRoundedをダウンロードしてた
https://github.com/google/material-design-icons/blob/master/variablefont/MaterialSymbolsRounded[FILL%2CGRAD%2Copsz%2Cwght].ttf

ダウンロードしたfontをSliceにいれて、Fillの値をいじって、FilledとOutlinedのttfファイルをつくる


これでFilledとOutlinedのアイコンを出すためのフォントファイルが準備できた


memo

この工程、いらんかったかもしれん

記事書きながら見てたら、MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf とかいう outlinedぽいフォントあるやんけとなった
https://github.com/google/material-design-icons/tree/master/variablefont

後述する .codepoint のファイルも、同一っぽいから、この作業なくてよさそう

私はRoundedのアイコンを使いたかったから、MaterialSymbolsOutlinedとMaterialSymbolsRoundedだと見た目が違うはずなので、書いた手順が適切そうだった ☺️よかった

mrtrymrtry

codepointをglyphMapに変換

createIconSet() の引数に必要な glyphMap.codepoint から作る

You can also use createIconSet() directly in your project. This returns your own custom font based on the glyphMap where the key is the icon name and the value is either a UTF-8 character or it's character code.
https://docs.expo.dev/guides/icons/#createiconset

.codepoint は以下にあるもの
https://github.com/google/material-design-icons/tree/master/variablefont

これをChatGPTにぶちこんで変換した

https://github.com/google/material-design-icons/blob/master/variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.codepoints
のcodepointsファイルをjsonにしたいです

- 1行の内容をspaceで区切る
- 0番目をiconname、1番目をcodepointとします
- { [iconname]: [codepoint] } の形式で出力する
    - codepointは、UTG-8文字に変換してください

出力されたスクリプトはこんなかんじ

import json

uploaded_file_path_utf8 = "/mnt/data/MaterialSymbolsRounded.codepoints"
output_file_path_utf8 = "/mnt/data/MaterialSymbolsRounded_glyphMap.json"

glyph_map_utf8 = {}

with open(uploaded_file_path_utf8, "r") as file:
    for line in file:
        parts = line.split()  # 行をスペースで分割
        if len(parts) == 2:
            iconname, codepoint = parts
            utf8_char = chr(int(codepoint, 16))  # コードポイントを16進数として解析し、UTF-8文字に変換
            glyph_map_utf8[iconname] = utf8_char  # 辞書に追加

with open(output_file_path_utf8, "w") as json_file:
    json.dump(glyph_map_utf8, json_file, ensure_ascii=False, indent=2)

こんな感じのJSONが得られるはずで、これをglyphMapとして使う

mrtrymrtry

createIconSet() して、componentをつくる

あとは、最初に分割したfontとglyphMapを createIconSet() に食わせて、それを元にしてcomponentつくってexportすれば終わり
ほぼChatGPTにかかせた

import React, { FC, memo } from "react";
import createIconSet from "@expo/vector-icons/createIconSet";
import glyphMap from "@/assets/fonts/MaterialSymbolsRounded/GlyphMap.json";
import {
  IconButtonProps,
  IconProps,
} from "@expo/vector-icons/build/createIconSet";

// Filled and Outlined Icon definitions
const FilledIcon = createIconSet(
  glyphMap,
  "MaterialSymbolsRounded-Regular-Filled", // SliceのName Editorで指定した名前を入れる
  require("@/assets/fonts/MaterialSymbolsRounded/MaterialSymbolsRounded-Regular-Filled.ttf")
);
const OutlinedIcon = createIconSet(
  glyphMap,
  "MaterialSymbolsRounded-Regular-Outlined", // SliceのName Editorで指定した名前を入れる
  require("@/assets/fonts/MaterialSymbolsRounded/MaterialSymbolsRounded-Regular-Outlined.ttf")
);

// glyphMapは共通なので、suffixを指定して表示等を分岐させる
type FilledIconName = `${keyof typeof glyphMap}`;
type OutlinedIconName = `${keyof typeof glyphMap}-outlined`;

type FilledIconProps = IconProps<FilledIconName>;
type OutlinedIconProps = IconProps<OutlinedIconName>;

// Helper function to check if the icon is outlined
const isOutlinedIcon = (
  props: FilledIconProps | OutlinedIconProps
): props is OutlinedIconProps => {
  return props.name.endsWith("-outlined");
};

// MaterialSymbolsIcon definition
const MaterialSymbolsIconComponent: FC<FilledIconProps | OutlinedIconProps> = (
  props
) => {
  if (isOutlinedIcon(props)) {
    return (
      <OutlinedIcon
        {...props}
        name={props.name.replace("-outlined", "") as unknown as FilledIconName}
      />
    );
  } else {
    return <FilledIcon {...props} />;
  }
};

// Button props for Filled and Outlined Icons
type FilledIconButtonProps = IconButtonProps<FilledIconName>;
type OutlinedIconButtonProps = IconButtonProps<OutlinedIconName>;

// Helper function to check if the button is outlined
const isOutlinedIconButton = (
  props: FilledIconButtonProps | OutlinedIconButtonProps
): props is OutlinedIconButtonProps => {
  return props.name.endsWith("-outlined");
};

const MaterialSymbolsIconButton: FC<
  FilledIconButtonProps | OutlinedIconButtonProps
> = memo((props) => {
  if (isOutlinedIconButton(props)) {
    return (
      <OutlinedIcon.Button
        {...props}
        name={props.name.replace("-outlined", "") as unknown as FilledIconName}
      />
    );
  } else {
    return <FilledIcon.Button {...props} />;
  }
});

type MaterialSymbolsIconType = typeof MaterialSymbolsIconComponent & {
  Button: typeof MaterialSymbolsIconButton;
};

type MaterialSymbolsIconType = typeof MaterialSymbolsIconComponent & {
  Button: typeof MaterialSymbolsIconButton;
};

const MaterialSymbolsIcon = memo(
  MaterialSymbolsIconComponent
) as unknown as MaterialSymbolsIconType;
MaterialSymbolsIcon.Button = MaterialSymbolsIconButton;

export { MaterialSymbolsIcon };

mrtrymrtry

こんな感じで使えるはず

const Sample = () => {
  return (
    <>
      <MaterialSymbolsIcon
        size={24}
        color={"gray"}
        name={"more_vert"}
      />
      <MaterialSymbolsIcon.Button
        size={24}
        color={"gray"}
        name={"more_vert"}
      />
    </>
  )
}
このスクラップは2025/01/05にクローズされました