Android Ble蓝牙App(一)扫描(下)

简介: Android Ble蓝牙App(一)扫描(下)

Android Ble蓝牙App(一)扫描(上)https://developer.aliyun.com/article/1407782


② 点击监听

首先是ScanActivity的一些基本配置,如下所示:

class ScanActivity : BaseActivity() {
    private val TAG = ScanActivity::class.java.simpleName
    private val binding by viewBinding(ActivityScanBinding::inflate)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_scan)
    }
}


然后增加布局中按钮的点击监听,创建一个initView()函数,在onCreate()中调用它,代码如下所示:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_scan)
        initView()
    }
    private fun initView() {
        binding.requestBluetoothConnectLay.btnRequestConnectPermission.setOnClickListener(this)
        binding.enableBluetoothLay.btnEnableBluetooth.setOnClickListener(this)
        binding.requestLocationLay.btnRequestLocationPermission.setOnClickListener(this)
        binding.enableLocationLay.btnEnableLocation.setOnClickListener(this)
        binding.requestBluetoothScanLay.btnRequestScanPermission.setOnClickListener(this)
        binding.toolbar.setOnClickListener(this)
        binding.tvScanStatus.setOnClickListener(this)
    }


然后实现点击监听

class ScanActivity : BaseActivity(), View.OnClickListener


重写onClick()函数,代码如下所示:

    override fun onClick(v: View) {
        when (v.id) {
            //请求蓝牙连接权限
            R.id.btn_request_connect_permission -> {}
            //打开蓝牙开关
            R.id.btn_enable_bluetooth -> {}
            //请求定位权限
            R.id.btn_request_location_permission -> {}
            //打开位置开关
            R.id.btn_enable_location -> {}
            //请求蓝牙扫描权限
            R.id.btn_request_scan_permission -> {}
            //扫描或停止扫描
            R.id.tv_scan_status -> {}
            else -> {}
        }
    }


在这里我们先不写内容,后面再完善,然后我们可以先处理权限,再重写Activity的onResume()函数,代码如下所示:

    override fun onResume() {
        super.onResume()
        if (isAndroid12()) {
            //蓝牙连接
            binding.requestBluetoothConnectLay.root.visibility = if (hasBluetoothConnect()) View.GONE else View.VISIBLE
            if (!hasBluetoothConnect()) {
                Log.d(TAG, "onResume: 未获取蓝牙连接权限")
                return
            }
            //打开蓝牙开关
            binding.enableBluetoothLay.root.visibility = if (isOpenBluetooth()) View.GONE else View.VISIBLE
            if (!isOpenBluetooth()) {
                Log.d(TAG, "onResume: 未打开蓝牙")
                return
            }
            //蓝牙扫描
            binding.requestBluetoothScanLay.root.visibility = if (hasBluetoothScan()) View.GONE else View.VISIBLE
            if (!hasBluetoothScan()) {
                Log.d(TAG, "onResume: 未获取蓝牙扫描权限")
                return
            }
        }
        //打开蓝牙
        binding.enableBluetoothLay.root.visibility = if (isOpenBluetooth()) View.GONE else View.VISIBLE
        if (!isOpenBluetooth()) {
            Log.d(TAG, "onResume: 未打开蓝牙")
            return
        }
        //打开定位
        binding.enableLocationLay.root.visibility = if (isOpenLocation()) View.GONE else View.VISIBLE
        if (!isOpenLocation()) {
            Log.d(TAG, "onResume: 未打开位置")
            return
        }
        //请求定位
        binding.requestLocationLay.root.visibility = if (hasCoarseLocation() && hasAccessFineLocation()) View.GONE else View.VISIBLE
        if (!hasAccessFineLocation()) {
            Log.d(TAG, "onResume: 未获取定位权限")
            return
        }
        binding.tvScanStatus.visibility = View.VISIBLE
        //开始扫描
    }


