自定义控件之 Gamepad (游戏手柄)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zgcqflqinhao/article/details/77089542

这段时间自己在复刻一个小时候玩过的小游戏——魔塔,在人物操控的时候刚开始用的感觉 low low 的上下左右四个方向键,后来受王者农药启发,决定采用现在很多游戏中的那种游戏手柄,网上也有例子,不过最近自己对自定义控件很感兴趣,决定自己撸一个,最后实现的效果是这样的:



看到这样的需要实现的效果应该就有个大致的思路了,首先需要画两个圆,一大一小,然后小圆可以被拖动,但是圆心不能在大圆外,最后我们需要能够监听到它是向哪个方向移动的。


1 准备工作

先定义好大圆小圆的半径,以及将圆心设置在控件的中心位置。
    private int width = 320;    //控件宽
    private int height = 320;   //控件高
    private Paint mPaint;   //画笔
    private float bigCircleX = 160; //大圆在 x 轴的坐标
    private float bigCircleY = 160; //大圆在 y 轴的坐标
    private float smallCircleX = 160;   //小圆在 x 轴的坐标
    private float smallCircleY = 160;   //小圆在 y 轴的坐标
    private float bigCircleR = 120; //大圆的半径
    private float smallCircleR = 40;    //小圆的半径
    private OnDirectionListener mOnDirectionListener;   //移动方向监听器

    public void setOnDirectionListener(OnDirectionListener onDirectionListener) {
        this.mOnDirectionListener = onDirectionListener;
    }

    public Gamepad(Context context) {
        this(context, null);
    }

    public Gamepad(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Gamepad(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //新建画笔
        mPaint = new Paint();
        //设置画笔粗细
        mPaint.setStrokeWidth(2);
        //设置抗锯齿
        mPaint.setAntiAlias(true);
        //设置画笔样式
        mPaint.setStyle(Paint.Style.STROKE);
        //设置画笔颜色
        mPaint.setColor(Color.BLACK);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(width, height);
    }

2 画圆

第一步非常 easy,根据已确定的大圆小圆半径,在 onDraw() 方法中画出两个圆:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画大圆
        canvas.drawCircle(bigCircleX, bigCircleY, bigCircleR, mPaint);
        //画小圆
        canvas.drawCircle(smallCircleX, smallCircleY, smallCircleR, mPaint);
    }

效果如下:



3 拖动小圆

接下来需要重写这个控件的 onTouchEvent() 方法,在按下和移动的时候记录下坐标,根据这个坐标来重绘小圆的位置,在抬起的时候将小圆的位置还原:

    @Override
    public boolean onTouchEvent(MotionEvent motionEvent) {
        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN || motionEvent.getAction() == MotionEvent.ACTION_MOVE) {
            //记录触摸的位置,根据这个位置来重绘小圆
            smallCircleX = motionEvent.getX();
            smallCircleY = motionEvent.getY();
        } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
            //在手指抬起离开屏幕后将小圆的位置还原
            smallCircleX = width / 2;
            smallCircleY = height / 2;
        }
        //重绘
        invalidate();
        return true;
    }

效果如下:



可以看到实现了拖动小圆的效果,但是同时也可以看到小圆会被拖到大圆外面,导致一部分显示不全,所以我们通过计算来保证小圆的圆心不能在大圆外。

当触摸点在大圆内的时候其实是无所谓的,我们只需要判断当触摸点在大圆外时,设置小圆圆心在该触摸点到大圆圆心的连接线与大圆边界的交点处,有点绕,先看一张图:



以上图为一个例子,我们以大圆圆心为原点建立坐标系,当触摸点为红色点时已经在大圆外了,所以我们要让这时的小圆圆心在蓝色点,如何计算这就要用到数学知识了,这应该是高中的数学知识。先说说如何判断触摸点在大圆外,这个很简单,利用勾股定理即可:Math.sqrt(Math.pow(motionEvent.getX() - width / 2, 2) + Math.pow(height / 2 - motionEvent.getY(), 2)) > bigCircleR,当红色三角形的斜边大于大圆半径时,即在大圆外。然后计算出这个红色三角形的 sin 值:double sin = (height / 2 - motionEvent.getY()) / Math.sqrt(Math.pow(motionEvent.getX() - width / 2, 2) + Math.pow(height / 2 - motionEvent.getY(), 2)) ,
以及 cos 值 :double cos = (motionEvent.getX() - width / 2) / Math.sqrt(Math.pow(motionEvent.getX() - width / 2, 2) + Math.pow(height / 2 - motionEvent.getY(), 2)),通过sin 和 cos 就能计算出蓝色三角形的邻边(在 x 轴的坐标)和对边(在 y 轴的坐标):smallCircleX = (float) (cos * bigCircleR + width / 2),smallCircleY = (float) (height / 2 - sin * bigCircleR),需要注意的是上面的计算中有对 width 和 height 进行的计算,这是因为我们上面自己假设了一个坐标系,但是画图的坐标系其实并不是我们假设的这个坐标系,画图的坐标系的原点是在左上角。

