diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/StackViewProperties.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/StackViewProperties.kt new file mode 100644 index 0000000000..65fff51f32 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/StackViewProperties.kt @@ -0,0 +1,48 @@ +/* + * 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.configuration.view + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable +import org.smartregister.fhircore.engine.domain.model.ViewType +import org.smartregister.fhircore.engine.util.extension.interpolate + +@Serializable +@Parcelize +data class StackViewProperties( + override val viewType: ViewType = ViewType.STACK, + override val weight: Float = 0f, + override val backgroundColor: String? = "#FFFFFF", + override val padding: Int = 0, + override val borderRadius: Int = 0, + override val alignment: ViewAlignment = ViewAlignment.NONE, + override val fillMaxWidth: Boolean = false, + override val fillMaxHeight: Boolean = false, + override val clickable: String = "false", + override val visible: String = "true", + val opacity: Float = 0f, + val size: Int? = 0, + val children: List<ViewProperties> = emptyList(), +) : ViewProperties(), Parcelable { + override fun interpolate(computedValuesMap: Map<String, Any>): StackViewProperties { + return this.copy( + backgroundColor = backgroundColor?.interpolate(computedValuesMap), + visible = visible.interpolate(computedValuesMap), + ) + } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewProperties.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewProperties.kt index ede4af52a5..13cc346747 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewProperties.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewProperties.kt @@ -70,4 +70,8 @@ enum class ViewAlignment { END, CENTER, NONE, + TOPSTART, + TOPEND, + BOTTOMSTART, + BOTTOMEND, } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewPropertiesSerializer.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewPropertiesSerializer.kt index 34061a4813..7dec5bc376 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewPropertiesSerializer.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewPropertiesSerializer.kt @@ -51,6 +51,7 @@ object ViewPropertiesSerializer : ViewType.LIST -> ListProperties.serializer() ViewType.IMAGE -> ImageProperties.serializer() ViewType.BORDER -> DividerProperties.serializer() + ViewType.STACK -> StackViewProperties.serializer() } } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ViewType.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ViewType.kt index 132158a2a5..8b4940cee4 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ViewType.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ViewType.kt @@ -56,4 +56,7 @@ enum class ViewType { /** A type of view component used to render divider between views */ @JsonNames("border", "Border") BORDER, + + /** A type of view component used to overlay different views */ + @JsonNames("stack", "Stack") STACK, } diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/StackViewTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/StackViewTest.kt new file mode 100644 index 0000000000..d203e9f434 --- /dev/null +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/StackViewTest.kt @@ -0,0 +1,48 @@ +/* + * 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.quest.integration.ui.shared.components + +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.navigation.compose.rememberNavController +import org.hl7.fhir.r4.model.ResourceType +import org.junit.Rule +import org.junit.Test +import org.smartregister.fhircore.engine.configuration.view.StackViewProperties +import org.smartregister.fhircore.engine.configuration.view.ViewAlignment +import org.smartregister.fhircore.engine.domain.model.ResourceData +import org.smartregister.fhircore.quest.ui.shared.components.STACK_VIEW_TEST_TAG +import org.smartregister.fhircore.quest.ui.shared.components.StackView + +class StackViewTest { + @get:Rule val composeTestRule = createComposeRule() + + @Test + fun testStackViewIsRendered() { + val stackViewProperties = StackViewProperties(alignment = ViewAlignment.CENTER, size = 250) + composeTestRule.setContent { + StackView( + modifier = Modifier, + stackViewProperties = stackViewProperties, + resourceData = ResourceData("", ResourceType.Patient, emptyMap()), + navController = rememberNavController(), + ) + } + composeTestRule.onNodeWithTag(STACK_VIEW_TEST_TAG).assertExists() + } +} diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ViewGeneratorTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ViewGeneratorTest.kt index ca2e8b7fcd..d33cc7b89a 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ViewGeneratorTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ViewGeneratorTest.kt @@ -43,6 +43,7 @@ import org.smartregister.fhircore.engine.configuration.view.PersonalDataProperti import org.smartregister.fhircore.engine.configuration.view.RowArrangement import org.smartregister.fhircore.engine.configuration.view.RowProperties import org.smartregister.fhircore.engine.configuration.view.ServiceCardProperties +import org.smartregister.fhircore.engine.configuration.view.StackViewProperties import org.smartregister.fhircore.engine.configuration.view.ViewAlignment import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger import org.smartregister.fhircore.engine.configuration.workflow.ApplicationWorkflow @@ -386,4 +387,35 @@ class ViewGeneratorTest { .assertExists() .assertIsDisplayed() } + + @Test + fun testStackViewIsRenderedCorrectlyWhenStackViewPropertiesHasChildren() { + composeRule.setContent { + GenerateView( + properties = + StackViewProperties( + size = 250, + alignment = ViewAlignment.CENTER, + children = + listOf( + ButtonProperties(status = "DUE", text = "Due Task"), + ButtonProperties(status = "COMPLETED", text = "Completed Task"), + ButtonProperties(status = "READY", text = "Ready Task"), + ), + viewType = ViewType.STACK, + ), + resourceData = resourceData, + navController = TestNavHostController(LocalContext.current), + ) + } + composeRule + .onNodeWithText("Due Task", useUnmergedTree = true) + .assertExists() + .assertIsDisplayed() + composeRule.onNodeWithText("Completed Task", useUnmergedTree = true).assertExists() + composeRule + .onNodeWithText("Ready Task", useUnmergedTree = true) + .assertExists() + .assertIsDisplayed() + } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/StackView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/StackView.kt new file mode 100644 index 0000000000..8410c08d6d --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/StackView.kt @@ -0,0 +1,100 @@ +/* + * 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.quest.ui.shared.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import org.hl7.fhir.r4.model.ResourceType +import org.smartregister.fhircore.engine.configuration.view.StackViewProperties +import org.smartregister.fhircore.engine.configuration.view.ViewAlignment +import org.smartregister.fhircore.engine.domain.model.ResourceData +import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundExcludeGenerated +import org.smartregister.fhircore.engine.util.extension.parseColor + +const val STACK_VIEW_TEST_TAG = "stackViewTestTag" + +@Composable +fun StackView( + modifier: Modifier, + stackViewProperties: StackViewProperties, + resourceData: ResourceData, + navController: NavController, +) { + val backgroundColor = stackViewProperties.backgroundColor.parseColor() + val size = stackViewProperties.size + + Box( + modifier = + Modifier.background(backgroundColor.copy(alpha = stackViewProperties.opacity)) + .size(size!!.dp) + .testTag(STACK_VIEW_TEST_TAG), + contentAlignment = castViewAlignment(stackViewProperties.alignment), + ) { + stackViewProperties.children.forEach { child -> + GenerateView( + modifier = generateModifier(viewProperties = child), + properties = child.interpolate(resourceData.computedValuesMap), + resourceData = resourceData, + navController = navController, + ) + } + } +} + +fun castViewAlignment( + viewAlignment: ViewAlignment, +): Alignment { + return when (viewAlignment) { + ViewAlignment.TOPSTART -> Alignment.TopStart + ViewAlignment.TOPEND -> Alignment.TopEnd + ViewAlignment.CENTER -> Alignment.Center + ViewAlignment.BOTTOMSTART -> Alignment.BottomStart + ViewAlignment.BOTTOMEND -> Alignment.BottomEnd + else -> { + Alignment.Center + } + } +} + +@PreviewWithBackgroundExcludeGenerated +@Composable +private fun PreviewStack() { + StackView( + stackViewProperties = + StackViewProperties( + opacity = 0.2f, + size = 250, + backgroundColor = "successColor", + ), + modifier = Modifier, + navController = rememberNavController(), + resourceData = + ResourceData( + baseResourceId = "baseId", + baseResourceType = ResourceType.Patient, + computedValuesMap = emptyMap(), + ), + ) +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewGenerator.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewGenerator.kt index 3c5e56f18b..903825a322 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewGenerator.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewGenerator.kt @@ -51,6 +51,7 @@ import org.smartregister.fhircore.engine.configuration.view.PersonalDataProperti import org.smartregister.fhircore.engine.configuration.view.RowProperties import org.smartregister.fhircore.engine.configuration.view.ServiceCardProperties import org.smartregister.fhircore.engine.configuration.view.SpacerProperties +import org.smartregister.fhircore.engine.configuration.view.StackViewProperties import org.smartregister.fhircore.engine.configuration.view.ViewAlignment import org.smartregister.fhircore.engine.configuration.view.ViewProperties import org.smartregister.fhircore.engine.domain.model.ResourceData @@ -119,6 +120,9 @@ fun GenerateView( ViewAlignment.END -> Alignment.End ViewAlignment.CENTER -> Alignment.CenterHorizontally ViewAlignment.NONE -> Alignment.Start + else -> { + Alignment.Start + } }, modifier = modifier @@ -191,6 +195,9 @@ fun GenerateView( ViewAlignment.END -> Alignment.Bottom ViewAlignment.CENTER -> Alignment.CenterVertically ViewAlignment.NONE -> Alignment.CenterVertically + else -> { + Alignment.CenterVertically + } }, modifier = modifier @@ -262,6 +269,13 @@ fun GenerateView( resourceData = resourceData, navController = navController, ) + ViewType.STACK -> + StackView( + modifier = modifier, + stackViewProperties = properties as StackViewProperties, + resourceData = resourceData, + navController = navController, + ) } } } diff --git a/docs/engineering/android-app/configuring/config-types/widget.mdx b/docs/engineering/android-app/configuring/config-types/widget.mdx index 7f26b4a458..283cffe72b 100644 --- a/docs/engineering/android-app/configuring/config-types/widget.mdx +++ b/docs/engineering/android-app/configuring/config-types/widget.mdx @@ -651,6 +651,66 @@ height | The height of the Spacer. | Yes | _ | backgroundColor | The background color of the view, specified as a string in the format "#RRGGBB" or "#AARRGGBB". If this property is null, the view will use its parent's background color. | No | #FFFFFF | padding | Offsets the content of the view by a specific number of pixels. This should be a number | No | 0 | +## Stack view widget +The Stack View Widget can hold a list of child views, allowing you to arrange them one on top of the other. +The Stack View acts as a container that vertically stacks its child views. It provides a way to create layered layouts where elements can be positioned on top of each other. This makes it ideal for use cases like: +- Overlaying informational elements like badges or icons on top of content. +- Creating progress indicators or loaders that sit on top of the main view. +- Implementing toasts or pop-up notifications that appear above the primary content. +### Example JSON: +```json +{ + "viewType": "STACK", + "backgroundColor": "successColor", + "size": 100, + "opacity": 0.1, + "children": [ + { + "viewType": "IMAGE", + "alignment": "CENTER", + "size": 90, + "imageConfig": { + "type": "local", + "reference": "ic_main_image" + }, + "isCircular": false + }, + { + "viewType": "IMAGE", + "size": 38, + "visible": "true", + "imageConfig": { + "type": "local", + "reference": "ic_alert_triangle" + }, + "isCircular": true + } + ] +} +``` +### Config properties of STACK VIEW WIDGET +The Stack View Widget inherits common properties from the ViewProperties class. These properties allow you to configure the general appearance and behavior of the stack, including: +- weight +- backgroundColor +- padding +- borderRadius +- alignment +- fillMaxWidth +- fillMaxHeight +- clickable +- visible + +Stack view specific properties include : + +|Property | Description | Required | Default | +|--|--|:--:|:--:| +viewType | Specifies the type of view. In this case, it's always `STACK` to identify it during runtime | Yes | [STACK]| +backgroundColor | this represents the background color of the container view, specified as a color code in the format "#RRGGBB" or "#AARRGGBB". If null, it indicates a transparent background. | | String? | +opacity | Opacity of the view, ranging from 0.0 (fully transparent) to 1.0 (fully opaque).| No | Float | +size | Height of the stack view in pixels. If not set, the stack will wrap its content | No | Int | +children | List of child view definitions. Each child view definition should be a valid JSON object representing a supported view type (e.g., COMPUNDTEXT, IMAGE, BUTTON)..| | List | + + ## Personal Data Widgets Personal data widget, defines personal data like age to it's corresponding value in a more organised way.It takes a list of mapped items as input.It contains various properties that defines its behavior.