如何使用 Jetpack Compose 动画创建定时 Instagram 故事加载栏?
How to create timed Instagram story loading bar using Jetpack Compose animation?
我想创建一个与 Instagram 故事加载栏非常相似的可组合组件,持续时间为 10 秒。
我有一个想法如何去做,但我不知道如何执行。我正在考虑使用 BOX 添加一个静态栏(灰色),然后添加另一个栏(白色),它在 10 秒内从 0 动画到可组合宽度的最终宽度。
你知道我如何实现这个组件吗?
您可以为 LinearProgressIndicator
的进度设置动画。
类似于:
var enabled by remember { mutableStateOf(false) }
val progress: Float by animateFloatAsState(
if (enabled) 1f else 0.0f,
animationSpec = tween(
durationMillis = 10000,
delayMillis = 40,
easing = LinearOutSlowInEasing
)
)
LinearProgressIndicator(
color = White,
backgroundColor = LightGray,
progress = progress,
modifier = Modifier.width(100.dp)
)
只需要设置enabled=true
就可以开始播放动画(只是一个例子)
您可以使用此 Composable 来创建分段进度条
private const val BackgroundOpacity = 0.25f
private const val NumberOfSegments = 8
private val StrokeWidth = 4.dp
private val SegmentGap = 8.dp
@Composable
fun SegmentedProgressIndicator(
/*@FloatRange(from = 0.0, to = 1.0)*/
progress: Float,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.primary,
backgroundColor: Color = color.copy(alpha = BackgroundOpacity),
strokeWidth: Dp = StrokeWidth,
numberOfSegments: Int = NumberOfSegments,
segmentGap: Dp = SegmentGap
) {
val gap: Float
val stroke: Float
with(LocalDensity.current) {
gap = segmentGap.toPx()
stroke = strokeWidth.toPx()
}
Canvas(
modifier
.progressSemantics(progress)
.fillMaxWidth()
.height(strokeWidth)
.focusable()
) {
drawSegments(1f, backgroundColor, stroke, numberOfSegments, gap)
drawSegments(progress, color, stroke, numberOfSegments, gap)
}
}
private fun DrawScope.drawSegments(
progress: Float,
color: Color,
strokeWidth: Float,
segments: Int,
segmentGap: Float,
) {
val width = size.width
val start = 0f
val gaps = (segments - 1) * segmentGap
val segmentWidth = (width - gaps) / segments
val barsWidth = segmentWidth * segments
val end = barsWidth * progress + (progress * segments).toInt()* segmentGap
repeat(segments) { index ->
val offset = index * (segmentWidth + segmentGap)
if (offset < end) {
val barEnd = (offset + segmentWidth).coerceAtMost(end)
drawLine(
color,
Offset(start + offset, 0f),
Offset(barEnd, 0f),
strokeWidth
)
}
}
}
你这样用
var running by remember { mutableStateOf(false) }
val progress: Float by animateFloatAsState(
if (running) 1f else 0f,
animationSpec = tween(
durationMillis = 10_000,
easing = LinearEasing
)
)
Surface(color = MaterialTheme.colors.background) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
SegmentedProgressIndicator(
progress = progress,
modifier = Modifier
.padding(top = 64.dp, start = 32.dp, end = 32.dp)
.fillMaxWidth(),
)
Button(
onClick = { running = !running },
modifier = Modifier.padding(top = 32.dp)
) {
Text(
text = if (running) "Reverse Animation" else "Start Animation"
)
}
}
}
这是结果
试试这个解决方案,它应该有效:
var progress by remember {mutableStateOf(0.0f)}
var enabled by remember { mutableStateOf(true) }
LaunchedEffect(key1 = progress, key2 = enabled) {
if(progress<1 && enabled) {
delay(100L)
progress += 0.01F
}
}
LinearProgressIndicator(
color = White,
backgroundColor = LightGray,
progress = progress,
modifier = Modifier.width(100.dp)
)
这是一个完整的示例解决方案,您可以查看并尝试扩展和重构。代码是self-explanatory,你会看到它是怎么回事。
故事可组合:
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun Story(story: Stories) {
AnimatedContent(story) {
when (story) {
Stories.ONE -> {
StoryContent("1")
}
Stories.TWO -> {
StoryContent("2")
}
Stories.THREE -> {
StoryContent("3")
}
}
}
}
@Composable
private fun StoryContent(content: String) {
Text("Story $content")
}
故事进度指示器:
@Composable
fun StoryProgressIndicator(running: Boolean, modifier: Modifier = Modifier, onTenSecondsOnThisStory: () -> Unit) {
val progress: Float by animateFloatAsState(
if (running) 1f else 0f,
animationSpec = tween(
durationMillis = if (running) 10_000 else 0,
easing = LinearEasing
)
)
if (progress == 1f) {
onTenSecondsOnThisStory()
}
LinearProgressIndicator(
progress, modifier
)
}
以及包含故事导航和播放动画的屏幕。
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun InstagramLikeStories() {
var screenWidth by remember { mutableStateOf(1) }
var currentStory by remember { mutableStateOf(Stories.ONE) }
var currentStoryPointer by remember { mutableStateOf(0) }
var runningStoryOne by remember { mutableStateOf(false) }
var runningStoryTwo by remember { mutableStateOf(false) }
var runningStoryThree by remember { mutableStateOf(false) }
val runStoryOne = { runningStoryOne = true; runningStoryTwo = false; runningStoryThree = false }
val runStoryTwo = { runningStoryOne = false; runningStoryTwo = true; runningStoryThree = false }
val runStoryThree = { runningStoryOne = false; runningStoryTwo = false; runningStoryThree = true }
val stories = Stories.values()
LaunchedEffect(Unit) { runStoryOne() }
Column(
Modifier.fillMaxSize().onGloballyPositioned {
screenWidth = it.size.width
}.pointerInput(Unit) {
detectTapGestures(onTap = {
if ((it.x / screenWidth) * 100 > 50) {
if (currentStoryPointer == stories.size - 1) {
currentStoryPointer = 0
} else {
currentStoryPointer++
}
currentStory = stories[currentStoryPointer]
} else {
if (currentStoryPointer != 0) {
currentStoryPointer--
currentStory = stories[currentStoryPointer]
}
}
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
})
}
) {
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
StoryProgressIndicator(runningStoryOne, Modifier.weight(1f), onTenSecondsOnThisStory = {
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
currentStoryPointer = 1
currentStory = stories[currentStoryPointer]
})
Spacer(Modifier.weight(0.1f))
StoryProgressIndicator(runningStoryTwo, Modifier.weight(1f), onTenSecondsOnThisStory = {
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
currentStoryPointer = 2
currentStory = stories[currentStoryPointer]
})
Spacer(Modifier.weight(0.1f))
StoryProgressIndicator(runningStoryThree, Modifier.weight(1f), onTenSecondsOnThisStory = {
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
// go to first one
currentStoryPointer = 0
currentStory = stories[currentStoryPointer]
})
}
}
Story(currentStory)
Row {
Button(onClick = {
}) {
Text("button")
}
}
}
private fun runStoryIndicator(
currentStory: Stories,
runStoryOne: () -> Unit,
runStoryTwo: () -> Unit,
runStoryThree: () -> Unit
) {
when (currentStory) {
Stories.ONE -> runStoryOne()
Stories.TWO -> runStoryTwo()
Stories.THREE -> runStoryThree()
}
}
enum class Stories {
ONE, TWO, THREE
}
我想创建一个与 Instagram 故事加载栏非常相似的可组合组件,持续时间为 10 秒。
我有一个想法如何去做,但我不知道如何执行。我正在考虑使用 BOX 添加一个静态栏(灰色),然后添加另一个栏(白色),它在 10 秒内从 0 动画到可组合宽度的最终宽度。
你知道我如何实现这个组件吗?
您可以为 LinearProgressIndicator
的进度设置动画。
类似于:
var enabled by remember { mutableStateOf(false) }
val progress: Float by animateFloatAsState(
if (enabled) 1f else 0.0f,
animationSpec = tween(
durationMillis = 10000,
delayMillis = 40,
easing = LinearOutSlowInEasing
)
)
LinearProgressIndicator(
color = White,
backgroundColor = LightGray,
progress = progress,
modifier = Modifier.width(100.dp)
)
只需要设置enabled=true
就可以开始播放动画(只是一个例子)
您可以使用此 Composable 来创建分段进度条
private const val BackgroundOpacity = 0.25f
private const val NumberOfSegments = 8
private val StrokeWidth = 4.dp
private val SegmentGap = 8.dp
@Composable
fun SegmentedProgressIndicator(
/*@FloatRange(from = 0.0, to = 1.0)*/
progress: Float,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.primary,
backgroundColor: Color = color.copy(alpha = BackgroundOpacity),
strokeWidth: Dp = StrokeWidth,
numberOfSegments: Int = NumberOfSegments,
segmentGap: Dp = SegmentGap
) {
val gap: Float
val stroke: Float
with(LocalDensity.current) {
gap = segmentGap.toPx()
stroke = strokeWidth.toPx()
}
Canvas(
modifier
.progressSemantics(progress)
.fillMaxWidth()
.height(strokeWidth)
.focusable()
) {
drawSegments(1f, backgroundColor, stroke, numberOfSegments, gap)
drawSegments(progress, color, stroke, numberOfSegments, gap)
}
}
private fun DrawScope.drawSegments(
progress: Float,
color: Color,
strokeWidth: Float,
segments: Int,
segmentGap: Float,
) {
val width = size.width
val start = 0f
val gaps = (segments - 1) * segmentGap
val segmentWidth = (width - gaps) / segments
val barsWidth = segmentWidth * segments
val end = barsWidth * progress + (progress * segments).toInt()* segmentGap
repeat(segments) { index ->
val offset = index * (segmentWidth + segmentGap)
if (offset < end) {
val barEnd = (offset + segmentWidth).coerceAtMost(end)
drawLine(
color,
Offset(start + offset, 0f),
Offset(barEnd, 0f),
strokeWidth
)
}
}
}
你这样用
var running by remember { mutableStateOf(false) }
val progress: Float by animateFloatAsState(
if (running) 1f else 0f,
animationSpec = tween(
durationMillis = 10_000,
easing = LinearEasing
)
)
Surface(color = MaterialTheme.colors.background) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
SegmentedProgressIndicator(
progress = progress,
modifier = Modifier
.padding(top = 64.dp, start = 32.dp, end = 32.dp)
.fillMaxWidth(),
)
Button(
onClick = { running = !running },
modifier = Modifier.padding(top = 32.dp)
) {
Text(
text = if (running) "Reverse Animation" else "Start Animation"
)
}
}
}
这是结果
试试这个解决方案,它应该有效:
var progress by remember {mutableStateOf(0.0f)}
var enabled by remember { mutableStateOf(true) }
LaunchedEffect(key1 = progress, key2 = enabled) {
if(progress<1 && enabled) {
delay(100L)
progress += 0.01F
}
}
LinearProgressIndicator(
color = White,
backgroundColor = LightGray,
progress = progress,
modifier = Modifier.width(100.dp)
)
这是一个完整的示例解决方案,您可以查看并尝试扩展和重构。代码是self-explanatory,你会看到它是怎么回事。
故事可组合:
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun Story(story: Stories) {
AnimatedContent(story) {
when (story) {
Stories.ONE -> {
StoryContent("1")
}
Stories.TWO -> {
StoryContent("2")
}
Stories.THREE -> {
StoryContent("3")
}
}
}
}
@Composable
private fun StoryContent(content: String) {
Text("Story $content")
}
故事进度指示器:
@Composable
fun StoryProgressIndicator(running: Boolean, modifier: Modifier = Modifier, onTenSecondsOnThisStory: () -> Unit) {
val progress: Float by animateFloatAsState(
if (running) 1f else 0f,
animationSpec = tween(
durationMillis = if (running) 10_000 else 0,
easing = LinearEasing
)
)
if (progress == 1f) {
onTenSecondsOnThisStory()
}
LinearProgressIndicator(
progress, modifier
)
}
以及包含故事导航和播放动画的屏幕。
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun InstagramLikeStories() {
var screenWidth by remember { mutableStateOf(1) }
var currentStory by remember { mutableStateOf(Stories.ONE) }
var currentStoryPointer by remember { mutableStateOf(0) }
var runningStoryOne by remember { mutableStateOf(false) }
var runningStoryTwo by remember { mutableStateOf(false) }
var runningStoryThree by remember { mutableStateOf(false) }
val runStoryOne = { runningStoryOne = true; runningStoryTwo = false; runningStoryThree = false }
val runStoryTwo = { runningStoryOne = false; runningStoryTwo = true; runningStoryThree = false }
val runStoryThree = { runningStoryOne = false; runningStoryTwo = false; runningStoryThree = true }
val stories = Stories.values()
LaunchedEffect(Unit) { runStoryOne() }
Column(
Modifier.fillMaxSize().onGloballyPositioned {
screenWidth = it.size.width
}.pointerInput(Unit) {
detectTapGestures(onTap = {
if ((it.x / screenWidth) * 100 > 50) {
if (currentStoryPointer == stories.size - 1) {
currentStoryPointer = 0
} else {
currentStoryPointer++
}
currentStory = stories[currentStoryPointer]
} else {
if (currentStoryPointer != 0) {
currentStoryPointer--
currentStory = stories[currentStoryPointer]
}
}
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
})
}
) {
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
StoryProgressIndicator(runningStoryOne, Modifier.weight(1f), onTenSecondsOnThisStory = {
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
currentStoryPointer = 1
currentStory = stories[currentStoryPointer]
})
Spacer(Modifier.weight(0.1f))
StoryProgressIndicator(runningStoryTwo, Modifier.weight(1f), onTenSecondsOnThisStory = {
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
currentStoryPointer = 2
currentStory = stories[currentStoryPointer]
})
Spacer(Modifier.weight(0.1f))
StoryProgressIndicator(runningStoryThree, Modifier.weight(1f), onTenSecondsOnThisStory = {
runStoryIndicator(currentStory, runStoryOne, runStoryTwo, runStoryThree)
// go to first one
currentStoryPointer = 0
currentStory = stories[currentStoryPointer]
})
}
}
Story(currentStory)
Row {
Button(onClick = {
}) {
Text("button")
}
}
}
private fun runStoryIndicator(
currentStory: Stories,
runStoryOne: () -> Unit,
runStoryTwo: () -> Unit,
runStoryThree: () -> Unit
) {
when (currentStory) {
Stories.ONE -> runStoryOne()
Stories.TWO -> runStoryTwo()
Stories.THREE -> runStoryThree()
}
}
enum class Stories {
ONE, TWO, THREE
}