③ 扫描处理

  在这个函数中对activity_scan.xml中引入的布局判断是否显示,在请求权限或者是打开开关之后都会触发这个函数,然后进行检查,当所有检查都通过之后说明你可以开始扫描了。那么如果要扫描,我们需要得到BleCore的对象,先声明,然后在onCreate中进行实例化。

    private lateinit var bleCore: BleCore
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        bleCore = (application as BleApp).getBleCore()
    }   


下面我们可以写扫描相关的方法,代码如下所示:

    private fun startScan() {
        bleCore?.startScan()
        binding.tvScanStatus.text = "停止"
        binding.pbScanLoading.visibility = View.VISIBLE
    }
    private fun stopScan() {
        bleCore?.stopScan()
        binding.tvScanStatus.text = "搜索"
        binding.pbScanLoading.visibility = View.INVISIBLE
    }


这里就是开始和停止扫描,别忘了还有扫描回调,这个回调应该写在哪里,首先是在onCreate()函数中,代码如下:

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        //设置扫描回调
        if (isOpenBluetooth()) bleCore!!.setPhyScanCallback(this@ScanActivity)
    }


这里还判断了一下是否开启蓝牙,扫描的结果需要实现BleScanCallback接口,如下所示:

class ScanActivity : BaseActivity(), View.OnClickListener, BleScanCallback 


重写onScanResult()函数,如下所示:

    /**
     * 扫描回调
     */
    override fun onScanResult(result: ScanResult) {
    }


④ 广播处理

然后别忘记了我们还有一个广播处理,在onCreate()函数中进行广播注册,代码如下所示:

    override fun onCreate(savedInstanceState: Bundle?) {
    ...
        //注册广播
        registerReceiver(
            ScanReceiver().apply { setCallback(this@ScanActivity) },
            IntentFilter().apply {
                addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
                addAction(LocationManager.PROVIDERS_CHANGED_ACTION)
            })
    }


实现接口ReceiverCallback,代码如下所示:

class ScanActivity : BaseActivity(), View.OnClickListener, BleScanCallback, ReceiverCallback


重写里面的函数,代码如下所示:

    /**
     * 蓝牙关闭
     */
    override fun bluetoothClose() {
    }
    /**
     * 位置关闭
     */
    override fun locationClose() {
    }


四、权限处理


下面我们进行权限和开关的请求处理,在ScanActivity中新增如下代码:

    //蓝牙连接权限
    private val requestConnect =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) {
            showMsg(if (it) "可以打开蓝牙" else "Android12 中不授予此权限无法打开蓝牙")
        }
    //启用蓝牙
    private val enableBluetooth =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == Activity.RESULT_OK) {
                showMsg("蓝牙已打开")
                Log.d(TAG, ": 蓝牙已打开")
                bleCore.setPhyScanCallback(this@ScanActivity)
            }
        }
    //请求定位
    private val requestLocation =
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
            val coarseLocation = result[Manifest.permission.ACCESS_COARSE_LOCATION]
            val fineLocation = result[Manifest.permission.ACCESS_FINE_LOCATION]
            if (coarseLocation == true && fineLocation == true) {
                //开始扫描设备
                showMsg("定位权限已获取")
                if (isOpenBluetooth()) bleCore.setPhyScanCallback(this@ScanActivity)
            }
        }
    //启用定位
    private val enableLocation =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == Activity.RESULT_OK) {
                showMsg("位置已打开")
                Log.d(TAG, ": 位置已打开")
                if (isOpenBluetooth()) bleCore.setPhyScanCallback(this@ScanActivity)
            }
        }
    //蓝牙连接权限
    private val requestScan =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) {
            showMsg(if (it) "可以开始扫描设备了" else "Android12 Android12 中不授予此权限无法扫描蓝牙")
        }


