Android 低功耗蓝牙开发(扫描连接数据交互)Kotlin版

Posted 初学者-Study

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 低功耗蓝牙开发(扫描连接数据交互)Kotlin版相关的知识,希望对你有一定的参考价值。

前言

  写这篇文章是因为有读者想看看Kotlin中怎么操作低功耗蓝牙,再加上我也想写一些关于Kotlin的内容,对于低功耗蓝牙的Java版的,我写了两篇,一个是扫描、连接,另一篇就是数据交互,而这篇Kotlin文章我会减少讲解的环节,更多的注重业务逻辑和UI以及Kotlin的语法。

运行效果图

正文

创建项目

一、配置项目

  配置项目常规来说两个环节,androidManifest.xml和build.gradle。

AndroidManifest.xml

	<!-- 蓝牙权限 -->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <!-- 支持ble的设备 -->
    <uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="true" />
    <!-- 定位权限 -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

然后是build.gradle,这个地方有两处,一个是项目的build.gradle,增加如下代码:

	maven  url "https://jitpack.io"


然后是app模块下的build.gradle,增加如下代码:

	//使用viewBinding
    viewBinding 
        enabled = true
    
	//蓝牙扫描库
    implementation 'no.nordicsemi.android.support.v18:scanner:1.5.0'
    //权限请求 支持Androidx
    implementation 'com.permissionx.guolindev:permissionx:1.4.0'
    //让你的适配器一目了然,告别代码冗余
    implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'

两处代码:

这里注意一点,viewBinding的开启代码是在android闭包中的,不要放错地方了,然后点击Sync或者Sync Now。当程序编译完成之后,运行到自己手机上,先确保项目配置这一步没有问题。

二、页面设计

  首先改一下主题的颜色,列如标题,改成绿色。在colors.xml中增加:

	<color name="green">#2b9247</color>
    <color name="green_light">#00cd66</color>

然后修改style中的样式:

修改activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    android:orientation="vertical"
    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="空空如也~" />
    </LinearLayout>

    <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" />

</RelativeLayout>

这里面有两个图标,代码如下:
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" />
</vector>

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"/>
</vector>

下面写扫描到的列表适配器布局文件,在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地址" />
        </LinearLayout>

        <TextView
            android:id="@+id/tv_rssi"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="信号强度" />
    </LinearLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="0.5dp"
        android:background="#EEE" />
</LinearLayout>

这里也有一个蓝牙图标的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" />

</vector>

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

三、扫描设备

  首先想清楚扫描之前要做什么,扫描之后要做什么。扫描之前要判断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方法中调用

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

⑤ 扫描结果

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

	//扫描结果回调
    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<BleDevice>?) :
    BaseQuickAdapter<BleDevice, BaseViewHolder>(layoutResId, data) 
    
    override fun convert(holder: BaseViewHolder, item: BleDevice) 
        
    

以上是关于Android 低功耗蓝牙开发(扫描连接数据交互)Kotlin版的主要内容,如果未能解决你的问题,请参考以下文章

Android 低功耗蓝牙开发(扫描连接)

Android 低功耗蓝牙开发(扫描连接)

Android 低功耗蓝牙开发(扫描连接)

Android 低功耗蓝牙开发 (扫描过滤自定义服务与特性)Kotlin版

Android 低功耗蓝牙开发 (扫描过滤自定义服务与特性)Kotlin版

Android BLE低功耗蓝牙开发极简系列(二)之读写操作