Vis dyplenket tilfluktsrom i lista, selv utenfor topp-3 (hybrid)

Når en dyplenke (eller markørtap) velger et tilfluktsrom som ikke er
blant de N nærmeste, blir det nå appendet til bunnpanelets liste med
et tydelig "Valgt – utenfor nærområdet"-badge, og lista scroller til
den valgte raden. Hvis valget er innenfor topp-N, scrollelementet
fortsatt synes — løser begge symptomene som rapporten pekte på.

Endringer:
- Ny ShelterListItem(swd, isOutsideNearest)-wrapper i adapteren
- ShelterListAdapter: viser badge + a11y-suffiks når isOutsideNearest=true
- item_shelter.xml: badge-TextView (orange bakgrunn, hvit tekst, gone som default)
- MainActivity: rebuildShelterList()-helper bygger top-N + maybe-appended,
  smoothScrollToPosition(selectedIdx) sikrer synlig markering
- Strings i en/nb/nn

Forgejo: #13
This commit is contained in:
Ole-Morten Duesund 2026-04-29 16:49:28 +02:00
commit 1fb9f14ad4
7 changed files with 109 additions and 33 deletions

View file

@ -44,6 +44,7 @@ import no.naiv.tilfluktsrom.location.ShelterFinder
import no.naiv.tilfluktsrom.location.ShelterWithDistance
import no.naiv.tilfluktsrom.ui.CivilDefenseInfoDialog
import no.naiv.tilfluktsrom.ui.ShelterListAdapter
import no.naiv.tilfluktsrom.ui.ShelterListItem
import no.naiv.tilfluktsrom.util.DistanceUtils
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
@ -486,25 +487,60 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
allShelters, location.latitude, location.longitude, NEAREST_COUNT
)
// Highlight which nearest-list item matches the current selection
val selectedIdx = if (selectedShelter != null) {
nearestShelters.indexOfFirst { it.shelter.lokalId == selectedShelter!!.shelter.lokalId }
} else -1
shelterAdapter.submitList(nearestShelters)
shelterAdapter.selectPosition(selectedIdx)
if (userSelectedShelter && selectedShelter != null) {
// Recalculate distance/bearing for the user's picked shelter
refreshSelectedShelterDistance(location)
rebuildShelterList()
updateSelectedShelterUI()
} else if (nearestShelters.isNotEmpty()) {
// Auto-select nearest; selectShelter handles list rebuild + UI
selectShelter(nearestShelters[0])
} else {
// Auto-select nearest
if (nearestShelters.isNotEmpty()) {
selectShelter(nearestShelters[0])
}
rebuildShelterList()
updateSelectedShelterUI()
}
}
/**
* Rebuild the bottom-sheet list from the current nearest set + selection.
*
* Hybrid behaviour for Forgejo #13: when a shelter has been explicitly
* selected (deep link, marker tap, ...) and is *not* among the N nearest,
* append it to the list with an "outside nearest" badge so the user can
* see what they selected. The list also auto-scrolls to the selected
* row, so a manually-picked nearby entry comes into view too.
*/
private fun rebuildShelterList() {
val items = nearestShelters
.map { ShelterListItem(it, isOutsideNearest = false) }
.toMutableList()
val selected = selectedShelter
val isSelectedAmongNearest = selected != null &&
nearestShelters.any { it.shelter.lokalId == selected.shelter.lokalId }
if (selected != null && !isSelectedAmongNearest) {
// Only flag as "outside nearest" when there *is* a nearest list to
// contrast with - otherwise the selection is just the only entry.
items.add(
ShelterListItem(
selected,
isOutsideNearest = nearestShelters.isNotEmpty()
)
)
}
updateSelectedShelterUI()
shelterAdapter.submitList(items)
val selectedIdx = if (selected != null) {
items.indexOfFirst { it.swd.shelter.lokalId == selected.shelter.lokalId }
} else -1
shelterAdapter.selectPosition(selectedIdx)
if (selectedIdx >= 0) {
binding.shelterList.post {
binding.shelterList.smoothScrollToPosition(selectedIdx)
}
}
}
/**
@ -515,10 +551,7 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
selectedShelter = swd
currentLocation?.let { refreshSelectedShelterDistance(it) }
// Update list highlight
val idx = nearestShelters.indexOfFirst { it.shelter.lokalId == swd.shelter.lokalId }
shelterAdapter.selectPosition(idx)
rebuildShelterList()
updateSelectedShelterUI()
}

View file

@ -2,6 +2,7 @@ package no.naiv.tilfluktsrom.ui
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@ -11,12 +12,23 @@ import no.naiv.tilfluktsrom.databinding.ItemShelterBinding
import no.naiv.tilfluktsrom.location.ShelterWithDistance
import no.naiv.tilfluktsrom.util.DistanceUtils
/**
* One row in the bottom-sheet list. The list normally holds the N nearest
* shelters to the user, but a deep-linked / explicitly-selected shelter that
* is *not* among them is appended with isOutsideNearest=true so the user can
* see what they picked. See Forgejo #13 / beads tilfluktsrom-9sf.
*/
data class ShelterListItem(
val swd: ShelterWithDistance,
val isOutsideNearest: Boolean
)
/**
* Adapter for the list of nearest shelters shown in the bottom sheet.
*/
class ShelterListAdapter(
private val onShelterSelected: (ShelterWithDistance) -> Unit
) : ListAdapter<ShelterWithDistance, ShelterListAdapter.ViewHolder>(DIFF_CALLBACK) {
) : ListAdapter<ShelterListItem, ShelterListAdapter.ViewHolder>(DIFF_CALLBACK) {
private var selectedPosition = 0
@ -42,23 +54,34 @@ class ShelterListAdapter(
private val binding: ItemShelterBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: ShelterWithDistance, isSelected: Boolean) {
fun bind(item: ShelterListItem, isSelected: Boolean) {
val ctx = binding.root.context
binding.shelterAddress.text = item.shelter.adresse
binding.shelterDistance.text = DistanceUtils.formatDistance(item.distanceMeters)
val swd = item.swd
binding.shelterAddress.text = swd.shelter.adresse
binding.shelterDistance.text = DistanceUtils.formatDistance(swd.distanceMeters)
binding.shelterCapacity.text = ctx.getString(
R.string.shelter_capacity, item.shelter.plasser
R.string.shelter_capacity, swd.shelter.plasser
)
binding.shelterRoomNr.text = ctx.getString(
R.string.shelter_room_nr, item.shelter.romnr
R.string.shelter_room_nr, swd.shelter.romnr
)
binding.root.contentDescription = ctx.getString(
binding.outsideNearestBadge.visibility =
if (item.isOutsideNearest) View.VISIBLE else View.GONE
// Build accessible description; suffix the badge text so screen-
// reader users learn the same context that sighted users see.
val baseDesc = ctx.getString(
R.string.content_desc_shelter_item,
item.shelter.adresse,
DistanceUtils.formatDistance(item.distanceMeters),
item.shelter.plasser
swd.shelter.adresse,
DistanceUtils.formatDistance(swd.distanceMeters),
swd.shelter.plasser
)
binding.root.contentDescription = if (item.isOutsideNearest) {
ctx.getString(R.string.shelter_outside_nearest_badge) + ". " + baseDesc
} else {
baseDesc
}
binding.root.isSelected = isSelected
binding.root.alpha = if (isSelected) 1.0f else 0.7f
@ -68,18 +91,18 @@ class ShelterListAdapter(
val pos = adapterPosition
if (pos != RecyclerView.NO_POSITION) {
selectPosition(pos)
onShelterSelected(getItem(pos))
onShelterSelected(getItem(pos).swd)
}
}
}
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ShelterWithDistance>() {
override fun areItemsTheSame(a: ShelterWithDistance, b: ShelterWithDistance) =
a.shelter.lokalId == b.shelter.lokalId
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ShelterListItem>() {
override fun areItemsTheSame(a: ShelterListItem, b: ShelterListItem) =
a.swd.shelter.lokalId == b.swd.shelter.lokalId
override fun areContentsTheSame(a: ShelterWithDistance, b: ShelterWithDistance) =
override fun areContentsTheSame(a: ShelterListItem, b: ShelterListItem) =
a == b
}
}

View file

@ -9,6 +9,21 @@
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<TextView
android:id="@+id/outsideNearestBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:background="@color/shelter_primary"
android:paddingHorizontal="8dp"
android:paddingVertical="2dp"
android:text="@string/shelter_outside_nearest_badge"
android:textColor="@color/white"
android:textSize="11sp"
android:textStyle="bold"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/shelterAddress"
android:layout_width="wrap_content"

View file

@ -62,6 +62,7 @@
<!-- Tilgjengelighet -->
<string name="direction_arrow_description">Retning til tilfluktsrom, %s unna</string>
<string name="content_desc_shelter_item">%1$s, %2$s, %3$d plasser</string>
<string name="shelter_outside_nearest_badge">Valgt utenfor nærområdet</string>
<string name="compass_accuracy_warning">Upresist kompass - %s</string>
<string name="a11y_map">Tilfluktsromkart</string>
<string name="a11y_compass">Kompassnavigasjon</string>

View file

@ -62,6 +62,7 @@
<!-- Tilgjenge -->
<string name="direction_arrow_description">Retning til tilfluktsrom, %s unna</string>
<string name="content_desc_shelter_item">%1$s, %2$s, %3$d plassar</string>
<string name="shelter_outside_nearest_badge">Vald utanfor nærområdet</string>
<string name="compass_accuracy_warning">Upresis kompass - %s</string>
<string name="a11y_map">Tilfluktsromkart</string>
<string name="a11y_compass">Kompassnavigasjon</string>

View file

@ -62,6 +62,9 @@
<!-- Accessibility -->
<string name="direction_arrow_description">Direction to shelter, %s away</string>
<string name="content_desc_shelter_item">%1$s, %2$s, %3$d places</string>
<!-- Badge shown on a list row that is not among the nearest shelters but
was explicitly selected (e.g. via a deep link). Forgejo #13. -->
<string name="shelter_outside_nearest_badge">Selected (outside nearest)</string>
<string name="compass_accuracy_warning">Low accuracy - %s</string>
<string name="a11y_map">Shelter map</string>
<string name="a11y_compass">Compass navigation</string>