美文网首页Android项目Android架构
Android蓝牙开发「低功耗蓝牙4.0,搜索设备,显示搜索设备

Android蓝牙开发「低功耗蓝牙4.0,搜索设备,显示搜索设备

作者: 唐_夏影 | 来源:发表于2018-12-03 10:31 被阅读13次

Android蓝牙开发「低功耗蓝牙4.0,搜索设备,显示搜索设备列表」

解释一下蓝牙低功耗4.0,简单的理解就是,有的设备支持的是传统蓝牙,而有的设备支持的,是低功耗的蓝牙4.0,优势之一就是比较省电

而传统蓝牙的ApI,和低功耗蓝牙4.0的Api大部分是不一样的,举个例子,我公司的物联网设备是支持低功耗蓝牙4.0,那么我用传统蓝牙的ApI去连接,那是没办法建立起和设备的联系的

这里笔者一开始就踩了这个坑,用传统蓝牙的Api搞鼓了一天,最后还是前辈过来看了一眼,才得以改正

搜索结果.gif

具体传统蓝牙Api的使用,可以查看刘前辈的蓝牙文章,传送门,需要注意的是,该文章的发布时间比较久了,所以里面没有添加Android6.0获取地理位置权限动态处理的部分,这里我们得自己添加,不然无法搜索到设备

看了本文章后,你可以学习到如何使用低功耗蓝牙Api搜索蓝牙设备,使用recyclerView显示搜索到的蓝牙设备,然后点击相应的设备名存储搜索到相应设备的地址

一,UI界面的代码

相关源代码已经上传到仓库的bluetooth文件夹,传送门

1)添加依赖,初始化控件

我们先添加下依赖,design库用来使用recyclerView,Anko库等等拿来弹吐司用

//材料设计
compile 'com.android.support:design:28.0.0'
//Anko
compile "org.jetbrains.anko:anko-commons:0.10.5"

然后到我们的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:orientation="vertical"
    tools:context=".module.blue.BlueActivity">

    <!--搜索设备-->
    <Button
        android:id="@+id/btnSearch"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="搜索设备" />

    <!--连接设备,完成设备服务的绑定-->
    <Button
        android:id="@+id/btnConnect"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="连接设备" />

    <!--发送指令-->
    <Button
        android:id="@+id/btnSend"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发送指令" />

    <!--显示当前选中的设备名称-->
    <TextView
        android:id="@+id/tvDeviceName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="当前设备名称" />

    <!--显示当前设备的状态-->
    <TextView
        android:id="@+id/tvState"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="显示当前设备的状态"
        />

    <!--显示搜索到的蓝牙设备名称-->
    <android.support.v7.widget.RecyclerView
        android:id="@+id/ryDevice"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

    </android.support.v7.widget.RecyclerView>

</LinearLayout>

连接设备按钮的功能其实从代码的角度来看是要分为两步的,第一步是建立起设备的连接,第二步是建立起设备和服务的连接,这里先有个映像即可,可以理解是为连接蓝牙设备需要的上下两个步骤

接下来到Activity里面初始化我们的控件,本篇教程采用kotlin语言,不熟悉的同学可以查看我文章的教程推荐



import kotlinx.android.synthetic.main.activity_blue.*
import org.jetbrains.anko.toast

/**
 * 博客源码:Android蓝牙开发「低功耗蓝牙4.0,搜索设备,连接设备,连接服务,发送指令」
 * 地址:
 */
class BlueActivity : AppCompatActivity(), View.OnClickListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_blue)
        //初始化控件
        initView()
    }

    /**
     * 初始化控件
     */
    private fun initView() {
        btnSearch.setOnClickListener(this)
        btnConnect.setOnClickListener(this)
        btnSend.setOnClickListener(this)
    }

    /**
     * 声明点击事件
     */
    override fun onClick(p0: View?) {
        when (p0?.id) {
            //开始搜索
            R.id.btnSearch -> {
                toast("开始搜索")
            }
            //开始连接
            R.id.btnConnect -> {
                toast("开始连接")
            }
            //开始发送
            R.id.btnSend -> {
                toast("开始发送")
            }
        }
    }

}

2)实现recyclerView效果

声明我们的设备的实体类,实体类有两个属性,一个是设备名称,用于显示在列表上,一个是设备地址,用于我们等等点击列表后就是拿着这个地址来连接到对应的设备

