Skip to content

Commit 73e74fb

Browse files
Migrate TaskView to compose (#3617)
- Refactor data collection task fragments to use Compose-based layout - Replace `TaskView` and `TaskViewFactory` with a new `TaskViewLayout` Compose component. - Remove `onCreateTaskView` from `AbstractTaskFragment` and subclasses, replacing it with a `taskHeader` property. - Update `AbstractTaskFragment` to use `createComposeView` for the entire fragment view, integrating header, body, and footer into a unified `TaskViewLayout`. - Migrated all task fragments (Text, Date, Number, Photo, MultipleChoice, Time, Instruction, DrawArea, DropPin, CaptureLocation) to the new layout system. - Introduce `Header` data class to handle task labels and optional icons in a standardized way. - Refactor LoiNameDialog to be a Composable function within AbstractTaskFragment - Move `LoiNameDialog` logic from `renderComposableDialog` into a dedicated `@Composable` function. - Update `onCreateView` to include `LoiNameDialog` when the task is an "Add LOI" task. - Simplify `handleNext` to trigger the dialog via the `loiNameDialogOpen` state. - Consolidate dialog state and value handling within the new `LoiNameDialog` component. - Refactor instructions dialog to use Compose state and centralized layout - Refactor the self-intersection dialog in `DrawAreaTaskFragment` to use Compose state - Refactor PhotoTaskFragment to handle permission dialogs using Compose state - Refactor location permission dialog in CaptureLocationTaskFragment to use Compose - Refactor `InstructionsDialog` logic into a dedicated Composable in `AbstractTaskFragment` - Remove unused xml layouts and associated views - Use compose to reposition the progress bar - Update `AbstractTaskFragment` to track footer position when keyboard visibility changes --------- Co-authored-by: Andreia Ferreira <51242456+andreia-ferreira@users.noreply.github.com>
1 parent 34ff9a7 commit 73e74fb

33 files changed

Lines changed: 431 additions & 622 deletions

app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionFragment.kt

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import androidx.navigation.fragment.findNavController
3333
import androidx.viewpager2.widget.ViewPager2
3434
import dagger.hilt.android.AndroidEntryPoint
3535
import javax.inject.Inject
36-
import kotlinx.coroutines.delay
3736
import kotlinx.coroutines.launch
3837
import org.groundplatform.android.R
3938
import org.groundplatform.android.databinding.DataCollectionFragBinding
@@ -81,19 +80,11 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {
8180
viewPager.isUserInputEnabled = false
8281
viewPager.offscreenPageLimit = 1
8382

84-
viewPager.registerOnPageChangeCallback(
85-
object : ViewPager2.OnPageChangeCallback() {
86-
override fun onPageScrollStateChanged(state: Int) {
87-
super.onPageScrollStateChanged(state)
88-
if (state == ViewPager2.SCROLL_STATE_IDLE) {
89-
viewLifecycleOwner.lifecycleScope.launch {
90-
delay(100) // Wait for the keyboard to close
91-
setProgressBarPosition(view)
92-
}
93-
}
94-
}
83+
viewLifecycleOwner.lifecycleScope.launch {
84+
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
85+
viewModel.footerVerticalPosition.collect { setProgressBarPosition(it) }
9586
}
96-
)
87+
}
9788

9889
// Collect UI state safely across the Fragment view lifecycle.
9990
viewLifecycleOwner.lifecycleScope.launch {
@@ -139,21 +130,15 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {
139130
}
140131
}
141132

142-
private fun setProgressBarPosition(view: View) {
143-
val buttonContainer = view.findViewById<View>(R.id.action_buttons) ?: return
133+
private fun setProgressBarPosition(topPosition: Float) {
134+
val insets = requireView().rootWindowInsets ?: return
135+
val windowInsets = WindowInsetsCompat.toWindowInsetsCompat(insets)
136+
val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
144137

145-
buttonContainer.doOnLayout {
146-
val anchorLocation = IntArray(2)
147-
it.getLocationInWindow(anchorLocation)
138+
val guidelineTop = topPosition.toInt() - systemBarsInsets.top
148139

149-
val windowInsets = WindowInsetsCompat.toWindowInsetsCompat(buttonContainer.rootWindowInsets)
150-
val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
151-
152-
val guidelineTop = anchorLocation[1] - systemBarsInsets.top
153-
154-
if (guidelineTop > 0) {
155-
guideline.setGuidelineBegin(guidelineTop)
156-
}
140+
if (guidelineTop > 0) {
141+
guideline.setGuidelineBegin(guidelineTop)
157142
}
158143
}
159144

app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ internal constructor(
7474
private val dataCollectionInitializer: DataCollectionInitializer,
7575
) : AbstractViewModel() {
7676

77+
/** The current vertical position of the task view footer. */
78+
private val _footerVerticalPosition = MutableStateFlow(0.0f)
79+
val footerVerticalPosition: StateFlow<Float> = _footerVerticalPosition
80+
7781
private val _uiState = MutableStateFlow<DataCollectionUiState>(DataCollectionUiState.Loading)
7882
val uiState: StateFlow<DataCollectionUiState> = _uiState
7983

@@ -367,6 +371,10 @@ internal constructor(
367371
.map { (it as? DataCollectionUiState.Ready)?.currentTaskId == taskId }
368372
.distinctUntilChanged()
369373

374+
fun updateFooterPosition(top: Float) {
375+
_footerVerticalPosition.value = top
376+
}
377+
370378
companion object {
371379
private const val TASK_JOB_ID_KEY = "jobId"
372380
private const val TASK_LOI_ID_KEY = "locationOfInterestId"

app/src/main/java/org/groundplatform/android/ui/datacollection/components/InstructionsDialog.kt

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,58 +15,63 @@
1515
*/
1616
package org.groundplatform.android.ui.datacollection.components
1717

18-
import androidx.compose.foundation.layout.height
19-
import androidx.compose.foundation.layout.width
18+
import androidx.compose.foundation.layout.size
2019
import androidx.compose.material3.AlertDialog
2120
import androidx.compose.material3.Icon
21+
import androidx.compose.material3.MaterialTheme
2222
import androidx.compose.material3.OutlinedButton
2323
import androidx.compose.material3.Text
2424
import androidx.compose.runtime.Composable
25-
import androidx.compose.runtime.mutableStateOf
26-
import androidx.compose.runtime.remember
2725
import androidx.compose.ui.Modifier
2826
import androidx.compose.ui.graphics.vector.ImageVector
2927
import androidx.compose.ui.res.stringResource
3028
import androidx.compose.ui.res.vectorResource
3129
import androidx.compose.ui.tooling.preview.Preview
3230
import androidx.compose.ui.unit.dp
33-
import androidx.compose.ui.unit.sp
3431
import org.groundplatform.android.R
3532
import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport
3633
import org.groundplatform.ui.theme.AppTheme
3734

3835
@Composable
39-
fun InstructionsDialog(iconId: Int, stringId: Int) {
40-
val showDialog = remember { mutableStateOf(true) }
41-
if (showDialog.value) {
42-
AlertDialog(
43-
icon = {
44-
Icon(
45-
imageVector = ImageVector.vectorResource(id = iconId),
46-
contentDescription = "",
47-
modifier = Modifier.width(48.dp).height(48.dp),
48-
)
49-
},
50-
title = { Text(text = stringResource(stringId), fontSize = 18.sp) },
51-
onDismissRequest = {}, // Prevent dismissing the dialog by clicking outside
52-
confirmButton = {}, // Hide confirm button
53-
dismissButton = {
54-
OutlinedButton(onClick = { showDialog.value = false }) {
55-
Text(text = stringResource(R.string.close))
56-
}
57-
},
58-
)
59-
}
36+
fun InstructionsDialog(data: InstructionData, onDismissed: () -> Unit) {
37+
AlertDialog(
38+
icon = {
39+
Icon(
40+
imageVector = ImageVector.vectorResource(id = data.iconId),
41+
contentDescription = null,
42+
modifier = Modifier.size(48.dp),
43+
)
44+
},
45+
title = {
46+
Text(text = stringResource(data.stringId), style = MaterialTheme.typography.titleLarge)
47+
},
48+
onDismissRequest = {}, // Prevent dismissing the dialog by clicking outside
49+
confirmButton = {}, // Hide confirm button
50+
dismissButton = {
51+
OutlinedButton(onClick = onDismissed) { Text(text = stringResource(R.string.close)) }
52+
},
53+
)
6054
}
6155

56+
/**
57+
* Data class representing the visual elements of an instruction dialog.
58+
*
59+
* @property iconId The resource ID of the drawable icon to display.
60+
* @property stringId The resource ID of the string message to display.
61+
*/
62+
data class InstructionData(val iconId: Int, val stringId: Int)
63+
6264
@Composable
6365
@Preview
6466
@ExcludeFromJacocoGeneratedReport
6567
private fun PreviewInstructionsDialog() {
6668
AppTheme {
6769
InstructionsDialog(
68-
iconId = R.drawable.touch_app_24,
69-
stringId = R.string.draw_area_task_instruction,
70-
)
70+
data =
71+
InstructionData(
72+
iconId = R.drawable.touch_app_24,
73+
stringId = R.string.draw_area_task_instruction,
74+
)
75+
) {}
7176
}
7277
}

app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskView.kt

Lines changed: 0 additions & 74 deletions
This file was deleted.

app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskViewFactory.kt

Lines changed: 0 additions & 52 deletions
This file was deleted.

0 commit comments

Comments
 (0)