Add initial loading wheel animation

Change-Id: Iaf4f80c34914022d79c7e089799fc6dd5554a532
pull/2/head
Simona Stojanovic 3 years ago committed by Don Turner
parent 7ac15771b3
commit 2145f6a7cd

@ -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

@ -18,17 +18,13 @@ package com.google.samples.apps.nowinandroid.core.ui
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material.ripple.RippleTheme import androidx.compose.material.ripple.RippleTheme
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -38,8 +34,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource 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 import androidx.compose.ui.text.font.FontWeight
@Composable @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 { object ClearRippleTheme : RippleTheme {
@Composable @Composable

@ -65,7 +65,7 @@ class InterestsScreenTest {
} }
@Test @Test
fun niaLoadingIndicator_inTopics_whenScreenIsLoading_showLoading() { fun niaLoadingWheel_inTopics_whenScreenIsLoading_showLoading() {
composeTestRule.setContent { composeTestRule.setContent {
InterestsScreen(uiState = FollowingUiState.Loading, tabIndex = 0) InterestsScreen(uiState = FollowingUiState.Loading, tabIndex = 0)
} }
@ -76,7 +76,7 @@ class InterestsScreenTest {
} }
@Test @Test
fun niaLoadingIndicator_inAuthors_whenScreenIsLoading_showLoading() { fun niaLoadingWheel_inAuthors_whenScreenIsLoading_showLoading() {
composeTestRule.setContent { composeTestRule.setContent {
InterestsScreen(uiState = FollowingUiState.Loading, tabIndex = 1) InterestsScreen(uiState = FollowingUiState.Loading, tabIndex = 1)
} }

@ -27,7 +27,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel 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.NiaToolbar
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTab import com.google.samples.apps.nowinandroid.core.ui.component.NiaTab
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTabRow import com.google.samples.apps.nowinandroid.core.ui.component.NiaTabRow
@ -72,7 +72,7 @@ fun FollowingScreen(
NiaToolbar(titleRes = R.string.interests) NiaToolbar(titleRes = R.string.interests)
when (uiState) { when (uiState) {
FollowingUiState.Loading -> FollowingUiState.Loading ->
NiaLoadingIndicator( LoadingWheel(
modifier = modifier, modifier = modifier,
contentDesc = stringResource(id = R.string.following_loading), contentDesc = stringResource(id = R.string.following_loading),
) )

@ -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.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource 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.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.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.component.NiaToggleButton
import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTypography import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTypography
@ -102,7 +102,7 @@ fun ForYouScreen(
when (uiState) { when (uiState) {
is ForYouFeedUiState.Loading -> { is ForYouFeedUiState.Loading -> {
item { item {
NiaLoadingIndicator( LoadingWheel(
modifier = modifier, modifier = modifier,
contentDesc = stringResource(id = R.string.for_you_loading), contentDesc = stringResource(id = R.string.for_you_loading),
) )

@ -49,7 +49,7 @@ class TopicScreenTest {
} }
@Test @Test
fun niaLoadingIndicator_whenScreenIsLoading_showLoading() { fun niaLoadingWheel_whenScreenIsLoading_showLoading() {
composeTestRule.setContent { composeTestRule.setContent {
TopicScreen( TopicScreen(
topicState = TopicUiState.Loading, topicState = TopicUiState.Loading,

@ -44,7 +44,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic 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.core.ui.component.NiaFilterChip
import com.google.samples.apps.nowinandroid.feature.topic.R.string import com.google.samples.apps.nowinandroid.feature.topic.R.string
import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading
@ -81,7 +81,7 @@ internal fun TopicScreen(
) { ) {
when (topicState) { when (topicState) {
Loading -> Loading ->
NiaLoadingIndicator( LoadingWheel(
modifier = modifier, modifier = modifier,
contentDesc = stringResource(id = string.topic_loading), contentDesc = stringResource(id = string.topic_loading),
) )
@ -140,7 +140,7 @@ private fun TopicList(news: NewsUiState, modifier: Modifier = Modifier) {
} }
} }
is NewsUiState.Loading -> { is NewsUiState.Loading -> {
NiaLoadingIndicator(contentDesc = "Loading news") // TODO LoadingWheel(contentDesc = "Loading news") // TODO
} }
else -> { else -> {
Text("Error") // TODO Text("Error") // TODO

Loading…
Cancel
Save