Skip to content

Commit 4610b94

Browse files
committed
Add tabs for video editing screen.
1 parent 24ff0ff commit 4610b94

File tree

1 file changed

+238
-80
lines changed

1 file changed

+238
-80
lines changed

app/src/main/java/com/google/android/samples/socialite/ui/videoedit/VideoEditScreen.kt

Lines changed: 238 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ package com.google.android.samples.socialite.ui.videoedit
1818

1919
import android.graphics.Bitmap
2020
import android.media.MediaMetadataRetriever
21-
import android.util.Log
22-
import androidx.compose.foundation.Image
2321
import android.content.Context
2422
import androidx.compose.foundation.background
2523
import androidx.compose.foundation.layout.Arrangement
@@ -45,9 +43,7 @@ import androidx.compose.material.icons.filled.Close
4543
import androidx.compose.material.icons.filled.ColorLens
4644
import androidx.compose.material.icons.filled.DonutLarge
4745
import androidx.compose.material.icons.filled.FormatSize
48-
import androidx.compose.material.icons.filled.Movie
4946
import androidx.compose.material.icons.filled.Style
50-
import androidx.compose.material.icons.filled.VolumeMute
5147
import androidx.compose.material3.Button
5248
import androidx.compose.material3.ButtonDefaults
5349
import androidx.compose.material3.CircularProgressIndicator
@@ -58,6 +54,9 @@ import androidx.compose.material3.Icon
5854
import androidx.compose.material3.IconButton
5955
import androidx.compose.material3.MaterialTheme
6056
import androidx.compose.material3.Scaffold
57+
import androidx.compose.material3.SecondaryTabRow
58+
import androidx.compose.material3.Tab
59+
import androidx.compose.material3.TabRowDefaults
6160
import androidx.compose.material3.Text
6261
import androidx.compose.material3.TextField
6362
import androidx.compose.material3.TextFieldDefaults
@@ -66,15 +65,14 @@ import androidx.compose.material3.TopAppBarDefaults
6665
import androidx.compose.runtime.Composable
6766
import androidx.compose.runtime.LaunchedEffect
6867
import androidx.compose.runtime.Immutable
69-
import androidx.compose.runtime.LaunchedEffect
68+
import androidx.compose.runtime.MutableLongState
7069
import androidx.compose.runtime.collectAsState
7170
import androidx.compose.runtime.getValue
7271
import androidx.compose.runtime.mutableIntStateOf
7372
import androidx.compose.runtime.mutableLongStateOf
7473
import androidx.compose.runtime.mutableStateOf
7574
import androidx.compose.runtime.remember
7675
import androidx.compose.runtime.rememberCoroutineScope
77-
import androidx.compose.runtime.remember
7876
import androidx.compose.runtime.saveable.rememberSaveable
7977
import androidx.compose.runtime.setValue
8078
import 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

Comments
 (0)