概要

0:03:00

ホリデーセール向けのアニメーション付きクリスマスペイウォールを構築するこのコードラボへようこそ。

このコードラボでは、Jetpack Compose を使って、目を引く華やかなペイウォール画面の作り方を学びます。作成するペイウォールには、複数種類の雪片を持つリアルな降雪パーティクルシステム、グロー効果のある浮遊クリスマスオーナメント、点滅するクリスマスライトの列、跳ねるギフトボックスのアニメーション、プレミアム機能を際立たせる光る枠線のカード、そして揺れるキャンディケインの装飾が含まれます。

このコードラボを終えるころには、美しく目を引くペイウォールが完成し、ホリデーシーズンのコンバージョン向上に役立ちます。

Christmas Paywall Preview

前提条件

このコードラボを始める前に、Kotlin と Jetpack Compose の基礎知識があると望ましいです。Compose に対応した Android Studio または IntelliJ IDEA をインストールしておいてください。各アニメーション手法はひとつずつ詳しく説明するので、Compose のアニメーションに詳しくなくても構いませんが、知っていると理解がスムーズです。

作成するもの

複数のアニメーション要素が連動して、華やかでプレミアムな雰囲気を生み出すクリスマステーマの完成されたペイウォール画面です。ホリデーシーズンにユーザーの購読を後押しします。

プロジェクトのセットアップ

0:05:00

まずは、クリスマスペイウォールの基本構造を用意しましょう。はじめに、ChristmasPaywallScreen.kt という新しい Kotlin ファイルを作成します。

必要なインポート

アニメーション、Canvas 描画、Compose UI コンポーネントに必要なインポートをすべて追加します。

kotlin
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.*
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlin.math.*
import kotlin.random.Random

データクラスを定義する

雪片を表すデータクラスが必要です。各雪片は、位置、速度、サイズ、回転のプロパティを持ちます。

kotlin
data class Snowflake(
  var x: Float,
  var y: Float,
  var vx: Float,
  var vy: Float,
  var size: Float,
  var alpha: Float,
  var rotation: Float,
  var rotationSpeed: Float,
  var type: SnowflakeType,
)

enum class SnowflakeType {
  SMALL,
  MEDIUM,
  LARGE,
  SPARKLE,
}

SnowflakeType enum を使うと、サイズの違いや特別なきらめき効果によって、降雪に見た目の変化を持たせられます。

降雪パーティクルシステム

0:10:00

降雪システムは、このクリスマスの雰囲気の心臓部です。ここでは、雪片の生成、更新、描画を管理するパーティクルシステムを作成します。パーティクルシステムは、パーティクル(雪片)のコレクションを保持し、フレームごとに各パーティクルの位置を更新し、画面外に出たら取り除くという仕組みで動きます。

SnowfallSystem クラス

このクラスは、すべての雪片のライフサイクルを扱います。動作を順に見ていきましょう。

kotlin
class SnowfallSystem {
  private val snowflakes = mutableListOf<Snowflake>()
  private val random = Random
  private val maxSnowflakes = 100

  fun update(width: Float, height: Float, deltaTime: Float) {
    // Spawn new snowflakes
    if (snowflakes.size < maxSnowflakes && random.nextFloat() < 0.3f) {
      spawnSnowflake(width)
    }

    // Update existing snowflakes
    val iterator = snowflakes.iterator()
    while (iterator.hasNext()) {
      val snowflake = iterator.next()

      // Apply wind sway using sine wave
      snowflake.vx = sin(snowflake.y * 0.01f + snowflake.x * 0.005f) * 30f
      snowflake.x += snowflake.vx * deltaTime
      snowflake.y += snowflake.vy * deltaTime
      snowflake.rotation += snowflake.rotationSpeed * deltaTime

      // Remove if out of bounds
      if (snowflake.y > height + 20f ||
          snowflake.x < -20f ||
          snowflake.x > width + 20f) {
        iterator.remove()
      }
    }
  }
}

ステップ 1: データ構造を定義する。 snowflakes リストには、アクティブな雪片がすべて格納されます。パフォーマンスを保つため、最大数を 100 に制限します。

