Cleaned up merge UI, added reset step button

This commit is contained in:
Gabriel Low
2026-09-10 12:09:46 +08:00
parent ac4366d04d
commit 17fad430c1
9 changed files with 170 additions and 91 deletions
@@ -133,6 +133,7 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
startActivity(Intent(this, ParameterEditorActivity::class.java)) startActivity(Intent(this, ParameterEditorActivity::class.java))
} }
} }
binding.btnResetCounter.setOnClickListener { viewModel.resetStepCounter() }
observeViewModel() observeViewModel()
@@ -243,7 +244,8 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
// detection is actually seeing each footfall while // detection is actually seeing each footfall while
// testing/tuning it, rather than only finding out at // testing/tuning it, rather than only finding out at
// step 5 that earlier steps were silently missed. // step 5 that earlier steps were silently missed.
if (events.isEmpty()) { val isRecording = viewModel.recordingState.value is CameraViewModel.RecordingState.Recording
if (events.isEmpty() || !isRecording) {
binding.textFinalPosition.visibility = View.GONE binding.textFinalPosition.visibility = View.GONE
} else { } else {
val reachedFinal = events.size >= FINAL_STEP_COUNT val reachedFinal = events.size >= FINAL_STEP_COUNT
@@ -264,8 +266,9 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
} }
launch { launch {
viewModel.poseStageFeedback.collect { feedback -> viewModel.poseStageFeedback.collect { feedback ->
val isRecording = viewModel.recordingState.value is CameraViewModel.RecordingState.Recording
binding.textPoseStageFeedback.text = feedback binding.textPoseStageFeedback.text = feedback
binding.textPoseStageFeedback.visibility = if (feedback != null) View.VISIBLE else View.GONE binding.textPoseStageFeedback.visibility = if ((feedback != null && isRecording)) View.VISIBLE else View.GONE
} }
} }
// Deliberately its own collector, independent of stepEvents // Deliberately its own collector, independent of stepEvents
@@ -314,6 +317,8 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
is CameraViewModel.RecordingState.Idle -> { is CameraViewModel.RecordingState.Idle -> {
binding.layoutRecordingIndicator.visibility = View.GONE binding.layoutRecordingIndicator.visibility = View.GONE
stepCounterUi.setVisible(visible = false) stepCounterUi.setVisible(visible = false)
binding.textFinalPosition.visibility = View.GONE
binding.textPoseStageFeedback.visibility = View.GONE
binding.btnRecord.isEnabled = true binding.btnRecord.isEnabled = true
binding.btnRecord.setText(R.string.record) binding.btnRecord.setText(R.string.record)
// Pose mode can only be changed between recordings, not // Pose mode can only be changed between recordings, not
@@ -149,17 +149,14 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
fun onPoseFrameUpdated(landmarks: Map<Int, SmoothedLandmark>, angles: PoseAngles) { fun onPoseFrameUpdated(landmarks: Map<Int, SmoothedLandmark>, angles: PoseAngles) {
_poseAngles.value = angles _poseAngles.value = angles
// Update pose phase detector with this frame's landmarks and angles // Update pose phase detector with this frame's landmarks and angles (independent of step counter)
val phaseResult = posePhaseDetector.update(landmarks, angles) val phaseResult = posePhaseDetector.update(landmarks, angles)
val stepCount = stepEvents.value.size _posePhase.value = phaseResult.phase
val stepPhase = PosePhaseDetector.phaseForStep(stepCount)
_posePhase.value = phaseResult.phase ?: stepPhase
_poseMetrics.value = phaseResult.metrics _poseMetrics.value = phaseResult.metrics
// Check if the current pose matches the Starting Stance // Check if the current pose matches the Starting Stance (pure posture query)
val isStartingStance = (phaseResult.phase == BowlingPhase.STARTING_STANCE) val isStartingStance = (phaseResult.phase == BowlingPhase.STARTING_STANCE)
|| posePhaseDetector.isStartingStanceValid(phaseResult.metrics) || posePhaseDetector.isStartingStanceValid(phaseResult.metrics)
|| (PoseStageAdvisor.feedback(stepCount.takeIf { it > 0 }, angles)?.contains("Starting position") == true)
if (_recordingState.value is RecordingState.Recording) { if (_recordingState.value is RecordingState.Recording) {
stepCountingSession.onFrame( stepCountingSession.onFrame(
@@ -168,13 +165,20 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
timestampMs = System.currentTimeMillis(), timestampMs = System.currentTimeMillis(),
isStartingPosition = isStartingStance, isStartingPosition = isStartingStance,
) )
val currentStepCount = stepEvents.value.size
_poseStageFeedback.value = PoseStageAdvisor.feedback( _poseStageFeedback.value = PoseStageAdvisor.feedback(
stepNumber = stepCount.takeIf { it > 0 }, stepNumber = currentStepCount.takeIf { it > 0 },
angles = angles, angles = angles,
) )
} }
} }
/** @brief Manually resets step counting state for the current recording session. */
fun resetStepCounter() {
stepCountingSession.resetStepCounter()
_poseStageFeedback.value = null
}
/** /**
* @brief Marks a recording as being requested and resets all * @brief Marks a recording as being requested and resets all
* per-session buffering/detection state. * per-session buffering/detection state.
@@ -152,6 +152,20 @@ class CameraXController(
style = Paint.Style.FILL style = Paint.Style.FILL
} }
} }
private val overlayCardBorderPaint by lazy {
Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = ContextCompat.getColor(appContext, R.color.step_counter_accent)
style = Paint.Style.STROKE
strokeWidth = 3f
}
}
private val overlayAccentTextPaint by lazy {
TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = ContextCompat.getColor(appContext, R.color.step_counter_accent)
textSize = 14f
isFakeBoldText = true
}
}
private var feedbackUI: FeedbackUI? = null private var feedbackUI: FeedbackUI? = null
@@ -316,7 +330,7 @@ class CameraXController(
feedbackUI?.drawCircles(canvas, singleLandmark, transform, forRecord = true) feedbackUI?.drawCircles(canvas, singleLandmark, transform, forRecord = true)
val state = recordingOverlayStateProvider?.invoke() val state = recordingOverlayStateProvider?.invoke()
val scale = (minOf(targetWidth, targetHeight) / 720f).coerceAtLeast(1f) val scale = feedbackUI?.computeCamScale(canvas) ?: (minOf(targetWidth, targetHeight) / 1080f)
// 1. Step Counter Card (Top Center) // 1. Step Counter Card (Top Center)
val stepCount = state?.stepCount ?: 0 val stepCount = state?.stepCount ?: 0
@@ -327,7 +341,7 @@ class CameraXController(
drawPhaseBadgeOverlay(canvas, phase, scale) drawPhaseBadgeOverlay(canvas, phase, scale)
} }
// 3. Body Angle Metrics Readout (Top Left) // 3. Body Angle Metrics Readout (Bottom Left)
state?.metrics?.let { metrics -> state?.metrics?.let { metrics ->
drawMetricsOverlay(canvas, metrics, scale) drawMetricsOverlay(canvas, metrics, scale)
} }
@@ -345,22 +359,49 @@ class CameraXController(
} }
private fun drawStepCounterOverlay(canvas: Canvas, stepCount: Int, scale: Float) { private fun drawStepCounterOverlay(canvas: Canvas, stepCount: Int, scale: Float) {
val countText = "$stepCount STEPS" val numberText = stepCount.toString()
val textPaint = overlayTextPaint.apply { textSize = 26f * scale } val labelText = "STEPS"
val textWidth = textPaint.measureText(countText)
val paddingX = 18f * scale val numPaint = overlayTextPaint.apply { textSize = 32f * scale }
val paddingY = 8f * scale val labelPaint = overlayAccentTextPaint.apply {
val cardWidth = textWidth + (paddingX * 2f) textSize = 11f * scale
val cardHeight = (26f * scale) + (paddingY * 2f) strokeWidth = 0f
style = Paint.Style.FILL
}
val numWidth = numPaint.measureText(numberText)
val labelWidth = labelPaint.measureText(labelText)
val contentWidth = maxOf(numWidth, labelWidth)
val paddingX = 20f * scale
val paddingY = 6f * scale
val cardWidth = contentWidth + (paddingX * 2f)
val cardHeight = (32f * scale) + (11f * scale) + (paddingY * 2f)
val left = (canvas.width - cardWidth) / 2f val left = (canvas.width - cardWidth) / 2f
val top = 32f * scale val top = 104f * scale
val right = left + cardWidth val right = left + cardWidth
val bottom = top + cardHeight val bottom = top + cardHeight
val rect = RectF(left, top, right, bottom) val rect = RectF(left, top, right, bottom)
canvas.drawRoundRect(rect, 14f * scale, 14f * scale, overlayScrimPaint) val radius = 16f * scale
canvas.drawText(countText, left + paddingX, top + paddingY + (22f * scale), textPaint)
// Draw card background
canvas.drawRoundRect(rect, radius, radius, overlayScrimPaint)
// Draw card border
overlayCardBorderPaint.strokeWidth = 2f * scale
canvas.drawRoundRect(rect, radius, radius, overlayCardBorderPaint)
// Draw big number text (centered)
val numX = left + ((cardWidth - numWidth) / 2f)
val numY = top + paddingY + (28f * scale)
canvas.drawText(numberText, numX, numY, numPaint)
// Draw "STEPS" label text (centered below number)
val labelX = left + ((cardWidth - labelWidth) / 2f)
val labelY = numY + (14f * scale)
canvas.drawText(labelText, labelX, labelY, labelPaint)
} }
private fun drawPhaseBadgeOverlay(canvas: Canvas, phase: BowlingPhase, scale: Float) { private fun drawPhaseBadgeOverlay(canvas: Canvas, phase: BowlingPhase, scale: Float) {
@@ -380,16 +421,16 @@ class CameraXController(
BowlingPhase.POWER_STEP -> R.color.Power_step_ready BowlingPhase.POWER_STEP -> R.color.Power_step_ready
BowlingPhase.SLIDE_AND_RELEASE -> R.color.Slide_and_release_ready BowlingPhase.SLIDE_AND_RELEASE -> R.color.Slide_and_release_ready
} }
val textPaint = overlayTextPaint.apply { textSize = 20f * scale } val textPaint = overlayTextPaint.apply { textSize = 14f * scale }
val textWidth = textPaint.measureText(label) val textWidth = textPaint.measureText(label)
val paddingX = 14f * scale val paddingX = 10f * scale
val paddingY = 6f * scale val paddingY = 4f * scale
val badgeWidth = textWidth + (paddingX * 2f) val badgeWidth = textWidth + (paddingX * 2f)
val badgeHeight = (20f * scale) + (paddingY * 2f) val badgeHeight = (14f * scale) + (paddingY * 2f)
val right = canvas.width - (24f * scale) val right = canvas.width - (16f * scale)
val left = right - badgeWidth val left = right - badgeWidth
val top = 32f * scale val top = 112f * scale
val bottom = top + badgeHeight val bottom = top + badgeHeight
val badgePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { val badgePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
@@ -398,8 +439,8 @@ class CameraXController(
} }
val rect = RectF(left, top, right, bottom) val rect = RectF(left, top, right, bottom)
canvas.drawRoundRect(rect, 12f * scale, 12f * scale, badgePaint) canvas.drawRoundRect(rect, 10f * scale, 10f * scale, badgePaint)
canvas.drawText(label, left + paddingX, top + paddingY + (18f * scale), textPaint) canvas.drawText(label, left + paddingX, top + paddingY + (12f * scale), textPaint)
} }
private fun drawMetricsOverlay(canvas: Canvas, metrics: PosePhaseDetector.Metrics, scale: Float) { private fun drawMetricsOverlay(canvas: Canvas, metrics: PosePhaseDetector.Metrics, scale: Float) {
@@ -407,24 +448,24 @@ class CameraXController(
val line1 = "Torso: ${angleText(metrics.torsoTiltDegrees)} · Knee L: ${angleText(metrics.leftKneeAngleDegrees)} R: ${angleText(metrics.rightKneeAngleDegrees)}" val line1 = "Torso: ${angleText(metrics.torsoTiltDegrees)} · Knee L: ${angleText(metrics.leftKneeAngleDegrees)} R: ${angleText(metrics.rightKneeAngleDegrees)}"
val line2 = "Elbow L: ${angleText(metrics.leftElbowAngleDegrees)} R: ${angleText(metrics.rightElbowAngleDegrees)}" val line2 = "Elbow L: ${angleText(metrics.leftElbowAngleDegrees)} R: ${angleText(metrics.rightElbowAngleDegrees)}"
val textPaint = overlayTextPaint.apply { textSize = 16f * scale } val textPaint = overlayTextPaint.apply { textSize = 12f * scale }
val w1 = textPaint.measureText(line1) val w1 = textPaint.measureText(line1)
val w2 = textPaint.measureText(line2) val w2 = textPaint.measureText(line2)
val maxWidth = maxOf(w1, w2) val maxWidth = maxOf(w1, w2)
val paddingX = 12f * scale val paddingX = 8f * scale
val paddingY = 6f * scale val paddingY = 4f * scale
val cardWidth = maxWidth + (paddingX * 2f) val cardWidth = maxWidth + (paddingX * 2f)
val cardHeight = (18f * 2f * scale) + (paddingY * 2f) + (4f * scale) val cardHeight = (12f * 2f * scale) + (paddingY * 2f) + (4f * scale)
val left = 24f * scale val left = 16f * scale
val top = 32f * scale val bottom = canvas.height - (80f * scale)
val top = bottom - cardHeight
val right = left + cardWidth val right = left + cardWidth
val bottom = top + cardHeight
val rect = RectF(left, top, right, bottom) val rect = RectF(left, top, right, bottom)
canvas.drawRoundRect(rect, 10f * scale, 10f * scale, overlayScrimPaint) canvas.drawRoundRect(rect, 8f * scale, 8f * scale, overlayScrimPaint)
canvas.drawText(line1, left + paddingX, top + paddingY + (15f * scale), textPaint) canvas.drawText(line1, left + paddingX, top + paddingY + (11f * scale), textPaint)
canvas.drawText(line2, left + paddingX, top + paddingY + (15f * 2f * scale) + (4f * scale), textPaint) canvas.drawText(line2, left + paddingX, top + paddingY + (11f * 2f * scale) + (4f * scale), textPaint)
} }
/** /**
@@ -180,7 +180,7 @@ class FeedbackUI(rootView: View) {
* @param recordCanvas The canvas used for recording output. * @param recordCanvas The canvas used for recording output.
* @return A float scale factor to apply when drawing to the recording canvas. * @return A float scale factor to apply when drawing to the recording canvas.
*/ */
private fun computeCamScale(recordCanvas: Canvas): Float { fun computeCamScale(recordCanvas: Canvas): Float {
if ((liveUiWidth == 0 || liveUiHeight == 0)) return 1f if ((liveUiWidth == 0 || liveUiHeight == 0)) return 1f
val scaleX = recordCanvas.width.toFloat() / liveUiWidth.toFloat() val scaleX = recordCanvas.width.toFloat() / liveUiWidth.toFloat()
val scaleY = recordCanvas.height.toFloat() / liveUiHeight.toFloat() val scaleY = recordCanvas.height.toFloat() / liveUiHeight.toFloat()
@@ -139,9 +139,6 @@ class PoseOverlayView @JvmOverloads constructor(
} }
// end test code // end test code
feedbackUI?.drawCircles(canvas, singleLandmark, transform) feedbackUI?.drawCircles(canvas, singleLandmark, transform)
// Trigger feedback advice for this step
feedbackUI?.showBanner(canvas, "Body too upright, take a larger 1st step")
} }
/** /**
@@ -82,6 +82,14 @@ class StepCountingSession {
} }
} }
/**
* @brief Manually resets live step detector state and clears detected step events.
*/
fun resetStepCounter() {
liveStepDetector.reset()
_stepEvents.value = emptyList()
}
/** /**
* @brief Clears all buffering/detection state and rebuilds the step * @brief Clears all buffering/detection state and rebuilds the step
* detector from [settings]; call when a new recording starts. * detector from [settings]; call when a new recording starts.
@@ -75,31 +75,12 @@
android:fontFamily="monospace" /> android:fontFamily="monospace" />
</LinearLayout> </LinearLayout>
<!-- Top Left: Body Angles Readout (below Recording Indicator / Back Button) -->
<TextView
android:id="@+id/text_pose_metrics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/overlay_scrim"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:textColor="@color/white"
android:textSize="12sp"
android:fontFamily="monospace"
android:visibility="gone"
tools:visibility="visible"
tools:text="Torso: 12° · Knee L: 168° R: 170°&#10;Elbow L: 85° R: 90°"
app:layout_constraintTop_toBottomOf="@id/layout_recording_indicator"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp" />
<!-- TOP MIDDLE: Step Counter Card --> <!-- TOP MIDDLE: Step Counter Card -->
<LinearLayout <LinearLayout
android:id="@+id/card_step_counter" android:id="@+id/card_step_counter"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="24dp"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center" android:gravity="center"
android:background="@drawable/shape_step_counter_card" android:background="@drawable/shape_step_counter_card"
@@ -136,10 +117,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center_horizontal" android:gravity="center_horizontal"
app:layout_constraintTop_toBottomOf="@id/card_step_counter" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="8dp"> android:layout_marginTop="96dp">
<TextView <TextView
android:id="@+id/text_final_position" android:id="@+id/text_final_position"
@@ -170,6 +151,25 @@
tools:visibility="visible" /> tools:visibility="visible" />
</LinearLayout> </LinearLayout>
<!-- Bottom Left: Body Angles Readout (above Pose Switch) -->
<TextView
android:id="@+id/text_pose_metrics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/overlay_scrim"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:textColor="@color/white"
android:textSize="12sp"
android:fontFamily="monospace"
android:visibility="gone"
tools:visibility="visible"
tools:text="Torso: 12° · Knee L: 168° R: 170°&#10;Elbow L: 85° R: 90°"
app:layout_constraintBottom_toTopOf="@id/switch_pose"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginBottom="12dp"
android:layout_marginStart="24dp" />
<!-- Delivery Phase Badge --> <!-- Delivery Phase Badge -->
<TextView <TextView
android:id="@+id/text_pose_feedback" android:id="@+id/text_pose_feedback"
@@ -229,6 +229,17 @@
app:layout_constraintBottom_toTopOf="@id/btn_switch_camera" app:layout_constraintBottom_toTopOf="@id/btn_switch_camera"
app:layout_constraintEnd_toEndOf="@id/btn_record" /> app:layout_constraintEnd_toEndOf="@id/btn_record" />
<!-- Bottom Right: Manual Reset Step Counter Button -->
<Button
android:id="@+id/btn_reset_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginBottom="16dp"
android:text="@string/reset_counter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Permission Rationale Screen --> <!-- Permission Rationale Screen -->
<LinearLayout <LinearLayout
android:id="@+id/layout_permission_rationale" android:id="@+id/layout_permission_rationale"
@@ -75,25 +75,6 @@
android:fontFamily="monospace" /> android:fontFamily="monospace" />
</LinearLayout> </LinearLayout>
<!-- Top Left: Body Angles Readout (below Recording Indicator / Back Button) -->
<TextView
android:id="@+id/text_pose_metrics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/overlay_scrim"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:textColor="@color/white"
android:textSize="12sp"
android:fontFamily="monospace"
android:visibility="gone"
tools:visibility="visible"
tools:text="Torso: 12° · Knee L: 168° R: 170°&#10;Elbow L: 85° R: 90°"
app:layout_constraintTop_toBottomOf="@id/layout_recording_indicator"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp" />
<!-- Top Right Controls: Switch Camera & Settings Editor --> <!-- Top Right Controls: Switch Camera & Settings Editor -->
<Button <Button
android:id="@+id/btn_switch_camera" android:id="@+id/btn_switch_camera"
@@ -128,18 +109,18 @@
android:textStyle="bold" android:textStyle="bold"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" tools:visibility="visible"
tools:text="Get into starting stance" tools:text="Starting Stance"
app:layout_constraintTop_toBottomOf="@id/btn_editor" app:layout_constraintTop_toBottomOf="@id/btn_editor"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="8dp" android:layout_marginTop="16dp"
android:layout_marginEnd="16dp" /> android:layout_marginEnd="16dp" />
<!-- TOP MIDDLE: Step Counter Card (Primary Retained Element) --> <!-- TOP MIDDLE: Step Counter Card (Positioned below top controls) -->
<LinearLayout <LinearLayout
android:id="@+id/card_step_counter" android:id="@+id/card_step_counter"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="56dp" android:layout_marginTop="104dp"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center" android:gravity="center"
android:background="@drawable/shape_step_counter_card" android:background="@drawable/shape_step_counter_card"
@@ -170,16 +151,17 @@
android:textStyle="bold" /> android:textStyle="bold" />
</LinearLayout> </LinearLayout>
<!-- CENTERED BELOW STEP COUNTER: Final Position & Live Coaching Cue --> <!-- CENTERED BELOW STEP COUNTER: Final Position & Live Coaching Cue
(Anchored to parent so it doesn't jump to the top of the screen when step counter becomes GONE) -->
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center_horizontal" android:gravity="center_horizontal"
app:layout_constraintTop_toBottomOf="@id/card_step_counter" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="8dp"> android:layout_marginTop="176dp">
<TextView <TextView
android:id="@+id/text_final_position" android:id="@+id/text_final_position"
@@ -210,6 +192,25 @@
tools:visibility="visible" /> tools:visibility="visible" />
</LinearLayout> </LinearLayout>
<!-- Bottom Left: Body Angles Readout (above Pose Switch) -->
<TextView
android:id="@+id/text_pose_metrics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/overlay_scrim"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:textColor="@color/white"
android:textSize="12sp"
android:fontFamily="monospace"
android:visibility="gone"
tools:visibility="visible"
tools:text="Torso: 12° · Knee L: 168° R: 170°&#10;Elbow L: 85° R: 90°"
app:layout_constraintBottom_toTopOf="@id/switch_pose"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginBottom="12dp"
android:layout_marginStart="16dp" />
<!-- BOTTOM CONTROLS: Pose Overlay Switch & Record Button --> <!-- BOTTOM CONTROLS: Pose Overlay Switch & Record Button -->
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_pose" android:id="@+id/switch_pose"
@@ -233,6 +234,17 @@
android:text="@string/record" android:text="@string/record"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/switch_pose" app:layout_constraintStart_toEndOf="@id/switch_pose"
app:layout_constraintEnd_toStartOf="@id/btn_reset_counter" />
<!-- Bottom Right: Manual Reset Step Counter Button -->
<Button
android:id="@+id/btn_reset_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
android:text="@string/reset_counter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />
<!-- Permission Rationale Screen --> <!-- Permission Rationale Screen -->
+1
View File
@@ -14,6 +14,7 @@
<string name="recording_timer_placeholder">00:00</string> <string name="recording_timer_placeholder">00:00</string>
<string name="step_count_big_placeholder">0</string> <string name="step_count_big_placeholder">0</string>
<string name="step_counter_label">STEPS</string> <string name="step_counter_label">STEPS</string>
<string name="reset_counter">Reset Steps</string>
<string name="reset_hint_idle">✋ Raise a hand, hold 5s to reset</string> <string name="reset_hint_idle">✋ Raise a hand, hold 5s to reset</string>
<string name="reset_hint_holding">Keep holding… %1$d%%</string> <string name="reset_hint_holding">Keep holding… %1$d%%</string>
<string name="step_count_placeholder">Step 0</string> <string name="step_count_placeholder">Step 0</string>