//数据实体类,设备名称,设备连接地址
data class Device(var deviceName:String,var address:String)

我们先来看看蓝牙设备的地址一般什么样的,F1:B8:07:8B:D5:73

大概就是这样的形式,后面我们会通过代码来搜索到这样的数据

接下来我们写RecyclerView的item布局device_rec_item.xml,不熟悉recyclerView使用的同学可以查看这篇文章,传送门

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="myItemClick"
    android:id="@+id/item_layout"
    android:background="#ff33b5e5">

    <TextView
        android:id="@+id/tvDeviceName"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:gravity="center"
        android:textColor="@android:color/white"
        tools:text="别看了,我就是一个TextView" />
</RelativeLayout>

这里就是一个显示设置名称的TextView

然后是RecyclerView的适配器

class BlueAdapter(val context: Context, val mDates: List<Device>) : RecyclerView.Adapter<BlueAdapter.MyViewHolder>() {

    /**
     * 加载布局
     */
    override fun onCreateViewHolder(p0: ViewGroup, p1: Int): MyViewHolder {
        var view: View = View.inflate(context, R.layout.device_rec_item, null)
        return MyViewHolder(view)
    }

    /**
     * 返回列表大小
     */
    override fun getItemCount(): Int {
        return mDates.size
    }

    /**
     * 加载每个item要显示的数据
     */
    override fun onBindViewHolder(p0: MyViewHolder, p1: Int) {
        //设置设备的名称
        p0.tvDeviceName.text = mDates[p1].deviceName
    }
    
    
    inner class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        var tvDeviceName: TextView = view.findViewById(R.id.tvDeviceName)
    }
}

最后在Activity里面声明好布局管理器,加载适配器,我们先写些本地的数据测试一下

 //布局管理器
 private var linearLayoutManager: LinearLayoutManager? = null

 //设备数据源
 private var mDate = ArrayList<Device>()

 //recyclerView适配器
 private var blueAdapter: BlueAdapter = BlueAdapter(this, mDate)

initView()方法

//加载适配器
//加载布局管理器,设置recyclerView的排列方式是垂直排列,设置为false表示不进行列表顺序翻转操作
linearLayoutManager = LinearLayoutManager(this, LinearLayout.VERTICAL, false)
ryDevice.layoutManager = linearLayoutManager
ryDevice.adapter = blueAdapter//设置适配器
//加载测试数据
for (s in 1..36) {
   mDate.add(Device(s.toString(), s.toString()))
}
blueAdapter.notifyDataSetChanged()//通知适配器更新,刷新UI

完整代码

import kotlinx.android.synthetic.main.activity_blue.*
import org.jetbrains.anko.toast

/**
 * 博客源码:Android蓝牙开发「低功耗蓝牙4.0,搜索设备,连接设备,连接服务,发送指令」
 * 地址:
 */
class BlueActivity : AppCompatActivity(), View.OnClickListener {

    //布局管理器
    private var linearLayoutManager: LinearLayoutManager? = null

    //设备数据源
    private var mDate = ArrayList<Device>()

    //recyclerView适配器
    private var blueAdapter: BlueAdapter = BlueAdapter(this, mDate)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_blue)
        //初始化控件
        initView()
    }

    /**
     * 初始化控件
     */
    private fun initView() {
        btnSearch.setOnClickListener(this)
        btnConnect.setOnClickListener(this)
        btnSend.setOnClickListener(this)
        //加载适配器
        //加载布局管理器,设置recyclerView的排列方式是垂直排列,设置为false表示不进行列表顺序翻转操作
        linearLayoutManager = LinearLayoutManager(this, LinearLayout.VERTICAL, false)
        ryDevice.layoutManager = linearLayoutManager
        ryDevice.adapter = blueAdapter//设置适配器
        //加载测试数据
        for (s in 1..36) {
            mDate.add(Device(s.toString(), s.toString()))
        }
        blueAdapter.notifyDataSetChanged()//通知适配器更新,刷新UI
    }

    /**
     * 声明点击事件
     */
    override fun onClick(p0: View?) {
        when (p0?.id) {
            //开始搜索
            R.id.btnSearch -> {
                toast("开始搜索")
            }
            //开始连接
            R.id.btnConnect -> {
                toast("开始连接")
            }
            //开始发送
            R.id.btnSend -> {
                toast("开始发送")
            }
        }
    }

}

写好后运行界面,测试一下