ステップ 2: 新しい雪片を生成する。 更新のたびに、雪片を追加する余地があるか確認します。random.nextFloat() < 0.3f という条件は、フレームごとに 30% の確率で新しい雪片を生成することを意味します。これにより、雪片がいっせいに現れるのではなく、自然に少しずつ降り積もる効果が生まれます。

ステップ 3: 風による揺れを加える。 snowflake.vx = sin(snowflake.y * 0.01f + snowflake.x * 0.005f) * 30f の行が、緩やかな揺れの動きを生み出します。sin 関数は -1 から 1 の値を返すので、これに 30 を掛けて水平方向の動きにします。yx の両方の位置を入力に使うことで、均一でない多様な揺れのパターンが得られます。

ステップ 4: 位置を更新する。 速度に deltaTime を掛けて、フレームレートに依存しない動きにします。これにより、端末が 30fps で動いても 120fps で動いても、降雪は同じように見えます。

ステップ 5: 画面外の雪片を取り除く。 雪片が画面の境界(20 ピクセルの余裕を持たせています)を越えたら、リストから取り除きます。こうしてメモリを解放し、新しい雪片を生成できるようにします。

雪片を生成する

各雪片は、その種類に応じたランダムなプロパティで生成されます。これにより、降雪に見た目の変化が生まれます。

kotlin
private fun spawnSnowflake(width: Float) {
  val type = when {
    random.nextFloat() < 0.1f -> SnowflakeType.SPARKLE
    random.nextFloat() < 0.3f -> SnowflakeType.LARGE
    random.nextFloat() < 0.6f -> SnowflakeType.MEDIUM
    else -> SnowflakeType.SMALL
  }

  val size = when (type) {
    SnowflakeType.SMALL -> random.nextFloat() * 3f + 2f
    SnowflakeType.MEDIUM -> random.nextFloat() * 5f + 4f
    SnowflakeType.LARGE -> random.nextFloat() * 8f + 6f
    SnowflakeType.SPARKLE -> random.nextFloat() * 6f + 4f
  }

  snowflakes.add(
    Snowflake(
      x = random.nextFloat() * width,
      y = -20f,
      vx = 0f,
      vy = random.nextFloat() * 80f + 40f,
      size = size,
      alpha = random.nextFloat() * 0.5f + 0.5f,
      rotation = random.nextFloat() * 360f,
      rotationSpeed = random.nextFloat() * 100f - 50f,
      type = type,
    ),
  )
}

雪の結晶を描画する

魔法が起こるのは、この描画関数です。6 本の腕を持つ雪の結晶を、枝付きで描きます。

kotlin
private fun drawSnowflakeCrystal(
  drawScope: DrawScope,
  snowflake: Snowflake
) {
  val arms = 6
  val armLength = snowflake.size

  for (i in 0 until arms) {
    val angle = (i * 60f + snowflake.rotation) * PI.toFloat() / 180f
    val endX = cos(angle) * armLength
    val endY = sin(angle) * armLength

    // Draw main arm
    drawScope.drawLine(
      color = Color.White.copy(alpha = snowflake.alpha),
      start = Offset.Zero,
      end = Offset(endX, endY),
      strokeWidth = 1.5f,
      cap = StrokeCap.Round,
    )

    // Draw branches for larger snowflakes
    if (snowflake.type != SnowflakeType.SMALL) {
      val branchLength = armLength * 0.4f
      val branchStart = armLength * 0.5f
      for (side in listOf(-1, 1)) {
        val branchAngle = angle + side * 45f * PI.toFloat() / 180f
        val startX = cos(angle) * branchStart
        val startY = sin(angle) * branchStart
        val branchEndX = startX + cos(branchAngle) * branchLength
        val branchEndY = startY + sin(branchAngle) * branchLength

        drawScope.drawLine(
          color = Color.White.copy(alpha = snowflake.alpha * 0.8f),
          start = Offset(startX, startY),
          end = Offset(branchEndX, branchEndY),
          strokeWidth = 1f,
          cap = StrokeCap.Round,
        )
      }
    }
  }
}

浮遊するオーナメント

0:07:00

浮遊するオーナメントは、背景に奥行きと華やかな彩りを加えます。各オーナメントは、グロー効果とともに緩やかに上下に揺れます。

Ornament データクラス

