android/ui: add game of life to show progress when connecting (ENG-2860) (#244)

Fixes ENG-2860

Adds a game of life animation with the Tailscale logo when launching the app and waiting for the VPN tunnel to be established.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
pull/243/head
Andrea Gottardo 2 months ago committed by GitHub
parent bf74edd551
commit 3fea68ef2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -120,6 +120,7 @@ fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewMode
onNavigateToPeerDetails = navigation.onNavigateToPeerDetails,
onSearch = { viewModel.searchPeers(it) })
}
Ipn.State.NoState,
Ipn.State.Starting -> StartingView()
else ->
ConnectView(
@ -208,15 +209,11 @@ fun SettingsButton(user: IpnLocal.LoginProfile?, action: () -> Unit) {
@Composable
fun StartingView() {
// (jonathan) TODO: On iOS this is the game-of-life Tailscale animation.
Column(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.secondaryContainer),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = stringResource(id = R.string.starting),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary)
TailscaleLogoView(animated = true, Modifier.size(72.dp))
}
}
@ -266,7 +263,7 @@ fun ConnectView(
fontSize = MaterialTheme.typography.titleMedium.fontSize)
}
} else {
TailscaleLogoView(Modifier.size(50.dp))
TailscaleLogoView(modifier = Modifier.size(50.dp))
Spacer(modifier = Modifier.size(1.dp))
Text(
text = stringResource(id = R.string.welcome_to_tailscale),

@ -11,53 +11,126 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.tailscale.ipn.ui.util.set
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlin.concurrent.timer
// TODO(angott):
// - Implement game-of-life animation for progress indicator.
// - Remove hardcoded dots, use a for-each and make it dynamically
// use the space available instead of unit = 10.dp
// DotsMatrix represents the state of the progress indicator.
typealias DotsMatrix = List<List<Boolean>>
// The initial DotsMatrix that represents the Tailscale logo (T-shaped).
val logoDotsMatrix: DotsMatrix =
listOf(
listOf(false, false, false),
listOf(true, true, true),
listOf(false, true, false),
)
@Composable
fun TailscaleLogoView(modifier: Modifier) {
val primaryColor: Color = MaterialTheme.colorScheme.primary
val secondaryColor: Color = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
fun TailscaleLogoView(animated: Boolean = false, modifier: Modifier) {
val primaryColor: Color = MaterialTheme.colorScheme.secondary
val secondaryColor: Color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f)
val currentDotsMatrix: StateFlow<DotsMatrix> = MutableStateFlow(logoDotsMatrix)
var currentDotsMatrixIndex = 0
fun advanceToNextMatrix() {
currentDotsMatrixIndex = (currentDotsMatrixIndex + 1) % gameOfLife.size
val newMatrix =
if (animated) {
gameOfLife[currentDotsMatrixIndex]
} else {
logoDotsMatrix
}
currentDotsMatrix.set(newMatrix)
}
if (animated) {
timer(period = 300L) { advanceToNextMatrix() }
}
@Composable
fun EnabledDot(modifier: Modifier) {
Canvas(modifier = modifier, onDraw = { drawCircle(primaryColor) })
}
@Composable
fun DisabledDot(modifier: Modifier) {
Canvas(modifier = modifier, onDraw = { drawCircle(secondaryColor) })
}
BoxWithConstraints(modifier) {
val currentMatrix = currentDotsMatrix.collectAsState().value
Column(verticalArrangement = Arrangement.spacedBy(this@BoxWithConstraints.maxWidth.div(8))) {
Row(horizontalArrangement = Arrangement.spacedBy(this@BoxWithConstraints.maxWidth.div(8))) {
Canvas(
modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)),
onDraw = { drawCircle(color = secondaryColor) })
Canvas(
modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)),
onDraw = { drawCircle(color = secondaryColor) })
Canvas(
modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)),
onDraw = { drawCircle(color = secondaryColor) })
}
Row(horizontalArrangement = Arrangement.spacedBy(this@BoxWithConstraints.maxWidth.div(8))) {
Canvas(
modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)),
onDraw = { drawCircle(color = primaryColor) })
Canvas(
modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)),
onDraw = { drawCircle(color = primaryColor) })
Canvas(
modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)),
onDraw = { drawCircle(color = primaryColor) })
}
Row(horizontalArrangement = Arrangement.spacedBy(this@BoxWithConstraints.maxWidth.div(8))) {
Canvas(
modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)),
onDraw = { drawCircle(color = secondaryColor) })
Canvas(
modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)),
onDraw = { drawCircle(color = primaryColor) })
Canvas(
modifier = Modifier.size(this@BoxWithConstraints.maxWidth.div(4)),
onDraw = { drawCircle(color = secondaryColor) })
for (y in 0..2) {
Row(horizontalArrangement = Arrangement.spacedBy(this@BoxWithConstraints.maxWidth.div(8))) {
for (x in 0..2) {
if (currentMatrix[y][x]) {
EnabledDot(Modifier.size(this@BoxWithConstraints.maxWidth.div(4)))
} else {
DisabledDot(Modifier.size(this@BoxWithConstraints.maxWidth.div(4)))
}
}
}
}
}
}
}
val gameOfLife: List<DotsMatrix> =
listOf(
listOf(
listOf(false, true, true),
listOf(true, false, true),
listOf(false, false, true),
),
listOf(
listOf(false, true, true),
listOf(false, false, true),
listOf(false, true, false),
),
listOf(
listOf(false, true, true),
listOf(false, false, false),
listOf(false, false, true),
),
listOf(
listOf(false, false, true),
listOf(false, true, false),
listOf(false, false, false),
),
listOf(
listOf(false, true, false),
listOf(false, false, false),
listOf(false, false, false),
),
listOf(
listOf(false, false, false),
listOf(false, false, true),
listOf(false, false, false),
),
listOf(
listOf(false, false, false),
listOf(false, false, false),
listOf(false, false, false),
),
listOf(
listOf(false, false, true),
listOf(false, false, false),
listOf(false, false, false),
),
listOf(
listOf(false, false, false),
listOf(false, false, false),
listOf(true, false, false),
),
listOf(listOf(false, false, false), listOf(false, false, false), listOf(true, true, false)),
listOf(listOf(false, false, false), listOf(true, false, false), listOf(true, true, false)),
listOf(listOf(false, false, false), listOf(true, true, false), listOf(false, true, false)),
listOf(listOf(false, false, false), listOf(true, true, false), listOf(false, true, true)),
listOf(listOf(false, false, false), listOf(true, true, true), listOf(false, false, true)),
listOf(listOf(false, true, false), listOf(true, true, true), listOf(true, false, true)))

Loading…
Cancel
Save