From 7dfe35caeba9b81098cc075d9b38a2eb8182399d Mon Sep 17 00:00:00 2001 From: "arthur.valin" Date: Tue, 10 Oct 2023 13:06:52 +0200 Subject: [PATCH] Adding pull to refresh on Bet Screen --- src/app/build.gradle | 1 + .../fr/iut/alldev/allin/ui/bet/BetScreen.kt | 57 ++++- .../iut/alldev/allin/ui/bet/BetViewModel.kt | 38 ++++ .../iut/alldev/allin/ui/core/AllInLoading.kt | 209 ++++++++++++++++++ .../iut/alldev/allin/ui/login/LoginScreen.kt | 10 +- .../alldev/allin/ui/login/LoginViewModel.kt | 19 ++ 6 files changed, 321 insertions(+), 13 deletions(-) create mode 100644 src/app/src/main/java/fr/iut/alldev/allin/ui/bet/BetViewModel.kt create mode 100644 src/app/src/main/java/fr/iut/alldev/allin/ui/core/AllInLoading.kt diff --git a/src/app/build.gradle b/src/app/build.gradle index b090f6b..36bf23c 100644 --- a/src/app/build.gradle +++ b/src/app/build.gradle @@ -56,6 +56,7 @@ dependencies { implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation 'androidx.compose.material3:material3:1.2.0-alpha08' + implementation "androidx.compose.material:material:1.5.3" implementation "androidx.navigation:navigation-compose:2.7.3" implementation project(path: ':data') testImplementation 'junit:junit:4.13.2' diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/bet/BetScreen.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/bet/BetScreen.kt index 11f06d8..bbd2cf6 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/bet/BetScreen.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/bet/BetScreen.kt @@ -1,40 +1,73 @@ package fr.iut.alldev.allin.ui.bet +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import fr.iut.alldev.allin.R import fr.iut.alldev.allin.ui.bet.components.BetScreenCard import fr.iut.alldev.allin.ui.bet.components.BetScreenPopularCard import fr.iut.alldev.allin.ui.core.AllInChip import fr.iut.alldev.allin.ui.theme.AllInTheme -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @Composable -fun BetScreen(){ +fun BetScreen( + viewModel: BetViewModel = hiltViewModel(), +){ val horizontalPadding = 23.dp - LazyColumn{ + val refreshing by viewModel.isRefreshing.collectAsState() + val pullRefreshState = rememberPullRefreshState(refreshing, { viewModel.refresh() }) + + val progressAnimation by animateFloatAsState( + pullRefreshState.progress*15 + ) + + LazyColumn( + Modifier + .pullRefresh(pullRefreshState) + .padding(top = with(LocalDensity.current) { + progressAnimation.toDp() + }) + ){ item { - BetScreenPopularCard( - modifier = Modifier - .padding(top = 13.dp, bottom = 10.dp) - .padding(horizontal = 13.dp), - nbPlayers = 12, - points = 2.35f, - pointUnit = "k", - title = "Emre va réussir son TP de CI/CD mercredi?" - ) + Box( + Modifier.fillMaxWidth() + ) { + BetScreenPopularCard( + modifier = Modifier + .padding(top = 13.dp, bottom = 10.dp) + .padding(horizontal = 13.dp), + nbPlayers = 12, + points = 2.35f, + pointUnit = "k", + title = "Emre va réussir son TP de CI/CD mercredi?" + ) + PullRefreshIndicator( + modifier = Modifier + .align(Alignment.TopCenter), + refreshing = refreshing, + state = pullRefreshState + ) + } } stickyHeader { LazyRow( diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/bet/BetViewModel.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/bet/BetViewModel.kt new file mode 100644 index 0000000..ed071f3 --- /dev/null +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/bet/BetViewModel.kt @@ -0,0 +1,38 @@ +package fr.iut.alldev.allin.ui.bet + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + + +@HiltViewModel +class BetViewModel @Inject constructor( +) : ViewModel() { + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow + get() = _isRefreshing.asStateFlow() + + + private fun refreshData(){ + Thread.sleep(1000) + } + + fun refresh() { + viewModelScope.launch { + _isRefreshing.emit(true) + withContext(Dispatchers.IO) { + refreshData() + } + _isRefreshing.emit(false) + } + } + +} \ No newline at end of file diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/core/AllInLoading.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/core/AllInLoading.kt new file mode 100644 index 0000000..cf6b8f2 --- /dev/null +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/core/AllInLoading.kt @@ -0,0 +1,209 @@ +package fr.iut.alldev.allin.ui.core + +import android.content.res.Configuration +import androidx.compose.animation.core.* +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.progressSemantics +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import fr.iut.alldev.allin.ui.theme.AllInTheme +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.max + +@Composable +fun AllInLoading( + modifier: Modifier = Modifier, + brush: Brush = AllInTheme.colors.allIn_MainGradient +) { + val interactionSource = remember { MutableInteractionSource() } + Box( + modifier = modifier + .fillMaxSize() + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = {} + ) + .background(AllInTheme.themeColors.main_surface.copy(alpha = .4f)) + ) { + AllInCircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .size(50.dp), + brush = brush, + strokeWidth = 7.dp + ) + } +} + +@Composable +fun AllInCircularProgressIndicator( + modifier: Modifier = Modifier, + brush: Brush = AllInTheme.colors.allIn_MainGradient, + strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, + strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularIndeterminateStrokeCap, +) { + val stroke = with(LocalDensity.current) { + Stroke(width = strokeWidth.toPx(), cap = strokeCap) + } + + val transition = rememberInfiniteTransition() + val currentRotation = transition.animateValue( + 0, + RotationsPerCycle, + Int.VectorConverter, + infiniteRepeatable( + animation = tween( + durationMillis = RotationDuration * RotationsPerCycle, + easing = LinearEasing + ) + ) + ) + val baseRotation = transition.animateFloat( + 0f, + BaseRotationAngle, + infiniteRepeatable( + animation = tween( + durationMillis = RotationDuration, + easing = LinearEasing + ) + ) + ) + val endAngle = transition.animateFloat( + 0f, + JumpRotationAngle, + infiniteRepeatable( + animation = keyframes { + durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration + 0f at 0 with CircularEasing + JumpRotationAngle at HeadAndTailAnimationDuration + } + ) + ) + val startAngle = transition.animateFloat( + 0f, + JumpRotationAngle, + infiniteRepeatable( + animation = keyframes { + durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration + 0f at HeadAndTailDelayDuration with CircularEasing + JumpRotationAngle at durationMillis + } + ) + ) + Canvas( + modifier + .progressSemantics() + .size(CircularIndicatorDiameter) + ) { + drawCircularIndicator(0f, 360f, Color.Transparent, stroke) + val currentRotationAngleOffset = (currentRotation.value * RotationAngleOffset) % 360f + + val sweep = abs(endAngle.value - startAngle.value) + + val offset = StartAngleOffset + currentRotationAngleOffset + baseRotation.value + drawIndeterminateCircularIndicator( + startAngle.value + offset, + strokeWidth, + sweep, + brush, + stroke + ) + } +} + +private fun DrawScope.drawCircularIndicator( + startAngle: Float, + sweep: Float, + color: Color, + stroke: Stroke +) { + val diameterOffset = stroke.width / 2 + val arcDimen = size.width - 2 * diameterOffset + drawArc( + color = color, + startAngle = startAngle, + sweepAngle = sweep, + useCenter = false, + topLeft = Offset(diameterOffset, diameterOffset), + size = Size(arcDimen, arcDimen), + style = stroke + ) +} + +private fun DrawScope.drawCircularIndicator( + startAngle: Float, + sweep: Float, + brush: Brush, + stroke: Stroke +) { + val diameterOffset = stroke.width / 2 + val arcDimen = size.width - 2 * diameterOffset + drawArc( + brush = brush, + startAngle = startAngle, + sweepAngle = sweep, + useCenter = false, + topLeft = Offset(diameterOffset, diameterOffset), + size = Size(arcDimen, arcDimen), + style = stroke + ) +} + +private fun DrawScope.drawIndeterminateCircularIndicator( + startAngle: Float, + strokeWidth: Dp, + sweep: Float, + brush: Brush, + stroke: Stroke +) { + val strokeCapOffset = if (stroke.cap == StrokeCap.Butt) { + 0f + } else { + (180.0 / PI).toFloat() * (strokeWidth / (CircularIndicatorDiameter / 2)) / 2f + } + val adjustedStartAngle = startAngle + strokeCapOffset + val adjustedSweep = max(sweep, 0.1f) + drawCircularIndicator(adjustedStartAngle, adjustedSweep, brush, stroke) +} + +private const val RotationsPerCycle = 5 +private const val RotationDuration = 1332 +private const val StartAngleOffset = -90f +private const val BaseRotationAngle = 286f +private const val JumpRotationAngle = 290f +private val CircularIndicatorDiameter = 38.dp +private const val RotationAngleOffset = (BaseRotationAngle + JumpRotationAngle) % 360f +private const val HeadAndTailAnimationDuration = (RotationDuration * 0.5).toInt() +private const val HeadAndTailDelayDuration = HeadAndTailAnimationDuration +private val CircularEasing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f) + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun AllInLoadingPreview() { + AllInTheme { + AllInLoading() + } +} \ No newline at end of file diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/login/LoginScreen.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/login/LoginScreen.kt index d8f80e9..d9af848 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/login/LoginScreen.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/login/LoginScreen.kt @@ -1,5 +1,6 @@ package fr.iut.alldev.allin.ui.login +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.relocation.BringIntoViewRequester @@ -17,6 +18,7 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import fr.iut.alldev.allin.R import fr.iut.alldev.allin.ui.core.AllInGradientButton +import fr.iut.alldev.allin.ui.core.AllInLoading import fr.iut.alldev.allin.ui.core.AllInPasswordField import fr.iut.alldev.allin.ui.core.AllInTextField import fr.iut.alldev.allin.ui.theme.AllInTheme @@ -30,6 +32,7 @@ fun LoginScreen( ) { val bringIntoViewRequester = BringIntoViewRequester() + val loading by remember{ loginViewModel.loading } Box( Modifier @@ -98,7 +101,9 @@ fun LoginScreen( ) { AllInGradientButton( text = stringResource(id = R.string.Login), - onClick = navigateToDashboard, + onClick = { + loginViewModel.onLogin(navigateToDashboard) + }, modifier = Modifier ) Spacer(modifier = Modifier.height(30.dp)) @@ -127,4 +132,7 @@ fun LoginScreen( } } } + AnimatedVisibility(visible = loading) { + AllInLoading() + } } \ No newline at end of file diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/login/LoginViewModel.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/login/LoginViewModel.kt index 149c6de..2fbe80f 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/login/LoginViewModel.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/login/LoginViewModel.kt @@ -1,11 +1,30 @@ package fr.iut.alldev.allin.ui.login +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( ) : ViewModel() { + var loading = mutableStateOf(false) + fun onLogin( + navigateToDashboard: ()->Unit + ){ + viewModelScope.launch { + loading.value = true + withContext(Dispatchers.IO) { + Thread.sleep(3000) + } + navigateToDashboard() + loading.value = false + } + } + } \ No newline at end of file