kotlin
data class FloatingOrnament(
  var x: Float,
  var y: Float,
  var baseY: Float,
  var size: Float,
  var color: Color,
  var glowColor: Color,
  var phase: Float,
  var speed: Float,
)

OrnamentSystem クラス

このシステムは、背景に浮かぶ色とりどりのクリスマスオーナメントを管理します。

kotlin
class OrnamentSystem {
  private val ornaments = mutableListOf<FloatingOrnament>()
  private val random = Random

  // Festive color palette
  private val ornamentColors = listOf(
    Color(0xFFE53935) to Color(0xFFFF6F60), // Red
    Color(0xFFC62828) to Color(0xFFFF5252), // Dark Red
    Color(0xFFFFD700) to Color(0xFFFFE57F), // Gold
    Color(0xFF2E7D32) to Color(0xFF60AD5E), // Green
    Color(0xFF1565C0) to Color(0xFF5E92F3), // Blue
    Color(0xFF6A1B9A) to Color(0xFF9C4DCC), // Purple
  )

  fun initialize(width: Float, height: Float, count: Int = 15) {
    ornaments.clear()
    for (i in 0 until count) {
      val colorPair = ornamentColors[random.nextInt(ornamentColors.size)]
      val y = random.nextFloat() * height
      ornaments.add(
        FloatingOrnament(
          x = random.nextFloat() * width,
          y = y,
          baseY = y,
          size = random.nextFloat() * 15f + 10f,
          color = colorPair.first,
          glowColor = colorPair.second,
          phase = random.nextFloat() * 2f * PI.toFloat(),
          speed = random.nextFloat() * 2f + 1f,
        ),
      )
    }
  }

  fun update(time: Float) {
    for (ornament in ornaments) {
      // Gentle bobbing motion
      ornament.y = ornament.baseY + sin(time * ornament.speed + ornament.phase) * 20f
    }
  }
}

上下に揺れる動きの仕組み: ornament.y = ornament.baseY + sin(time * ornament.speed + ornament.phase) * 20f の行が、緩やかな上下運動を生み出します。もとの baseY の位置を保持しておき、そこに sin 波のオフセットを加えます。phase パラメータによって、各オーナメントは波のサイクルの異なる時点から動き始めるため、すべてが同期して揺れることはありません。speed の倍率により、一部のオーナメントは他より速く動き、見た目に変化が加わります。

グロー付きでオーナメントを描画する

各オーナメントは、リアルなグロー効果を出すために複数のレイヤーで描画します。この手法は「レイヤードグロー」と呼ばれ、奥行きを生み出します。

kotlin
fun draw(drawScope: DrawScope) {
  for (ornament in ornaments) {
    // Outer glow
    drawScope.drawCircle(
      color = ornament.glowColor.copy(alpha = 0.2f),
      radius = ornament.size * 2f,
      center = Offset(ornament.x, ornament.y),
      blendMode = BlendMode.Plus,
    )
    // Mid glow
    drawScope.drawCircle(
      color = ornament.glowColor.copy(alpha = 0.4f),
      radius = ornament.size * 1.3f,
      center = Offset(ornament.x, ornament.y),
      blendMode = BlendMode.Plus,
    )
    // Core ornament
    drawScope.drawCircle(
      color = ornament.color,
      radius = ornament.size,
      center = Offset(ornament.x, ornament.y),
    )
    // Highlight reflection
    drawScope.drawCircle(
      color = Color.White.copy(alpha = 0.6f),
      radius = ornament.size * 0.3f,
      center = Offset(
        ornament.x - ornament.size * 0.3f,
        ornament.y - ornament.size * 0.3f,
      ),
    )
  }
}

ステップ 1: 外側のグローを描く。 オーナメントの 2 倍の大きさで、不透明度 20% の透明な大きい円を描きます。ここで BlendMode.Plus が重要です。色の値を加算するため、単に色を重ねるのではなく、発光するようなグロー効果が生まれます。

ステップ 2: 中間のグローを描く。 やや小さい円(オーナメントの 1.3 倍)を不透明度 40% で描くと、グローに強さが加わり、中心からグラデーションのように減衰する効果が出ます。

ステップ 3: オーナメント本体を描く。 これは実際のサイズで描かれる、完全に不透明な本体です。グローのレイヤーの上に重なります。