UI界面部分.gif
3)实现recyclerView的点击事件

recyclerView点击事件详细教程,传送门,我们在RecyclerView的适配器中声明回调接口

  private var onItemClickListener: OnItemClickListener? = null

    //设置回调接口
    interface OnItemClickListener {
        fun onItemClick(view: View, position: Int)
    }

    fun setOnItemClickListener(onItemClickListener: OnItemClickListener) {
        this.onItemClickListener = onItemClickListener
    }

然后在适配器的onBindViewHolder中设置点击的回调

    /**
     * 加载每个item要显示的数据
     */
    override fun onBindViewHolder(myViewHolder: MyViewHolder, position: Int) {
        //设置设备的名称
        myViewHolder.tvDeviceName.text = mDates[position].deviceName
        //设置点击事件回调
        if (onItemClickListener != null) {
            myViewHolder.itemView.setOnClickListener { view ->
                onItemClickListener?.onItemClick(view, position)
            }
        }
    }

完整代码


class BlueAdapter(val context: Context, val mDates: List<Device>) : RecyclerView.Adapter<BlueAdapter.MyViewHolder>() {

    private var onItemClickListener: OnItemClickListener? = null

    //设置回调接口
    interface OnItemClickListener {
        fun onItemClick(view: View, position: Int)
    }

    fun setOnItemClickListener(onItemClickListener: OnItemClickListener) {
        this.onItemClickListener = onItemClickListener
    }


    /**
     * 加载布局
     */
    override fun onCreateViewHolder(p0: ViewGroup, p1: Int): MyViewHolder {
        var view: View = View.inflate(context, R.layout.device_rec_item, null)
        return MyViewHolder(view)
    }

    /**
     * 返回列表大小
     */
    override fun getItemCount(): Int {
        return mDates.size
    }

    /**
     * 加载每个item要显示的数据
     */
    override fun onBindViewHolder(myViewHolder: MyViewHolder, position: Int) {
        //设置设备的名称
        myViewHolder.tvDeviceName.text = mDates[position].deviceName
        //设置点击事件回调
        if (onItemClickListener != null) {
            myViewHolder.itemView.setOnClickListener { view ->
                onItemClickListener?.onItemClick(view, position)
            }
        }
    }


    inner class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        var tvDeviceName: TextView = view.findViewById(R.id.tvDeviceName)
    }
}

最后在Activity中使用,我们声明一个地址变量,等会我们点击哪个蓝牙设备,就将点击到的蓝牙设备的地址赋值给我们的地址变量,最后我们点击连接按钮时,就是拿着这个地址去连接你点击的蓝牙设备

    //当前选中设备的地址,等会连接的时候就是拿着这个地址去连接我们的设备
    private var address: String? = null

适配器回调

  /**
     * 初始化控件
     */
    private fun initView() {
        btnSearch.setOnClickListener(this)
        btnConnect.setOnClickListener(this)
        btnSend.setOnClickListener(this)
        //加载适配器
        //加载布局管理器,设置recyclerView的排列方式是垂直排列,设置为false表示不进行列表顺序翻转操作
        linearLayoutManager = LinearLayoutManager(this, LinearLayout.VERTICAL, false)
        ryDevice.layoutManager = linearLayoutManager
        ryDevice.adapter = blueAdapter//设置适配器
        //加载测试数据
        for (s in 1..36) {
            mDate.add(Device(s.toString(), s.toString()))
        }
        blueAdapter.notifyDataSetChanged()//通知适配器更新,刷新UI
        //设置列表的点击事件回调
        blueAdapter.setOnItemClickListener(object : BlueAdapter.OnItemClickListener {
            //点击事件回调
            override fun onItemClick(view: View, position: Int) {
                //将点击的列表地址存储起来
                address = mDate[position].address
                //设置当前选中设备的名册个
                tvDeviceName.text = mDate[position].deviceName
                //弹出提示
                toast("当前设备的名称是:${mDate[position].deviceName},设备地址是:${mDate[position].address}")
            }
        })
    }

这里我们设置当设备时,弹出相应的吐司,运行程序,测试一下吧

点击事件.gif

二,搜索设备

1)权限的获取

接下来完成的,是使用蓝牙Api,搜索到设备,将设备名称添加到列表上

