diff options
Diffstat (limited to 'android-app/app/src/main')
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> |
