Android 蓝牙开发(扫描设备、绑定、解绑)Kotlin版

简介: Android 蓝牙开发(扫描设备、绑定、解绑)Kotlin版

Kotlin版 蓝牙开发 (扫描设备、绑定、解绑)


前言


之前写了一个蓝牙的小Demo,看的人还是有一些的,也有人私信我说,在学Kotlin,能不能出一版Kotlin的博客讲述这个蓝牙开发,这个想法还是不错的,不过就怕写了没有人看,因为在国内Kotlin是受众群体确实比较少,当然了也是有大的方向在往这边推动的,但是小公司依然不会去用Kotlin,如果你看不惯我这个说法也不要告诉我。我只是把这个博客当成是笔记而已,如果能在写作的时候帮助到别人也是乐意的,不能就自勉吧。


运行效果图


20200703093532131.gif


扫描蓝牙设备


20200703093532520.gif


如果你对上面的效果图感觉满意,那么可以往下面看了,不满意就不浪费你查看其它文章的时间了。


正文


  当然还是新创建一个项目,名为MyBluetooth-Kotlin,为了区分我之前写的Java版的项目。

20201116174305923.png


选择语言为Kotlin,然后点击Finish完成项目的创建。第一次创建Kotlin项目花费的时间会比较长,耐心等待。


创建好了之后你看到的第一个页面应该是这样的。


20201116174557844.png


没错,这就是Kotlin语言的Android项目,和Java还是有区别的,建议了解了Kotlin的语法再看文章会比较好,当然你也可以对照我写的Android 蓝牙开发(扫描设备、绑定、解绑)Java版来看,我尽量保持差不多的业务逻辑流程来编写项目。


① 配置项目


在工程的build.gradle中,添加如下依赖

maven { url "https://jitpack.io" }


添加位置如下图所示:


20201116175116473.png


然后是在app下的build.gradle中添加依赖库

  compileOptions {//指定使用的JDK1.8
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }
  //Google Material控件,以及迁移到AndroidX下一些控件的依赖
    implementation 'com.google.android.material:material:1.2.0'
    //RecyclerView最好的适配器,让你的适配器一目了然,告别代码冗余
    implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.30'
    //权限请求框架
    implementation 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
    implementation "io.reactivex.rxjava2:rxjava:2.0.0"


添加位置如下图所示:

20201116175342303.png


现在工程和app模块的build.gradle就修改完成了,直接Sync同步一下,同步之后你添加的依赖库才能使用。


然后配置AndroidManifest.xml文件

  <!--蓝牙连接权限-->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <!--蓝牙通讯权限-->
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <!--位置信息  获取精准位置-->
    <!--Android 6.0及后续版本,使用蓝牙扫描,还需要添加如下的权限,且该权限还需要在使用时动态申请-->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />


添加位置如下图所示:


2020111617564237.png


然后改动colors.xml中系统默认的颜色


20201116175741998.png


这两个颜色会影响到你的状态栏。


然后是styles.xml文件

20201116175916811.png


上面不涉及到代码,所以Java和Kotlin中的资源文件配置是差不多的。


② 布局和样式


图片资源

20200703095318564.png

20200703095405367.png

20200703095405377.png

20200703095405371.png

20200703095405363.png

20200703095405361.png


当然里面的一些其他的图标请到我的源码里面去拿,我就不一一贴出来了


在drawable下创建一个名为progressbar.xml的样式文件,代码如下:


<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <rotate
            android:drawable="@drawable/icon_loading"
            android:fromDegrees="0.0"
            android:pivotX="50.0%"
            android:pivotY="50.0%"
            android:toDegrees="360.0" />
        <!-- 其中360.0值越大,转的圈圈越快 -->
        <span style="white-space:pre" />
    </item>
</layer-list>


