tools:context=“.MainActivity”>
<ProgressBar
android:id=“@+id/progress_bar”
style=“@style/Widget.AppCompat.ProgressBar.Horizontal”
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:indeterminate=“true”
android:visibility=“invisible” />
<androidx.recyclerview.widget.RecyclerView
android:id=“@+id/rv_device”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:layout_below=“@+id/progress_bar”
android:overScrollMode=“never”
android:scrollbars=“none” />
<LinearLayout
android:id=“@+id/lay_no_equipment”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:layout_below=“@+id/progress_bar”
android:gravity=“center”
android:orientation=“vertical”>
<ImageView
android:layout_width=“60dp”
android:layout_height=“60dp”
android:layout_marginBottom=“12dp”
android:src=“@drawable/ic_widgets” />
<TextView
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:text=“空空如也~” />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id=“@+id/fab_add”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:layout_alignParentEnd=“true”
android:layout_alignParentBottom=“true”
android:layout_margin=“20dp”
android:text=“Scanning”
android:textColor=“@color/white”
app:elevation=“6dp”
app:fabSize=“normal”
app:icon=“@drawable/ic_add_white”
app:iconTint=“@color/white” />

这里面有两个图标,代码如下:

ic_widget.xml
<vector xmlns:android=“http://schemas.android.com/apk/res/android”
android:width=“24dp”
android:height=“24dp”
android:alpha=“0.6”
android:viewportWidth=“24.0”
android:viewportHeight=“24.0”>
<path
android:fillColor=“#FF000000”
android:pathData=“M13,13v8h8v-8h-8zM3,21h8v-8L3,13v8zM3,3v8h8L11,3L3,3zM16.66,1.69L11,7.34 16.66,13l5.66,-5.66 -5.66,-5.65z” />
ic_add.xml
<vector xmlns:android=“http://schemas.android.com/apk/res/android”
android:width=“24dp”
android:height=“24dp”
android:viewportWidth=“24.0”
android:viewportHeight=“24.0”>
<path
android:fillColor=“#FFFFFFFF”
android:pathData=“M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z”/>

下面写扫描到的列表适配器布局文件,在layout下新建一个item_bluetooth.xml,里面的代码如下:

<?xml version="1.0" encoding="utf-8"?> 
<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:background=“@color/white”
android:foreground=“?attr/selectableItemBackground”
android:orientation=“vertical”>
<LinearLayout
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:gravity=“center_vertical”
android:padding=“16dp”>
<ImageView
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:src=“@drawable/ic_bluetooth” />
<LinearLayout
android:layout_width=“0dp”
android:layout_height=“wrap_content”
android:layout_weight=“1”
android:orientation=“vertical”
android:paddingStart=“12dp”>
<TextView
android:id=“@+id/tv_device_name”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:ellipsize=“end”
android:singleLine=“true”
android:text=“设备名称”
android:textColor=“@color/black”
android:textSize=“16sp” />
<TextView
android:id=“@+id/tv_mac_address”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:layout_marginTop=“8dp”
android:ellipsize=“end”
android:singleLine=“true”
android:text=“Mac地址” />
<TextView
android:id=“@+id/tv_rssi”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:text=“信号强度” />
<View
android:layout_width=“match_parent”
android:layout_height=“0.5dp”
android:background=“#EEE” />

这里也有一个蓝牙图标的ic_bluetooth.xml,如下:

<?xml version="1.0" encoding="UTF-8" standalone="no"?> 
<vector xmlns:android=“http://schemas.android.com/apk/res/android”
android:width=“36dp”
android:height=“36dp”
android:autoMirrored=“true”
android:tint=“@color/green”
android:viewportWidth=“24.0”
android:viewportHeight=“24.0”>
<path
android:fillColor=“@android:color/white”

