牛骨文教育服务平台(让学习变的简单)
博文笔记

Android自定义摇杆实现蓝牙控制小车

创建时间:2015-05-17 投稿人: 浏览次数:2349

1.1 背景

本文继《Android通过蓝牙HC06与Arduino通信实例》一文进行UI设计,考虑到四方向按键操作智能小车的体验性,不如做一个摇杆来控制来得好。

1.2 需求

1)控制摇杆由摇杆(小圆)和底座(大圆)组成; 2)全屏触摸,摇杆位置不离开底座范围; 3)停止触摸,摇杆恢复到中心,小车停止运动; 4)摇杆分成6个方向,分别控制小车 前进、后退、前进左、前进右、后退左、后退右。

2.1 SurfaceView

在Android系统中,有一种特殊的视图,称为SurfaceView,它拥有独立的绘图表面,即它不与其宿主窗口共享同一个绘图表面。由于拥有独立的绘图表面,因此SurfaceView的UI就可以在一个独立的线程中进行绘制。又由于不会占用主线程资源,SurfaceView一方面可以实现复杂而高效的UI,另一方面又不会导致用户输入得不到及时响应[1]。 Surface是纵深排序(Z-ordered)的,这表明它总在自己所在窗口的后面。SurfaceView提供了一个可见区域,只有在这个可见区域内 的surface部分内容才可见,可见区域外的部分不可见。
实现步骤: 1) SurfaceView.getHolder()获得SurfaceHolder对象
2) SurfaceHolder.addCallback(callback)添加回调函数
3) SurfaceHolder.lockCanvas()获得Canvas对象并锁定画布
4) Canvas绘画
5) SurfaceHolder.unlockCanvasAndPost(Canvas canvas)结束锁定画图,并提交改变,将图形显示

2.2 三角函数

使用余弦函数、反余弦函数对摇杆的角度进行计算

3.1 SurfaceView绘制

创建了SurfaceView类,MySurfaceView
private static final String TAG = "MySurfaceView";
private SurfaceHolder mHolder;
private Paint mPaint;
private Thread mThread;
private boolean mFlag;
private Canvas mCanvas;
public double mRad, mAngle;
public int mLogicType, mLogicStatus;
public Context mContext;

private int mRockCentX, mRockCentY, mRockRadius;
private int mBaseCentX, mBaseCentY, mBaseRadius;

private final int LOGIC_STOP   = 0x00;
private final int LOGIC_UP     = 0x01;
private final int LOGIC_DOWN   = 0x02;
private final int LOGIC_ULEFT  = 0x03;
private final int LOGIC_URIGHT = 0x04;
private final int LOGIC_DLEFT  = 0x05;
private final int LOGIC_DRIGHT = 0x06;
构造函数,对摇杆大小进行传参
/**
 * Constructor to initialize the size
 * @param context
 * @param x
 * @param y
 * @param r
 */
public MySurfaceView(Context context, int x, int y, int r) {
	super(context);
	mContext = context;
	mRockCentX  = mBaseCentX = x;
	mRockCentY  = mBaseCentY = y;
	mRockRadius = r;
	mBaseRadius = r * 3;
	
	mLogicStatus = -1;
	mHolder = this.getHolder();
	mHolder.addCallback(this);
	mPaint = new Paint();
	mPaint.setColor(Color.BLUE);
	mPaint.setAntiAlias(true);
	setFocusable(true);
}
生命周期
@Override
public void surfaceCreated(SurfaceHolder holder) {
	mFlag = true;

	/* Setup thread to handle events and plain canvas */
	mThread = new Thread(this);
	mThread.start();
}
然后看一下 绘制画布的线程,每50ms(粗糙值)进行绘图,其中绘图的圆心坐标,半径大小取决于给定的参数
/**
 * Thread
 */
