这个程序要用到摇杆了,可想而知,我们需要多级的控制才行,不然的话拨来拨去只有四个动作就没意思了。这个程序与前面程序最大的区别是加入了一个守护进程来实时的监控摇杆的位置并传送控制指令,同时加入了检测蓝牙连接是否断开并自动重连的功能。
这里我可要被2.1的sdk气死了,2.1的蓝牙库没有提供检测连接断开的功能,socket一直是连着的,断了它也不会给什么反馈,我就直接用单片机给个反馈,结果这个程序在2.3的机器上跑的相当好,唯独我这个倒霉的2.1各种出错,模拟机没法用蓝牙调试就只能用真机,一遍一遍的打包apk连接上传运行然后在真机上多的想吐的DEBUG信息中在溢出前找到自己的DEBUG然后分析在重新编译…………到后来连哭的力气都没有,于是一狠心就给他屏蔽掉了,反正是用在稳定电源的车模上的,而且手机也不会远离车,一般不会断开…………下面的代码是有重连功能的,如果你有和我一样的问题就找到“if(!CarToContral.isBonded())”改成if(false)吧。
运行程序,可以用方向键控制,在打开守护进程后可以用摇杆来控制。
检测连接是用类似电脑上ping的方法实现的,手机发送一个“A”再由单片机接到后再传回去,如果手机接到了这个反馈则说明连接正常,若接不到则说明连接断开。为了防止在这个过程中有其他反馈命令混杂其中,检测的时候迭代整个缓冲区来寻找反馈信息。同时要注意这里有个sleep是必须的,我们用的串口的速度是9600bit/s传送一个字节是需要时间的,更何况要传送两回并且单片机还有反应一下……
关于那个“守护进程”,由于我的访问是时序的,所以没有考虑死锁之类的问题,如果改的话遇到随机访问还是加上锁安全一些。
/**
* 创建用于定时发送位置状态的守护进程
*/
public void ThreadCreate()
{
if(Guarder != null) //确保只有一个守护进程可以被创建
return;
Guarder = new Thread(this);
ThreadFlag = true;
Guarder.start();
}
这段代码是为了防止生成多个线程用的,线程只能有这个View类来创建销毁并且只记录一个,这样就不会出现因为前一个线程未消毁导致两个线程抢资源的问题了。
好了,废话少说,上代码了
AndroidBluetoothCarActivity.java
package android.lynx.BluetoothCar;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.Toast;
import android.widget.ToggleButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
public class AndroidBluetoothCarActivity extends Activity implements CarPositionSetable{
private static final String TAG = "BluetoothTest";
private static final boolean D = true;
private BluetoothAdapter mBluetoothAdapter = null;
private BluetoothDevice device = null;
private BluetoothSocket btSocket = null;
private OutputStream outStream = null;
Button mButtonF;
Button mButtonB;
Button mButtonL;
Button mButtonR;
Button mButtonS;
ToggleButton JoyStickSwitcher;
VirtualJoyStickView JoyStick;
private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); //这条是蓝牙串口通用的UUID,不要更改
private static String address = "00:19:5D:EE:2E:A5"; // <==要连接的蓝牙设备MAC地址
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//前进
mButtonF=(Button)findViewById(R.id.btnF);
mButtonF.setOnTouchListener(new Button.OnTouchListener(){
@Override
public boolean onTouch(View v, MotionEvent event) {
String message;
byte[] msgBuffer;
int action = event.getAction();
switch(action)
{
case MotionEvent.ACTION_DOWN:
try {
outStream = btSocket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
message = "CMF";
msgBuffer = message.getBytes();
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
break;
case MotionEvent.ACTION_UP:
try {
outStream = btSocket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
message = "CS";
msgBuffer = message.getBytes();
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
break;
}
return false;
}
});
//后退
mButtonB=(Button)findViewById(R.id.btnB);
mButtonB.setOnTouchListener(new Button.OnTouchListener(){
@Override
public boolean onTouch(View v, MotionEvent event) {
String message;
byte[] msgBuffer;
int action = event.getAction();
switch(action)
{
case MotionEvent.ACTION_DOWN:
try {
outStream = btSocket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
message = "CMB";
msgBuffer = message.getBytes();
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
break;
case MotionEvent.ACTION_UP:
try {
outStream = btSocket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
message = "CS";
msgBuffer = message.getBytes();
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
break;
}
return false;
}
});
//左转
mButtonL=(Button)findViewById(R.id.btnL);
mButtonL.setOnTouchListener(new Button.OnTouchListener(){
@Override
public boolean onTouch(View v, MotionEvent event) {
String message;
byte[] msgBuffer;
int action = event.getAction();
switch(action)
{
case MotionEvent.ACTION_DOWN:
try {
outStream = btSocket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
message = "CTL";
msgBuffer = message.getBytes();
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
break;
case MotionEvent.ACTION_UP:
try {
outStream = btSocket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
message = "CTL";
msgBuffer = message.getBytes();
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
break;
}
return false;
}
});
//右转
mButtonR=(Button)findViewById(R.id.btnR);
mButtonR.setOnTouchListener(new Button.OnTouchListener(){
@Override
public boolean onTouch(View v, MotionEvent event) {
String message;
byte[] msgBuffer;
int action = event.getAction();
switch(action)
{
case MotionEvent.ACTION_DOWN:
try {
outStream = btSocket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
message = "CTR";
msgBuffer = message.getBytes();
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
break;
case MotionEvent.ACTION_UP:
try {
outStream = btSocket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
message = "CTR";
msgBuffer = message.getBytes();
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
break;
}
return false;
}
});
//停止
mButtonS=(Button)findViewById(R.id.btnS);
mButtonS.setOnTouchListener(new Button.OnTouchListener(){
@Override
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction()==MotionEvent.ACTION_DOWN)
try {
outStream = btSocket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
String message = "CR";
byte[] msgBuffer = message.getBytes();
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
return false;
}
});
JoyStick = (VirtualJoyStickView)findViewById(R.id.JoyStick);
JoyStick.setPositionSetable(this);
JoyStickSwitcher=(ToggleButton)findViewById(R.id.JoyStickSwitcher);
JoyStickSwitcher.setOnCheckedChangeListener(new OnCheckedChangeListener(){
@Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
if(isChecked){
mButtonF.setEnabled(false);
mButtonB.setEnabled(false);
mButtonS.setEnabled(false);
mButtonL.setEnabled(false);
mButtonR.setEnabled(false);
JoyStick.ThreadCreate();
}else{
mButtonF.setEnabled(true);
mButtonB.setEnabled(true);
mButtonS.setEnabled(true);
mButtonL.setEnabled(true);
mButtonR.setEnabled(true);
JoyStick.ThreadDestory();
}
}
});
if(D)
Log.e(TAG, "+++ ON CREATE +++");
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if(mBluetoothAdapter == null)
{
Toast.makeText(this, "Bluetooth is not available.", Toast.LENGTH_LONG).show();
finish();
return;
}
if(!mBluetoothAdapter.isEnabled())
{
Toast.makeText(this, "Please enable your Bluetooth and re-run this program.", Toast.LENGTH_LONG).show();
finish();
return;
}
if(D)
Log.e(TAG, "+++ DONE IN ON CREATE, GOT LOCAL BT ADAPTER +++");
}
@Override
public void onStart()
{
super.onStart();
if(D)
Log.e(TAG, "++ ON START ++");
}
@Override
public void onResume()
{
super.onResume();
if (D)
{
Log.e(TAG, "+ ON RESUME +");
Log.e(TAG, "+ ABOUT TO ATTEMPT CLIENT CONNECT +");
}
device = mBluetoothAdapter.getRemoteDevice(address);
try {
btSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Socket creation failed.", e);
}
mBluetoothAdapter.cancelDiscovery();
try {
btSocket.connect();
Log.e(TAG, "ON RESUME: BT connection established, data transfer link open.");
} catch (IOException e) {
try {
btSocket.close();
} catch (IOException e2) {
Log .e(TAG,"ON RESUME: Unable to close socket during connection failure", e2);
}
}
// Create a data stream so we can talk to server.
if (D)
Log.e(TAG, "+ ABOUT TO SAY SOMETHING TO SERVER +");
/* try {
outStream = btSocket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
String message = "1";
byte[] msgBuffer = message.getBytes();
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
*/
}
@Override
public void onPause()
{
super.onPause();
if (D)
Log.e(TAG, "- ON PAUSE -");
if (outStream != null)
{
try {
outStream.flush();
} catch (IOException e) {
Log.e(TAG, "ON PAUSE: Couldn't flush output stream.", e);
}
}
try {
btSocket.close();
} catch (IOException e2) {
Log.e(TAG, "ON PAUSE: Unable to close socket.", e2);
}
}
@Override
public void onStop()
{
super.onStop();
if (D)Log.e(TAG, "-- ON STOP --");
}
@Override
public void onDestroy()
{
super.onDestroy();
if (D) Log.e(TAG, "--- ON DESTROY ---");
}
@Override
public void setPositionMove(double movePosition) {
String message;
byte[] msgBuffer;
try {
outStream = btSocket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
if(movePosition > 1 || movePosition < -1)
message = "CM4";
else if(movePosition > 0.9)
message = "CM7";
else if(movePosition > 0.7)
message = "CM6";
else if(movePosition > 0.3)
message = "CM5";
else if(movePosition < -0.9)
message = "CM1";
else if(movePosition < -0.7)
message = "CM2";
else if(movePosition < -0.3)
message = "CM3";
else
message = "CM4";
msgBuffer = message.getBytes();
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
}
@Override
public void setPositionTurn(double turnPosition) {
String message;
byte[] msgBuffer;
try {
outStream = btSocket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
if(turnPosition > 1 || turnPosition < -1)
message = "CT4";
else if(turnPosition > 0.9)
message = "CT7";
else if(turnPosition > 0.7)
message = "CT6";
else if(turnPosition > 0.3)
message = "CT5";
else if(turnPosition < -0.9)
message = "CT1";
else if(turnPosition < -0.7)
message = "CT2";
else if(turnPosition < -0.3)
message = "CT3";
else
message = "CT4";
msgBuffer = message.getBytes();
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
}
@Override
public void Connect() {
if (D)
{
Log.e(TAG, "+ ABOUT TO ATTEMPT CLIENT CONNECT +");
}
device = mBluetoothAdapter.getRemoteDevice(address);
try {
btSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
Log.e(TAG, "ON CONNECT: Socket creation failed.", e);
}
mBluetoothAdapter.cancelDiscovery();
try {
btSocket.connect();
Log.e(TAG, "ON RESUME: BT connection established, data transfer link open.");
} catch (IOException e) {
try {
btSocket.close();
} catch (IOException e2) {
Log .e(TAG,"ON RESUME: Unable to close socket during connection failure", e2);
}
}
}
@Override
public boolean isBonded() {
InputStream inStream = null;
try {
outStream = btSocket.getOutputStream();
inStream = btSocket.getInputStream();
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Output stream creation failed.", e);
}
String message = "A";
byte[] msgBuffer = message.getBytes();
byte[] msgBuffer2 =new byte[20];
try {
outStream.write(msgBuffer);
} catch (IOException e) {
Log.e(TAG, "ON RESUME: Exception during write.", e);
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
inStream.read(msgBuffer2);
} catch (IOException e) {
e.printStackTrace();
}
for(byte ThisByte : msgBuffer2)
{
if(ThisByte == (byte)'A')
return true;
}
return false;
/*
if(device.getBondState() == BluetoothDevice.BOND_NONE || device == null)
{
return false;
}
else
{
return true;
}*/
}
}
CarPositionSetable.java
/**
*
*/
package android.lynx.BluetoothCar;
/**
* @author lynx@ynu 2011/10/15
*
*/
public interface CarPositionSetable {
/**
* 设置车轮转向的位置,-1为左最大,1为右最大,0为正中
* @param turnPosition -1~1
*/
public void setPositionTurn(double turnPosition);
/**
* 设置速度等级,-1为后退最大,1为前进最大,0为静止
* @param movePosition -1~1
*/
public void setPositionMove(double movePosition);
/**
* Get the bond state
* @return True if bonded
*/
public boolean isBonded();
/**
* Connect to the Setter
*/
public void Connect();
}
StickPositionGetable.java
/**
*
*/
package android.lynx.BluetoothCar;
/**
* @author lynx@ynu 2011/10/15
*
*/
public interface StickPositionGetable {
/**
* Get the X position percent of the max distance.
* @return The position X from -1 to 1 in double
*/
public double getX();
/**
* Get the Y position percent of the max distance.
* @return The position Y from -1 to 1 in double
*/
public double getY();
}
VirtualJoyStickView.java
/**
*
*/
package android.lynx.BluetoothCar;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.MotionEvent;
public class VirtualJoyStickView extends View implements StickPositionGetable, Runnable{
private Paint paint;
//固定摇杆背景圆形的X,Y坐标以及半径
private int RockerCircleX = 170;
private int RockerCircleY = 110;
private int RockerCircleR = 80;
//摇杆的初始X,Y坐标及半径
private float SmallRockerInitCircleX =170;
private float SmallRockerInitCircleY = 110;
private float SmallRockerInitCircleR = 30;
//摇杆的X,Y坐标以及摇杆的半径
private float SmallRockerCircleX = SmallRockerInitCircleX;
private float SmallRockerCircleY = SmallRockerInitCircleY;
private float SmallRockerCircleR = SmallRockerInitCircleR;
//守护进程用field
private Thread Guarder = null;
private boolean ThreadFlag = false;
private CarPositionSetable CarToContral = null;
public VirtualJoyStickView(Context context) {
super(context);
init();
}
public VirtualJoyStickView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public VirtualJoyStickView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public void init()
{
this.setKeepScreenOn(true);
paint = new Paint();
paint.setAntiAlias(true);
setFocusable(true);
setFocusableInTouchMode(true);
}
/***
* 得到两点之间的弧度
*/
public double getRad(float px1, float py1, float px2, float py2) {
//得到两点X的距离
float x = px2 - px1;
//得到两点Y的距离
float y = py1 - py2;
//算出斜边长
float xie = (float) Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
//得到这个角度的余弦值(通过三角函数中的定理 :邻边/斜边=角度余弦值)
float cosAngle = x / xie;
//通过反余弦定理获取到其角度的弧度
float rad = (float) Math.acos(cosAngle);
//注意:当触屏的位置Y坐标<摇杆的Y坐标我们要取反值-0~-180
if (py2 < py1) {
rad = -rad;
}
return rad;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {
// 当触屏区域不在活动范围内
if (Math.sqrt(Math.pow((RockerCircleX - (int) event.getX()), 2) + Math.pow((RockerCircleY - (int) event.getY()), 2)) >= RockerCircleR) {
//得到摇杆与触屏点所形成的角度
double tempRad = getRad(RockerCircleX, RockerCircleY, event.getX(), event.getY());
//保证内部小圆运动的长度限制
getXY(RockerCircleX, RockerCircleY, RockerCircleR, tempRad);
} else {//如果小球中心点小于活动区域则随着用户触屏点移动即可
SmallRockerCircleX = (int) event.getX();
SmallRockerCircleY = (int) event.getY();
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
//当释放按键时摇杆要恢复摇杆的位置为初始位置
SmallRockerCircleX = SmallRockerInitCircleX;
SmallRockerCircleY = SmallRockerInitCircleY;
}
this.invalidate();
return true;
}
/**
*
* @param R
* 圆周运动的旋转点
* @param centerX
* 旋转点X
* @param centerY
* 旋转点Y
* @param rad
* 旋转的弧度
*/
public void getXY(float centerX, float centerY, float R, double rad) {
//获取圆周运动的X坐标
SmallRockerCircleX = (float) (R * Math.cos(rad)) + centerX;
//获取圆周运动的Y坐标
SmallRockerCircleY = (float) (R * Math.sin(rad)) + centerY;
}
/* (non-Javadoc)
* @see android.view.View#onDraw(android.graphics.Canvas)
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
try {
//canvas.drawColor(Color.WHITE);
//设置透明度
paint.setColor(0x70808080);
//绘制摇杆背景
canvas.drawCircle(RockerCircleX, RockerCircleY, RockerCircleR, paint);
paint.setColor(0x70ff0000);
//绘制摇杆
canvas.drawCircle(SmallRockerCircleX, SmallRockerCircleY, SmallRockerCircleR, paint);
} catch (Exception e) {
}
}
@Override
public double getX() {
double temp;
if(SmallRockerCircleX == SmallRockerInitCircleX)
return 0;
temp = SmallRockerCircleX - SmallRockerInitCircleX;
temp = temp/RockerCircleR;
return temp;
}
@Override
public double getY() {
double temp;
if(SmallRockerCircleY == SmallRockerInitCircleY)
return 0;
temp = SmallRockerCircleY - SmallRockerInitCircleY;
temp = temp/RockerCircleR*(-1);
return temp;
}
/**
* 定时发送位置状态的守护进程动作
*/
@Override
public void run() {
while (ThreadFlag) {
if(!CarToContral.isBonded())
{
CarToContral.Connect();
}
CarToContral.setPositionTurn(this.getX());
try {
Thread.sleep(50);
} catch (Exception ex) {
}
CarToContral.setPositionMove(this.getY());
try {
Thread.sleep(50);
} catch (Exception ex) {
}
}
}
/**
* 创建用于定时发送位置状态的守护进程
*/
public void ThreadCreate()
{
if(Guarder != null) //确保只有一个守护进程可以被创建
return;
Guarder = new Thread(this);
ThreadFlag = true;
Guarder.start();
}
/**
* 销毁守护进程
*/
public void ThreadDestory()
{
ThreadFlag = false;
Guarder = null;
}
/**
* 设置守护进程位置状态输出对象
* @param in
*/
public void setPositionSetable(CarPositionSetable in)
{
CarToContral = in;
}
}
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/linearLayout1" android:orientation="vertical">
<RelativeLayout android:layout_width="fill_parent" android:id="@+id/relativeLayout2" android:layout_height="wrap_content">
<Button android:layout_width="200dp" android:layout_height="wrap_content" android:id="@+id/btnF" android:text="Forward" android:layout_alignParentTop="true" android:layout_centerHorizontal="true"></Button>
</RelativeLayout>
</LinearLayout>
<LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/linearLayout2" android:orientation="vertical">
<RelativeLayout android:layout_width="fill_parent" android:id="@+id/relativeLayout1" android:layout_height="wrap_content">
<Button android:layout_width="100dp" android:layout_height="wrap_content" android:id="@+id/btnS" android:text="Middle" android:layout_alignParentTop="true" android:layout_centerHorizontal="true"></Button>
<Button android:layout_width="100dp" android:layout_height="wrap_content" android:id="@+id/btnR" android:text="Right" android:layout_alignParentTop="true" android:layout_alignParentRight="true"></Button>
<Button android:layout_width="100dp" android:layout_height="wrap_content" android:id="@+id/btnL" android:text="Left" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"></Button>
</RelativeLayout>
</LinearLayout>
<LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/linearLayout3" android:orientation="vertical">
<RelativeLayout android:layout_width="fill_parent" android:id="@+id/relativeLayout3" android:layout_height="wrap_content">
<Button android:layout_width="200dp" android:layout_height="wrap_content" android:id="@+id/btnB" android:text="Back" android:layout_alignParentTop="true" android:layout_centerHorizontal="true"></Button>
</RelativeLayout>
</LinearLayout>
<LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/linearLayout4" android:orientation="vertical">
<LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/linearLayout5" android:orientation="vertical">
<TextView android:text="Joy Stick Switcher" android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content"></TextView>
<ToggleButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/JoyStickSwitcher" android:text="ToggleButton" android:checked="false"></ToggleButton>
</LinearLayout>
<android.lynx.BluetoothCar.VirtualJoyStickView android:layout_width="wrap_content" android:id="@+id/JoyStick" android:layout_height="fill_parent"></android.lynx.BluetoothCar.VirtualJoyStickView>
</LinearLayout>
</LinearLayout>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android.lynx.BluetoothCar"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="7" />
<uses-permission android:name="android.permission.BLUETOOTH"></uses-permission>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"></uses-permission>
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".AndroidBluetoothCarActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
工程文件下载地址:http://download.csdn.net/detail/lynx2/4013038