第3节 获取要连接的设备
这一节我们开始设计蓝牙聊天应用的界面。根据之前的规划,连接管理将放在单独的ConnectionManager
模块当中,所以每当要使用连接功能的时候,我们就暂时把它空着,等到ConnectionManager
开发完成之后再加进来。
这里我们将完成下面的界面设计,
3.1 主界面
主界面是一个独立的Activity
-ChatActivity
,它要实现三个主要功能,
- 当蓝牙没有开启或者设备不能被发现的时候,请求用户打开对应的功能;
- 下方有输入框输入要发送的文字内容,点击按钮后能实现文字的发送;输入框上方的大部分区域用来显示聊天的内容;
- 菜单栏根据当前蓝牙连接的状态,显示不同的菜单项。例如,没有连接时启动
蓝牙设备选择
界面;
3.1.1 打开蓝牙功能
在ChatActivity
创建的时候,查询当前蓝牙设备是否满足运行的要求,
提示开启蓝牙功能,
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_chat); BluetoothAdapter BTAdapter = BluetoothAdapter.getDefaultAdapter(); if (!BTAdapter.isEnabled()) { Intent i = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivity(i); finish(); return; }}
提示开启被其它蓝牙设备发现的功能,
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_chat); ...... if(BTAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { Intent i = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); i.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0); startActivity(i); }}
3.1.2 界面布局
界面布局比较简单,使用垂直的线性布局LinearLayout将界面分成两个区域,上面的大区域显示聊天的内容,用ListView
的显示;下面文字输入和发送用TextEditor
和ImageButton
组合起来。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ListView android:id="@+id/message_list" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:divider="#0000" ---数据项之间的分割行,设置成透明的,我们将用别的方式来区分每条数据项 android:stackFromBottom="true" android:transcriptMode="alwaysScroll" /> <View android:layout_width="match_parent" android:layout_height="2dp" android:background="#000"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <EditText android:id="@+id/msg_editor" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom" android:layout_weight="1" android:imeOptions="actionSend"/> <ImageButton android:id="@+id/send_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@mipmap/ic_content_add_circle" /> </LinearLayout></LinearLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
在代码中,获取将来要操作的控件,
private ImageButton mSendBtn;private ListView mMessageListView;private EditText mMessageEditor;@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_chat); ...... mMessageEditor = (EditText) findViewById(R.id.msg_editor); mSendBtn = (ImageButton) findViewById(R.id.send_btn); mMessageListView = (ListView) findViewById(R.id.message_list); ......}
3.1.3 菜单项显示
菜单栏根据当前蓝牙连接的状态,显示不同的菜单项,
没有连接时,显示启动连接
。点击该菜单,将启动显示可连接设备的Activity
-DeviceListActivity
;
正在连接时,显示取消
。点击该菜单,将取消正在进行的连接;
已经连接时,显示断开连接
。点击该菜单,将断开与其它设备已经建立好的连接;
由于这里要根据蓝牙设备连接的状况设计不同的逻辑,所以接下来设计的ConnectionManager
要为其它模块提供获取当前连接状态
的接口。
目前,我们就暂时将它设计成满足条件1的状况,
定义一个菜单main_menu.xml
,
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:apps="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/connect_menu" android:title="@string/connect" apps:showAsAction="always"/> <item android:id="@+id/about_menu" android:title="@string/about" apps:showAsAction="never"/></menu>
将菜单添加到菜单栏
中,
private MenuItem mConnectionMenuItem;@Overridepublic boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.main_menu, menu); mConnectionMenuItem = menu.findItem(R.id.connect_menu); return true;}
响应菜单栏
,启动DeviceListActivity
获取可以连接到设备名称
@Overridepublic boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.connect_menu: { } return true; case R.id.about_menu: { } return true; default: return false; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
我们从ChatActivity
启动DeviceListActivity
,目的是要获取DeviceListActivity
返回的内容-蓝牙设备的连接地址。所以不能简单的使用startActivity()
方法了。
两个Activity之间传递数据,可以使用startActivityForResult()
方法,这里面要设置一个ResultCode
,用来主返回结果的时候使用辨别结果对应的是哪个请求,
private final int RESULT_CODE_BTDEVICE = 0;@Overridepublic boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.connect_menu: { Intent i = new Intent(ChatActivity.this, DeviceListActivity.class); startActivityForResult(i, RESULT_CODE_BTDEVICE); } return true; ...... }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
返回的结果将在onActivityResult()
函数中被通知到。这里参数的requestCode
就是我们在startActivityForResult()
中填入的那个数值;而resultCode
代表另一个Activity
是否如我们所愿返回了结果,
@Overrideprotected 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模块去连接设备了。
在蓝牙设备连接之前,是不需要编辑文字和发送内容的。所以,可以使用View
的setEnabled()
函数,将TextEditor
和ImageButton
给禁用掉(点击它们不会有任何响应)。等到设备连接上之后,在把它们开启。
mMessageEditor.setEnabled(false);mSendBtn.setEnabled(false);
3.2 设备列表界面开发
为设备列表界面创建一个DeviceListActivity
。
3.2.1 主界面布局
界面布局很简单,就是一个ListView
,
<ListView 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" tools:context="com.anddle.anddlechat.MainActivity" android:id="@+id/device_list"></ListView>
在代码中,设置上返回按钮,并获取这个ListView
,以备将来使用,
private ListView mBTDeviceListView;@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.device_list_activity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mBTDeviceListView = (ListView) findViewById(R.id.device_list); ...... }
为了展示可连接的蓝牙设备,我们会把收集到的可连接设备保存起来,通过ListView
进行显示。
这里将自定义一个Adapter
-DeviceItemAdapter
,让它显示设备的名字和地址,
数据项的界面布局,
<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"> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:id="@+id/device_name" android:gravity="center_vertical" android:drawableLeft="@mipmap/ic_device_bluetooth"/> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/device_info" android:gravity="center_vertical|right"/></LinearLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
自定义的DeviceItemAdapter
将继承自ArrayAdapter
,
public class DeviceItemAdapter extends ArrayAdapter<BluetoothDevice> { private final LayoutInflater mInflater; private int mResource; public DeviceItemAdapter(Context context, int resource) { super(context, resource); mInflater = LayoutInflater.from(context); mResource = resource; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = mInflater.inflate(mResource, parent, false); } TextView name = (TextView) convertView.findViewById(R.id.device_name); TextView info = (TextView) convertView.findViewById(R.id.device_info); BluetoothDevice device = getItem(position); name.setText(device.getName()); info.setText(device.getAddress()); return convertView; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
使用ListView
,
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.device_list_activity); ...... DeviceItemAdapter adapter = new DeviceItemAdapter(this, R.layout.device_list_item); mBTDeviceListView = (ListView) findViewById(R.id.device_list); mBTDeviceListView.setAdapter(adapter); ...... }
3.2.2 展现可连接的设备
可连接的设备包括两种,
- 曾经连接过的,已经被系统记录在案,连接这种设备时,系统不会提示用户有设备需要配对;
完全新发现的设备,连接这种设备时,系统会提示用户有设备需要配对。
3.2.2.1 获取已绑定过的设备
获取第一种设备很简单,使用BluetoothAdapter
的getBondedDevices()
方法就可以了。找到后,添加到ListView
中显示,
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.device_list_activity); ...... mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); DeviceItemAdapter adapter = (DeviceItemAdapter) mBTDeviceListView.getAdapter(); if (pairedDevices.size() > 0) { for (BluetoothDevice device : pairedDevices) { adapter.add(device); } } ......}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
3.2.2.2 获取新发现的设备
获取第二种设备,就采用技术验证时使用的mBluetoothAdapter.startDiscovery()
方法;
首先要注册一个BroadcastReceiver
,然后startDiscovery()
,之后系统会发出BluetoothAdapter.ACTION_DISCOVERY_STARTED
的广播,告知搜索开始;发出BluetoothAdapter.ACTION_DISCOVERY_FINISHED
的广播,告知搜索结束,
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.device_list_activity); ...... IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_FOUND); filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); registerReceiver(mReceiver, filter); mBluetoothAdapter.startDiscovery(); ......}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
根据收到的广播,更新显示列表。假如搜索到的设备是曾经绑定过的,说明之前已经加到设备列表里面了,这里不需要重复添加,
private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (BluetoothDevice.ACTION_FOUND.equals(action)) { BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (device.getBondState() != BluetoothDevice.BOND_BONDED) { DeviceItemAdapter adapter = (DeviceItemAdapter) mBTDeviceListView.getAdapter(); adapter.add(device); adapter.notifyDataSetChanged(); } } }};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
注意,这里能够在BroadcastReceiver
的onReceive()
方法中直接修改界面元素,是因为onReceive()
是运行在UI线程-主线程当中的。
在DeviceListActivity
销毁的时候,注销BroadcastReceiver
,同时也别忘了取消可能正在进行的搜索,
@Overrideprotected void onDestroy() { super.onDestroy(); if (mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.cancelDiscovery(); } unregisterReceiver(mReceiver);}
至此,DeviceListActivity
已经可以列出可被发现和连接到设备了。
3.2.3 设置菜单栏
设置菜单栏
的菜单项device_menu.xml
,让菜单项一直显示,
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:apps="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/search_menu" android:title="@string/search" apps:showAsAction="always"/></menu>
我们将根据搜索设备的状态更改该菜单项的名称。所以,这里要定义当前搜索的状态,
- 当正在搜索的时候,显示
取消
,此时状态是BT_SEARCH_STATE_SEARCHING
; - 当没有搜索的时候,显示
搜索
,此时对应的状态是BT_SEARCH_STATE_IDLE
;
这两种状态,都要记录下来,
private final int BT_SEARCH_STATE_IDLE = 0;private final int BT_SEARCH_STATE_SEARCHING = 1;private int mBTSearchingState = BT_SEARCH_STATE_IDLE;
在代码中添加菜单项,
private MenuItem mSearchMenuItem;@Overridepublic boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.device_menu, menu); mSearchMenuItem = menu.findItem(R.id.search_menu); updateUI(); return true;}private void updateUI() { switch (mBTSearchingState) { case BT_SEARCH_STATE_IDLE: { if(mSearchMenuItem != null) { mSearchMenuItem.setTitle(R.string.search); } } break; case BT_SEARCH_STATE_SEARCHING: { if(mSearchMenuItem != null) { mSearchMenuItem.setTitle(R.string.cancel); } } break; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
响应菜单项,
@Overridepublic boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case R.id.search_menu: { if(mBTSearchingState == BT_SEARCH_STATE_IDLE) { if (mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.cancelDiscovery(); } updateDeviceList(); } else if(mBTSearchingState == BT_SEARCH_STATE_SEARCHING) { if (mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.cancelDiscovery(); } } } break; case android.R.id.home: this.finish(); break; } return true;}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
为了更新菜单项,还需要BroadcastReceiver
的配合,
@Overridepublic void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (BluetoothDevice.ACTION_FOUND.equals(action)) { ...... } else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) { mBTSearchingState = BT_SEARCH_STATE_SEARCHING; updateUI(); } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { mBTSearchingState = BT_SEARCH_STATE_IDLE; updateUI(); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
3.3 得到要连接的设备
当用户点击要连接的设备后,将把该设备的地址返回给ChatActivity
,由ChatActivity
去连接设备。
- 为设备列表设置点击响应;
- 假如点击的时候还在进行搜索,取消搜索;
- 获取设备的地址,将它存储到
Intent
当中,最后通过setResult()
方法,将结果传递给启动DeviceListActivity
的Activity
-ChatActivity
,
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.device_list_activity); ...... mBTDeviceListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.cancelDiscovery(); } ArrayAdapter adapter = (ArrayAdapter) mBTDeviceListView.getAdapter(); BluetoothDevice device = (BluetoothDevice) adapter.getItem(position); Intent i = new Intent(); i.putExtra("DEVICE_ADDR", device.getAddress()); setResult(RESULT_OK, i); finish(); } }); ......}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
点击之后,选中的设备地址会传递到ChatActivity
的onActivityResult()
方法中,
@Overrideprotected 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"); }}
0 0