android:pathData=“M14.58,12.36l1.38,1.38c0.28,0.28 0.75,0.14 0.84,-0.24c0.12,-0.48 0.18,-0.99 0.18,-1.5c0,-0.51 -0.06,-1.01 -0.18,-1.48c-0.09,-0.38 -0.56,-0.52 -0.84,-0.24l-1.39,1.38C14.39,11.85 14.39,12.17 14.58,12.36zM18.72,7.51l-0.05,0.05c-0.25,0.25 -0.3,0.62 -0.16,0.94c0.47,1.07 0.73,2.25 0.73,3.49c0,1.24 -0.26,2.42 -0.73,3.49c-0.14,0.32 -0.09,0.69 0.16,0.94l0,0c0.41,0.41 1.1,0.29 1.35,-0.23c0.63,-1.3 0.98,-2.76 0.98,-4.3c-0.01,-1.48 -0.34,-2.89 -0.93,-4.16C19.83,7.22 19.13,7.1 18.72,7.51zM15,7l-4.79,-4.79C10.07,2.07 9.89,2 9.71,2h0C9.32,2 9,2.32 9,2.71v6.88L5.12,5.7c-0.39,-0.39 -1.02,-0.39 -1.41,0l0,0c-0.39,0.39 -0.39,1.02 0,1.41L8.59,12l-4.89,4.89c-0.39,0.39 -0.39,1.02 0,1.41h0c0.39,0.39 1.02,0.39 1.41,0L9,14.41v6.88C9,21.68 9.32,22 9.71,22h0c0.19,0 0.37,-0.07 0.5,-0.21L15,17c0.39,-0.39 0.39,-1.02 0,-1.42L11.41,12L15,8.42C15.39,8.03 15.39,7.39 15,7zM11,5.83l1.88,1.88L11,9.59V5.83zM12.88,16.29L11,18.17v-3.76L12.88,16.29z” />

设备扫描页面就差不多了,下面进行这个页面的代码编写。

三、扫描设备


首先想清楚扫描之前要做什么,扫描之后要做什么。扫描之前要判断Android版本,6.0及以上需要动态请求权限,请求之后要判断蓝牙是否打开,蓝牙打开权限也有了就可以点击扫描蓝牙开始扫描了,扫描时显示加载条表示正在扫描,扫描到设备后添加到列表中,页面上渲染出来。当点击一个设备时连接这个设备,然后就是连接设备后的数据交互了,先写现在的业务逻辑。

① 绑定视图

先进行视图绑定,activity_main.xml 对应的就是ActivityMainBinding。由ViewBinding根据布局生成的

//视图绑定
private lateinit var binding: ActivityMainBinding
然后在onCreate中进行绑定
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}

② 检查Android版本

当进入页面是检查版本

/**
• Android版本
*/
private fun checkAndroidVersion() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) requestPermission() else openBluetooth()

这里的语法就是Kotlin的语法,等价于Java中的如下代码。

private fun checkAndroidVersion() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermission()
} else {
openBluetooth()
}
}

当你用Kotlin时间越久你就越觉得Kotlin设计的好,非常的简洁。当然最主要的是多使用Kotlin,作为弱类型语言,代码的阅读需要有一定的Kotlin基础才可以,高阶的写法可读性很差,但是效率很高代码也很简洁。后面我就直接写Kotlin代码,不熟悉的可以留言提问,事先声明我的Kotlin很菜,所以可读性相对来说高一些。

从上面的方法中可以知道逻辑就是Android6.0以上就请求权限,以下就打开蓝牙。这两个方法现在还都没有的,先写打开蓝牙的方法。

③ 打开蓝牙

//默认蓝牙适配器
private var defaultAdapter = BluetoothAdapter.getDefaultAdapter()
/**
• 打开蓝牙
*/
private fun openBluetooth() = defaultAdapter.let {
if (it.isEnabled) showMsg(“蓝牙已打开,可以开始扫描设备了”) else activityResult.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
}

这个方法中主要就是当蓝牙开发未打开的时候,通过Intent去打开系统蓝牙,注意这一行代码:

activityResult.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))

在Android高版本中弃用了startActivityForResult,改用registerForActivityResult。使用此方法需要在onCreate之前进行初始化。

//注册开启蓝牙 注意在onCreate之前注册
private val activityResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) showMsg(if (defaultAdapter.isEnabled) “蓝牙已打开” else “蓝牙未打开”)
}

这里的showMsg代码如下:

/**
• Toast提示
*/
private fun showMsg(msg: String = “权限未通过”) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()

④ 请求权限

/**
• 请求权限
*/
private fun requestPermission() =
PermissionX.init(this).permissions(Manifest.permission.ACCESS_FINE_LOCATION)
.request { allGranted, _, _ -> if (allGranted) openBluetooth() else showMsg() }

初始阶段完成了,最终在onCreate方法中调用

android 蓝牙ble配对流程_kotlin

当权限同意之后就打开蓝牙,如果都打开了就可以开始进行扫描蓝牙的操作了,在扫描之后先要确定蓝牙设备需要什么信息。

⑤ 扫描结果

现在前期的准备工作就做好了,那么下面就是点击扫描按钮进行蓝牙设备的扫描了。

//扫描结果回调
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
}
}

看这段代码相对于Java的区别还是很大的,不过返回的结果值是一样的,然后就是触发回调的地方,这里容我一会儿再写这个开始扫描和停止扫描的方法,因为这两个方法牵扯到的内容比较多,需要控制数据、视图、业务逻辑。因此等先把数据展示出来再去进行这个扫描的开始和结束的操作方法的编写。

