diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index b1d5e6a..97d1522 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -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), diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TailscaleLogoView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TailscaleLogoView.kt index 734c69f..b655670 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/TailscaleLogoView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TailscaleLogoView.kt @@ -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> + +// 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 = 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 = + 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)))