197 lines
7.3 KiB
Kotlin
197 lines
7.3 KiB
Kotlin
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 landmark’s 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 x‑coordinate of the circle’s center.
|
||
* @param y The y‑coordinate of the circle’s 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)
|
||
}
|
||
|
||
} |