⑥ 设备适配器编写

首先我们要定义一个设备类,用来存放扫描到的结果,在Kotlin中有一个数据类,来做这个事情,新建一个BleDevice,代码如下:

data class BleDevice(var device:BluetoothDevice, var rssi:Int, var name:String?)

扫描毫无疑问肯定要展示数据在页面上的。然后就需要一个视图来显示数据,之前创建了item的xml文件,现在我们需要写一个适配器去配合这个item的xm去渲染列表数据。

BaseQuickAdapter的使用,之前我是没有通过ViewBinding去进行布局绑定的,都是通过R.layout.item布局文件进行的,那么换成了ViewBinding要怎么操作呢?BaseQuickAdapter的源码中没有提到ViewBinding,倒是提到了DataBinding,很明显这是两回事,因此我们需要自己扩展一下,让BaseQuickAdapter中可以使用ViewBinding,看下面这一段代码:

class BleDeviceBaseAdapter(layoutResId: Int, data: MutableList?) :
BaseQuickAdapter<BleDevice, BaseViewHolder>(layoutResId, data) {
override fun convert(holder: BaseViewHolder, item: BleDevice) {
}
}

这是常规的写法,只要传入数据和布局文件的id就可以了,但是现在布局id变成了ViewBinding,因此就需要对这个BaseViewHolder进行一个覆写,这个方式我也是参考了网上博客的内容,

新建一个adapter包,包下新建一个ViewBindingHolder类,里面的代码如下:

class ViewBindingHolder(val vb: VB, view: View) : BaseViewHolder(view)

这里我们自定义了一个ViewBindingHolder,这个类继承了BaseViewHolder,同时构造这个类的时候传入了一个VIewBinding,这说明支持任何ViewBinding,然后就是构造参数vb,view。

在这个ViewBindingHolder类中 新增一个抽象类ViewBindingAdapter,代码如下:

abstract class ViewBindingAdapter<VB : ViewBinding, T>(data: MutableList? = null) :
BaseQuickAdapter<T, ViewBindingHolder>(0, data) {
//重写返回自定义 ViewHolder
override fun onCreateDefViewHolder(parent: ViewGroup, viewType: Int): ViewBindingHolder {
//这里为了使用简洁性,使用反射来实例ViewBinding
val viewBindingClass: Class =
(javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class
val inflate = viewBindingClass.getDeclaredMethod(
“inflate”,
LayoutInflater::class.java,
ViewGroup::class.java,
Boolean::class.java
)
val mBinding = inflate.invoke(null, LayoutInflater.from(parent.context), parent, false) as VB
return ViewBindingHolder(mBinding, mBinding.root)
}
}

ViewBindingAdapter中传入的参数是VB和T,也就是ViewBinding和Data,然后继承BaseQuickAdapter,传入T和ViewBindingHolder,因为ViewBindingHolder继承了BaseViewHolder,因此是可以这么写的。

然后看下面的这个构造方法。onCreateDefViewHolder,创建默认到的ViewHolder,然后就是根据这个传进来的VB进行一个相应的编译类寻找,因为ViewBinding使用了编译时技术,会在布局完成时构建一个编译类,这个类对应一个xml文件,因此通过这个ViewBinding去反射拿到对应的类,再通过这个类名的中infalte,infalte相信你不会默认,因为在MainActivity中也用到了这个,然后通过infalte去获取mBinding,这个就等价于

android 蓝牙ble配对流程_xml_02

然后mBinding.root对应的就是具体的View,也就是ViewBindingHolder中的View。

android 蓝牙ble配对流程_android 蓝牙ble配对流程_03

刚才的一系列操作就是通过ViewBinding去获取View,一句话说起来很简单,但是你要实践起来很复杂。

现在这个类就写好了,下面在adapter包下新建一个BleDeviceAdapter类,代码如下:

class BleDeviceAdapter(data: MutableList? = null) :
ViewBindingAdapter<ItemBluetoothBinding, BleDevice>(data) {
@SuppressLint(“SetTextI18n”)
override fun convert(holder: ViewBindingHolder, item: BleDevice) {
val binding = holder.vb
binding.tvDeviceName.text = item.name
binding.tvMacAddress.text = item.device.address
binding.tvRssi.text = “${item.rssi} dBm”
}
}

我相信经过了上面的代码之后,你现在很好理解现在的这种方式,唯一的区别就是从之前的layoutId变成了ViewBinding。ItemBluetoothBinding对应的就是之前的item_bluetooth.xml文件。

⑦ 数据渲染

适配器编写好了,下面就是使用了。先定义一些变量

//低功耗蓝牙适配器
private lateinit var bleAdapter: BleDeviceAdapter
//蓝牙列表
private var mList: MutableList = ArrayList()
//地址列表
private var addressList: MutableList = ArrayList()
//当前是否扫描
private var isScanning = false
然后新增一个页面初始化的方法initView(),代码如下:
private fun initView() {
bleAdapter = BleDeviceAdapter(mList).apply {
setOnItemClickListener { _, _, position ->
stopScan()
val device = mList[position].device
//跳转页面
}
animationEnable = true
setAnimationWithDefault(AnimationType.SlideInRight)
}
binding.rvDevice.apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = bleAdapter
}
//扫描蓝牙
binding.fabAdd.setOnClickListener { if (isScanning) stopScan() else scan() }
}

