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
This commit is contained in:
shahddaghash 2025-01-07 09:29:29 -08:00 committed by Copybara-Service
parent 229aadc91b
commit 6f187a3859
4 changed files with 185 additions and 1 deletions

20
demos/effect/lint.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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
https://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.
-->
<lint>
<issue id="UnsafeOptInUsageError">
<option name="opt-in" value="androidx.media3.common.util.UnstableApi" />
</issue>
</lint>

View File

@ -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<Confetti>()
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
}
}

View File

@ -68,8 +68,11 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util.SDK_INT import androidx.media3.common.util.Util.SDK_INT
import androidx.media3.effect.Contrast import androidx.media3.effect.Contrast
import androidx.media3.effect.OverlayEffect
import androidx.media3.effect.TextureOverlay
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import com.google.common.collect.ImmutableList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class EffectActivity : ComponentActivity() { class EffectActivity : ComponentActivity() {
@ -279,7 +282,15 @@ class EffectActivity : ComponentActivity() {
onClick = { onClick = {
val effectsList = mutableListOf<Effect>() val effectsList = mutableListOf<Effect>()
effectsList += Contrast(effectControlsState.contrastValue) if (effectControlsState.contrastValue != 0f) {
effectsList += Contrast(effectControlsState.contrastValue)
}
val overlaysBuilder = ImmutableList.builder<TextureOverlay>()
if (effectControlsState.confettiOverlayChecked) {
overlaysBuilder.add(ConfettiOverlay())
}
effectsList += OverlayEffect(overlaysBuilder.build())
onApplyEffectsClicked(effectsList) onApplyEffectsClicked(effectsList)
effectControlsState = effectControlsState.copy(effectsChanged = false) 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( data class EffectControlsState(
val effectsChanged: Boolean = false, val effectsChanged: Boolean = false,
val contrastValue: Float = 0f, val contrastValue: Float = 0f,
val confettiOverlayChecked: Boolean = false,
) )
companion object { companion object {

View File

@ -24,4 +24,5 @@
<string name="can_not_open_file_error">"File couldn't be opened. Please try again."</string> <string name="can_not_open_file_error">"File couldn't be opened. Please try again."</string>
<string name="permission_not_granted_error">"Permission was not granted."</string> <string name="permission_not_granted_error">"Permission was not granted."</string>
<string name="contrast">Contrast</string> <string name="contrast">Contrast</string>
<string name="confetti_overlay">Confetti Overlay</string>
</resources> </resources>