@Override
public void run() {
	while (mFlag) {
		myDraw();
		try {
			Thread.sleep(50);
		} 
		catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
/**
 * Draw the rocker in the thread
 */
public void myDraw() {
	try {
		mCanvas = mHolder.lockCanvas();
		if (mCanvas != null) {
			mPaint.setAlpha(0x77);
			mCanvas.drawColor(Color.WHITE);
			
			/* draw base */
			mCanvas.drawCircle(mBaseCentX, mBaseCentY, mBaseRadius, mPaint);
			/* draw rocker */
			mCanvas.drawCircle(mRockCentX, mRockCentY, mRockRadius, mPaint);
		}
	} 
	catch (Exception e) {
		// TODO: handle exception
	} 
	finally {
		if (mCanvas != null) {
			mHolder.unlockCanvasAndPost(mCanvas);
		}
	}
}
摇杆位置则由 touch event来计算得出
@Override
public boolean onTouchEvent(MotionEvent event) {
	/* Reset rocker when touch up */
	if (event.getAction() == MotionEvent.ACTION_UP) {
		mRockCentX = mBaseCentX;
		mRockCentY = mBaseCentY;
		mLogicType = LOGIC_STOP;
	} 
<span style="white-space:pre">	</span>else {
		float pointDx = event.getX() - mBaseCentX;
		float pointDy = event.getY() - mBaseCentY;
		double pointR = Math.sqrt(pointDx * pointDx + pointDy * pointDy);
		if ( pointR <= mBaseRadius ) {
			mRockCentX = (int) event.getX();
			mRockCentY = (int) event.getY();
		}
		else {
			mRockCentX = mBaseCentX + (int) (mBaseRadius * pointDx / pointR);
			mRockCentY = mBaseCentY + (int) (mBaseRadius * pointDy / pointR);
		}
		
		mRad = Math.acos(pointDx / pointR);
		if ( event.getY() > mRockCentY ) {
			mRad = -mRad;
		}
		else {
			mAngle = Math.toDegrees(mRad);
			mLogicType = setLogicType(mAngle);
			Log.i(TAG, "Degrees: " + mAngle + "", Set Logic Type: " + mLogicType);
		}
	}
	mSendBroadcast(0x04, mLogicType);
	return true;
}
上述也就是核心的计算过程: 需求2实现,不触摸摇杆则将摇杆圆心恢复到底座圆心位置去,mBaseCentX/Y是底座固定的值;
需求3实现,摇杆圆心位置 不超出底座范围,根据touch获取到用户触摸坐标, 若触摸的位置超出底座范围,则将坐标 按照同样角度投影到 底座边缘上, 若触摸处于底座范围内,则将坐标 直接给摇杆圆心位置。 需求4实现,根据触摸坐标 与圆心坐标 形成的角度进行判断 划分不同的动作,具体看一下setLogicType()
/**
 * Calculate logic action type 
 * @param angle - rocker angle -180~180
 * @return type 0~5 on success, -1 on failure
 */
int setLogicType(double angle) {
	int type = -1;
	if (angle > 0 && angle <= 60) {
		type = LOGIC_URIGHT;
	}
	else if (angle > 60 && angle <= 120) {
		type = LOGIC_UP;
	}
	else if (angle > 120 && angle <= 180) {
		type = LOGIC_ULEFT;
	}
	else if (angle > -180 && angle <= -120) {
		type = LOGIC_DLEFT;
	}
	else if (angle > -120 && angle <= -60) {
		type = LOGIC_DOWN;
	}
	else if (angle > -60 && angle <= 0) {
		type = LOGIC_DRIGHT;
	}
	return type;
}
最后回顾一下,绘图部分OK了,逻辑部分处理也OK了,就差去响应了 发广播给Service,告诉小车去执行动作,注意下,这MySurfaceView类是在Activity中实例化的。
/**
 * Send message to service
 * @param cmd
 * @param value
 */
public void mSendBroadcast(int cmd, int value) {
	if (mLogicStatus != value) {
		Intent intent = new Intent();
		intent.setAction("android.intent.action.cmdservice");
		intent.putExtra("cmd", cmd);
		intent.putExtra("value", value); 
		mContext.sendBroadcast(intent);
		Log.d(TAG, "sendBroadcast: " + cmd + " " + value);
		mLogicStatus = value;
	}
}
Service实现的步骤请看第一期的文章《Android创建Service后台常驻服务并使用Broadcast通信》

文章三篇下来已经完成了智能小车的第一期计划:手机蓝牙遥控小车。 这三期文章主要的侧重点还是Android这块的编程,单片机上的编程涉及的确实不多,我的想法是把UI这一块给拿起来了,到时需要什么界面就自己动手做了。 后续的计划,将智能小车三轮底盘升级成双轮平衡小车,中间需要一个PID算法,哈当然是工作有闲下来后展开。

工程下载地址:http://download.csdn.net/detail/stayneckwind2/8708607
参考文章:

[1] 老罗blog的详解, http://blog.csdn.net/luoshengyang/article/details/8661317

[2] Android游戏开发 http://blog.csdn.net/xiaominghimi/article/details/6423983




声明:该文观点仅代表作者本人,牛骨文系教育信息发布平台,牛骨文仅提供信息存储空间服务。