Merge branch 'master' into Gabriel
This commit is contained in:
@@ -18,9 +18,11 @@ import androidx.camera.core.CameraSelector
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.example.jnicpp.R
|
||||
import com.example.jnicpp.databinding.ActivityBowlingCameraBinding
|
||||
import com.google.mlkit.vision.pose.PoseLandmark
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ExecutorService
|
||||
@@ -66,6 +68,18 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
||||
// see StepCounterUiController's class doc for why this isn't just
|
||||
// inline here.
|
||||
private lateinit var stepCounterUi: StepCounterUiController
|
||||
// class for FeedbackUI
|
||||
private lateinit var feedbackUI: FeedbackUI
|
||||
private val stepLabels = listOf(
|
||||
R.string.pose_phase_waiting,
|
||||
R.string.pose_phase_starting_stance,
|
||||
R.string.first_step,
|
||||
R.string.second_step,
|
||||
R.string.third_step,
|
||||
R.string.fourth_step,
|
||||
R.string.end_position
|
||||
)
|
||||
private var currentStepIndex = 0
|
||||
|
||||
private val permissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { _ ->
|
||||
@@ -101,7 +115,9 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
||||
progressHandRaise = binding.progressHandRaise,
|
||||
textResetHint = binding.textResetHint
|
||||
)
|
||||
feedbackUI = FeedbackUI(this, binding.root)
|
||||
|
||||
binding.poseOverlay.attachFeedback(feedbackUI)
|
||||
binding.btnGrantPermissions.setOnClickListener {
|
||||
permissionLauncher.launch(CameraPermissions.REQUIRED)
|
||||
}
|
||||
@@ -114,6 +130,7 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
||||
startActivity(Intent(this, ParameterEditorActivity::class.java))
|
||||
}
|
||||
}
|
||||
binding.btnShowStep.setOnClickListener { onStepIncrease() }
|
||||
|
||||
observeViewModel()
|
||||
|
||||
@@ -133,7 +150,8 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
||||
lifecycleOwner = this,
|
||||
previewView = binding.cameraPreview,
|
||||
callback = this,
|
||||
lensFacing = lensFacing
|
||||
lensFacing = lensFacing,
|
||||
feedbackUi = feedbackUI
|
||||
)
|
||||
}
|
||||
|
||||
@@ -215,6 +233,23 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
||||
}
|
||||
launch {
|
||||
viewModel.handRaiseProgress.collect { progress -> stepCounterUi.renderHandRaiseProgress(progress) }
|
||||
// Deliberately its own collector, independent of stepEvents
|
||||
// above -- delivery-phase feedback and step counting are
|
||||
// separate concerns (see PosePhaseDetector's class doc).
|
||||
// Combined with poseEnabled (rather than posePhase alone) so
|
||||
// the label can tell "pose off" (hidden) apart from "pose on
|
||||
// but not yet in the target posture" (amber prompt) -- both
|
||||
// cases otherwise report a null phase.
|
||||
launch {
|
||||
combine(viewModel.poseEnabled, viewModel.posePhase) { enabled, phase -> enabled to phase }
|
||||
.collect { (enabled, phase) -> renderPosePhase(enabled, phase) }
|
||||
}
|
||||
// Raw angle readout backing the label above -- its own
|
||||
// collector since it's driven by a separate StateFlow
|
||||
// (poseMetrics is null on its own whenever pose detection is
|
||||
// off, so no need to combine with poseEnabled here).
|
||||
launch {
|
||||
viewModel.poseMetrics.collect { metrics -> renderPoseMetrics(metrics) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,6 +289,8 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
||||
// (see ParameterEditorActivity's class doc), so only offer
|
||||
// it while there isn't one already in progress.
|
||||
binding.btnEditor.visibility = View.VISIBLE
|
||||
// Feedback UI - buttons only shown when recording
|
||||
binding.btnShowStep.isEnabled = false
|
||||
}
|
||||
is CameraViewModel.RecordingState.Starting -> {
|
||||
// Can't stop a recording that hasn't started yet, and pose
|
||||
@@ -261,6 +298,8 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
||||
binding.btnRecord.isEnabled = false
|
||||
binding.switchPose.isEnabled = false
|
||||
binding.btnEditor.visibility = View.GONE
|
||||
binding.btnShowStep.isEnabled = true
|
||||
binding.btnShowStep.setText(R.string.pose_phase_waiting)
|
||||
}
|
||||
is CameraViewModel.RecordingState.Recording -> {
|
||||
binding.btnRecord.isEnabled = true
|
||||
@@ -272,10 +311,82 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
||||
val minutes = state.elapsedSeconds / 60
|
||||
val seconds = state.elapsedSeconds % 60
|
||||
binding.textTimer.text = String.format(Locale.US, "%02d:%02d", minutes, seconds)
|
||||
binding.btnShowStep.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shows or hides the delivery-phase feedback label, and colors/labels
|
||||
* it for whether [phase] currently validates.
|
||||
*
|
||||
* Visible for the entire time [poseEnabled] is on -- not just at the
|
||||
* moment a phase is confirmed -- so the bowler gets a continuous
|
||||
* "not yet"/"confirmed" signal to line themselves up against, rather
|
||||
* than a label that silently disappears whenever they drift out of
|
||||
* position. Independent of [renderRecordingState]/the step counter --
|
||||
* see [PosePhaseDetector]'s class doc for why phase feedback and step
|
||||
* counting are kept as separate concerns.
|
||||
*
|
||||
* @param poseEnabled Whether pose detection is currently on at all.
|
||||
* @param phase The bowler's current delivery phase, or null if none currently validates.
|
||||
*/
|
||||
private fun renderPosePhase(poseEnabled: Boolean, phase: BowlingPhase?) {
|
||||
if (!poseEnabled) {
|
||||
binding.textPoseFeedback.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
binding.textPoseFeedback.visibility = View.VISIBLE
|
||||
// Every other BowlingPhase falls back to the "waiting" message too --
|
||||
// see PosePhaseDetector's class doc, only STARTING_STANCE is detected today.
|
||||
when (phase) {
|
||||
BowlingPhase.STARTING_STANCE -> {
|
||||
binding.textPoseFeedback.text = getString(R.string.pose_phase_starting_stance)
|
||||
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Starting_stance_ready))
|
||||
}
|
||||
BowlingPhase.APPROACH -> {
|
||||
binding.textPoseFeedback.text = getString(R.string.pose_phase_approach)
|
||||
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Approach_ready))
|
||||
}
|
||||
BowlingPhase.PUSHAWAY -> {
|
||||
binding.textPoseFeedback.text = getString(R.string.pose_phase_pushaway)
|
||||
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Pushaway_ready))
|
||||
}
|
||||
else -> {
|
||||
binding.textPoseFeedback.text = getString(R.string.pose_phase_waiting)
|
||||
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Starting_stance_waiting))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shows or hides the raw torso/knee/elbow angle readout backing [renderPosePhase]'s label.
|
||||
* @param metrics This frame's angle readings, or null to hide the readout (pose detection off).
|
||||
*/
|
||||
private fun renderPoseMetrics(metrics: PosePhaseDetector.Metrics?) {
|
||||
if (metrics == null) {
|
||||
binding.textPoseMetrics.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
binding.textPoseMetrics.visibility = View.VISIBLE
|
||||
binding.textPoseMetrics.text = getString(
|
||||
R.string.pose_metrics_format,
|
||||
angleText(metrics.torsoTiltDegrees),
|
||||
angleText(metrics.leftKneeAngleDegrees),
|
||||
angleText(metrics.rightKneeAngleDegrees),
|
||||
angleText(metrics.leftElbowAngleDegrees),
|
||||
angleText(metrics.rightElbowAngleDegrees)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Formats one angle reading for display.
|
||||
* @param degrees The angle in degrees, or null if that landmark wasn't confidently detected this frame.
|
||||
* @return e.g. "12°", or "--" if [degrees] is null.
|
||||
*/
|
||||
private fun angleText(degrees: Float?): String =
|
||||
if (degrees == null) "--" else "${degrees.toInt()}°"
|
||||
|
||||
/** @brief Hides the permission-rationale screen, revealing the camera UI underneath. */
|
||||
private fun showCameraUi() {
|
||||
binding.layoutPermissionRationale.visibility = View.GONE
|
||||
@@ -393,4 +504,18 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
||||
cameraExecutor.shutdown()
|
||||
debugSessionLogger.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Advances the step index and updates the step button label.
|
||||
*
|
||||
* This method increments the current step index, cycling back to zero
|
||||
* once the end of the [stepLabels] list is reached. It then updates the
|
||||
* `btnShowStep` text to reflect the new step, ensuring the UI button
|
||||
* always displays the correct label for the current position in the
|
||||
* sequence.
|
||||
*/
|
||||
fun onStepIncrease() {
|
||||
currentStepIndex = (currentStepIndex + 1) % stepLabels.size
|
||||
binding.btnShowStep.setText(stepLabels[currentStepIndex])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user