当前位置: 首页 > news >正文

Android BLE 蓝牙扫描完全指南:使用 RxAndroidBle框架

📋 前言

在现代移动应用开发中,蓝牙低功耗(BLE)技术广泛应用于物联网设备、健康监测、智能家居等领域。本文将带你从零开始,完整实现一个 Android BLE 扫描功能。

🛠️ 1. 环境配置

添加依赖

在 app/build.gradle 中添加:

dependencies {// RxAndroidBle 核心库implementation "com.polidea.rxandroidble3:rxandroidble:1.17.2"// RxJava 3implementation "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? = nullprivate val scanResultsSubject = PublishSubject.create<ScanResult>()private val scannedDevices = mutableMapOf<String, ScanResult>()private var isScanning = false// 扫描状态回调var onScanStateChanged: ((Boolean) -> Unit)? = nullvar onDeviceFound: ((ScanResult) -> Unit)? = nullvar 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 = trueonScanStateChanged?.invoke(true)scanDisposable = rxBleClient.scanBleDevices(scanSettings).subscribe({ scanResult ->handleScanResult(scanResult)},{ error ->handleScanError(error)})}/*** 处理扫描结果*/private fun handleScanResult(scanResult: ScanResult) {val macAddress = scanResult.bleDevice.macAddressscannedDevices[macAddress] = scanResult// 通知监听器onDeviceFound?.invoke(scanResult)scanResultsSubject.onNext(scanResult)}/*** 处理扫描错误*/private fun handleScanError(error: Throwable) {isScanning = falseonScanStateChanged?.invoke(false)onScanError?.invoke(error)scanResultsSubject.onError(error)}/*** 停止扫描*/fun stopScan() {scanDisposable?.dispose()scanDisposable = nullisScanning = falseonScanStateChanged?.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 trueif (javaClass != other?.javaClass) return falseother as BleDeviceInforeturn 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.nametxtMac.text = device.macAddresstxtRssi.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.sizefun addOrUpdateDevice(device: BleDeviceInfo) {val existingIndex = devices.indexOfFirst { it.macAddress == device.macAddress }if (existingIndex != -1) {devices[existingIndex] = devicenotifyItemChanged(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: BleScannerprivate lateinit var deviceAdapter: DeviceAdapterprivate var resultsDisposable: Disposable? = nulloverride 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 = deviceAdapterlayoutManager = 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 = !isScanningbtnStopScan.isEnabled = isScanningprogressBar.visibility = if (isScanning) View.VISIBLE else View.GONEtxtStatus.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"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:gravity="center_vertical"><Buttonandroid:id="@+id/btnStartScan"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="开始扫描"android:layout_marginEnd="8dp"/><Buttonandroid: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"/><Buttonandroid:id="@+id/btnClear"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="清空结果"/></LinearLayout><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:gravity="center_vertical"android:layout_marginTop="16dp"><ProgressBarandroid:id="@+id/progressBar"android:layout_width="24dp"android:layout_height="24dp"android:visibility="gone"/><TextViewandroid: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"/><TextViewandroid:id="@+id/txtDeviceCount"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="设备数量: 0"android:textSize="14sp"/></LinearLayout><androidx.recyclerview.widget.RecyclerViewandroid: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"><TextViewandroid:id="@+id/txtDeviceName"android:layout_width="match_parent"android:layout_height="wrap_content"android:textSize="18sp"android:textStyle="bold"/><TextViewandroid:id="@+id/txtDeviceMac"android:layout_width="match_parent"android:layout_height="wrap_content"android:textSize="14sp"android:layout_marginTop="4dp"/><TextViewandroid: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 = 0bleScanner.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> = _scanStateinit {bleScanner.onScanStateChanged = { isScanning ->_scanState.postValue(isScanning)}}override fun onCleared() {super.onCleared()bleScanner.release()}
}

🎯 总结

本文完整介绍了如何使用 RxAndroidBle 实现 Android BLE 扫描功能,包括:

  1. 环境配置和依赖添加
  2. 权限管理和检查
  3. 核心扫描功能实现
  4. UI 界面和列表展示
  5. 错误处理和最佳实践

