Android Ble蓝牙App(一)扫描,Android Ble蓝牙App,设备扫描指南

马肤

温馨提示:这篇文章已超过419天没有更新,请注意相关的内容是否还可用!

摘要:本篇文章介绍了关于Android平台上的Ble蓝牙App的扫描功能。该App具备强大的蓝牙扫描能力,能够搜索并连接附近的蓝牙设备。通过简单的操作,用户可以轻松实现设备的发现和配对,为数据传输和通信提供了便捷的途径。该App的扫描功能为用户提供了高效的蓝牙设备管理体验。

Ble蓝牙App(一)扫描

  • 前言
  • 目录
  • 正文
    • 一、基本配置
    • 二、扫描准备
    • 三、扫描页面
      • ① 增加UI布局
      • ② 点击监听
      • ③ 扫描处理
      • ④ 广播处理
      • 四、权限处理
      • 五、扫描结果
        • ① 列表适配器
        • ② 扫描结果处理
        • ③ 接收结果
        • 六、源码

          前言

            关于低功耗的蓝牙介绍我已经做过很多了,只不过很多人不是奔着学习的目的去的,拿着源码就去运行,后面又发现连接设备后马上断开,然后不会自己看问题,这个现象就是快餐式的,你不了解里面的知识内容,自然就不知道是怎么回事,重复的问题我回答了好多次了。而我也是觉得写的有问题,本意上来说我是希望读者可以参考来写,能看一看文章内容,而结果绝大多数,看个标题看个运行效果,下载源码就运行,运行有问题就问你,没有什么思考。

            针对这个情况,我决定做了系列性的Ble蓝牙App,尽可能的避免在你运行的时候出现bug,所以这是一个低功耗蓝牙工具App,可以让你了解到一些东西。注意是低功耗,不是经典蓝牙,如果你不知道两者之间的区别,建议你先了解一下。本文的效果:

          Android Ble蓝牙App(一)扫描,Android Ble蓝牙App,设备扫描指南 第1张

          App下载地址:GoodBle

          目录

          • Ble蓝牙App(一)扫描
          • Ble蓝牙App(二)连接与发现服务
          • Ble蓝牙App(三)特性和属性
          • Ble蓝牙App(四)UI优化和描述符
          • Ble蓝牙App(五)数据操作

            正文

              本文将会重新创建一个项目,功能一个一个的做,尽量的做好每一个功能的优化,下面我们创建一个名为GoodBle的项目,语言为Kotlin。

            Android Ble蓝牙App(一)扫描,Android Ble蓝牙App,设备扫描指南 第2张

              至于为什么使用Kotlin,稳固一下,不然太久不用就会生疏,文本我们讲述的是扫描,你可能回想,一个扫描有什么好写,不就是开始、结束、显示设备嘛?至于单独作为一个功能来写一篇文章嘛?那么我们带着问题来看这篇文章,看看扫描到底有没有必要这样来做。

            一、基本配置

              当前我们创建项目有一个MainActivity,然后我们需要打开viewBinding的开关,在app的build.gradle中的android{}闭包中添加如下代码:

            	buildFeatures {
                    viewBinding true
                }
            

            然后Sync Now,同步一下,开启成功。随后我们就可以在Activity中使用ViewBinding了,常规的使用方式是这样的:

            class MainActivity : AppCompatActivity() {
                private lateinit var binding: ActivityMainBinding;
                
                override fun onCreate(savedInstanceState: Bundle?) {
                    super.onCreate(savedInstanceState)
                    binding = ActivityMainBinding.inflate(layoutInflater);
                    setContentView(binding.root)
                }
            }
            

              在Java中封装通常采用反射的方式,在Kotlin中如果要对ViewBinding进行封装的话同时利用上Kotlin的一些特性的话,可以这样做,原文地址如下:Viewbinding使用和委托封装,感觉写得蛮好的,太久没用Kotlin了,还是看了一会才看懂,感兴趣的可以看看。

              那么我们在com.llw.goodble下面创建一个base包,base包下创建BaseViewBinding.kt文件,里面的代码如下所示:

            package com.llw.goodble.base
            import android.app.Activity
            import android.view.LayoutInflater
            import androidx.viewbinding.ViewBinding
            import kotlin.properties.ReadOnlyProperty
            import kotlin.reflect.KProperty
            fun  viewBinding(viewInflater: (LayoutInflater) -> VB):
                    ReadOnlyProperty = ActivityViewBindingProperty(viewInflater)
            class ActivityViewBindingProperty(
                private val viewInflater: (LayoutInflater) -> VB
            ) : ReadOnlyProperty {
                private var binding: VB? = null
                override fun getValue(thisRef: Activity, property: KProperty): VB {
                    return binding ?: viewInflater(thisRef.layoutInflater).also {
                        thisRef.setContentView(it.root)
                        binding = it
                    }
                }
            }
            

            通过委托的方式进行封装,下面来看在MainActivity中怎么使用它,

            class MainActivity : AppCompatActivity() {
                private val binding by viewBinding(ActivityMainBinding::inflate)
                override fun onCreate(savedInstanceState: Bundle?) {
                    super.onCreate(savedInstanceState)
                    setContentView(R.layout.activity_main)
                }
            }
            

              使用起来很简单,后面就采用这种方式,你可以运行一下,看看有没有问题,然后我们可以再创建一个ScanActivity类,用于扫描页面,修改一下activity_main.xml中的代码,如下所示:

            
                
            
            

            这里用到了图标,代码如下所示:

                
                
                
                
                
            
            

            颜色值,在colors.xml中增加:

            	#FF5722
                #FFC107
                #FF9800
                #FFF3E0
                #F8F8F8
                #989898
            

              这里给toolbar设置导航图标,点击这个导航到扫描页面,不过再次之前我们可以在base包下再创建一个BaseActivity,这里面可以写一些常用的函数,代码如下所示:

            open class BaseActivity : AppCompatActivity() {
                private var context: Context? = null
                override fun onCreate(savedInstanceState: Bundle?) {
                    super.onCreate(savedInstanceState)
                    context = this
                }
                protected fun jumpActivity(clazz: Class?, finish: Boolean = false) {
                    startActivity(Intent(context, clazz))
                    if (finish) finish()
                }
                protected fun back(toolbar: Toolbar, finish: Boolean = false) =
                    toolbar.setNavigationOnClickListener { if (finish) finish() else onBackPressed() }
                protected fun showMsg(msg: CharSequence) =
                    Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
            	
            	protected open fun isAndroid12() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
                protected open fun hasAccessFineLocation() =
                    hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)
                protected open fun hasCoarseLocation() =
                    hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
                @RequiresApi(Build.VERSION_CODES.S)
                protected open fun hasBluetoothConnect() = hasPermission(Manifest.permission.BLUETOOTH_CONNECT)
                @RequiresApi(Build.VERSION_CODES.S)
                protected open fun hasBluetoothScan() = hasPermission(Manifest.permission.BLUETOOTH_SCAN)
                /**
                 * 检查是有拥有某权限
                 *
                 * @param permission 权限名称
                 * @return true 有  false 没有
                 */
                protected open fun hasPermission(permission: String) = checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
                /**
                 * 蓝牙是否打开
                 *
                 * @return true or false
                 */
                protected open fun isOpenBluetooth(): Boolean {
                    (getSystemService(BLUETOOTH_SERVICE) as BluetoothManager).also {
                        it.adapter ?: return false
                        return it.adapter.isEnabled
                    }
                }
                /**
                 * 位置是否打开
                 */
                protected open fun isOpenLocation(): Boolean {
                    val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
                    val gps = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
                    val network = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
                    val locationEnabled = isLocationEnabled()
                    Log.d("TAG", "gps: $gps,network:$network,locationEnabled:$locationEnabled")
                    return gps || network || locationEnabled
                }
                open fun isLocationEnabled(): Boolean {
                    val locationMode = try {
                        Settings.Secure.getInt(contentResolver, Settings.Secure.LOCATION_MODE)
                    } catch (e: SettingNotFoundException) {
                        e.printStackTrace()
                        return false
                    }
                    return locationMode != Settings.Secure.LOCATION_MODE_OFF
                }
            }
            

              这里面就是一些比较基础的方法,在后面扫描页面会用到的,然后再修改一下MainActivity中的代码,继承BaseActivity,点击中跳转扫描页面:

            class MainActivity : BaseActivity() {
                private val binding by viewBinding(ActivityMainBinding::inflate)
                override fun onCreate(savedInstanceState: Bundle?) {
                    super.onCreate(savedInstanceState)
                    setContentView(R.layout.activity_main)
                    binding.toolbar.setNavigationOnClickListener { jumpActivity(ScanActivity::class.java) }
                }
            }
            

            为了保持一样的UI效果,下面更改一下themes.xml中的代码,如下所示:

                
                
                    
                    @color/orange
                    @color/orange
                    @color/white
                    
                    @color/light_orange
                    @color/dark_orange
                    @color/white
                    
                    ?attr/colorPrimaryVariant
                    
                    @color/gray_white
                
                
                    @null
                    true
                    true
                    @android:color/transparent
                    true
                    @android:color/transparent
                
            
            

              主要就是修改状态栏颜色,窗口默认背景颜色,现在前置的条件都准备的差不多了,运行一下看看MainActivity的页面效果。

            Android Ble蓝牙App(一)扫描,Android Ble蓝牙App,设备扫描指南 第3张

            二、扫描准备

              下面在com.llw.goodble包下新建一个ble包,里面我们需要创建一些类来处理扫描的相关事务,首先在ble包下创建一个BleCore类,里面先不写内容,然后我们在ble包下新建一个scan包。在scan包下新建一个BleScanCallback接口,这是一个扫描回调接口,代码如下所示:

            interface BleScanCallback {
                /**
                 * 扫描结果
                 */
                fun onScanResult(result: ScanResult)
                /**
                 * 批量扫描结果
                 */
                fun onBatchScanResults(results: List) {}
                /**
                 * 扫描错误
                 */
                fun onScanFailed(failed: String) {}
            }
            

            同时在扫描页面需要监听一下蓝牙和定位是否打开,在scan包下添加一个广播接收器的ReceiverCallback 接口,代码如下所示:

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

            下面在scan创建广播接收器ScanReceiver,代码如下所示:

            class ScanReceiver : BroadcastReceiver() {
                private var callback: ReceiverCallback? = null
                fun setCallback(callback: ReceiverCallback?) {
                    this.callback = callback
                }
                private var isSend = 0
                override fun onReceive(context: Context, intent: Intent) {
                    val action = intent.action
                    if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
                        when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
                            BluetoothAdapter.STATE_OFF -> Log.d(TAG, "STATE_OFF Phone bluetooth off")
                            BluetoothAdapter.STATE_TURNING_OFF -> {
                                callback!!.bluetoothClose()
                                Log.d(TAG, "STATE_TURNING_OFF Phone bluetooth is turning off")
                            }
                            BluetoothAdapter.STATE_ON -> Log.d(TAG, "STATE_ON Phone bluetooth turned on")
                            BluetoothAdapter.STATE_TURNING_ON -> Log.d(TAG, "STATE_TURNING_ON Phone bluetooth is on")
                        }
                    } else if (action == LocationManager.PROVIDERS_CHANGED_ACTION) {
                        if (!isGPSOpen(context)) {
                            isSend++
                            if (isSend == 1) {
                                Log.d(TAG, "Positioning off")
                                callback!!.locationClose()
                            } else if (isSend == 4) {
                                isSend = 0
                            }
                        }
                    }
                }
                companion object {
                    val TAG: String = ScanReceiver::class.java.simpleName
                    fun isGPSOpen(context: Context): Boolean {
                        val locationMode = try {
                            Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE)
                        } catch (e: SettingNotFoundException) {
                            e.printStackTrace()
                            return false
                        }
                        return locationMode != Settings.Secure.LOCATION_MODE_OFF
                    }
                }
            }
            

              这里的代码相对简单就是广播接收器接收相关的动作信息,再进行回调,然后我们写一个用于扫描类,在scan包下新建一个BleScan类,代码如下所示:

            /**
             * 低功耗扫描类
             */
            @SuppressLint("MissingPermission", "InlinedApi")
            class BleScan private constructor(private val context: Context) {
                private var mScanFilters: List
                private var mScanSettings: ScanSettings
                private var bleScanCallback: BleScanCallback? = null
                var mIsScanning = false
                init {
                    mScanFilters = ArrayList()
                    mScanSettings = ScanSettings.Builder().build()
                }
                companion object {
                    @SuppressLint("StaticFieldLeak")
                    @Volatile
                    private var instance: BleScan? = null
                    private var mBluetoothAdapter: BluetoothAdapter? = null
                    private var mScanner: BluetoothLeScanner? = null
                    fun getInstance(context: Context) = instance ?: synchronized(this) {
                        instance ?: BleScan(context).also {
                            instance = it
                            val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
                            mBluetoothAdapter = manager.adapter
                            if (mBluetoothAdapter != null) {
                                mScanner = mBluetoothAdapter?.bluetoothLeScanner
                            }
                        }
                    }
                }
                /**
                 * 设置扫描过滤
                 */
                fun setScanFilters(scanFilters: List) {
                    mScanFilters = scanFilters
                }
                /**
                 * 设置扫描设置选项
                 */
                fun setScanSettings(scanSettings: ScanSettings) {
                    mScanSettings = scanSettings
                }
                /**
                 * 设置扫描回调
                 */
                fun setPhyScanCallback(bleScanCallback: BleScanCallback?) {
                    this.bleScanCallback = bleScanCallback
                }
                fun isScanning() = mIsScanning
                /**
                 * 扫描回调
                 */
                private val scanCallback: ScanCallback = object : ScanCallback() {
                    override fun onScanResult(callbackType: Int, result: ScanResult) {
                        bleScanCallback?.onScanResult(result)
                    }
                    override fun onBatchScanResults(results: List) {
                        bleScanCallback?.onBatchScanResults(results)
                    }
                    override fun onScanFailed(errorCode: Int) {
                        localScanFailed(
                            when (errorCode) {
                                SCAN_FAILED_ALREADY_STARTED -> "Fails to start scan as BLE scan with the same settings is already started by the app."
                                SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "Fails to start scan as app cannot be registered."
                                SCAN_FAILED_INTERNAL_ERROR -> "Fails to start scan due an internal error"
                                SCAN_FAILED_FEATURE_UNSUPPORTED -> "Fails to start power optimized scan as this feature is not supported."
                                else -> "UNKNOWN_ERROR"
                            }
                        )
                    }
                }
                /**
                 * 显示本地扫描错误
                 */
                private fun localScanFailed(failed: String) = bleScanCallback?.onScanFailed(failed)
                /**
                 * 开始扫描
                 */
                @SuppressLint("MissingPermission")
                fun startScan() {
                    if (!isOpenBluetooth()) {
                        localScanFailed("Bluetooth is not turned on.")
                        return
                    }
                    if (isAndroid12()) {
                        if (!hasBluetoothScan()) {
                            localScanFailed("Android 12 needs to dynamically request bluetooth scan permission.")
                            return
                        }
                    } else {
                        if (!hasAccessFineLocation()) {
                            localScanFailed("Android 6 to 12 requires dynamic request location permission.")
                            return
                        }
                    }
                    if (mIsScanning) {
                        localScanFailed("Currently scanning, please close the current scan and scan again.")
                        return
                    }
                    if (mScanner == null) mScanner = mBluetoothAdapter?.bluetoothLeScanner
                    if (!mBluetoothAdapter!!.isEnabled) {
                        localScanFailed("Bluetooth not turned on.")
                        return
                    }
                    mScanner?.startScan(mScanFilters, mScanSettings, scanCallback)
                    mIsScanning = true
                }
                /**
                 * 停止扫描
                 */
                fun stopScan() {
                    if (!mIsScanning) {
                        localScanFailed("Not currently scanning, your stop has no effect.")
                        return
                    }
                    if (mScanner == null) {
                        localScanFailed("BluetoothLeScanner is Null.")
                        return
                    }
                    if (!mBluetoothAdapter!!.isEnabled) {
                        localScanFailed("Bluetooth not turned on.")
                        return
                    }
                    mIsScanning = false
                    mScanner?.stopScan(scanCallback)
                }
                /**
                 * 是否打开蓝牙
                 */
                private fun isOpenBluetooth() = if (mBluetoothAdapter == null) {
                    localScanFailed("BluetoothAdapter is Null."); false
                } else mBluetoothAdapter!!.isEnabled
                private fun isAndroid12() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
                private fun hasAccessFineLocation() = hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)
                private fun hasBluetoothConnect() = hasPermission(Manifest.permission.BLUETOOTH_CONNECT)
                private fun hasBluetoothScan() = hasPermission(Manifest.permission.BLUETOOTH_SCAN)
                private fun hasPermission(permission: String) = context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
            }
            

              这里首先是创建一个单例,在里面对当前类和一些变量进行初始化,核心就是一个扫描回调,开始和停止扫描的方法处理。因为后面还需要写Ble相关的数据处理,因此在ble包下创建一个BleCore类,代码如下所示:

            class BleCore private constructor(private val context: Context) {
                @SuppressLint("StaticFieldLeak")
                companion object {
                    @SuppressLint("StaticFieldLeak")
                    @Volatile
                    private var instance: BleCore? = null
                    @SuppressLint("StaticFieldLeak")
                    private lateinit var bleScan: BleScan
                    fun getInstance(context: Context) = instance ?: synchronized(this) {
                        instance ?: BleCore(context).also {
                            instance = it
                            //蓝牙扫描
                            bleScan = BleScan.getInstance(context)
                        }
                    }
                }
                fun setPhyScanCallback(bleScanCallback: BleScanCallback) {
                    bleScan.setPhyScanCallback(bleScanCallback)
                }
                fun isScanning() = bleScan.isScanning()
                fun startScan() = bleScan.startScan()
                fun stopScan() = bleScan.stopScan()
            }
            

              同样是一个单例,在里面初始化BleScan,然后增加几个函数去调用BleScan中的函数,最后我们在com.llw.goodble包下创建一个BleApp类,代码如下所示:

            class BleApp : Application() {
                @SuppressLint("StaticFieldLeak")
                private lateinit var context: Context
                @SuppressLint("StaticFieldLeak")
                private lateinit var bleCore: BleCore
                override fun onCreate() {
                    super.onCreate()
                    context = applicationContext
                    //初始化Ble核心库
                    bleCore = BleCore.getInstance(this)
                }
                fun getBleCore() = bleCore
            }
            

              这里继承Application,通过自定义的方式在App启动的时候加载这个类,然后在onCreate()函数中,完成对于Ble核心类的初始化,顺便完成对于Ble扫描类的初始化。最后在AndroidManifest.xml中的application标签中配置这个BleApp,如下所示:

                
            

            三、扫描页面

              在Android12及以上版本,使用蓝牙时需要请求扫描、连接权限、如果还需要使用手机作为从机的话,就请求广播权限,后面会提到的,同时在低版本Android中我们扫描蓝牙请求定位权限,那么首先我们就把权限的部分先做了。

            首先声明静态权限,在AndroidManifest.xml中增加如下代码:

                
                
                
                
                
                
                
            

            ① 增加UI布局

              动态权限请求有两种方式,一种是进入这个页面一下子请求多个权限,另一种是一个一个来请求,让你知道为什么会请求这个权限,这里我们选择第二种,因此需要增加一些布局xml,如下图所示的布局XML。

            Android Ble蓝牙App(一)扫描,Android Ble蓝牙App,设备扫描指南 第4张

            下面我们依次创建,lay_android12_should_connect.xml

            
                
                
                
                
            
            

            用到一个图标ic_bluetooth_connected.xml

            
                
            
            

            布局lay_android12_should_scan.xml

            
                
                
                
                
            
            

            图标ic_bluetooth_scan.xml

            
                
            
            

            布局lay_empty.xml:

            
                
                
            
            

              图标不是XML图片,去源码中获取,这是在扫描不到设备的时候显示的布局,布局lay_should_enable_bluetooth.xml。

            
                
                
                
                
            
            

            图标ic_bluetooth_disabled.xml

                
            
            

            布局lay_should_enable_location.xml

            
                
                
                
                
            
            

            图标ic_location_disabled.xml

            
                
            
            

            布局lay_should_location_lay.xml

            
                
                
                
                
            
            

            图标ic_location_off.xml

                
            
            

            好了,在我们的努力下这些布局总算是创建完成了,下面我们将它们放置到activity_scan.xml中,代码如下所示:

            
                
                    
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                
            
            

            下面我们可以写代码了,在ScanActivity中,

            ② 点击监听

            首先是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,代码如下所示:

            
                
                
                
                
            
            

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

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

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

            class BleDeviceAdapter(
                private val mDevices: List
            ) : RecyclerView.Adapter() {
                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 = mutableListOf()
                private fun findIndex(bleDevice: BleDevice, mList: MutableList): 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)) }
                }
            }
            

            下面我们运行一下:

            Android Ble蓝牙App(一)扫描,Android Ble蓝牙App,设备扫描指南 第1张

            六、源码

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

            源码地址:GoodBle


