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

相关文章
|
2月前
|
XML Java 数据库
安卓项目:app注册/登录界面设计
本文介绍了如何设计一个Android应用的注册/登录界面,包括布局文件的创建、登录和注册逻辑的实现,以及运行效果的展示。
182 0
安卓项目:app注册/登录界面设计
|
25天前
|
人工智能 自然语言处理 前端开发
100个降噪蓝牙耳机免费领,用通义灵码从 0 开始打造一个完整APP
打开手机,录制下你完成的代码效果,发布到你的社交媒体,前 100 个@玺哥超Carry、@通义灵码的粉丝,可以免费获得一个降噪蓝牙耳机。
5998 16
|
2月前
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
74 15
Android 系统缓存扫描与清理方法分析
|
19天前
|
人工智能 自然语言处理
完成 100个降噪蓝牙耳机免费领,用通义灵码从 0 开始打造一个完整APP
通义灵码 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。
77 2
|
21天前
完成 100个降噪蓝牙耳机免费领,用通义灵码从 0 开始打造一个完整APP
完成 100个降噪蓝牙耳机免费领,用通义灵码从 0 开始打造一个完整APP @玺哥超Carry @通义灵码
|
2月前
|
移动开发 前端开发 Android开发
开发指南059-App实现微信扫描登录
App是用uniapp开发的,打包为apk,上传到安卓平板中使用
|
3月前
|
存储 开发工具 Android开发
使用.NET MAUI开发第一个安卓APP
【9月更文挑战第24天】使用.NET MAUI开发首个安卓APP需完成以下步骤:首先,安装Visual Studio 2022并勾选“.NET Multi-platform App UI development”工作负载;接着,安装Android SDK。然后,创建新项目时选择“.NET Multi-platform App (MAUI)”模板,并仅针对Android平台进行配置。了解项目结构,包括`.csproj`配置文件、`Properties`配置文件夹、平台特定代码及共享代码等。
259 2
|
2月前
|
安全 网络安全 Android开发
深度解析:利用Universal Links与Android App Links实现无缝网页至应用跳转的安全考量
【10月更文挑战第2天】在移动互联网时代,用户经常需要从网页无缝跳转到移动应用中。这种跳转不仅需要提供流畅的用户体验,还要确保安全性。本文将深入探讨如何利用Universal Links(仅限于iOS)和Android App Links技术实现这一目标,并分析其安全性。
355 0
|
3月前
|
XML 数据库 Android开发
10分钟手把手教你用Android手撸一个简易的个人记账App
该文章提供了使用Android Studio从零开始创建一个简单的个人记账应用的详细步骤,包括项目搭建、界面设计、数据库处理及各功能模块的实现方法。
|
存储 缓存 安全
Android14 适配之——现有 App 安装到 Android14 手机上需要注意些什么?
Android14 适配之——现有 App 安装到 Android14 手机上需要注意些什么?
534 0