这个实现提供了完整的蓝牙扫描解决方案,可以直接用于生产环境,也可以根据具体需求进行扩展和定制。

优点:

· 响应式编程,代码简洁
· 完整的错误处理
· 自动设备去重
· 灵活的过滤功能
· 良好的生命周期管理

希望这篇指南对你的博客写作有所帮助!


文章转载自:

http://Pp0ReOJJ.kbbmj.cn
http://2grdlZgG.kbbmj.cn
http://h14Oq8BG.kbbmj.cn
http://DF2RSML0.kbbmj.cn
http://g2l3tZK3.kbbmj.cn
http://TPOcqhJV.kbbmj.cn
http://wJ2F9Muo.kbbmj.cn
http://lwvPnIH8.kbbmj.cn
http://9caSOC7G.kbbmj.cn
http://pCVcLV6o.kbbmj.cn
http://UNsrhFxj.kbbmj.cn
http://Ef9R2LXn.kbbmj.cn
http://P0YhseKk.kbbmj.cn
http://qFpdFCzJ.kbbmj.cn
http://hTxtBpij.kbbmj.cn
http://rpokioUM.kbbmj.cn
http://2C9RFUlB.kbbmj.cn
http://DiDJySD2.kbbmj.cn
http://mQNf2ugX.kbbmj.cn
http://ntkvVn8j.kbbmj.cn
http://zLEteGGT.kbbmj.cn
http://YZIOUek8.kbbmj.cn
http://jbmVjcyh.kbbmj.cn
http://v6GNpzRV.kbbmj.cn
http://4zmx89w5.kbbmj.cn
http://TIAf5ijt.kbbmj.cn
http://ZtusKhZv.kbbmj.cn
http://p7gaUVEW.kbbmj.cn
http://oTnBLrUw.kbbmj.cn
http://qGxDbg6E.kbbmj.cn
http://www.dtcms.com/a/382855.html

相关文章:

  • CKS-CN 考试知识点分享(3)---Dockerfile 安全最佳实践
  • 新一代控制理论框架:人机环境系统控制论
  • easyPoi实现动表头Excel的导入和导出
  • 【Zephyr电源与功耗专题】13_PMU电源驱动介绍
  • Coze源码分析-资源库-创建知识库-后端源码-应用/领域/数据访问
  • React Server Components (RSC) 与 App Router 简介:Next.js 的未来范式
  • 状态机SMACH相关教程介绍与应用案例分析——机器人操作进阶系列之一
  • Grafana与Prometheus实战
  • godot+c#操作godot-sqlite并加解密
  • Scikit-learn 机器学习:构建、训练与评估预测模型
  • React学习教程,从入门到精通,React 组件核心语法知识点详解(类组件体系)(19)
  • Java分布式编程:RMI机制
  • 5-12 WPS JS宏 Range数组规范性测试
  • MySQL 的安装、启动、连接(Windows、macOS 和 Linux)
  • (附源码)基于Spring Boot的宿舍管理系统设计
  • Mac下Python3安装
  • C++数组与字符串:从基础到实战技巧
  • 第13课:分布式Agent系统
  • Docker 容器化部署核心实战——Nginx 服务配置与正反向代理原理解析
  • 【分享】中小学教材课本 PDF 资源获取指南
  • 如何用 Git Hook 和 CI 流水线为 FastAPI 项目保驾护航?
  • 安卓旋转屏幕后如何防止数据丢失-ViewModel入门
  • STM32_05_时钟树
  • 元宇宙与体育产业:沉浸式体验重构体育全链条生态
  • LeetCode 每日一题 966. 元音拼写检查器
  • C++密码锁 2023年CSP-S认证真题 CCF信息学奥赛C++ 中小学提高组 第二轮真题解析
  • Vue3 视频播放器完整指南 – @videojs-player/vue 从入门到精通
  • 零售企业数字化转型的道、法、术:基于开源AI大模型AI智能名片S2B2C商城小程序的战略重构
  • 【编号500】(道路分类)广东路网数据广东路网分类数据(2025年)
  • 【PHP7内核剖析】-1.3 FPM