功能特性:
-
支持360度方向检测
-
摇杆手柄跟随触摸移动
-
手柄限制在摇杆圆盘范围内
-
使用协程实现高频率回调(60fps)
-
提供X/Y偏移百分比、角度、幅度等信息
正文
RobotRocker
Kotlin
package com.example.dsbridge_demo
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import kotlinx.coroutines.*
import kotlin.math.*
/**
* 摇杆控制View
* 功能特性:
* - 支持360度方向检测
* - 摇杆手柄跟随触摸移动
* - 手柄限制在摇杆圆盘范围内
* - 使用协程实现高频率回调(60fps)
* - 提供X/Y偏移百分比、角度、幅度等信息
*/
class RobotRocker @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// ==================== 位置和尺寸 ====================
/** 圆盘中心X坐标 */
private var centerX = 0f
/** 圆盘中心Y坐标 */
private var centerY = 0f
/** 圆盘半径 */
private var baseRadius = 0f
/** 手柄半径 */
private var hatRadius = 0f
/** 手柄当前位置X坐标 */
private var currentX = 0f
/** 手柄当前位置Y坐标 */
private var currentY = 0f
// ==================== 画笔 ====================
/** 圆盘画笔 */
private val basePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.LTGRAY
style = Paint.Style.FILL
}
/** 手柄画笔 */
private val hatPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.DKGRAY
style = Paint.Style.FILL
}
// ==================== 监听和协程 ====================
/** 摇杆监听器 */
private var listener: RockerListener? = null
/** 协程任务 */
private var rockerJob: Job? = null
/** 协程作用域 */
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
/** 回调间隔(毫秒),16ms约60fps */
private val callbackInterval = 16L
/**
* View尺寸改变时调用,重新计算圆盘和手柄的参数
*/
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// 圆盘中心位于View中心
centerX = width / 2f
centerY = height / 2f
// 初始化手柄位置为圆盘中心
currentX = centerX
currentY = centerY
// 圆盘半径取View宽度和高度的最小值的1/3
baseRadius = (minOf(width, height) / 3f)
// 手柄半径取View宽度和高度的最小值的1/6
hatRadius = (minOf(width, height) / 6f)
}
/**
* 绘制View
*/
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制摇杆圆盘(灰色圆形)
canvas.drawCircle(centerX, centerY, baseRadius, basePaint)
// 绘制摇杆手柄(深灰色圆形)
canvas.drawCircle(currentX, currentY, hatRadius, hatPaint)
}
/**
* 处理触摸事件
*/
override fun onTouchEvent(event: MotionEvent?): Boolean {
event ?: return false
return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
handleTouch(event.x, event.y, event.action == MotionEvent.ACTION_DOWN)
true
}
MotionEvent.ACTION_UP -> {
resetRocker()
true
}
else -> super.onTouchEvent(event)
}
}
/**
* 处理触摸位置
* @param x 触摸点X坐标
* @param y 触摸点Y坐标
* @param isActionDown 是否为按下事件
*/
private fun handleTouch(x: Float, y: Float, isActionDown: Boolean) {
// 计算触摸点到圆心的距离
val distance = sqrt((x - centerX).pow(2) + (y - centerY).pow(2))
// 限制手柄在圆盘范围内
if (distance <= baseRadius) {
currentX = x
currentY = y
} else {
// 触摸点超出范围,将手柄限制在圆盘边缘
val angle = atan2(y - centerY, x - centerX)
currentX = centerX + cos(angle) * baseRadius
currentY = centerY + sin(angle) * baseRadius
}
invalidate()
// 按下时启动协程回调
if (isActionDown && listener != null) {
startCallbackLoop()
}
}
/**
* 启动回调循环
*/
private fun startCallbackLoop() {
rockerJob?.cancel()
rockerJob = coroutineScope.launch {
while (isActive) {
val xPercent = (currentX - centerX) / baseRadius
val yPercent = (currentY - centerY) / baseRadius
val angle = Math.toDegrees(atan2(-yPercent.toDouble(), -xPercent.toDouble())).toFloat()
val magnitude = sqrt(xPercent.pow(2) + yPercent.pow(2)) * 100f
listener?.onRockerMoved(xPercent, yPercent, angle, magnitude)
delay(callbackInterval)
}
}
}
/**
* 重置摇杆到中心位置
*/
private fun resetRocker() {
currentX = centerX
currentY = centerY
invalidate()
listener?.onRockerMoved(0f, 0f, 0f, 0f)
rockerJob?.cancel()
rockerJob = null
}
/**
* 设置摇杆监听器
* @param listener 摇杆监听器
*/
fun setRockerListener(listener: RockerListener?) {
this.listener = listener
}
/**
* View从窗口分离时调用,清理资源
*/
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
rockerJob?.cancel()
coroutineScope.cancel()
}
/**
* 摇杆监听器接口
*/
interface RockerListener {
/**
* 摇杆移动回调
* @param xPercent X轴偏移百分比(-1到1)
* @param yPercent Y轴偏移百分比(-1到1)
* @param angle 角度(-180到180度)
* @param magnitude 移动幅度(0到100)
*/
fun onRockerMoved(xPercent: Float, yPercent: Float, angle: Float, magnitude: Float)
}
}
DirectionalDiskActivity
Kotlin
package com.example.dsbridge_demo
import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
/**
* 摇杆控制演示Activity
*/
class DirectionalDiskActivity : AppCompatActivity() {
private lateinit var robotRocker: RobotRocker
private lateinit var infoText: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_directional_disk)
initViews()
setupRockerListener()
}
private fun initViews() {
robotRocker = findViewById(R.id.robotRocker)
infoText = findViewById(R.id.directionText)
}
/**
* 设置摇杆监听器
*/
private fun setupRockerListener() {
robotRocker.setRockerListener(object : RobotRocker.RockerListener {
override fun onRockerMoved(xPercent: Float, yPercent: Float, angle: Float, magnitude: Float) {
Log.d("RobotRocker", "X: $xPercent, Y: $yPercent, Angle: $angle, Magnitude: $magnitude")
val direction = getDirectionByAngle(angle, magnitude)
updateInfoText(xPercent, yPercent, angle, magnitude, direction)
}
})
}
/**
* 根据角度和幅度获取方向
* @param angle 角度(-180到180度)
* @param magnitude 幅度(0到100)
* @return 方向文字
*/
private fun getDirectionByAngle(angle: Float, magnitude: Float): String {
return when {
magnitude < 10f -> "无"
angle >= -22.5 && angle < 22.5 -> "右"
angle >= 22.5 && angle < 67.5 -> "右上"
angle >= 67.5 && angle < 112.5 -> "上"
angle >= 112.5 && angle < 157.5 -> "左上"
(angle >= 157.5 && angle <= 180) || (angle >= -180 && angle < -157.5) -> "左"
angle >= -157.5 && angle < -112.5 -> "左下"
angle >= -112.5 && angle < -67.5 -> "下"
angle >= -67.5 && angle < -22.5 -> "右下"
else -> "无"
}
}
/**
* 更新信息显示
*/
private fun updateInfoText(
xPercent: Float,
yPercent: Float,
angle: Float,
magnitude: Float,
direction: String
) {
infoText.text = buildString {
appendLine("摇杆状态")
appendLine("X偏移: ${String.format("%.2f", xPercent * 100)}%")
appendLine("Y偏移: ${String.format("%.2f", yPercent * 100)}%")
appendLine("角度: ${String.format("%.1f", angle)}°")
appendLine("幅度: ${String.format("%.1f", magnitude)}%")
append("方向: $direction")
}
}
}
activity_directional_disk
html
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:background="#FAFAFA">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="摇杆控制"
android:textSize="24sp"
android:textColor="#333333"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="32dp" />
<com.example.dsbridge_demo.RobotRocker
android:id="@+id/robotRocker"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_gravity="center" />
<TextView
android:id="@+id/directionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="当前方向: 无"
android:textSize="18sp"
android:textColor="#666666"
android:gravity="center"
android:layout_marginTop="32dp"
android:padding="16dp"
android:background="#E0E0E0" />
</LinearLayout>