修改activity_main.xml布局文件,代码如下:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:fitsSystemWindows="true"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <!--标题-->
    <androidx.appcompat.widget.Toolbar
        android:elevation="3dp"
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="我的蓝牙"
            android:textColor="#000"
            android:textSize="18sp" />
    </androidx.appcompat.widget.Toolbar>
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#EEEEEE" />
    <!--加载布局-->
    <LinearLayout
        android:id="@+id/loading_lay"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:visibility="gone">
        <ProgressBar
            android:layout_width="@dimen/dp_40"
            android:layout_height="@dimen/dp_40"
            android:indeterminate="true"
            android:indeterminateDrawable="@drawable/progressbar" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="扫描中..." />
    </LinearLayout>
    <!--设备展示列表-->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv"
        android:background="#FFF"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#EEEEEE" />
    <!--扫描蓝牙-->
    <TextView
        android:id="@+id/scan_devices"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="?android:attr/selectableItemBackground"
        android:gravity="center"
        android:text="扫描蓝牙" />
</LinearLayout>


在layout下创建列表展示的item的布局文件,名为item_device_list.xml


20200703095901744.png


代码如下:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:id="@+id/item_device"
    android:background="?android:attr/selectableItemBackground"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <LinearLayout
        android:gravity="center_vertical"
        android:padding="12dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <ImageView
            android:id="@+id/iv_device_type"
            android:src="@mipmap/icon_bluetooth"
            android:layout_width="30dp"
            android:layout_height="30dp"/>
        <TextView
            android:id="@+id/tv_name"
            android:paddingLeft="12dp"
            android:textSize="16sp"
            android:text="设备名称"
            android:textColor="#000"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>
        <TextView
            android:gravity="right"
            android:id="@+id/tv_bond_state"
            android:text="绑定状态"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    <View
        android:background="#EBEBEB"
        android:layout_marginLeft="54dp"
        android:layout_width="match_parent"
        android:layout_height="1dp"/>
</LinearLayout>


③ 编码


 一切准备工作都已经就绪了,下面就进入编码环节,前面的内容其实和Kotlin的关系都不大,下面上正菜,Kotlin相比于Java来说的优势就是简洁,这一点会在下面的编码过程中体现。


1. 通知栏样式修改


首先修改状态栏的文字颜色,如果你现在运行这个项目在手机上时,你会发现状态栏是白色的背景以及白色的文字。如下图所示:


20201117105511231.png


这样的用户体验是很不好的,而在Android6.0以后支持设置高亮状态栏样式。在之前我写Java版的时候特别弄了一个工具类,里面有针对性状态栏的一些样式和颜色改动,但实际上我只用了其中的一个方法,为了一个方法而去写一个工具类显然多此一举了。所以在Kotlin中我想到了更简单的办法,直接在MainActivity中修改状态栏样式。


代码如下:


    //设置亮色状态栏模式 systemUiVisibility在Android11中弃用了,可以尝试一下。
        window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
                View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR


放在onCreate方法中,然后运行。


20201117105715174.png


是不是立竿见影,这个效果一行代码解决问题还不用写工具类,完全调用系统的方法,请注意我是Android10.0版本的手机,也是我自己用的手机。


下面写列表的适配器,因为你扫描蓝牙肯定会是一个列表,既然是一个列表那么肯定要有适配器。


2. 蓝牙设备列表适配器编写


创建一个adapter包,包下创建一个DeviceAdapter.kt文件,如下所示


20201117164011199.png


DeviceAdapter的代码如下:

package com.llw.bluetooth.adapter
import android.bluetooth.BluetoothClass
import android.bluetooth.BluetoothClass.Device.*
import android.bluetooth.BluetoothClass.Device.Major.*
import android.bluetooth.BluetoothDevice
import android.widget.ImageView
import android.widget.TextView
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.BaseViewHolder
import com.llw.bluetooth.R
/**
 * 蓝牙设备适配器
 */
