demo: add loading spinner to screens

This commit is contained in:
Oscar Mira 2023-10-25 11:20:24 +02:00
parent e022a27f04
commit 45edf35d83
3 changed files with 192 additions and 2 deletions

View File

@ -1,7 +1,9 @@
package im.molly.monero.demo.ui package im.molly.monero.demo.ui
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@ -12,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import im.molly.monero.demo.ui.component.LoadingWheel
import im.molly.monero.demo.ui.component.Toolbar import im.molly.monero.demo.ui.component.Toolbar
@Composable @Composable
@ -66,10 +69,16 @@ private fun HomeScreen(
private fun LazyListScope.walletCards( private fun LazyListScope.walletCards(
walletListUiState: WalletListUiState, walletListUiState: WalletListUiState,
onWalletClick: (Long) -> Unit, onWalletClick: (Long) -> Unit,
modifier: Modifier = Modifier,
) { ) {
when (walletListUiState) { when (walletListUiState) {
WalletListUiState.Loading -> item { WalletListUiState.Loading -> item {
Text(text = "Loading wallet list...") // TODO LoadingWheel(
modifier = modifier
.fillMaxWidth()
.wrapContentSize(),
contentDesc = "Loading wallets",
)
} }
is WalletListUiState.Loaded -> { is WalletListUiState.Loaded -> {

View File

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -14,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import im.molly.monero.demo.ui.component.LoadingWheel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -77,7 +79,12 @@ fun WalletCardError() {
@Composable @Composable
fun WalletCardLoading() { fun WalletCardLoading() {
Text(text = "Loading wallet...") // TODO LoadingWheel(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(),
contentDesc = "Loading wallet",
)
} }
//@Preview //@Preview

View File

@ -0,0 +1,174 @@
/*
* Copyright 2022 The Android Open Source Project
*
* 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.
*/
package im.molly.monero.demo.ui.component
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.StartOffset
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import im.molly.monero.demo.ui.theme.AppTheme
import kotlinx.coroutines.launch
@Composable
fun LoadingWheel(
contentDesc: String,
modifier: Modifier = Modifier,
) {
val infiniteTransition = rememberInfiniteTransition(label = "wheel transition")
// Specifies the float animation for slowly drawing out the lines on entering
val startValue = if (LocalInspectionMode.current) 0F else 1F
val floatAnimValues = (0 until NUM_OF_LINES).map { remember { Animatable(startValue) } }
LaunchedEffect(floatAnimValues) {
(0 until NUM_OF_LINES).map { index ->
launch {
floatAnimValues[index].animateTo(
targetValue = 0F,
animationSpec = tween(
durationMillis = 100,
easing = FastOutSlowInEasing,
delayMillis = 40 * index,
),
)
}
}
}
// Specifies the rotation animation of the entire Canvas composable
val rotationAnim by infiniteTransition.animateFloat(
initialValue = 0F,
targetValue = 360F,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = ROTATION_TIME, easing = LinearEasing),
),
label = "wheel rotation animation",
)
// Specifies the color animation for the base-to-progress line color change
val baseLineColor = MaterialTheme.colorScheme.onBackground
val progressLineColor = MaterialTheme.colorScheme.inversePrimary
val colorAnimValues = (0 until NUM_OF_LINES).map { index ->
infiniteTransition.animateColor(
initialValue = baseLineColor,
targetValue = baseLineColor,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = ROTATION_TIME / 2
progressLineColor at ROTATION_TIME / NUM_OF_LINES / 2 with LinearEasing
baseLineColor at ROTATION_TIME / NUM_OF_LINES with LinearEasing
},
repeatMode = RepeatMode.Restart,
initialStartOffset = StartOffset(ROTATION_TIME / NUM_OF_LINES / 2 * index),
),
label = "wheel color animation",
)
}
// Draws out the LoadingWheel Canvas composable and sets the animations
Canvas(
modifier = modifier
.size(48.dp)
.padding(8.dp)
.graphicsLayer { rotationZ = rotationAnim }
.semantics { contentDescription = contentDesc }
.testTag("loadingWheel"),
) {
repeat(NUM_OF_LINES) { index ->
rotate(degrees = index * 30f) {
drawLine(
color = colorAnimValues[index].value,
// Animates the initially drawn 1 pixel alpha from 0 to 1
alpha = if (floatAnimValues[index].value < 1f) 1f else 0f,
strokeWidth = 4F,
cap = StrokeCap.Round,
start = Offset(size.width / 2, size.height / 4),
end = Offset(size.width / 2, floatAnimValues[index].value * size.height / 4),
)
}
}
}
}
@Composable
fun OverlayLoadingWheel(
contentDesc: String,
modifier: Modifier = Modifier,
) {
Surface(
shape = RoundedCornerShape(60.dp),
shadowElevation = 8.dp,
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.83f),
modifier = modifier
.size(60.dp),
) {
LoadingWheel(
contentDesc = contentDesc,
)
}
}
@Preview
@Composable
fun LoadingWheelPreview() {
AppTheme {
Surface {
LoadingWheel(contentDesc = "LoadingWheel")
}
}
}
@Preview
@Composable
fun OverlayLoadingWheelPreview() {
AppTheme {
Surface {
OverlayLoadingWheel(contentDesc = "LoadingWheel")
}
}
}
private const val ROTATION_TIME = 12000
private const val NUM_OF_LINES = 12