Android6.0以后,要搜索到蓝牙设备,必须添加获取地址位置的权限,具体我们要添加的权限有以下3个

    <!--使用蓝牙的权限,允许程序连接配对过的设备,蓝牙使用权限-->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <!--允许程序进行进行发现和配对新的蓝牙设备,蓝牙管理权限-->
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <!--通过WiFi或移动基站的方式获取用户错略的经纬度信息,定位精度大概误差在30~1500米-->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

接着因为地理位置权限需要动态获取,所以我们创建一个父类的Activity,在父类Actiivty中进行权限的动态获取

/**
 * 父类Activity,记得加上open关键字
 */
open class BaseActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //如果当前手机的版本号打鱼23,Android6.0
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            //检查是否已经获取到地理位置权限,如果没有!=,进行权限获取
            if (this.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), 1)
            }
        }
    }
}

让Activity继承自该父类,完成对权限的获取

接着我们在Activity中声明适配器

 //声明蓝牙适配器
 private var bluetoothAdapter: BluetoothAdapter? = null

注意不要和之前recyclerView的适配器弄混了,这里的是系统自带的,蓝牙操作的第一步

接着初始化适配器

    /**
     * 初始化适配器
     * 该Api只能在Api版本18以上,Android4.3以上使用
     */
    @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private fun initAdapter() {
        val bluetoothManager: BluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        bluetoothAdapter = bluetoothManager.adapter
    }

2)搜索方法

然后我们就开始写搜索的方法了,我们先在Actiivty里写一个搜索的回调,等会我们点击开始搜索后,如果有搜索到设备,就会回调该方法


    /**
     * 搜索设备回调
     */
    private var leScanCallback: BluetoothAdapter.LeScanCallback = BluetoothAdapter.LeScanCallback { bluetoothDevice, i, bytes ->
        val name: String? = bluetoothDevice?.name//搜索到的设备的名称
        val address: String? = bluetoothDevice?.name//搜索到的设备的地址
        //打个Log测试一下
        L.d("设备的名称$name,设备的地址$address")
    }

L类是简单的Log封装类

/**
 * 加上object表示这是一个单例模式的类
 */
object L {
    //打印log
    fun d(msg: String) {
        Log.d("extra", msg)
    }
}

最后终于是我们的搜索方法了


    /**
     * 搜索设备
     */
    private fun search() {
        //判断设备的蓝牙设置是否已经打开,如果没有打开,我们弹出系统的蓝牙提示框,提示用户打开
        if (!bluetoothAdapter?.isEnabled!!) {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            startActivityForResult(enableBtIntent, 1)
        }
        //开始搜索蓝牙设备
        bluetoothAdapter?.startLeScan(leScanCallback)
    }

    /**
     * 搜索设备回调
     */
    private var leScanCallback: BluetoothAdapter.LeScanCallback = BluetoothAdapter.LeScanCallback { bluetoothDevice, i, bytes ->
        val name: String? = bluetoothDevice?.name//搜索到的设备的名称
        val address: String? = bluetoothDevice?.name//搜索到的设备的地址
        //打个Log测试一下
        L.d("设备的名称$name,设备的地址$address")
        //因为我们会搜索到一些蓝牙设备是没有名字的,显示不是我们需要的,我们把没有名字的排除掉
        if (name != null) {
            //将搜索到的设备添加到集合中
            mDate.add(Device(name, bluetoothDevice.address))
        }
        //最后recyclerView适配器更新我们的UI
        blueAdapter.notifyDataSetChanged()
    }

完整代码

import com.example.tonjies.bluetooth.R
import com.example.tonjies.bluetooth.adapter.BlueAdapter
import com.example.tonjies.bluetooth.module.blue.bean.Device
import com.example.tonjies.bluetooth.util.L


import kotlinx.android.synthetic.main.activity_blue.*
import org.jetbrains.anko.toast

/**
 * 博客源码:Android蓝牙开发「低功耗蓝牙4.0,搜索设备,连接设备,连接服务,发送指令」
 * 地址:
 */
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
class BlueActivity : AppCompatActivity(), View.OnClickListener {

    //布局管理器
    private var linearLayoutManager: LinearLayoutManager? = null

    //设备数据源
    private var mDate = ArrayList<Device>()

    //recyclerView适配器
    private var blueAdapter: BlueAdapter = BlueAdapter(this, mDate)

    //当前选中设备的地址,等会连接的时候就是拿着这个地址去连接我们的设备
    private var address: String? = null

