summaryrefslogtreecommitdiff
path: root/android-app
diff options
context:
space:
mode:
Diffstat (limited to 'android-app')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt46
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt18
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt85
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt127
-rw-r--r--android-app/app/src/main/res/layout/layout_track_detail_sheet.xml179
5 files changed, 321 insertions, 134 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt
index b2181c9..b4a0a91 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt
@@ -1,5 +1,6 @@
package org.terst.nav.track
+import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -11,6 +12,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.button.MaterialButton
import kotlinx.coroutines.launch
import org.terst.nav.MainActivity
import org.terst.nav.R
@@ -32,17 +34,19 @@ class SavedTracksFragment : Fragment() {
(requireActivity() as MainActivity).hideOverlays()
}
- val rv = view.findViewById<RecyclerView>(R.id.rv_saved_tracks)
- val empty = view.findViewById<TextView>(R.id.tv_empty)
+ val rv = view.findViewById<RecyclerView>(R.id.rv_saved_tracks)
+ val layoutEmpty = view.findViewById<View>(R.id.layout_empty)
+ val tvEmpty = view.findViewById<TextView>(R.id.tv_empty)
+ val btnSetup = view.findViewById<MaterialButton>(R.id.btn_setup_storage)
+
rv.layoutManager = LinearLayoutManager(requireContext())
rv.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
val adapter = SavedTrackAdapter { track ->
viewModel.selectTrack(track)
- parentFragmentManager
- .beginTransaction()
+ parentFragmentManager.beginTransaction()
.replace(R.id.fragment_container, TrackDetailSheet.newInstance())
- .addToBackStack(null)
+ .addToBackStack("track_detail")
.commit()
}
rv.adapter = adapter
@@ -50,18 +54,42 @@ class SavedTracksFragment : Fragment() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.savedTracks.collect { tracks ->
adapter.submitList(tracks)
- empty.visibility = if (tracks.isEmpty()) View.VISIBLE else View.GONE
- rv.visibility = if (tracks.isEmpty()) View.GONE else View.VISIBLE
+ val empty = tracks.isEmpty()
+ layoutEmpty.visibility = if (empty) View.VISIBLE else View.GONE
+ rv.visibility = if (empty) View.GONE else View.VISIBLE
+ }
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.safState.collect { state ->
+ when (state) {
+ SafState.NOT_CONFIGURED -> {
+ tvEmpty.text = "No saved tracks yet.\nRecord a trip to see it here."
+ btnSetup.text = "Set up track storage"
+ btnSetup.visibility = View.VISIBLE
+ }
+ SafState.PERMISSION_LOST -> {
+ tvEmpty.text = "Track storage needs re-authorization."
+ btnSetup.text = "Restore track access"
+ btnSetup.visibility = View.VISIBLE
+ }
+ SafState.CONFIGURED -> btnSetup.visibility = View.GONE
+ }
+ }
+ }
+ btnSetup.setOnClickListener {
+ (requireActivity() as MainActivity).launchSafPicker()
}
}
}
}
-private val DATE_FMT: DateTimeFormatter =
+internal val DATE_FMT: DateTimeFormatter =
DateTimeFormatter.ofPattern("EEE dd MMM yyyy · HH:mm", Locale.getDefault())
.withZone(ZoneId.systemDefault())
-class SavedTrackAdapter(
+internal class SavedTrackAdapter(
private val onSelect: (SavedTrack) -> Unit
) : RecyclerView.Adapter<SavedTrackAdapter.VH>() {
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt
index 6616acf..9668fd1 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt
@@ -3,20 +3,22 @@ package org.terst.nav.track
import kotlin.math.abs
object TackDetector {
- private const val HALF_WIN = 2
- private const val MIN_DELTA = 60.0
- private const val MAX_DELTA = 140.0
- private const val SKIP_AFTER = 5
- private const val STABILITY_MAX = 30.0
+ private const val HALF_WIN = 2
+ private const val MIN_DELTA = 60.0
+ private const val MAX_DELTA = 140.0
+ private const val STABILITY_MAX = 30.0
+ private const val MIN_GAP_MS = 45_000L // 45 s minimum between tacks
+ private const val START_SKIP_MS = 120_000L // skip first 2 min (GPS cold-start noise)
fun detectTacks(points: List<TrackPoint>): List<TackEvent> {
if (points.size < 2 * HALF_WIN + 1) return emptyList()
val results = mutableListOf<TackEvent>()
- var nextCandidate = 0
+ var lastTackMs = Long.MIN_VALUE
for (i in HALF_WIN until points.size - HALF_WIN) {
- if (i < nextCandidate) continue
+ if (points[i].timestampMs - points.first().timestampMs < START_SKIP_MS) continue
+ if (points[i].timestampMs - lastTackMs < MIN_GAP_MS) continue
val spreadBefore = abs(angleDiff(points[i - 2].cogDeg, points[i - 1].cogDeg))
val spreadAfter = abs(angleDiff(points[i + 1].cogDeg, points[i + 2].cogDeg))
@@ -28,7 +30,7 @@ object TackDetector {
if (delta in MIN_DELTA..MAX_DELTA) {
results += TackEvent(i, points[i].lat, points[i].lon, cogBefore, cogAfter)
- nextCandidate = i + SKIP_AFTER
+ lastTackMs = points[i].timestampMs
}
}
return results
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt
index c1764a8..158e513 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt
@@ -10,6 +10,23 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+<<<<<<< Updated upstream
+=======
+import org.maplibre.android.camera.CameraUpdateFactory
+import org.maplibre.android.geometry.LatLng
+import org.maplibre.android.geometry.LatLngBounds
+import org.maplibre.android.maps.MapLibreMap
+import org.maplibre.android.maps.MapView
+import org.maplibre.android.maps.Style
+import org.maplibre.android.style.layers.CircleLayer
+import org.maplibre.android.style.layers.LineLayer
+import org.maplibre.android.style.layers.PropertyFactory
+import org.maplibre.android.style.sources.GeoJsonSource
+import org.maplibre.geojson.Feature
+import org.maplibre.geojson.FeatureCollection
+import org.maplibre.geojson.LineString
+import org.maplibre.geojson.Point
+>>>>>>> Stashed changes
import org.terst.nav.R
import org.terst.nav.ui.MainViewModel
import java.time.Instant
@@ -31,6 +48,8 @@ data class LogEvent(
class TrackDetailSheet : Fragment() {
private val viewModel: MainViewModel by activityViewModels()
+ private lateinit var trackMapView: MapView
+ private var trackMap: MapLibreMap? = null
companion object {
fun newInstance() = TrackDetailSheet()
@@ -68,11 +87,75 @@ class TrackDetailSheet : Fragment() {
rv.layoutManager = LinearLayoutManager(requireContext())
rv.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
rv.adapter = LogEventAdapter(events) { event ->
- viewModel.panToTrackPoint(event.lat, event.lon)
+ trackMap?.easeCamera(CameraUpdateFactory.newLatLng(LatLng(event.lat, event.lon)), 300)
}
+<<<<<<< Updated upstream
+=======
+
+ trackMapView = view.findViewById(R.id.track_map_view)
+ trackMapView.onCreate(savedInstanceState)
+ trackMapView.getMapAsync { map ->
+ trackMap = map
+ map.setStyle(Style.Builder().fromUri("https://tiles.openfreemap.org/styles/bright")) { style ->
+ drawTrack(style, track.points)
+ drawTacks(style, track.tacks)
+ zoomToTrack(map, track.points)
+ }
+ }
+ }
+
+ private fun drawTrack(style: Style, points: List<TrackPoint>) {
+ if (points.size < 2) return
+ val source = GeoJsonSource("track-detail-source",
+ Feature.fromGeometry(LineString.fromLngLats(points.map { Point.fromLngLat(it.lon, it.lat) })))
+ style.addSource(source)
+ style.addLayer(LineLayer("track-detail-layer", "track-detail-source").apply {
+ setProperties(
+ PropertyFactory.lineColor("#2B8FC4"),
+ PropertyFactory.lineWidth(4f),
+ PropertyFactory.lineCap("round"),
+ PropertyFactory.lineJoin("round")
+ )
+ })
+ }
+
+ private fun drawTacks(style: Style, tacks: List<TackEvent>) {
+ if (tacks.isEmpty()) return
+ val features = tacks.map { Feature.fromGeometry(Point.fromLngLat(it.lon, it.lat)) }
+ val source = GeoJsonSource("tack-detail-source", FeatureCollection.fromFeatures(features))
+ style.addSource(source)
+ style.addLayer(CircleLayer("tack-detail-layer", "tack-detail-source").apply {
+ setProperties(
+ PropertyFactory.circleColor("#FDD835"),
+ PropertyFactory.circleRadius(7f),
+ PropertyFactory.circleStrokeColor("#F57F17"),
+ PropertyFactory.circleStrokeWidth(2f)
+ )
+ })
+ }
+
+ private fun zoomToTrack(map: MapLibreMap, points: List<TrackPoint>) {
+ if (points.size < 2) return
+ val bounds = LatLngBounds.Builder().apply {
+ points.forEach { include(LatLng(it.lat, it.lon)) }
+ }.build()
+ map.easeCamera(CameraUpdateFactory.newLatLngBounds(bounds, 80), 600)
+ }
+
+ override fun onStart() { super.onStart(); if (::trackMapView.isInitialized) trackMapView.onStart() }
+ override fun onResume() { super.onResume(); if (::trackMapView.isInitialized) trackMapView.onResume() }
+ override fun onPause() { super.onPause(); if (::trackMapView.isInitialized) trackMapView.onPause() }
+ override fun onStop() { super.onStop(); if (::trackMapView.isInitialized) trackMapView.onStop() }
+ override fun onLowMemory() { super.onLowMemory(); if (::trackMapView.isInitialized) trackMapView.onLowMemory() }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ if (::trackMapView.isInitialized) trackMapView.onSaveInstanceState(outState)
+>>>>>>> Stashed changes
}
override fun onDestroyView() {
+ if (::trackMapView.isInitialized) trackMapView.onDestroy()
super.onDestroyView()
viewModel.clearSelectedTrack()
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
index ab30d96..116a90c 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
@@ -5,6 +5,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.speech.RecognitionListener
import android.speech.RecognizerIntent
@@ -18,24 +19,35 @@ import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputEditText
import kotlinx.coroutines.launch
+import org.terst.nav.MainActivity
import org.terst.nav.R
import org.terst.nav.logbook.VoiceLogState
import org.terst.nav.logbook.VoiceLogViewModel
+import org.terst.nav.track.SafState
+import org.terst.nav.track.SavedTrackAdapter
+import org.terst.nav.track.TrackDetailSheet
+import org.terst.nav.ui.MainViewModel
import java.io.File
import java.io.FileOutputStream
import java.util.Locale
class VoiceLogFragment : Fragment() {
- private val viewModel by lazy {
+ private val logViewModel by lazy {
VoiceLogViewModel(repository = org.terst.nav.NavApplication.logbookRepository)
}
+ private val mainViewModel: MainViewModel by activityViewModels()
+ private lateinit var layoutLogEntry: View
private lateinit var speechRecognizer: SpeechRecognizer
private lateinit var etNote: TextInputEditText
private lateinit var fabMic: FloatingActionButton
@@ -48,13 +60,9 @@ class VoiceLogFragment : Fragment() {
private lateinit var btnSave: MaterialButton
private lateinit var btnClear: MaterialButton
private lateinit var tvSavedConfirmation: TextView
- private lateinit var btnGenerateReport: MaterialButton
- /** Path or URI string for the currently attached photo, null if none. */
private var pendingPhotoPath: String? = null
- // ── Camera (TakePicturePreview returns a thumbnail bitmap — no FileProvider needed) ──
-
private val cameraLauncher = registerForActivityResult(
ActivityResultContracts.TakePicturePreview()
) { bitmap: Bitmap? ->
@@ -69,18 +77,12 @@ class VoiceLogFragment : Fragment() {
}
}
- // ── Gallery (GetContent — no storage permission needed; URI valid for this session) ──
-
private val galleryLauncher = registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
- if (uri != null) {
- setPhoto(uri.toString()) { ivPhoto.setImageURI(uri) }
- }
+ if (uri != null) setPhoto(uri.toString()) { ivPhoto.setImageURI(uri) }
}
- // ── Lifecycle ─────────────────────────────────────────────────────────────
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -90,18 +92,18 @@ class VoiceLogFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- etNote = view.findViewById(R.id.et_note)
- fabMic = view.findViewById(R.id.fab_mic)
- btnCamera = view.findViewById(R.id.btn_camera)
- btnGallery = view.findViewById(R.id.btn_gallery)
- tvStatus = view.findViewById(R.id.tv_status)
- framePhoto = view.findViewById(R.id.frame_photo)
- ivPhoto = view.findViewById(R.id.iv_photo)
- btnRemovePhoto = view.findViewById(R.id.btn_remove_photo)
- btnSave = view.findViewById(R.id.btn_save)
- btnClear = view.findViewById(R.id.btn_clear)
+ layoutLogEntry = view.findViewById(R.id.layout_log_entry)
+ etNote = view.findViewById(R.id.et_note)
+ fabMic = view.findViewById(R.id.fab_mic)
+ btnCamera = view.findViewById(R.id.btn_camera)
+ btnGallery = view.findViewById(R.id.btn_gallery)
+ tvStatus = view.findViewById(R.id.tv_status)
+ framePhoto = view.findViewById(R.id.frame_photo)
+ ivPhoto = view.findViewById(R.id.iv_photo)
+ btnRemovePhoto = view.findViewById(R.id.btn_remove_photo)
+ btnSave = view.findViewById(R.id.btn_save)
+ btnClear = view.findViewById(R.id.btn_clear)
tvSavedConfirmation = view.findViewById(R.id.tv_saved_confirmation)
- btnGenerateReport = view.findViewById(R.id.btn_generate_report)
setupSpeechRecognizer()
@@ -109,23 +111,77 @@ class VoiceLogFragment : Fragment() {
btnCamera.setOnClickListener { cameraLauncher.launch(null) }
btnGallery.setOnClickListener { galleryLauncher.launch("image/*") }
btnRemovePhoto.setOnClickListener { clearPhoto() }
-
btnSave.setOnClickListener {
- val text = etNote.text?.toString()?.trim() ?: ""
- viewModel.save(text, pendingPhotoPath)
+ logViewModel.save(etNote.text?.toString()?.trim() ?: "", pendingPhotoPath)
}
-
btnClear.setOnClickListener { clearEntry() }
- btnGenerateReport.setOnClickListener {
+ view.findViewById<MaterialButton>(R.id.btn_generate_report).setOnClickListener {
parentFragmentManager.beginTransaction()
.replace(R.id.fragment_container, org.terst.nav.tripreport.TripReportFragment())
.addToBackStack(null)
.commit()
}
+ // Show log-entry section only while recording
viewLifecycleOwner.lifecycleScope.launch {
- viewModel.state.collect { state -> renderState(state) }
+ mainViewModel.isRecording.collect { recording ->
+ layoutLogEntry.visibility = if (recording) View.VISIBLE else View.GONE
+ }
+ }
+
+ // Inline track list
+ val rv = view.findViewById<RecyclerView>(R.id.rv_saved_tracks)
+ val layoutEmpty = view.findViewById<View>(R.id.layout_empty)
+ val tvEmpty = view.findViewById<TextView>(R.id.tv_empty)
+ val btnSetup = view.findViewById<MaterialButton>(R.id.btn_setup_storage)
+
+ rv.layoutManager = LinearLayoutManager(requireContext())
+ rv.isNestedScrollingEnabled = false
+ rv.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
+
+ val tracksAdapter = SavedTrackAdapter { track ->
+ mainViewModel.selectTrack(track)
+ parentFragmentManager.beginTransaction()
+ .replace(R.id.fragment_container, TrackDetailSheet.newInstance())
+ .addToBackStack("track_detail")
+ .commit()
+ }
+ rv.adapter = tracksAdapter
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ mainViewModel.savedTracks.collect { tracks ->
+ tracksAdapter.submitList(tracks)
+ layoutEmpty.visibility = if (tracks.isEmpty()) View.VISIBLE else View.GONE
+ rv.visibility = if (tracks.isEmpty()) View.GONE else View.VISIBLE
+ }
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ viewLifecycleOwner.lifecycleScope.launch {
+ mainViewModel.safState.collect { state ->
+ when (state) {
+ SafState.NOT_CONFIGURED -> {
+ tvEmpty.text = "No saved tracks yet.\nRecord a trip to see it here."
+ btnSetup.text = "Set up track storage"
+ btnSetup.visibility = View.VISIBLE
+ }
+ SafState.PERMISSION_LOST -> {
+ tvEmpty.text = "Track storage needs re-authorization."
+ btnSetup.text = "Restore track access"
+ btnSetup.visibility = View.VISIBLE
+ }
+ SafState.CONFIGURED -> btnSetup.visibility = View.GONE
+ }
+ }
+ }
+ btnSetup.setOnClickListener {
+ (requireActivity() as MainActivity).launchSafPicker()
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ logViewModel.state.collect { state -> renderState(state) }
}
}
@@ -139,16 +195,14 @@ class VoiceLogFragment : Fragment() {
}
speechRecognizer = SpeechRecognizer.createSpeechRecognizer(requireContext())
speechRecognizer.setRecognitionListener(object : RecognitionListener {
- override fun onReadyForSpeech(params: Bundle?) { viewModel.onListeningStarted() }
+ override fun onReadyForSpeech(params: Bundle?) { logViewModel.onListeningStarted() }
override fun onResults(results: Bundle?) {
val text = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
?.firstOrNull() ?: ""
- if (text.isNotBlank()) viewModel.onSpeechRecognized(text)
- else viewModel.onRecognitionError("Could not understand speech")
- }
- override fun onError(error: Int) {
- viewModel.onRecognitionError("Recognition error: $error")
+ if (text.isNotBlank()) logViewModel.onSpeechRecognized(text)
+ else logViewModel.onRecognitionError("Could not understand speech")
}
+ override fun onError(error: Int) { logViewModel.onRecognitionError("Recognition error: $error") }
override fun onBeginningOfSpeech() {}
override fun onBufferReceived(b: ByteArray?) {}
override fun onEndOfSpeech() {}
@@ -191,7 +245,7 @@ class VoiceLogFragment : Fragment() {
private fun clearEntry() {
etNote.setText("")
clearPhoto()
- viewModel.retry()
+ logViewModel.retry()
tvSavedConfirmation.text = ""
tvStatus.text = ""
}
@@ -209,7 +263,6 @@ class VoiceLogFragment : Fragment() {
fabMic.isEnabled = false
}
is VoiceLogState.Result -> {
- // Fill the text field; user can edit before saving
etNote.setText(state.recognized)
etNote.setSelection(state.recognized.length)
tvStatus.text = ""
diff --git a/android-app/app/src/main/res/layout/layout_track_detail_sheet.xml b/android-app/app/src/main/res/layout/layout_track_detail_sheet.xml
index 6a2ba50..fafd75a 100644
--- a/android-app/app/src/main/res/layout/layout_track_detail_sheet.xml
+++ b/android-app/app/src/main/res/layout/layout_track_detail_sheet.xml
@@ -6,100 +6,121 @@
android:orientation="vertical"
android:background="?attr/colorSurface">
+<<<<<<< Updated upstream
<!-- Header -->
+=======
+ <!-- Track map — top half -->
+ <org.maplibre.android.maps.MapView
+ android:id="@+id/track_map_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <!-- Detail panel — bottom half -->
+>>>>>>> Stashed changes
<LinearLayout
android:layout_width="match_parent"
- android:layout_height="56dp"
- android:orientation="horizontal"
- android:gravity="center_vertical"
- android:paddingStart="8dp"
- android:paddingEnd="16dp"
- android:elevation="4dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="vertical"
android:background="?attr/colorSurface">
- <ImageButton
- android:id="@+id/btn_back"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:src="@drawable/ic_back"
- android:background="?attr/selectableItemBackgroundBorderless"
- android:contentDescription="Back" />
+ <!-- Header -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:paddingStart="8dp"
+ android:paddingEnd="16dp"
+ android:elevation="4dp"
+ android:background="?attr/colorSurface">
- <TextView
- android:id="@+id/detail_title"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:textSize="17sp"
- android:textStyle="bold"
- android:layout_marginStart="4dp" />
+ <ImageButton
+ android:id="@+id/btn_back"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:src="@drawable/ic_back"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="Back" />
- </LinearLayout>
+ <TextView
+ android:id="@+id/detail_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textSize="17sp"
+ android:textStyle="bold"
+ android:layout_marginStart="4dp" />
- <!-- Stats row: distance · duration · max · avg -->
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:paddingHorizontal="12dp"
- android:paddingTop="12dp"
- android:paddingBottom="12dp">
-
- <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
- android:layout_weight="1" android:orientation="vertical" android:gravity="center">
- <TextView android:id="@+id/detail_distance" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
- <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="nm" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
- <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
- android:layout_weight="1" android:orientation="vertical" android:gravity="center">
- <TextView android:id="@+id/detail_duration" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
- <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="time" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
- </LinearLayout>
+ <!-- Stats row: distance · duration · max · avg -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingHorizontal="12dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp">
- <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
- android:layout_weight="1" android:orientation="vertical" android:gravity="center">
- <TextView android:id="@+id/detail_max_sog" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
- <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="max kt" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
- </LinearLayout>
+ <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
+ android:layout_weight="1" android:orientation="vertical" android:gravity="center">
+ <TextView android:id="@+id/detail_distance" android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="nm" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
+ android:layout_weight="1" android:orientation="vertical" android:gravity="center">
+ <TextView android:id="@+id/detail_duration" android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="time" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
+ android:layout_weight="1" android:orientation="vertical" android:gravity="center">
+ <TextView android:id="@+id/detail_max_sog" android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="max kt" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
+ android:layout_weight="1" android:orientation="vertical" android:gravity="center">
+ <TextView android:id="@+id/detail_avg_sog" android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="avg kt" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
+ </LinearLayout>
- <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
- android:layout_weight="1" android:orientation="vertical" android:gravity="center">
- <TextView android:id="@+id/detail_avg_sog" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
- <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="avg kt" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
- </LinearLayout>
+ <View android:layout_width="match_parent" android:layout_height="1dp"
+ android:background="?attr/colorOutlineVariant" android:layout_marginHorizontal="16dp" />
- <View android:layout_width="match_parent" android:layout_height="1dp"
- android:background="?attr/colorOutlineVariant" android:layout_marginHorizontal="16dp" />
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="20dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="4dp"
+ android:text="Event Log"
+ android:textSize="13sp"
+ android:textAllCaps="true"
+ android:letterSpacing="0.08"
+ android:textColor="?attr/colorOnSurfaceVariant" />
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingHorizontal="20dp"
- android:paddingTop="12dp"
- android:paddingBottom="4dp"
- android:text="Event Log"
- android:textSize="13sp"
- android:textAllCaps="true"
- android:letterSpacing="0.08"
- android:textColor="?attr/colorOnSurfaceVariant" />
-
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/rv_log_entries"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:clipToPadding="false"
- android:paddingBottom="24dp" />
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/rv_log_entries"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:clipToPadding="false"
+ android:paddingBottom="24dp" />
+
+ </LinearLayout>
</LinearLayout>