class DeviceAdapter(layoutResId: Int, data: MutableList<BluetoothDevice>?) :
    BaseQuickAdapter<BluetoothDevice, BaseViewHolder>(layoutResId, data) {
    override fun convert(helper: BaseViewHolder?, item: BluetoothDevice?) {
        val tvName = helper!!.getView<TextView>(R.id.tv_name)
        val icon = helper.getView<ImageView>(R.id.iv_device_type)
        //根据设备类型设置图标
        getDeviceType(item!!.bluetoothClass.majorDeviceClass, icon)
        tvName.text = if (item.name == null) "无名" else item.name
        //蓝牙设备绑定状态判断
        val tvState = helper!!.getView<TextView>(R.id.tv_bond_state)
        tvState.text = when (item.bondState) {
            10 -> "未配对"
            11 -> "正在配对..."
            12 -> "已配对"
            else -> "未配对"
        }
        //添加item点击事件
        helper.addOnClickListener(R.id.item_device)
    }
    /**
     * 根据类型设置图标
     * @param type 类型码
     * @param icon 图标
     */
    private fun getDeviceType(type: Int, icon: ImageView) {
        when (type) {
            AUDIO_VIDEO_HEADPHONES,//耳机
            AUDIO_VIDEO_WEARABLE_HEADSET,//穿戴式耳机
            AUDIO_VIDEO_HANDSFREE,//蓝牙耳机
            AUDIO_VIDEO //音频设备
            -> icon.setImageResource(R.mipmap.icon_headset)
            COMPUTER //电脑
            -> icon.setImageResource(R.mipmap.icon_computer)
            PHONE //手机
            -> icon.setImageResource(R.mipmap.icon_phone)
            HEALTH //健康类设备
            -> icon.setImageResource(R.mipmap.icon_health)
            AUDIO_VIDEO_CAMCORDER, //照相机录像机
            AUDIO_VIDEO_VCR //录像机
            -> icon.setImageResource(R.mipmap.icon_vcr)
            AUDIO_VIDEO_CAR_AUDIO //车载设备
            -> icon.setImageResource(R.mipmap.icon_car)
            AUDIO_VIDEO_LOUDSPEAKER //扬声器
            -> icon.setImageResource(R.mipmap.icon_loudspeaker)
            AUDIO_VIDEO_MICROPHONE //麦克风
            -> icon.setImageResource(R.mipmap.icon_microphone)
            AUDIO_VIDEO_PORTABLE_AUDIO //打印机
            -> icon.setImageResource(R.mipmap.icon_printer)
            AUDIO_VIDEO_SET_TOP_BOX //音频视频机顶盒
            -> icon.setImageResource(R.mipmap.icon_top_box)
            AUDIO_VIDEO_VIDEO_CONFERENCING //音频视频视频会议
            -> icon.setImageResource(R.mipmap.icon_meeting)
            AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER //显示器和扬声器
            -> icon.setImageResource(R.mipmap.icon_tv)
            AUDIO_VIDEO_VIDEO_GAMING_TOY //游戏
            -> icon.setImageResource(R.mipmap.icon_game)
            AUDIO_VIDEO_VIDEO_MONITOR //可穿戴设备
            -> icon.setImageResource(R.mipmap.icon_wearable_devices)
            else -> icon.setImageResource(R.mipmap.icon_bluetooth)
        }
    }
    /**
     * 刷新适配器
     */
    fun changeBondDevice() {
        notifyDataSetChanged()
    }
}


代码讲解:

class DeviceAdapter(layoutResId: Int, data: MutableList<BluetoothDevice>?) :
    BaseQuickAdapter<BluetoothDevice, BaseViewHolder>(layoutResId, data)


首先看这个类,在Kotlin继承和实现都是通过 :(英文下的冒号)来操作的,而Java中继承是extends,实现是implements。在上面的代码中DeviceAdapter继承了BaseQuickAdapter,这一点和Java的相似,如下图所示

20201117172359862.png


而Kotlin的语法可以让你把构造方法的参数作为类参数使用,这样解释不知道是不是对的,这里传了一个布局id和数据源。


然后重写里面convert方法

override fun convert(helper: BaseViewHolder?, item: BluetoothDevice?) {
        val tvName = helper!!.getView<TextView>(R.id.tv_name)
        val icon = helper.getView<ImageView>(R.id.iv_device_type)
        getDeviceType(item!!.bluetoothClass.majorDeviceClass, icon)
        tvName.text = if (item.name == null) "无名" else item.name
        //蓝牙设备绑定状态判断
        val tvState = helper!!.getView<TextView>(R.id.tv_bond_state)
        tvState.text = when (item.bondState) {
            10 -> "未配对"
            11 -> "正在配对..."
            12 -> "已配对"
            else -> "未配对"
        }
        //添加item点击事件
        helper.addOnClickListener(R.id.item_device)
    }


