Files
PinPoint/app/src/main/java/com/example/jnicpp/bowling/FeedbackUI.kt
T
2026-09-07 19:06:10 +08:00

197 lines
7.3 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.example.jnicpp.bowling
import android.text.Layout
import android.text.TextPaint
import android.text.StaticLayout
import android.graphics.Canvas
import android.graphics.RectF
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.content.Context
import android.graphics.Paint
import android.view.View
import android.widget.TextView
import com.example.jnicpp.R
import android.graphics.BlurMaskFilter
import android.graphics.Matrix
import kotlin.math.min
import androidx.constraintlayout.widget.ConstraintLayout
class FeedbackUI(private val context: Context, private val rootView: View) {
private var landmarks: Map<Int, SmoothedLandmark>? = null
private var CIRCLE_RADIUS = 128f
private val uiTextSize = 32f
private val uiStrokeWidth = 12f
private val bannerPaddingY = 24
private val bannerPaddingX = 24
private val camScale = 0.5f
private var liveUiWidth: Int = 0
private var liveUiHeight: Int = 0
private val glowPaint = Paint().apply {
color = Color.RED
style = Paint.Style.STROKE
isAntiAlias = true
strokeWidth = uiStrokeWidth // thicker stroke so glow is visible
maskFilter = BlurMaskFilter(25f, BlurMaskFilter.Blur.OUTER)
}
private val circlePaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.STROKE
isAntiAlias = true
strokeWidth = uiStrokeWidth
}
private val textPaint = TextPaint().apply {
color = Color.WHITE
textSize = uiTextSize
isAntiAlias = true
}
private val bgPaint = Paint().apply {
color = Color.argb(64, 255, 255, 255)
isAntiAlias = true
}
init {
rootView.viewTreeObserver.addOnGlobalLayoutListener {
setLiveUiSize(rootView)
}
}
/**
* @brief Displays a banner with the given message on the provided canvas.
*
* The banner is centered horizontally and offset vertically. It uses
* `StaticLayout` to support multi-line text and scales its size depending
* on whether the canvas is for live UI or recording output.
*
* @param canvas The canvas to draw the banner on.
* @param message The text message to display inside the banner.
* @param forRecord If true, scales the banner relative to recording canvas
* dimensions; otherwise uses live UI scale.
*/
fun showBanner(canvas: Canvas, message: String, forRecord: Boolean = false) {
val scale = if (forRecord) computeCamScale(canvas) else 1f
val paddingX = bannerPaddingX * scale
val paddingY = bannerPaddingY * scale
val maxWidth = (canvas.width * 0.8f).toInt()
// Build StaticLayout for multi-line text
textPaint.textSize = uiTextSize * scale
val staticLayout = StaticLayout.Builder
.obtain(message, 0, message.length, textPaint, maxWidth)
.setAlignment(Layout.Alignment.ALIGN_CENTER) // center text horizontally
.setLineSpacing(0f, 1f)
.setIncludePad(false)
.build()
// Use StaticLayout dimensions
val textWidth = staticLayout.width.toFloat()
val textHeight = staticLayout.height.toFloat()
val bannerWidth = textWidth + paddingX * 2
val bannerHeight = textHeight + paddingY * 2
// Center horizontally
val left = (canvas.width - bannerWidth) / 2f
val top = 172f * scale
val right = left + bannerWidth
val bottom = top + bannerHeight
// Draw background
val rect = RectF(left, top, right, bottom)
// Scale corner radius too
val cornerRadius = 24f * scale
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, bgPaint)
// Draw text layout inside background
canvas.save()
canvas.translate(left + paddingX, top + paddingY)
staticLayout.draw(canvas)
canvas.restore()
}
/**
* @brief Draws glowing circles around all provided landmarks.
*
* Each landmarks coordinates are transformed by the given matrix before
* drawing. Circles are rendered with both a white stroke and a red glow
* effect for visibility.
*
* @param canvas The canvas to draw circles on.
* @param landmarks A map of landmark indices to smoothed landmark positions.
* @param transform A matrix applied to landmark coordinates before drawing.
* @param forRecord If true, scales circle radius and stroke width for
* recording output.
*/
// --- Shape overlay (circle) ---
fun drawCircles(canvas: Canvas, landmarks: Map<Int, SmoothedLandmark>, transform: Matrix, forRecord: Boolean = false) {
for (landmark in landmarks.values) {
val point = floatArrayOf(landmark.x, landmark.y)
transform.mapPoints(point)
drawCircle(canvas, point[0], point[1], 0f, forRecord)
}
}
/**
* @brief Draws a single circle with both a solid stroke and a glowing outline.
*
* This method renders a circle at the specified coordinates using two
* layered paints: a white stroke (`circlePaint`) and a red glow (`glowPaint`).
* The radius and stroke widths are scaled depending on whether the canvas
* is for live UI or recording output, ensuring consistent visual feedback
* across different resolutions.
*
* @param canvas The canvas to draw the circle on.
* @param x The xcoordinate of the circles center.
* @param y The ycoordinate of the circles center.
* @param radius The circle radius. If set to 0, defaults to [CIRCLE_RADIUS].
* @param forRecord If true, applies recording scale factor to radius and
* stroke width; otherwise uses live UI scale.
*/
fun drawCircle(canvas: Canvas, x: Float, y: Float, radius: Float = 0f, forRecord: Boolean = false) {
val scale = if (forRecord) camScale else 1f
var rad = (if (radius == 0f) CIRCLE_RADIUS else radius) * scale
circlePaint.strokeWidth = uiStrokeWidth * scale
glowPaint.strokeWidth = uiStrokeWidth * scale
canvas.drawCircle(x, y, rad, circlePaint)
canvas.drawCircle(x, y, rad, glowPaint)
}
/**
* @brief Updates stored live UI dimensions based on the root view.
*
* This method caches the width and height of the root view so that
* recording canvas scaling can be computed consistently later.
*
* @param rootView The root view whose dimensions are measured.
*/
fun setLiveUiSize(rootView: View) {
liveUiWidth = rootView.width
liveUiHeight = rootView.height
}
/**
* @brief Computes the scaling factor between live UI and recording canvas.
*
* The scale is determined by comparing the recording canvas dimensions
* against the cached live UI dimensions, using the smaller ratio to
* preserve aspect consistency.
*
* @param recordCanvas The canvas used for recording output.
* @return A float scale factor to apply when drawing to the recording canvas.
*/
private fun computeCamScale(recordCanvas: Canvas): Float {
if (liveUiWidth == 0 || liveUiHeight == 0) return 1f
val scaleX = recordCanvas.width.toFloat() / liveUiWidth.toFloat()
val scaleY = recordCanvas.height.toFloat() / liveUiHeight.toFloat()
return min(scaleX, scaleY)
}
}