打击位置

This commit is contained in:
2026-06-02 10:09:11 +08:00
parent c4fd35bfe4
commit a1ff4d100a
4 changed files with 232 additions and 56 deletions

View File

@@ -1,17 +1,54 @@
package com.example.defenseapplication.liveVideo package com.example.defenseapplication.liveVideo
import android.animation.ObjectAnimator
import android.os.Bundle import android.os.Bundle
import android.widget.RelativeLayout import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.example.defenseapplication.R import com.example.defenseapplication.R
import com.example.defenseapplication.bean.DeviceBean
import com.example.defenseapplication.databinding.StrikePositionActivityBinding 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.CustomDialogFragment
import com.example.defenseapplication.view.showDefenseDialog import com.example.defenseapplication.view.showDefenseDialog
import com.gyf.immersionbar.ImmersionBar 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() { class StrikePositionActivity : AppCompatActivity() {
private lateinit var binding: StrikePositionActivityBinding 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = StrikePositionActivityBinding.inflate(layoutInflater) binding = StrikePositionActivityBinding.inflate(layoutInflater)
@@ -22,35 +59,163 @@ class StrikePositionActivity : AppCompatActivity() {
.titleBar(binding.topBar) .titleBar(binding.topBar)
.init() .init()
binding.ivBack.setOnClickListener { deviceId = DeviceIdEvent.getDeviceId()
finish()
}
binding.btnSubmit.setOnClickListener { val initialPos = intent.getStringExtra("strikePosition")?.toIntOrNull() ?: Zone.CHEST.rawValue
showConfirmDialog() selectedZone = Zone.fromRawValue(initialPos)
}
// 获取准星的 LayoutParams并移除居中约束
markerLayoutParams = binding.ivStrikeMarker.layoutParams as RelativeLayout.LayoutParams
markerLayoutParams.removeRule(RelativeLayout.CENTER_HORIZONTAL)
markerLayoutParams.removeRule(RelativeLayout.CENTER_VERTICAL)
binding.ivStrikeMarker.layoutParams = markerLayoutParams
// 设置人体图片的点击监听 binding.ivBack.setOnClickListener { finish() }
binding.btnSubmit.setOnClickListener { showConfirmDialog() }
// Use OnTouchListener to track touch coordinates
binding.ivHumanBody.setOnTouchListener { _, event -> binding.ivHumanBody.setOnTouchListener { _, event ->
if (event.action == android.view.MotionEvent.ACTION_DOWN) { lastTouchX = event.x
moveMarkerTo(event.x, event.y) 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 true
} }
// 初始化准星位置人体头部假设在图片宽度的1/2高度的1/5处 }
binding.ivHumanBody.post {
val humanWidth = binding.ivHumanBody.width /**
val humanHeight = binding.ivHumanBody.height * Converts a screen coordinate (from onTouch) into a normalised [0..1] offset
if (humanWidth > 0 && humanHeight > 0) { * inside the drawable, accounting for:
val headX = humanWidth / 2f * - ImageView scaling (scaleType="fitCenter" adds letterboxing)
val headY = humanHeight / 5f * - Letterbox / pillarbox margins when image aspect ≠ view aspect
moveMarkerTo(headX, headY) */
} 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), positiveText = getString(R.string.confirm),
negativeText = getString(R.string.cancel) negativeText = getString(R.string.cancel)
) )
showDefenseDialog(config, onPositiveClick = { showDefenseDialog(config, onPositiveClick = { submitStrikePosition() })
finish()
// if(){
// ToastUtils.showSuccess(this, getString(R.string.wifi_connection_success))
// }else{
// ToastUtils.showError(this, getString(R.string.wifi_connection_failed))
// }
})
} }
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() private fun submitStrikePosition() {
markerLayoutParams.topMargin = top.toInt() val id = deviceId
marker.layoutParams = markerLayoutParams 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()
}
}
}
} }
} }

View File

@@ -9,7 +9,6 @@
android:background="@color/bg_gray" android:background="@color/bg_gray"
tools:context=".liveVideo.StrikePositionActivity"> tools:context=".liveVideo.StrikePositionActivity">
<!-- 标题栏 -->
<LinearLayout <LinearLayout
android:id="@+id/topBar" android:id="@+id/topBar"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -34,7 +33,6 @@
android:textStyle="bold" /> android:textStyle="bold" />
</LinearLayout> </LinearLayout>
<!-- 提示文字 -->
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -44,34 +42,32 @@
android:textColor="@color/gray" android:textColor="@color/gray"
android:textSize="14sp" /> android:textSize="14sp" />
<!-- 人体模型区域 --> <FrameLayout
<RelativeLayout android:layout_width="match_parent"
android:id="@+id/bodyLayout"
android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_margin="16dp"> android:layout_margin="16dp">
<!-- 人体模型图片 --> <!-- 人体背景图:点击此图选择打击区域 -->
<ImageView <ImageView
android:id="@+id/ivHumanBody" android:id="@+id/ivHumanBody"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="439dp" android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/human_body" /> android:src="@drawable/human_body" />
<!-- 打击位置标记 --> <!-- 打击标记图标(跟随选中的区域移动,坐标相对于父容器中心) -->
<ImageView <ImageView
android:id="@+id/ivStrikeMarker" android:id="@+id/ivStrikeMarker"
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="100dp" android:layout_height="100dp"
android:layout_centerHorizontal="true" android:layout_gravity="center"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/dajitubiao" /> android:src="@drawable/dajitubiao" />
</RelativeLayout> </FrameLayout>
<!-- 提交按钮 -->
<android.widget.Button <android.widget.Button
android:id="@+id/btnSubmit" android:id="@+id/btnSubmit"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -82,4 +78,4 @@
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="16sp" /> android:textSize="16sp" />
</LinearLayout> </LinearLayout>

View File

@@ -204,6 +204,8 @@
<string name="strike_position">打击位置</string> <string name="strike_position">打击位置</string>
<string name="click_model_select_strike_pos">请点击下方人体模型,选择大致打击位置</string> <string name="click_model_select_strike_pos">请点击下方人体模型,选择大致打击位置</string>
<string name="confirm_modify_strike_pos">确定修改为该打击位置?</string> <string name="confirm_modify_strike_pos">确定修改为该打击位置?</string>
<string name="submit_success">提交成功</string>
<string name="submit_failed">提交失败</string>
<string name="container_1">容器1</string> <string name="container_1">容器1</string>
<string name="container_2">容器2</string> <string name="container_2">容器2</string>
<string name="container_3">容器3</string> <string name="container_3">容器3</string>

View File

@@ -387,6 +387,8 @@
<string name="strike_position">Strike Position</string> <string name="strike_position">Strike Position</string>
<string name="click_model_select_strike_pos">Please click the human model below to select the approximate strike position</string> <string name="click_model_select_strike_pos">Please click the human model below to select the approximate strike position</string>
<string name="confirm_modify_strike_pos">Confirm to modify to this strike position?</string> <string name="confirm_modify_strike_pos">Confirm to modify to this strike position?</string>
<string name="submit_success">Submitted successfully</string>
<string name="submit_failed">Submission failed</string>
<string name="container_1">Container 1</string> <string name="container_1">Container 1</string>
<string name="container_2">Container 2</string> <string name="container_2">Container 2</string>
<string name="container_3">Container 3</string> <string name="container_3">Container 3</string>