手把手教你做蓝牙聊天应用(五)-界面使用ConnectionManager

第5节 界面使用ConnectionManager

ConnectionManager已经设计完成了,它的价值需要在ChatActivity中体现出来。

5.1 监听ConnectionManager

实现对ConnectionManager各个状态的监听,当ConnectionManager的状态有变化、收到发送的数据时,需要让ChatActivity知道,它才能将各种变化反应到用户界面上。

5.1.1 创建监听器

ConnectionManager定义了ConnectionListener接口,状态变化、数据的接收可以通过这个接口获得。创建一个ConnectionListener监听器,

public class ChatActivity extends AppCompatActivity {
    ......
    private ConnectionManager.ConnectionListener mConnectionListener = new ConnectionManager.ConnectionListener() {

       @Override
       public void onConnectStateChange(int oldState, int State) {

       }

       @Override
       public void onListenStateChange(int oldState, int State) {

       }

       @Override
       public void onSendData(boolean suc, byte[] data) {

       }

       @Override
       public void onReadData(byte[] data) {

       }

    };
    ......
}

5.1.2 创建Handler

因为监听器触发的函数不一定是在UI线程被调用的,例如onConnectStateChange(),所以不能在监听器当中对界面做修改,必须把界面更新的任务交给UI线程进行。

安卓系统提供了Handler的机制,让其它非UI线程能通过Handler把界面更新的操作,从工作线程布置给主线程完成。

  1. 创建一个能在主线程当中工作的Handler,

    public class ChatActivity extends AppCompatActivity {
        ......
        private final static int MSG_SENT_DATA = 0;
        private final static int MSG_RECEIVE_DATA = 1;
        private final static int MSG_UPDATE_UI = 2;
    
        //不使用参数创建Handler,说明这个Handler是给主线程服务的
        private Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
    
                switch (msg.what) {
                    case MSG_SENT_DATA: {
                        //UI线程处理发送成功的数据,
                        //把文字内容展示到主界面上
                    }
                    break;
    
                    case MSG_RECEIVE_DATA: {
                        //UI线程处理接收到的对方发送的数据,
                        //把文字内容展示到主界面上
                    }
                    break;
    
                    case MSG_UPDATE_UI: {
                        //更新界面上的菜单等显示状态
                    }
                    break;
                }
            }
        };
        ......
    }
  2. ConnectionManager通知的内容,转交给主线程的Handler处理,
    private ConnectionManager.ConnectionListener mConnectionListener = new ConnectionManager.ConnectionListener() {
    
        @Override
        public void onConnectStateChange(int oldState, int State) {
            //连接状态的变化通知给UI线程,请UI线程处理
            mHandler.obtainMessage(MSG_UPDATE_UI).sendToTarget();
        }
    
        @Override
        public void onListenStateChange(int oldState, int State) {
            //监听状态的变化通知给UI线程,请UI线程处理
            mHandler.obtainMessage(MSG_UPDATE_UI).sendToTarget();
        }
    
        @Override
        public void onSendData(boolean suc, byte[] data) {
            //将发送的数据交给UI线程,请UI线程处理
            mHandler.obtainMessage(MSG_SENT_DATA, suc?1:0, 0, data).sendToTarget();
        }
    
        @Override
        public void onReadData(byte[] data) {
            //将收到的数据交给UI线程,请UI线程处理
            mHandler.obtainMessage(MSG_RECEIVE_DATA,  data).sendToTarget();
        }
    };
  3. 创建ConnectionManager,添加监听,
    private ConnectionManager mConnectionManager;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chat);
        ......
    
        mConnectionManager = new ConnectionManager(mConnectionListener);
        ......
    }

5.2 启动与停止监听

当聊天应用运行起来的时候,需要开启对其它蓝牙设备可能接入的监听,

private ConnectionManager mConnectionManager;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_chat);
    ......
    //开启监听
    mConnectionManager.startListen();
    ......
}

当应用退出的时候,要断开可能存在的连接并停止监听,

@Override
protected void onDestroy() {
    super.onDestroy();
    //移除Handler中可能存在的各种任务
    mHandler.removeMessages(MSG_UPDATE_UI);
    mHandler.removeMessages(MSG_SENT_DATA);
    mHandler.removeMessages(MSG_RECEIVE_DATA);

    //停止监听
    if(mConnectionManager != null) {
        mConnectionManager.disconnect();
        mConnectionManager.stopListen();
    }
}

5.3 启动连接

5.3.1 通过选择设备主动连接

