Android基于wifi的无线HID设备实现
2016-06-14 10:56
387 查看
偶然间突发奇想,想到能不能让我们的在我们的手机设备上滑动触摸屏进而控制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
运行时的效果如下:
键盘的布局就比较复杂了,这部分也是一个fragment,整体布局如下:
这个布局中首先放上3个textview在一个LinearLayout这三个textview充当真实键盘上的3个led灯:num lock, caps lock, scroll lock。然后就是一个存放实际keyboard布局的LinearLayout容器,这么做的目的是这样的:因为手机的屏幕很小,想要放下一个标准键盘上的所有的按键肯定是不行的,因此需要将键盘分区,然后分别展示,这里的这个容器就是用来存放不同分区,不同布局的键盘部分。目前我把键盘分成了2个部分:主键盘部分,导航部分加上数字部分。其中主键盘部分是我们最常使用的部分,这部分包含了26个字母,0~9数字(字母上排),12个功能键等,导航部分就是上下左右键盘,上页下页部分等,数字部分就是数字小键盘和一些控制按键,我把导航键和数字键合并在一起了,这两部分的布局如下:
主键盘部分:
导航键部分:
数字键部分:
这里的布局需要说明一下,这里我使用了layout标签表明,然后使用XmlResourceParser类来解析这个里面的内容,最后再添加到布局中去。下面贴出两张键盘的运行效果图:
主键盘:
从键盘(导航键和数字键):
代码的基本分布如下:
各个类的作用如下:
一个looper线程
界面Fragment使用Handler和server交互,fragment需要实现server的DataHandlerListener接口,当Handler变化的时候通知Fragment,以便Fragment拿到最新的对象引用:
需要实现的接口
设置listener的接口:
fragment也可以主动获取:
这里的发送使用的就是protobuf的序列化接口,关于这个接口的描述这里就不详述,可以参考google protobuf的java部分的编程指导:
https://developers.google.com/protocol-buffers/docs/javatutorial
1. 捕获用户触摸屏移动,点击事件
设置滚动速度
更新移动速度
更行滚动速度:
这里针对两类键盘设计了两个创建接口:
主键盘:
从键盘:
他们都使用了createKeyboard接口创建实际的键盘:
这里返回一个Keyboard类对象,Keyboard类就是键盘实际的类了,这个类是LinearLayout的子类,使用XmlResourceParser来解析刚才我们定义的xml文件去获得键值和创建布局。
这里我只定义了一个消息类型,那就是HidData这是android需要发送给pc的消息数据。这其中有消息的类型:鼠标移动,鼠标按下,鼠标释放,鼠标滚轴,键盘长按,键盘长按释放,键盘单击。x轴偏移,y轴偏移(pc系统的鼠标移动是以坐标偏移作为参数的;鼠标按键键值,鼠标滚动值,键盘按键值。关于protobuf详细的数据定义语法请见:
https://developers.google.com/protocol-buffers/docs/proto
WirelessHidProto类就是上面这个文件通过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设备地址
解决了传输技术之后,还需要解决的是都有哪些数据类型,怎么传递数据,使用什么样的协议的问题。这些问题很关键,这涉及到以后的程序可扩展性问题,如果这部分欠缺考虑的话,那么后期的修改和扩展将是一个灾难。进过仔细考量之后,决定采用google的protobuf来封装所有的数据,因为protobuf灵活,小巧,高效,正好就是我要的。
进过了几天的业余时间开发,终于出来了一个可以运行展示的初级版本,这个版本可以满足基本的需求。目前我已经将这个代码开源出来了,项目地址是github:https://github.com/CreateChance/WirelessHid
UI的设计
我要做的就是使用手机实现一个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端
Server整体代码就是一个app,内容不是很复杂,这里我只陈述我的代码功能和必要的代码片段,详细代码内容有限于篇幅就不贴出来了,可以查看我的github项目主页(https://github.com/CreateChance/WirelessHid)上的开源代码。代码的基本分布如下:
各个类的作用如下:
MainActivity
这是主界面类,基本就是MouseFragment的容器,另外就是监听用户点击回退事件,如果用户在1.5s之内连续点击两次回退就退出app,基本逻辑比较简单。WirelessHidService
这是整个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
MouseFragment
这个类是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 :) } } }
KeyboardFragment
这是键盘的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; }
WirelessHid.proto
这是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端的代码就比较简单了,这里我只有两个类:具体类的说明如下:
WirelessHidClient
这个是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); }
WirelessHidProto
这个类是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设备地址
相关文章推荐
- Android保持屏幕常亮的三种方法
- Android保持屏幕常亮的三种方法
- Android 的几种Animation
- Android 退出Activity
- Android开发实践:屏幕旋转的处理
- Pro Android学习笔记(四八):ActionBar(1):Home图标区
- Android中的Drawable资源——Bitmap
- Android下pm 命令详解 - 安装APK
- Android控件学习之TextView:设置中划线,下划线效果
- Android百度地图应用之图层展示
- Android 集成支付宝支付详解
- 打造极致Material Design动画风格Button
- Android中将布局文件/View添加至窗口过程分析 ---- 从setContentView()谈起
- 补间动画
- Android设备上的传感器模拟工具:SensorSimulator
- A20 预装APK
- Android 生成so
- Android线程间的通信
- SparseBooleanArray sba
- 为自己的AndroidStudio添加Material Design Icons ADT Template