ステップ 4: ハイライトの反射を加える。 左上にずらした小さな白い円が、リアルな光の反射を作り出し、オーナメントを艶やかで立体的に見せます。ずらす位置によって、左上から光が当たっている様子を再現します。

クリスマスライトと装飾

0:07:00

次に、アニメーション付きのクリスマスライトと装飾を加えて、ペイウォールを本格的に華やかにします。これらの要素は、Compose の rememberInfiniteTransition API を使い、無限に繰り返すなめらかなアニメーションを作ります。

クリスマスライトの列

色とりどりのライトが並び、少しずつずれたアニメーションで点滅します。ここでのポイントは、delayMillis を使って、各ライトが前のライトよりわずかに遅れてアニメーションを開始する連鎖的な効果を作ることです。

kotlin
@Composable
fun ChristmasLightsString(modifier: Modifier = Modifier) {
  val infiniteTransition = rememberInfiniteTransition(label = "lights")

  val lightColors = listOf(
    Color(0xFFFF1744), // Red
    Color(0xFF00E676), // Green
    Color(0xFFFFD600), // Yellow
    Color(0xFF2979FF), // Blue
    Color(0xFFFF9100), // Orange
  )

  Row(
    modifier = modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceEvenly,
  ) {
    lightColors.forEachIndexed { index, color ->
      val alpha by infiniteTransition.animateFloat(
        initialValue = 0.4f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
          animation = tween(
            durationMillis = 800,
            easing = LinearEasing,
            delayMillis = index * 150, // Staggered delay
          ),
          repeatMode = RepeatMode.Reverse,
        ),
        label = "light_$index",
      )

      val scale by infiniteTransition.animateFloat(
        initialValue = 0.8f,
        targetValue = 1.2f,
        animationSpec = infiniteRepeatable(
          animation = tween(
            durationMillis = 800,
            easing = FastOutSlowInEasing,
            delayMillis = index * 150,
          ),
          repeatMode = RepeatMode.Reverse,
        ),
        label = "scale_$index",
      )

      // Glow effect
      Box(
        modifier = Modifier
          .size(16.dp)
          .scale(scale)
          .background(color.copy(alpha = alpha), CircleShape)
          .blur(4.dp),
      )
      // Core light
      Box(
        modifier = Modifier
          .size(12.dp)
          .scale(scale)
          .offset(x = (-14).dp)
          .background(color.copy(alpha = alpha), CircleShape),
      )
    }
  }
}

ずらしたアニメーションの仕組み: 美しい連鎖効果を生み出しているのが delayMillis = index * 150 です。1 番目のライト(index 0)はすぐに開始し、2 番目のライト(index 1)は 150ms 後、3 番目は 300ms 後、というように続きます。RepeatMode.Reverse と組み合わせることで、ライトが列を追いかけ合うように見えます。

2 層のグロー手法: 各電球は 2 つの Box で構成されます。1 つ目は、ライトの周囲にグローの輪を作る、より大きくぼかした円です。2 つ目は、実際の電球を表す、より小さく鮮明なコアです。グローのレイヤーに掛けた blur(4.dp) 修飾子が、やわらかな発光効果を生み出します。

アニメーションするギフトボックス

跳ねてゆらゆら動く、目を引くギフトボックスです。上下の跳ねと回転という 2 つのアニメーションを同時に使い、遊び心のある注目を集める効果を作ります。

kotlin
@Composable
fun AnimatedGiftBox(modifier: Modifier = Modifier) {
  val infiniteTransition = rememberInfiniteTransition(label = "gift")

  val bounce by infiniteTransition.animateFloat(
    initialValue = 0f,
    targetValue = -10f,
    animationSpec = infiniteRepeatable(
      animation = tween(500, easing = FastOutSlowInEasing),
      repeatMode = RepeatMode.Reverse,
    ),
    label = "bounce",
  )

  val rotate by infiniteTransition.animateFloat(
    initialValue = -5f,
    targetValue = 5f,
    animationSpec = infiniteRepeatable(
      animation = tween(1000, easing = FastOutSlowInEasing),
      repeatMode = RepeatMode.Reverse,
    ),
    label = "rotate",
  )

  Box(
    modifier = modifier
      .offset(y = bounce.dp)
      .rotate(rotate),
    contentAlignment = Alignment.Center,
  ) {
    Text(text = "🎁", fontSize = 48.sp)
  }
}