当用户点击菜单栏的启动连接菜单项时,会启动DeviceListActivity,让用户从刷新的列表中,选取一个希望连接的设备。用户选择后,会把选中设备的地址返回给ChatActivity

这样,就可以利用ConnectionManager发起主动连接的请求了,

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);

  if(requestCode == RESULT_CODE_BTDEVICE && resultCode == RESULT_OK) {
      //取出传回来的地址
      String deviceAddr = data.getStringExtra("DEVICE_ADDR");
      //得到蓝牙设备的地址后,就可以通过ConnectionManager模块去连接设备
      mConnectionManager.connect(deviceAddr);
  }
}

之后,ConnectionManager的各种状态变化,就会通过监听器ConnectionListener,传递到ChatActivity当中,据此更新界面就好了。

5.3.2 通过监听被动连接

这种情况,并不需要用户去做任何点击的操作。

之后,ConnectionManager的各种状态变化,就会通过监听器ConnectionListener,传递到ChatActivity当中,据此更新界面就好了。

5.3.3 菜单项的改变

我们之前已经通过ChatActivityonCreateOptionsMenu()方法,把菜单项添加到了菜单栏。现在需要菜单项随着ConnectionManager状态的变化,跟着做变化了。

  1. 当监听器的onConnectStateChange()或者onListenStateChange被触发后,我们将变化通过Handler通知到了UI线程,

    private ConnectionManager.ConnectionListener mConnectionListener = new ConnectionManager.ConnectionListener() {
    
        @Override
        public void onConnectStateChange(int oldState, int State) {
            //连接状态的变化通知给UI线程,请UI线程处理
            mHandler.obtainMessage(MSG_UPDATE_UI).sendToTarget();
        }
    
        @Override
        public void onListenStateChange(int oldState, int State) {
            //监听状态的变化通知给UI线程,请UI线程处理
            mHandler.obtainMessage(MSG_UPDATE_UI).sendToTarget();
        }
        ......
    };
    
    ---------------------------------------------
    
    private Handler mHandler = new Handler() {
      @Override
      public void handleMessage(Message msg) {
    
          switch (msg.what) {
              ......
    
              case MSG_UPDATE_UI: {
                  //更新界面上的菜单等显示状态
                  updateUI();
              }
              break;
          }
    
      }
    };

    在更新UI到方法updateUI()中,修改菜单项的显示,

    private void updateUI()
    {
        if(mConnectionManager == null) {
            return;
        }
    
        //默认情况下,禁止点击发送按钮和文字编辑框
        if(mConnectionMenuItem == null) {
            mMessageEditor.setEnabled(false);
            mSendBtn.setEnabled(false);
    
            return;
        }
    
        //设置成连接状态,允许点击发送按钮和文字编辑框
        if(mConnectionManager.getCurrentConnectState() == ConnectionManager.CONNECT_STATE_CONNECTED) {
            mConnectionMenuItem.setTitle(R.string.disconnect);
    
            mMessageEditor.setEnabled(true);
            mSendBtn.setEnabled(true);
        }
        //设置成正在连接状态,禁止点击发送按钮和文字编辑框
        else if(mConnectionManager.getCurrentConnectState() == ConnectionManager.CONNECT_STATE_CONNECTING) {
            mConnectionMenuItem.setTitle(R.string.cancel);
    
            mMessageEditor.setEnabled(false);
            mSendBtn.setEnabled(false);
        }
        //设置成未连接状态,禁止点击发送按钮和文字编辑框
        else if(mConnectionManager.getCurrentConnectState() == ConnectionManager.CONNECT_STATE_IDLE) {
            mConnectionMenuItem.setTitle(R.string.connect);
    
            mMessageEditor.setEnabled(false);
            mSendBtn.setEnabled(false);
        }
    }
  2. 菜单项的响应也需要根据当前的连接状态,做进一步的修改,
@Override
public boolean onOptionsItemSelected(MenuItem item) {

    switch (item.getItemId())
    {
        case R.id.connect_menu: {
            //如果当前处于连接状态,点击后就取消当前连接
            if(mConnectionManager.getCurrentConnectState() == ConnectionManager.CONNECT_STATE_CONNECTED) {
                mConnectionManager.disconnect();
            }
            //如果当前处于正在连接,点击后就取消当前连接
            else if(mConnectionManager.getCurrentConnectState() == ConnectionManager.CONNECT_STATE_CONNECTING) {
                mConnectionManager.disconnect();
            }
            //如果当前处于未连接状态,点击后就启动查找当前可连接设备的Activity
            else if(mConnectionManager.getCurrentConnectState() == ConnectionManager.CONNECT_STATE_IDLE) {
                Intent i = new Intent(ChatActivity.this, DeviceListActivity.class);
                startActivityForResult(i, RESULT_CODE_BTDEVICE);
            }
        }
        return true;
        ......
    }
}

