2022-09-02

react-native-skiaでRipple Effectボタンを作成する

Material UIでも使われているRipple Effectのボタンをreact-native-skiaで作成してみます。

以下は早速ですがこの記事の成果物です。タップ(クリック)すると波紋が拡がります。

Loading Skia..

タップ位置を取得して円を描画

仕組みとしてはボタン上でタップした座標を取得し、そこを中心として円を描画し拡大していけばよさそうです。

Canvas内でタップした座標はuseTouchHandlerで取得できます。取得した座標をSkiaValueに保持しその座標を中心に円を描画してみます。

const cx = useValue<number>(canvasSize.width / 2)
const cy = useValue<number>(canvasSize.height / 2 + buttonSize.height / 3)

const touchHandler = useTouchHandler({
  onStart: ({ x, y }) => {
    cx.current = x
    cy.current = y
  },
})

return (
  <Canvas style={[canvasSize]} onTouch={touchHandler}>
    <RoundedRect
      {...buttonSize}
      {...buttonPos}
      r={buttonRadius}
      color="lightblue"
      style="fill"
    />
    <Circle
      cx={cx || 0}
      cy={cy || 0}
      r={30}
      opacity={0.2}
      color={rippleColor}
    />
  </Canvas>
)

すると以下のようにタップで円が表示されますが、隅をタップした時にボタンの領域をはみ出して表示されてしまいます。

Loading Skia..

ボタンの領域内にしか表示されないようにするには対象を<Group>で囲みclipします。

Group: Clip Rounded Rectangle | React Native Skia

<Group
  clip={rrect(
    { ...buttonPos, ...buttonSize },
    buttonRadius,
    buttonRadius,
  )}
>
  ...
</Group>

これでボタンの外側には描画されなくなります。

Loading Skia..

波紋が拡がるアニメーションを作成

次は波紋の円が拡がっていくアニメーションを作成します。タップ後に半径のサイズをアニメーションで大きくしていきます。

const rippleProgress = useValue<number>(0)

const touchHandler = useTouchHandler({
  onStart: ({ x, y }) => {
    ...
    cy.current = y
    rippleProgress.animation?.cancel()
    rippleProgress.current = 0
    runTiming(
      rippleProgress,
      1,
      { duration: 1000 },
      () => {
        setTimeout(() => {
          rippleProgress.current = 0
        }, 0)
      },
    )
  },
})
const rippleRadius = useComputedValue(() => {
  return interpolate(rippleProgress.current, [0, 1], [0, 300])
}, [rippleProgress])


<Circle
  cx={cx}
  cy={cy}
  r={rippleRadius}
  opacity={0.2}
  color={rippleColor}
/>

これでタップすると波紋が拡がっていくようになりました。

Loading Skia..

easing関数の変更

より自然なアニメーションになるようにイージング関数を変更してみます。skiaのrunTiming(またはuseTiming)のオプションでeasing関数を指定できます(何も指定していない場合はlinearが使われます)。

runTiming(
  rippleProgress,
  1,
  { duration: 1000, easing: Easing.out(Easing.quad) }
)

以下はeaseOutQuadを指定した場合です。

Loading Skia..

以下はeaseInOutQuadを指定した場合です。

Loading Skia..

Linearを指定するよりも大分自然な感じになったと思います。微妙な違いですが、easeInOutを指定した時の方が、easeOutより出だしが少し緩慢な印象です。この辺りはアニメーションの長さによっても印象が変わりそうです。

Easing関数の種類については以下が参考になります。

Easing Functions Cheat Sheet

全体のコード

こちら で確認できます。

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