From b23c574746b4f39d6716f55ed1ff77889c6d7de5 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 23 Jun 2022 16:50:35 +0100 Subject: [PATCH 01/13] Replace the cancel button with a cross icon --- .../android/ui/shipping/InstallWCShippingFlow.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt index a098b1b7fae..ceab1d257b6 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt @@ -22,9 +22,11 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.outlined.Info import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -43,7 +45,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.woocommerce.android.R import com.woocommerce.android.ui.compose.component.WCColoredButton -import com.woocommerce.android.ui.compose.component.WCTextButton import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState.InstallationState import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState.InstallationState.PreInstallation @@ -79,6 +80,12 @@ private fun PreInstallationContent(viewState: PreInstallation, transition: Trans vertical = dimensionResource(id = R.dimen.major_150) ) ) { + IconButton(onClick = viewState.onCancelClick) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = stringResource(id = R.string.cancel) + ) + } Column( verticalArrangement = Arrangement.Center, modifier = Modifier @@ -86,9 +93,6 @@ private fun PreInstallationContent(viewState: PreInstallation, transition: Trans .verticalScroll(rememberScrollState()) .offset(y = -offset.dp) ) { - WCTextButton(onClick = viewState.onCancelClick) { - Text(text = stringResource(id = R.string.cancel)) - } SpacerWithMinHeight(1f, dimensionResource(id = R.dimen.major_100)) Box( modifier = Modifier From 706788c2201d7390b5c6291296bcb14c8a6608da Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 23 Jun 2022 20:02:34 +0100 Subject: [PATCH 02/13] Add an installation state and refactor how animations work --- .../ui/shipping/InstallWCShippingFlow.kt | 201 ++++++++++++------ .../shipping/InstallWCShippingOnboarding.kt | 43 ++-- .../ui/shipping/InstallWCShippingScreen.kt | 30 ++- .../ui/shipping/InstallWCShippingViewModel.kt | 28 ++- 4 files changed, 196 insertions(+), 106 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt index ceab1d257b6..e7d11d1aea6 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt @@ -1,9 +1,24 @@ +@file:OptIn(ExperimentalAnimationApi::class) + package com.woocommerce.android.ui.shipping +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.Crossfade +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.EnterTransition.Companion +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.ExperimentalTransitionApi import androidx.compose.animation.core.Transition import androidx.compose.animation.core.animateInt +import androidx.compose.animation.core.createChildTransition import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -35,6 +50,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource @@ -44,34 +60,26 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.woocommerce.android.R +import com.woocommerce.android.R.string import com.woocommerce.android.ui.compose.component.WCColoredButton import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground +import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState.InstallationState +import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState.InstallationState.InstallationOngoing import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState.InstallationState.PreInstallation +@OptIn(ExperimentalTransitionApi::class) @Composable -fun InstallWCShippingFlow(viewState: InstallationState, transition: Transition? = null) { +fun AnimatedVisibilityScope.InstallWCShippingFlow(viewState: InstallationState) { when (viewState) { - is PreInstallation -> PreInstallationContent(viewState, transition) + is PreInstallation -> PreInstallationContent(viewState) + is InstallationOngoing -> TODO() } } @Composable -private fun PreInstallationContent(viewState: PreInstallation, transition: Transition? = null) { - val offset by transition?.animateInt( - transitionSpec = { - tween( - durationMillis = 500, - delayMillis = 500, - // Ensure a bit of elasticity at the end of the animation - easing = CubicBezierEasing(0.7f, 0.6f, 0.74f, 1.3f) - ) - }, - label = "offset" - ) { - if (it) 0 else 120 - } ?: remember { mutableStateOf(0) } - +private fun AnimatedVisibilityScope.PreInstallationContent(viewState: InstallationState.PreInstallation) { + val initialOffset = with(LocalDensity.current) { 120.dp.roundToPx() } Column( modifier = Modifier .fillMaxSize() @@ -80,18 +88,32 @@ private fun PreInstallationContent(viewState: PreInstallation, transition: Trans vertical = dimensionResource(id = R.dimen.major_150) ) ) { - IconButton(onClick = viewState.onCancelClick) { - Icon( - imageVector = Icons.Default.Clear, - contentDescription = stringResource(id = R.string.cancel) - ) + (viewState as? PreInstallation)?.let { + IconButton(onClick = viewState.onCancelClick) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = stringResource(id = R.string.cancel) + ) + } } Column( verticalArrangement = Arrangement.Center, modifier = Modifier .weight(1f) .verticalScroll(rememberScrollState()) - .offset(y = -offset.dp) + .animateEnterExit( + enter = slideInVertically( + animationSpec = + tween( + durationMillis = 500, + delayMillis = 500, + // Ensure a bit of elasticity at the end of the animation + easing = CubicBezierEasing(0.7f, 0.6f, 0.74f, 1.3f) + ), + initialOffsetY = { -initialOffset } + ), + exit = ExitTransition.None + ) ) { SpacerWithMinHeight(1f, dimensionResource(id = R.dimen.major_100)) Box( @@ -114,49 +136,89 @@ private fun PreInstallationContent(viewState: PreInstallation, transition: Trans ) } Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_150))) - Text( - text = stringResource(id = R.string.install_wc_shipping_preinstall_title), - style = MaterialTheme.typography.h4, - fontWeight = FontWeight.Bold - ) - Text( - text = stringResource(id = viewState.extensionsName), - style = MaterialTheme.typography.h4, - fontWeight = FontWeight.Bold, - color = colorResource(id = R.color.woo_purple_50) - ) - Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_150))) - Text( - text = viewState.siteName, - style = MaterialTheme.typography.h4 - ) + MainContent(viewState) + SpacerWithMinHeight(0.75f, dimensionResource(id = R.dimen.major_100)) + AnimatedVisibility(visible = viewState is PreInstallation) { + InstallationInfoLink { (viewState as? PreInstallation)?.onInfoClick?.invoke() } + } SpacerWithMinHeight(0.75f, dimensionResource(id = R.dimen.major_100)) - Row( - horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.minor_100)), + } + (viewState as? PreInstallation)?.let { + WCColoredButton( + onClick = viewState.onProceedClick, modifier = Modifier - .clickable(onClick = viewState.onInfoClick) + .fillMaxWidth() + .animateEnterExit( + enter = slideInVertically( + animationSpec = + tween( + durationMillis = 500, + delayMillis = 500, + // Ensure a bit of elasticity at the end of the animation + easing = CubicBezierEasing(0.7f, 0.6f, 0.74f, 1.3f) + ), + initialOffsetY = { initialOffset } + ), + exit = ExitTransition.None + ) ) { - Icon( - imageVector = Icons.Outlined.Info, - contentDescription = null, - tint = colorResource(id = R.color.link_text) - ) - Text( - style = MaterialTheme.typography.subtitle1, - color = colorResource(id = R.color.link_text), - text = stringResource(id = R.string.install_wc_shipping_installation_info), - ) + Text(text = stringResource(id = R.string.install_wc_shipping_proceed_button)) } - SpacerWithMinHeight(0.75f, dimensionResource(id = R.dimen.major_100)) } - WCColoredButton( - onClick = viewState.onProceedClick, - modifier = Modifier - .fillMaxWidth() - .offset(y = offset.dp) - ) { - Text(text = stringResource(id = R.string.install_wc_shipping_proceed_button)) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun AnimatedVisibilityScope.MainContent(viewState: InstallationState) { + Column { + val text = when (viewState) { + is PreInstallation -> stringResource(id = string.install_wc_shipping_preinstall_title) + is InstallationOngoing -> "Installing" } + Text( + text = text, + style = MaterialTheme.typography.h4, + fontWeight = FontWeight.Bold, + modifier = Modifier.animateEnterExit( + enter = if (viewState is InstallationOngoing) { + fadeIn(tween(500, delayMillis = 100)) + } else EnterTransition.None, + exit = ExitTransition.None + ) + ) + + Text( + text = stringResource(id = viewState.extensionsName), + style = MaterialTheme.typography.h4, + fontWeight = FontWeight.Bold, + color = colorResource(id = R.color.woo_purple_50) + ) + Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_150))) + Text( + text = viewState.siteName, + style = MaterialTheme.typography.h4 + ) + } +} + +@Composable +private fun InstallationInfoLink(onClick: () -> Unit) { + Row( + horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.minor_100)), + modifier = Modifier + .clickable(onClick = onClick) + ) { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = null, + tint = colorResource(id = R.color.link_text) + ) + Text( + style = MaterialTheme.typography.subtitle1, + color = colorResource(id = R.color.link_text), + text = stringResource(id = R.string.install_wc_shipping_installation_info), + ) } } @@ -170,14 +232,17 @@ private fun ColumnScope.SpacerWithMinHeight(weight: Float, minHeight: Dp) { @Composable private fun PreInstallationPreview() { WooThemeWithBackground { - PreInstallationContent( - viewState = PreInstallation( - extensionsName = R.string.install_wc_shipping_extension_name, - siteName = "Site", - onCancelClick = {}, - onProceedClick = {}, - onInfoClick = {} + AnimatedContent(targetState = Unit) { + InstallWCShippingFlow( + viewState = PreInstallation( + extensionsName = R.string.install_wc_shipping_extension_name, + siteName = "Site", + siteUrl = "URL", + onCancelClick = {}, + onProceedClick = {}, + onInfoClick = {} + ) ) - ) + } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingOnboarding.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingOnboarding.kt index a9624ef3dc1..9520aedc633 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingOnboarding.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingOnboarding.kt @@ -1,9 +1,12 @@ +@file:OptIn(ExperimentalAnimationApi::class) + package com.woocommerce.android.ui.shipping -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.Transition -import androidx.compose.animation.core.animateInt +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -12,7 +15,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -21,9 +23,9 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource @@ -39,10 +41,9 @@ import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.InstallWCS import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState.Onboarding @Composable -fun InstallWcShippingOnboarding( - viewState: Onboarding, - transition: Transition -) { +fun AnimatedVisibilityScope.InstallWcShippingOnboarding(viewState: Onboarding) { + val targetExitOffset = with(LocalDensity.current) { 120.dp.roundToPx() } + Column( modifier = Modifier .fillMaxSize() @@ -51,18 +52,19 @@ fun InstallWcShippingOnboarding( end = dimensionResource(id = R.dimen.major_200) ) ) { - val offset by transition.animateInt( - transitionSpec = { tween(durationMillis = 500, easing = LinearOutSlowInEasing) }, - label = "offset" - ) { - if (it) 0 else 120 - } Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .weight(1f) .verticalScroll(rememberScrollState()) - .offset(y = -offset.dp) + .animateEnterExit( + enter = EnterTransition.None, + exit = slideOutVertically( + animationSpec = + tween(durationMillis = 500), + targetOffsetY = { -targetExitOffset } + ) + ) ) { Text( modifier = Modifier.padding(top = dimensionResource(id = R.dimen.major_350)), @@ -93,7 +95,14 @@ fun InstallWcShippingOnboarding( top = dimensionResource(id = R.dimen.major_200), bottom = dimensionResource(id = R.dimen.major_200), ) - .offset(y = offset.dp) + .animateEnterExit( + enter = EnterTransition.None, + exit = slideOutVertically( + animationSpec = + tween(durationMillis = 500), + targetOffsetY = { targetExitOffset } + ) + ) ) { WCColoredButton( modifier = Modifier.fillMaxWidth(), diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt index d8ee7fc6cec..f9954a2f5ac 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt @@ -1,8 +1,7 @@ package com.woocommerce.android.ui.shipping +import android.annotation.SuppressLint import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.ExperimentalTransitionApi import androidx.compose.animation.core.LinearOutSlowInEasing @@ -16,11 +15,19 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.woocommerce.android.R +import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState.InstallationState +import kotlinx.coroutines.delay @Composable fun InstallWCShippingScreen(viewModel: InstallWCShippingViewModel) { @@ -43,24 +50,15 @@ fun InstallWCShippingScreen(viewState: ViewState) { fadeIn(tween(500, delayMillis = 500)) .with(fadeOut(tween(500, easing = LinearOutSlowInEasing))) } else { - // TODO - EnterTransition.None.with(ExitTransition.None) + // No-op animation, just defining durations + fadeIn(tween(500), initialAlpha = 1f) + .with(fadeOut(tween(500), targetAlpha = 1f)) } } ) { targetState -> when (targetState) { - is ViewState.Onboarding -> InstallWcShippingOnboarding( - viewState = targetState, - transition = transition.createChildTransition(label = "OnboardingTransition") { - it is ViewState.Onboarding - } - ) - is InstallationState -> InstallWCShippingFlow( - viewState = targetState, - transition = transition.createChildTransition(label = "InstallationTransition") { - it is InstallationState - } - ) + is ViewState.Onboarding -> InstallWcShippingOnboarding(viewState = targetState) + is InstallationState -> InstallWCShippingFlow(viewState = targetState) } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingViewModel.kt index 030e7ccbf6e..9b202d9ad14 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingViewModel.kt @@ -37,7 +37,6 @@ class InstallWCShippingViewModel @Inject constructor( title = R.string.install_wc_shipping_flow_onboarding_screen_title, subtitle = R.string.install_wc_shipping_flow_onboarding_screen_subtitle, bullets = getBulletPointsForInstallingWcShippingFlow(), - linkUrl = WC_SHIPPING_INFO_URL, onInfoLinkClicked = { onLinkClicked(WC_SHIPPING_INFO_URL) }, onInstallClicked = ::onInstallWcShippingClicked, onDismissFlowClicked = ::onDismissWcShippingFlowClicked @@ -47,11 +46,18 @@ class InstallWCShippingViewModel @Inject constructor( siteName = selectedSite.get().let { site -> site.displayName?.takeIf { it.isNotBlank() } ?: site.name.orEmpty() }, + siteUrl = selectedSite.get().url.orEmpty(), onCancelClick = ::onDismissWcShippingFlowClicked, - onProceedClick = { /*TODO*/ }, + onProceedClick = ::onStartInstallation, onInfoClick = { onLinkClicked("https://url") } // TODO ) - Step.Installation -> TODO() + Step.Installation -> ViewState.InstallationState.InstallationOngoing( + extensionsName = R.string.install_wc_shipping_extension_name, + siteName = selectedSite.get().let { site -> + site.displayName?.takeIf { it.isNotBlank() } ?: site.name.orEmpty() + }, + siteUrl = selectedSite.get().url.orEmpty() + ) Step.PostInstallationSuccess -> TODO() is Step.PostInstallationFailure -> TODO() } @@ -88,6 +94,10 @@ class InstallWCShippingViewModel @Inject constructor( triggerEvent(OpenLinkEvent(url)) } + private fun onStartInstallation() { + step.value = Step.Installation + } + private sealed interface Step : Parcelable { @Parcelize object Onboarding : Step @@ -110,7 +120,6 @@ class InstallWCShippingViewModel @Inject constructor( @StringRes val title: Int, @StringRes val subtitle: Int, val bullets: List, - val linkUrl: String, val onInstallClicked: () -> Unit = {}, val onDismissFlowClicked: () -> Unit = {}, val onInfoLinkClicked: () -> Unit = {} @@ -118,14 +127,23 @@ class InstallWCShippingViewModel @Inject constructor( sealed class InstallationState : ViewState { abstract val extensionsName: Int + abstract val siteName: String + abstract val siteUrl: String data class PreInstallation( @StringRes override val extensionsName: Int, - val siteName: String, + override val siteName: String, + override val siteUrl: String, val onCancelClick: () -> Unit, val onProceedClick: () -> Unit, val onInfoClick: () -> Unit ) : InstallationState() + + data class InstallationOngoing( + @StringRes override val extensionsName: Int, + override val siteName: String, + override val siteUrl: String, + ) : InstallationState() } } From 55fee2c9fd571447e36a0ca4ec709e14cb83f173 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 23 Jun 2022 20:45:01 +0100 Subject: [PATCH 03/13] Animation between pre-install and install screens --- .../ui/shipping/InstallWCShippingFlow.kt | 129 ++++++++++++++---- .../ui/shipping/InstallWCShippingScreen.kt | 59 +++++++- 2 files changed, 158 insertions(+), 30 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt index e7d11d1aea6..9933d40b70f 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt @@ -3,18 +3,12 @@ package com.woocommerce.android.ui.shipping import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.Crossfade import androidx.compose.animation.EnterTransition -import androidx.compose.animation.EnterTransition.Companion import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.ExperimentalTransitionApi -import androidx.compose.animation.core.Transition -import androidx.compose.animation.core.animateInt -import androidx.compose.animation.core.createChildTransition import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -30,7 +24,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState @@ -44,9 +37,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.outlined.Info import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -60,10 +50,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.woocommerce.android.R -import com.woocommerce.android.R.string import com.woocommerce.android.ui.compose.component.WCColoredButton import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground -import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState.InstallationState import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState.InstallationState.InstallationOngoing import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState.InstallationState.PreInstallation @@ -73,7 +61,7 @@ import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState. fun AnimatedVisibilityScope.InstallWCShippingFlow(viewState: InstallationState) { when (viewState) { is PreInstallation -> PreInstallationContent(viewState) - is InstallationOngoing -> TODO() + is InstallationOngoing -> InstallationContent(viewState) } } @@ -88,13 +76,11 @@ private fun AnimatedVisibilityScope.PreInstallationContent(viewState: Installati vertical = dimensionResource(id = R.dimen.major_150) ) ) { - (viewState as? PreInstallation)?.let { - IconButton(onClick = viewState.onCancelClick) { - Icon( - imageVector = Icons.Default.Clear, - contentDescription = stringResource(id = R.string.cancel) - ) - } + IconButton(onClick = viewState.onCancelClick) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = stringResource(id = R.string.cancel) + ) } Column( verticalArrangement = Arrangement.Center, @@ -137,11 +123,20 @@ private fun AnimatedVisibilityScope.PreInstallationContent(viewState: Installati } Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_150))) MainContent(viewState) - SpacerWithMinHeight(0.75f, dimensionResource(id = R.dimen.major_100)) - AnimatedVisibility(visible = viewState is PreInstallation) { - InstallationInfoLink { (viewState as? PreInstallation)?.onInfoClick?.invoke() } + Box( + modifier = Modifier.weight(1.5f), + contentAlignment = Alignment.Center + ) { + InstallationInfoLink( + onClick = viewState.onInfoClick, + modifier = Modifier + .padding(vertical = dimensionResource(id = R.dimen.major_100)) + .animateEnterExit( + enter = EnterTransition.None, + exit = fadeOut(tween(500)) + ) + ) } - SpacerWithMinHeight(0.75f, dimensionResource(id = R.dimen.major_100)) } (viewState as? PreInstallation)?.let { WCColoredButton( @@ -168,42 +163,102 @@ private fun AnimatedVisibilityScope.PreInstallationContent(viewState: Installati } } +@Composable +private fun AnimatedVisibilityScope.InstallationContent(viewState: InstallationOngoing) { + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = dimensionResource(id = R.dimen.major_100), + vertical = dimensionResource(id = R.dimen.major_150) + ) + ) { + // fill equivalent space as the cross-icon + Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_300))) + SpacerWithMinHeight(1f, dimensionResource(id = R.dimen.major_100)) + Box( + modifier = Modifier + .clip(CircleShape) + .border( + width = dimensionResource(id = R.dimen.major_75), + color = colorResource(id = R.color.woo_purple_20), + shape = CircleShape + ) + .size(dimensionResource(id = R.dimen.image_major_120)) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_forward_rounded), + contentDescription = null, + tint = colorResource(id = R.color.woo_purple_50), + modifier = Modifier + .align(Alignment.Center) + .size(dimensionResource(id = R.dimen.image_major_64)) + ) + } + Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_150))) + MainContent(viewState) + Box(modifier = Modifier.weight(1.5f)) { + Text( + text = viewState.siteUrl, + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(vertical = dimensionResource(id = R.dimen.major_100)) + .animateEnterExit(enter = fadeIn(tween(400, delayMillis = 600), initialAlpha = 0.5f)) + ) + } + // fill equivalent space as the proceed-button + Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_300))) + } +} + @OptIn(ExperimentalAnimationApi::class) @Composable private fun AnimatedVisibilityScope.MainContent(viewState: InstallationState) { Column { val text = when (viewState) { - is PreInstallation -> stringResource(id = string.install_wc_shipping_preinstall_title) + is PreInstallation -> stringResource(id = R.string.install_wc_shipping_preinstall_title) is InstallationOngoing -> "Installing" } Text( text = text, style = MaterialTheme.typography.h4, fontWeight = FontWeight.Bold, + // Animate the step title when starting the installation modifier = Modifier.animateEnterExit( enter = if (viewState is InstallationOngoing) { - fadeIn(tween(500, delayMillis = 100)) + fadeIn(tween(600, delayMillis = 400)) } else EnterTransition.None, exit = ExitTransition.None ) ) + // Animate the extension and site names when starting the installation + val extensionAndNameModifier = Modifier.animateEnterExit( + enter = if (viewState is InstallationOngoing) { + fadeIn(tween(400, delayMillis = 600), initialAlpha = 0.5f) + } else EnterTransition.None, + exit = ExitTransition.None + ) + Text( text = stringResource(id = viewState.extensionsName), style = MaterialTheme.typography.h4, fontWeight = FontWeight.Bold, - color = colorResource(id = R.color.woo_purple_50) + color = colorResource(id = R.color.woo_purple_50), + modifier = extensionAndNameModifier ) Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_150))) Text( text = viewState.siteName, - style = MaterialTheme.typography.h4 + style = MaterialTheme.typography.h4, + modifier = extensionAndNameModifier ) } } @Composable -private fun InstallationInfoLink(onClick: () -> Unit) { +private fun InstallationInfoLink(onClick: () -> Unit, modifier: Modifier = Modifier) { Row( horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.minor_100)), modifier = Modifier @@ -246,3 +301,19 @@ private fun PreInstallationPreview() { } } } + +@Preview +@Composable +private fun InstallationOngoingPreview() { + WooThemeWithBackground { + AnimatedContent(targetState = Unit) { + InstallationContent( + viewState = InstallationOngoing( + extensionsName = R.string.install_wc_shipping_extension_name, + siteName = "Site", + siteUrl = "URL" + ) + ) + } + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt index f9954a2f5ac..201b54f03bf 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.ExperimentalTransitionApi import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.createChildTransition import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition import androidx.compose.animation.fadeIn @@ -63,3 +62,61 @@ fun InstallWCShippingScreen(viewState: ViewState) { } } } + +@SuppressLint("RememberReturnType") +@Preview +@Composable +private fun PreviewInstallWCShippingScreen() { + val states = remember { + mutableListOf() + } + + var state by remember { + mutableStateOf(states.getOrNull(0)) + } + + remember { + states.add( + ViewState.Onboarding( + title = R.string.install_wc_shipping_flow_onboarding_screen_title, + subtitle = R.string.install_wc_shipping_flow_onboarding_screen_subtitle, + bullets = emptyList(), + onInstallClicked = { state = states[1] } + ) + ) + + states.add( + InstallationState.PreInstallation( + extensionsName = R.string.install_wc_shipping_extension_name, + siteName = "Site", + siteUrl = "URL", + onCancelClick = {}, + onProceedClick = { state = states[2] }, + onInfoClick = {} + ) + ) + + states.add( + InstallationState.InstallationOngoing( + extensionsName = R.string.install_wc_shipping_extension_name, + siteName = "Site", + siteUrl = "URL" + ) + ) + + state = states[0] + } + + LaunchedEffect(state) { + if (state is InstallationState.InstallationOngoing) { + delay(5000) + state = states[0] + } + } + + WooThemeWithBackground { + state?.let { + InstallWCShippingScreen(viewState = it) + } + } +} From c150bf45fa38311c8068aaaffa2737f5ceac87ac Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Thu, 23 Jun 2022 20:58:14 +0100 Subject: [PATCH 04/13] Add a TODO comment about starting the installation --- .../ui/shipping/InstallWCShippingViewModel.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingViewModel.kt index 9b202d9ad14..6e1eb44264a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingViewModel.kt @@ -7,12 +7,16 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.asLiveData import com.woocommerce.android.R import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.Step.Installation import com.woocommerce.android.viewmodel.MultiLiveEvent import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit import com.woocommerce.android.viewmodel.ScopedViewModel import com.woocommerce.android.viewmodel.getStateFlow import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import javax.inject.Inject @@ -31,6 +35,16 @@ class InstallWCShippingViewModel @Inject constructor( .map { prepareStep(it) } .asLiveData() + init { + launch { + // Wait for installation step + step.filter { it == Installation } + .first() + + // TODO launch plugin installation + } + } + private fun prepareStep(step: Step): ViewState { return when (step) { Step.Onboarding -> ViewState.Onboarding( From 136668a1b8b81ee89e9158e14f451f61549e8097 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Fri, 24 Jun 2022 20:30:34 +0100 Subject: [PATCH 05/13] Use None as parent animation between pre-install and installation --- .../android/ui/shipping/InstallWCShippingScreen.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt index 201b54f03bf..5a5ccbb96f6 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt @@ -2,6 +2,9 @@ package com.woocommerce.android.ui.shipping import android.annotation.SuppressLint import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.ExitTransition.Companion import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.ExperimentalTransitionApi import androidx.compose.animation.core.LinearOutSlowInEasing @@ -49,9 +52,8 @@ fun InstallWCShippingScreen(viewState: ViewState) { fadeIn(tween(500, delayMillis = 500)) .with(fadeOut(tween(500, easing = LinearOutSlowInEasing))) } else { - // No-op animation, just defining durations - fadeIn(tween(500), initialAlpha = 1f) - .with(fadeOut(tween(500), targetAlpha = 1f)) + // No-op animation, each screen will define animations for specific components separately + EnterTransition.None.with(ExitTransition.None) } } ) { targetState -> From 85d9ca3cb2b6b805af185ab17cffddfd0dc50942 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Fri, 24 Jun 2022 21:51:39 +0100 Subject: [PATCH 06/13] Adjust animation durations --- .../woocommerce/android/ui/shipping/InstallWCShippingFlow.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt index 9933d40b70f..6930d6fd20a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt @@ -9,6 +9,7 @@ import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.ExperimentalTransitionApi +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -227,7 +228,7 @@ private fun AnimatedVisibilityScope.MainContent(viewState: InstallationState) { // Animate the step title when starting the installation modifier = Modifier.animateEnterExit( enter = if (viewState is InstallationOngoing) { - fadeIn(tween(600, delayMillis = 400)) + fadeIn(tween(400, delayMillis = 600, easing = LinearEasing)) } else EnterTransition.None, exit = ExitTransition.None ) @@ -236,7 +237,7 @@ private fun AnimatedVisibilityScope.MainContent(viewState: InstallationState) { // Animate the extension and site names when starting the installation val extensionAndNameModifier = Modifier.animateEnterExit( enter = if (viewState is InstallationOngoing) { - fadeIn(tween(400, delayMillis = 600), initialAlpha = 0.5f) + fadeIn(tween(200, delayMillis = 800, easing = LinearEasing), initialAlpha = 0.5f) } else EnterTransition.None, exit = ExitTransition.None ) From 71d529d562f7ce9f95bf5c96431a2a4f67729c72 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Mon, 27 Jun 2022 14:15:09 +0100 Subject: [PATCH 07/13] Remove unused imports --- .../woocommerce/android/ui/shipping/InstallWCShippingScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt index 5a5ccbb96f6..472c1008cc2 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingScreen.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import androidx.compose.animation.AnimatedContent import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition -import androidx.compose.animation.ExitTransition.Companion import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.ExperimentalTransitionApi import androidx.compose.animation.core.LinearOutSlowInEasing From 2b3618ba07697109a7a91ce50d11b8ff63be0244 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Mon, 27 Jun 2022 17:20:29 +0100 Subject: [PATCH 08/13] Add animation for the loading indicator --- .../ui/shipping/InstallWCShippingFlow.kt | 143 ++++++++++++++---- 1 file changed, 113 insertions(+), 30 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt index 6930d6fd20a..c71e86cb830 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt @@ -10,11 +10,16 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.ExperimentalTransitionApi import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateFloatAsState +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.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.border +import androidx.compose.foundation.Canvas import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -28,7 +33,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -38,9 +42,19 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.outlined.Info import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource @@ -103,16 +117,13 @@ private fun AnimatedVisibilityScope.PreInstallationContent(viewState: Installati ) ) { SpacerWithMinHeight(1f, dimensionResource(id = R.dimen.major_100)) - Box( - modifier = Modifier - .clip(CircleShape) - .border( - width = dimensionResource(id = R.dimen.major_75), - color = colorResource(id = R.color.woo_purple_20), - shape = CircleShape - ) - .size(dimensionResource(id = R.dimen.image_major_120)) - ) { + Box { + InstallationLoadingIndicator( + showLoadingIndicator = false, + modifier = Modifier + .size(dimensionResource(id = R.dimen.image_major_120)) + ) + Icon( painter = painterResource(id = R.drawable.ic_arrow_forward_rounded), contentDescription = null, @@ -178,25 +189,52 @@ private fun AnimatedVisibilityScope.InstallationContent(viewState: InstallationO // fill equivalent space as the cross-icon Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_300))) SpacerWithMinHeight(1f, dimensionResource(id = R.dimen.major_100)) - Box( - modifier = Modifier - .clip(CircleShape) - .border( - width = dimensionResource(id = R.dimen.major_75), - color = colorResource(id = R.color.woo_purple_20), - shape = CircleShape - ) - .size(dimensionResource(id = R.dimen.image_major_120)) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_arrow_forward_rounded), - contentDescription = null, - tint = colorResource(id = R.color.woo_purple_50), - modifier = Modifier - .align(Alignment.Center) - .size(dimensionResource(id = R.dimen.image_major_64)) + + Box(modifier = Modifier.size(dimensionResource(id = R.dimen.image_major_120))) { + var isCursorVisible by remember { mutableStateOf(true) } + var isShowingLoadingIndicator by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + isCursorVisible = false + } + val alpha by animateFloatAsState( + targetValue = if (isCursorVisible) 1f else 0f, + animationSpec = tween(1000, delayMillis = 1000), + finishedListener = { + isShowingLoadingIndicator = true + } ) + val rotation by animateFloatAsState( + targetValue = if (isCursorVisible) 0f else 180f, + animationSpec = keyframes { + if (isCursorVisible) return@keyframes + durationMillis = 2000 + 0f at 1000 + -20f at 1100 + 180f at 2000 + } + ) + + Box { + InstallationLoadingIndicator( + showLoadingIndicator = isShowingLoadingIndicator, + modifier = Modifier + .size(dimensionResource(id = R.dimen.image_major_120)) + ) + + Icon( + painter = painterResource(id = R.drawable.ic_arrow_forward_rounded), + contentDescription = null, + tint = colorResource(id = R.color.woo_purple_50), + modifier = Modifier + .align(Alignment.Center) + .alpha(alpha) + .rotate(rotation) + .size(dimensionResource(id = R.dimen.image_major_64)) + ) + } } + Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_150))) MainContent(viewState) Box(modifier = Modifier.weight(1.5f)) { @@ -284,6 +322,51 @@ private fun ColumnScope.SpacerWithMinHeight(weight: Float, minHeight: Dp) { Spacer(modifier = Modifier.weight(weight)) } +@Composable +private fun InstallationLoadingIndicator(showLoadingIndicator: Boolean, modifier: Modifier = Modifier) { + val stroke = with(LocalDensity.current) { + Stroke(width = dimensionResource(id = R.dimen.major_75).toPx(), cap = StrokeCap.Round) + } + + val circleColor = colorResource(id = R.color.woo_purple_20) + val progressColor = colorResource(id = R.color.woo_purple_50) + + val startAngle by if (showLoadingIndicator) { + val transition = rememberInfiniteTransition() + + transition.animateFloat( + -90f, + 270f, + infiniteRepeatable( + animation = tween(1332, easing = LinearEasing) + ) + ) + } else { + remember { mutableStateOf(-90f) } + } + + Canvas(modifier) { + val size = size.width - stroke.width + + drawCircle( + color = circleColor, + radius = (size) / 2, + style = stroke + ) + if (showLoadingIndicator) { + drawArc( + color = progressColor, + startAngle = startAngle, + sweepAngle = 30f, + useCenter = false, + size = Size(size, size), + topLeft = Offset(stroke.width / 2, stroke.width / 2), + style = stroke + ) + } + } +} + @Preview @Composable private fun PreInstallationPreview() { From 946f23a5c85af03ba311bd626b59a7e52c0cc058 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Mon, 27 Jun 2022 18:28:14 +0100 Subject: [PATCH 09/13] Improve the extension and site name animation --- .../ui/shipping/InstallWCShippingFlow.kt | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt index c71e86cb830..cb90eb94862 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt @@ -46,6 +46,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -135,20 +136,18 @@ private fun AnimatedVisibilityScope.PreInstallationContent(viewState: Installati } Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_150))) MainContent(viewState) - Box( - modifier = Modifier.weight(1.5f), - contentAlignment = Alignment.Center - ) { - InstallationInfoLink( - onClick = viewState.onInfoClick, - modifier = Modifier - .padding(vertical = dimensionResource(id = R.dimen.major_100)) - .animateEnterExit( - enter = EnterTransition.None, - exit = fadeOut(tween(500)) - ) - ) - } + SpacerWithMinHeight(0.75f, dimensionResource(id = R.dimen.major_100)) + + InstallationInfoLink( + onClick = viewState.onInfoClick, + modifier = Modifier + .animateEnterExit( + enter = EnterTransition.None, + exit = fadeOut(tween(500)) + ) + ) + + SpacerWithMinHeight(0.75f, dimensionResource(id = R.dimen.major_100)) } (viewState as? PreInstallation)?.let { WCColoredButton( @@ -181,6 +180,7 @@ private fun AnimatedVisibilityScope.InstallationContent(viewState: InstallationO verticalArrangement = Arrangement.Center, modifier = Modifier .fillMaxSize() + .verticalScroll(rememberScrollState()) .padding( horizontal = dimensionResource(id = R.dimen.major_100), vertical = dimensionResource(id = R.dimen.major_150) @@ -191,8 +191,8 @@ private fun AnimatedVisibilityScope.InstallationContent(viewState: InstallationO SpacerWithMinHeight(1f, dimensionResource(id = R.dimen.major_100)) Box(modifier = Modifier.size(dimensionResource(id = R.dimen.image_major_120))) { - var isCursorVisible by remember { mutableStateOf(true) } - var isShowingLoadingIndicator by remember { mutableStateOf(false) } + var isCursorVisible by rememberSaveable { mutableStateOf(true) } + var isShowingLoadingIndicator by rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { isCursorVisible = false @@ -237,15 +237,14 @@ private fun AnimatedVisibilityScope.InstallationContent(viewState: InstallationO Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_150))) MainContent(viewState) - Box(modifier = Modifier.weight(1.5f)) { - Text( - text = viewState.siteUrl, - style = MaterialTheme.typography.body1, - modifier = Modifier - .padding(vertical = dimensionResource(id = R.dimen.major_100)) - .animateEnterExit(enter = fadeIn(tween(400, delayMillis = 600), initialAlpha = 0.5f)) - ) - } + Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_125))) + Text( + text = viewState.siteUrl, + style = MaterialTheme.typography.body1, + modifier = Modifier + .animateEnterExit(enter = fadeIn(tween(400, delayMillis = 600), initialAlpha = 0.5f)) + ) + SpacerWithMinHeight(1.5f, dimensionResource(id = R.dimen.major_100)) // fill equivalent space as the proceed-button Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.major_300))) } @@ -275,7 +274,15 @@ private fun AnimatedVisibilityScope.MainContent(viewState: InstallationState) { // Animate the extension and site names when starting the installation val extensionAndNameModifier = Modifier.animateEnterExit( enter = if (viewState is InstallationOngoing) { - fadeIn(tween(200, delayMillis = 800, easing = LinearEasing), initialAlpha = 0.5f) + fadeIn( + keyframes { + durationMillis = 1000 + 1f at 0 + 0.5f at 200 + 0.5f at 800 + 1f at 1000 + } + ) } else EnterTransition.None, exit = ExitTransition.None ) From 939d6cd96d90610c4f7d71be6b2e91c45cc2693f Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Mon, 27 Jun 2022 18:54:25 +0100 Subject: [PATCH 10/13] Remove the usages of the rememberSaveable It causes issues as the value is remembered beyond the scope of the step itself --- .../woocommerce/android/ui/shipping/InstallWCShippingFlow.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt index cb90eb94862..5e706349135 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt @@ -46,7 +46,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -191,8 +190,8 @@ private fun AnimatedVisibilityScope.InstallationContent(viewState: InstallationO SpacerWithMinHeight(1f, dimensionResource(id = R.dimen.major_100)) Box(modifier = Modifier.size(dimensionResource(id = R.dimen.image_major_120))) { - var isCursorVisible by rememberSaveable { mutableStateOf(true) } - var isShowingLoadingIndicator by rememberSaveable { mutableStateOf(false) } + var isCursorVisible by remember { mutableStateOf(true) } + var isShowingLoadingIndicator by remember { mutableStateOf(false) } LaunchedEffect(Unit) { isCursorVisible = false From 416ed0e3d7221e89f9fcd859d3b201bfc282ec4e Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Mon, 27 Jun 2022 19:11:31 +0100 Subject: [PATCH 11/13] Link arrow animations to the transition This avoid restarting them on screen rotation --- .../ui/shipping/InstallWCShippingFlow.kt | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt index 5e706349135..172e7a4cfee 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt @@ -4,6 +4,7 @@ package com.woocommerce.android.ui.shipping import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.EnterExitState.PreEnter import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalAnimationApi @@ -11,7 +12,6 @@ import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.ExperimentalTransitionApi import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.rememberInfiniteTransition @@ -42,11 +42,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.outlined.Info import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -190,29 +189,33 @@ private fun AnimatedVisibilityScope.InstallationContent(viewState: InstallationO SpacerWithMinHeight(1f, dimensionResource(id = R.dimen.major_100)) Box(modifier = Modifier.size(dimensionResource(id = R.dimen.image_major_120))) { - var isCursorVisible by remember { mutableStateOf(true) } - var isShowingLoadingIndicator by remember { mutableStateOf(false) } - - LaunchedEffect(Unit) { - isCursorVisible = false - } - val alpha by animateFloatAsState( - targetValue = if (isCursorVisible) 1f else 0f, - animationSpec = tween(1000, delayMillis = 1000), - finishedListener = { - isShowingLoadingIndicator = true + val alpha by transition.animateFloat( + transitionSpec = { tween(1000, delayMillis = 1000) }, + label = "arrowAlpha" + ) { + when (it) { + PreEnter -> 1f + else -> 0f } - ) - val rotation by animateFloatAsState( - targetValue = if (isCursorVisible) 0f else 180f, - animationSpec = keyframes { - if (isCursorVisible) return@keyframes - durationMillis = 2000 - 0f at 1000 - -20f at 1100 - 180f at 2000 + } + + val rotation by transition.animateFloat( + transitionSpec = { + keyframes { + durationMillis = 2000 + 0f at 1000 + -20f at 1100 + 180f at 2000 + } + }, + label = "arrowAlpha" + ) { + when (it) { + PreEnter -> 0f + else -> 180f } - ) + } + val isShowingLoadingIndicator by derivedStateOf { alpha == 0f } Box { InstallationLoadingIndicator( From 619a9b71ecaa3ba5ed6de74a0cdfd874158db0e9 Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 29 Jun 2022 10:55:33 +0100 Subject: [PATCH 12/13] Fix usage of the modifier argument --- .../woocommerce/android/ui/shipping/InstallWCShippingFlow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt index 172e7a4cfee..ef62224da1e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt @@ -309,7 +309,7 @@ private fun AnimatedVisibilityScope.MainContent(viewState: InstallationState) { private fun InstallationInfoLink(onClick: () -> Unit, modifier: Modifier = Modifier) { Row( horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.minor_100)), - modifier = Modifier + modifier = modifier .clickable(onClick = onClick) ) { Icon( From 0fbd272112c9474652c55e0f3fc7fa236fbef75c Mon Sep 17 00:00:00 2001 From: Hicham Boushaba Date: Wed, 29 Jun 2022 10:58:27 +0100 Subject: [PATCH 13/13] Cleanup the usages of the OptIn --- .../woocommerce/android/ui/shipping/InstallWCShippingFlow.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt index ef62224da1e..98d1964da3e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/shipping/InstallWCShippingFlow.kt @@ -1,5 +1,4 @@ @file:OptIn(ExperimentalAnimationApi::class) - package com.woocommerce.android.ui.shipping import androidx.compose.animation.AnimatedContent @@ -9,7 +8,6 @@ import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.CubicBezierEasing -import androidx.compose.animation.core.ExperimentalTransitionApi import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable @@ -70,7 +68,6 @@ import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState. import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState.InstallationState.InstallationOngoing import com.woocommerce.android.ui.shipping.InstallWCShippingViewModel.ViewState.InstallationState.PreInstallation -@OptIn(ExperimentalTransitionApi::class) @Composable fun AnimatedVisibilityScope.InstallWCShippingFlow(viewState: InstallationState) { when (viewState) { @@ -252,7 +249,6 @@ private fun AnimatedVisibilityScope.InstallationContent(viewState: InstallationO } } -@OptIn(ExperimentalAnimationApi::class) @Composable private fun AnimatedVisibilityScope.MainContent(viewState: InstallationState) { Column {