Skip to content

Commit 176e67a

Browse files
Googlercopybara-github
authored andcommitted
Add multiple effects to the demo app in runtime
This allows for the effects to be added in a more customizable fashion (including multiselect) to the composition during runtime of the application. PiperOrigin-RevId: 785789215
1 parent 9dad660 commit 176e67a

File tree

3 files changed

+115
-38
lines changed

3 files changed

+115
-38
lines changed

demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.kt

Lines changed: 95 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ import androidx.activity.viewModels
2727
import androidx.appcompat.app.AppCompatActivity
2828
import androidx.compose.foundation.background
2929
import androidx.compose.foundation.border
30+
import androidx.compose.foundation.clickable
3031
import androidx.compose.foundation.layout.Arrangement
3132
import androidx.compose.foundation.layout.Box
3233
import androidx.compose.foundation.layout.Column
3334
import androidx.compose.foundation.layout.Row
3435
import androidx.compose.foundation.layout.Spacer
3536
import androidx.compose.foundation.layout.fillMaxSize
3637
import androidx.compose.foundation.layout.fillMaxWidth
38+
import androidx.compose.foundation.layout.height
3739
import androidx.compose.foundation.layout.heightIn
3840
import androidx.compose.foundation.layout.padding
3941
import androidx.compose.foundation.lazy.LazyColumn
@@ -45,17 +47,15 @@ import androidx.compose.foundation.shape.RoundedCornerShape
4547
import androidx.compose.foundation.verticalScroll
4648
import androidx.compose.material.icons.Icons
4749
import androidx.compose.material.icons.filled.Add
48-
import androidx.compose.material.icons.filled.Star
4950
import androidx.compose.material.icons.twotone.Delete
50-
import androidx.compose.material.icons.twotone.Star
5151
import androidx.compose.material3.Button
5252
import androidx.compose.material3.Card
53+
import androidx.compose.material3.Checkbox
5354
import androidx.compose.material3.ElevatedButton
5455
import androidx.compose.material3.FilledIconButton
5556
import androidx.compose.material3.HorizontalDivider
5657
import androidx.compose.material3.Icon
5758
import androidx.compose.material3.IconButton
58-
import androidx.compose.material3.IconToggleButton
5959
import androidx.compose.material3.MaterialTheme
6060
import androidx.compose.material3.OutlinedButton
6161
import androidx.compose.material3.RadioButton
@@ -361,13 +361,24 @@ class CompositionPreviewActivity : AppCompatActivity() {
361361

362362
@Composable
363363
fun VideoSequenceList(viewModel: CompositionPreviewViewModel) {
364-
var showDialog by remember { mutableStateOf(false) }
364+
var selectedMediaItemIndex by remember { mutableStateOf<Int?>(null) }
365+
var showEditMediaItemsDialog by remember { mutableStateOf(false) }
365366

366-
if (showDialog) {
367+
if (showEditMediaItemsDialog) {
367368
VideoSequenceDialog(
368-
{ showDialog = false },
369-
viewModel.mediaItemOptions,
370-
{ index -> viewModel.addItem(index) },
369+
onDismissRequest = { showEditMediaItemsDialog = false },
370+
itemOptions = viewModel.mediaItemOptions,
371+
addSelectedVideo = { index -> viewModel.addItem(index) },
372+
)
373+
}
374+
375+
selectedMediaItemIndex?.let { index ->
376+
val item = viewModel.selectedMediaItems[index]
377+
EffectSelectionDialog(
378+
onDismissRequest = { selectedMediaItemIndex = null },
379+
effectOptions = viewModel.availableEffectNames,
380+
currentSelections = item.selectedEffects.value,
381+
onEffectsSelected = { newEffects -> viewModel.updateEffectsForItem(index, newEffects) },
371382
)
372383
}
373384

@@ -396,33 +407,27 @@ class CompositionPreviewActivity : AppCompatActivity() {
396407
Row(
397408
horizontalArrangement = Arrangement.SpaceBetween,
398409
verticalAlignment = Alignment.CenterVertically,
399-
modifier = Modifier.fillMaxWidth(),
410+
modifier = Modifier.fillMaxWidth().clickable { selectedMediaItemIndex = index },
400411
) {
401-
Text(
402-
text = "${index + 1}. ${item.title}",
403-
modifier = Modifier.textPadding().weight(1f),
404-
)
405-
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
406-
IconToggleButton(
407-
checked = item.applyEffects.value,
408-
onCheckedChange = { checked -> viewModel.updateEffects(index, checked) },
409-
) {
410-
Icon(
411-
imageVector =
412-
if (item.applyEffects.value) Icons.Filled.Star else Icons.TwoTone.Star,
413-
contentDescription = "Apply effects to item ${index + 1}",
414-
)
415-
}
416-
IconButton({ viewModel.removeItem(index) }) {
417-
Icon(Icons.TwoTone.Delete, contentDescription = "Remove item ${index + 1}")
418-
}
412+
Column(modifier = Modifier.textPadding().weight(1f)) {
413+
Text(text = "${index + 1}. ${item.title}")
414+
val effectsText = item.selectedEffects.value.joinToString().ifEmpty { "None" }
415+
Text(
416+
text = "Effect: $effectsText",
417+
fontSize = 12.sp,
418+
fontStyle = FontStyle.Italic,
419+
color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f),
420+
)
421+
}
422+
IconButton({ viewModel.removeItem(index) }) {
423+
Icon(Icons.TwoTone.Delete, contentDescription = "Remove item ${index + 1}")
419424
}
420425
}
421426
}
422427
}
423428
HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.secondary)
424429
ElevatedButton(
425-
onClick = { showDialog = true },
430+
onClick = { showEditMediaItemsDialog = true },
426431
modifier = Modifier.align(Alignment.CenterHorizontally),
427432
) {
428433
Text(text = stringResource(R.string.edit))
@@ -533,6 +538,68 @@ class CompositionPreviewActivity : AppCompatActivity() {
533538
}
534539
}
535540