在代码中你会看到 !!? 这个你就不明所以了,因为Java中是没有的,这里解释一下,首先是Kotlin对于空安全做了处理, !! 表示当前对象不会空的情况下执行,而 ? 表示当前对象可以为空。


    val tvName = helper!!.getView<TextView>(R.id.tv_name)
        val icon = helper.getView<ImageView>(R.id.iv_device_type)


而这两行代码,可以看到,第一行我给了!!,第二行没有给,这是因为在Kotlin中只要一开始做了处理之后后面就可以不用再次处理,当然你加上!!也没有问题。val 表示不可变量,而通过Kotlin的类型推导机制,tvName此时代表的就是一个通过R.id.tv_name实例化之后的TextView。


    //根据设备类型设置图标
        getDeviceType(item!!.bluetoothClass.majorDeviceClass, icon)


这行代码调用getDeviceType方法,传入两个参数,这两个参数都已经做了非空的处理,所以在getDeviceType的里面就不用在做空处理了。下面看这个方法的代码:

  /**
     * 根据类型设置图标
     * @param type 类型码
     * @param icon 图标
     */
    private fun getDeviceType(type: Int, icon: ImageView) {
        when (type) {
            AUDIO_VIDEO_HEADPHONES,//耳机
            AUDIO_VIDEO_WEARABLE_HEADSET,//穿戴式耳机
            AUDIO_VIDEO_HANDSFREE,//蓝牙耳机
            AUDIO_VIDEO //音频设备
            -> icon.setImageResource(R.mipmap.icon_headset)
            COMPUTER //电脑
            -> icon.setImageResource(R.mipmap.icon_computer)
            PHONE //手机
            -> icon.setImageResource(R.mipmap.icon_phone)
            HEALTH //健康类设备
            -> icon.setImageResource(R.mipmap.icon_health)
            AUDIO_VIDEO_CAMCORDER, //照相机录像机
            AUDIO_VIDEO_VCR //录像机
            -> icon.setImageResource(R.mipmap.icon_vcr)
            AUDIO_VIDEO_CAR_AUDIO //车载设备
            -> icon.setImageResource(R.mipmap.icon_car)
            AUDIO_VIDEO_LOUDSPEAKER //扬声器
            -> icon.setImageResource(R.mipmap.icon_loudspeaker)
            AUDIO_VIDEO_MICROPHONE //麦克风
            -> icon.setImageResource(R.mipmap.icon_microphone)
            AUDIO_VIDEO_PORTABLE_AUDIO //打印机
            -> icon.setImageResource(R.mipmap.icon_printer)
            AUDIO_VIDEO_SET_TOP_BOX //音频视频机顶盒
            -> icon.setImageResource(R.mipmap.icon_top_box)
            AUDIO_VIDEO_VIDEO_CONFERENCING //音频视频视频会议
            -> icon.setImageResource(R.mipmap.icon_meeting)
            AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER //显示器和扬声器
            -> icon.setImageResource(R.mipmap.icon_tv)
            AUDIO_VIDEO_VIDEO_GAMING_TOY //游戏
            -> icon.setImageResource(R.mipmap.icon_game)
            AUDIO_VIDEO_VIDEO_MONITOR //可穿戴设备
            -> icon.setImageResource(R.mipmap.icon_wearable_devices)
            else -> icon.setImageResource(R.mipmap.icon_bluetooth)
        }
    }


其实就是条件分支,根据不同的条件做不同的事情,在Java中使用switch/case,而在Kotlin中使用when。when的语法结构更加的简洁明了,通过 -> 代替了 : ,冒号前面是条件,冒号后面是执行业务。而当多个条件对应一个执行业务时,条件之间用英文逗号隔开,一行代码完成一个条件分支,很简洁,但是不要忘了加上else,这是标准写法,你不加也没事,就如同你写switch/case不加default一样。相信这么一解释你已经理解了when的基本用法了,当然还有很多其他的用法由于业务的原因无法展示,自行百度吧。


    tvName.text = if (item.name == null) "无名" else item.name


这行代码等同于

    if (item.getName() == null) {
            helper.setText(R.id.tv_name, "无名");
        } else {
            helper.setText(R.id.tv_name, item.getName());
        }