上面是以第一象限为例子,其他象限的计算稍微有点不同,但是思维是一样的,就不一一举例了,这一部分的具体代码可以在最后整体源码中查看。

至此,效果就已经如博文开头那样了。


4 添加移动方向监听器

同样在 onTouchEvent() 方法中,我们在最后判断一下触摸点的相对位置,然后自己设立一个标准,通过回调,可以实现移动方向的监听:
        //添加移动方向监听器
        if (mOnDirectionListener != null) {
            if (motionEvent.getY() < height / 4) {
                mOnDirectionListener.onUp();
            } else if (motionEvent.getY() > height / 4 * 3) {
                mOnDirectionListener.onDown();
            } else if (motionEvent.getX() < width / 4) {
                mOnDirectionListener.onLeft();
            } else if (motionEvent.getX() > width / 4 * 3) {
                mOnDirectionListener.onRight();
            }
        }

在 MainActivity 中设置监听试试:
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Gamepad gpGamepad= (Gamepad) findViewById(R.id.gp_gamepad);
        gpGamepad.setOnDirectionListener(new Gamepad.OnDirectionListener() {
            @Override
            public void onUp() {
                Log.i("Gamepad","Up");
            }

            @Override
            public void onDown() {
                Log.i("Gamepad","Down");
            }

            @Override
            public void onLeft() {
                Log.i("Gamepad","Left");
            }

            @Override
            public void onRight() {
                Log.i("Gamepad","Right");
            }
        });
    }
}

然后像下图这样移动:

打印如下(部分打印):


5 总结

以前总是听到数学在理科中很重要,但是之前在编程开发中并没有很深的体会,直到最近研究自定义控件才感受到,数学真的很重要,上面计算小圆圆心超出大圆后的应该在的位置还可以用相似三角形来做。学一个东西就能发现自己在很多方面的不足,自己的路还很长,给自己一点信心和动力,我还可以做得更好。

6 源码

最后附上完整代码:

public class Gamepad extends View {
    private int width = 320;    //控件宽
    private int height = 320;   //控件高
    private Paint mPaint;   //画笔
    private float bigCircleX = 160; //大圆在 x 轴的坐标
    private float bigCircleY = 160; //大圆在 y 轴的坐标
    private float smallCircleX = 160;   //小圆在 x 轴的坐标
    private float smallCircleY = 160;   //小圆在 y 轴的坐标
    private float bigCircleR = 120; //大圆的半径
    private float smallCircleR = 40;    //小圆的半径
    private OnDirectionListener mOnDirectionListener;   //移动方向监听器

    public void setOnDirectionListener(OnDirectionListener onDirectionListener) {
        this.mOnDirectionListener = onDirectionListener;
    }

    public Gamepad(Context context) {
        this(context, null);
    }

