Cleaned up merge UI, added reset step button
This commit is contained in:
@@ -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° 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° 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° 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° 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 -->
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user