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

相关文章
|
5月前
|
XML 自然语言处理 Android开发
🌐Android国际化与本地化全攻略!让你的App走遍全球无障碍!🌍
【7月更文挑战第28天】在全球化背景下,实现Android应用的国际化与本地化至关重要 for 用户基础扩展。本文通过旅游指南App案例,介绍全攻略。步骤包括资源文件拆分与命名、适配布局与方向、处理日期时间及货币格式、考虑文化习俗及进行详尽测试。采用Android Studio支持,创建如`res/values-en/strings.xml`等多语言资源文件夹,使用灵活布局解决文本长度差异问题,并通过用户反馈迭代优化。最终,打造一款能无缝融入全球各地文化的App。
209 3
|
2月前
|
移动开发 前端开发 Android开发
开发指南059-App实现微信扫描登录
App是用uniapp开发的,打包为apk,上传到安卓平板中使用
|
4月前
|
Android开发
Android 配置蓝牙遥控器键值
本文详细介绍了Android系统中配置蓝牙遥控器键值的步骤,包括查看设备号、配置键位映射文件(kl文件)、部署kl文件以及调试过程,确保蓝牙遥控器的按键能正确映射到Android系统对应的按键功能。
179 1
|
5月前
|
消息中间件 Android开发 开发者
🔍深度剖析Android内存泄漏,让你的App远离崩溃边缘,稳如老狗!🐶
【7月更文挑战第28天】在 Android 开发中,内存管理至关重要。内存泄漏可悄无声息地累积,最终导致应用崩溃或性能下滑。它通常由不正确地持有 Activity 或 Fragment 的引用引起。常见原因包括静态变量持有组件引用、非静态内部类误用、Handler 使用不当、资源未关闭及集合对象未清理。使用 Android Studio Profiler 和 LeakCanary 可检测泄漏,修复方法涉及使用弱引用、改用静态内部类、妥善管理 Handler 和及时释放资源。良好的内存管理是保证应用稳定性的基石。
95 4
|
5月前
|
XML 缓存 Android开发
🎯解锁Android性能优化秘籍!让你的App流畅如飞,用户爱不释手!🚀
【7月更文挑战第28天】在移动应用竞争中,性能是关键。掌握Android性能优化技巧对开发者至关重要。
45 2
|
4月前
|
负载均衡 网络协议 安全
【Azure 应用服务】Azure Web App的服务(基于Windows 操作系统部署)在被安全漏洞扫描时发现了TCP timestamps漏洞
【Azure 应用服务】Azure Web App的服务(基于Windows 操作系统部署)在被安全漏洞扫描时发现了TCP timestamps漏洞
|
6月前
|
ARouter IDE 开发工具
Android面试题之App的启动流程和启动速度优化
App启动流程概括: 当用户点击App图标,Launcher通过Binder IPC请求system_server启动Activity。system_server指示Zygote fork新进程,接着App进程向system_server申请启动Activity。经过Binder通信,Activity创建并回调生命周期方法。启动状态分为冷启动、温启动和热启动,其中冷启动耗时最长。优化技巧包括异步初始化、避免主线程I/O、类加载优化和简化布局。
83 3
Android面试题之App的启动流程和启动速度优化
|
5月前
|
机器学习/深度学习 人工智能 文字识别
文本,文字扫描01,OCR文本识别技术展示,一个安卓App,一个简单的设计,文字识别可以应用于人工智能,机器学习,车牌识别,身份证识别,银行卡识别,PaddleOCR+SpringBoot+Andr
文本,文字扫描01,OCR文本识别技术展示,一个安卓App,一个简单的设计,文字识别可以应用于人工智能,机器学习,车牌识别,身份证识别,银行卡识别,PaddleOCR+SpringBoot+Andr
|
5月前
|
Android开发
Android面试题经典之如何全局替换App的字体
在Android应用中替换字体有全局和局部方法。全局替换涉及在`Application`的`onCreate`中设置自定义字体,并创建新主题。局部替换则可在布局中通过`ResourcesCompat.getFont()`加载字体文件并应用于`TextView`。
85 2
|
5月前
|
存储 物联网 数据库
App Inventor 2 低功耗蓝牙 BlueToothLE 拓展中文文档(完整翻译加强版)
低功耗蓝牙,也称为蓝牙LE 或简称 BLE,是一种类似于经典蓝牙的新通信协议,不同之处在于它旨在消耗更少的功耗和成本,同时保持同等的功能。 因此,低功耗蓝牙是与耗电资源有限的物联网设备进行通信的首选。
171 0