打击位置
This commit is contained in:
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user