Files
PinPoint/app/src/main/java/com/example/jnicpp/bowling/PoseOverlayView.kt
T
2026-08-14 18:16:55 +08:00

128 lines
4.8 KiB
Kotlin

/**
* @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<Int, SmoothedLandmark>? = 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) }
}
}