Skip to content

Commit

Permalink
Improve visibility on loading map data and sync UX (#3621)
Browse files Browse the repository at this point in the history
* Implement LineSpinFadeLoaderProgressIndicator

Signed-off-by: Elly Kitoto <[email protected]>

* Run spotless

Signed-off-by: Elly Kitoto <[email protected]>

* Fix spacing

Signed-off-by: Elly Kitoto <[email protected]>

* Show the loader dialog when syncing the location data (#3598)

* Show the LoaderDialog when syncing the Location data
Update the LoaderDialog:
    - Make box and progressBar size dynamic
    - Make the progressDialog message as optional
    - Add the ability to show either the ProgressBar or LineSpinFadeLoaderProgressIndicator

* Add an option in the LoaderDialog to show a dialog with non blocking UI
Show a non blocking dialog when applying location filtering

* clean-up: rename variables

* fix tests

* 🚧 Increase the loading dialog size on the geowidget screen

* Fix showing map snackbar messages

Signed-off-by: Elly Kitoto <[email protected]>

* Formate code

Signed-off-by: Elly Kitoto <[email protected]>

---------

Signed-off-by: Elly Kitoto <[email protected]>
Co-authored-by: qaziabubakar-vd <[email protected]>
Co-authored-by: Benjamin Mwalimu <[email protected]>
Co-authored-by: qaziabubakar-vd <[email protected]>
  • Loading branch information
4 people authored Dec 11, 2024
1 parent 1feead0 commit a795d9c
Show file tree
Hide file tree
Showing 7 changed files with 398 additions and 128 deletions.
3 changes: 1 addition & 2 deletions android/engine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ dependencies {
implementation(libs.slf4j.nop)
implementation(libs.fhir.sdk.common)

// Shared dependencies
api(libs.bundles.datastore.kt)
api(libs.bundles.navigation)
api(libs.bundles.materialicons)
Expand All @@ -158,8 +159,6 @@ dependencies {
api(libs.bundles.okhttp3)
api(libs.bundles.paging)
api(libs.ui)

// Shared dependencies
api(libs.glide)
api(libs.knowledge) { exclude(group = "org.slf4j", module = "jcl-over-slf4j") }
api(libs.p2p.lib)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Copyright 2021-2024 Ona Systems, Inc
*
* 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
*
* http://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 org.smartregister.fhircore.engine.ui.components

import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.unit.dp
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundExcludeGenerated

const val ANIMATION_LABEL = "LineSpinFadeLoaderProgressIndicator"

/**
* A custom progress indicator that displays rotating lines in a circular pattern. Each line fades
* in and out as it rotates, creating a smooth loading animation effect.
*
* @param modifier Modifier to be applied to the Canvas composable
* @param color The color of the lines in the loading indicator
* @param lineCount The number of lines to be displayed in the circular pattern (default is 8)
* @param lineWidth The width/thickness of each line (default is 3f)
* @param lineLength The length of each line (default is 8f)
* @param innerRadius The radius of the circle on which the lines are positioned (default is 10f)
*
* Example usage:
* ```
* LineSpinFadeLoaderProgressIndicator(
* modifier = Modifier.size(80.dp),
* color = Color.Blue,
* lineCount = 8,
* lineWidth = 3f,
* lineLength = 8f,
* innerRadius = 10f
* )
* ```
*
* The animation creates a rotating effect where:
* - All lines are visible simultaneously
* - Each line's opacity changes based on its current position in the rotation
* - Lines maintain fixed positions but fade in/out to create a rotation illusion
* - The animation continuously loops with a smooth transition
*
* @see Canvas
* @see rememberInfiniteTransition
*/
@Composable
fun LineSpinFadeLoaderProgressIndicator(
modifier: Modifier = Modifier,
color: Color = Color.Blue,
lineCount: Int = 12,
lineWidth: Float = 4f,
lineLength: Float = 20f,
innerRadius: Float = 20f,
) {
val infiniteTransition = rememberInfiniteTransition(ANIMATION_LABEL)

val rotationAnimation by
infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = lineCount.toFloat(),
animationSpec =
infiniteRepeatable(
animation = tween(durationMillis = 1000, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = ANIMATION_LABEL,
)

Canvas(modifier = modifier.wrapContentSize()) {
val canvasWidth = size.width
val canvasHeight = size.height
val centerX = canvasWidth / 2
val centerY = canvasHeight / 2

for (i in 0 until lineCount) {
val angle = 2 * PI * i / lineCount
val startX = centerX + cos(angle).toFloat() * innerRadius
val startY = centerY + sin(angle).toFloat() * innerRadius
val endX = centerX + cos(angle).toFloat() * (innerRadius + lineLength)
val endY = centerY + sin(angle).toFloat() * (innerRadius + lineLength)

// Calculate alpha based on the current rotation
val distance = (i - rotationAnimation + lineCount) % lineCount
val alpha =
when {
distance < lineCount / 2f -> 1f - (distance / (lineCount / 2f))
else -> (distance - (lineCount / 2f)) / (lineCount / 2f)
}

drawLine(
color = color.copy(alpha = alpha),
start = androidx.compose.ui.geometry.Offset(startX, startY),
end = androidx.compose.ui.geometry.Offset(endX, endY),
strokeWidth = lineWidth,
cap = StrokeCap.Round,
)
}
}
}

@PreviewWithBackgroundExcludeGenerated
@Composable
fun LoadingScreen() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
LineSpinFadeLoaderProgressIndicator(
modifier = Modifier.padding(8.dp),
color = Color.Blue,
)

Spacer(modifier = Modifier.height(32.dp))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ package org.smartregister.fhircore.engine.ui.components.register
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Surface
Expand All @@ -37,13 +36,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.smartregister.fhircore.engine.R
import org.smartregister.fhircore.engine.ui.components.LineSpinFadeLoaderProgressIndicator
import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundExcludeGenerated

const val LOADER_DIALOG_PROGRESS_BAR_TAG = "loaderDialogProgressBarTag"
Expand All @@ -52,58 +53,113 @@ const val LOADER_DIALOG_PROGRESS_MSG_TAG = "loaderDialogProgressMsgTag"
@Composable
fun LoaderDialog(
modifier: Modifier = Modifier,
dialogMessage: String,
dialogMessage: String? = null,
percentageProgressFlow: Flow<Int> = flowOf(0),
showPercentageProgress: Boolean = false,
boxWidth: Dp = 240.dp,
boxHeight: Dp = 180.dp,
progressBarSize: Dp = 40.dp,
showBackground: Boolean = true,
showLineSpinIndicator: Boolean = false,
showOverlay: Boolean = true,
alignment: Alignment = Alignment.Center,
) {
val currentPercentage = percentageProgressFlow.collectAsState(0).value

if (showOverlay) {
Dialog(onDismissRequest = {}, properties = DialogProperties(dismissOnBackPress = false)) {
LoaderContent(
modifier = modifier,
dialogMessage = dialogMessage,
currentPercentage = currentPercentage,
showPercentageProgress = showPercentageProgress,
boxWidth = boxWidth,
boxHeight = boxHeight,
progressBarSize = progressBarSize,
showBackground = showBackground,
showLineSpinIndicator = showLineSpinIndicator,
)
}
} else {
Box(
modifier = modifier.wrapContentSize(),
contentAlignment = alignment,
) {
LoaderContent(
modifier = modifier,
dialogMessage = dialogMessage,
currentPercentage = currentPercentage,
showPercentageProgress = showPercentageProgress,
boxWidth = boxWidth,
boxHeight = boxHeight,
progressBarSize = progressBarSize,
showBackground = showBackground,
showLineSpinIndicator = showLineSpinIndicator,
)
}
}
}

@Composable
private fun LoaderContent(
modifier: Modifier,
dialogMessage: String?,
currentPercentage: Int,
showPercentageProgress: Boolean,
boxWidth: Dp,
boxHeight: Dp,
progressBarSize: Dp,
showBackground: Boolean,
showLineSpinIndicator: Boolean,
) {
val openDialog = remember { mutableStateOf(true) }
if (openDialog.value) {
Dialog(
onDismissRequest = { openDialog.value = true },
properties = DialogProperties(dismissOnBackPress = true),
) {
Box(modifier.size(240.dp, 180.dp)) {
Column(
modifier = modifier.padding(8.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
Box(modifier.size(boxWidth, boxHeight)) {
Column(
modifier = modifier.padding(8.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Surface(
modifier = modifier.size(boxWidth, boxHeight),
shape = RoundedCornerShape(8.dp),
color = if (showBackground) Color.Black.copy(alpha = 0.56f) else Color.Transparent,
) {
Surface(
color = Color.Black.copy(alpha = 0.56f),
modifier = modifier.fillMaxSize(),
shape = RoundedCornerShape(8),
Column(
modifier = modifier.padding(8.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (showLineSpinIndicator) {
LineSpinFadeLoaderProgressIndicator(
color = Color.White,
lineLength = 12f,
innerRadius = 16f,
)
} else {
CircularProgressIndicator(
color = Color.White,
strokeWidth = 3.dp,
modifier = modifier.testTag(LOADER_DIALOG_PROGRESS_BAR_TAG).size(40.dp),
modifier = modifier.testTag(LOADER_DIALOG_PROGRESS_BAR_TAG).size(progressBarSize),
)
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
fontSize = 16.sp,
color = Color.White,
text = dialogMessage,
modifier =
modifier.testTag(LOADER_DIALOG_PROGRESS_MSG_TAG).padding(vertical = 16.dp),
)
}

if (showPercentageProgress) {
Text(
fontSize = 15.sp,
color = Color.White,
text = stringResource(id = R.string.percentage_progress, currentPercentage),
modifier = modifier.padding(horizontal = 3.dp, vertical = 16.dp),
)
}
}
dialogMessage?.let {
Text(
text = it,
color = Color.White,
fontSize = 14.sp,
modifier = modifier.testTag(LOADER_DIALOG_PROGRESS_MSG_TAG).padding(top = 8.dp),
)
}

if (showPercentageProgress) {
Text(
fontSize = 15.sp,
color = Color.White,
text = "$currentPercentage%",
modifier = modifier.padding(top = 4.dp),
)
}
}
}
Expand All @@ -122,3 +178,16 @@ fun LoaderDialog(
fun LoaderDialogPreview() {
LoaderDialog(dialogMessage = stringResource(id = R.string.syncing))
}

@PreviewWithBackgroundExcludeGenerated
@Composable
fun LoaderDialogPreviewTest() {
LoaderDialog(
boxWidth = 50.dp,
boxHeight = 50.dp,
progressBarSize = 25.dp,
showBackground = false,
showLineSpinIndicator = true,
showOverlay = false,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener {
launchQuestionnaire = geoWidgetLauncherViewModel::launchQuestionnaire,
decodeImage = geoWidgetLauncherViewModel::getImageBitmap,
onAppMainEvent = appMainViewModel::onEvent,
isSyncing = geoWidgetLauncherViewModel.isSyncing,
)
}
}
Expand Down
Loading

0 comments on commit a795d9c

Please sign in to comment.