    //声明蓝牙适配器
    private var bluetoothAdapter: BluetoothAdapter? = null


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_blue)
        //初始化控件
        initView()
        //初始化适配器
        initAdapter()
    }


    /**
     * 初始化控件
     */
    private fun initView() {
        btnSearch.setOnClickListener(this)
        btnConnect.setOnClickListener(this)
        btnSend.setOnClickListener(this)
        //加载适配器
        //加载布局管理器,设置recyclerView的排列方式是垂直排列,设置为false表示不进行列表顺序翻转操作
        linearLayoutManager = LinearLayoutManager(this, LinearLayout.VERTICAL, false)
        ryDevice.layoutManager = linearLayoutManager
        ryDevice.adapter = blueAdapter//设置适配器
        //加载测试数据
        for (s in 1..36) {
            mDate.add(Device(s.toString(), s.toString()))
        }
        blueAdapter.notifyDataSetChanged()//通知适配器更新,刷新UI
        //设置列表的点击事件回调
        blueAdapter.setOnItemClickListener(object : BlueAdapter.OnItemClickListener {
            //点击事件回调
            override fun onItemClick(view: View, position: Int) {
                //将点击的列表地址存储起来
                address = mDate[position].address
                //设置当前选中设备的名册个
                tvDeviceName.text = mDate[position].deviceName
                //弹出提示
                toast("当前设备的名称是:${mDate[position].deviceName},设备地址是:${mDate[position].address}")
            }
        })
    }


    /**
     * 初始化适配器
     * 该Api只能在Api版本18以上,Android4.3以上使用
     */
    @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private fun initAdapter() {
        val bluetoothManager: BluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        bluetoothAdapter = bluetoothManager.adapter
    }

    /**
     * 声明点击事件
     */
    override fun onClick(p0: View?) {
        when (p0?.id) {
            //开始搜索
            R.id.btnSearch -> {
                search()
//                toast("开始搜索")
            }
            //开始连接
            R.id.btnConnect -> {
                toast("开始连接")
            }
            //开始发送
            R.id.btnSend -> {
                toast("开始发送")
            }
        }
    }

    /**
     * 搜索设备
     */
    private fun search() {
        //判断设备的蓝牙设置是否已经打开,如果没有打开,我们弹出系统的蓝牙提示框,提示用户打开
        if (!bluetoothAdapter?.isEnabled!!) {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            startActivityForResult(enableBtIntent, 1)
        }
        //开始搜索蓝牙
        bluetoothAdapter?.startLeScan(leScanCallback)
    }

   /**
     * 搜索设备回调
     */
    private var leScanCallback: BluetoothAdapter.LeScanCallback = BluetoothAdapter.LeScanCallback { bluetoothDevice, i, bytes ->
        val name: String? = bluetoothDevice?.name//搜索到的设备的名称
        val address: String? = bluetoothDevice?.address//搜索到的设备的地址
        //打个Log测试一下
        L.d("设备的名称$name,设备的地址$address")
        //因为我们会搜索到一些蓝牙设备是没有名字的,显示不是我们需要的,我们把没有名字的排除掉
        if (name != null) {
            //将搜索到的设备添加到集合中
            mDate.add(Device(name, bluetoothDevice.address))
        }
        //最后recyclerView适配器更新我们的UI
        blueAdapter.notifyDataSetChanged()
    }

}

运行程序,查看log的输出,可以看到已经输出了设备的名称和Mac地址,该地址对于每个设备是唯一的,所以等等我们可以拿着这个地址去连接设备

搜索设备.png
3)完善搜索方法

上面已经搜索到了设备,但其实这样做还是会有问题的,就是设备被重复的显示了,这是因为我们的搜索回调方法是没有说搜索出来一个设备下次回调就不会再次出现这个设备这个机制的

为了避免这个问题,我们需要做列表去重复的操作,但是这个去重可能和我们正常的想法不太一样,因为如果是正常的,给你一个列表去重,你可以会这样做

只要再声明一个集合,去遍历原来集合,把新集合没有的值添加进去就可以了,但是这里我们原来的数组mDates一开始是没有值的