这里使用了Activity Result API,需要注意的是它们是与onCreate()函数平级的,下面修改onClick()函数中的代码:

    override fun onClick(v: View) {
        when (v.id) {
            //请求蓝牙连接权限
            R.id.btn_request_connect_permission -> if (isAndroid12()) requestConnect.launch(Manifest.permission.BLUETOOTH_CONNECT)
            //打开蓝牙开关
            R.id.btn_enable_bluetooth -> enableBluetooth.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
            //请求定位权限
            R.id.btn_request_location_permission -> requestLocation.launch(
                arrayOf(
                    Manifest.permission.ACCESS_COARSE_LOCATION,
                    Manifest.permission.ACCESS_FINE_LOCATION
                )
            )
            //打开位置开关
            R.id.btn_enable_location -> enableLocation.launch(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
            //请求蓝牙扫描权限
            R.id.btn_request_scan_permission -> if (isAndroid12()) requestScan.launch(Manifest.permission.BLUETOOTH_SCAN)
            //扫描或停止扫描
            R.id.tv_scan_status -> if (bleCore.isScanning()) stopScan() else startScan()
            else -> {}
        }
    }


  这里就比较的简单了,下面再修改bluetoothClose()locationClose()函数,在回调时都判断当前是否正在扫描,在扫描则停止,同时显示对应的布局。

    override fun bluetoothClose() {
        //蓝牙关闭时停止扫描
        if (bleCore.isScanning()) {
            stopScan()
            binding.enableBluetoothLay.root.visibility = View.VISIBLE
        }
    }
    override fun locationClose() {
        //位置关闭时停止扫描
        if (bleCore.isScanning()) {
            stopScan()
            binding.enableLocationLay.root.visibility = View.VISIBLE
        }
    }


最后再增加一个onStop()函数,代码如下:

    override fun onStop() {
        super.onStop()
        //页面停止时停止扫描
        if (bleCore.isScanning()) stopScan()
    }


当页面销毁了或者是进入后台了,那么触发回调,停止扫描。


五、扫描结果


  要显示扫描结果,首先要做的是定义一个类去装载扫描结果,在ble包下新建一个BleDevice数据类,代码如下所示:

data class BleDevice(
    var realName: String? = "Unknown device", //蓝牙设备真实名称
    var macAddress: String, //蓝牙设备Mac地址
    var rssi: Int, //信号强度
    var device: BluetoothDevice,//蓝牙设备
    var gatt: BluetoothGatt? = null//gatt
)


扫描的结果我们可以用列表来展示,选择使用RecyclerView,那么相应的会使用到适配器。


① 列表适配器

首先创建适配器的布局,在layout下新建一个item_device_rv.xml,代码如下所示:

<?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:id="@+id/item_device"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="1dp"
    android:background="@color/white"
    android:foreground="?attr/selectableItemBackground"
    android:orientation="vertical">
    <ImageView
        android:id="@+id/imageView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:src="@drawable/ic_bluetooth_blue"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <TextView
        android:id="@+id/tv_device_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:ellipsize="end"
        android:singleLine="true"
        android:text="设备名称"
        android:textColor="@color/black"
        android:textSize="16sp"
        app:layout_constraintStart_toEndOf="@+id/imageView2"
        app:layout_constraintTop_toTopOf="parent" />
    <TextView
        android:id="@+id/tv_mac_address"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:ellipsize="end"
        android:singleLine="true"
        android:text="Mac地址"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="@+id/tv_device_name"
        app:layout_constraintTop_toBottomOf="@+id/tv_device_name" />
    <TextView
        android:id="@+id/tv_rssi"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:text="信号强度"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


  这里的内容不多,主要内容就是设备名称、地址、信号强度,下面我们创建适配器,在com.llw.goodble包下新建一个adapter包,该包下新建一个OnItemClickListener接口,用于实现Item的点击监听,代码如下所示:

interface OnItemClickListener {
    fun onItemClick(view: View?, position: Int)
}


下面我们写适配器,在adapter包下新建一个BleDeviceAdapter类,代码如下所示:

class BleDeviceAdapter(
    private val mDevices: List<BleDevice>
) : RecyclerView.Adapter<BleDeviceAdapter.ViewHolder>() {
    private var mOnItemClickListener: OnItemClickListener? = null
    fun setOnItemClickListener(mOnItemClickListener: OnItemClickListener?) {
        this.mOnItemClickListener = mOnItemClickListener
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val viewHolder = ViewHolder(ItemDeviceRvBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        viewHolder.binding.itemDevice.setOnClickListener { v ->
            if (mOnItemClickListener != null) mOnItemClickListener!!.onItemClick(v, viewHolder.adapterPosition)
        }
        return viewHolder
    }
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val bleDevice: BleDevice = mDevices[position]
        val rssi: Int = bleDevice.rssi
        holder.binding.tvRssi.text = String.format(Locale.getDefault(), "%d dBm", rssi)
        //设备名称
        holder.binding.tvDeviceName.text = bleDevice.realName
        //Mac地址
        holder.binding.tvMacAddress.text = bleDevice.macAddress
    }
    override fun getItemCount() = mDevices.size
    class ViewHolder(itemView: ItemDeviceRvBinding) : RecyclerView.ViewHolder(itemView.root) {
        var binding: ItemDeviceRvBinding
        init {
            binding = itemView
        }
    }
}


  这里就是基本的写法,结合了ViewBinding,在onBindViewHolder()中进行数据渲染,那么适配器就写好了,下面我们回到ScanActivity中,去完成后的扫描结果显示。


② 扫描结果处理

首先我们声明变量,在ScanActivity中增加如下代码:

    private var mAdapter: BleDeviceAdapter? = null
    //设备列表
    private val mList: MutableList<BleDevice> = mutableListOf()
    private fun findIndex(bleDevice: BleDevice, mList: MutableList<BleDevice>): Int {
        var index = 0
        for (devi in mList) {
            if (bleDevice.macAddress.contentEquals(devi.macAddress)) return index
            index += 1
        }
        return -1
    }


这个findIndex()函数用于在列表中找是否有添加过设备,下面修改扫描的回调函数onScanResult(),代码如下所示:

    override fun onScanResult(result: ScanResult) {
        if (result.scanRecord!!.deviceName == null) return
        if (result.scanRecord!!.deviceName!!.isEmpty()) return
        val bleDevice = BleDevice(
            result.scanRecord!!.deviceName,
            result.device.address,
            result.rssi,
            result.device
        )
        Log.d(TAG, "onScanResult: ${bleDevice.macAddress}")
        if (mList.size == 0) {
            mList.add(bleDevice)
        } else {
            val index = findIndex(bleDevice, mList)
            if (index == -1) {
                //添加新设备
                mList.add(bleDevice)
            } else {
                //更新已有设备的rssi
                mList[index].rssi = bleDevice.rssi
            }
        }
        //如果未扫描到设备,则显示空内容布局
        binding.emptyLay.root.visibility = if (mList.size == 0) View.VISIBLE else View.GONE
        //如果mAdapter为空则会执行run{}中的代码,进行相关配置,最终返回配置的结果mAdapter
        mAdapter ?: run {
            mAdapter = BleDeviceAdapter(mList)
            binding.rvDevice.apply {
                (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
                layoutManager = LinearLayoutManager(this@ScanActivity)
                adapter = mAdapter
            }
            mAdapter!!.setOnItemClickListener(this@ScanActivity)
            mAdapter
        }
        mAdapter!!.notifyDataSetChanged()
    }


那么在开始扫描的时候我们最好清理一下列表,修改一下startScan()函数,代码如下所示:

    private fun startScan() {
        mList.clear()
        mAdapter?.notifyDataSetChanged()
        bleCore.startScan()
        binding.tvScanStatus.text = "停止"
        binding.pbScanLoading.visibility = View.VISIBLE
    }


同时在扫描回调中还有一个适配器的Item点击监听,先实现它,修改代码:

class ScanActivity : BaseActivity(), View.OnClickListener, BleScanCallback, ReceiverCallback,
    OnItemClickListener {


重写onItemClick()函数,代码如下:

    override fun onItemClick(view: View?, position: Int) {
        if (bleCore.isScanning()) stopScan()
        //选中设备处理
        val intent = Intent()
        intent.putExtra("device", mList[position].device)
        setResult(RESULT_OK, intent)
        finish()
    }


  我们是通过MainActivity进入到ScanActivity的,那么在选中设备之后将设备对象返回并销毁当前页面。ScanActivity中还有最后一个修改的地方,那就是在onResume()函数中增加开始扫描的代码,代码如下所示:

    override fun onResume() {
        ...
        //开始扫描
        if (!bleCore.isScanning()) startScan()
    }


这里的意思就是当进入页面检查到条件都满足时就开始扫描。


③ 接收结果

最后我们在MainActivity中接收结果,修改代码如下所示:

class MainActivity : BaseActivity() {
    private val binding by viewBinding(ActivityMainBinding::inflate)
    @SuppressLint("MissingPermission")
    private val scanIntent =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == Activity.RESULT_OK) {
                if (result.data == null) return@registerForActivityResult
                //获取选中的设备
                val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    result.data!!.getParcelableExtra("device", BluetoothDevice::class.java)
                } else {
                    result.data!!.getParcelableExtra("device") as BluetoothDevice?
                }
                showMsg("${device?.name} , ${device?.address}")
            }
        }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        binding.toolbar.setNavigationOnClickListener { scanIntent.launch(Intent(this,ScanActivity::class.java)) }
    }
}