5.4 发送与显示数据

聊天文字发送成功或者接收到对方发来的文字时,要显示到列表中。为此,我们需要专门设计一个Adapter来展示它们。

5.4.1 文字信息的数据结构

首先定义一个记录每条信息的数据结构ChatMessage,每一条消息要注明是由谁发来的,是自己还是对方,

public class ChatMessage {
   //主动发出的消息
   static public final int MSG_SENDER_ME = 0;
   //接收到的消息
   static public final int MSG_SENDER_OTHERS = 1;

   public int messageSender;
   public String messageContent;
}

5.4.2 信息展示的Adapter

我们采用类似微信聊天的样子来展示聊天内容。每条消息的背景图片是9patch形式的PNG图片,将它们放在res\drawable目录中。针对不同的屏幕像素密度,设计了对应的图片,放到对应的drawable目录下就行了。例如为xxhdip设计的背景图片就放在res\drawable-xxhdip目录中。

这些图片可以在示例代码中获得。

  1. 定义展示对方发来信息的布局-others_list_item.xml

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal" android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="58dp"
        android:padding="5dp">
    
        <!--显示类似头像的图片-->
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_device_bluetooth"/>
    
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"> ---设置为1,让文字显示尽情利用右边区域
            <!--显示文字内容-->
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:id="@+id/message_content"
                android:padding="5dp"
                android:textSize="16sp"
                android:gravity="center_vertical"
                android:background="@drawable/others"/>
        </FrameLayout>
    </LinearLayout>
  2. 定义展示自己发送信息的布局-me_list_item.xml

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal" android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="58dp"
        android:padding="5dp"
        android:gravity="right">
    
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"> ---设置为1,让文字显示尽情利用左边区域
            <!--显示文字内容-->
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/message_content"
                android:gravity="center_vertical"
                android:layout_gravity="right"
                android:padding="5dp"
                android:textSize="16sp"
                android:background="@drawable/me"/>
    
        </FrameLayout>
        <!--显示类似头像的图片-->
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_device_bluetooth"/>
    
    </LinearLayout>
  3. 定义Adapter-MessageAdapter,
    public class MessageAdapter extends ArrayAdapter<ChatMessage> {
    
        private final LayoutInflater mInflater;
        private int mResourceMe;
        private int mResourceOthers;
    
        //要指定自己发送的消息显示用的布局-me_list_item,
        //以及对方发送消息显示用的布局-others_list_item
        public MessageAdapter(Context context, int resourceMe, int resourceOthers) {
            super(context, 0);
            mInflater = LayoutInflater.from(context);
            mResourceMe = resourceMe;
            mResourceOthers = resourceOthers;
        }
    
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
    
            ChatMessage message = getItem(position);
            //根据消息类型的不同,使用不同的布局作为消息项
            convertView = mInflater.inflate(message.messageSender == ChatMessage.MSG_SENDER_ME ? mResourceMe : mResourceOthers, parent, false);
    
            //显示消息的内容
            TextView name = (TextView) convertView.findViewById(R.id.message_content);
            name.setText(message.messageContent);
    
            return convertView;
        }
    }
  4. 使用聊天列表,
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chat);
        ......
        mMessageListView = (ListView) findViewById(R.id.message_list);
        MessageAdapter adapter = new MessageAdapter(this, R.layout.me_list_item, R.layout.others_list_item);
        mMessageListView.setAdapter(adapter);
        ......
    }

5.4.3 文字的发送

