@@ -18,8 +18,6 @@ package com.google.android.samples.socialite.ui.videoedit
1818
1919import android.graphics.Bitmap
2020import android.media.MediaMetadataRetriever
21- import android.util.Log
22- import androidx.compose.foundation.Image
2321import android.content.Context
2422import androidx.compose.foundation.background
2523import androidx.compose.foundation.layout.Arrangement
@@ -45,9 +43,7 @@ import androidx.compose.material.icons.filled.Close
4543import androidx.compose.material.icons.filled.ColorLens
4644import androidx.compose.material.icons.filled.DonutLarge
4745import androidx.compose.material.icons.filled.FormatSize
48- import androidx.compose.material.icons.filled.Movie
4946import androidx.compose.material.icons.filled.Style
50- import androidx.compose.material.icons.filled.VolumeMute
5147import androidx.compose.material3.Button
5248import androidx.compose.material3.ButtonDefaults
5349import androidx.compose.material3.CircularProgressIndicator
@@ -58,6 +54,9 @@ import androidx.compose.material3.Icon
5854import androidx.compose.material3.IconButton
5955import androidx.compose.material3.MaterialTheme
6056import androidx.compose.material3.Scaffold
57+ import androidx.compose.material3.SecondaryTabRow
58+ import androidx.compose.material3.Tab
59+ import androidx.compose.material3.TabRowDefaults
6160import androidx.compose.material3.Text
6261import androidx.compose.material3.TextField
6362import androidx.compose.material3.TextFieldDefaults
@@ -66,15 +65,14 @@ import androidx.compose.material3.TopAppBarDefaults
6665import androidx.compose.runtime.Composable
6766import androidx.compose.runtime.LaunchedEffect
6867import androidx.compose.runtime.Immutable
69- import androidx.compose.runtime.LaunchedEffect
68+ import androidx.compose.runtime.MutableLongState
7069import androidx.compose.runtime.collectAsState
7170import androidx.compose.runtime.getValue
7271import androidx.compose.runtime.mutableIntStateOf
7372import androidx.compose.runtime.mutableLongStateOf
7473import androidx.compose.runtime.mutableStateOf
7574import androidx.compose.runtime.remember
7675import androidx.compose.runtime.rememberCoroutineScope
77- import androidx.compose.runtime.remember
7876import androidx.compose.runtime.saveable.rememberSaveable
7977import androidx.compose.runtime.setValue
8078import androidx.compose.ui.Alignment
@@ -258,89 +256,249 @@ fun VideoEditScreen(
258256 }
259257 Spacer (modifier = Modifier .height(20 .dp))
260258
261- Column (
262- modifier = Modifier
263- .fillMaxWidth()
264- .align(Alignment .CenterHorizontally )
265- .padding(16 .dp)
266- .background(
267- color = colorResource(R .color.dark_gray),
268- shape = RoundedCornerShape (size = 28 .dp),
269- )
270- .padding(15 .dp),
271- ) {
272- VideoEditFilterChip (
273- icon = Icons .AutoMirrored .Filled .VolumeMute ,
274- selected = removeAudioEnabled,
275- onClick = { removeAudioEnabled = ! removeAudioEnabled },
276- label = stringResource(id = R .string.remove_audio),
259+ // Tabbed Controls Area
260+ VideoEditTabs (
261+ removeAudioEnabled = removeAudioEnabled,
262+ onRemoveAudioToggle = { removeAudioEnabled = ! removeAudioEnabled },
263+ rgbAdjustmentEffectEnabled = rgbAdjustmentEffectEnabled,
264+ onRgbAdjustmentEffectToggle = {
265+ rgbAdjustmentEffectEnabled = ! rgbAdjustmentEffectEnabled
266+ },
267+ periodicVignetteEffectEnabled = periodicVignetteEffectEnabled,
268+ onPeriodicVignetteEffectToggle = {
269+ periodicVignetteEffectEnabled = ! periodicVignetteEffectEnabled
270+ },
271+ styleTransferEffectEnabled = styleTransferEffectEnabled,
272+ onStyleTransferEffectToggle = {
273+ styleTransferEffectEnabled = ! styleTransferEffectEnabled
274+ },
275+ overlayText = overlayText,
276+ onOverlayTextChange = { if (it.length <= 20 ) overlayText = it },
277+ redOverlayTextEnabled = redOverlayTextEnabled,
278+ onRedOverlayTextToggle = { redOverlayTextEnabled = ! redOverlayTextEnabled },
279+ largeOverlayTextEnabled = largeOverlayTextEnabled,
280+ onLargeOverlayTextToggle = { largeOverlayTextEnabled = ! largeOverlayTextEnabled },
281+ videoTrimStart = videoTrimStart,
282+ videoTrimEnd = videoTrimEnd,
283+ onTrimChanged = { startMs, endMs ->
284+ videoTrimStart = startMs
285+ videoTrimEnd = endMs
286+ },
287+ frames = frames,
288+ durationMs = duration,
289+ )
290+ }
291+ }
292+
293+ // Show a loading indicator while the video is being processed.
294+ CenteredCircularProgressIndicator (isProcessing.value)
295+ }
296+
297+ @Composable
298+ fun VideoEditTabs (
299+ removeAudioEnabled : Boolean ,
300+ onRemoveAudioToggle : () -> Unit ,
301+ rgbAdjustmentEffectEnabled : Boolean ,
302+ onRgbAdjustmentEffectToggle : () -> Unit ,
303+ periodicVignetteEffectEnabled : Boolean ,
304+ onPeriodicVignetteEffectToggle : () -> Unit ,
305+ styleTransferEffectEnabled : Boolean ,
306+ onStyleTransferEffectToggle : () -> Unit ,
307+ overlayText : String ,
308+ onOverlayTextChange : (String ) -> Unit ,
309+ redOverlayTextEnabled : Boolean ,
310+ onRedOverlayTextToggle : () -> Unit ,
311+ largeOverlayTextEnabled : Boolean ,
312+ onLargeOverlayTextToggle : () -> Unit ,
313+ videoTrimStart : Long ,
314+ videoTrimEnd : Long ,
315+ onTrimChanged : (startMs: Long , endMs: Long ) -> Unit ,
316+ frames : List <Bitmap >,
317+ durationMs : MutableLongState ,
318+ ) {
319+ var selectedTabIndex by remember { mutableStateOf(0 ) }
320+ val tabs = listOf (" Edit" , " Overlay" , " Trim" )
321+
322+ Column (
323+ modifier = Modifier
324+ .fillMaxWidth()
325+ .padding(
326+ top = 16 .dp,
327+ start = 16 .dp,
328+ end = 16 .dp,
329+ bottom = 32 .dp,
330+ )
331+ .background(
332+ color = colorResource(R .color.dark_gray),
333+ shape = RoundedCornerShape (size = 28 .dp),
334+ ),
335+ ) {
336+ SecondaryTabRow (
337+ selectedTabIndex = selectedTabIndex,
338+ containerColor = colorResource(R .color.dark_gray),
339+ contentColor = Color .White ,
340+ indicator = {
341+ TabRowDefaults .SecondaryIndicator (
342+ Modifier .tabIndicatorOffset(selectedTabIndex),
343+ color = colorResource(id = R .color.aqua),
277344 )
278- VideoEditFilterChip (
279- icon = Icons .Filled .ColorLens ,
280- selected = rgbAdjustmentEffectEnabled,
281- onClick = { rgbAdjustmentEffectEnabled = ! rgbAdjustmentEffectEnabled },
282- label = stringResource(id = R .string.add_rgb_adjustment_effect),
345+ },
346+ divider = {}, // Remove default divider
347+ ) {
348+ tabs.forEachIndexed { index, title ->
349+ Tab (
350+ selected = selectedTabIndex == index,
351+ onClick = { selectedTabIndex = index },
352+ text = { Text (title) },
353+ selectedContentColor = colorResource(id = R .color.aqua),
354+ unselectedContentColor = Color .LightGray ,
283355 )
284- VideoEditFilterChip (
285- icon = Icons .Filled .Brightness1 ,
286- selected = periodicVignetteEffectEnabled,
287- onClick = { periodicVignetteEffectEnabled = ! periodicVignetteEffectEnabled },
288- label = stringResource(id = R .string.add_periodic_vignette_effect),
356+ }
357+ }
358+
359+ Box (modifier = Modifier .padding(15 .dp)) {
360+ when (selectedTabIndex) {
361+ 0 -> VideoEditControls (
362+ removeAudioEnabled = removeAudioEnabled,
363+ onRemoveAudioToggle = onRemoveAudioToggle,
364+ rgbAdjustmentEffectEnabled = rgbAdjustmentEffectEnabled,
365+ onRgbAdjustmentEffectToggle = onRgbAdjustmentEffectToggle,
366+ periodicVignetteEffectEnabled = periodicVignetteEffectEnabled,
367+ onPeriodicVignetteEffectToggle = onPeriodicVignetteEffectToggle,
368+ styleTransferEffectEnabled = styleTransferEffectEnabled,
369+ onStyleTransferEffectToggle = onStyleTransferEffectToggle,
289370 )
290- VideoEditFilterChip (
291- icon = Icons .Filled .Style ,
292- selected = styleTransferEffectEnabled,
293- onClick = { styleTransferEffectEnabled = ! styleTransferEffectEnabled },
294- label = stringResource(id = R .string.add_style_transfer_effect),
371+
372+ 1 -> VideoOverlayControls (
373+ overlayText = overlayText,
374+ onOverlayTextChange = onOverlayTextChange,
375+ redOverlayTextEnabled = redOverlayTextEnabled,
376+ onRedOverlayTextToggle = onRedOverlayTextToggle,
377+ largeOverlayTextEnabled = largeOverlayTextEnabled,
378+ onLargeOverlayTextToggle = onLargeOverlayTextToggle,
295379 )
296- Spacer (modifier = Modifier .height(10 .dp))
297- TextOverlayOption (
298- inputtedText = overlayText,
299- inputtedTextChange = {
300- // Limit character count to 20
301- if (it.length <= 20 ) {
302- overlayText = it
303- }
304- },
305- redTextCheckedState = redOverlayTextEnabled,
306- redTextCheckedStateChange = {
307- redOverlayTextEnabled = ! redOverlayTextEnabled
308- },
309- largeTextCheckedState = largeOverlayTextEnabled,
310- largeTextCheckedStateChange = {
311- largeOverlayTextEnabled = ! largeOverlayTextEnabled
312- },
380+
381+ 2 -> VideoTrimControls (
382+ videoTrimStart = videoTrimStart,
383+ videoTrimEnd = videoTrimEnd,
384+ onTrimChanged = onTrimChanged,
385+ frames = frames,
386+ durationMs = durationMs,
313387 )
314- val rangeStart = " %.2f" .format(videoTrimStart / 1000.0 )
315- val rangeEnd = " %.2f" .format(videoTrimEnd / 1000.0 )
316- Text (text = " Video segment: $rangeStart s .. $rangeEnd s" )
317-
318- if (frames.isNotEmpty()) {
319- FrameRangeSlider (
320- frames = frames,
321- state = TrimState (
322- durationMs = duration.longValue,
323- startMs = videoTrimStart,
324- endMs = videoTrimEnd,
325- ),
326- onTrimChanged = { startMs, endMs ->
327- videoTrimStart = startMs
328- videoTrimEnd = endMs
329- },
330- )
331- } else {
332- CircularProgressIndicator (
333- modifier = Modifier
334- .padding(16 .dp)
335- .align(Alignment .CenterHorizontally ),
336- )
337- }
338388 }
339389 }
340390 }
391+ }
341392
342- // Show a loading indicator while the video is being processed.
343- CenteredCircularProgressIndicator (isProcessing.value)
393+ @Composable
394+ fun VideoEditControls (
395+ removeAudioEnabled : Boolean ,
396+ onRemoveAudioToggle : () -> Unit ,
397+ rgbAdjustmentEffectEnabled : Boolean ,
398+ onRgbAdjustmentEffectToggle : () -> Unit ,
399+ periodicVignetteEffectEnabled : Boolean ,
400+ onPeriodicVignetteEffectToggle : () -> Unit ,
401+ styleTransferEffectEnabled : Boolean ,
402+ onStyleTransferEffectToggle : () -> Unit ,
403+ ) {
404+ Column {
405+ VideoEditFilterChip (
406+ icon = Icons .AutoMirrored .Filled .VolumeMute ,
407+ selected = removeAudioEnabled,
408+ onClick = onRemoveAudioToggle,
409+ label = stringResource(id = R .string.remove_audio),
410+ )
411+ VideoEditFilterChip (
412+ icon = Icons .Filled .ColorLens ,
413+ selected = rgbAdjustmentEffectEnabled,
414+ onClick = onRgbAdjustmentEffectToggle,
415+ label = stringResource(id = R .string.add_rgb_adjustment_effect),
416+ )
417+ VideoEditFilterChip (
418+ icon = Icons .Filled .Brightness1 ,
419+ selected = periodicVignetteEffectEnabled,
420+ onClick = onPeriodicVignetteEffectToggle,
421+ label = stringResource(id = R .string.add_periodic_vignette_effect),
422+ )
423+ VideoEditFilterChip (
424+ icon = Icons .Filled .Style ,
425+ selected = styleTransferEffectEnabled,
426+ onClick = onStyleTransferEffectToggle,
427+ label = stringResource(id = R .string.add_style_transfer_effect),
428+ )
429+ }
430+ }
431+
432+ @Composable
433+ fun VideoOverlayControls (
434+ overlayText : String ,
435+ onOverlayTextChange : (String ) -> Unit ,
436+ redOverlayTextEnabled : Boolean ,
437+ onRedOverlayTextToggle : () -> Unit ,
438+ largeOverlayTextEnabled : Boolean ,
439+ onLargeOverlayTextToggle : () -> Unit ,
440+ ) {
441+ Column (
442+ modifier = Modifier .fillMaxWidth(),
443+ horizontalAlignment = Alignment .CenterHorizontally ,
444+ ) {
445+ TextOverlayOption (
446+ inputtedText = overlayText,
447+ inputtedTextChange = onOverlayTextChange,
448+ redTextCheckedState = redOverlayTextEnabled,
449+ redTextCheckedStateChange = onRedOverlayTextToggle,
450+ largeTextCheckedState = largeOverlayTextEnabled,
451+ largeTextCheckedStateChange = onLargeOverlayTextToggle,
452+ )
453+ }
454+ }
455+
456+ @Composable
457+ fun VideoTrimControls (
458+ videoTrimStart : Long ,
459+ videoTrimEnd : Long ,
460+ onTrimChanged : (startMs: Long , endMs: Long ) -> Unit ,
461+ frames : List <Bitmap >,
462+ durationMs : MutableLongState ,
463+ ) {
464+ Column (horizontalAlignment = Alignment .CenterHorizontally ) {
465+ val rangeStart = " %.2f" .format(videoTrimStart / 1000.0 )
466+ val rangeEnd = " %.2f" .format(videoTrimEnd / 1000.0 )
467+ Text (
468+ text = " Video segment: $rangeStart s .. $rangeEnd s" ,
469+ color = Color .White ,
470+ modifier = Modifier .padding(bottom = 8 .dp),
471+ )
472+
473+ if (frames.isNotEmpty() && durationMs.longValue > 0 ) {
474+ FrameRangeSlider (
475+ frames = frames,
476+ state = TrimState (
477+ durationMs = durationMs.longValue,
478+ startMs = videoTrimStart,
479+ endMs = videoTrimEnd,
480+ ),
481+ onTrimChanged = onTrimChanged,
482+ )
483+ } else if (durationMs.longValue == 0L && videoTrimStart == 0L && videoTrimEnd == 0L ) {
484+ // Still loading duration and frames
485+ CircularProgressIndicator (
486+ modifier = Modifier
487+ .padding(16 .dp)
488+ .align(Alignment .CenterHorizontally ),
489+ )
490+ } else if (frames.isEmpty() && durationMs.longValue > 0 ) {
491+ // Duration loaded but frames not yet (or failed)
492+ Text (" Loading frames..." , color = Color .White )
493+ CircularProgressIndicator (
494+ modifier = Modifier
495+ .padding(16 .dp)
496+ .align(Alignment .CenterHorizontally ),
497+ )
498+ } else {
499+ Text (" Video too short or unable to load trim controls." , color = Color .White )
500+ }
501+ }
344502}
345503
346504@OptIn(ExperimentalMaterial3Api ::class )
0 commit comments