下面我们运行一下:

51434893b9ee48938c30dc842ec50f2e.gif


六、源码


如果对你有所帮助的话,不妨 StarFork,山高水长,后会有期~

源码地址:GoodBle

相关文章
|
3月前
|
Android开发 开发者 iOS开发
APP开发后如何上架,上架Android应用市场前要准备什么
移动应用程序(APP)的开发已经成为现代企业和开发者的常见实践。然而,开发一个成功的APP只是第一步,将其上架到应用商店让用户下载和使用是实现其潜力的关键一步。
|
6天前
|
测试技术 Android开发
Android App获取不到pkgInfo信息问题原因
Android App获取不到pkgInfo信息问题原因
14 0
|
1月前
|
移动开发 数据安全/隐私保护
HC05蓝牙模块与手机APP连接
HC05蓝牙模块与手机APP连接
42 1
|
1月前
|
设计模式 测试技术 数据库
基于Android的食堂点餐APP的设计与实现(论文+源码)_kaic
基于Android的食堂点餐APP的设计与实现(论文+源码)_kaic
|
2月前
|
安全 Java 数据挖掘
当 App 有了系统权限,真的可以为所欲为? Android Performance Systrace
当 App 有了系统权限,真的可以为所欲为? Android Performance Systrace 转载自: https://androidperformance.com/2023/05/14/bad-android-app-with-system-permissions/#/0-Dex-%E6%96%87%E4%BB%B6%E4%BF%A1%E6%81%AF
31 0
|
3月前
|
Android开发
闲暇时间收集和整理的Android的一些常用的App
闲暇时间收集和整理的Android的一些常用的App
14 0
|
3月前
|
Android开发 UED 开发者
解释Android App Bundle是什么,它的优势是什么?
解释Android App Bundle是什么,它的优势是什么?
59 0
|
3月前
|
传感器 内存技术
毕业设计 江科大STM32的智能温室控制蓝牙声光报警APP系统设计
毕业设计 江科大STM32的智能温室控制蓝牙声光报警APP系统设计
|
3月前
|
JavaScript Android开发
Cordova 后台运行 Android APP
Cordova 后台运行 Android APP