当连接建立以后,禁止点击的发送按钮和文字编辑框将被解禁。再文字编辑框中编辑好文字,点击发送按钮,就能将文字发送出去了。

  1. 为按钮创建监听器,当点击后,获取文字编辑框中的数据,再使用ConnectionManager提供的接口,把数据发送出去,

    private View.OnClickListener mSendClickListener = new View.OnClickListener() {
    
        @Override
        public void onClick(View v) {
            //获取要发送的文字内容
            String content = mMessageEditor.getText().toString();
            if(content != null) {
                content = content.trim();
                if(content.length() > 0) {
                    //利用ConnectionManager发送数据
                    boolean ret = mConnectionManager.sendData(content.getBytes());
                    if(!ret) {
                        Toast.makeText(ChatActivity.this, R.string.send_fail, Toast.LENGTH_SHORT).show();
                    }
                }
            }
        }
    };
  2. 注册监听函数,
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chat);
    
        ......
    
        mSendBtn = (ImageButton) findViewById(R.id.send_btn);
        mSendBtn.setOnClickListener(mSendClickListener);
    
        ......
    }
  3. ConnectionListeneronSendData回调方法中,将发送结果传递给UI线程,让UI线程把聊天内容更新到消息列表中,
    private ConnectionManager.ConnectionListener mConnectionListener = new ConnectionManager.ConnectionListener() {
    
        ......
        @Override
        public void onSendData(boolean suc, byte[] data) {
            //发送的结果传递给UI线程,文字的二进制内容包含在data参数中
            mHandler.obtainMessage(MSG_SENT_DATA, suc?1:0, 0, data).sendToTarget();
        }
        ......
    
    };
    
    ---------------------------------------------
    
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
    
            switch (msg.what) {
                case MSG_SENT_DATA: {
                    //获取发送的文字内容
                    byte [] data = (byte []) msg.obj;
                    boolean suc = msg.arg1 == 1;
                    if(data != null && suc) {
                        //发送成功后创建消息
                        ChatMessage chatMsg = new ChatMessage();
                        chatMsg.messageSender = ChatMessage.MSG_SENDER_ME;
                        chatMsg.messageContent = new String(data);
    
                        //将消息展示到消息列表中
                        MessageAdapter adapter = (MessageAdapter) mMessageListView.getAdapter();
                        adapter.add(chatMsg);
                        adapter.notifyDataSetChanged();
    
                        mMessageEditor.setText("");
                    }
                }
                break;
                ......
            }
    
        }
    };

5.4.4 文字的接收

当接收到对方发来的消息时,ConnectionListeneronReadData回调方法,将发送结果传递给UI线程,让UI线程把聊天内容更新到消息列表中,

private ConnectionManager.ConnectionListener mConnectionListener = new ConnectionManager.ConnectionListener() {
    ......
    @Override
    public void onReadData(byte[] data) {
        //接收到的内容传递给UI线程,文字的二进制内容包含在data参数中
        mHandler.obtainMessage(MSG_RECEIVE_DATA,  data).sendToTarget();
    }
};

-------------------------------------------------

private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {

        switch (msg.what) {
            ......
            case MSG_RECEIVE_DATA: {

                byte [] data = (byte []) msg.obj;
                if(data != null) {

                    ChatMessage chatMsg = new ChatMessage();
                    chatMsg.messageSender = ChatMessage.MSG_SENDER_OTHERS;
                    chatMsg.messageContent = new String(data);

                    //将消息展示到消息列表中
                    MessageAdapter adapter = (MessageAdapter) mMessageListView.getAdapter();
                    adapter.add(chatMsg);
                    adapter.notifyDataSetChanged();
                }

            }
            break;

            ......
        }

    }
};

至此,蓝牙聊天的整个流程都得以实现了。

时间: 2024-10-25 21:57:36

手把手教你做蓝牙聊天应用(五)-界面使用ConnectionManager的相关文章

手把手教你做蓝牙聊天应用(三)-获取要连接的设备

第3节 获取要连接的设备 这一节我们开始设计蓝牙聊天应用的界面.根据之前的规划,连接管理将放在单独的ConnectionManager模块当中,所以每当要使用连接功能的时候,我们就暂时把它空着,等到ConnectionManager开发完成之后再加进来. 这里我们将完成下面的界面设计, 3.1 主界面 主界面是一个独立的Activity-ChatActivity,它要实现三个主要功能, 当蓝牙没有开启或者设备不能被发现的时候,请求用户打开对应的功能: 下方有输入框输入要发送的文字内容,点击按钮后

手把手教你做蓝牙聊天应用(一)-设计方案

前言 通过"计算器"和"视频播放器"我们已经能够开始开发一些比较像样的应用了. 今天,我们将开始制作一个"蓝牙聊天"应用.这个应用其实很简单,没有炫酷的界面,就是一对一.通过蓝牙连接两台设备,让两个人互相发送信息. 可别觉得它太无聊.没有什么实用性,其实我们正是想通过它让你开始接触网络编程(蓝牙和wifi都是无线连接技术,它们的程序设计方法和思路非常的相似). 另外,学会了使用蓝牙,就为大家打开了技术开发的另一扇大门-物联网,现在很多物联网硬件都

手把手教你做蓝牙聊天应用(六)-界面优化

