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

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

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

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

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

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

Loading…
Cancel
Save