@@ -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 . s etOnClickListener {
finish ( )
}
deviceId = DeviceIdEvent . g etDeviceId ( )
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 ( )
}
}
}
}
}
}