一篇文带你掌握Android蓝牙开发!
大家好,我是皇叔,最近开了一个安卓进阶涨薪训练营,可以帮助大家突破技术&职场瓶颈,从而度过难关,进入心仪的公司。
详情见文章:没错!皇叔开了个训练营
作者:初学者-Study
https://llw-study.blog.csdn.net/?type=blog
前言
Android版本中蓝牙简介
Android1.5 中增加了蓝牙功能,立体声 Bluetooth 支持:A2DP [Advanced Audio Distribution Profile]、AVCRP [Audio/Video Remote Control Profile],自动配对。 Android2.0 中支持Bluetooth2.1协议。 Android3.0 中能让应用查询已经连接上 Bluetooth 设备的 Bluetooth Profile、音频状态等,然后通知用户。 Android3.1 中系统可以通过 Bluetooth HID 方式同时接入一到多款输入设备。 Android4.0 中新增支持连接 Bluetooth HDP [Health Device Profile)] 设备,通过第三方应用的支持,用户可以连接到医院、健身中心或者家庭等场合中的无线医疗设备和传感器。 Android4.2 中引入了一种新的针对 Android 设备优化的 Bluetooth 协议栈 BlueDroid,从而取代 BlueZ 协议栈。Bluedroid 协议栈由 Google 和 Broadcom 公司共同开发,相对于 BlueZ 协议栈,BlueDroid 提升了兼容性和可靠性。 Android4.3 中增加了对低功耗蓝牙的支持,内置支持 Bluetooth AVRCP 1.3,基于 Google 和 Broadcom 公司功能研发的针对于 Android 设备优化的新的蓝牙协议栈 BlueDroid。 Android4.4 中新增两种新 Proifle 支持:HID [Human Interface Device]、MAP [Message Access Profile] Android5.0 中支持Bluetooth4.1协议。 Android6.0 中扫描蓝牙需要动态获取定位才行。 Android7.0 中支持Bluetooth4.2协议。 Android8.0 中支持Bluetooth5.0协议,强化了蓝牙音频的表现。比如编码/传输格式可选SBC、AAC、aptX/aptX HD、LDAC等四种,音质依次提高。 Android10.0 中支持Bluetooth5.1协议,在5.0的基础上,增加了侧向功能和厘米级定位服务,大幅度提高了定位精度。使室内定位更精准。 Android11.0 中支持Bluetooth5.2协议,增强版ATT协议,LE功耗控制和信号同步,连接更快,更稳定,抗干扰性更好。 Android12.0 中支持Bluetooth5.3协议,增强了经典蓝牙BR/EDR(基础速率和增强速率)的安全性。蓝牙5.3的延迟更低、抗干扰性更强、提升了电池续航时间。系统引入了新的运行时权限 BLUETOOTH_SCAN、BLUETOOTH_ADVERTISE 和 BLUETOOTH_CONNECT权限,用于更好地管理应用于附近蓝牙设备的连接。
新建项目
maven { url "https://jitpack.io" }
然后在app的build.gradle中增加。
buildFeatures {
viewBinding true
dataBinding true
}
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'
然后Sync Now。
配置AndroidManifest.xml
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<!--Android12 的蓝牙权限 如果您的应用与已配对的蓝牙设备通信或者获取当前手机蓝牙是否打开-->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<!--Android12 的蓝牙权限 如果您的应用查找蓝牙设备(如蓝牙低功耗 (BLE) 外围设备)-->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<!--Android12 的蓝牙权限 如果您的应用使当前设备可被其他蓝牙设备检测到-->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
打开蓝牙
<?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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_open_bluetooth"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="10dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:text="打开蓝牙"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
private fun isOpenBluetooth(): Boolean {
val manager = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
val adapter = manager.adapter ?: return false
return adapter.isEnabled
}
private fun isAndroid12() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
private fun hasPermission(permission: String) =
checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
private fun showMsg(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
打开蓝牙意图
//打开蓝牙意图
val enableBluetooth = registerForActivityResult(StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
showMsg(if (isOpenBluetooth()) "蓝牙已打开" else "蓝牙未打开")
}
}
//请求BLUETOOTH_CONNECT权限意图
val requestBluetoothConnect = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
//打开蓝牙
enableBluetooth.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
} else {
showMsg("Android12中未获取此权限,则无法打开蓝牙。")
}
}
enableBluetooth.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
这里有一个initView的函数,在这个函数中我们对按钮的点击事件进行操作,新增initView()函数,代码如下:
private fun initView() {
binding.btnOpenBluetooth.setOnClickListener {
//蓝牙是否已打开
if (isOpenBluetooth()){
showMsg("蓝牙已打开")
return@setOnClickListener
}
//是Android12
if (isAndroid12()) {
//检查是否有BLUETOOTH_CONNECT权限
if (hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) {
//打开蓝牙
enableBluetooth.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
} else {
//请求权限
requestBluetoothConnect.launch(Manifest.permission.BLUETOOTH_CONNECT)
}
return@setOnClickListener
}
//不是Android12 直接打开蓝牙
enableBluetooth.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
}
}
蓝牙扫描
private val TAG = MainActivity::class.java.simpleName
//获取系统蓝牙适配器
private lateinit var mBluetoothAdapter: BluetoothAdapter
//扫描者
private lateinit var scanner: BluetoothLeScanner
//是否正在扫描
var isScanning = false
扫描回调
//扫描结果回调
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
Log.d(TAG, "name: ${device.name}, address: ${device.address}")
}
}
@SuppressLint("MissingPermission")
这个注解加上去之后你需要小心蓝牙权限的问题。
扫描方法
private fun startScan() {
if (!isScanning) {
scanner.startScan(scanCallback)
isScanning = true
binding.btnScanBluetooth.text = "停止扫描"
}
}
private fun stopScan() {
if (isScanning) {
scanner.stopScan(scanCallback)
isScanning = false
binding.btnScanBluetooth.text = "扫描蓝牙"
}
}
执行扫描
//请求BLUETOOTH_SCAN权限意图
private val requestBluetoothScan =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
//进行扫描
startScan()
} else {
showMsg("Android12中未获取此权限,则无法扫描蓝牙。")
}
}
//扫描蓝牙
binding.btnScanBluetooth.setOnClickListener {
if (isAndroid12()) {
if (hasPermission(Manifest.permission.BLUETOOTH_SCAN)) {
//扫描或者停止扫描
if (isScanning) stopScan() else startScan()
} else {
//请求权限
requestBluetoothScan.launch(Manifest.permission.BLUETOOTH_SCAN)
}
}
}
页面显示扫描设备
<?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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_open_bluetooth"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="10dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:text="打开蓝牙"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_scan_bluetooth"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="10dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:text="扫描蓝牙"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_open_bluetooth" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_device"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="20dp"
android:overScrollMode="never"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_scan_bluetooth" />
</androidx.constraintlayout.widget.ConstraintLayout>
蓝牙设备适配器
这个里的适配器使我们自己去写的,需要显示数据的,首先我们需要创建一个蓝牙图标,在drawable包下新建一个icon_bluetooth.xml,里面的代码如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:autoMirrored="true"
android:tint="#000000"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M14.24,12.01l2.32,2.32c0.28,-0.72 0.44,-1.51 0.44,-2.33 0,-0.82 -0.16,-1.59 -0.43,-2.31l-2.33,2.32zM19.53,6.71l-1.26,1.26c0.63,1.21 0.98,2.57 0.98,4.02s-0.36,2.82 -0.98,4.02l1.2,1.2c0.97,-1.54 1.54,-3.36 1.54,-5.31 -0.01,-1.89 -0.55,-3.67 -1.48,-5.19zM15.71,7.71L10,2L9,2v7.59L4.41,5 3,6.41 8.59,12 3,17.59 4.41,19 9,14.41L9,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM11,5.83l1.88,1.88L11,9.59L11,5.83zM12.88,16.29L11,18.17v-3.76l1.88,1.88z" />
</vector>
data class MyDevice(val device: BluetoothDevice, var rssi: Int)
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="device"
type="com.llw.bluetooth.MyDevice" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="70dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:padding="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/icon_bluetooth" />
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="10dp"
android:text="@{device.device.name ?? `Unknown` }"
android:textColor="@color/black"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/tv_rssi"
app:layout_constraintStart_toEndOf="@+id/imageView"
app:layout_constraintTop_toTopOf="@+id/imageView" />
<TextView
android:id="@+id/tv_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="4dp"
android:text="@{device.device.address}"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="@+id/imageView"
app:layout_constraintEnd_toStartOf="@+id/tv_rssi"
app:layout_constraintStart_toEndOf="@+id/imageView" />
<TextView
android:id="@+id/tv_rssi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@{device.rssi+`dBm`}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
class MyDeviceAdapter(data: MutableList<MyDevice>) :
BaseQuickAdapter<MyDevice, BaseDataBindingHolder<ItemDeviceBinding>>(R.layout.item_device, data) {
override fun convert(holder: BaseDataBindingHolder<ItemDeviceBinding>, item: MyDevice) {
holder.dataBinding?.apply {
device = item
executePendingBindings()
}
}
}
显示列表设备
//设备列表
private val deviceList = mutableListOf<MyDevice>()
//适配器
private lateinit var myDeviceAdapter: MyDeviceAdapter
private fun findDeviceIndex(scanDevice: MyDevice, deviceList: List<MyDevice>): Int {
var index = 0
for (device in deviceList) {
if (scanDevice.device.address.equals(device.device.address)) return index
index += 1
}
return -1
}
private fun addDeviceList(device: MyDevice) {
val index = findDeviceIndex(device, deviceList)
if (index == -1) {
Log.d(TAG, "name: ${device.device.name}, address: ${device.device.address}")
deviceList.add(device)
myDeviceAdapter.notifyDataSetChanged()
} else {
deviceList[index].rssi = device.rssi
myDeviceAdapter.notifyItemChanged(index)
}
}
适配Android12以下设备
<!--Android6-11 定位权限-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
//请求定位权限意图
private val requestLocation =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
//扫描蓝牙
startScan()
} else {
showMsg("Android12以下,6及以上需要定位权限才能扫描设备")
}
}
https://github.com/lilongweidev/Android12Bluetooth
为了失联,欢迎关注我防备的小号
微信改了推送机制,真爱请星标本公号👇
评论