美文网首页
用Kotlin自定义虚拟游戏十字键

用Kotlin自定义虚拟游戏十字键

作者: 超级绿茶 | 来源:发表于2021-01-25 10:35 被阅读0次

自定义虚拟十字键控件在我之前的“游戏开发”专辑中就已经用过到,只是当时用的是Java编写,而且也没有多作解释,这次用Kotlin翻新了一下。

实现原理:

以控件的中心为原点画一个正圆,圆的直径以控件最窄的一边为准,然后以12点钟方向开始顺时钟的划分出16个分区,以第0和第15区代表上,第1和第2区代表上右,以此类推每2个分区为一个方向,正好是8个方向。至于为什么不直接划分8个分区而是16个?我也没办法,现在这样的算法也是从网上找的。然后根据触摸事件的x和y坐标就能判断出具体对应的方向。


演示效果

使用方法:

首先在XML布局中放置十字键控件并作响应的设置,如:显示的背景色、背景图片、是否显示方向箭头等,具体见下文。

接着在代码中为这个控件设置响应方向的监听器。因为我是用Kotlin写的,而且只需要一个回调方法,所以我直接用了一个函数类型代替Java时代常见的接口。这个函数类型定义了一个名为direction的String类型参数,用于获取我们实际触摸到的方向。然后在调用时通过setActionListener方法传入这个函数类型即可。

val rocker = findViewById<CrossRocker>(R.id.rocker)
rocker.setActionListener { v, direction, action ->
    Log.i("123", "$direction")
    ...
}

属性设置:

  • app:padBackgroundColor 设置圆盘的背景色,默认为透明色。
  • app:padBackground 设置圆盘的背景图片,此属性会覆盖掉圆盘的背景色。
  • app:padHotSportColor 设置触屏时的焦点色。
  • app:showHotSport 是否显示触屏时的焦点区域,默认不显示
  • app:showPartitionLine 是否显示分区线,默认为不显示
  • app:showAxisArrow 是否显示正轴箭头,默认为不显示
  • app:showAngleArrow 是否显示夹角箭头(上左,上右,下左,下右),默认为不显示
  • app:arrowLight 设置箭头的高度色
  • app:arrowDark 设置箭头的普通色
    控件暂不支持Drawable格式的背景,如果需要的话可以用FrameLayout布局把此控件设成透明,然后叠加在一个ImagView之上。

activity_main.xml

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/vgBox"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <View
            android:id="@+id/viewBox"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_gravity="center"
            android:background="@color/design_default_color_primary" />
    </FrameLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="@android:color/darker_gray">

        <FrameLayout
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:layout_gravity="center"
            android:background="@drawable/shape_rocker">

            <com.bamboo.crossrockerdemo.CrossRocker
                android:id="@+id/rocker"
                android:layout_width="300dp"
                android:layout_height="300dp"
                app:arrowDark="#999"
                app:arrowLight="@android:color/white"
                app:padBackgroundColor="#3000"
                app:padHotSportColor="#ff6600"
                app:showAxisArrow="true"
                app:showPartitionLine="false" />
        </FrameLayout>
    </FrameLayout>
</LinearLayout>

MainActivity.kt

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.MotionEvent
import android.view.View
import com.jeremyliao.liveeventbus.LiveEventBus
import kotlinx.android.synthetic.main.activity_main.*
import kotlin.properties.Delegates