541+
@Composable
542+
fun EffectSelectionDialog(
543+
onDismissRequest: () -> Unit,
544+
effectOptions: List<String>,
545+
currentSelections: Set<String>,
546+
onEffectsSelected: (Set<String>) -> Unit,
547+
) {
548+
var selectedOptions by remember { mutableStateOf(currentSelections) }
549+
550+
Dialog(onDismissRequest = onDismissRequest) {
551+
Card(shape = RoundedCornerShape(16.dp)) {
552+
Column(modifier = Modifier.padding(MaterialTheme.spacing.standard)) {
553+
Text(
554+
text = stringResource(R.string.select_effects),
555+
fontWeight = FontWeight.Bold,
556+
modifier = Modifier.padding(bottom = MaterialTheme.spacing.small),
557+
)
558+
Column {
559+
effectOptions.forEach { effectName ->
560+
Row(
561+
Modifier.fillMaxWidth()
562+
.clickable {
563+
selectedOptions =
564+
if (selectedOptions.contains(effectName)) {
565+
selectedOptions - effectName
566+
} else {
567+
selectedOptions + effectName
568+
}
569+
}
570+
.padding(vertical = MaterialTheme.spacing.mini),
571+
verticalAlignment = Alignment.CenterVertically,
572+
) {
573+
Checkbox(checked = selectedOptions.contains(effectName), onCheckedChange = null)
574+
Text(
575+
text = effectName,
576+
modifier = Modifier.padding(start = MaterialTheme.spacing.small),
577+
)
578+
}
579+
}
580+
}
581+
Spacer(modifier = Modifier.height(MaterialTheme.spacing.standard))
582+
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
583+
OutlinedButton(
584+
onClick = onDismissRequest,
585+
modifier = Modifier.padding(end = MaterialTheme.spacing.small),
586+
) {
587+
Text(stringResource(R.string.cancel))
588+
}
589+
Button(
590+
onClick = {
591+
onEffectsSelected(selectedOptions)
592+
onDismissRequest()
593+
}
594+
) {
595+
Text(stringResource(R.string.ok))
596+
}
597+
}
598+
}
599+
}
600+
}
601+
}
602+
536603
companion object {
537604
private const val TAG = "CompPreviewActivity"
538605

demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewViewModel.kt

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class CompositionPreviewViewModel(application: Application, val compositionLayou
7575
val title: String,
7676
val uri: String,
7777
val durationUs: Long,
78-
var applyEffects: MutableState<Boolean>,
78+
var selectedEffects: MutableState<Set<String>>,
7979
)
8080

8181
var snackbarMessage by mutableStateOf<String?>(null)
@@ -110,16 +110,20 @@ class CompositionPreviewViewModel(application: Application, val compositionLayou
110110
}
111111
)
112112