这么一看是不是觉得Kotlin的语法很简单,它允许你的返回值一致的判断进行直接赋值,比如这里判断设备名称为空则显示无名二字,不为空则显示设备名,这两个返回都是String类型,而tvName.text设置的就是String类型,所以就有了上面的简洁代码,有点像三目运算符。

    //蓝牙设备绑定状态判断
        val tvState = helper!!.getView<TextView>(R.id.tv_bond_state)
        tvState.text = when (item.bondState) {
            10 -> "未配对"
            11 -> "正在配对..."
            12 -> "已配对"
            else -> "未配对"
        }
        //添加item点击事件
        helper.addOnClickListener(R.id.item_device)


这几行代码也没有什么好讲解的了,都讲过了,这也是when的另一种使用方法,可以直接赋值使用。


  /**
     * 刷新适配器
     */
    fun changeBondDevice() {
        notifyDataSetChanged()
    }


最后一个方法就是刷新适配器,当页面的数据有变动是及时刷新。好了这个适配器就讲完了,应该够详细了吧。


3. 权限请求


 不管你是用的什么语言来开发Android,你都得遵守Android制定的规则,因此也是要做Android版本大于6.0时动态请求权限。


于是就有了如下这个方法

  /**
     * 检查Android版本
     */
    private fun checkVersion() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            //6.0或6.0以上
            permissionsRequest() //动态权限申请
        } else {
            //6.0以下
            initBlueTooth() //初始化蓝牙配置
        }
    }


记得在OnCreate中调用喔!


20201118145135261.png


然后来看看这个权限请求的方法


  /**
     * 动态权限申请
     */
    private fun permissionsRequest() {
        val rxPermissions = RxPermissions(this)
        rxPermissions.request(Manifest.permission.ACCESS_FINE_LOCATION)
            .subscribe {
                if (it) {
                    initBlueTooth()
                } else {
                    showMsg("权限未开启")
                }
            }
    }


showMsg方法

  /**
     * 显示提示消息
     */
    private fun showMsg(llw: String) {
        Toast.makeText(this, llw, Toast.LENGTH_SHORT).show()
    }


这个方法的代码很好理解,和Java的逻辑如出一辙,无非就是不知道这个it是什么意思,it就是它本身的意思,结合方法中的逻辑来看就容易理解了,权限请求自然会有两种结果,同意和不同意,也就是结果是true和false的结果,而这个it就代表这个请求的结果。

4. 初始化蓝牙


首先声明一些成员变量,这里用的是MutableList,表示可变列表,可以有很多方法。


  //蓝牙广播接收器
    private var bluetoothReceiver: BluetoothReceiver? = null
    //蓝牙适配器
    private var bluetoothAdapter: BluetoothAdapter? = null
    //蓝牙设备适配器
    private var mAdapter: DeviceAdapter? = null
    //可变列表
    private var list: MutableList<BluetoothDevice> = mutableListOf()
    //请求码
    private val REQUEST_ENABLE_BLUETOOTH = 1 


BluetoothReceiver报红没有关系不要慌,下面会写的,先看这个初始化蓝牙的方法,比较简单,我想不用讲代码了。


  /**
     * 初始化蓝牙
     */
    private fun initBlueTooth() {
        var intentFilter = IntentFilter()
        intentFilter.addAction(BluetoothDevice.ACTION_FOUND) //获得扫描结果
        intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED) //绑定状态变化
        intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) //开始扫描
        intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) //扫描结束
        bluetoothReceiver = BluetoothReceiver() //实例化广播接收器
        registerReceiver(bluetoothReceiver, intentFilter) //注册广播接收器
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() //获取蓝牙适配器
    }


5. 扫描蓝牙


  在布局中底部我放了一个TextView,点击之后扫描蓝牙,


  <!--扫描蓝牙-->
    <TextView
        android:id="@+id/scan_devices"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="?android:attr/selectableItemBackground"
        android:gravity="center"
        android:onClick="scanBluetooth"
        android:text="扫描蓝牙" />


注意看这一句话

android:onClick="scanBluetooth"


通过在布局中点击触发MainActivity中的方法,在MainActivity中写入,或者Alt+Enter

20201118152026107.png


然后你就会看到这样的一个方法

fun scanBluetooth(view: View) {}


首先想清楚这个里面要做什么?难道仅仅只有扫描蓝牙吗?当然不是,首先要看你的设备是否支持蓝牙,其次蓝牙是否打开,最后才是扫描蓝牙

