当ブログにアニメーション付きのハートのLikeボタンを追加しました。

累計をカウントしたり、誰が押したかをトラッキングする等はしていません。ただ押したら通知がいくという、筆者のモチベーションを上げるためだけのものです。

各記事の下部に配置しているため、よいと感じた場合にぜひ押してみてください👍

実装

アニメーションの実装を簡単に説明します。React Native Skia、React Native Reanimatedで実装しています。

ボタンの見た目

ボタンの形はシンプルなハートです。

Loading Skia..

Skiaでベジェ曲線を使って描画しています。3次ベジェ曲線については以前の記事react-native-skiaで三次ベジェ曲線を描くで解説しています。

Pathの子要素にShadowを配置し、ドロップシャドウを追加しています。

const path = usePathValue(p => {
  p.moveTo(props.x + centerX, centerY * 0.5 + props.y)
  // left
  p.cubicTo(
    centerX - props.size * 0.1 + props.x,
    centerY * 0.1 + props.y,
    centerX - props.size * 0.5 + props.x,
    centerY * 0.5 + props.y,
    centerX + props.x,
    centerY * 1.2 + props.y,
  )
  // right
  p.cubicTo(
    centerX + props.size * 0.5 + props.x,
    centerY * 0.5 + props.y,
    centerX + props.size * 0.1 + props.x,
    centerY * 0.1 + props.y,
    centerX + props.x,
    centerY * 0.5 + props.y,
  )
  p.close()
})

<Path path={path} style="fill">
  <Shadow dx={0} dy={3} blur={0} color="rgba(235, 150, 50, 0.3)" />
</Path>

パルスアニメーション

ボタンを押し待ちの時に軽くアピールするために、拡大縮小を小刻みに繰り返すアニメーションを設定します。

Loading Skia..

withSpringを使いscaleの値を変化させます。

const heartScale = useSharedValue(1)

heartScale.value = withSpring(
  1.175,
  {
    mass: 0.3,
    stiffness: 600,
    damping: 2,
    velocity: 0.3,
  }
)

各パラメータの意味は以下です。

  • mass: 質量。重いほど遅くなる。
  • stiffness: 硬さ。大きいほど速くなる。
  • damping: 減衰。大きいほど速くなる。
  • velocity: 速度。大きいほど速くなる。

プレス時のアニメーション

ボタンをプレスするとハートが少し小さくなり、離すとパルスした後にハートが消えていきます。

Loading Skia..
const heartScale = useSharedValue(1)
const heartOpacity = useSharedValue(1)
const handlePressIn = useCallback(() => {
  heartScale.value = withTiming(0.9, { duration: 300 })
}, [heartScale])

const handlePressOut = useCallback(() => {
  heartOpacity.value = withTiming(0, { duration: 2500 })
  heartScale.value = withSpring(
    1.3,
    {
      mass: 0.4,
      stiffness: 700,
      damping: 2.5,
      velocity: 0,
    }
  )
}, [heartScale])

パーティクルアニメーション

ハートが消えていくと同時に、パーティクル(小さな円)が散らばって消えます。

Loading Skia..

これは小さな円の<Particle />コンポーネントを用意し、各Particleにアニメーション進捗のSharedValueを渡します。各Particleでは中心からの角度と距離を元に、アニメーション進捗に応じて位置を変化させます。

const particleMoveProgress = useSharedValue(0)

const particleGroups = Array.from({ length: 6 }).map((_, index) => {
  const angle = (index * Math.PI * 2) / 6
  const distance = 28

  const particleAngleOffset = Math.PI / 15

  const particles = [
    {
      angleRadian: angle - particleAngleOffset / 2,
      distanceFromCenter: distance,
      color: colors[index % colors.length],
    },
    {
      angleRadian: angle + particleAngleOffset / 2,
      distanceFromCenter: distance,
      color: colors[(index + 1) % colors.length],
    },
  ]

  return { particles }
})


<Group>
  {particleGroups.map((group, groupIndex) => (
    <Group key={`group-${groupIndex}`}>
      {group.particles.map((particle, particleIndex) => (
        <Particle
          key={`particle-${groupIndex}-${particleIndex}`}
          color={particle.color}
          centerOrigin={center}
          initialPosition={particle}
          progress={particleMoveProgress}
        />
      ))}
    </Group>
  ))}
</Group>

終わりに

これらを組み合わせた完成形がこの下に配置しているハートボタンです!

今回の全コードはこちらを参照してください。

最終更新: 2025-05-01 02:19
筆者: @gaishimo 主にReact Nativeでのアプリ開発を専門に行っています。 React Nativeのお仕事お受けいたしますのでお気軽にご相談ください。
© 2025 Omoidasu, Inc. All rights reserved.