而且他还会再搜索的回调中不断添加新值,如果我们一开始就去去遍历一个没有值的数列,那它永远不会进入循环,那就没意义的,如

  /**
     * 搜索设备回调
     */
    private var leScanCallback: BluetoothAdapter.LeScanCallback = BluetoothAdapter.LeScanCallback { bluetoothDevice, i, bytes ->
        val name: String? = bluetoothDevice?.name//搜索到的设备的名称
        val address: String? = bluetoothDevice?.address//搜索到的设备的地址
        //打个Log测试一下
        L.d("设备的名称$name,设备的地址$address")
        //因为我们会搜索到一些蓝牙设备是没有名字的,显示不是我们需要的,我们把没有名字的排除掉
        if (name != null) {
            val iterator = mDate.iterator()
            while (iterator.hasNext()) {
                //在这里进行去重操作
                
                //将搜索到的设备添加到集合中
                mDate.add(Device(name, bluetoothDevice.address))
            }
        }
        //最后recyclerView适配器更新我们的UI
        blueAdapter.notifyDataSetChanged()
    }

因为mDates数列没有值,所以我们永远不会进入,所以我们这里应该设置两个变量,

  • 一个让蓝牙搜索到第一个设备的时候,直接添加到集合中,后面再次搜索到第二及其他设备时,在遍历集合进行去重判断
  • 第二个是用来判断搜索到的设备是否已经存在于该集合
    //控制循环,当值为1时,表示是第一个设备
    private var count: Int = 1

    //判断搜索到的设备在集合中是否已经存在
    private var boolean: Boolean = true

点击搜索方法时,赋值为1

    /**
 *搜索设备
  */
    private fun search() {
        //将控制去重的变量设置为1
        count=1
        //判断设备的蓝牙设置是否已经打开,如果没有打开,我们弹出系统的蓝牙提示框,提示用户打开
        if (!bluetoothAdapter?.isEnabled!!) {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            startActivityForResult(enableBtIntent, 1)
        }
        //开始搜索蓝牙
        bluetoothAdapter?.startLeScan(leScanCallback)
    }

然后是搜索回调

/**
     * 搜索设备回调
     */
    private var leScanCallback: BluetoothAdapter.LeScanCallback = BluetoothAdapter.LeScanCallback { bluetoothDevice, i, bytes ->
        val name: String? = bluetoothDevice?.name//搜索到的设备的名称
        val address: String? = bluetoothDevice?.address//搜索到的设备的地址
        //打个Log测试一下

        //因为我们会搜索到一些蓝牙设备是没有名字的,显示不是我们需要的,我们把没有名字的排除掉
        if (name != null) {
            //第一次添加的设备
            if (count == 1) {
                L.d("设备的名称$name,设备的地址$address")
                mDate.add(Device(name, bluetoothDevice.address))
            }
            //将count设置为0,表示之后搜索到的设备都不是第一台设备了
            count = 0
            //遍历集合
            val iterator = mDate.iterator()
            while (iterator.hasNext()) {
                //在这里进行去重操作
                val device: Device = iterator.next()
                //判断当前搜索到的设备和遍历集合的设备名称是否相当,相等表示该设备在集合中已经存在
                if (device.deviceName == name) {
                    //如果存在,将变量设置为false,结束循环
                    boolean = false
                    break
                } else {
                    boolean = true
                }
            }
            //如果当前搜索到的设备在列表中不存在,则添加到集合中
            if (boolean) {
                L.d("设备的名称$name,设备的地址$address")
                //将搜索到的设备添加到集合中
                mDate.add(Device(name, bluetoothDevice.address))
            }
            //最后recyclerView适配器更新我们的UI
            blueAdapter.notifyDataSetChanged()
        }
    }

这样我们的去重就完成了,运行一下程序,现在搜索到的设备就是真实的设备数了

搜索设备2.png

还有两个问题,第一个是我们这样让蓝牙一直搜索,其实是比较耗费电量的,所以我们要设置让程序搜索一段时间后,自动停止搜索设备

   //声明handler,控制蓝牙搜索一段时间后停止
    private var handelr:Handler=Handler()
/**
     * 搜索设备
     */
    private fun search() {
        //将控制去重的变量设置为1
        count = 1
        //判断设备的蓝牙设置是否已经打开,如果没有打开,我们弹出系统的蓝牙提示框,提示用户打开
        if (!bluetoothAdapter?.isEnabled!!) {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            startActivityForResult(enableBtIntent, 1)
        }
        //30秒后停止搜索
        handler.postDelayed({
            mScanning = false
            bluetoothAdapter?.stopLeScan(leScanCallback)
        }, 30000)
        //开始搜索蓝牙
        bluetoothAdapter?.startLeScan(leScanCallback)
    }