第6节 应用的美化与完善 现在,我们还可以为聊天应用加上多国语言的支持和关于界面,把使用到的颜色和尺寸定义到资源文件当中,这样一来,安豆的蓝牙聊天应用就算是比较完整的完成了. 这两部分在以前"计算器"章节中,已经介绍过了,大家就自己动手吧. 这一节,我们将重点介绍聊天文字的背景图片是如何制作的. 6.1 9Patch图片的原理 观察一下安卓系统中需要经常用到的图片,可以发现: 很多要使用透明效果的地方在转角处: 很多图片不同的地方只在靠近边缘的地方,内部区域几乎都是一样的: 为此安卓系

手把手教你做蓝牙聊天应用(二)-设计方案

第2节 设计方案 功能确定后,就要开始围绕功能进行功能的验证.界面设计的规划.以及程序结构的规划了. 2.1 技术验证 选定了现阶段要完成的核心功能后,我们首先需要对它们做技术上的验证,看看用什么样的方法能实现它们.在进行技术验证的同时,也能让我们发现很多我们在头脑风暴阶段没有意识到的现实问题. 现在的手机和移动设备已经把蓝牙作为了标准配置,它常常用到与周边小设备的数据连接上,例如蓝牙自拍杆,蓝牙音箱,蓝牙键盘,运动手环等等,可以看出这都是一些不需要太大数据量传输而需要保持长时间数据通信的设备.

手把手教你做蓝牙聊天应用(四)-蓝牙连接模块

第4节 蓝牙连接模块 蓝牙连接的管理模块需要为ChatActivity提供于连接相关的所有功能,要设计的方便使用,并尽量隐藏连接的细节. 4.1 对外接口 我们首先来看看ConnectionManager需要向Chat Activity提供哪些接口. 监听.当应用运行起来后,聊天应用需要启动对其它蓝牙设备的监听,迎接随时可能到来的连接请求.所以ConnectionManager需要提供启动监听-startListen()和停止监听-stopListen()的两个接口: 主动连接.应用搜索到可连接

手把手教你做蓝牙小车(二)

第5节 BTChat 本节开始介绍Arduino蓝牙模块,配合Android应用,实现一个蓝牙聊天应用. 5.1 什么是蓝牙 简单说就是一种不同设备之间点对点通讯的技术. 有大篇大篇的蓝牙各种协议,各种规范... 本课程只讲用到的内容,不展开更多内容了. 5.2 SDP Service Discovery Protocol,简称SDP,是允许设备发现其他设备所支持服务的协议. 蓝牙协议给每个服务分配一个UUID,用来区分各种服务. SDP的UUID是00001101-0000-1000-8000

手把手教你做Android聊天机器人

我相信大家应该知道有款应用叫小黄鸡吧! 如果不知道,那你见过那种可以秒回复的聊天应用么? 如果仍然没看到!那你总见过可以迅速回复你的微信公共账吧! 如果仍然....亲出门左拐 好,不多说. 首先大家都应该了解程序,程序就是由人为的设定搭建起来的一套系统, 这里的机器人也是,简单的原理就是当你输入关键字后,通过一套算法, 在数据库中找到与之最为匹配的内容在返回给你.这个已经有人实现,我们这节教程就用别人 已经实现好的东西去做一??个应用来玩耍~~ (当然如果你非要想知道如何去做,给我留言.看情况我

手把手教你做蓝牙小车(一)

第1节 选择Arduino开发板 1.1 Arduino是什么 对Arduino,官方有一堆解释. 作为一个软件程序猿,在我眼里,Arduino是学习"可怕硬件"的一个便捷通道.它把复杂的硬件名称,属性给我们隐藏起来,只需要一些简单的软件知识,就可以学习硬件开发. 1.2 怎么选择Arduino开发板 1.2.1 官方版本还是兼容版本 Arduino是开源项目,硬件结构,软件设计都开源. 所以不存在盗版的问题. 官方版质量肯定是杠杠的,但价格也贵. 两者价格数字差不多,一个卖人民币,一

手把手教你做蓝牙小车(三)

第6节 马达 要说蓝牙小车哪个模块最重要,多数人一定会以为是马达. 之前说过,为了防止开发板被电流击穿,控制马达时要增加一块扩展板. 所以,控制马达,只对扩展板编程,而不需要对马达编程. 此外,扩展板厂家会提供通过扩展板控制马达的代码. 综上,对开发人员来说,马达,只要确认存在就可以了. 6.1 扩展板 Arduino开发板只提供了一些基础.通用的接口,针对一些常见的特殊功能,Arduino专门为其推出了扩展板. 6.1.1 官方扩展板 Arduino官方目前总共推出了5款扩展板. 分别是 Ar