From 6f187a38593b2dfb433c0d5e48f3099289f82ab0 Mon Sep 17 00:00:00 2001 From: shahddaghash Date: Tue, 7 Jan 2025 09:29:29 -0800 Subject: [PATCH] Add confetti overlay to Effect demo Added the first overlay effect to the Effect demo. It includes an emitter of confetti that drops from the center of the frame. PiperOrigin-RevId: 712940163 --- demos/effect/lint.xml | 20 +++ .../media3/demo/effect/ConfettiOverlay.kt | 140 ++++++++++++++++++ .../media3/demo/effect/EffectActivity.kt | 25 +++- demos/effect/src/main/res/values/strings.xml | 1 + 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 demos/effect/lint.xml create mode 100644 demos/effect/src/main/java/androidx/media3/demo/effect/ConfettiOverlay.kt diff --git a/demos/effect/lint.xml b/demos/effect/lint.xml new file mode 100644 index 0000000000..46a2afc3a1 --- /dev/null +++ b/demos/effect/lint.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/demos/effect/src/main/java/androidx/media3/demo/effect/ConfettiOverlay.kt b/demos/effect/src/main/java/androidx/media3/demo/effect/ConfettiOverlay.kt new file mode 100644 index 0000000000..e978c3ae46 --- /dev/null +++ b/demos/effect/src/main/java/androidx/media3/demo/effect/ConfettiOverlay.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.demo.effect + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PorterDuff +import android.os.Handler +import androidx.media3.common.VideoFrameProcessingException +import androidx.media3.common.util.Size +import androidx.media3.common.util.Util +import androidx.media3.effect.CanvasOverlay +import kotlin.math.abs +import kotlin.random.Random + +/** Mimics an emitter of confetti, dropping from the center of the frame. */ +internal class ConfettiOverlay : CanvasOverlay(/* useInputFrameSize= */ true) { + + private val confettiList = mutableListOf() + private val paint = Paint() + private val handler = Handler(Util.getCurrentOrMainLooper()) + + private var addConfettiTask: (() -> Unit)? = null + private var width = 0f + private var height = 0f + private var started = false + + override fun configure(videoSize: Size) { + super.configure(videoSize) + this.width = videoSize.width.toFloat() + this.height = videoSize.height.toFloat() + } + + @Synchronized + override fun onDraw(canvas: Canvas, presentationTimeUs: Long) { + if (!started) { + start() + } + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) + + confettiList.removeAll { confetti -> + confetti.y > height / 2 || confetti.x <= 0 || confetti.x > width + } + for (confetti in confettiList) { + confetti.draw(canvas, paint) + confetti.update() + } + } + + @Throws(VideoFrameProcessingException::class) + override fun release() { + super.release() + handler.post(this::stop) + } + + /** Starts the confetti. */ + fun start() { + addConfettiTask = this::addConfetti + handler.post(checkNotNull(addConfettiTask)) + started = true + } + + /** Stops the confetti. */ + fun stop() { + handler.removeCallbacks(checkNotNull(addConfettiTask)) + confettiList.clear() + started = false + addConfettiTask = null + } + + @Synchronized + fun addConfetti() { + repeat(5) { + confettiList.add( + Confetti( + text = CONFETTI_TEXTS[abs(Random.nextInt()) % CONFETTI_TEXTS.size], + x = width / 2f, + y = EMITTER_POSITION_Y.toFloat(), + size = CONFETTI_BASE_SIZE + Random.nextInt(CONFETTI_SIZE_VARIATION), + color = Color.HSVToColor(floatArrayOf(Random.nextInt(360).toFloat(), 0.6f, 0.8f)), + ) + ) + } + handler.postDelayed(this::addConfetti, /* delayMillis= */ 100) + } + + private class Confetti( + private val text: String, + private val size: Int, + private val color: Int, + var x: Float, + var y: Float, + ) { + private val speedX = 4 * (Random.nextFloat() * 2 - 1) // Random speed in x direction + private val speedY = 4 * Random.nextFloat() // Random speed in y direction + private val rotationSpeed = (Random.nextFloat() - 0.5f) * 4f // Random rotation speed + + private var rotation = Random.nextFloat() * 360f + + /** Draws the [Confetti] on the [Canvas]. */ + fun draw(canvas: Canvas, paint: Paint) { + canvas.save() + paint.color = color + canvas.translate(x, y) + canvas.rotate(rotation) + paint.textSize = size.toFloat() + canvas.drawText(text, /* x= */ 0f, /* y= */ 0f, paint) // Only draw text + canvas.restore() + } + + /** Updates the [Confetti]. */ + fun update() { + x += speedX + y += speedY + rotation += rotationSpeed + } + } + + private companion object { + val CONFETTI_TEXTS = listOf("❊", "✿", "❊", "✦︎", "♥︎", "☕︎") + const val EMITTER_POSITION_Y = -50 + const val CONFETTI_BASE_SIZE = 30 + const val CONFETTI_SIZE_VARIATION = 10 + } +} diff --git a/demos/effect/src/main/java/androidx/media3/demo/effect/EffectActivity.kt b/demos/effect/src/main/java/androidx/media3/demo/effect/EffectActivity.kt index 3beb442b5a..5ab2f12d01 100644 --- a/demos/effect/src/main/java/androidx/media3/demo/effect/EffectActivity.kt +++ b/demos/effect/src/main/java/androidx/media3/demo/effect/EffectActivity.kt @@ -68,8 +68,11 @@ import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util.SDK_INT import androidx.media3.effect.Contrast +import androidx.media3.effect.OverlayEffect +import androidx.media3.effect.TextureOverlay import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView +import com.google.common.collect.ImmutableList import kotlinx.coroutines.launch class EffectActivity : ComponentActivity() { @@ -279,7 +282,15 @@ class EffectActivity : ComponentActivity() { onClick = { val effectsList = mutableListOf() - effectsList += Contrast(effectControlsState.contrastValue) + if (effectControlsState.contrastValue != 0f) { + effectsList += Contrast(effectControlsState.contrastValue) + } + + val overlaysBuilder = ImmutableList.builder() + if (effectControlsState.confettiOverlayChecked) { + overlaysBuilder.add(ConfettiOverlay()) + } + effectsList += OverlayEffect(overlaysBuilder.build()) onApplyEffectsClicked(effectsList) effectControlsState = effectControlsState.copy(effectsChanged = false) @@ -333,6 +344,17 @@ class EffectActivity : ComponentActivity() { } } } + item { + EffectItem( + name = stringResource(R.string.confetti_overlay), + enabled = enabled, + onCheckedChange = { checked -> + onEffectControlsStateChange( + effectControlsState.copy(effectsChanged = true, confettiOverlayChecked = checked) + ) + }, + ) + } } } @@ -382,6 +404,7 @@ class EffectActivity : ComponentActivity() { data class EffectControlsState( val effectsChanged: Boolean = false, val contrastValue: Float = 0f, + val confettiOverlayChecked: Boolean = false, ) companion object { diff --git a/demos/effect/src/main/res/values/strings.xml b/demos/effect/src/main/res/values/strings.xml index 19e097ff49..d6977494a3 100644 --- a/demos/effect/src/main/res/values/strings.xml +++ b/demos/effect/src/main/res/values/strings.xml @@ -24,4 +24,5 @@ "File couldn't be opened. Please try again." "Permission was not granted." Contrast + Confetti Overlay