add customizable seek bar
This commit is contained in:
parent
1375cfc877
commit
186fbf0c12
|
@ -56,7 +56,8 @@ data class VideoPlayerUIState(
|
|||
var uiVisible: Boolean,
|
||||
val contentRatio: Float,
|
||||
val embeddedUiRatio: Float,
|
||||
val contentFitMode: ContentScale
|
||||
val contentFitMode: ContentScale,
|
||||
val seekerPosition: Float
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
val DEFAULT = VideoPlayerUIState(
|
||||
|
@ -66,7 +67,8 @@ data class VideoPlayerUIState(
|
|||
uiVisible = false,
|
||||
contentRatio = 16 / 9F,
|
||||
embeddedUiRatio = 16F / 9F,
|
||||
contentFitMode = ContentScale.FIT_INSIDE
|
||||
contentFitMode = ContentScale.FIT_INSIDE,
|
||||
seekerPosition = 0F
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +91,8 @@ interface VideoPlayerViewModel {
|
|||
fun switchToEmbeddedView()
|
||||
fun showUi()
|
||||
fun hideUi()
|
||||
fun seekPositionChanged(newValue: Float)
|
||||
fun seekingFinished()
|
||||
|
||||
interface Listener {
|
||||
fun onFullscreenToggle(isFullscreen: Boolean)
|
||||
|
@ -260,6 +264,16 @@ class VideoPlayerViewModelImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun seekPositionChanged(newValue: Float) {
|
||||
uiVisibilityJob?.cancel()
|
||||
mutableUiState.update { it.copy(seekerPosition = newValue) }
|
||||
}
|
||||
|
||||
override fun seekingFinished() {
|
||||
resetHideUiDelayed()
|
||||
Log.d(TAG, "TODO: Implement seeking Finished")
|
||||
}
|
||||
|
||||
override fun switchToEmbeddedView() {
|
||||
callbackListener?.onFullscreenToggle(false)
|
||||
uiVisibilityJob?.cancel()
|
||||
|
@ -303,6 +317,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
|
|||
println("dummy impl")
|
||||
}
|
||||
|
||||
|
||||
override fun play() {
|
||||
println("dummy impl")
|
||||
}
|
||||
|
@ -323,6 +338,14 @@ class VideoPlayerViewModelImpl @Inject constructor(
|
|||
println("dummy impl")
|
||||
}
|
||||
|
||||
override fun seekPositionChanged(newValue: Float) {
|
||||
println("dummy impl")
|
||||
}
|
||||
|
||||
override fun seekingFinished() {
|
||||
println("dummy impl")
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
println("dummy pause")
|
||||
}
|
||||
|
|
|
@ -91,6 +91,7 @@ import androidx.compose.ui.unit.DpOffset
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import net.newpipe.newplayer.R
|
||||
import net.newpipe.newplayer.ui.seeker.Seeker
|
||||
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
||||
import net.newpipe.newplayer.ui.theme.video_player_onSurface
|
||||
|
||||
|
@ -99,6 +100,7 @@ fun VideoPlayerControllerUI(
|
|||
isPlaying: Boolean,
|
||||
fullscreen: Boolean,
|
||||
uiVissible: Boolean,
|
||||
seekPosition: Float,
|
||||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
prevStream: () -> Unit,
|
||||
|
@ -106,7 +108,9 @@ fun VideoPlayerControllerUI(
|
|||
switchToFullscreen: () -> Unit,
|
||||
switchToEmbeddedView: () -> Unit,
|
||||
showUi: () -> Unit,
|
||||
hideUi: () -> Unit
|
||||
hideUi: () -> Unit,
|
||||
seekPositionChanged: (Float) -> Unit,
|
||||
seekingFinished: () -> Unit
|
||||
) {
|
||||
|
||||
val insets =
|
||||
|
@ -124,7 +128,7 @@ fun VideoPlayerControllerUI(
|
|||
fullscreen = fullscreen
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(uiVissible){
|
||||
AnimatedVisibility(uiVissible) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(), color = Color(0x75000000)
|
||||
) {
|
||||
|
@ -167,8 +171,11 @@ fun VideoPlayerControllerUI(
|
|||
.defaultMinSize(minHeight = 40.dp)
|
||||
.fillMaxWidth(),
|
||||
isFullscreen = fullscreen,
|
||||
seekPosition,
|
||||
switchToFullscreen,
|
||||
switchToEmbeddedView
|
||||
switchToEmbeddedView,
|
||||
seekPositionChanged,
|
||||
seekingFinished
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -432,8 +439,11 @@ private fun CenterControllButton(
|
|||
private fun BottomUI(
|
||||
modifier: Modifier,
|
||||
isFullscreen: Boolean,
|
||||
seekPosition: Float,
|
||||
switchToFullscreen: () -> Unit,
|
||||
switchToEmbeddedView: () -> Unit
|
||||
switchToEmbeddedView: () -> Unit,
|
||||
seekPositionChanged: (Float) -> Unit,
|
||||
seekingFinished: () -> Unit
|
||||
) {
|
||||
|
||||
Row(
|
||||
|
@ -442,8 +452,17 @@ private fun BottomUI(
|
|||
modifier = modifier
|
||||
) {
|
||||
Text("00:06:45")
|
||||
Slider(value = 0.4F, onValueChange = {}, modifier = Modifier.weight(1F))
|
||||
Text("00:09:40")
|
||||
Seeker(
|
||||
modifier.weight(1F),
|
||||
value = seekPosition,
|
||||
onValueChange = seekPositionChanged,
|
||||
onValueChangeFinished = seekingFinished
|
||||
)
|
||||
|
||||
//Slider(value = 0.4F, onValueChange = {}, modifier = Modifier.weight(1F))
|
||||
|
||||
//Text("00:09:40")
|
||||
|
||||
IconButton(onClick = if (isFullscreen) switchToEmbeddedView else switchToFullscreen) {
|
||||
Icon(
|
||||
imageVector = if (isFullscreen) Icons.Filled.FullscreenExit
|
||||
|
@ -451,6 +470,7 @@ private fun BottomUI(
|
|||
contentDescription = stringResource(R.string.widget_description_toggle_fullscreen)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -482,6 +502,7 @@ fun VideoPlayerControllerUIPreviewEmbeded() {
|
|||
VideoPlayerControllerUI(isPlaying = false,
|
||||
fullscreen = false,
|
||||
uiVissible = true,
|
||||
seekPosition = 0.3F,
|
||||
play = {},
|
||||
pause = {},
|
||||
prevStream = {},
|
||||
|
@ -489,7 +510,9 @@ fun VideoPlayerControllerUIPreviewEmbeded() {
|
|||
switchToFullscreen = {},
|
||||
switchToEmbeddedView = {},
|
||||
showUi = {},
|
||||
hideUi = {})
|
||||
hideUi = {},
|
||||
seekPositionChanged = {},
|
||||
seekingFinished = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -502,6 +525,7 @@ fun VideoPlayerControllerUIPreviewLandscape() {
|
|||
VideoPlayerControllerUI(isPlaying = true,
|
||||
fullscreen = true,
|
||||
uiVissible = true,
|
||||
seekPosition = 0.3F,
|
||||
play = {},
|
||||
pause = {},
|
||||
prevStream = {},
|
||||
|
@ -509,7 +533,9 @@ fun VideoPlayerControllerUIPreviewLandscape() {
|
|||
switchToEmbeddedView = {},
|
||||
switchToFullscreen = {},
|
||||
showUi = {},
|
||||
hideUi = {})
|
||||
hideUi = {},
|
||||
seekPositionChanged = {},
|
||||
seekingFinished = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -523,6 +549,7 @@ fun VideoPlayerControllerUIPreviewPortrait() {
|
|||
isPlaying = false,
|
||||
fullscreen = true,
|
||||
uiVissible = true,
|
||||
seekPosition = 0.3F,
|
||||
play = {},
|
||||
pause = {},
|
||||
prevStream = {},
|
||||
|
@ -530,7 +557,9 @@ fun VideoPlayerControllerUIPreviewPortrait() {
|
|||
switchToEmbeddedView = {},
|
||||
switchToFullscreen = {},
|
||||
showUi = {},
|
||||
hideUi = {})
|
||||
hideUi = {},
|
||||
seekPositionChanged = {},
|
||||
seekingFinished = {})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -118,6 +118,7 @@ fun VideoPlayerUI(
|
|||
isPlaying = uiState.playing,
|
||||
fullscreen = uiState.fullscreen,
|
||||
uiVissible = uiState.uiVissible,
|
||||
seekPosition = uiState.seekerPosition,
|
||||
play = viewModel::play,
|
||||
pause = viewModel::pause,
|
||||
prevStream = viewModel::prevStream,
|
||||
|
@ -125,7 +126,9 @@ fun VideoPlayerUI(
|
|||
switchToFullscreen = viewModel::switchToFullscreen,
|
||||
switchToEmbeddedView = viewModel::switchToEmbeddedView,
|
||||
showUi = viewModel::showUi,
|
||||
hideUi = viewModel::hideUi
|
||||
hideUi = viewModel::hideUi,
|
||||
seekPositionChanged = viewModel::seekPositionChanged,
|
||||
seekingFinished = viewModel::seekingFinished
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,587 @@
|
|||
/*
|
||||
* Copyright 2023 Vivek Singh
|
||||
*
|
||||
* @Author Vivek Singh
|
||||
* @Author Christian Schabesberger
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Original code was taken from: https://github.com/2307vivek/Seeker/
|
||||
*
|
||||
*/
|
||||
|
||||
package net.newpipe.newplayer.ui.seeker
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.MutatePriority
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.indication
|
||||
import androidx.compose.foundation.interaction.Interaction
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.requiredSizeIn
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.progressSemantics
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.inset
|
||||
import androidx.compose.ui.graphics.drawscope.rotateRad
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.semantics.disabled
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.setProgress
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.atan2
|
||||
|
||||
/**
|
||||
* A seekbar/slider with support for read ahead indicator and segments. The segments can be
|
||||
* separated with gaps in between or with their respective colors, or by both.
|
||||
*
|
||||
* Read ahead indicator shows the amount of content which is ready to use.
|
||||
*
|
||||
* @param modifier modifiers for the seeker layout
|
||||
* @param state state for Seeker
|
||||
* @param value current value of the seeker. If outside of [range] provided, value will be
|
||||
* coerced to this range.
|
||||
* @param thumbValue current value of the thumb. This allows the thumb to move independent of the
|
||||
* progress position. If outside of [range] provided, value will be coerced to this range.
|
||||
* @param progressStartPosition starting point of the indicator as a fraction of track width.
|
||||
* The passed value will be clamped between 0 and 1.
|
||||
* @param readAheadValue the read ahead value for seeker. If outside of [range] provided, value will be
|
||||
* coerced to this range.
|
||||
* @param onValueChange lambda in which value should be updated
|
||||
* @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
|
||||
* shouldn't be used to update the slider value (use [onValueChange] for that), but rather to
|
||||
* know when the user has completed selecting a new value by ending a drag or a click.
|
||||
* @param segments a list of [Segment] for seeker. The track will be divided into different parts based
|
||||
* on the provided start values.
|
||||
* The first segment must start form the start value of the [range], and all the segments must lie in
|
||||
* the specified [range], else an [IllegalArgumentException] will be thrown.
|
||||
* will be thrown.
|
||||
* @param enabled whether or not component is enabled and can be interacted with or not
|
||||
* @param colors [SeekerColors] that will be used to determine the color of the Slider parts in
|
||||
* different state. See [SeekerDefaults.seekerColors] to customize.
|
||||
* @param dimensions [SeekerDimensions] that will be used to determine the dimensions of
|
||||
* different Seeker parts in different state. See [SeekerDefaults.seekerDimensions] to customize.
|
||||
* @param interactionSource the [MutableInteractionSource] representing the stream of
|
||||
* [Interaction]s for this Seeker. You can create and pass in your own remembered
|
||||
* [MutableInteractionSource] if you want to observe [Interaction]s and customize the
|
||||
* appearance / behavior of this Seeker in different [Interaction]s.
|
||||
* */
|
||||
@Composable
|
||||
fun Seeker(
|
||||
modifier: Modifier = Modifier,
|
||||
state: SeekerState = rememberSeekerState(),
|
||||
value: Float,
|
||||
thumbValue: Float = value,
|
||||
range: ClosedFloatingPointRange<Float> = 0f..1f,
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
progressStartPosition: Float = 0f,
|
||||
readAheadValue: Float = lerp(range.start, range.endInclusive, progressStartPosition),
|
||||
onValueChange: (Float) -> Unit,
|
||||
onValueChangeFinished: (() -> Unit)? = null,
|
||||
segments: List<Segment> = emptyList(),
|
||||
enabled: Boolean = true,
|
||||
colors: SeekerColors = SeekerDefaults.seekerColors(),
|
||||
dimensions: SeekerDimensions = SeekerDefaults.seekerDimensions(),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
if (segments.isNotEmpty()) {
|
||||
require(segments.first().start == range.start) {
|
||||
"the first segment should start from range start value"
|
||||
}
|
||||
segments.forEach {
|
||||
require(it.start in range) {
|
||||
"segment must start from withing the range."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onValueChangeState by rememberUpdatedState(onValueChange)
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.requiredSizeIn(
|
||||
minHeight = SeekerDefaults.ThumbRippleRadius * 2,
|
||||
minWidth = SeekerDefaults.ThumbRippleRadius * 2
|
||||
)
|
||||
.progressSemantics(value, range, onValueChange, onValueChangeFinished, enabled)
|
||||
.focusable(enabled, interactionSource)
|
||||
) {
|
||||
val thumbRadius by dimensions.thumbRadius()
|
||||
val trackStart: Float
|
||||
val endPx = constraints.maxWidth.toFloat()
|
||||
val widthPx: Float
|
||||
|
||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
|
||||
with(LocalDensity.current) {
|
||||
trackStart = thumbRadius.toPx()
|
||||
widthPx = endPx - (trackStart * 2)
|
||||
}
|
||||
|
||||
val segmentStarts = remember(segments, range, widthPx) {
|
||||
segmentToPxValues(segments, range, widthPx)
|
||||
}
|
||||
|
||||
LaunchedEffect(thumbValue, segments) {
|
||||
state.currentSegment(thumbValue, segments)
|
||||
}
|
||||
|
||||
val valuePx = remember(value, widthPx, range) {
|
||||
valueToPx(value, widthPx, range)
|
||||
}
|
||||
|
||||
val thumbValuePx = remember(thumbValue, widthPx, range) {
|
||||
when (thumbValue) {
|
||||
value -> valuePx // reuse valuePx if thumbValue equal to value
|
||||
else -> valueToPx(thumbValue, widthPx, range)
|
||||
}
|
||||
}
|
||||
|
||||
val readAheadValuePx = remember(readAheadValue, widthPx, range) {
|
||||
valueToPx(readAheadValue, widthPx, range)
|
||||
}
|
||||
|
||||
var dragPositionX by remember { mutableStateOf(0f) }
|
||||
var pressOffset by remember { mutableStateOf(0f) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val draggableState = state.draggableState
|
||||
|
||||
LaunchedEffect(widthPx, range) {
|
||||
state.onDrag = {
|
||||
dragPositionX += it + pressOffset
|
||||
|
||||
pressOffset = 0f
|
||||
onValueChangeState(pxToValue(dragPositionX, widthPx, range))
|
||||
}
|
||||
}
|
||||
|
||||
val press =
|
||||
Modifier.pointerInput(
|
||||
range,
|
||||
widthPx,
|
||||
endPx,
|
||||
isRtl,
|
||||
enabled,
|
||||
thumbRadius,
|
||||
interactionSource
|
||||
) {
|
||||
detectTapGestures(
|
||||
onPress = { position ->
|
||||
dragPositionX = 0f
|
||||
pressOffset =
|
||||
if (!isRtl) position.x - trackStart else (endPx - position.x) - trackStart
|
||||
},
|
||||
onTap = {
|
||||
scope.launch {
|
||||
draggableState.drag(MutatePriority.UserInput) {
|
||||
dragBy(0f)
|
||||
}
|
||||
onValueChangeFinished?.invoke()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val drag = Modifier.draggable(
|
||||
state = draggableState,
|
||||
reverseDirection = isRtl,
|
||||
orientation = Orientation.Horizontal,
|
||||
onDragStopped = {
|
||||
onValueChangeFinished?.invoke()
|
||||
},
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
|
||||
Seeker(
|
||||
modifier = if (enabled) press.then(drag) else Modifier,
|
||||
widthPx = widthPx,
|
||||
valuePx = valuePx,
|
||||
thumbValuePx = thumbValuePx,
|
||||
progressStartPosition = progressStartPosition.coerceIn(0f, 1f),
|
||||
readAheadValuePx = readAheadValuePx,
|
||||
enabled = enabled,
|
||||
segments = segmentStarts,
|
||||
colors = colors,
|
||||
dimensions = dimensions,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Seeker(
|
||||
modifier: Modifier,
|
||||
widthPx: Float,
|
||||
valuePx: Float,
|
||||
thumbValuePx: Float,
|
||||
progressStartPosition: Float,
|
||||
readAheadValuePx: Float,
|
||||
enabled: Boolean,
|
||||
segments: List<SegmentPxs>,
|
||||
colors: SeekerColors,
|
||||
dimensions: SeekerDimensions,
|
||||
interactionSource: MutableInteractionSource
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.defaultSeekerDimensions(dimensions),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Track(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
enabled = enabled,
|
||||
segments = segments,
|
||||
colors = colors,
|
||||
widthPx = widthPx,
|
||||
valuePx = valuePx,
|
||||
progressStartPosition = progressStartPosition,
|
||||
readAheadValuePx = readAheadValuePx,
|
||||
dimensions = dimensions
|
||||
)
|
||||
Thumb(
|
||||
valuePx = { thumbValuePx },
|
||||
dimensions = dimensions,
|
||||
colors = colors,
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Track(
|
||||
modifier: Modifier,
|
||||
enabled: Boolean,
|
||||
segments: List<SegmentPxs>,
|
||||
colors: SeekerColors,
|
||||
widthPx: Float,
|
||||
valuePx: Float,
|
||||
progressStartPosition: Float,
|
||||
readAheadValuePx: Float,
|
||||
dimensions: SeekerDimensions
|
||||
) {
|
||||
val trackColor by colors.trackColor(enabled)
|
||||
val progressColor by colors.progressColor(enabled)
|
||||
val readAheadColor by colors.readAheadColor(enabled)
|
||||
val thumbRadius by dimensions.thumbRadius()
|
||||
val trackHeight by dimensions.trackHeight()
|
||||
val progressHeight by dimensions.progressHeight()
|
||||
val segmentGap by dimensions.gap()
|
||||
|
||||
Canvas(
|
||||
modifier = modifier.graphicsLayer {
|
||||
alpha = 0.99f
|
||||
}
|
||||
) {
|
||||
val isRtl = layoutDirection == LayoutDirection.Rtl
|
||||
val left = thumbRadius.toPx()
|
||||
|
||||
translate(left = left) {
|
||||
if (segments.isEmpty()) {
|
||||
// draw the track with a single line.
|
||||
drawLine(
|
||||
start = Offset(rtlAware(0f, widthPx, isRtl), center.y),
|
||||
end = Offset(rtlAware(widthPx, widthPx, isRtl), center.y),
|
||||
color = trackColor,
|
||||
strokeWidth = trackHeight.toPx(),
|
||||
cap = StrokeCap.Round
|
||||
)
|
||||
} else {
|
||||
// draw segments in their respective color,
|
||||
// excluding gaps (which will be cleared out later)
|
||||
for (index in segments.indices) {
|
||||
val segment = segments[index]
|
||||
val segmentColor = when (segment.color) {
|
||||
Color.Unspecified -> trackColor
|
||||
else -> segment.color
|
||||
}
|
||||
drawSegment(
|
||||
startPx = rtlAware(segment.startPx, widthPx, isRtl),
|
||||
endPx = rtlAware(segment.endPx, widthPx, isRtl),
|
||||
trackColor = segmentColor,
|
||||
trackHeight = trackHeight.toPx(),
|
||||
blendMode = BlendMode.SrcOver,
|
||||
startCap = if (index == 0) StrokeCap.Round else null,
|
||||
endCap = if (index == segments.lastIndex) StrokeCap.Round else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// readAhead indicator
|
||||
drawLine(
|
||||
start = Offset(rtlAware(widthPx * progressStartPosition, widthPx, isRtl), center.y),
|
||||
end = Offset(rtlAware(readAheadValuePx, widthPx, isRtl), center.y),
|
||||
color = readAheadColor,
|
||||
strokeWidth = progressHeight.toPx(),
|
||||
cap = StrokeCap.Round
|
||||
)
|
||||
|
||||
// progress indicator
|
||||
drawLine(
|
||||
start = Offset(rtlAware(widthPx * progressStartPosition, widthPx, isRtl), center.y),
|
||||
end = Offset(rtlAware(valuePx, widthPx, isRtl), center.y),
|
||||
color = progressColor,
|
||||
strokeWidth = progressHeight.toPx(),
|
||||
cap = StrokeCap.Round
|
||||
)
|
||||
|
||||
// clear segment gaps
|
||||
for (index in segments.indices) {
|
||||
if (index == segments.lastIndex) break // skip "gap" after last segment
|
||||
val segment = segments[index]
|
||||
drawGap(
|
||||
startPx = rtlAware(segment.endPx - segmentGap.toPx(), widthPx, isRtl),
|
||||
endPx = rtlAware(segment.endPx, widthPx, isRtl),
|
||||
trackHeight = trackHeight.toPx(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawLine(
|
||||
color: Color,
|
||||
start: Offset,
|
||||
end: Offset,
|
||||
strokeWidth: Float = Stroke.HairlineWidth,
|
||||
startCap: StrokeCap? = null,
|
||||
endCap: StrokeCap? = null,
|
||||
blendMode: BlendMode
|
||||
) {
|
||||
val endOffset = if (endCap != null) {
|
||||
end.copy(x = end.x - strokeWidth)
|
||||
} else {
|
||||
end
|
||||
}
|
||||
inset(horizontal = strokeWidth / 2) {
|
||||
drawLine(
|
||||
color = color,
|
||||
start = start,
|
||||
end = endOffset,
|
||||
strokeWidth = strokeWidth,
|
||||
cap = StrokeCap.Butt,
|
||||
)
|
||||
|
||||
startCap?.let {
|
||||
drawCap(
|
||||
color = color,
|
||||
start = start,
|
||||
end = end,
|
||||
strokeWidth = strokeWidth,
|
||||
cap = it,
|
||||
blendMode = blendMode
|
||||
)
|
||||
}
|
||||
|
||||
endCap?.let {
|
||||
drawCap(
|
||||
color = color,
|
||||
start = endOffset,
|
||||
end = start,
|
||||
strokeWidth = strokeWidth,
|
||||
cap = it,
|
||||
blendMode = blendMode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawCap(
|
||||
color: Color,
|
||||
start: Offset,
|
||||
end: Offset,
|
||||
strokeWidth: Float = Stroke.HairlineWidth,
|
||||
cap: StrokeCap,
|
||||
blendMode: BlendMode
|
||||
) {
|
||||
when (cap) {
|
||||
StrokeCap.Butt -> Unit
|
||||
StrokeCap.Round -> {
|
||||
drawArc(
|
||||
color = color,
|
||||
startAngle = -90f,
|
||||
sweepAngle = 180f,
|
||||
useCenter = true,
|
||||
topLeft = start - Offset(strokeWidth / 2, strokeWidth / 2),
|
||||
size = Size(strokeWidth, strokeWidth),
|
||||
blendMode = blendMode,
|
||||
)
|
||||
}
|
||||
|
||||
StrokeCap.Square -> {
|
||||
val offset = Offset(strokeWidth / 2, strokeWidth / 2)
|
||||
val size = Size(strokeWidth, strokeWidth)
|
||||
|
||||
rotateRad(
|
||||
radians = (end - start).run { atan2(x, y) },
|
||||
pivot = start
|
||||
) {
|
||||
drawRect(color, topLeft = start - offset, size = size, blendMode = blendMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawSegment(
|
||||
startPx: Float,
|
||||
endPx: Float,
|
||||
trackColor: Color,
|
||||
trackHeight: Float,
|
||||
blendMode: BlendMode,
|
||||
startCap: StrokeCap? = null,
|
||||
endCap: StrokeCap? = null
|
||||
) {
|
||||
drawLine(
|
||||
start = Offset(startPx, center.y),
|
||||
end = Offset(endPx, center.y),
|
||||
color = trackColor,
|
||||
strokeWidth = trackHeight,
|
||||
blendMode = blendMode,
|
||||
endCap = endCap,
|
||||
startCap = startCap
|
||||
)
|
||||
}
|
||||
|
||||
private fun DrawScope.drawGap(
|
||||
startPx: Float,
|
||||
endPx: Float,
|
||||
trackHeight: Float,
|
||||
) {
|
||||
drawLine(
|
||||
start = Offset(startPx, center.y),
|
||||
end = Offset(endPx, center.y),
|
||||
color = Color.Black, // any color will do
|
||||
strokeWidth = trackHeight + 2, // add 2 to prevent hairline borders from rounding
|
||||
blendMode = BlendMode.Clear
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Thumb(
|
||||
valuePx: () -> Float,
|
||||
dimensions: SeekerDimensions,
|
||||
colors: SeekerColors,
|
||||
enabled: Boolean,
|
||||
interactionSource: MutableInteractionSource
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.offset {
|
||||
IntOffset(x = valuePx().toInt(), 0)
|
||||
}
|
||||
.indication(
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(
|
||||
bounded = false,
|
||||
radius = SeekerDefaults.ThumbRippleRadius
|
||||
)
|
||||
)
|
||||
.hoverable(interactionSource)
|
||||
.size(dimensions.thumbRadius().value * 2)
|
||||
.clip(CircleShape)
|
||||
.background(colors.thumbColor(enabled = enabled).value)
|
||||
)
|
||||
}
|
||||
|
||||
private fun Modifier.defaultSeekerDimensions(dimensions: SeekerDimensions) = composed {
|
||||
this.then(
|
||||
Modifier
|
||||
.heightIn(
|
||||
max = (dimensions.thumbRadius().value * 2).coerceAtLeast(SeekerDefaults.MinSliderHeight)
|
||||
)
|
||||
.widthIn(
|
||||
min = SeekerDefaults.MinSliderWidth
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun Modifier.progressSemantics(
|
||||
value: Float,
|
||||
range: ClosedFloatingPointRange<Float>,
|
||||
onValueChange: (Float) -> Unit,
|
||||
onValueChangeFinished: (() -> Unit)? = null,
|
||||
enabled: Boolean
|
||||
): Modifier {
|
||||
val coerced = value.coerceIn(range.start, range.endInclusive)
|
||||
return semantics {
|
||||
if (!enabled) disabled()
|
||||
setProgress { targetValue ->
|
||||
val newValue = targetValue.coerceIn(range.start, range.endInclusive)
|
||||
|
||||
if (newValue == coerced) {
|
||||
false
|
||||
} else {
|
||||
onValueChange(newValue)
|
||||
onValueChangeFinished?.invoke()
|
||||
true
|
||||
}
|
||||
}
|
||||
}.progressSemantics(value, range, 0)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SeekerPreview() {
|
||||
val segments = listOf(
|
||||
Segment(name = "Intro", start = 0f),
|
||||
Segment(name = "Talk 1", start = 0.5f),
|
||||
Segment(name = "Talk 2", start = 0.8f),
|
||||
)
|
||||
Seeker(
|
||||
value = 0.7f,
|
||||
range = 0f..1f,
|
||||
segments = segments,
|
||||
onValueChange = { },
|
||||
)
|
||||
}
|
|
@ -0,0 +1,309 @@
|
|||
/*
|
||||
* Copyright 2023 Vivek Singh
|
||||
*
|
||||
* @Author Vivek Singh
|
||||
* @Author Christian Schabesberger
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Original code was taken from: https://github.com/2307vivek/Seeker/
|
||||
*
|
||||
*/
|
||||
package net.newpipe.newplayer.ui.seeker
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.newpipe.newplayer.ui.theme.VideoPlayerColorScheme
|
||||
|
||||
object SeekerDefaults {
|
||||
|
||||
/**
|
||||
* Creates a [SeekerColors] that represents the different colors used in parts of the
|
||||
* [Seeker] in different states.
|
||||
*
|
||||
* @param progressColor color of the progress indicator.
|
||||
* @param trackColor color of the track.
|
||||
* @param disabledProgressColor color of the progress indicator when the Slider is
|
||||
* disabled.
|
||||
* @param disabledTrackColor color of the track when theSlider is disabled.
|
||||
* @param thumbColor thumb color when enabled
|
||||
* @param disabledThumbColor thumb color when disabled.
|
||||
* @param readAheadColor color of the read ahead indicator.
|
||||
*/
|
||||
@Composable
|
||||
fun seekerColors(
|
||||
progressColor: Color = VideoPlayerColorScheme.primary,
|
||||
trackColor: Color = TrackColor,
|
||||
disabledProgressColor: Color = VideoPlayerColorScheme.onSurface.copy(alpha = DisabledProgressAlpha),
|
||||
disabledTrackColor: Color = disabledProgressColor
|
||||
.copy(alpha = DisabledTrackAlpha)
|
||||
.compositeOver(VideoPlayerColorScheme.onSurface),
|
||||
thumbColor: Color = VideoPlayerColorScheme.primary,
|
||||
disabledThumbColor: Color = VideoPlayerColorScheme.onSurface
|
||||
.copy(alpha = 0.0F)
|
||||
.compositeOver(VideoPlayerColorScheme.surface),
|
||||
readAheadColor: Color = ReadAheadColor
|
||||
): SeekerColors = DefaultSeekerColor(
|
||||
progressColor = progressColor,
|
||||
trackColor = trackColor,
|
||||
disabledProgressColor = disabledProgressColor,
|
||||
disabledTrackColor = disabledTrackColor,
|
||||
thumbColor = thumbColor,
|
||||
disabledThumbColor = disabledThumbColor,
|
||||
readAheadColor = readAheadColor
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a [SeekerDimensions] which represents dimension of different parts of [Seeker] in
|
||||
* different states.
|
||||
*
|
||||
* @param trackHeight height of the track.
|
||||
* @param progressHeight height of the progress indicator.
|
||||
* @param thumbRadius radius of the thumb slider.
|
||||
* @param gap gap between two segments in the track.
|
||||
* */
|
||||
@Composable
|
||||
fun seekerDimensions(
|
||||
trackHeight: Dp = TrackHeight,
|
||||
progressHeight: Dp = trackHeight,
|
||||
thumbRadius: Dp = ThumbRadius,
|
||||
gap: Dp = Gap
|
||||
): SeekerDimensions = DefaultSeekerDimensions(
|
||||
trackHeight = trackHeight,
|
||||
progressHeight = progressHeight,
|
||||
thumbRadius = thumbRadius,
|
||||
gap = gap
|
||||
)
|
||||
|
||||
private val TrackColor = Color(0xFFD9D9D9)
|
||||
private val ReadAheadColor = Color(0xFFBDBDBD)
|
||||
|
||||
private const val TrackAlpha = 0.24f
|
||||
private const val ReadAheadAlpha = 0.44f
|
||||
private const val DisabledTrackAlpha = 0.22f
|
||||
private const val DisabledProgressAlpha = 0.32f
|
||||
|
||||
internal val ThumbRadius = 10.dp
|
||||
private val TrackHeight = 4.dp
|
||||
private val Gap = 2.dp
|
||||
|
||||
internal val MinSliderHeight = 48.dp
|
||||
internal val MinSliderWidth = ThumbRadius * 2
|
||||
|
||||
internal val ThumbDefaultElevation = 1.dp
|
||||
internal val ThumbPressedElevation = 6.dp
|
||||
|
||||
internal val ThumbRippleRadius = 24.dp
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the colors used by different parts of [Seeker] in different states.
|
||||
*
|
||||
* See [SeekerDefaults.seekerColors] for default implementation.
|
||||
* */
|
||||
@Stable
|
||||
interface SeekerColors {
|
||||
|
||||
/**
|
||||
* Represents the color used for the seeker's track, depending on [enabled].
|
||||
*
|
||||
* @param enabled whether the [Seeker] is enabled or not
|
||||
*/
|
||||
@Composable
|
||||
fun trackColor(enabled: Boolean): State<Color>
|
||||
|
||||
/**
|
||||
* Represents the color used for the seeker's thumb, depending on [enabled].
|
||||
*
|
||||
* @param enabled whether the [Seeker] is enabled or not
|
||||
*/
|
||||
@Composable
|
||||
fun thumbColor(enabled: Boolean): State<Color>
|
||||
|
||||
/**
|
||||
* Represents the color used for the seeker's progress indicator, depending on [enabled].
|
||||
*
|
||||
* @param enabled whether the [Seeker] is enabled or not
|
||||
*/
|
||||
@Composable
|
||||
fun progressColor(enabled: Boolean): State<Color>
|
||||
|
||||
/**
|
||||
* Represents the color used for the seeker's read ahead indicator, depending on [enabled].
|
||||
*
|
||||
* @param enabled whether the [Seeker] is enabled or not
|
||||
*/
|
||||
@Composable
|
||||
fun readAheadColor(enabled: Boolean): State<Color>
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the dimensions used by different parts of [Seeker] in different states.
|
||||
*
|
||||
* See [SeekerDefaults.seekerDimensions] for default implementation.
|
||||
* */
|
||||
@Stable
|
||||
interface SeekerDimensions {
|
||||
|
||||
/**
|
||||
* Represents the height used for the seeker's track.
|
||||
*/
|
||||
@Composable
|
||||
fun trackHeight(): State<Dp>
|
||||
|
||||
/**
|
||||
* Represents the height used for the seeker's progress indicator.
|
||||
*/
|
||||
@Composable
|
||||
fun progressHeight(): State<Dp>
|
||||
|
||||
/**
|
||||
* Represents the gap used between two segments in seeker's track.
|
||||
*/
|
||||
@Composable
|
||||
fun gap(): State<Dp>
|
||||
|
||||
/**
|
||||
* Represents the radius used for seeker's thumb.
|
||||
*/
|
||||
@Composable
|
||||
fun thumbRadius(): State<Dp>
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal class DefaultSeekerDimensions(
|
||||
val trackHeight: Dp,
|
||||
val progressHeight: Dp,
|
||||
val gap: Dp,
|
||||
val thumbRadius: Dp
|
||||
) : SeekerDimensions {
|
||||
@Composable
|
||||
override fun trackHeight(): State<Dp> {
|
||||
return rememberUpdatedState(trackHeight)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun progressHeight(): State<Dp> {
|
||||
return rememberUpdatedState(progressHeight)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun gap(): State<Dp> {
|
||||
return rememberUpdatedState(gap)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun thumbRadius(): State<Dp> {
|
||||
return rememberUpdatedState(thumbRadius)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as DefaultSeekerDimensions
|
||||
|
||||
if (trackHeight != other.trackHeight) return false
|
||||
if (progressHeight != other.progressHeight) return false
|
||||
if (gap != other.gap) return false
|
||||
if (thumbRadius != other.thumbRadius) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = trackHeight.hashCode()
|
||||
|
||||
result = 31 * result + progressHeight.hashCode()
|
||||
result = 31 * result + gap.hashCode()
|
||||
result = 31 * result + thumbRadius.hashCode()
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal class DefaultSeekerColor(
|
||||
val progressColor: Color,
|
||||
val trackColor: Color,
|
||||
val disabledTrackColor: Color,
|
||||
val disabledProgressColor: Color,
|
||||
val thumbColor: Color,
|
||||
val disabledThumbColor: Color,
|
||||
val readAheadColor: Color
|
||||
) : SeekerColors {
|
||||
@Composable
|
||||
override fun trackColor(enabled: Boolean): State<Color> {
|
||||
return rememberUpdatedState(
|
||||
if (enabled) trackColor else disabledTrackColor
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun thumbColor(enabled: Boolean): State<Color> {
|
||||
return rememberUpdatedState(
|
||||
if (enabled) thumbColor else disabledThumbColor
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun progressColor(enabled: Boolean): State<Color> {
|
||||
return rememberUpdatedState(
|
||||
if (enabled) progressColor else disabledProgressColor
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun readAheadColor(enabled: Boolean): State<Color> {
|
||||
return rememberUpdatedState(
|
||||
readAheadColor
|
||||
)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as DefaultSeekerColor
|
||||
|
||||
if (progressColor != other.progressColor) return false
|
||||
if (trackColor != other.trackColor) return false
|
||||
if (disabledTrackColor != other.disabledTrackColor) return false
|
||||
if (disabledProgressColor != other.disabledProgressColor) return false
|
||||
if (thumbColor != other.thumbColor) return false
|
||||
if (disabledThumbColor != other.disabledThumbColor) return false
|
||||
if (readAheadColor != other.readAheadColor) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = progressColor.hashCode()
|
||||
|
||||
result = 31 * result + trackColor.hashCode()
|
||||
result = 31 * result + disabledTrackColor.hashCode()
|
||||
result = 31 * result + disabledProgressColor.hashCode()
|
||||
result = 31 * result + thumbColor.hashCode()
|
||||
result = 31 * result + disabledThumbColor.hashCode()
|
||||
result = 31 * result + readAheadColor.hashCode()
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright 2023 Vivek Singh
|
||||
*
|
||||
* @Author Vivek Singh
|
||||
* @Author Christian Schabesberger
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Original code was taken from: https://github.com/2307vivek/Seeker/
|
||||
*
|
||||
*/
|
||||
package net.newpipe.newplayer.ui.seeker
|
||||
|
||||
import androidx.compose.foundation.gestures.DraggableState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* A state object which can be hoisted to observe the current segment of Seeker. In most cases this
|
||||
* will be created by [rememberSeekerState]
|
||||
* */
|
||||
@Stable
|
||||
class SeekerState() {
|
||||
|
||||
/**
|
||||
* The current segment corresponding to the current seeker value.
|
||||
* */
|
||||
var currentSegment: Segment by mutableStateOf(Segment.Unspecified)
|
||||
|
||||
internal var onDrag: ((Float) -> Unit)? = null
|
||||
|
||||
internal val draggableState = DraggableState {
|
||||
onDrag?.invoke(it)
|
||||
}
|
||||
|
||||
internal fun currentSegment(
|
||||
value: Float,
|
||||
segments: List<Segment>
|
||||
) = (segments.findLast { value >= it.start } ?: Segment.Unspecified).also { this.currentSegment = it }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SeekerState which will be remembered across compositions.
|
||||
* */
|
||||
@Composable
|
||||
fun rememberSeekerState(): SeekerState = remember {
|
||||
SeekerState()
|
||||
}
|
||||
|
||||
/**
|
||||
* A class to hold information about a segment.
|
||||
* @param name name of the segment
|
||||
* @param start the value at which this segment should start in the track. This should must be in the
|
||||
* range of the Seeker range values.
|
||||
* @param color the color of the segment
|
||||
* */
|
||||
@Immutable
|
||||
data class Segment(
|
||||
val name: String,
|
||||
val start: Float,
|
||||
val color: Color = Color.Unspecified
|
||||
) {
|
||||
companion object {
|
||||
val Unspecified = Segment(name = "", start = 0f)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal data class SegmentPxs(
|
||||
val name: String,
|
||||
val startPx: Float,
|
||||
val endPx: Float,
|
||||
val color: Color
|
||||
)
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright 2023 Vivek Singh
|
||||
*
|
||||
* @Author Vivek Singh
|
||||
* @Author Christian Schabesberger
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Original code was taken from: https://github.com/2307vivek/Seeker/
|
||||
*
|
||||
*/
|
||||
package net.newpipe.newplayer.ui.seeker
|
||||
|
||||
// returns the corresponding position in pixels of progress in the the slider.
|
||||
internal fun valueToPx(
|
||||
value: Float,
|
||||
widthPx: Float,
|
||||
range: ClosedFloatingPointRange<Float>
|
||||
): Float {
|
||||
val rangeSIze = range.endInclusive - range.start
|
||||
val p = value.coerceIn(range.start, range.endInclusive)
|
||||
val progressPercent = (p - range.start) * 100 / rangeSIze
|
||||
return (progressPercent * widthPx / 100)
|
||||
}
|
||||
|
||||
// returns the corresponding progress value for a position in slider
|
||||
internal fun pxToValue(
|
||||
position: Float,
|
||||
widthPx: Float,
|
||||
range: ClosedFloatingPointRange<Float>
|
||||
): Float {
|
||||
val rangeSize = range.endInclusive - range.start
|
||||
val percent = position * 100 / widthPx
|
||||
return ((percent * (rangeSize) / 100) + range.start).coerceIn(
|
||||
range.start,
|
||||
range.endInclusive
|
||||
)
|
||||
}
|
||||
|
||||
// converts the start value of a segment to the corresponding start and end pixel values
|
||||
// at which the segment will start and end on the track.
|
||||
internal fun segmentToPxValues(
|
||||
segments: List<Segment>,
|
||||
range: ClosedFloatingPointRange<Float>,
|
||||
widthPx: Float,
|
||||
): List<SegmentPxs> {
|
||||
|
||||
val rangeSize = range.endInclusive - range.start
|
||||
val sortedSegments = segments.distinct().sortedBy { it.start }
|
||||
val segmentStartPxs = sortedSegments.map { segment ->
|
||||
|
||||
// percent of the start of this segment in the range size
|
||||
val percent = (segment.start - range.start) * 100 / rangeSize
|
||||
val startPx = percent * widthPx / 100
|
||||
startPx
|
||||
}
|
||||
|
||||
return sortedSegments.mapIndexed { index, segment ->
|
||||
val endPx = if (index != sortedSegments.lastIndex) segmentStartPxs[index + 1] else widthPx
|
||||
SegmentPxs(
|
||||
name = segment.name,
|
||||
color = segment.color,
|
||||
startPx = segmentStartPxs[index],
|
||||
endPx = endPx
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun rtlAware(value: Float, widthPx: Float, isRtl: Boolean) =
|
||||
if (isRtl) widthPx - value else value
|
||||
|
||||
internal fun lerp(start: Float, end: Float, fraction: Float): Float {
|
||||
return (1 - fraction) * start + fraction * end
|
||||
}
|
|
@ -25,7 +25,7 @@ import androidx.compose.material3.darkColorScheme
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
private val VideoPlayerColorScheme = darkColorScheme(
|
||||
val VideoPlayerColorScheme = darkColorScheme(
|
||||
primary = video_player_primary,
|
||||
onPrimary = video_player_onPrimary,
|
||||
primaryContainer = video_player_primaryContainer,
|
||||
|
|
|
@ -59,10 +59,9 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
startStreamButton.setOnClickListener {
|
||||
newPlayer.playWhenReady = true
|
||||
newPlayer.setStream(getString(R.string.portrait_video_example))
|
||||
newPlayer.setStream(getString(R.string.ccc_6502_video))
|
||||
}
|
||||
|
||||
|
||||
videoPlayerViewModel.newPlayer = newPlayer
|
||||
|
||||
//videoPlayerViewModel.maxContentRatio = 4F/3F
|
||||
|
|
Loading…
Reference in New Issue