跳ねるアニメーションの仕組み: 跳ねのアニメーションは 0 から -10 へ動きます(Compose の座標系では、Y のマイナスは上方向を意味します)。FastOutSlowInEasing によって、開始時は素早く加速し、頂点に近づくにつれて減速するので、実際の物理法則を思わせる動きになります。RepeatMode.Reverse により、なめらかに開始位置へ戻ります。

複数のアニメーションを組み合わせる: 同じ Box に offset(y = bounce.dp)rotate(rotate) の両方を適用し、上下の動きと回転を組み合わせます。継続時間が異なる(跳ねは 500ms、回転は 1000ms)ことで、単調に繰り返さない面白いパターンが生まれ、アニメーションに目を引きつけ続けます。

揺れるキャンディケイン

振り子のように左右に揺れるキャンディケインの装飾です。これはよりシンプルで、回転だけを使うアニメーションです。

kotlin
@Composable
fun CandyCaneDecoration(modifier: Modifier = Modifier) {
  val infiniteTransition = rememberInfiniteTransition(label = "candy")

  val swing by infiniteTransition.animateFloat(
    initialValue = -15f,
    targetValue = 15f,
    animationSpec = infiniteRepeatable(
      animation = tween(2000, easing = FastOutSlowInEasing),
      repeatMode = RepeatMode.Reverse,
    ),
    label = "swing",
  )

  Text(
    text = "🍬",
    fontSize = 28.sp,
    modifier = modifier.rotate(swing),
  )
}

揺れる動きの仕組み: 回転は -15 度から +15 度の間でアニメーションし、30 度の緩やかな弧を描きます。2000ms という継続時間により、そよ風に揺れる飾りのように、ゆったりとした印象になります。FastOutSlowInEasing によって、揺れの両端で動きが減速し、本物の振り子のような動きになります。

光るフィーチャーカード

0:05:00

プレミアム機能のカードは、しっかり目立たせる必要があります! ここでは、クリスマスカラーのアニメーションする光る枠線を持つカードを作成します。

GlowingBorderCard コンポーザブル

kotlin
@Composable
fun GlowingBorderCard(
  modifier: Modifier = Modifier,
  content: @Composable () -> Unit
) {
  val infiniteTransition = rememberInfiniteTransition(label = "glow")

  val glowAlpha by infiniteTransition.animateFloat(
    initialValue = 0.3f,
    targetValue = 0.8f,
    animationSpec = infiniteRepeatable(
      animation = tween(2000, easing = FastOutSlowInEasing),
      repeatMode = RepeatMode.Reverse,
    ),
    label = "glowAlpha",
  )

  Box(modifier = modifier) {
    // Glow effect layer
    Box(
      modifier = Modifier
        .matchParentSize()
        .blur(20.dp)
        .background(
          brush = Brush.verticalGradient(
            colors = listOf(
              Color(0xFFE53935).copy(alpha = glowAlpha), // Red
              Color(0xFF2E7D32).copy(alpha = glowAlpha), // Green
            ),
          ),
          shape = RoundedCornerShape(24.dp),
        ),
    )

    // Card content
    Box(
      modifier = Modifier
        .matchParentSize()
        .clip(RoundedCornerShape(24.dp))
        .background(
          brush = Brush.verticalGradient(
            colors = listOf(
              Color(0xFF1A1A2E),
              Color(0xFF16213E),
            ),
          ),
        )
        .border(
          width = 2.dp,
          brush = Brush.verticalGradient(
            colors = listOf(
              Color(0xFFE53935).copy(alpha = 0.6f),
              Color(0xFF2E7D32).copy(alpha = 0.6f),
            ),
          ),
          shape = RoundedCornerShape(24.dp),
        )
        .padding(24.dp),
    ) {
      content()
    }
  }
}

アニメーションするグローの仕組み: glowAlpha の値は、2 秒かけて 0.3 から 0.8 の間でアニメーションします。この値が、外側のグローレイヤーとグラデーションカラーの両方の不透明度を制御し、脈打つように呼吸する効果を生み出します。

