diff --git a/app/src/main/java/com/example/defenseapplication/liveVideo/StrikePositionActivity.kt b/app/src/main/java/com/example/defenseapplication/liveVideo/StrikePositionActivity.kt index cc0d805..1804cee 100644 --- a/app/src/main/java/com/example/defenseapplication/liveVideo/StrikePositionActivity.kt +++ b/app/src/main/java/com/example/defenseapplication/liveVideo/StrikePositionActivity.kt @@ -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 { + 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() + } + } + } } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/strike_position_activity.xml b/app/src/main/res/layout/strike_position_activity.xml index 7186810..cc20775 100644 --- a/app/src/main/res/layout/strike_position_activity.xml +++ b/app/src/main/res/layout/strike_position_activity.xml @@ -9,7 +9,6 @@ android:background="@color/bg_gray" tools:context=".liveVideo.StrikePositionActivity"> - - - - - + - + - + - - \ No newline at end of file + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 6ebda08..bb960cb 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -204,6 +204,8 @@ 打击位置 请点击下方人体模型,选择大致打击位置 确定修改为该打击位置? + 提交成功 + 提交失败 容器1 容器2 容器3 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d654678..f7764ff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -387,6 +387,8 @@ Strike Position Please click the human model below to select the approximate strike position Confirm to modify to this strike position? + Submitted successfully + Submission failed Container 1 Container 2 Container 3