113-
private val perItemVideoEffects =
114-
listOf<Effect>(createDizzyCropEffect(), RgbFilter.createGrayscaleFilter())
113+
private val effectOptions: Map<String, Effect> =
114+
mapOf("Grayscale" to RgbFilter.createGrayscaleFilter(), "Dizzy Crop" to createDizzyCropEffect())
115+
116+
val availableEffectNames: List<String> = effectOptions.keys.toList()
115117

116118
init {
117119
// Load media items
118120
val titles = application.resources.getStringArray(/* id= */ R.array.preset_descriptions)
119121
val uris = application.resources.getStringArray(/* id= */ R.array.preset_uris)
120122
val durations = application.resources.getIntArray(/* id= */ R.array.preset_durations)
121123
for (i in titles.indices) {
122-
mediaItemOptions.add(Item(titles[i], uris[i], durations[i].toLong(), mutableStateOf(false)))
124+
mediaItemOptions.add(
125+
Item(titles[i], uris[i], durations[i].toLong(), mutableStateOf(emptySet()))
126+
)
123127
}
124128
// Load initial media item selections. No need to show the Snackbar message at this point
125129
addItem(0, showSnackbarMessage = false)
@@ -141,7 +145,9 @@ class CompositionPreviewViewModel(application: Application, val compositionLayou
141145
}
142146

143147
fun addItem(index: Int, showSnackbarMessage: Boolean = true) {
144-
selectedMediaItems.add(mediaItemOptions[index].copy(applyEffects = mutableStateOf(false)))
148+
selectedMediaItems.add(
149+
mediaItemOptions[index].copy(selectedEffects = mutableStateOf(emptySet()))
150+
)
145151
if (showSnackbarMessage) {
146152
snackbarMessage = "Added item: ${mediaItemOptions[index].title}"
147153
}
@@ -151,8 +157,8 @@ class CompositionPreviewViewModel(application: Application, val compositionLayou
151157
selectedMediaItems.removeAt(index)
152158
}
153159

154-
fun updateEffects(index: Int, checked: Boolean) {
155-
selectedMediaItems[index].applyEffects.value = checked
160+
fun updateEffectsForItem(index: Int, newEffects: Set<String>) {
161+
selectedMediaItems[index].selectedEffects.value = newEffects
156162
}
157163

158164
fun previewComposition() {
@@ -268,8 +274,11 @@ class CompositionPreviewViewModel(application: Application, val compositionLayou
268274
.setUri(item.uri)
269275
.setImageDurationMs(usToMs(item.durationUs)) // Ignored for audio/video
270276
.build()
271-
val finalVideoEffects =
272-
globalVideoEffects + if (item.applyEffects.value) perItemVideoEffects else emptyList()
277+
val effectsForItem = mutableListOf<Effect>()
278+
for (effectName in item.selectedEffects.value) {
279+
effectOptions[effectName]?.let { effectsForItem.add(it) }
280+
}
281+
val finalVideoEffects = globalVideoEffects + effectsForItem
273282
val itemBuilder =
274283
EditedMediaItem.Builder(mediaItem)
275284
.setEffects(

demos/composition/src/main/res/values/strings.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<string name="app_name">Composition Demo</string>
1818
<string name="edit">Edit</string>
1919
<string name="add_effects">Add effects</string>
20-
<string name="add_effects_hint">Click the star to apply effects</string>
20+
<string name="add_effects_hint">Click the Media Item to apply effects</string>
2121
<string name="preview" translatable="false">Preview</string>
2222
<string name="preview_composition" translatable="false">Composition preview</string>
2323
<string name="video_sequence_items" translatable="false">Video sequence items</string>
@@ -32,6 +32,7 @@
3232
<string name="hdr_mode" translatable="false">HDR mode</string>
3333
<string name="ok" translatable="false">OK</string>
3434
<string name="select" translatable="false">Select</string>
35+
<string name="select_effects" translatable="false">Select Effects</string>
3536
<string name="cancel" translatable="false">Cancel</string>
3637
<string name="export_settings" translatable="false">Export Settings</string>
3738
<string name="output_audio_mime_type" translatable="false">Output audio MIME type</string>

0 commit comments

Comments
 (0)