class MainActivity : AppCompatActivity() {
    private var centerX by Delegates.notNull<Float>()
    private var centerY by Delegates.notNull<Float>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 给十字键设置响应监听
        rocker.setActionListener { v, direction, motionEvent ->
            Log.i("123", "$direction")
            onGamePadKey(motionEvent.action, direction)
        }
        // 保存演示控件位于父控件的中心位置
        vgBox.post {
            centerX = (vgBox.width - viewBox.width) / 2f
            centerY = (vgBox.height - viewBox.height) / 2f
        }
    }

    private fun onGamePadKey(action: Int, direction: String) {
        when (action) {
            MotionEvent.ACTION_DOWN,
            MotionEvent.ACTION_MOVE,
            MotionEvent.ACTION_POINTER_DOWN -> {
                var vx = 0f
                var vy = 0f
                when (direction) {
                    CrossRocker.RIGHT -> vx = 30f
                    CrossRocker.LEFT -> vx = -30f
                    CrossRocker.TOP -> vy = -30f
                    CrossRocker.BOTTOM -> vy = 30f
                    CrossRocker.TOP_RIGHT -> {
                        vx = 30f
                        vy = -30f
                    }
                    CrossRocker.BOTTOM_RIGHT -> {
                        vx = 30f
                        vy = 30f
                    }
                    CrossRocker.BOTTOM_LEFT -> {
                        vx = -30f
                        vy = 30f
                    }
                    CrossRocker.TOP_LEFT -> {
                        vx = -30f
                        vy = -30f
                    }
                }
                viewBox.x = centerX + vx
                viewBox.y = centerY + vy
            }
            MotionEvent.ACTION_UP,
            MotionEvent.ACTION_CANCEL -> {
                viewBox.x = centerX
                viewBox.y = centerY
            }
        }
    }
}

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="CrossRocker">
        <attr name="padHotSportColor" format="reference|color" />
        <attr name="padBackgroundColor" format="reference|color" />
        <attr name="padBackground" format="reference|color" />
        <attr name="padLightColor" format="reference|color" />
        <attr name="arrowLight" format="reference|color" />
        <attr name="arrowDark" format="reference|color" />
        <attr name="showPartitionLine" format="reference|boolean" />
        <attr name="showHotSport" format="reference|boolean" />
        <attr name="showAxisArrow" format="reference|boolean" />
        <attr name="showAngleArrow" format="reference|boolean" />
    </declare-styleable>

</resources>

CrossRocker.kt

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.core.graphics.withRotation
import kotlin.math.cos
import kotlin.math.sin

/**
 * 游戏手柄的虚拟十字键,此控件主要处理上下左右的事件
 */
class CrossRocker : View {
    companion object {
        private const val PARTITION = 16
        private const val CENTER_PART_SIZE_RATIO = 0.28f
        const val TOP = "top"
        const val BOTTOM = "bottom"
        const val LEFT = "left"
        const val RIGHT = "right"
        const val TOP_LEFT = "top_left"
        const val TOP_RIGHT = "top_right"
        const val BOTTOM_LEFT = "bottom_left"
        const val BOTTOM_RIGHT = "bottom_right"
    }

    private val paint = Paint()
    private var currWidth = -1
    private var currHeight = -1
    private var currRadius = -1.0f
    private var centerX = -1f
    private var centerY = -1f
    private var touched = false
    private var touchedX = -1f
    private var touchedY = -1f
    private var lastPartition = -1

    private var vectorX = FloatArray(PARTITION + 1)
    private var vectorY = FloatArray(PARTITION + 1)

