Legg til TalkBack-støtte og nordindikator på kompasset

Tilgjengelegheit (Android + PWA):
- Semantiske landemerke (header, main, aside, role=dialog)
- aria-live-regionar for statusoppdateringar og lasteoverlegg
- Fokusindikatorar (:focus-visible) og prefers-reduced-motion
- Auka trykkmål til 48dp (infoknapp, oppdater, del, widget)
- contentDescription på kart, kompass og framdriftsindikator
- aria-current og role=listitem på tilfluktsromliste
- Fokusfangst og fokusgjenoppretting i lasteoverlegg
- Ikkje-farge-indikator (▶) for valt tilfluktsrom
- Dynamisk lang-attributt basert på oppdaga språk
- Lokaliserte aria-label (en/nb/nn)

Nordindikator:
- DirectionArrowView teiknar diskret «N»-markør på omkrinsen
- Roterer uavhengig av hovudpila for kompasskalibrering
- Berre på stor kompassvisning, ikkje minipila

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-23 14:02:32 +01:00
commit 6ba35add2f
17 changed files with 240 additions and 36 deletions

View file

@ -522,10 +522,11 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
R.string.direction_arrow_description, distanceText
)
// Update compass view
// Update compass view (large arrow gets a north indicator)
binding.compassDistanceText.text = distanceText
binding.compassAddressText.text = selected.shelter.adresse
binding.directionArrow.setDirection(arrowAngle)
binding.directionArrow.setNorthAngle(-deviceHeading)
binding.directionArrow.contentDescription = getString(
R.string.direction_arrow_description, distanceText
)
@ -840,6 +841,7 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
val arrowAngle = bearing - deviceHeading
binding.directionArrow.setDirection(arrowAngle)
binding.directionArrow.setNorthAngle(-deviceHeading)
binding.miniArrow.setDirection(arrowAngle)
}

View file

@ -17,6 +17,9 @@ import no.naiv.tilfluktsrom.R
* rotationAngle = shelterBearing - deviceHeading
* This gives the direction the user needs to walk, adjusted for which
* way they're currently facing.
*
* Optionally draws a discrete north indicator on the perimeter so users
* can validate compass calibration against a known direction.
*/
class DirectionArrowView @JvmOverloads constructor(
context: Context,
@ -25,6 +28,7 @@ class DirectionArrowView @JvmOverloads constructor(
) : View(context, attrs, defStyleAttr) {
private var rotationAngle = 0f
private var northAngle = Float.NaN
private val arrowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getColor(R.color.shelter_primary)
@ -37,7 +41,18 @@ class DirectionArrowView @JvmOverloads constructor(
strokeWidth = 4f
}
private val northPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = 0x99CFD8DC.toInt() // text_secondary at ~60% opacity
style = Paint.Style.FILL
}
private val northTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = 0x99CFD8DC.toInt()
textAlign = Paint.Align.CENTER
}
private val arrowPath = Path()
private val northPath = Path()
/**
* Set the rotation angle in degrees.
@ -48,6 +63,16 @@ class DirectionArrowView @JvmOverloads constructor(
invalidate()
}
/**
* Set the angle to north in the view's coordinate space.
* This is typically -deviceHeading (where north is on screen).
* Set to Float.NaN to hide the north indicator.
*/
fun setNorthAngle(degrees: Float) {
northAngle = degrees
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
@ -55,6 +80,11 @@ class DirectionArrowView @JvmOverloads constructor(
val cy = height / 2f
val size = minOf(width, height) * 0.4f
// Draw north indicator first (behind the main arrow)
if (!northAngle.isNaN()) {
drawNorthIndicator(canvas, cx, cy, size)
}
canvas.save()
canvas.rotate(rotationAngle, cx, cy)
@ -74,4 +104,32 @@ class DirectionArrowView @JvmOverloads constructor(
canvas.restore()
}
/**
* Draw a small north indicator: a tiny triangle and "N" label
* placed on the perimeter of the view, pointing inward toward center.
*/
private fun drawNorthIndicator(canvas: Canvas, cx: Float, cy: Float, arrowSize: Float) {
val radius = arrowSize * 1.35f
val tickSize = arrowSize * 0.1f
// Scale "N" text relative to the view
northTextPaint.textSize = arrowSize * 0.18f
canvas.save()
canvas.rotate(northAngle, cx, cy)
// Small triangle at the top of the perimeter circle
northPath.reset()
northPath.moveTo(cx, cy - radius)
northPath.lineTo(cx - tickSize, cy - radius - tickSize * 1.8f)
northPath.lineTo(cx + tickSize, cy - radius - tickSize * 1.8f)
northPath.close()
canvas.drawPath(northPath, northPaint)
// "N" label just outside the triangle
canvas.drawText("N", cx, cy - radius - tickSize * 2.2f, northTextPaint)
canvas.restore()
}
}

View file

@ -31,14 +31,15 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:accessibilityLiveRegion="polite"
android:textColor="@color/status_text"
android:textSize="12sp"
tools:text="@string/status_ready" />
<ImageButton
android:id="@+id/infoButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_civil_defense_info"
android:src="@drawable/ic_info"
@ -46,8 +47,8 @@
<ImageButton
android:id="@+id/refreshButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_refresh"
android:src="@drawable/ic_refresh"
@ -71,6 +72,7 @@
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:contentDescription="@string/a11y_map"
app:layout_constraintTop_toBottomOf="@id/statusBar"
app:layout_constraintBottom_toTopOf="@id/bottomSheet" />
@ -80,6 +82,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/compass_bg"
android:contentDescription="@string/a11y_compass"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/statusBar"
app:layout_constraintBottom_toTopOf="@id/bottomSheet">
@ -223,8 +226,8 @@
<ImageButton
android:id="@+id/shareButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_share"
@ -249,6 +252,8 @@
android:background="@color/loading_bg"
android:clickable="true"
android:focusable="true"
android:importantForAccessibility="yes"
android:accessibilityLiveRegion="assertive"
android:visibility="gone">
<LinearLayout
@ -262,6 +267,7 @@
android:id="@+id/loadingProgress"
android:layout_width="64dp"
android:layout_height="64dp"
android:contentDescription="@string/status_loading"
android:indeterminate="true" />
<TextView

View file

@ -65,9 +65,10 @@
<ImageView
android:id="@+id/widgetRefreshButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="4dp"
android:padding="8dp"
android:contentDescription="@string/action_refresh"
android:src="@drawable/ic_refresh" />
</LinearLayout>

View file

@ -67,6 +67,8 @@
<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="compass_accuracy_warning">Upresist kompass - %s</string>
<string name="a11y_map">Tilfluktsromkart</string>
<string name="a11y_compass">Kompassnavigasjon</string>
<!-- Sivilforsvar -->
<string name="action_civil_defense_info">Sivilforsvarsinformasjon</string>

View file

@ -67,6 +67,8 @@
<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="compass_accuracy_warning">Upresis kompass - %s</string>
<string name="a11y_map">Tilfluktsromkart</string>
<string name="a11y_compass">Kompassnavigasjon</string>
<!-- Sivilforsvar -->
<string name="action_civil_defense_info">Sivilforsvarsinformasjon</string>

View file

@ -67,6 +67,8 @@
<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>
<string name="compass_accuracy_warning">Low accuracy - %s</string>
<string name="a11y_map">Shelter map</string>
<string name="a11y_compass">Compass navigation</string>
<!-- Civil defense info -->
<string name="action_civil_defense_info">Civil defense information</string>