2022-08-30

react-native-skiaでSkeleton Screenを作成する

よくUIのローディング中の表示で使われるのがSkeleton Screenです。

Facebookなどで使われているローディングの名称を知ってる? / 調べたらたくさんありすぎた話 - omuriceman's blog

React Nativeで実装する場合 react-content-loaderreact-native-svg-animated-linear-gradient 等のライブラリを使う方法がありますが、これらは基本的にSVGを利用しています。今回はSkia(react-native-skia)を用いて同様のものを実装してみたいと思います。

前回の記事 でテキストにグラデーションを設定しグラデーションを変化させるアニメーションを実装しましたが、仕組み的には同じのため、同様の作りで実装してみたいと思います。

実装方法

グラデーションには<LinearGradient /> を使います。

<LinearGradient
  start={{ x: 0, y: 150 }}
  end={{ x: 300, y: 150 }}
  colors={[basicColor, accentColor, basicColor]}
/>

これをアニメーションで動的にグラデーションが移動するようにしていきます。実装の仕方は前回の記事 と同じですが、繰り返しアニメーションを指定したSkiaValueを用意し(progress)、この値に応じてuseComputedValue<LinearGradient />のポイント位置を動的に変化させます。

const progress = useTiming(
  { loop: true, yoyo: false },
  { easing: Easing.inOut(Easing.ease), duration: 2500 },
)

const positions = useComputedValue(() => {
  return [
    interpolate(progress.current, [0, 0.25, 0.5, 0.75, 1], [0, 0, 0, 0.5, 1]),
    interpolate(progress.current, [0, 0.25, 0.5, 0.75, 1], [0, 0, 0.5, 1, 1]),
    interpolate(progress.current, [0, 0.25, 0.5, 0.75, 1], [0, 0.5, 1, 1, 1]),
  ]
}, [progress])


<LinearGradient
  ...
  colors={color1, color2, color1]}
  positions={positions}
/>

<LinearGradient /><Paint>で囲み、Paintのrefを<Group>に指定します。こうすることで、<Group />配下すべてにグラデーションを適用できます。その下にprops.childrenを渡し、childrenを外側のcomponentから渡せるようにすることで、Skeleton Screenの中身を自由に外から指定できるようにします。

type Props = {
  basicColor: string
  accentColor: string
  canvasSize: { width: number; height: number }
  children: ReactNode
}

export function SkeletonViewContainer(props: Props) {
  const paintRef = usePaintRef()
  const progress = useTiming(
    { loop: true, yoyo: false },
    { easing: Easing.inOut(Easing.ease), duration: 2300 },
  )
  const positions = useComputedValue(() => {
    return [
      interpolate(progress.current, [0, 0.25, 0.5, 0.75, 1], [0, 0, 0, 0.5, 1]),
      interpolate(progress.current, [0, 0.25, 0.5, 0.75, 1], [0, 0, 0.5, 1, 1]),
      interpolate(progress.current, [0, 0.25, 0.5, 0.75, 1], [0, 0.5, 1, 1, 1]),
    ]
  }, [progress])
  return (
    <Canvas style={props.canvasSize}>
      <Paint ref={paintRef}>
        <LinearGradient
          start={{ x: 0, y: props.canvasSize.height % 2 }}
          end={{ x: props.canvasSize.width, y: props.canvasSize.height % 2 }}
          colors={[props.basicColor, props.accentColor, props.basicColor]}
          positions={positions}
        />
      </Paint>
      <Group paint={paintRef}>{props.children}</Group>
    </Canvas>
  )
}

外側からは以下のようにコンテンツの中身をchildrenに指定してrenderさせます。

<SkeletonViewContainer
  basicColor="rgb(245, 245, 245)"
  accentColor="rgb(225, 225, 225)"
  canvasSize={canvasSize}
>
  <RoundedRect x={0} y={0} r={8} width={70} height={70} />
  <RoundedRect x={80} y={17} r={8} width={250} height={13} />
  <RoundedRect x={80} y={40} r={8} width={250} height={10} />
  <RoundedRect x={0} y={80} r={8} width={330} height={10} />
  <RoundedRect x={0} y={100} r={8} width={200} height={10} />
  <RoundedRect x={0} y={120} r={8} width={330} height={10} />
</SkeletonViewContainer>

実際の表示サンプル

以下のようにSkeleton Screenが表示されます!

Loading Skia..

全体のコード

こちら で確認できます。

最終更新: 2022-08-31 11:00
筆者: @gaishimo 主にReact Nativeでのアプリ開発を行っています。
© 2021 Omoidasu, Inc. All rights reserved.