diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/LoadingWheel.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/LoadingWheel.kt new file mode 100644 index 000000000..fb0b31be6 --- /dev/null +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/LoadingWheel.kt @@ -0,0 +1,146 @@ +/* + * 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 com.google.samples.apps.nowinandroid.core.ui + +import android.content.res.Configuration +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.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.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme +import kotlinx.coroutines.launch + +@Composable +fun LoadingWheel( + contentDesc: String, + modifier: Modifier = Modifier +) { + val infiniteTransition = rememberInfiniteTransition() + + // Specifies the float animation for slowly drawing out the lines on entering + val floatAnimValues = (0 until NUM_OF_LINES).map { remember { Animatable(1F) } } + 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) + ) + ) + + // 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) + ) + ) + } + + // 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 } + ) { + 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) + ) + } + } + } +} + +@Preview( + name = "Loading Wheel Light Preview", + uiMode = Configuration.UI_MODE_NIGHT_NO, +) +@Preview( + name = "Loading Wheel Dark Preview", + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +@Composable +fun LoadingWheelPreview() { + NiaTheme { + Surface { + LoadingWheel(contentDesc = "LoadingWheel") + } + } +} + +private const val ROTATION_TIME = 12000 +private const val NUM_OF_LINES = 12 diff --git a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NiaScreens.kt b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NiaScreens.kt index 5906ddede..a3553ad65 100644 --- a/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NiaScreens.kt +++ b/core-ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NiaScreens.kt @@ -18,17 +18,13 @@ package com.google.samples.apps.nowinandroid.core.ui import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Search import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material.ripple.RippleTheme -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -38,8 +34,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight @Composable @@ -74,22 +68,6 @@ fun NiaToolbar( } } -@Composable -fun NiaLoadingIndicator( - modifier: Modifier = Modifier, - contentDesc: String -) { - Box( - modifier = modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center) - ) { - CircularProgressIndicator( - modifier = Modifier.semantics { contentDescription = contentDesc }, - ) - } -} - object ClearRippleTheme : RippleTheme { @Composable diff --git a/feature-following/src/androidTest/java/com/google/samples/apps/nowinandroid/following/FollowingScreenTest.kt b/feature-following/src/androidTest/java/com/google/samples/apps/nowinandroid/following/FollowingScreenTest.kt index 0e7ea1a29..bffc281b9 100644 --- a/feature-following/src/androidTest/java/com/google/samples/apps/nowinandroid/following/FollowingScreenTest.kt +++ b/feature-following/src/androidTest/java/com/google/samples/apps/nowinandroid/following/FollowingScreenTest.kt @@ -65,7 +65,7 @@ class InterestsScreenTest { } @Test - fun niaLoadingIndicator_inTopics_whenScreenIsLoading_showLoading() { + fun niaLoadingWheel_inTopics_whenScreenIsLoading_showLoading() { composeTestRule.setContent { InterestsScreen(uiState = FollowingUiState.Loading, tabIndex = 0) } @@ -76,7 +76,7 @@ class InterestsScreenTest { } @Test - fun niaLoadingIndicator_inAuthors_whenScreenIsLoading_showLoading() { + fun niaLoadingWheel_inAuthors_whenScreenIsLoading_showLoading() { composeTestRule.setContent { InterestsScreen(uiState = FollowingUiState.Loading, tabIndex = 1) } diff --git a/feature-following/src/main/java/com/google/samples/apps/nowinandroid/feature/following/FollowingScreen.kt b/feature-following/src/main/java/com/google/samples/apps/nowinandroid/feature/following/FollowingScreen.kt index 83ae7c4a1..4ea98253b 100644 --- a/feature-following/src/main/java/com/google/samples/apps/nowinandroid/feature/following/FollowingScreen.kt +++ b/feature-following/src/main/java/com/google/samples/apps/nowinandroid/feature/following/FollowingScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.google.samples.apps.nowinandroid.core.ui.NiaLoadingIndicator +import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel import com.google.samples.apps.nowinandroid.core.ui.NiaToolbar import com.google.samples.apps.nowinandroid.core.ui.component.NiaTab import com.google.samples.apps.nowinandroid.core.ui.component.NiaTabRow @@ -72,7 +72,7 @@ fun FollowingScreen( NiaToolbar(titleRes = R.string.interests) when (uiState) { FollowingUiState.Loading -> - NiaLoadingIndicator( + LoadingWheel( modifier = modifier, contentDesc = stringResource(id = R.string.following_loading), ) diff --git a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 71d713ed4..95affbbf6 100644 --- a/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature-foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -61,8 +61,8 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Topic +import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel import com.google.samples.apps.nowinandroid.core.ui.NewsResourceCardExpanded -import com.google.samples.apps.nowinandroid.core.ui.NiaLoadingIndicator import com.google.samples.apps.nowinandroid.core.ui.component.NiaToggleButton import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTypography @@ -102,7 +102,7 @@ fun ForYouScreen( when (uiState) { is ForYouFeedUiState.Loading -> { item { - NiaLoadingIndicator( + LoadingWheel( modifier = modifier, contentDesc = stringResource(id = R.string.for_you_loading), ) diff --git a/feature-topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt b/feature-topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt index c306a9f4a..5d15903b3 100644 --- a/feature-topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt +++ b/feature-topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt @@ -49,7 +49,7 @@ class TopicScreenTest { } @Test - fun niaLoadingIndicator_whenScreenIsLoading_showLoading() { + fun niaLoadingWheel_whenScreenIsLoading_showLoading() { composeTestRule.setContent { TopicScreen( topicState = TopicUiState.Loading, diff --git a/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index 3be056587..9210ba167 100644 --- a/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature-topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic -import com.google.samples.apps.nowinandroid.core.ui.NiaLoadingIndicator +import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel import com.google.samples.apps.nowinandroid.core.ui.component.NiaFilterChip import com.google.samples.apps.nowinandroid.feature.topic.R.string import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading @@ -81,7 +81,7 @@ internal fun TopicScreen( ) { when (topicState) { Loading -> - NiaLoadingIndicator( + LoadingWheel( modifier = modifier, contentDesc = stringResource(id = string.topic_loading), ) @@ -140,7 +140,7 @@ private fun TopicList(news: NewsUiState, modifier: Modifier = Modifier) { } } is NewsUiState.Loading -> { - NiaLoadingIndicator(contentDesc = "Loading news") // TODO + LoadingWheel(contentDesc = "Loading news") // TODO } else -> { Text("Error") // TODO