通过surfaceView实现的虚拟摇杆控制

  我们的机器人开发当中,移动端APP有一个控制的功能,实现当中,使用了一个类似于游戏手柄的界面,面对这样的界面首先肯定想到了使用"绘制"的方式搞定.那么就会使用到view或者surfaceview来实现了,而使用surfaceview能够在非UI线程上面进行,这必然是一大优势.先上图:

通过surfaceView实现的虚拟摇杆控制

  外部是一个背景图片,中间的渐变色圆圈是可以滑动"虚拟摇杆",控制区域是分为四个区域,如下图所示:

通过surfaceView实现的虚拟摇杆控制

  和第一个图相对比,其实就是要让虚拟摇杆在上图滑动的时候,在"A/B/C/D"四个区域内做方向的输出指令变化,由于下位机提供的指令只有前后左右四个指令,因此目前我认为只有这样实现比较方便和直观了,要实现这样一个绘制区域,那么肯定会使用到坐标和对应的直线方程知识了,在这里先暂时不具体说方程,先看看布局如何加载:

<com.~~.demoapplication.ControlView
    android:id="@+id/control_view"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:layout_gravity="center"
    />
  新建一个xml文件,然后添加这个布局在里面背景图片我们在代码里面添加:

public ControlView(Context context, AttributeSet attrs){
    super(context, attrs);

    setBackgroundResource(R.drawable.hand_control_base);
     setZOrderOnTop(true);//使surfaceview放到最顶层
    getHolder().setFormat(PixelFormat.TRANSLUCENT);//使窗口支持透明度

    //初始化画笔
    initPaint();
    sfh = getHolder();
    sfh.addCallback(this);
    th = new Thread(new DrawViewRunnable());
}
  在ControlView生命周期函数里面增加背景图片,然后设置为最顶层,并且透明显示,要不然只会出现一个黑框的surfaceView区域.然后初始化paint,得到holder和 callback.最后一句是开启绘制线程.

  绘制线程我设置了200ms绘制更新一次界面:

class DrawViewRunnable implements Runnable {

    @Override
    public void run() {
        while(beginDrawing){
            try{
                myDraw();
                Thread.sleep(200);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
  主要的绘制方法即:myDraw(),如下:

    protected void myDraw() {
        //获取canvas实例
        canvas = sfh.lockCanvas();
        try {
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);//绘制透明色
            canvas.drawBitmap(mBitmap, cx, cy, paint);
            directionControl(cx, cy);
       
        }catch (java.lang.NullPointerException e){
            Log.d("TIEJIANG", "NuLLPointerException");
        }
        sfh.unlockCanvasAndPost(canvas);
    }
  然后是里面的directionControl方法了,也就是处理坐标和方程的地方,这里简单实用了直线方程的知识,如第一个图的两根对角线(对角箭头线),通过判断坐标是落在这两根直线的上方还是下方,然后综合判断即可得出中间的圆圈摇杆的实时位置,根据这个位置就可以发送对应的控制指令了.那么到这里就是要使用两个直线方程了,方程通过两点式或者斜率的方法即可写出,写之前要准备相应的坐标点:

public float[] getRect(){
    //获取屏幕宽度 (实际为获取了所创建的surfaceview的大小,并且是"绘制区域"大小)
    //区域分为:屏幕区域/应用区域/绘制区域
    float[] coord = new float[2];
    //获取屏幕宽,高度
    coord[0] = getWidth();
    coord[1] = getHeight();
    return coord;
}
  上述方法返回了绘制区域,也就是surfaceView区域的宽高值,另外,中间的圆圈摇杆也要处理一下,因为他是有大小的,要获取他的中心点坐标,也就是整个绘制区域的中心点坐标:

mBitmap = BitmapFactory.decodeResource(mResources, R.drawable.control_point);
radius = mBitmap.getHeight()/2;
cx = screenW/2 - radius;
cy = screenH/2 - radius;

  以上都是在surfaceViewChange生命周期方法中执行的,后两句是建立surfaceView后获得的中心位置坐标.要动态获得坐标信息肯定需要重写触摸滑动的方法:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 按下
        cx = (int) event.getX() - radius;
                cy = (int) event.getY() - radius;
                Log.d("TIEJIANG", "ACTION_DOWN---CX= " + cx + ", CY= " + cy);
                break;
            case MotionEvent.ACTION_MOVE:
                // 移动
        cx = (int) event.getX() - radius;
                cy = (int) event.getY() - radius;

//                Log.d("TIEJIANG", "CX= " + cx + ", CY= " + cy);
                break;
            case MotionEvent.ACTION_UP:
                // 抬起
        cx = (int) event.getX() - radius;
                cy = (int) event.getY() - radius;

                //小球回到原点
        cx = screenW/2 - radius;
                cy = screenH/2 - radius;
                break;
        }

        return true;
    }
  要注意添加上圆圈回到中点的坐标,这样每次触摸摇杆都是以中心为初始位置,抬起手指后,圆圈也会回到中心位置坐标.处理数据的直线方程(函数)如下:

    /**
     * return 1:forward; 2 back; 3 turn left; 4 turn right; 0 origin point
     * */
    public int directionControl(float x, float y){

        float circlePointX = screenW/2 - radius;
        float circlePointY = screenH/2 - radius;
        float equationOne = 0; //(x,y)和方程1比较的值
        float equationTwo = 0;//(x,y)和方程2比较的值
     /**
         * 构建方程
     * 方程1: y = (circlePointY)/(circlePointX) * x
         * 方程2: (y-circlePointY)/circlePointY = (x-circlePointX)/(screenW-circlePointX)
         * 方程2: (-circlePointY)*x + (circlePointX-screenW+radius)*y + (screenW-radius)*circlePointY = 0
         * */
        equationOne = (circlePointY)/(circlePointX) * x - y;
        equationTwo = (-circlePointY)*x + (circlePointX-screenW+radius)*y + (screenW-radius)*circlePointY;
//        Log.d("TIEJIANG", "equationOne= " + equationOne + ", equationTwo= " + equationTwo);

        // 注意去掉等号部分,等号部分在原点--初始位置
        if (equationOne > 0 && equationTwo > 0){  //"前进区域"
            Log.d("TIEJIANG", "forward");
            return 1 ;
        } else if (equationOne < 0 && equationTwo < 0){  // "后退区域"
            Log.d("TIEJIANG", "back");
            return 2;
        } else if(equationOne < 0 && equationTwo > 0){    //"左转区域"
            Log.d("TIEJIANG", "turn left");
            return 3;
        } else if (equationOne > 0 && equationTwo < 0){    //"右转区域"
            Log.d("TIEJIANG", "turn right");
            return 4;
        } else{
            Log.d("TIEJIANG", "origin point");
            return 0;
        }

    }
  如方法内的注释所示,两个直线方程是化解后的形式,后面是对坐标的判断,根据不同的位置得到对应的指令输出,在这里只是打印了log,并没有添加对应的逻辑.通过操作虚拟摇杆就能够得到对应的位置输出了:

09-06 09:03:51.686 14532-14611/? D/TIEJIANG: forward
09-06 09:03:51.888 14532-14611/? D/TIEJIANG: forward
09-06 09:03:52.090 14532-14611/? D/TIEJIANG: forward
09-06 09:03:52.292 14532-14611/? D/TIEJIANG: turn right
09-06 09:03:52.493 14532-14611/? D/TIEJIANG: turn right
09-06 09:03:52.701 14532-14611/? D/TIEJIANG: back
09-06 09:03:52.904 14532-14611/? D/TIEJIANG: back
09-06 09:03:53.106 14532-14611/? D/TIEJIANG: back
09-06 09:03:53.307 14532-14611/? D/TIEJIANG: origin point
09-06 09:03:53.509 14532-14611/? D/TIEJIANG: origin point
09-06 09:03:53.712 14532-14611/? D/TIEJIANG: origin point
09-06 09:03:53.915 14532-14611/? D/TIEJIANG: origin point
  相应的工程代码我打包放下载模块了,可以下载后稍作修改来测试(代码下载),这里并没有过多考虑潜在的问题,只是按照心里的想法直接做了实现最后测试也米有什么问题发生,打算就这样先使用了,欢迎有更好实现方法的童鞋提出来,感激之至.