0
收藏0
文章版权声明:除非注明,否则均为VPS857原创文章,转载或复制请以超链接形式并注明出处。

相关阅读

  • 【研发日记】Matlab/Simulink自动生成代码(二)——五种选择结构实现方法,Matlab/Simulink自动生成代码的五种选择结构实现方法(二),Matlab/Simulink自动生成代码的五种选择结构实现方法详解(二)
  • 超级好用的C++实用库之跨平台实用方法,跨平台实用方法的C++实用库超好用指南,C++跨平台实用库使用指南,超好用实用方法集合,C++跨平台实用库超好用指南,方法与技巧集合
  • 【动态规划】斐波那契数列模型(C++),斐波那契数列模型(C++实现与动态规划解析),斐波那契数列模型解析与C++实现(动态规划)
  • 【C++】,string类底层的模拟实现,C++中string类的模拟底层实现探究
  • uniapp 小程序实现微信授权登录(前端和后端),Uniapp小程序实现微信授权登录全流程(前端后端全攻略),Uniapp小程序微信授权登录全流程攻略,前端后端全指南
  • Vue脚手架的安装(保姆级教程),Vue脚手架保姆级安装教程,Vue脚手架保姆级安装指南,Vue脚手架保姆级安装指南,从零开始教你如何安装Vue脚手架
  • 如何在树莓派 Raspberry Pi中本地部署一个web站点并实现无公网IP远程访问,树莓派上本地部署Web站点及无公网IP远程访问指南,树莓派部署Web站点及无公网IP远程访问指南,本地部署与远程访问实践,树莓派部署Web站点及无公网IP远程访问实践指南,树莓派部署Web站点及无公网IP远程访问实践指南,本地部署与远程访问详解,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南。
  • vue2技术栈实现AI问答机器人功能(流式与非流式两种接口方法),Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法探究,Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法详解
  • 发表评论

    快捷回复:表情:
    评论列表 (暂无评论,0人围观)

    还没有评论,来说两句吧...

    目录[+]

    取消
    微信二维码
    微信二维码
    支付宝二维码