    public Gamepad(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Gamepad(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //新建画笔
        mPaint = new Paint();
        //设置画笔粗细
        mPaint.setStrokeWidth(2);
        //设置抗锯齿
        mPaint.setAntiAlias(true);
        //设置画笔样式
        mPaint.setStyle(Paint.Style.STROKE);
        //设置画笔颜色
        mPaint.setColor(Color.BLACK);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画大圆
        canvas.drawCircle(bigCircleX, bigCircleY, bigCircleR, mPaint);
        //画小圆
        canvas.drawCircle(smallCircleX, smallCircleY, smallCircleR, mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent motionEvent) {
        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN || motionEvent.getAction() == MotionEvent.ACTION_MOVE) {
            //记录触摸的位置,根据这个位置来重绘小圆
            if (motionEvent.getX() > width / 2 && motionEvent.getY() < height / 2) {    //第一象限
                if (Math.sqrt(Math.pow(motionEvent.getX() - width / 2, 2) + Math.pow(height / 2 - motionEvent.getY(), 2)) > bigCircleR) {   //圆外
                    double sin = (height / 2 - motionEvent.getY()) / Math.sqrt(Math.pow(motionEvent.getX() - width / 2, 2) + Math.pow(height / 2 - motionEvent.getY(), 2));
                    double cos = (motionEvent.getX() - width / 2) / Math.sqrt(Math.pow(motionEvent.getX() - width / 2, 2) + Math.pow(height / 2 - motionEvent.getY(), 2));
                    smallCircleX = (float) (cos * bigCircleR + width / 2);
                    smallCircleY = (float) (height / 2 - sin * bigCircleR);
                } else {    //圆内
                    smallCircleX = motionEvent.getX();
                    smallCircleY = motionEvent.getY();
                }
            } else if (motionEvent.getX() < width / 2 && motionEvent.getY() < height / 2) { //第二象限
                if (Math.sqrt(Math.pow(width / 2 - motionEvent.getX(), 2) + Math.pow(height / 2 - motionEvent.getY(), 2)) > bigCircleR) {   //圆外
                    double sin = (height / 2 - motionEvent.getY()) / Math.sqrt(Math.pow(width / 2 - motionEvent.getX(), 2) + Math.pow(height / 2 - motionEvent.getY(), 2));
                    double cos = (width / 2 - motionEvent.getX()) / Math.sqrt(Math.pow(width / 2 - motionEvent.getX(), 2) + Math.pow(height / 2 - motionEvent.getY(), 2));
                    smallCircleX = (float) (width / 2 - cos * bigCircleR);
                    smallCircleY = (float) (height / 2 - sin * bigCircleR);
                } else {    //圆内
                    smallCircleX = motionEvent.getX();
                    smallCircleY = motionEvent.getY();
                }
            } else if (motionEvent.getX() < width / 2 && motionEvent.getY() > height / 2) { //第三象限
                if (Math.sqrt(Math.pow(width / 2 - motionEvent.getX(), 2) + Math.pow(motionEvent.getY() - height / 2, 2)) > bigCircleR) {   //圆外
                    double sin = (motionEvent.getY() - height / 2) / Math.sqrt(Math.pow(width / 2 - motionEvent.getX(), 2) + Math.pow(motionEvent.getY() - height / 2, 2));
                    double cos = (width / 2 - motionEvent.getX()) / Math.sqrt(Math.pow(width / 2 - motionEvent.getX(), 2) + Math.pow(motionEvent.getY() - height / 2, 2));
                    smallCircleX = (float) (width / 2 - cos * bigCircleR);
                    smallCircleY = (float) (height / 2 + sin * bigCircleR);
                } else {    //圆内
                    smallCircleX = motionEvent.getX();
                    smallCircleY = motionEvent.getY();
                }
            } else if (motionEvent.getX() > width / 2 && motionEvent.getY() > height / 2) { //第四象限
                if (Math.sqrt(Math.pow(motionEvent.getX() - width / 2, 2) + Math.pow(motionEvent.getY() - height / 2, 2)) > bigCircleR) {   //圆外
                    double sin = (motionEvent.getY() - height / 2) / Math.sqrt(Math.pow(motionEvent.getX() - width / 2, 2) + Math.pow(motionEvent.getY() - height / 2, 2));
                    double cos = (motionEvent.getX() - width / 2) / Math.sqrt(Math.pow(motionEvent.getX() - width / 2, 2) + Math.pow(motionEvent.getY() - height / 2, 2));
                    smallCircleX = (float) (width / 2 + cos * bigCircleR);
                    smallCircleY = (float) (height / 2 + sin * bigCircleR);
                } else {    //圆内
                    smallCircleX = motionEvent.getX();
                    smallCircleY = motionEvent.getY();
                }
            }
        } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
            //在手指抬起离开屏幕后将小圆的位置还原
            smallCircleX = width / 2;
            smallCircleY = height / 2;
        }
        //添加移动方向监听器
        if (mOnDirectionListener != null) {
            if (motionEvent.getY() < height / 4) {
                mOnDirectionListener.onUp();
            } else if (motionEvent.getY() > height / 4 * 3) {
                mOnDirectionListener.onDown();
            } else if (motionEvent.getX() < width / 4) {
                mOnDirectionListener.onLeft();
            } else if (motionEvent.getX() > width / 4 * 3) {
                mOnDirectionListener.onRight();
            }
        }
        //重绘
        invalidate();
        return true;
    }

    public interface OnDirectionListener {
        void onUp();

        void onDown();

        void onLeft();

        void onRight();
    }
}




猜你喜欢

转载自blog.csdn.net/zgcqflqinhao/article/details/77089542