Timeline
filmstrip-compose-ui holds the drawn pieces of an editor, built on Compose foundation with no Material. It sits on top
of filmstrip-compose.
FilmstripTimeline
Section titled “FilmstripTimeline”FilmstripTimeline assembles a ruler, a scrolling strip, a playhead and an optional trim window.
val composition = remember(source) { compositionOf { clip(source) } }val player = rememberFilmstripPlayer(filmstrip, composition)val playerState by player.state.collectAsState()val playhead = rememberPlayheadState(player)
Column { VideoSurface(player, Modifier.weight(1f))
FilmstripTimeline( renderer = filmstrip, composition = composition, duration = playhead.duration ?: Duration.ZERO, position = playhead.positionProvider(), isPlaying = playerState.isPlaying, scrub = rememberScrubState(player), )}A drag on the ruler moves the playhead and a drag on the strip moves the view. The strip follows the playhead during playback, and stops as soon as the user scrolls it.
It doesn’t need a player. position comes from whatever clock you run, and a timeline with no scrub is one where a drag
on the ruler doesn’t seek.
One track is drawn. A composition with several shows the timeline they lay out on, with no clip edges marked.
The trim window draws when you pass both trim and onTrimChange. The handles only move where trim next puts them, so
the range lives in your state.
var trimRange by remember(source) { mutableStateOf(TimeRange.from(Duration.ZERO)) }val composition = remember(source) { compositionOf { clip(source) } }val player = rememberFilmstripPlayer(filmstrip, composition)val playerState by player.state.collectAsState()val playhead = rememberPlayheadState(player)
FilmstripTimeline( renderer = filmstrip, composition = composition, duration = playhead.duration ?: Duration.ZERO, position = playhead.positionProvider(), isPlaying = playerState.isPlaying, scrub = rememberScrubState(player), trim = trimRange, onTrimChange = { range -> trimRange = range player.setPlaybackRange(range) },)setPlaybackRange confines playback to the window while the strip keeps covering the whole source, so a drag doesn’t
rebuild the composition the player is running. The trim reaches the edit when you build the one you export.
val composition = compositionOf { clip(source) { trim(trimRange) } }The window keeps to FilmstripTimelineDefaults.Trim, a minimum of 200 milliseconds, unless you set another
TrimConstraint. MinDuration, MinMaxDuration and FixedDuration are the options.
Options
Section titled “Options”zoom sets how far in the timeline is drawn, and null fits the whole composition on screen. A null zoom refits when the
width changes, like on a rotation or a window resize, until someone zooms. TimelineZoom.of takes a step from 0 to 10.
TimelineOptions holds the rest, such as sizes, followPlayhead and the pinchZoom and wheelZoom switches.
FilmstripTimeline( renderer = filmstrip, composition = composition, duration = duration, position = position, isPlaying = isPlaying, zoom = TimelineZoom.of(4), options = TimelineOptions( followPlayhead = false, trimConstraint = TrimConstraint.MinDuration(1.seconds), ),)Keyboard control doesn’t go through here, since it needs focus and a component that took focus on composition would steal
it from your screen. Apply Modifier.timelineKeys yourself, such as on an element in the overlay slot, where
TimelineState is at hand.
The overlay slot draws over the tiles and under the trim window and the playhead. Anything FilmstripTimeline doesn’t lay
out the way you want, you can build from the same public parts: FilmstripStrip, TimelineRuler, Playhead and
TrimOverlay.
VideoStage and crop
Section titled “VideoStage and crop”VideoStage is a box the shape of the output frame, holding the video and whatever you draw over it. It takes the largest
rectangle of that shape that fits, so give it room to fit into, like a padding inside a centred box, and not a
fillMaxSize.
CropOverlay inside a stage draws a draggable rectangle over the frame.
val composition = remember(source) { compositionOf { clip(source) } }val player = rememberFilmstripPlayer(filmstrip, composition)var rect by remember(source) { mutableStateOf(NormalizedRect.Full) }
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { VideoStage( player = player, outputAspect = aspect, modifier = Modifier.padding(16.dp), ) { CropOverlay(rect = rect, onRectChange = { rect = it }) }}A live player’s frame wins over outputAspect, so the stage and the picture change shape together. Passing a null player
draws the fallback slot at the same size instead.
The rectangle addresses the frame under it, so the preview stays uncropped and the crop joins the edit when you export.
val composition = compositionOf { clip(source) effects { crop(rect) } }