/** * @file PoseOverlayView.kt * @brief Live on-screen View that draws the pose skeleton and angle labels over the camera preview. */ package com.example.jnicpp.bowling import android.content.Context import android.graphics.Canvas import android.graphics.Matrix import android.graphics.Paint import android.util.AttributeSet import android.view.View import androidx.core.content.ContextCompat import com.example.jnicpp.R /** * @brief Draws the 33 ML Kit pose landmarks and connecting skeleton lines * on top of the camera preview. * * The landmark topology and the coordinate mapping math (analysis-image * pixels -> view pixels, replicating `PreviewView`'s `FILL_CENTER` scaling) * live in [PoseSkeletonRenderer], shared with [CameraXController]'s * baked-into-the-recorded-video overlay so both paths can't drift apart. * * @param context Android context, as required by [View]'s constructor. * @param attrs XML attribute set, as required by [View]'s constructor; null when constructed in code. */ class PoseOverlayView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : View(context, attrs) { private val jointPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ContextCompat.getColor(context, R.color.skeleton_joint) style = Paint.Style.FILL } private val bonePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ContextCompat.getColor(context, R.color.skeleton_bone) style = Paint.Style.STROKE strokeWidth = PoseSkeletonRenderer.STROKE_WIDTH } private val anglePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ContextCompat.getColor(context, R.color.white) style = Paint.Style.FILL textSize = 36f isFakeBoldText = true // Readable regardless of what's behind it (skin, clothing, wall) -- // the skeleton paints get away without this since a line/dot has a // consistent look, but small text needs the contrast. setShadowLayer(4f, 0f, 0f, ContextCompat.getColor(context, R.color.black)) } private var landmarks: Map? = null private var angles: PoseAngles? = null private var isFrontCamera = false // Source image size and rotation, as last reported by the analyzer -- // fed straight into PoseSkeletonRenderer.computeTransform on every // size/frame change. private var sourceWidth = 0 private var sourceHeight = 0 private var sourceRotationDegrees = 0 private var transform = Matrix() /** * @brief Updates the view with the latest analyzer result and triggers a redraw. * @param frame The latest analyzer result to draw, or null to clear the overlay. * Called from the main thread. */ fun update(frame: PoseAnalyzer.PoseFrameResult?) { landmarks = frame?.landmarks angles = frame?.angles if (frame != null) { isFrontCamera = frame.isFrontCamera sourceWidth = frame.imageWidth sourceHeight = frame.imageHeight sourceRotationDegrees = frame.rotationDegrees } recomputeTransform() invalidate() } /** @brief Clears the drawn skeleton/angles and triggers a redraw. */ fun clear() { landmarks = null angles = null invalidate() } /** * @brief Recomputes the source-to-view coordinate transform whenever the view's own size changes. * @param w New view width, in pixels. * @param h New view height, in pixels. * @param oldw Previous view width, in pixels. * @param oldh Previous view height, in pixels. */ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) recomputeTransform() } /** @brief Rebuilds [transform] from the current source image size/rotation and this view's current size. */ private fun recomputeTransform() { transform = PoseSkeletonRenderer.computeTransform( sourceWidth = sourceWidth, sourceHeight = sourceHeight, sourceRotationDegrees = sourceRotationDegrees, targetWidth = width, targetHeight = height, // Front camera preview is mirrored; flip the x axis about the // view's center so the overlay matches what's on screen. mirror = isFrontCamera ) } /** * @brief Draws the skeleton and angle labels for the most recent [update] call, if any. * @param canvas Canvas supplied by the View system to draw onto. */ override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val currentLandmarks = landmarks ?: return PoseSkeletonRenderer.draw(canvas, currentLandmarks, transform, bonePaint, jointPaint) angles?.let { PoseSkeletonRenderer.drawAngleLabels(canvas, currentLandmarks, it, transform, anglePaint) } } }