打击位置
This commit is contained in:
@@ -1,17 +1,54 @@
|
||||
package com.example.defenseapplication.liveVideo
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.os.Bundle
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.example.defenseapplication.R
|
||||
import com.example.defenseapplication.bean.DeviceBean
|
||||
import com.example.defenseapplication.databinding.StrikePositionActivityBinding
|
||||
import com.example.defenseapplication.utils.DeviceIdEvent
|
||||
import com.example.defenseapplication.utils.RetrofitNetwork
|
||||
import com.example.defenseapplication.view.CustomDialogFragment
|
||||
import com.example.defenseapplication.view.showDefenseDialog
|
||||
import com.gyf.immersionbar.ImmersionBar
|
||||
//打击位置
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.max
|
||||
|
||||
class StrikePositionActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: StrikePositionActivityBinding
|
||||
private lateinit var markerLayoutParams: RelativeLayout.LayoutParams
|
||||
private var deviceId: String? = null
|
||||
|
||||
private var humanBodyWidth = 0
|
||||
private var humanBodyHeight = 0
|
||||
private var hasAppliedInitialPosition = false
|
||||
|
||||
private enum class Zone(val rawValue: Int, val normalizedX: Float, val normalizedY: Float) {
|
||||
HEAD(1, 0.49f, 0.01f),
|
||||
LEFT_SHOULDER(2, 0.20f, 0.475f),
|
||||
CHEST(3, 0.5f, 0.475f),
|
||||
RIGHT_SHOULDER(4, 0.80f, 0.475f);
|
||||
|
||||
companion object {
|
||||
fun fromRawValue(raw: Int): Zone {
|
||||
return values().minByOrNull { abs(it.rawValue - raw) } ?: CHEST
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var selectedZone: Zone = Zone.CHEST
|
||||
|
||||
// Touch tracking for ImageView (captured in onTouch, used in ACTION_UP for click detection)
|
||||
private var lastTouchX = 0f
|
||||
private var lastTouchY = 0f
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = StrikePositionActivityBinding.inflate(layoutInflater)
|
||||
@@ -22,35 +59,163 @@ class StrikePositionActivity : AppCompatActivity() {
|
||||
.titleBar(binding.topBar)
|
||||
.init()
|
||||
|
||||
binding.ivBack.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
deviceId = DeviceIdEvent.getDeviceId()
|
||||
|
||||
binding.btnSubmit.setOnClickListener {
|
||||
showConfirmDialog()
|
||||
}
|
||||
// 获取准星的 LayoutParams,并移除居中约束
|
||||
markerLayoutParams = binding.ivStrikeMarker.layoutParams as RelativeLayout.LayoutParams
|
||||
markerLayoutParams.removeRule(RelativeLayout.CENTER_HORIZONTAL)
|
||||
markerLayoutParams.removeRule(RelativeLayout.CENTER_VERTICAL)
|
||||
binding.ivStrikeMarker.layoutParams = markerLayoutParams
|
||||
val initialPos = intent.getStringExtra("strikePosition")?.toIntOrNull() ?: Zone.CHEST.rawValue
|
||||
selectedZone = Zone.fromRawValue(initialPos)
|
||||
|
||||
// 设置人体图片的点击监听
|
||||
binding.ivBack.setOnClickListener { finish() }
|
||||
binding.btnSubmit.setOnClickListener { showConfirmDialog() }
|
||||
|
||||
// Use OnTouchListener to track touch coordinates
|
||||
binding.ivHumanBody.setOnTouchListener { _, event ->
|
||||
if (event.action == android.view.MotionEvent.ACTION_DOWN) {
|
||||
moveMarkerTo(event.x, event.y)
|
||||
lastTouchX = event.x
|
||||
lastTouchY = event.y
|
||||
// Return false so the touch event continues to be processed
|
||||
// (e.g., scrolling / scaling if you add it later) while still
|
||||
// letting us capture the coordinates.
|
||||
false
|
||||
}
|
||||
|
||||
// OnClick fires on ACTION_UP; coordinates captured from lastTouchX/Y
|
||||
binding.ivHumanBody.setOnClickListener {
|
||||
val imageView = it as ImageView
|
||||
val (nx, ny) = resolveTapLocation(lastTouchX, lastTouchY, imageView)
|
||||
val zone = zoneForLocation(nx, ny)
|
||||
selectedZone = zone
|
||||
updateMarkerPosition(zone, animated = true)
|
||||
}
|
||||
|
||||
binding.ivHumanBody.viewTreeObserver.addOnPreDrawListener {
|
||||
val w = binding.ivHumanBody.width
|
||||
val h = binding.ivHumanBody.height
|
||||
if (w > 0 && h > 0 && !hasAppliedInitialPosition) {
|
||||
hasAppliedInitialPosition = true
|
||||
humanBodyWidth = w
|
||||
humanBodyHeight = h
|
||||
updateMarkerPosition(selectedZone, animated = false)
|
||||
}
|
||||
true
|
||||
}
|
||||
// 初始化准星位置:人体头部(假设在图片宽度的1/2,高度的1/5处)
|
||||
binding.ivHumanBody.post {
|
||||
val humanWidth = binding.ivHumanBody.width
|
||||
val humanHeight = binding.ivHumanBody.height
|
||||
if (humanWidth > 0 && humanHeight > 0) {
|
||||
val headX = humanWidth / 2f
|
||||
val headY = humanHeight / 5f
|
||||
moveMarkerTo(headX, headY)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a screen coordinate (from onTouch) into a normalised [0..1] offset
|
||||
* inside the drawable, accounting for:
|
||||
* - ImageView scaling (scaleType="fitCenter" adds letterboxing)
|
||||
* - Letterbox / pillarbox margins when image aspect ≠ view aspect
|
||||
*/
|
||||
private fun resolveTapLocation(rawX: Float, rawY: Float, imageView: ImageView): Pair<Float, Float> {
|
||||
val drawable = imageView.drawable ?: return Pair(0f, 0f)
|
||||
val ivWidth = imageView.width.toFloat()
|
||||
val ivHeight = imageView.height.toFloat()
|
||||
if (ivWidth <= 0 || ivHeight <= 0) return Pair(0f, 0f)
|
||||
|
||||
val intrinsicWidth = drawable.intrinsicWidth.toFloat()
|
||||
val intrinsicHeight = drawable.intrinsicHeight.toFloat()
|
||||
|
||||
// scale: the uniform factor the image is scaled by to fit the view
|
||||
val scale = min(ivWidth / intrinsicWidth, ivHeight / intrinsicHeight)
|
||||
|
||||
// Physical pixel size of the drawable after scaling
|
||||
val drawnW = intrinsicWidth * scale
|
||||
val drawnH = intrinsicHeight * scale
|
||||
|
||||
// Letterbox offsets (top / left) when aspect ratios differ
|
||||
val drawableLeft = (ivWidth - drawnW) / 2f
|
||||
val drawableTop = (ivHeight - drawnH) / 2f
|
||||
|
||||
// Clamp touch to the actual drawable area
|
||||
val clampedX = max(drawableLeft, min(rawX, drawableLeft + drawnW))
|
||||
val clampedY = max(drawableTop, min(rawY, drawableTop + drawnH))
|
||||
|
||||
// Normalised offset [0..1] within the drawable
|
||||
val normalizedX = (clampedX - drawableLeft) / drawnW
|
||||
val normalizedY = (clampedY - drawableTop) / drawnH
|
||||
|
||||
return Pair(normalizedX, normalizedY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Zones are divided by:
|
||||
* - Horizontal boundary at ny = 0.5 → top = HEAD, bottom = shoulders
|
||||
* - Vertical boundary 1 at nx = 1/3 → left = LEFT_SHOULDER
|
||||
* - Vertical boundary 2 at nx = 2/3 → right = RIGHT_SHOULDER, middle = CHEST
|
||||
*
|
||||
* If the touch falls within ±tolerance of any boundary line, the nearest
|
||||
* zone centre (Euclidean distance in normalised space) is used to avoid
|
||||
* arbitrary assignments at boundary pixels.
|
||||
*/
|
||||
private fun zoneForLocation(nx: Float, ny: Float): Zone {
|
||||
val hBoundary = 0.5f
|
||||
val vBoundary1 = 1f / 3f
|
||||
val vBoundary2 = 2f / 3f
|
||||
val tolerance = 0.04f
|
||||
|
||||
val nearHBoundary = abs(ny - hBoundary) <= tolerance
|
||||
val nearVBoundary = ny >= hBoundary &&
|
||||
(abs(nx - vBoundary1) <= tolerance || abs(nx - vBoundary2) <= tolerance)
|
||||
|
||||
if (nearHBoundary || nearVBoundary) {
|
||||
return nearestZone(nx, ny)
|
||||
}
|
||||
|
||||
return when {
|
||||
ny < hBoundary -> Zone.HEAD
|
||||
nx < vBoundary1 -> Zone.LEFT_SHOULDER
|
||||
nx < vBoundary2 -> Zone.CHEST
|
||||
else -> Zone.RIGHT_SHOULDER
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the Zone whose normalised centre is closest (Euclidean) to (nx, ny). */
|
||||
private fun nearestZone(nx: Float, ny: Float): Zone {
|
||||
return Zone.values().minByOrNull { zone ->
|
||||
val dx = nx - zone.normalizedX
|
||||
val dy = ny - zone.normalizedY
|
||||
dx * dx + dy * dy
|
||||
} ?: Zone.CHEST
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions the marker (ivStrikeMarker) at the zone's centre, clamped so the
|
||||
* marker never goes out of the ivHumanBody bounds.
|
||||
*
|
||||
* Because the marker is a child of ivHumanBody and starts at its center
|
||||
* (layout_gravity="center"), we compute the offset from that center point
|
||||
* and apply it via translationX/Y so the marker's layout bounds stay intact.
|
||||
*/
|
||||
private fun updateMarkerPosition(zone: Zone, animated: Boolean) {
|
||||
if (humanBodyWidth <= 0 || humanBodyHeight <= 0) return
|
||||
|
||||
val markerView = binding.ivStrikeMarker
|
||||
val markerW = markerView.layoutParams.width.takeIf { it > 0 } ?: 200
|
||||
val markerH = markerView.layoutParams.height.takeIf { it > 0 } ?: 200
|
||||
|
||||
val halfMarkerW = markerW / 2f
|
||||
val halfMarkerH = markerH / 2f
|
||||
|
||||
val rawX = humanBodyWidth * zone.normalizedX
|
||||
val rawY = humanBodyHeight * zone.normalizedY
|
||||
|
||||
// Clamp X so the marker never extends beyond the drawable edges.
|
||||
// Y clamp only has an upper bound — HEAD at normalizedY=0 is allowed to go
|
||||
// all the way to the top (translationY can be negative).
|
||||
val clampedX = min(max(rawX, halfMarkerW), humanBodyWidth - halfMarkerW)
|
||||
val clampedY = min(rawY, humanBodyHeight - halfMarkerH)
|
||||
|
||||
// Offset from ivHumanBody's center (where the marker starts by default)
|
||||
val centerX = humanBodyWidth / 2f
|
||||
val centerY = humanBodyHeight / 2f
|
||||
val offsetX = clampedX - centerX
|
||||
val offsetY = clampedY - centerY
|
||||
|
||||
if (animated) {
|
||||
ObjectAnimator.ofFloat(markerView, "translationX", offsetX).apply { duration = 200; start() }
|
||||
ObjectAnimator.ofFloat(markerView, "translationY", offsetY).apply { duration = 200; start() }
|
||||
} else {
|
||||
markerView.translationX = offsetX
|
||||
markerView.translationY = offsetY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,23 +226,34 @@ class StrikePositionActivity : AppCompatActivity() {
|
||||
positiveText = getString(R.string.confirm),
|
||||
negativeText = getString(R.string.cancel)
|
||||
)
|
||||
showDefenseDialog(config, onPositiveClick = {
|
||||
finish()
|
||||
// if(){
|
||||
// ToastUtils.showSuccess(this, getString(R.string.wifi_connection_success))
|
||||
// }else{
|
||||
// ToastUtils.showError(this, getString(R.string.wifi_connection_failed))
|
||||
// }
|
||||
})
|
||||
showDefenseDialog(config, onPositiveClick = { submitStrikePosition() })
|
||||
}
|
||||
private fun moveMarkerTo(x: Float, y: Float) {
|
||||
val marker = binding.ivStrikeMarker
|
||||
// 让准星中心对准点击位置
|
||||
val left = x - marker.width / 2f
|
||||
val top = y - marker.height / 2f
|
||||
|
||||
markerLayoutParams.leftMargin = left.toInt()
|
||||
markerLayoutParams.topMargin = top.toInt()
|
||||
marker.layoutParams = markerLayoutParams
|
||||
private fun submitStrikePosition() {
|
||||
val id = deviceId
|
||||
if (id.isNullOrEmpty()) {
|
||||
Toast.makeText(this, "设备ID为空", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val bean = DeviceBean(id = id, strikePosition = selectedZone.rawValue.toString())
|
||||
val response = RetrofitNetwork.getNetworkService().updateDeviceInfo(bean)
|
||||
withContext(Dispatchers.Main) {
|
||||
//val body = response.body()
|
||||
// if (response.isSuccessful && body?.code == 0) {
|
||||
// Toast.makeText(this@StrikePositionActivity, R.string.submit_success, Toast.LENGTH_SHORT).show()
|
||||
// finish()
|
||||
// } else {
|
||||
// Toast.makeText(this@StrikePositionActivity, R.string.submit_failed, Toast.LENGTH_SHORT).show()
|
||||
// }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(this@StrikePositionActivity, R.string.submit_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user