Skip to content

Instantly share code, notes, and snippets.

@alexjlockwood
Last active May 4, 2021 01:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexjlockwood/868ddaa80d8724923ffc3ce7030297c2 to your computer and use it in GitHub Desktop.
Save alexjlockwood/868ddaa80d8724923ffc3ce7030297c2 to your computer and use it in GitHub Desktop.
Example of a TimerButton component that manages its own timer state (video: https://youtu.be/-qbJRDDwB8M)
package com.alexjlockwood.composetimerdemo
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme(if (isSystemInDarkTheme()) darkColors() else lightColors()) {
Surface {
ScreenContent()
}
}
}
}
}
@Composable
private fun ScreenContent() {
val context = LocalContext.current
fun showToast(message: String) = Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
val timerState = rememberTimerState(2000)
suspend fun startTimer() {
try {
timerState.startTimer()
showToast("Timer completed")
} catch (e: CancellationException) {
showToast("Timer cancelled")
}
}
suspend fun stopTimer() = timerState.stopTimer()
// Uncomment this code to auto-start the timer when the UI is first shown
// LaunchedEffect(Unit) {
// startTimer()
// }
val scope = rememberCoroutineScope()
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Box(
modifier = Modifier.fillMaxWidth().weight(1f),
contentAlignment = Alignment.Center,
) {
TimerButton(
state = timerState,
onClick = { /* ... */ },
) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null,
)
}
}
Text(text = "Timer running: ${timerState.isRunning}")
Text(text = "Timer value: ${(timerState.timerValue * 100).roundToInt() / 100f}")
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
modifier = Modifier.weight(1f),
onClick = { scope.launch { startTimer() } },
) {
Text("Start")
}
Button(
modifier = Modifier.weight(1f),
onClick = { scope.launch { stopTimer() } },
) {
Text("Stop")
}
}
}
}
package com.alexjlockwood.composetimerdemo
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
@Stable
class TimerState internal constructor(
private val durationMillis: Int,
) {
private var timer = Animatable(0f)
val isRunning: Boolean get() = timer.isRunning
val timerValue: Float get() = timer.value
suspend fun startTimer() {
timer.snapTo(0f)
timer.animateTo(
targetValue = 1f,
animationSpec = tween(durationMillis = durationMillis, easing = LinearEasing),
)
}
suspend fun stopTimer() {
timer.snapTo(0f)
}
}
@Composable
fun rememberTimerState(durationMillis: Int): TimerState {
return remember { TimerState(durationMillis = durationMillis) }
}
@Composable
fun TimerButton(
state: TimerState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit,
) {
val timerColor = MaterialTheme.colors.primary
Surface(
modifier = modifier
.sizeIn(minWidth = 64.dp, minHeight = 64.dp)
.drawWithContent {
drawContent()
drawTimerProgress(timerColor, state.timerValue)
},
shape = CircleShape,
border = BorderStroke(1.dp, MaterialTheme.colors.onSurface),
) {
Box(
modifier = Modifier.clickable(onClick = onClick, role = Role.Button),
contentAlignment = Alignment.Center,
content = content,
)
}
}
private fun ContentDrawScope.drawTimerProgress(color: Color, timerValue: Float) {
if (timerValue == 0f) return
val strokeWidth = 3.dp.toPx()
drawArc(
color = color,
topLeft = Offset(strokeWidth / 2f, strokeWidth / 2f),
size = Size(size.width - strokeWidth, size.height - strokeWidth),
startAngle = 270f,
sweepAngle = 360f * timerValue,
useCenter = false,
style = Stroke(width = strokeWidth),
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment