📋 前言
在现代移动应用开发中,蓝牙低功耗(BLE)技术广泛应用于物联网设备、健康监测、智能家居等领域。本文将带你从零开始,完整实现一个 Android BLE 扫描功能。
🛠️ 1. 环境配置
添加依赖
在 app/build.gradle 中添加:
dependencies {
// RxAndroidBle 核心库
implementation "com.polidea.rxandroidble3:rxandroidble:1.17.2"
// RxJava 3
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
// 可选:Lifecycle 用于更好的生命周期管理
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
}
权限配置
在 AndroidManifest.xml 中添加:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- Android 12+ 需要以下权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 如果需要定位功能 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
🎯 2. 权限工具类
// PermissionUtils.kt
object PermissionUtils {
/**
* 检查蓝牙权限
*/
@SuppressLint("InlinedApi")
fun checkBluetoothPermissions(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
} else {
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
}
/**
* 获取需要的权限数组
*/
@SuppressLint("InlinedApi")
fun getRequiredPermissions(): Array<String> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
} else {
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
}
}
/**
* 请求权限
*/
fun requestBluetoothPermissions(activity: Activity, requestCode: Int) {
val permissions = getRequiredPermissions()
ActivityCompat.requestPermissions(activity, permissions, requestCode)
}
}
🔧 3. 蓝牙扫描工具类
// BleScanner.kt
class BleScanner(private val context: Context) {
private val rxBleClient: RxBleClient by lazy { RxBleClient.create(context) }
private var scanDisposable: Disposable? = null
private val scanResultsSubject = PublishSubject.create<ScanResult>()
private val scannedDevices = mutableMapOf<String, ScanResult>()
private var isScanning = false
// 扫描状态回调
var onScanStateChanged: ((Boolean) -> Unit)? = null
var onDeviceFound: ((ScanResult) -> Unit)? = null
var onScanError: ((Throwable) -> Unit)? = null
/**
* 开始扫描
*/
@SuppressLint("MissingPermission")
fun startScan(scanMode: Int = ScanSettings.SCAN_MODE_LOW_LATENCY) {
if (!PermissionUtils.checkBluetoothPermissions(context)) {
onScanError?.invoke(SecurityException("缺少蓝牙扫描权限"))
return
}
stopScan()
val scanSettings = ScanSettings.Builder()
.setScanMode(scanMode)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.build()
isScanning = true
onScanStateChanged?.invoke(true)
scanDisposable = rxBleClient.scanBleDevices(scanSettings)
.subscribe(
{ scanResult ->
handleScanResult(scanResult)
},
{ error ->
handleScanError(error)
}
)
}
/**
* 处理扫描结果
*/
private fun handleScanResult(scanResult: ScanResult) {
val macAddress = scanResult.bleDevice.macAddress
scannedDevices[macAddress] = scanResult
// 通知监听器
onDeviceFound?.invoke(scanResult)
scanResultsSubject.onNext(scanResult)
}
/**
* 处理扫描错误
*/
private fun handleScanError(error: Throwable) {
isScanning = false
onScanStateChanged?.invoke(false)
onScanError?.invoke(error)
scanResultsSubject.onError(error)
}
/**
* 停止扫描
*/
fun stopScan() {
scanDisposable?.dispose()
scanDisposable = null
isScanning = false
onScanStateChanged?.invoke(false)
}
/**
* 获取扫描结果的Observable
*/
fun getScanResultsObservable(): Observable<ScanResult> {
return scanResultsSubject
}
/**
* 获取所有扫描到的设备
*/
fun getAllDevices(): List<ScanResult> {
return scannedDevices.values.toList()
}
/**
* 根据条件过滤设备
*/
fun filterDevices(
name: String? = null,
minRssi: Int? = null,
maxRssi: Int? = null
): List<ScanResult> {
return scannedDevices.values.filter { device ->
(name == null || device.bleDevice.name?.contains(name, true) == true) &&
(minRssi == null || device.rssi >= minRssi) &&
(maxRssi == null || device.rssi <= maxRssi)
}
}
/**
* 清空扫描结果
*/
fun clearResults() {
scannedDevices.clear()
}
/**
* 检查是否正在扫描
*/
fun isScanning(): Boolean = isScanning
/**
* 获取设备数量
*/
fun getDeviceCount(): Int = scannedDevices.size
/**
* 释放资源
*/
fun release() {
stopScan()
scanResultsSubject.onComplete()
}
}
📱 4. 设备信息数据类
// BleDeviceInfo.kt
data class BleDeviceInfo(
val name: String,
val macAddress: String,
val rssi: Int,
val scanRecord: ByteArray?,
val timestamp: Long = System.currentTimeMillis()
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BleDeviceInfo
return macAddress == other.macAddress
}
override fun hashCode(): Int {
return macAddress.hashCode()
}
}
🎨 5. RecyclerView 适配器
// DeviceAdapter.kt
class DeviceAdapter(
private val onDeviceClick: (BleDeviceInfo) -> Unit
) : RecyclerView.Adapter<DeviceAdapter.DeviceViewHolder>() {
private val devices = mutableListOf<BleDeviceInfo>()
inner class DeviceViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val txtName: TextView = itemView.findViewById(R.id.txtDeviceName)
private val txtMac: TextView = itemView.findViewById(R.id.txtDeviceMac)
private val txtRssi: TextView = itemView.findViewById(R.id.txtDeviceRssi)
fun bind(device: BleDeviceInfo) {
txtName.text = device.name
txtMac.text = device.macAddress
txtRssi.text = "信号: ${device.rssi}dBm"
itemView.setOnClickListener {
onDeviceClick(device)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_device, parent, false)
return DeviceViewHolder(view)
}
override fun onBindViewHolder(holder: DeviceViewHolder, position: Int) {
holder.bind(devices[position])
}
override fun getItemCount(): Int = devices.size
fun addOrUpdateDevice(device: BleDeviceInfo) {
val existingIndex = devices.indexOfFirst { it.macAddress == device.macAddress }
if (existingIndex != -1) {
devices[existingIndex] = device
notifyItemChanged(existingIndex)
} else {
devices.add(device)
notifyItemInserted(devices.size - 1)
}
}
fun clear() {
devices.clear()
notifyDataSetChanged()
}
fun getDevices(): List<BleDeviceInfo> = devices.toList()
}
📋 6. Activity/Fragment 实现
// MainActivity.kt
class MainActivity : AppCompatActivity() {
companion object {
private const val REQUEST_BLE_PERMISSIONS = 1001
}
private lateinit var bleScanner: BleScanner
private lateinit var deviceAdapter: DeviceAdapter
private var resultsDisposable: Disposable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initViews()
initBleScanner()
checkPermissions()
}
private fun initViews() {
deviceAdapter = DeviceAdapter { device ->
onDeviceSelected(device)
}
recyclerViewDevices.apply {
adapter = deviceAdapter
layoutManager = LinearLayoutManager(this@MainActivity)
addItemDecoration(DividerItemDecoration(this@MainActivity, DividerItemDecoration.VERTICAL))
}
btnStartScan.setOnClickListener { startScanning() }
btnStopScan.setOnClickListener { stopScanning() }
btnClear.setOnClickListener { clearResults() }
}
private fun initBleScanner() {
bleScanner = BleScanner(this)
bleScanner.onScanStateChanged = { isScanning ->
updateScanUI(isScanning)
}
bleScanner.onDeviceFound = { scanResult ->
val deviceInfo = convertToDeviceInfo(scanResult)
runOnUiThread {
deviceAdapter.addOrUpdateDevice(deviceInfo)
updateDeviceCount()
}
}
bleScanner.onScanError = { error ->
runOnUiThread {
showError("扫描错误: ${error.message}")
}
}
}
private fun convertToDeviceInfo(scanResult: ScanResult): BleDeviceInfo {
return BleDeviceInfo(
name = scanResult.bleDevice.name ?: "未知设备",
macAddress = scanResult.bleDevice.macAddress,
rssi = scanResult.rssi,
scanRecord = scanResult.scanRecord?.bytes
)
}
private fun checkPermissions() {
if (!PermissionUtils.checkBluetoothPermissions(this)) {
PermissionUtils.requestBluetoothPermissions(this, REQUEST_BLE_PERMISSIONS)
}
}
@SuppressLint("MissingPermission")
private fun startScanning() {
if (!PermissionUtils.checkBluetoothPermissions(this)) {
PermissionUtils.requestBluetoothPermissions(this, REQUEST_BLE_PERMISSIONS)
return
}
try {
bleScanner.startScan()
observeScanResults()
} catch (e: Exception) {
showError("启动扫描失败: ${e.message}")
}
}
private fun observeScanResults() {
resultsDisposable = bleScanner.getScanResultsObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ scanResult ->
val deviceInfo = convertToDeviceInfo(scanResult)
deviceAdapter.addOrUpdateDevice(deviceInfo)
updateDeviceCount()
}, { error ->
showError("扫描错误: ${error.message}")
})
}
private fun stopScanning() {
bleScanner.stopScan()
resultsDisposable?.dispose()
}
private fun clearResults() {
bleScanner.clearResults()
deviceAdapter.clear()
updateDeviceCount()
}
private fun onDeviceSelected(device: BleDeviceInfo) {
AlertDialog.Builder(this)
.setTitle("选择设备")
.setMessage("设备: ${device.name}\nMAC: ${device.macAddress}\n信号强度: ${device.rssi}dBm")
.setPositiveButton("连接") { _, _ ->
connectToDevice(device.macAddress)
}
.setNegativeButton("取消", null)
.show()
}
private fun connectToDevice(macAddress: String) {
// 这里实现设备连接逻辑
Toast.makeText(this, "连接设备: $macAddress", Toast.LENGTH_SHORT).show()
}
private fun updateScanUI(isScanning: Boolean) {
runOnUiThread {
btnStartScan.isEnabled = !isScanning
btnStopScan.isEnabled = isScanning
progressBar.visibility = if (isScanning) View.VISIBLE else View.GONE
txtStatus.text = if (isScanning) "扫描中..." else "扫描已停止"
}
}
private fun updateDeviceCount() {
txtDeviceCount.text = "设备数量: ${deviceAdapter.itemCount}"
}
private fun showError(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
updateScanUI(false)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_BLE_PERMISSIONS) {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
startScanning()
} else {
showError("需要蓝牙权限才能扫描")
}
}
}
override fun onDestroy() {
super.onDestroy()
bleScanner.release()
resultsDisposable?.dispose()
}
}
🎯 7. 布局文件
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<Button
android:id="@+id/btnStartScan"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="开始扫描"
android:layout_marginEnd="8dp"/>
<Button
android:id="@+id/btnStopScan"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="停止扫描"
android:layout_marginEnd="8dp"
android:enabled="false"/>
<Button
android:id="@+id/btnClear"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="清空结果"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="16dp">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:visibility="gone"/>
<TextView
android:id="@+id/txtStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="准备扫描"
android:layout_marginStart="8dp"
android:textSize="16sp"/>
<TextView
android:id="@+id/txtDeviceCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设备数量: 0"
android:textSize="14sp"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewDevices"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="16dp"/>
</LinearLayout>
item_device.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/txtDeviceName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/txtDeviceMac"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:layout_marginTop="4dp"/>
<TextView
android:id="@+id/txtDeviceRssi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:layout_marginTop="4dp"/>
</LinearLayout>
💡 8. 使用技巧和最佳实践
扫描模式选择
// 低功耗模式(省电,但扫描间隔长)
ScanSettings.SCAN_MODE_LOW_POWER
// 平衡模式
ScanSettings.SCAN_MODE_BALANCED
// 低延迟模式(快速响应,但耗电)
ScanSettings.SCAN_MODE_LOW_LATENCY
// 机会主义模式(最低功耗)
ScanSettings.SCAN_MODE_OPPORTUNISTIC
错误处理
// 添加重试机制
fun startScanWithRetry(maxRetries: Int = 3) {
var retryCount = 0
bleScanner.getScanResultsObservable()
.retryWhen { errors ->
errors.flatMap { error ->
if (++retryCount < maxRetries) {
Observable.timer(2, TimeUnit.SECONDS)
} else {
Observable.error(error)
}
}
}
.subscribe(...)
}
生命周期管理
// 在 ViewModel 中管理
class BleViewModel(application: Application) : AndroidViewModel(application) {
private val bleScanner = BleScanner(application)
// 使用 LiveData 暴露状态
private val _scanState = MutableLiveData<Boolean>()
val scanState: LiveData<Boolean> = _scanState
init {
bleScanner.onScanStateChanged = { isScanning ->
_scanState.postValue(isScanning)
}
}
override fun onCleared() {
super.onCleared()
bleScanner.release()
}
}
🎯 总结
本文完整介绍了如何使用 RxAndroidBle 实现 Android BLE 扫描功能,包括:
- 环境配置和依赖添加
- 权限管理和检查
- 核心扫描功能实现
- UI 界面和列表展示
- 错误处理和最佳实践
这个实现提供了完整的蓝牙扫描解决方案,可以直接用于生产环境,也可以根据具体需求进行扩展和定制。
优点:
· 响应式编程,代码简洁
· 完整的错误处理
· 自动设备去重
· 灵活的过滤功能
· 良好的生命周期管理
希望这篇指南对你的博客写作有所帮助!