2 層構成の手法: 1 つ目の Box は、グラデーションの背景に blur(20.dp) を適用してグロー効果を作ります。これにより、カードの周囲にやわらかく拡散した光が生まれます。2 つ目の Box には、半透明の枠線を持つ実際のカードのコンテンツが入ります。枠線にはグローと同じ赤から緑へのグラデーションを使い、視覚的なまとまりを持たせます。

matchParentSize を使う理由: 内側の 2 つの Box は、fillMaxSize() ではなく matchParentSize() を使っています。これは重要です。matchParentSize は親の計測に影響を与えないので、親の Box はコンテンツに応じて自身のサイズを決められ、グローレイヤーは決定されたサイズにぴったり合わせられます。

プレミアム機能の項目

機能の項目を表示する、シンプルな行のコンポーネントです。

kotlin
@Composable
fun PremiumFeatureItem(
  emoji: String,
  title: String,
  modifier: Modifier = Modifier
) {
  Row(
    modifier = modifier
      .fillMaxWidth()
      .padding(vertical = 6.dp),
    verticalAlignment = Alignment.CenterVertically,
  ) {
    Text(text = emoji, fontSize = 20.sp)
    Spacer(modifier = Modifier.width(12.dp))
    Text(
      text = title,
      style = TextStyle(
        color = Color.White,
        fontSize = 14.sp,
        fontWeight = FontWeight.Medium,
      ),
    )
  }
}

完成したペイウォール画面

0:08:00

いよいよ、すべてを組み合わせて完成したペイウォール画面に仕上げましょう!

SnowfallBackground コンポーザブル

まず、降雪とオーナメントを組み合わせたアニメーション背景を作成します。

kotlin
@Composable
fun SnowfallBackground(modifier: Modifier = Modifier) {
  val snowfallSystem = remember { SnowfallSystem() }
  val ornamentSystem = remember { OrnamentSystem() }
  var initialized by remember { mutableStateOf(false) }
  var lastFrameTimeNanos by remember { mutableLongStateOf(0L) }
  var totalTime by remember { mutableFloatStateOf(0f) }
  var canvasSize by remember { mutableStateOf(Size.Zero) }

  LaunchedEffect(Unit) {
    while (true) {
      withFrameNanos { frameTimeNanos ->
        val deltaTime = if (lastFrameTimeNanos == 0L) {
          0.016f
        } else {
          ((frameTimeNanos - lastFrameTimeNanos) / 1_000_000_000f).coerceIn(0f, 0.1f)
        }
        lastFrameTimeNanos = frameTimeNanos
        totalTime += deltaTime

        if (canvasSize.width > 0 && canvasSize.height > 0) {
          if (!initialized) {
            ornamentSystem.initialize(canvasSize.width, canvasSize.height)
            initialized = true
          }
          snowfallSystem.update(canvasSize.width, canvasSize.height, deltaTime)
          ornamentSystem.update(totalTime)
        }
      }
    }
  }

  Canvas(modifier = modifier.fillMaxSize()) {
    canvasSize = size
    ornamentSystem.draw(this)
    snowfallSystem.draw(this)
  }
}

メインの ChristmasPaywallScreen

最後に、すべての要素をひとつに組み上げます。

