Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Factor out job cards into Composables for new data entry points #2892

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SnapHelper
import com.google.android.ground.R
import com.google.android.ground.coroutines.ApplicationScope
Expand Down Expand Up @@ -105,8 +103,8 @@ class HomeScreenMapContainerFragment : AbstractMapContainerFragment() {
}

// Bind data for cards
mapContainerViewModel.getMapCardUiData().launchWhenStartedAndCollect { (mapCards, loiCount) ->
adapter.updateData(canUserSubmitData, mapCards, loiCount - 1)
mapContainerViewModel.getMapCardUiData().launchWhenStartedAndCollect { (loiCard, jobCards) ->
sufyanAbbasi marked this conversation as resolved.
Show resolved Hide resolved
adapter.updateData(canUserSubmitData, loiCard, jobCards)
}
}

Expand Down Expand Up @@ -259,37 +257,9 @@ class HomeScreenMapContainerFragment : AbstractMapContainerFragment() {
val recyclerViewBinding = LoiCardsRecyclerViewBinding.inflate(layoutInflater, container, true)
val recyclerView = recyclerViewBinding.recyclerView
recyclerView.adapter = adapter
recyclerView.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
val lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
val firstCompletelyVisiblePosition =
layoutManager.findFirstCompletelyVisibleItemPosition()
var midPosition = (firstVisiblePosition + lastVisiblePosition) / 2

// Focus the last card
if (firstCompletelyVisiblePosition > midPosition) {
midPosition = firstCompletelyVisiblePosition
}

adapter.focusItemAtIndex(midPosition)
}
}
)

val helper: SnapHelper = PagerSnapHelper()
helper.attachToRecyclerView(recyclerView)

mapContainerViewModel.loiClicks.launchWhenStartedAndCollect {
val index = it?.let { adapter.getIndex(it) } ?: -1
if (index != -1) {
recyclerView.scrollToPosition(index)
adapter.focusItemAtIndex(index)
}
}
}