于是里面的代码就可以这样写


  /**
     * 扫描蓝牙
     */
    fun scanBluetooth(view: View) {
        if (bluetoothAdapter != null) { //是否支持蓝牙
            if (bluetoothAdapter!!.isEnabled) { //打开
                //开始扫描周围的蓝牙设备,如果扫描到蓝牙设备,通过广播接收器发送广播
                if (mAdapter != null) { //当适配器不为空时,这时就说明已经有数据了,所以清除列表数据,再进行扫描
                    list.clear()
                    mAdapter!!.notifyDataSetChanged()
                }
                bluetoothAdapter!!.startDiscovery()
            } else { //未打开
                val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
                startActivityForResult(intent, REQUEST_ENABLE_BLUETOOTH)
            }
        } else {
            showMsg("你的设备不支持蓝牙")
        }
    }


6. 广播接收器


  点击扫描蓝牙之后会这行扫描事件,会发送一个广播出去,发送出去了自然要有一个地方来接收,这就是广播接收器,在MainActivity定义一个内部类,通过inner关键字


  /**
     * 广播接收器
     */
    inner class BluetoothReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            when (intent?.action) {
                //显示蓝牙设备
                BluetoothDevice.ACTION_FOUND -> showDevicesData(context, intent)
                //当有蓝牙绑定状态发生改变时,刷新列表数据
                BluetoothDevice.ACTION_BOND_STATE_CHANGED -> mAdapter?.changeBondDevice()
                //开始扫描
                BluetoothAdapter.ACTION_DISCOVERY_STARTED -> loading_lay.visibility = View.VISIBLE
                //停止扫描
                BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> loading_lay.visibility = View.GONE
                else -> showMsg("未知")
            }
        }
    }


另外别忘了在页面销毁的时候解注册广播

  /**
     * 销毁
     */
    override fun onDestroy() {
        super.onDestroy()
        //卸载广播接收器
        unregisterReceiver(bluetoothReceiver)
    }


当开始扫描的时候发送ACTION_FOUND,而作为接收方,自然要有相应的处理方法,这个时候注意到showDevicesData(context, intent),通过这个方法显示扫描到的蓝牙设备信息。


7. 显示蓝牙设备信息


  /**
     * 显示蓝牙设备信息
     *
     * @param context 上下文参数
     * @param intent  意图
     */
    private fun showDevicesData(context: Context?, intent: Intent) {
        //获取已绑定的设备
        getBondedDevice() 
        //获取周围蓝牙设备
        val device =
            intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
        if (list.indexOf(device) == -1) { //防止重复添加
            if (device.name != null) { //过滤掉设备名称为null的设备
                list.add(device)
            }
        }
        mAdapter = DeviceAdapter(R.layout.item_device_list, list)
        rv.layoutManager = LinearLayoutManager(context)
        rv.adapter = mAdapter
        //item的点击事件
        mAdapter!!.setOnItemChildClickListener { adapter, view, position ->
            //点击时获取状态,如果已经配对过了就不需要在配对
            if (list[position].bondState == BluetoothDevice.BOND_NONE) {
                createOrRemoveBond(1, list[position]) //开始匹配
            } else {
                showDialog(
                    "确定要取消配对吗?",
                    DialogInterface.OnClickListener { dialog, which ->
                        //取消配对
                        createOrRemoveBond(2, list[position]) //取消匹配
                    })
            }
        }
    }


在这里,有一个可以注意的地方


20201118153216437.png


这句话的意思是,参数未被使用,可以使用_代替。然后来看里面的代码,首先第一句是getBondedDevice()

  /**
     * 获取已绑定设备
     */
    private fun getBondedDevice() {
        val pairedDevices = bluetoothAdapter!!.bondedDevices
        if (pairedDevices.size > 0) { //如果获取的结果大于0,则开始逐个解析
            for (device in pairedDevices) {
                if (list.indexOf(device) == -1) { //防止重复添加
                    if (device.name != null) { //过滤掉设备名称为null的设备
                        list.add(device)
                    }
                }
            }
        }
    }


这里用到了in关键字,使用in来检查一个值是否在一个区间内。与他的代码已经有了注释了,就不过多的解释了。