kotlin
@Composable
fun ChristmasPaywallScreen(onDismiss: () -> Unit = {}) {
  Box(
    modifier = Modifier
      .fillMaxSize()
      .background(
        brush = Brush.verticalGradient(
          colors = listOf(
            Color(0xFF0F0F23),
            Color(0xFF1A1A2E),
            Color(0xFF0F0F23),
          ),
        ),
      ),
    contentAlignment = Alignment.Center,
  ) {
    // Animated background
    SnowfallBackground()

    // Christmas lights at top
    ChristmasLightsString(
      modifier = Modifier
        .align(Alignment.TopCenter)
        .padding(top = 60.dp),
    )

    // Main content
    Column(
      modifier = Modifier
        .fillMaxSize()
        .padding(horizontal = 24.dp),
      horizontalAlignment = Alignment.CenterHorizontally,
      verticalArrangement = Arrangement.Center,
    ) {
      // Holiday greeting
      Text(
        text = "🎄 Holiday Special 🎄",
        style = TextStyle(
          color = Color(0xFFFFD700),
          fontSize = 16.sp,
          fontWeight = FontWeight.Bold,
        ),
      )

      Spacer(modifier = Modifier.height(8.dp))

      Text(
        text = "Unlock Premium",
        style = TextStyle(
          color = Color.White,
          fontSize = 32.sp,
          fontWeight = FontWeight.Bold,
        ),
      )

      Text(
        text = "This Christmas",
        style = TextStyle(
          color = Color(0xFFE53935),
          fontSize = 28.sp,
          fontWeight = FontWeight.Bold,
        ),
      )

      Spacer(modifier = Modifier.height(24.dp))

      AnimatedGiftBox()

      Spacer(modifier = Modifier.height(24.dp))

      // Features card
      GlowingBorderCard(modifier = Modifier.fillMaxWidth()) {
        Column {
          Text(
            text = "Premium Features",
            style = TextStyle(
              color = Color.White,
              fontSize = 18.sp,
              fontWeight = FontWeight.Bold,
            ),
          )
          Spacer(modifier = Modifier.height(16.dp))
          PremiumFeatureItem("🎅", "Unlimited Holiday Themes")
          PremiumFeatureItem("🦌", "Exclusive Winter Content")
          PremiumFeatureItem("🎁", "Special Gift Rewards")
          PremiumFeatureItem("❄️", "Ad-Free Experience")
          PremiumFeatureItem("🌟", "Priority Support")
        }
      }

      Spacer(modifier = Modifier.height(24.dp))

      // Discount badge
      Row(verticalAlignment = Alignment.CenterVertically) {
        CandyCaneDecoration()
        Spacer(modifier = Modifier.width(8.dp))
        Text(
          text = "50% OFF",
          style = TextStyle(
            color = Color(0xFF4CAF50),
            fontSize = 24.sp,
            fontWeight = FontWeight.ExtraBold,
          ),
        )
        Spacer(modifier = Modifier.width(8.dp))
        CandyCaneDecoration(modifier = Modifier.rotate(180f))
      }

      Spacer(modifier = Modifier.height(24.dp))

      // Subscribe buttons
      Button(
        onClick = { /* Handle purchase */ },
        modifier = Modifier.fillMaxWidth().height(64.dp),
        colors = ButtonDefaults.buttonColors(
          containerColor = Color(0xFF2E7D32),
        ),
        shape = RoundedCornerShape(16.dp),
      ) {
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
          Text("🎁 Yearly - $29.99/year")
          Text(
            "Save 50% • Only $2.49/month",
            fontSize = 12.sp,
            color = Color.White.copy(alpha = 0.8f),
          )
        }
      }
    }
  }
}

まとめ

0:02:00

おめでとうございます! 目を引くアニメーション付きクリスマスペイウォールが完成しました。

学んだこと

このコードラボを通して、リアルな降雪効果のためのパーティクルシステムの作り方を学びました。BlendMode.Plus を使ったグロー効果付きの浮遊オーナメントアニメーションを構築しました。infiniteRepeatable で開始を遅らせ、クリスマスライトのずらしたアニメーションを実装しました。rememberInfiniteTransition で跳ねたり揺れたりする装飾を作りました。グラデーションブラシとぼかし効果で、アニメーションする光る枠線のカードを構築しました。そして最後に、これらの要素をすべて組み合わせて、まとまりのあるペイウォール画面に仕上げました。

次のステップ

このペイウォールを製品として使うには、実際の購入処理のために RevenueCat との連携を検討してください。割引率や訴求文を変えて A/B テストを行い、コンバージョンを最適化できます。色やアニメーションをブランドのアイデンティティに合わせてカスタマイズしましょう。ボタンを押したときのハプティックフィードバックを追加すれば、ユーザー体験を高められます。さらに没入感を出すために、控えめなホリデーのサウンド効果を加えることも検討してみてください。

ソースコード

このコードラボの完全なソースコードは GitHub で公開しています: compose-paywall-animations

リソース

さらに学びを深めるには、サブスクリプション管理については RevenueCat ドキュメントを、高度なアニメーション手法については Jetpack Compose アニメーションガイドを、カスタムグラフィックスについては Compose Canvas 描画のドキュメントをご覧ください。

すてきなホリデーと、楽しいコーディングを!