    private var isShowPartitionLine = false // 是否显示分区线
    private var isShowHotsport = false // 是否显示点击热点
    private var isShowAxisArrow = false // 是否显示上下左右方向的箭头
    private var isShowAngleArrow = false // 是否显示夹角方向的箭头
    private var resPadBackground = -1
    private var colorBackground = Color.TRANSPARENT
    private var colorPadLine = Color.WHITE
    private var colorHotspot = Color.CYAN
    private var colorArrowDark = Color.GRAY
    private var colorArrowLight = Color.WHITE
    private var onAction: ((View, String, MotionEvent) -> Unit)? = null

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        init(context, attrs)
    }

    constructor(context: Context?) : super(context) {
        init(context, null)
    }

    @SuppressLint("ClickableViewAccessibility")
    private fun init(context: Context?, attrs: AttributeSet?) {
        if (attrs != null) {
            context?.obtainStyledAttributes(attrs, R.styleable.CrossRocker)?.let {
                resPadBackground =
                    it.getResourceId(R.styleable.CrossRocker_padBackground, resPadBackground)
                colorBackground =
                    it.getColor(R.styleable.CrossRocker_padBackgroundColor, colorBackground)
                colorArrowDark = it.getColor(R.styleable.CrossRocker_arrowDark, colorArrowDark)
                colorArrowLight = it.getColor(R.styleable.CrossRocker_arrowLight, colorArrowLight)
                colorHotspot = it.getColor(R.styleable.CrossRocker_padHotSportColor, colorHotspot)
                isShowHotsport = it.getBoolean(R.styleable.CrossRocker_showHotSport, isShowHotsport)
                isShowPartitionLine =
                    it.getBoolean(R.styleable.CrossRocker_showPartitionLine, isShowPartitionLine)
                isShowAxisArrow =
                    it.getBoolean(R.styleable.CrossRocker_showAxisArrow, isShowAxisArrow)
                isShowAngleArrow =
                    it.getBoolean(R.styleable.CrossRocker_showAngleArrow, isShowAngleArrow)
            }
        }
        setPartition()
        setOnTouchListener { v, event ->
            this.onTouch(event)
            true
        }
    }

    fun setShowPartitionLine(show: Boolean) {
        isShowPartitionLine = show
    }

    fun setColorPadLine(color: Int) {
        colorPadLine = color
    }

    fun setHotSportColor(color: Int) {
        colorHotspot = color
    }

    /**
     * 设置方向分区
     * 将圆形等比例划分成n个分区
     */
    private fun setPartition() {
        val deg = 2 * Math.PI / PARTITION
        val cos = cos(-deg)
        val sin = sin(-deg)
        vectorX[PARTITION] = 0.0f
        vectorX[0] = vectorX[PARTITION]
        vectorY[PARTITION] = currRadius
        vectorY[0] = vectorY[PARTITION]
        for (i in 1 until PARTITION + 1) {
            vectorX[i] = (vectorX[i - 1] * cos - vectorY[i - 1] * sin).toFloat()
            vectorY[i] = (vectorX[i - 1] * sin + vectorY[i - 1] * cos).toFloat()
        }
    }

    private fun cross(x1: Float, y1: Float, x2: Float, y2: Float): Float {
        return x1 * y2 - x2 * y1
    }

    /**
     * 判断坐标在方向分区
     */
    private fun getPartition(x: Float, y: Float): Int {
        if (x <= -1.0f && y <= -1.0f) return -1
        val vx = x - centerX
        var vy = y - centerY
        vy *= -1.0f
        if (vx * vx + vy * vy < CENTER_PART_SIZE_RATIO * CENTER_PART_SIZE_RATIO * currRadius * currRadius / 4) return 0
        var left = 0
        var right = PARTITION
        while (right - left > 1) {
            val mid = (left + right) / 2
            if (cross(vectorX[left], vectorY[left], vx, vy) <= 0
                && cross(vectorX[mid], vectorY[mid], vx, vy) >= 0
            ) {
                right = mid
            } else {
                left = mid
            }
        }
        return left
    }

    private fun getDirection(partition: Int): String {
        return when (partition) {
            in intArrayOf(0, 15) -> "top"
            in intArrayOf(1, 2) -> "top_right"
            in intArrayOf(3, 4) -> "right"
            in intArrayOf(5, 6) -> "bottom_right"
            in intArrayOf(7, 8) -> "bottom"
            in intArrayOf(9, 10) -> "bottom_left"
            in intArrayOf(11, 12) -> "left"
            in intArrayOf(13, 14) -> "top_left"
            else -> ""
        }
    }

    private fun onTouch(evt: MotionEvent): Boolean {
        val action = evt.actionMasked
        val x = evt.x
        val y = evt.y
        lastPartition = getPartition(x, y)
        when (action and MotionEvent.ACTION_MASK) {
            MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
                touchedX = x
                touchedY = y
                touched = true
                this.postInvalidate()
                onAction?.let { it(this, getDirection(lastPartition), evt) }
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> {
                touchedX = -1f
                touchedY = -1f
                touched = false
                this.postInvalidate()
                onAction?.let { it(this, getDirection(lastPartition), evt) }
            }
            MotionEvent.ACTION_MOVE -> {
                touchedX = x
                touchedY = y
                this.postInvalidate()
                onAction?.let { it(this, getDirection(lastPartition), evt) }
            }
        }
        return true
    }

    public override fun onDraw(canvas: Canvas) {
        val width = this.width
        val height = this.height
        val radius = (if (width < height) width else height) / 2.0f
        if (width != currWidth || height != currHeight) {
            centerX = (width / 2).toFloat()
            centerY = (height / 2).toFloat()
            currWidth = width
            currHeight = height
            currRadius = radius
            setPartition()
        }
        // 绘制圆盘背景色
        drawBackground(canvas, width, height, radius)
        // 绘制分区线
        if (isShowPartitionLine) {
            drawPartitionLine(canvas)
        }
        // 绘制正轴箭头
        if (isShowAxisArrow) {
            drawArrow(canvas, radius, intArrayOf(15, 0), 0f)
            drawArrow(canvas, radius, intArrayOf(3, 4), 90f)
            drawArrow(canvas, radius, intArrayOf(7, 8), 180f)
            drawArrow(canvas, radius, intArrayOf(11, 12), 270f)
        }
        // 绘制夹角箭头
        if (isShowAngleArrow) {
            drawArrow(canvas, radius, intArrayOf(1, 2), 45f)
            drawArrow(canvas, radius, intArrayOf(5, 6), 135f)
            drawArrow(canvas, radius, intArrayOf(9, 10), 225f)
            drawArrow(canvas, radius, intArrayOf(13, 14), 315f)
        }
        // 绘制按下时的热点
        if (touched && isShowHotsport) {
            paint.color = colorHotspot
            paint.style = Paint.Style.FILL
            canvas.drawCircle(touchedX, touchedY, (radius / 5f).toFloat(), paint)
        }
    }

    /**
     * 绘制圆盘背景
     */
    private fun drawBackground(canvas: Canvas, width: Int, height: Int, radius: Float) {
        if (resPadBackground != -1) {
            val bmp = BitmapFactory.decodeResource(context.resources, resPadBackground)
            if (bmp != null)
                canvas.drawBitmap(bmp, null, RectF(0f, 0f, width.toFloat(), height.toFloat()), null)
        } else {
            paint.color = colorBackground
            canvas.drawCircle(centerX, centerY, radius, paint)
        }
    }

    /**
     * 绘制箭头
     * @param radius 圆盘半径
     * @param part   需要响应的分区
     * @param degree 绘制的箭头指向的角度
     */
    private fun drawArrow(canvas: Canvas, radius: Float, part: IntArray, degree: Float) {
        val tempMask = paint.maskFilter
        if (touched && lastPartition in part) {
            paint.color = colorArrowLight
            paint.style = Paint.Style.FILL
            paint.maskFilter = BlurMaskFilter(35f, BlurMaskFilter.Blur.SOLID)
        } else {
            paint.color = colorArrowDark
            paint.style = Paint.Style.FILL
        }
        val offset = radius / 10
        val size = radius / 4
        val path = Path()
        path.moveTo(centerX, (centerY - radius + offset))
        path.lineTo(
            centerX + size / 2,
            centerY - radius + size + offset
        )
        path.lineTo(centerX - (size / 2), centerY - radius + size + offset)
        path.close()
        canvas.withRotation(degree, centerX, centerY) {
            this.drawPath(path, paint)
        }
        paint.maskFilter = tempMask
    }

    /**
     * 绘制分区线,主要是方便观察触屏时的落点
     */
    private fun drawPartitionLine(canvas: Canvas) {
        paint.color = colorPadLine
        repeat(PARTITION) { i ->
            canvas.drawLine(
                centerX, centerY,
                vectorX[i] + centerX,
                (-vectorY[i]) + centerY,
                paint
            )
        }
    }

    /**
     * 十字键的监听
     * @param direction 响应的方向
     */
    fun setActionListener(onAction: (v: View, direction: String, action: MotionEvent) -> Unit) {
        this.onAction = onAction
    }
}

最后记得在app模块的build.gradle里加上

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }

关于虚拟十字键的自定义就介绍到这!

如果你对我的文章感兴趣
欢迎加入QQ群聊:口袋里的安卓
或关注微信公众号:口袋里的安卓

相关文章

网友评论

      本文标题:用Kotlin自定义虚拟游戏十字键

      本文链接:https://www.haomeiwen.com/subject/nnjezktx.html