第二个问题就是我们还没有添加暂停搜索的方法,这里我们通过一个变量来控制,我们还是只有一个按钮,点一下按钮让程序停止搜索,再点一下按钮让程序继续搜索

    //默认为false,表示当前的状态为停止搜索
    private var mScanning: Boolean = false

当开始搜索时间mScanning设置为true,表示当前的状态为正在搜索

 
    /**
     * 搜索设备
     */
    private fun search() {
        //将控制去重的变量设置为1
        count = 1
        //判断设备的蓝牙设置是否已经打开,如果没有打开,我们弹出系统的蓝牙提示框,提示用户打开
        if (!bluetoothAdapter?.isEnabled!!) {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            startActivityForResult(enableBtIntent, 1)
        }
        //如果检测到当前的状态是正在搜索时点击的搜索设备,我们停止搜索蓝牙设备
        if (mScanning) {
            mScanning = false
            bluetoothAdapter?.stopLeScan(leScanCallback)
            toast("停止搜索")
            btnSearch.text = "开始搜索"
        }
        //如果检测到当前的状态是正在停止时点击的搜索设备,我们就清空之前的集合,搜索蓝牙设备
        else{
            btnSearch.text = "停止搜索"
            mDate.clear()
            blueAdapter.notifyDataSetChanged()
            //30秒后停止搜索
            handler.postDelayed({
                mScanning = false
                bluetoothAdapter?.stopLeScan(leScanCallback)
            }, 30000)
            //开始搜索
            mScanning = true
            //开始搜索蓝牙
            bluetoothAdapter?.startLeScan(leScanCallback)
        }

    }

这里要注意,在实际操作时,点击搜索设备时,列表处于无法被点击的状态,如果这时候要存储列表地址,要先点击开始搜索,将搜索停止下来(这应该算是一个bug,暂时不知道为什么会这样)

Activity完整代码

import com.example.tonjies.bluetooth.R
import com.example.tonjies.bluetooth.adapter.BlueAdapter
import com.example.tonjies.bluetooth.base.BaseActivity
import com.example.tonjies.bluetooth.module.blue.bean.Device
import com.example.tonjies.bluetooth.util.L


import kotlinx.android.synthetic.main.activity_blue.*
import org.jetbrains.anko.toast

/**
 * 博客源码:Android蓝牙开发「低功耗蓝牙4.0,搜索设备,连接设备,连接服务,发送指令」
 * 地址:
 */
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
class BlueActivity : BaseActivity(), View.OnClickListener {

    //布局管理器
    private var linearLayoutManager: LinearLayoutManager? = null

    //设备数据源
    private var mDate = ArrayList<Device>()

    //recyclerView适配器
    private var blueAdapter: BlueAdapter = BlueAdapter(this, mDate)

    //当前选中设备的地址,等会连接的时候就是拿着这个地址去连接我们的设备
    private var address: String? = null

    //声明蓝牙适配器
    private var bluetoothAdapter: BluetoothAdapter? = null

    //控制循环,当值为1时,表示是第一个设备
    private var count: Int = 0

    //判断搜索到的设备在设备中是否存在
    private var boolean: Boolean = true

    //默认为false,表示当前的状态为停止搜索
    private var mScanning: Boolean = false

