Adding pull to refresh on Bet Screen
continuous-integration/drone/push Build is passing Details

pull/3/head
Arthur VALIN 1 year ago
parent 6104245af7
commit 7dfe35caeb

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

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

@ -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<Boolean>
get() = _isRefreshing.asStateFlow()
private fun refreshData(){
Thread.sleep(1000)
}
fun refresh() {
viewModelScope.launch {
_isRefreshing.emit(true)
withContext(Dispatchers.IO) {
refreshData()
}
_isRefreshing.emit(false)
}
}
}

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

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

@ -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
}
}
}
Loading…
Cancel
Save