通过surfaceView实现的虚拟摇杆控制
我们的机器人开发当中,移动端APP有一个控制的功能,实现当中,使用了一个类似于游戏手柄的界面,面对这样的界面首先肯定想到了使用"绘制"的方式搞定.那么就会使用到view或者surfaceview来实现了,而使用surfaceview能够在非UI线程上面进行,这必然是一大优势.先上图:
外部是一个背景图片,中间的渐变色圆圈是可以滑动"虚拟摇杆",控制区域是分为四个区域,如下图所示:
和第一个图相对比,其实就是要让虚拟摇杆在上图滑动的时候,在"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相应的工程代码我打包放下载模块了,可以下载后稍作修改来测试(代码下载),这里并没有过多考虑潜在的问题,只是按照心里的想法直接做了实现最后测试也米有什么问题发生,打算就这样先使用了,欢迎有更好实现方法的童鞋提出来,感激之至.