    //声明handler,控制蓝牙搜索一段时间后停止
    private var handler: Handler = Handler()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_blue)
        //初始化控件
        initView()
        //初始化适配器
        initAdapter()
    }


    /**
     * 初始化控件
     */
    private fun initView() {
        btnSearch.setOnClickListener(this)
        btnConnect.setOnClickListener(this)
        btnSend.setOnClickListener(this)
        //加载适配器
        //加载布局管理器,设置recyclerView的排列方式是垂直排列,设置为false表示不进行列表顺序翻转操作
        linearLayoutManager = LinearLayoutManager(this, LinearLayout.VERTICAL, false)
        ryDevice.layoutManager = linearLayoutManager
        ryDevice.adapter = blueAdapter//设置适配器
        //加载测试数据
//        for (s in 1..36) {
//            mDate.add(Device(s.toString(), s.toString()))
//        }
        blueAdapter.notifyDataSetChanged()//通知适配器更新,刷新UI
        //设置列表的点击事件回调
        blueAdapter.setOnItemClickListener(object : BlueAdapter.OnItemClickListener {
            //点击事件回调
            override fun onItemClick(view: View, position: Int) {
                L.d("点击事件")
                //将点击的列表地址存储起来
                address = mDate[position].address
                //设置当前选中设备的名册个
                tvDeviceName.text = mDate[position].deviceName
                //弹出提示
                toast("当前设备的名称是:${mDate[position].deviceName},设备地址是:${mDate[position].address}")
            }
        })
    }


    /**
     * 初始化适配器
     * 该Api只能在Api版本18以上,Android4.3以上使用
     */
    @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private fun initAdapter() {
        val bluetoothManager: BluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        bluetoothAdapter = bluetoothManager.adapter
    }

    /**
     * 声明点击事件
     */
    override fun onClick(p0: View?) {
        when (p0?.id) {
            //开始搜索
            R.id.btnSearch -> {
                search()
//                toast("开始搜索")
            }
            //开始连接
            R.id.btnConnect -> {
                toast("开始连接")
            }
            //开始发送
            R.id.btnSend -> {
                toast("开始发送")
            }
        }
    }

    /**
     * 搜索设备
     */
    private fun search() {
        //将控制去重的变量设置为1
        count = 1
        //判断设备的蓝牙设置是否已经打开,如果没有打开,我们弹出系统的蓝牙提示框,提示用户打开
        if (!bluetoothAdapter?.isEnabled!!) {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            startActivityForResult(enableBtIntent, 1)
        }
        //如果检测到当前的状态是正在搜索时点击的搜索设备,我们停止搜索蓝牙设备
        if (mScanning) {
            mScanning = false
            bluetoothAdapter?.stopLeScan(leScanCallback)
            toast("停止搜索")
            btnSearch.text = "开始搜索"
        }
        //如果检测到当前的状态是正在停止时点击的搜索设备,我们就清空之前的集合,搜索蓝牙设备
        else{
            btnSearch.text = "停止搜索"
            mDate.clear()
            blueAdapter.notifyDataSetChanged()
            //30秒后停止搜索
            handler.postDelayed({
                mScanning = false
                bluetoothAdapter?.stopLeScan(leScanCallback)
            }, 30000)
            //开始搜索
            mScanning = true
            //开始搜索蓝牙
            bluetoothAdapter?.startLeScan(leScanCallback)
        }

    }

    /**
     * 搜索设备回调
     */
    private var leScanCallback: BluetoothAdapter.LeScanCallback = BluetoothAdapter.LeScanCallback { bluetoothDevice, i, bytes ->
        val name: String? = bluetoothDevice?.name//搜索到的设备的名称
        val address: String? = bluetoothDevice?.address//搜索到的设备的地址
        //打个Log测试一下

        //因为我们会搜索到一些蓝牙设备是没有名字的,显示不是我们需要的,我们把没有名字的排除掉
        if (name != null) {
            //第一次添加的设备
            if (count == 1) {
                L.d("设备的名称$name,设备的地址$address")
                mDate.add(Device(name, bluetoothDevice.address))
            }
            //将count设置为0,表示之后搜索到的设备都不是第一台设备了
            count = 0
            //遍历集合
            val iterator = mDate.iterator()
            while (iterator.hasNext()) {
                //在这里进行去重操作
                val device: Device = iterator.next()
                //判断当前搜索到的设备和遍历集合的设备名称是否相当,相等表示该设备在集合中已经存在
                if (device.deviceName == name) {
                    //如果存在,将变量设置为false,结束循环
                    boolean = false
                    break
                } else {
                    boolean = true
                }
            }
            //如果当前搜索到的设备在列表中不存在,则添加到集合中
            if (boolean) {
                L.d("设备的名称$name,设备的地址$address")
                //将搜索到的设备添加到集合中
                mDate.add(Device(name, bluetoothDevice.address))
            }
            //最后recyclerView适配器更新我们的UI
            blueAdapter.notifyDataSetChanged()
        }
    }
}

搜索结果.gif

这一小节就到这里啦,希望能帮助到你_

蓝牙操作方面还是比较复杂的,所以我讲解的比较细致,希望你们不会觉得我讲的很绕

文章参考自以下教程,十分感谢:

Android BLE低功耗蓝牙开发极简系列(一)之扫描与连接

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

相关文章

网友评论

    本文标题:Android蓝牙开发「低功耗蓝牙4.0,搜索设备,显示搜索设备

    本文链接:https://www.haomeiwen.com/subject/mqarcqtx.html