Android Ble蓝牙App(一)扫描(上)https://developer.aliyun.com/article/1407782
② 点击监听
首先是ScanActivity的一些基本配置,如下所示:
class ScanActivity : BaseActivity() { private val TAG = ScanActivity::class.java.simpleName private val binding by viewBinding(ActivityScanBinding::inflate) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_scan) } }
然后增加布局中按钮的点击监听,创建一个initView()函数,在onCreate()中调用它,代码如下所示:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_scan) initView() } private fun initView() { binding.requestBluetoothConnectLay.btnRequestConnectPermission.setOnClickListener(this) binding.enableBluetoothLay.btnEnableBluetooth.setOnClickListener(this) binding.requestLocationLay.btnRequestLocationPermission.setOnClickListener(this) binding.enableLocationLay.btnEnableLocation.setOnClickListener(this) binding.requestBluetoothScanLay.btnRequestScanPermission.setOnClickListener(this) binding.toolbar.setOnClickListener(this) binding.tvScanStatus.setOnClickListener(this) }
然后实现点击监听
class ScanActivity : BaseActivity(), View.OnClickListener
重写onClick()函数,代码如下所示:
override fun onClick(v: View) { when (v.id) { //请求蓝牙连接权限 R.id.btn_request_connect_permission -> {} //打开蓝牙开关 R.id.btn_enable_bluetooth -> {} //请求定位权限 R.id.btn_request_location_permission -> {} //打开位置开关 R.id.btn_enable_location -> {} //请求蓝牙扫描权限 R.id.btn_request_scan_permission -> {} //扫描或停止扫描 R.id.tv_scan_status -> {} else -> {} } }
在这里我们先不写内容,后面再完善,然后我们可以先处理权限,再重写Activity的onResume()函数,代码如下所示:
override fun onResume() { super.onResume() if (isAndroid12()) { //蓝牙连接 binding.requestBluetoothConnectLay.root.visibility = if (hasBluetoothConnect()) View.GONE else View.VISIBLE if (!hasBluetoothConnect()) { Log.d(TAG, "onResume: 未获取蓝牙连接权限") return } //打开蓝牙开关 binding.enableBluetoothLay.root.visibility = if (isOpenBluetooth()) View.GONE else View.VISIBLE if (!isOpenBluetooth()) { Log.d(TAG, "onResume: 未打开蓝牙") return } //蓝牙扫描 binding.requestBluetoothScanLay.root.visibility = if (hasBluetoothScan()) View.GONE else View.VISIBLE if (!hasBluetoothScan()) { Log.d(TAG, "onResume: 未获取蓝牙扫描权限") return } } //打开蓝牙 binding.enableBluetoothLay.root.visibility = if (isOpenBluetooth()) View.GONE else View.VISIBLE if (!isOpenBluetooth()) { Log.d(TAG, "onResume: 未打开蓝牙") return } //打开定位 binding.enableLocationLay.root.visibility = if (isOpenLocation()) View.GONE else View.VISIBLE if (!isOpenLocation()) { Log.d(TAG, "onResume: 未打开位置") return } //请求定位 binding.requestLocationLay.root.visibility = if (hasCoarseLocation() && hasAccessFineLocation()) View.GONE else View.VISIBLE if (!hasAccessFineLocation()) { Log.d(TAG, "onResume: 未获取定位权限") return } binding.tvScanStatus.visibility = View.VISIBLE //开始扫描 }
③ 扫描处理
在这个函数中对activity_scan.xml中引入的布局判断是否显示,在请求权限或者是打开开关之后都会触发这个函数,然后进行检查,当所有检查都通过之后说明你可以开始扫描了。那么如果要扫描,我们需要得到BleCore的对象,先声明,然后在onCreate中进行实例化。
private lateinit var bleCore: BleCore override fun onCreate(savedInstanceState: Bundle?) { ... bleCore = (application as BleApp).getBleCore() }
下面我们可以写扫描相关的方法,代码如下所示:
private fun startScan() { bleCore?.startScan() binding.tvScanStatus.text = "停止" binding.pbScanLoading.visibility = View.VISIBLE } private fun stopScan() { bleCore?.stopScan() binding.tvScanStatus.text = "搜索" binding.pbScanLoading.visibility = View.INVISIBLE }
这里就是开始和停止扫描,别忘了还有扫描回调,这个回调应该写在哪里,首先是在onCreate()函数中,代码如下:
override fun onCreate(savedInstanceState: Bundle?) { ... //设置扫描回调 if (isOpenBluetooth()) bleCore!!.setPhyScanCallback(this@ScanActivity) }
这里还判断了一下是否开启蓝牙,扫描的结果需要实现BleScanCallback
接口,如下所示:
class ScanActivity : BaseActivity(), View.OnClickListener, BleScanCallback
重写onScanResult()
函数,如下所示:
/** * 扫描回调 */ override fun onScanResult(result: ScanResult) { }
④ 广播处理
然后别忘记了我们还有一个广播处理,在onCreate()函数中进行广播注册,代码如下所示:
override fun onCreate(savedInstanceState: Bundle?) { ... //注册广播 registerReceiver( ScanReceiver().apply { setCallback(this@ScanActivity) }, IntentFilter().apply { addAction(BluetoothAdapter.ACTION_STATE_CHANGED) addAction(LocationManager.PROVIDERS_CHANGED_ACTION) }) }
实现接口ReceiverCallback
,代码如下所示:
class ScanActivity : BaseActivity(), View.OnClickListener, BleScanCallback, ReceiverCallback
重写里面的函数,代码如下所示:
/** * 蓝牙关闭 */ override fun bluetoothClose() { } /** * 位置关闭 */ override fun locationClose() { }
四、权限处理
下面我们进行权限和开关的请求处理,在ScanActivity中新增如下代码:
//蓝牙连接权限 private val requestConnect = registerForActivityResult(ActivityResultContracts.RequestPermission()) { showMsg(if (it) "可以打开蓝牙" else "Android12 中不授予此权限无法打开蓝牙") } //启用蓝牙 private val enableBluetooth = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { showMsg("蓝牙已打开") Log.d(TAG, ": 蓝牙已打开") bleCore.setPhyScanCallback(this@ScanActivity) } } //请求定位 private val requestLocation = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> val coarseLocation = result[Manifest.permission.ACCESS_COARSE_LOCATION] val fineLocation = result[Manifest.permission.ACCESS_FINE_LOCATION] if (coarseLocation == true && fineLocation == true) { //开始扫描设备 showMsg("定位权限已获取") if (isOpenBluetooth()) bleCore.setPhyScanCallback(this@ScanActivity) } } //启用定位 private val enableLocation = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { showMsg("位置已打开") Log.d(TAG, ": 位置已打开") if (isOpenBluetooth()) bleCore.setPhyScanCallback(this@ScanActivity) } } //蓝牙连接权限 private val requestScan = registerForActivityResult(ActivityResultContracts.RequestPermission()) { showMsg(if (it) "可以开始扫描设备了" else "Android12 Android12 中不授予此权限无法扫描蓝牙") }
这里使用了Activity Result API,需要注意的是它们是与onCreate()
函数平级的,下面修改onClick()
函数中的代码:
override fun onClick(v: View) { when (v.id) { //请求蓝牙连接权限 R.id.btn_request_connect_permission -> if (isAndroid12()) requestConnect.launch(Manifest.permission.BLUETOOTH_CONNECT) //打开蓝牙开关 R.id.btn_enable_bluetooth -> enableBluetooth.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) //请求定位权限 R.id.btn_request_location_permission -> requestLocation.launch( arrayOf( Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION ) ) //打开位置开关 R.id.btn_enable_location -> enableLocation.launch(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) //请求蓝牙扫描权限 R.id.btn_request_scan_permission -> if (isAndroid12()) requestScan.launch(Manifest.permission.BLUETOOTH_SCAN) //扫描或停止扫描 R.id.tv_scan_status -> if (bleCore.isScanning()) stopScan() else startScan() else -> {} } }
这里就比较的简单了,下面再修改bluetoothClose()
和locationClose()
函数,在回调时都判断当前是否正在扫描,在扫描则停止,同时显示对应的布局。
override fun bluetoothClose() { //蓝牙关闭时停止扫描 if (bleCore.isScanning()) { stopScan() binding.enableBluetoothLay.root.visibility = View.VISIBLE } } override fun locationClose() { //位置关闭时停止扫描 if (bleCore.isScanning()) { stopScan() binding.enableLocationLay.root.visibility = View.VISIBLE } }
最后再增加一个onStop()函数,代码如下:
override fun onStop() { super.onStop() //页面停止时停止扫描 if (bleCore.isScanning()) stopScan() }
当页面销毁了或者是进入后台了,那么触发回调,停止扫描。
五、扫描结果
要显示扫描结果,首先要做的是定义一个类去装载扫描结果,在ble包下新建一个BleDevice
数据类,代码如下所示:
data class BleDevice( var realName: String? = "Unknown device", //蓝牙设备真实名称 var macAddress: String, //蓝牙设备Mac地址 var rssi: Int, //信号强度 var device: BluetoothDevice,//蓝牙设备 var gatt: BluetoothGatt? = null//gatt )
扫描的结果我们可以用列表来展示,选择使用RecyclerView,那么相应的会使用到适配器。
① 列表适配器
首先创建适配器的布局,在layout下新建一个item_device_rv.xml
,代码如下所示:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/item_device" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="1dp" android:background="@color/white" android:foreground="?attr/selectableItemBackground" android:orientation="vertical"> <ImageView android:id="@+id/imageView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:src="@drawable/ic_bluetooth_blue" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv_device_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:ellipsize="end" android:singleLine="true" android:text="设备名称" android:textColor="@color/black" android:textSize="16sp" app:layout_constraintStart_toEndOf="@+id/imageView2" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv_mac_address" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:ellipsize="end" android:singleLine="true" android:text="Mac地址" android:textSize="12sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="@+id/tv_device_name" app:layout_constraintTop_toBottomOf="@+id/tv_device_name" /> <TextView android:id="@+id/tv_rssi" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:text="信号强度" android:textSize="12sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
这里的内容不多,主要内容就是设备名称、地址、信号强度,下面我们创建适配器,在com.llw.goodble
包下新建一个adapter包,该包下新建一个OnItemClickListener
接口,用于实现Item的点击监听,代码如下所示:
interface OnItemClickListener { fun onItemClick(view: View?, position: Int) }
下面我们写适配器,在adapter包下新建一个BleDeviceAdapter
类,代码如下所示:
class BleDeviceAdapter( private val mDevices: List<BleDevice> ) : RecyclerView.Adapter<BleDeviceAdapter.ViewHolder>() { private var mOnItemClickListener: OnItemClickListener? = null fun setOnItemClickListener(mOnItemClickListener: OnItemClickListener?) { this.mOnItemClickListener = mOnItemClickListener } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val viewHolder = ViewHolder(ItemDeviceRvBinding.inflate(LayoutInflater.from(parent.context), parent, false)) viewHolder.binding.itemDevice.setOnClickListener { v -> if (mOnItemClickListener != null) mOnItemClickListener!!.onItemClick(v, viewHolder.adapterPosition) } return viewHolder } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val bleDevice: BleDevice = mDevices[position] val rssi: Int = bleDevice.rssi holder.binding.tvRssi.text = String.format(Locale.getDefault(), "%d dBm", rssi) //设备名称 holder.binding.tvDeviceName.text = bleDevice.realName //Mac地址 holder.binding.tvMacAddress.text = bleDevice.macAddress } override fun getItemCount() = mDevices.size class ViewHolder(itemView: ItemDeviceRvBinding) : RecyclerView.ViewHolder(itemView.root) { var binding: ItemDeviceRvBinding init { binding = itemView } } }
这里就是基本的写法,结合了ViewBinding
,在onBindViewHolder()
中进行数据渲染,那么适配器就写好了,下面我们回到ScanActivity中,去完成后的扫描结果显示。
② 扫描结果处理
首先我们声明变量,在ScanActivity中增加如下代码:
private var mAdapter: BleDeviceAdapter? = null //设备列表 private val mList: MutableList<BleDevice> = mutableListOf() private fun findIndex(bleDevice: BleDevice, mList: MutableList<BleDevice>): Int { var index = 0 for (devi in mList) { if (bleDevice.macAddress.contentEquals(devi.macAddress)) return index index += 1 } return -1 }
这个findIndex()函数用于在列表中找是否有添加过设备,下面修改扫描的回调函数onScanResult()
,代码如下所示:
override fun onScanResult(result: ScanResult) { if (result.scanRecord!!.deviceName == null) return if (result.scanRecord!!.deviceName!!.isEmpty()) return val bleDevice = BleDevice( result.scanRecord!!.deviceName, result.device.address, result.rssi, result.device ) Log.d(TAG, "onScanResult: ${bleDevice.macAddress}") if (mList.size == 0) { mList.add(bleDevice) } else { val index = findIndex(bleDevice, mList) if (index == -1) { //添加新设备 mList.add(bleDevice) } else { //更新已有设备的rssi mList[index].rssi = bleDevice.rssi } } //如果未扫描到设备,则显示空内容布局 binding.emptyLay.root.visibility = if (mList.size == 0) View.VISIBLE else View.GONE //如果mAdapter为空则会执行run{}中的代码,进行相关配置,最终返回配置的结果mAdapter mAdapter ?: run { mAdapter = BleDeviceAdapter(mList) binding.rvDevice.apply { (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false layoutManager = LinearLayoutManager(this@ScanActivity) adapter = mAdapter } mAdapter!!.setOnItemClickListener(this@ScanActivity) mAdapter } mAdapter!!.notifyDataSetChanged() }
那么在开始扫描的时候我们最好清理一下列表,修改一下startScan()函数,代码如下所示:
private fun startScan() { mList.clear() mAdapter?.notifyDataSetChanged() bleCore.startScan() binding.tvScanStatus.text = "停止" binding.pbScanLoading.visibility = View.VISIBLE }
同时在扫描回调中还有一个适配器的Item点击监听,先实现它,修改代码:
class ScanActivity : BaseActivity(), View.OnClickListener, BleScanCallback, ReceiverCallback, OnItemClickListener {
重写onItemClick()
函数,代码如下:
override fun onItemClick(view: View?, position: Int) { if (bleCore.isScanning()) stopScan() //选中设备处理 val intent = Intent() intent.putExtra("device", mList[position].device) setResult(RESULT_OK, intent) finish() }
我们是通过MainActivity进入到ScanActivity的,那么在选中设备之后将设备对象返回并销毁当前页面。ScanActivity中还有最后一个修改的地方,那就是在onResume()函数中增加开始扫描的代码,代码如下所示:
override fun onResume() { ... //开始扫描 if (!bleCore.isScanning()) startScan() }
这里的意思就是当进入页面检查到条件都满足时就开始扫描。
③ 接收结果
最后我们在MainActivity中接收结果,修改代码如下所示:
class MainActivity : BaseActivity() { private val binding by viewBinding(ActivityMainBinding::inflate) @SuppressLint("MissingPermission") private val scanIntent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { if (result.data == null) return@registerForActivityResult //获取选中的设备 val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { result.data!!.getParcelableExtra("device", BluetoothDevice::class.java) } else { result.data!!.getParcelableExtra("device") as BluetoothDevice? } showMsg("${device?.name} , ${device?.address}") } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) binding.toolbar.setNavigationOnClickListener { scanIntent.launch(Intent(this,ScanActivity::class.java)) } } }
下面我们运行一下:
六、源码
如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~
源码地址:GoodBle