private fun navigateToDataCollectionFragment(cardUiData: MapCardUiData) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ internal constructor(
*/
private val loisInViewport: StateFlow<List<LocationOfInterest>>

/** [LocationOfInterest] clicked by the user. */
val loiClicks: MutableStateFlow<LocationOfInterest?> = MutableStateFlow(null)
/** [Feature] clicked by the user. */
val featureClicked: MutableStateFlow<Feature?> = MutableStateFlow(null)

/**
* List of [Job]s which allow LOIs to be added during field collection, populated only when zoomed
Expand Down Expand Up @@ -184,12 +184,20 @@ internal constructor(
* Returns a flow of [MapCardUiData] associated with the active survey's LOIs and adhoc jobs for
* displaying the cards.
*/
fun getMapCardUiData(): Flow<Pair<List<MapCardUiData>, Int>> =
loisInViewport.combine(adHocLoiJobs) { lois, jobs ->
val loiCards = lois.map { MapCardUiData.LoiCardUiData(it) }
val jobCards = jobs.map { MapCardUiData.AddLoiCardUiData(it) }

Pair(loiCards + jobCards, lois.size)
fun getMapCardUiData():
Flow<Pair<MapCardUiData.LoiCardUiData?, List<MapCardUiData.AddLoiCardUiData>>> =
combine(loisInViewport, featureClicked, adHocLoiJobs) { loisInView, feature, jobs ->
val loiCard =
loisInView
.filter { it.geometry == feature?.geometry }
.firstOrNull()
?.let { MapCardUiData.LoiCardUiData(it) }
if (loiCard == null && feature != null) {
// The feature is not in view anymore.
featureClicked.value = null
}
val jobCard = jobs.map { MapCardUiData.AddLoiCardUiData(it) }
Pair(loiCard, jobCard)
}

private fun updatedLoiSelectedStates(
Expand All @@ -207,12 +215,7 @@ internal constructor(
* list of provided features is empty.
*/
fun onFeatureClicked(features: Set<Feature>) {
val geometry = features.map { it.geometry }.minByOrNull { it.area } ?: return
for (loi in loisInViewport.value) {
if (loi.geometry == geometry) {
loiClicks.value = loi
}
}
featureClicked.value = features.minByOrNull { it.geometry.area }
}

suspend fun updateDataSharingConsent(dataSharingTerms: Boolean) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.google.android.ground.ui.home.mapcontainer.cards
sufyanAbbasi marked this conversation as resolved.
Show resolved Hide resolved

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import com.google.android.ground.R

@Composable
fun JobSelectionDialog(
selectedJobId: String,
jobs: List<MapCardUiData.AddLoiCardUiData>,
onJobSelection: (MapCardUiData.AddLoiCardUiData) -> Unit,
onConfirmRequest: (MapCardUiData.AddLoiCardUiData) -> Unit,
onDismissRequest: () -> Unit,
) {
AlertDialog(
containerColor = MaterialTheme.colorScheme.surface,
onDismissRequest = onDismissRequest,
title = {
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.add_site), fontSize = 5.em)
}
},
text = {
Column {
Spacer(Modifier.height(16.dp))
jobs.forEach { JobSelectionRow(it, onJobSelection, it.job.id === selectedJobId) }
}
},
confirmButton = {
Button(
onClick = { jobs.find { it.job.id == selectedJobId }?.let { onConfirmRequest(it) } },
contentPadding = PaddingValues(25.dp, 0.dp),
enabled = selectedJobId != "",
) {
Text(stringResource(R.string.begin))
}
},
dismissButton = {
OutlinedButton(onClick = { onDismissRequest() }) {
Text(text = stringResource(R.string.cancel))
}
},
)
}

@Composable
fun JobSelectionRow(
job: MapCardUiData.AddLoiCardUiData,
onJobSelection: (MapCardUiData.AddLoiCardUiData) -> Unit,
selected: Boolean,
) {
Row(modifier = Modifier.fillMaxWidth().padding(8.dp).clickable { onJobSelection(job) }) {
Text(job.job.name ?: stringResource(R.string.unnamed_job), fontSize = 18.sp)
if (selected) {
Icon(
modifier = Modifier.size(25.dp),
painter = painterResource(id = R.drawable.baseline_check_24),
contentDescription = "selected",
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,20 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.ground.R
import com.google.android.ground.databinding.AddLoiCardItemBinding
import com.google.android.ground.databinding.LoiCardItemBinding
import com.google.android.ground.model.job.Job
import com.google.android.ground.model.locationofinterest.LocationOfInterest
import com.google.android.ground.ui.common.LocationOfInterestHelper
import com.google.android.ground.ui.theme.AppTheme

/**
* An implementation of [RecyclerView.Adapter] that associates [LocationOfInterest] data with the
Expand All @@ -37,9 +44,8 @@ class MapCardAdapter(
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

private var canUserSubmitData: Boolean = false
private var focusedIndex: Int = 0
private var indexOfLastLoi: Int = -1
private val itemsList: MutableList<MapCardUiData> = mutableListOf()
private var activeLoi: MapCardUiData.LoiCardUiData? = null
private val newLoiJobs: MutableList<MapCardUiData.AddLoiCardUiData> = mutableListOf()
private var cardFocusedListener: ((MapCardUiData?) -> Unit)? = null
private lateinit var collectDataListener: (MapCardUiData) -> Unit

Expand All @@ -58,44 +64,37 @@ class MapCardAdapter(
}

override fun getItemViewType(position: Int): Int =
if (position <= indexOfLastLoi) {
if (activeLoi != null) {
R.layout.loi_card_item
} else {
// Assume we don't render add LOI option unless we know the job allows it.
R.layout.add_loi_card_item
}

/** Binds [LocationOfInterest] data to [LoiViewHolder] or [AddLoiCardViewHolder]. */
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiData = itemsList[position]
val cardHolder = bindViewHolder(uiData, holder)
if (focusedIndex == position) {
cardFocusedListener?.invoke(uiData)
val loi = activeLoi
if (loi != null) {
val cardHolder = bindLoiCardViewHolder(loi, holder)
cardHolder.setOnClickListener { collectDataListener(loi) }
} else {
bindAddLoiCardViewHolder(newLoiJobs, holder) { job -> collectDataListener(job) }
}
cardHolder.setOnClickListener { collectDataListener(uiData) }
}

/** Returns the size of the list. */
override fun getItemCount() = itemsList.size

/** Updates the currently focused item. */
fun focusItemAtIndex(newIndex: Int) {
if (newIndex < 0 || newIndex >= itemCount || focusedIndex == newIndex) return

focusedIndex = newIndex
notifyDataSetChanged()
}
override fun getItemCount() = if (activeLoi != null || newLoiJobs.isNotEmpty()) 1 else 0

/** Overwrites existing cards. */
fun updateData(
canUserSubmitData: Boolean,
newItemsList: List<MapCardUiData>,
indexOfLastLoi: Int,
loiCard: MapCardUiData.LoiCardUiData?,
jobCards: List<MapCardUiData.AddLoiCardUiData>,
) {
this.canUserSubmitData = canUserSubmitData
this.indexOfLastLoi = indexOfLastLoi
itemsList.clear()
itemsList.addAll(newItemsList)
focusedIndex = 0
activeLoi = loiCard
newLoiJobs.clear()
newLoiJobs.addAll(jobCards)
notifyDataSetChanged()
}

Expand All @@ -107,32 +106,18 @@ class MapCardAdapter(
this.collectDataListener = listener
}

private fun bindViewHolder(
uiData: MapCardUiData,
private fun bindLoiCardViewHolder(
loiData: MapCardUiData.LoiCardUiData,
holder: RecyclerView.ViewHolder,
): CardViewHolder =
when (uiData) {
is MapCardUiData.LoiCardUiData -> {
(holder as LoiViewHolder).apply { bind(canUserSubmitData, uiData.loi) }
}
is MapCardUiData.AddLoiCardUiData -> {
(holder as AddLoiCardViewHolder).apply { bind(canUserSubmitData, uiData.job) }
}
}
): LoiViewHolder = (holder as LoiViewHolder).apply { bind(canUserSubmitData, loiData.loi) }

/** Returns index of job card with the given [LocationOfInterest]. */
fun getIndex(loi: LocationOfInterest): Int {
for ((index, item) in itemsList.withIndex()) {
if (item is MapCardUiData.LoiCardUiData && item.loi == loi) {
return index
}
}
return -1
}
private fun bindAddLoiCardViewHolder(
addLoiJobData: List<MapCardUiData.AddLoiCardUiData>,
holder: RecyclerView.ViewHolder,
callback: (MapCardUiData.AddLoiCardUiData) -> Unit,
): AddLoiCardViewHolder = (holder as AddLoiCardViewHolder).apply { bind(addLoiJobData, callback) }

abstract class CardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun setOnClickListener(callback: () -> Unit)
}
abstract class CardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {}

/** View item representing the [LocationOfInterest] data in the list. */
class LoiViewHolder(
Expand All @@ -152,25 +137,49 @@ class MapCardAdapter(
}
}

override fun setOnClickListener(callback: () -> Unit) {
fun setOnClickListener(callback: () -> Unit) {
binding.collectData.setOnClickListener { callback() }
}
}

/** View item representing the Add Loi Job data in the list. */
class AddLoiCardViewHolder(internal val binding: AddLoiCardItemBinding) :
CardViewHolder(binding.root) {
private val jobDialogOpened = mutableStateOf(false)

fun bind(canUserSubmitData: Boolean, job: Job) {
fun bind(
jobs: List<MapCardUiData.AddLoiCardUiData>,
callback: (MapCardUiData.AddLoiCardUiData) -> Unit,
) {
with(binding) {
jobName.text = job.name
collectData.visibility =
if (canUserSubmitData && job.hasTasks()) View.VISIBLE else View.GONE
loiCard.setOnClickListener {
jobDialogOpened.value = true
(root as ViewGroup).addView(
ComposeView(root.context).apply {
setContent { AppTheme { ShowJobSelectionDialog(jobs, callback, jobDialogOpened) } }
}
)
}
}
}

override fun setOnClickListener(callback: () -> Unit) {
binding.collectData.setOnClickListener { callback() }
@Composable
fun ShowJobSelectionDialog(
jobs: List<MapCardUiData.AddLoiCardUiData>,
callback: (MapCardUiData.AddLoiCardUiData) -> Unit,
jobDialogOpened: MutableState<Boolean>,
) {
var selectedJobId by rememberSaveable { mutableStateOf(jobs[0].job.id) }
var openJobsDialog by rememberSaveable { jobDialogOpened }
if (openJobsDialog) {
JobSelectionDialog(
selectedJobId = selectedJobId,
jobs = jobs,
onJobSelection = { selectedJobId = it.job.id },
onConfirmRequest = { callback(it) },
onDismissRequest = { openJobsDialog = false },
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ class GoogleMapsFragment : SupportMapFragment(), MapFragment {
val clickedPolygons = featureManager.getIntersectingPolygons(latLng)
if (clickedPolygons.isNotEmpty()) {
viewLifecycleOwner.lifecycleScope.launch { featureClicks.emit(clickedPolygons) }
} else {
viewLifecycleOwner.lifecycleScope.launch { featureClicks.emit(emptySet()) }
}
}

Expand Down
Loading
Loading