在这个方法中我配置了适配器和RecyclerView,最后是浮动按钮的点击事件,用于控制扫描的开始和停止。然后在onCreate中调用这个initView方法。

android 蓝牙ble配对流程_android 蓝牙ble配对流程_04

然后就是扫描后的数据处理,之前里面可是啥也没有的。增加代码如下图所示:

android 蓝牙ble配对流程_kotlin_05

当扫描到设备时添加到获取设备地址和设备名称,如果设备名称为null则赋值为Unkown。然后根据地址列表的size去进行数据处理,为空直接添加,不为空则检查地址列表中是否存在之前设备地址,因为一个设备是可以被重复扫描到的,因此这是为了避免重复添加数据。这里的addDeviceList方法,代码如下:

private fun addDeviceList(bleDevice: BleDevice) {
mList.add(bleDevice)
//无设备UI展示
binding.layNoEquipment.visibility = if (mList.size > 0) View.GONE else View.VISIBLE
//刷新列表适配器
bleAdapter.notifyDataSetChanged()
}

下面只要扫描设备就可以了,现在写这两个方法,scan和stopScan。

⑧ 开始和停止扫描

开始扫描
/**
• 扫描蓝牙
*/
private fun scan() {
if (!defaultAdapter.isEnabled) {
showMsg(“蓝牙未打开”);return
}
if (isScanning) {
showMsg(“正在扫描中…”);return
}
isScanning = true
addressList.clear()
mList.clear()
BluetoothLeScannerCompat.getScanner().startScan(scanCallback)
binding.progressBar.visibility = View.VISIBLE
binding.fabAdd.text = “扫描中”
}

首先判断手机蓝牙是否打开,没打开直接return,然后是判断是否正在扫描中,是直接return,然后设置isScanning = true,下一次点击就会return掉,之后就是清掉之前的设备数据。然后启动扫描,显示加载进度条表示当前正在扫描设备,最后修改浮动按钮的文字。

停止扫描

/**
• 停止扫描
*/
private fun stopScan() {
if (!defaultAdapter.isEnabled) {
showMsg(“蓝牙未打开”);return
}
if (isScanning) {
isScanning = false
BluetoothLeScannerCompat.getScanner().stopScan(scanCallback)
binding.progressBar.visibility = View.INVISIBLE
binding.fabAdd.text = “开始扫描”
}
}

这个方法就不用解释了,你明白对不对。你现在可以运行一下,不过我打算一气呵成,写完再运行。

四、连接和数据交互


这里的连接自然还是Gatt连接,同样的新建一个Activity,去哪里进行连接和数据交互操作。新建一个DataExchangeActivity,对应的布局activity_data_exchange.xml。生成了ActivityDataExchangeBinding,然后在onCreate中,进行配置。

① 绑定视图

private lateinit var binding: ActivityDataExchangeBinding

android 蓝牙ble配对流程_kotlin_06

② 初始化连接

从MainActivity中传递点击的Device过来。回到MainActivity中,添加如下图中所选处代码。

android 蓝牙ble配对流程_kotlin_07

然后回到DataExchangeActivity中新建一个initView方法,用于页面视图的初始化,同时也要接收传递过来的device。

//Gatt
private lateinit var gatt: BluetoothGatt
private fun initView() {
supportActionBar?.apply {
title = “Data Exchange”
setDisplayHomeAsUpEnabled(true)
}
val device = intent.getParcelableExtra(“device”)
//gatt连接
gatt = device!!.connectGatt(this, false, bleCallback)
}

③ Ble回调

这里有一个bleCallback,所以你的代码会报红,这很正常,只不过我们现在没有这个类,新建一个callback包,包下我们新建一个BleCallback类来管理回调,代码如下:

class BleCallback : BluetoothGattCallback() {
private val TAG = BleCallback::class.java.simpleName
private lateinit var uiCallback: UiCallback
fun setUiCallback(uiCallback: UiCallback) {
this.uiCallback = uiCallback
}
如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。