剩下的代码分为两部分,一部分是数据的处理,点击处理。避免重复添加和添加null的设备进入列表,而点击item,根据绑定状态而定,绑定过蓝牙的点击就是取消绑定,这里调用了一个方法。显示一个弹窗


  /**
     * 弹窗
     *
     * @param dialogTitle     标题
     * @param onClickListener 按钮的点击事件
     */
    private fun showDialog(dialogTitle: String, onClickListener: DialogInterface.OnClickListener) {
        val builder =
            AlertDialog.Builder(this)
        builder.setMessage(dialogTitle)
        builder.setPositiveButton("确定", onClickListener)
        builder.setNegativeButton("取消", null)
        builder.create().show()
    }


而未绑定的点击绑定,调用createOrRemoveBond方法,也可以说是配对,当你取消绑定是也会调用这个方法,只是传递的类型不同而已。


  /**
     * 创建或者取消匹配
     *
     * @param type   处理类型 1 匹配  2  取消匹配
     * @param device 设备
     */
    private fun createOrRemoveBond(type: Int, device: BluetoothDevice) {
        var method: Method? = null
        try {
            when (type) {
                1 -> {
                    method = BluetoothDevice::class.java.getMethod("createBond")
                    method.invoke(device)
                }
                2 -> {
                    method = BluetoothDevice::class.java.getMethod("removeBond")
                    method.invoke(device)
                    list.remove(device) //清除列表中已经取消了配对的设备
                }
            }
        } catch (e: NoSuchMethodException) {
            e.printStackTrace()
        } catch (e: InvocationTargetException) {
            e.printStackTrace()
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
        }
    }

到这里,代码就写完了。


相关文章
|
2天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
4天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
4天前
|
存储 API 开发工具
探索安卓开发:从基础到进阶
【10月更文挑战第37天】在这篇文章中,我们将一起探索安卓开发的奥秘。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和建议。我们将从安卓开发的基础开始,逐步深入到更复杂的主题,如自定义组件、性能优化等。最后,我们将通过一个代码示例来展示如何实现一个简单的安卓应用。让我们一起开始吧!
|
5天前
|
存储 XML JSON
探索安卓开发:从新手到专家的旅程
【10月更文挑战第36天】在这篇文章中,我们将一起踏上一段激动人心的旅程,从零基础开始,逐步深入安卓开发的奥秘。无论你是编程新手,还是希望扩展技能的老手,这里都有适合你的知识宝藏等待发掘。通过实际的代码示例和深入浅出的解释,我们将解锁安卓开发的关键技能,让你能够构建自己的应用程序,甚至贡献于开源社区。准备好了吗?让我们开始吧!
15 2
|
30天前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
19 1
|
2月前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
75 1
|
3月前
|
调度 Android开发 开发者
【颠覆传统!】Kotlin协程魔法:解锁Android应用极速体验,带你领略多线程优化的无限魅力!
【8月更文挑战第12天】多线程对现代Android应用至关重要,能显著提升性能与体验。本文探讨Kotlin中的高效多线程实践。首先,理解主线程(UI线程)的角色,避免阻塞它。Kotlin协程作为轻量级线程,简化异步编程。示例展示了如何使用`kotlinx.coroutines`库创建协程,执行后台任务而不影响UI。此外,通过协程与Retrofit结合,实现了网络数据的异步加载,并安全地更新UI。协程不仅提高代码可读性,还能确保程序高效运行,不阻塞主线程,是构建高性能Android应用的关键。
59 4
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
150 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
59 8
|
4月前
|
安全 Java Android开发
探索Android应用开发中的Kotlin语言
【7月更文挑战第19天】在移动应用开发的浩瀚宇宙中,Kotlin这颗新星以其简洁、安全与现代化的特性,正迅速在Android开发者之间获得青睐。从基本的语法结构到高级的编程技巧,本文将引导读者穿梭于Kotlin的世界,揭示其如何优化Android应用的开发流程并提升代码的可读性与维护性。我们将一起探究Kotlin的核心概念,包括它的数据类型、类和接口、可见性修饰符以及高阶函数等特性,并了解这些特性是如何在实际项目中得以应用的。无论你是刚入门的新手还是寻求进阶的开发者,这篇文章都将为你提供有价值的见解和实践指导。