2016.06.28更新
现在我将android端作为client,pc端作为server端。目的就是为了让android端能够更加灵活地链接pc端。主要修改如下:
1. 修改整体架构,将pc端作为server端,android端作为client端。 pc端一直在监听来自android端的消息,如果android端有链接请求则建立请求; 如果android端断开链接则pc端继续监听,直到有链接消息。
2. 增加服务自动发现机制,pc端只需要运行程序即可,android端也只需要 打开app,然后app会通过UDP的224.0.0.1地址进行组播查找服务,如果找到 服务,则向服务器发起链接。
3. 修改音量键对应的键值,音量下键对应方向键下键,音量上键对应方向键上键。
详细的代码可以查看我的开源项目主页:https://github.com/CreateChance/WirelessHid
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
偶然间突发奇想,想到能不能让我们的在我们的手机设备上滑动触摸屏进而控制pc上的鼠标移动,也就说把我们的android设备当成是pc设备的触摸板呢?要想实现这个目标,首先要想一想android设备和pc设备之间的通讯基础是什么?这个通讯技术必须是android和pc同时支持的,目前看来也就是wifi,蓝牙。首先说一下蓝牙,蓝牙是一个提供个人局域网的安全无线电通讯技术,相对于wifi而言,蓝牙的功耗相对较低,尤其是BLE技术使得蓝牙的功耗可以和zigbee媲美了,并且android也支持了基于蓝牙的socket操作。但是pc上的java部分对于蓝牙的socket支持就不是很好了,实现起来比较麻烦。但是wifi虽然功耗相对蓝牙而言比较高了点,但是实现起来非常容易,就是socket就好了!所以在第一版本中,可以先使用wifi作为传输技术。
解决了传输技术之后,还需要解决的是都有哪些数据类型,怎么传递数据,使用什么样的协议的问题。这些问题很关键,这涉及到以后的程序可扩展性问题,如果这部分欠缺考虑的话,那么后期的修改和扩展将是一个灾难。进过仔细考量之后,决定采用google的protobuf来封装所有的数据,因为protobuf灵活,小巧,高效,正好就是我要的。
进过了几天的业余时间开发,终于出来了一个可以运行展示的初级版本,这个版本可以满足基本的需求。目前我已经将这个代码开源出来了,项目地址是github:https://github.com/CreateChance/WirelessHid
我要做的就是使用手机实现一个touchpad和keyboard,这就决定了UI的设计必须符合我们日常见到的实体touchpad和keyboard的样式。进过设计之后,touchpad部分设计为一个fragment,它的布局如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" >
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal" >
<LinearLayout android:id="@+id/speed_control" android:layout_width="65dip" android:layout_height="match_parent" android:orientation="vertical" >
<ToggleButton android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:textOn="@string/speed_slow" android:textOff="@string/speed_slow" android:tag="1" />
<ToggleButton android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:textOn="@string/speed_medium" android:textOff="@string/speed_medium" android:tag="3" />
<ToggleButton android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:textOn="@string/speed_fast" android:textOff="@string/speed_fast" android:tag="5" />
</LinearLayout>
<View android:id="@+id/touchpad" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" />
<View android:id="@+id/scrollzone" android:layout_width="65dip" android:layout_height="match_parent" android:background="#2b2b2b" />
<LinearLayout android:id="@+id/scroll_speed_control" android:layout_width="65dip" android:layout_height="match_parent" android:orientation="vertical" >
<ToggleButton android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:textOn="@string/speed_slow" android:textOff="@string/speed_slow" android:tag="1" />
<ToggleButton android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:textOn="@string/speed_medium" android:textOff="@string/speed_medium" android:tag="3" />
<ToggleButton android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:textOn="@string/speed_fast" android:textOff="@string/speed_fast" android:tag="5" />
</LinearLayout>
</LinearLayout>
<LinearLayout android:id="@+id/buttons" android:layout_width="match_parent" android:layout_height="65dip" android:orientation="horizontal" >
<Button android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:tag="0" android:text="left" android:id="@+id/left_button" />
<Button android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:tag="1" />
<Button android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:tag="2" android:id="@+id/right_button" android:text="right" />
</LinearLayout>
</LinearLayout>
运行时的效果如下:
键盘的布局就比较复杂了,这部分也是一个fragment,整体布局如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" >
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" >
<TextView android:id="@+id/led_numlock" android:text="@string/led_numlock" android:background="@color/led_off" style="@style/keyboard_led" />
<TextView android:id="@+id/led_capslock" android:text="@string/led_capslock" android:background="@color/led_off" style="@style/keyboard_led" />
<TextView android:id="@+id/led_scrolllock" android:text="@string/led_scrolllock" android:background="@color/led_off" style="@style/keyboard_led" />
</LinearLayout>
<LinearLayout android:id="@+id/keyboard" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" >
</LinearLayout>
</LinearLayout>
这个布局中首先放上3个textview在一个LinearLayout这三个textview充当真实键盘上的3个led灯:num lock, caps lock, scroll lock。然后就是一个存放实际keyboard布局的LinearLayout容器,这么做的目的是这样的:因为手机的屏幕很小,想要放下一个标准键盘上的所有的按键肯定是不行的,因此需要将键盘分区,然后分别展示,这里的这个容器就是用来存放不同分区,不同布局的键盘部分。目前我把键盘分成了2个部分:主键盘部分,导航部分加上数字部分。其中主键盘部分是我们最常使用的部分,这部分包含了26个字母,0~9数字(字母上排),12个功能键等,导航部分就是上下左右键盘,上页下页部分等,数字部分就是数字小键盘和一些控制按键,我把导航键和数字键合并在一起了,这两部分的布局如下:
主键盘部分:
<Keyboard>
<Layout>
<Key keyLabel="Esc" keyCode="27"/>
<Key keyLabel="F1" keyCode="112"/>
<Key keyLabel="F2" keyCode="113"/>
<Key keyLabel="F3" keyCode="114"/>
<Key keyLabel="F4" keyCode="115"/>
<Key keyLabel="F5" keyCode="116"/>
<Key keyLabel="F6" keyCode="117"/>
<Key keyLabel="F7" keyCode="118"/>
<Key keyLabel="F8" keyCode="119"/>
<Key keyLabel="F9" keyCode="120"/>
<Key keyLabel="F10" keyCode="121"/>
<Key keyLabel="F11" keyCode="122"/>
<Key keyLabel="F12" keyCode="123"/>
<Key keyLabel="Del" keyCode="127"/>
</Layout>
<Layout>
<Key keyLabel="'" shiftLabel="~" keyCode="192"/>
<Key keyLabel="1" shiftLabel="!" keyCode="49"/>
<Key keyLabel="2" shiftLabel="\@" keyCode="50"/>
<Key keyLabel="3" shiftLabel="\#" keyCode="51"/>
<Key keyLabel="4" shiftLabel="$" keyCode="52"/>
<Key keyLabel="5" shiftLabel="%" keyCode="53"/>
<Key keyLabel="6" shiftLabel="^" keyCode="54"/>
<Key keyLabel="7" shiftLabel="&" keyCode="55"/>
<Key keyLabel="8" shiftLabel="*" keyCode="56"/>
<Key keyLabel="9" shiftLabel="(" keyCode="57"/>
<Key keyLabel="0" shiftLabel=")" keyCode="48"/>
<Key keyLabel="-" shiftLabel="_" keyCode="45"/>
<Key keyLabel="=" shiftLabel="+" keyCode="61"/>
<Key keyLabel="Backspace ←" keyCode="8" weight="1.5"/>
</Layout>
<Layout>
<Key keyLabel="Tab ↹" keyCode="9" weight="1.5"/>
<Key keyLabel="Q" keyCode="81"/>
<Key keyLabel="W" keyCode="87"/>
<Key keyLabel="E" keyCode="69"/>
<Key keyLabel="R" keyCode="82"/>
<Key keyLabel="T" keyCode="84"/>
<Key keyLabel="Y" keyCode="89"/>
<Key keyLabel="U" keyCode="85"/>
<Key keyLabel="I" keyCode="73"/>
<Key keyLabel="O" keyCode="79"/>
<Key keyLabel="P" keyCode="80"/>
<Key keyLabel="[" keyCode="91" shiftLabel="{"/>
<Key keyLabel="]" keyCode="93" shiftLabel="}"/>
<Key keyLabel="\\" keyCode="92" shiftLabel="|"/>
</Layout>
<Layout>
<Key keyLabel="Caps Lock" keyCode="20" weight="1.5"/>
<Key keyLabel="A" keyCode="65"/>
<Key keyLabel="S" keyCode="83"/>
<Key keyLabel="D" keyCode="68"/>
<Key keyLabel="F" keyCode="70"/>
<Key keyLabel="G" keyCode="71"/>
<Key keyLabel="H" keyCode="72"/>
<Key keyLabel="J" keyCode="74"/>
<Key keyLabel="K" keyCode="75"/>
<Key keyLabel="L" keyCode="76"/>
<Key keyLabel=";" keyCode="59" shiftLabel=":"/>
<Key keyLabel="'" keyCode="44" shiftLabel="""/>
<Key keyLabel="Enter ↵" keyCode="10" weight="3.0"/>
</Layout>
<Layout>
<Key keyLabel="Shift ⇧" keyCode="16" keyFunc="Shift" weight="1.5"/>
<Key keyLabel="Z" keyCode="90"/>
<Key keyLabel="X" keyCode="88"/>
<Key keyLabel="C" keyCode="67"/>
<Key keyLabel="V" keyCode="86"/>
<Key keyLabel="B" keyCode="66"/>
<Key keyLabel="N" keyCode="78"/>
<Key keyLabel="M" keyCode="77"/>
<Key keyLabel="," keyCode="44" shiftLabel="<"/>
<Key keyLabel="." keyCode="46" shiftLabel=">"/>
<Key keyLabel="/" keyCode="47" shiftLabel="\?"/>
<Key keyLabel="Shift ⇧" keyCode="16" keyFunc="Shift" weight="1.5"/>
</Layout>
<Layout>
<Key keyLabel="Ctrl" keyCode="17" weight="1.5"/>
<Key keyLabel="Win" keyCode="524"/>
<Key keyLabel="Alt" keyCode="18"/>
<Key keyLabel=" " keyCode="32" weight="10.0"/>
<Key keyLabel="Alt" keyCode="18"/>
<Key keyLabel="Win" keyCode="524"/>
<Key keyLabel="Menu" keyCode="93"/>
<Key keyLabel="Ctrl" keyCode="17" weight="1.5"/>
</Layout>
</Keyboard>
导航键部分:
<Keyboard>
<Layout>
<Key keyLabel="Print\nScreen" keyCode="154"/>
<Key keyLabel="Scroll\nLock" keyCode="145"/>
<Key keyLabel="Pause\nBreak" keyCode="19"/>
</Layout>
<Layout>
<Key keyLabel="Insert" keyCode="155"/>
<Key keyLabel="Home" keyCode="36"/>
<Key keyLabel="Page Up" keyCode="33"/>
</Layout>
<Layout>
<Key keyLabel="Delete" keyCode="127"/>
<Key keyLabel="End" keyCode="35"/>
<Key keyLabel="Page Down" keyCode="34"/>
</Layout>
<Layout>
<Key visible="false"/>
<Key keyLabel="↑" keyCode="38"/>
<Key visible="false"/>
</Layout>
<Layout>
<Key keyLabel="←" keyCode="37"/>
<Key keyLabel="↓" keyCode="40"/>
<Key keyLabel="→" keyCode="39"/>
</Layout>
</Keyboard>
数字键部分:
<Keyboard>
<Layout>
<Key keyLabel="Num\nLock" keyCode="144"/>
<Key keyLabel="/" keyCode="111"/>
<Key keyLabel="*" keyCode="106"/>
<Key keyLabel="-" keyCode="45"/>
</Layout>
<Layout>
<Key keyLabel="7\nHome" keyCode="0103"/>
<Key keyLabel="8 ↑" keyCode="104"/>
<Key keyLabel="9\nPgUp" keyCode="105"/>
<Key keyLabel="+" keyCode="521"/>
</Layout>
<Layout>
<Key keyLabel="4\n←" keyCode="100"/>
<Key keyLabel="5" keyCode="101"/>
<Key keyLabel="6\n→" keyCode="102"/>
<Key visible="false"/>
</Layout>
<Layout>
<Key keyLabel="1\nEnd" keyCode="97"/>
<Key keyLabel="2 ↓" keyCode="98"/>
<Key keyLabel="3\nPgDn" keyCode="99"/>
<Key visible="false"/>
</Layout>
<Layout>
<Key keyLabel="0\nIns" keyCode="96" weight="2.0"/>
<Key keyLabel=".\nDel" keyCode="110"/>
<Key keyLabel="Enter" keyCode="10"/>
</Layout>
</Keyboard>
这里的布局需要说明一下,这里我使用了layout标签表明,然后使用XmlResourceParser类来解析这个里面的内容,最后再添加到布局中去。下面贴出两张键盘的运行效果图:
主键盘:
从键盘(导航键和数字键):
Server整体代码就是一个app,内容不是很复杂,这里我只陈述我的代码功能和必要的代码片段,详细代码内容有限于篇幅就不贴出来了,可以查看我的github项目主页(https://github.com/CreateChance/WirelessHid)上的开源代码。
代码的基本分布如下:
各个类的作用如下:
这是主界面类,基本就是MouseFragment的容器,另外就是监听用户点击回退事件,如果用户在1.5s之内连续点击两次回退就退出app,基本逻辑比较简单。
这是整个app的服务,这个服务是实际将数据发送出去的地方,主要就是通过looper和handler的方式将消息队列中的数据发送出去。发送部分的逻辑:
一个looper线程
private class DataSendThread extends Thread {
private OutputStream os = null;
@Override
public void run() {
super.run();
Looper.prepare();
try {
Log.d(TAG, "I'm waiting for connecting.");
mServerSocket = new ServerSocket(Constant.HID_TCP_PORT);
mServerSocket.setReuseAddress(true);
mSocket = mServerSocket.accept();
os = mSocket.getOutputStream();
Toast.makeText(getApplicationContext(), "Client connected!",
Toast.LENGTH_SHORT).show();
Log.d(TAG, "client connected!");
} catch (IOException e) {
e.printStackTrace();
return;
}
mDataSendHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// send data here.
try {
((WirelessHidProto.HidData)msg.obj).writeDelimitedTo(os);
} catch (IOException e) {
Log.d(TAG, "IOException, close all resource.");
mDataSendHandler = null;
if (mListener != null) {
mListener.onHandlerChanged(mDataSendHandler);
}
this.getLooper().quit();
sendBroadcast(new Intent(ACTION_RESET_CONNECTION));
} finally {
try {
mServerSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
if (mListener != null) {
mListener.onHandlerChanged(mDataSendHandler);
}
Looper.loop();
}
}
界面Fragment使用Handler和server交互,fragment需要实现server的DataHandlerListener接口,当Handler变化的时候通知Fragment,以便Fragment拿到最新的对象引用:
需要实现的接口
public interface DataHandlerListener {
void onHandlerChanged(Handler handler);
}
设置listener的接口:
public void setListener(DataHandlerListener listener) {
this.mListener = listener;
}
fragment也可以主动获取:
public Handler getDataSendHandler() {
return this.mDataSendHandler;
}
这里的发送使用的就是protobuf的序列化接口,关于这个接口的描述这里就不详述,可以参考google protobuf的java部分的编程指导:
https://developers.google.com/protocol-buffers/docs/javatutorial
这个类是fragment类,主要是嵌套在MainActivity类中,主要逻辑功能如下:
1. 捕获用户触摸屏移动,点击事件
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//single and double click handle here.
mPrevX = (int) event.getX();
mPrevY = (int) event.getY();
time = new Date().getTime();
break;
case MotionEvent.ACTION_UP:
if (new Date().getTime() - time < mDoubleClickTimeThreshold) {
if ((int) event.getX() - mPrevX < mDoubleClickPosThreshold
&& (int) event.getY() - mPrevY < mDoubleClickPosThreshold) {
mouseClickPress(Constant.MOUSE_BUTTON_LEFT);
mouseClickRelease(Constant.MOUSE_BUTTON_LEFT);
}
}
case MotionEvent.ACTION_MOVE:
//mouse move handle here.
int x = (int) (event.getX() * mSpeed);
int y = (int) (event.getY() * mSpeed);
mouseMove(x - mPrevX, y - mPrevY);
mPrevX = x;
mPrevY = y;
break;
}
return true;
}
2. 鼠标右击,左击事件(通过button模拟)
// setup buttons
ViewGroup bar = (ViewGroup) view.findViewById(R.id.buttons);
int count = bar.getChildCount();
for (int i = 0; i < count; i++) {
View child = bar.getChildAt(i);
try {
Button button = (Button) child;
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int which = Integer.valueOf((String) v.getTag());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//mouse button pressed
//para which shows which button is pressed
//0 is left button
//1 is not used(reserved).
//2 is right button
if (which == 0) {
mouseClickPress(Constant.MOUSE_BUTTON_LEFT);
} else if (which == 1) {
//Do nothing for now.
} else if (which == 2) {
mouseClickPress(Constant.MOUSE_BUTTON_RIGHT);
}
break;
case MotionEvent.ACTION_UP:
//mouse button released
if (which == 0) {
mouseClickRelease(Constant.MOUSE_BUTTON_LEFT);
} else if (which == 1) {
//Do nothing for now.
} else if (which == 2) {
mouseClickRelease(Constant.MOUSE_BUTTON_RIGHT);
}
break;
}
return false;
}
});
} catch (ClassCastException e) {
// not a button :)
}
}
3. 鼠标滚轴滚动事件
// setup scroll
mScrollZone = view.findViewById(R.id.scrollzone);
mScrollZone.setOnTouchListener(new OnTouchListener() {
private int mPrevY;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//click scroll handle here.
mPrevY = (int) (event.getY() * mScrollSpeed);
break;
case MotionEvent.ACTION_MOVE:
//mouse scroll handle here.
int amt = (int) (event.getY() * mScrollSpeed);
mouseScroll(mPrevY - amt);
mPrevY = amt;
break;
}
return true;
}
});
4. 设置鼠标移动,滚轴滚动速度。
设置移动速度
// setup speed controls
bar = (ViewGroup) view.findViewById(R.id.speed_control);
count = bar.getChildCount();
for (int i = 0; i < count; i++) {
View child = bar.getChildAt(i);
try {
ToggleButton button = (ToggleButton) child;
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
ToggleButton button = (ToggleButton) v;
// do not allow to uncheck button
if (!button.isChecked()) {
button.setChecked(true);
return;
}
updateSpeed(Integer.parseInt((String) button.getTag()));
}
});
} catch (ClassCastException e) {
// not a button :)
}
}
设置滚动速度
// setup scroll speed controls
bar = (ViewGroup) view.findViewById(R.id.scroll_speed_control);
count = bar.getChildCount();
for (int i = 0; i < count; i++) {
View child = bar.getChildAt(i);
try {
ToggleButton button = (ToggleButton) child;
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
ToggleButton button = (ToggleButton) v;
// do not allow to uncheck button
if (!button.isChecked()) {
button.setChecked(true);
return;
}
updateScrollSpeed(Integer.parseInt((String) button.getTag()));
}
});
} catch (ClassCastException e) {
// not a button :)
}
}
更新移动速度
private void updateSpeed(int newSpeed) {
// note: we assume at least button have proper speed-tag so this will
// check what it should
mSpeed = newSpeed;
ViewGroup bar = (ViewGroup) getView().findViewById(R.id.speed_control);
int count = bar.getChildCount();
for (int i = 0; i < count; i++) {
View child = bar.getChildAt(i);
try {
ToggleButton button = (ToggleButton) child;
int speed = Integer.parseInt((String) button.getTag());
button.setChecked(speed == newSpeed);
} catch (ClassCastException e) {
// not a button :)
}
}
}
更行滚动速度:
private void updateScrollSpeed(int newSpeed) {
// note: we assume at least button have proper speed-tag so this will
// check what it should
mScrollSpeed = newSpeed;
ViewGroup bar = (ViewGroup) getView().findViewById(R.id.scroll_speed_control);
int count = bar.getChildCount();
for (int i = 0; i < count; i++) {
View child = bar.getChildAt(i);
try {
ToggleButton button = (ToggleButton) child;
int speed = Integer.parseInt((String) button.getTag());
button.setChecked(speed == newSpeed);
} catch (ClassCastException e) {
// not a button :)
}
}
}
这是键盘的fragment,这其实是一个FragmentActivity,具体的键盘通过ViewGroup添加view接口addView添加相应的view:
keyboard = (ViewGroup) this.findViewById(R.id.keyboard);
keyboard.addView(view);
这里针对两类键盘设计了两个创建接口:
主键盘:
private View creatQwertyKeyboard(Context context) {
return createKeyboard(context, R.xml.qwerty_keyboard);
}
从键盘:
private View createNavigationAndNumericKeyboard(Context context) {
ViewGroup view = (ViewGroup) View.inflate(context, R.layout.numeric_keyboard, null);
ViewGroup child;
child = (ViewGroup) view.findViewById(R.id.navigation_keyboard);
child.addView(createKeyboard(context, R.xml.navigation_keyboard));
child = (ViewGroup) view.findViewById(R.id.numeric_keyboard);
child.addView(createKeyboard(context, R.xml.numeric_keyboard));
return view;
}
他们都使用了createKeyboard接口创建实际的键盘:
private Keyboard createKeyboard(Context context, int xmlResourceID) {
final Keyboard keyboard = new Keyboard(context, xmlResourceID);
keyboard.setKeyboardListener(new Keyboard.KeyboardListener() {
@Override
public void onKeyUp(int keyCode) {
Log.d(TAG, "up keycode: " + keyCode);
if (mDataSendHandler != null) {
mDataSendHandler.removeCallbacks(mLongPressCheckTask);
}
if (mIsLongPressed) {
mIsLongPressed = false;
WirelessHidProto.HidData data = WirelessHidProto.HidData.newBuilder()
.setType(WirelessHidProto.HidData.DataType.KEYBOARD_LONG_RELEASE)
.setKeyboardValue(keyCode).build();
if (mDataSendHandler != null) {
mDataSendHandler.obtainMessage(0, data).sendToTarget();
}
} else {
WirelessHidProto.HidData data = WirelessHidProto.HidData.newBuilder()
.setType(WirelessHidProto.HidData.DataType.KEYBOARD_HIT)
.setKeyboardValue(keyCode).build();
if (mDataSendHandler != null) {
mDataSendHandler.obtainMessage(0, data).sendToTarget();
}
}
}
@Override
public void onKeyDown(int keyCode) {
Log.d(TAG, "key down: " + keyCode);
if (keyCode == 144) {
// 144 means number lock
mIsNumLockActive = !mIsNumLockActive;
KeyboardFragment.this.findViewById(R.id.led_numlock).
setBackgroundColor(getResources().getColor(mIsNumLockActive ? R.color.led_on : R.color.led_off));
} else if (keyCode == 20) {
// 20 means caps lock.
mIsCapsLockActive = !mIsCapsLockActive;
KeyboardFragment.this.findViewById(R.id.led_capslock).
setBackgroundColor(getResources().getColor(mIsCapsLockActive ? R.color.led_on : R.color.led_off));
} else if (keyCode == 145) {
// 145 means scroll lock
mIsScrollLockActive = !mIsScrollLockActive;
KeyboardFragment.this.findViewById(R.id.led_scrolllock).
setBackgroundColor(getResources().getColor(mIsScrollLockActive ? R.color.led_on : R.color.led_off));
} else if (mDataSendHandler != null) {
mLongPressCheckTask.setKeyCode(keyCode);
mDataSendHandler.postDelayed(mLongPressCheckTask, 1000);
}
}
});
return keyboard;
}
这里返回一个Keyboard类对象,Keyboard类就是键盘实际的类了,这个类是LinearLayout的子类,使用XmlResourceParser来解析刚才我们定义的xml文件去获得键值和创建布局。
private LinearLayout parseKeyLayout(Context context, XmlResourceParser xmlParser)
throws XmlPullParserException, IOException {
LinearLayout linearLayout = new LinearLayout(context);
linearLayout.setLayoutParams(new LayoutParams(
xmlParser.getAttributeIntValue(null, "width", LayoutParams.MATCH_PARENT),
xmlParser.getAttributeIntValue(null, "height", 0),
xmlParser.getAttributeFloatValue(null, "weight", 1.0f)));
linearLayout.setOrientation(xmlParser.getAttributeIntValue(null, "orientation",
LinearLayout.HORIZONTAL));
String tag;
do {
xmlParser.next();
tag = xmlParser.getName();
if (xmlParser.getEventType() == XmlResourceParser.START_TAG) {
if (tag.equals(XML_TAG_LAYOUT)) {
linearLayout.addView(parseKeyLayout(context, xmlParser));
} else if (tag.equals(XML_TAG_KEY)) {
Key.KeyAttributes attrs = new Key.KeyAttributes();
attrs.keyFunction = getStringAttributeValue(xmlParser, "keyFunc", "");
attrs.mainLabel = getStringAttributeValue(xmlParser, "keyLabel", "");
attrs.shiftLabel = getStringAttributeValue(xmlParser, "shiftLabel", "");
attrs.keyCode = xmlParser.getAttributeIntValue(null, "keyCode", 0);
Key key = new Key(context, attrs);
key.setLayoutParams(new LayoutParams(
xmlParser.getAttributeIntValue(null, "width", 0),
xmlParser.getAttributeIntValue(null, "height",
LayoutParams.MATCH_PARENT),
xmlParser.getAttributeFloatValue(null, "weight", 1)));
key.setVisibility(xmlParser.getAttributeBooleanValue(null, "visible", true) ?
VISIBLE : INVISIBLE);
key.setKeyListener(this);
if (attrs.shiftLabel != null & attrs.shiftLabel.length() > 0) {
mKeysWithShiftLabel.add(key);
}
linearLayout.addView(key);
}
}
} while (xmlParser.getEventType() != XmlResourceParser.END_TAG
|| !tag.equals(XML_TAG_LAYOUT));
return linearLayout;
}
这是protobuf的数据定义文件,内容如下:
syntax = "proto2";
option java_package = "com.baniel.wirelesshid";
option java_outer_classname = "WirelessHidProto";
message HidData {
enum DataType {
MOUSE_MOVE = 0;
MOUSE_CLICK_PRESS = 1;
MOUSE_CLICK_RELEASE = 2;
MOUSE_SCROLL = 3;
KEYBOARD_LONG_PRESS = 4;
KEYBOARD_LONG_RELEASE = 5;
KEYBOARD_HIT = 6;
}
required DataType type = 1;
optional int32 x_shift = 2;
optional int32 y_shift = 3;
optional int32 mouse_key_value = 4;
optional int32 mouse_scroll = 5;
optional int32 keyboard_value = 6;
}
这里我只定义了一个消息类型,那就是HidData这是android需要发送给pc的消息数据。这其中有消息的类型:鼠标移动,鼠标按下,鼠标释放,鼠标滚轴,键盘长按,键盘长按释放,键盘单击。x轴偏移,y轴偏移(pc系统的鼠标移动是以坐标偏移作为参数的;鼠标按键键值,鼠标滚动值,键盘按键值。关于protobuf详细的数据定义语法请见:
https://developers.google.com/protocol-buffers/docs/proto
WirelessHidProto类就是上面这个文件通过protobuf编译器编译生成的。
client端的代码就比较简单了,这里我只有两个类:
具体类的说明如下:
这个是client的主类,主要就是从socket读取来自android的数据,然后通过java Robot类移动鼠标,操作键盘输入操作:
主方法:
public static void main(String[] args) {
final int HID_TCP_PORT = 34567;
Socket mSocket = null;
InputStream is = null;
HidData data = null;
try {
mSocket = new Socket(args[0], HID_TCP_PORT);
is = mSocket.getInputStream();
mRobot = new Robot();
printClientInfo(mSocket);
while (true) {
data = HidData.parseDelimitedFrom(is);
if (data != null) {
handleData(data);
} else {
break;
}
}
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.exit(-1);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.exit(-1);
} catch (AWTException e) {
// TODO: handle exception
System.exit(-1);
}
System.out.println("Connection lost.");
}
具体操作:
//move mouse pointer to posX,posY of current position.
private static void doMouseMove(int posX, int posY) {
mRobot.mouseMove(mPrevX + posX, mPrevY + posY);
mPrevX += posX;
mPrevY += posY;
}
//handle mouse button press.
private static void doMousePress(int keyValue) {
//System.out.println("mouse click value = " + keyValue);
mRobot.mousePress(keyValue);
}
private static void doMouseRelease(int keyValue) {
mRobot.mouseRelease(keyValue);
}
private static void doMouseScroll(int amt) {
mRobot.mouseWheel(amt);
}
private static void doKeyHit(int keyCode) {
mRobot.keyPress(keyCode);
mRobot.keyRelease(keyCode);
}
private static void doKeyLongPress(int keyCode) {
mRobot.keyPress(keyCode);
}
private static void doKeyLongRelease(int keyCode) {
mRobot.keyRelease(keyCode);
}
这个类是protobuf编译器生成的,主要包含数据类的序列化操作逻辑。
好了,到这里就完全分析完了我的实现,感兴趣的朋友可以从我的github下载编译好的二进制文件,直接运行感受一下(提醒一下,客户端最好在linux上运行,windows上有点卡顿,影响体验,具体的原因以后我会找出并且解决这个问题!或者哪位大神知道可以告诉我哦~~)。下载地址:
https://github.com/CreateChance/WirelessHid/tree/master/bin
运行方式:
server端(android):
直接安装app,然后启动即可(前提是你要链接到一个局域的wifi,并且你的pc电脑能够和android设备通讯)
client端(linux或者windows):
命令运行:java -jar WirelessHidClient.jar 你的android设备地址