Android 蓝牙开发(扫描设备、绑定、解绑)Kotlin版
View③ 编码一切准备工作都已经就绪了,下面就进入编码环节,前面的内容其实和Kotlin的关系都不大,下面上正菜,Kotlin相比于Java来说的优势就是简洁,这一点会在下面的编码过程中体现。
<View
android:background=“#EBEBEB”
android:layout_marginLeft=“54dp”
android:layout_width=“match_parent”
android:layout_height=“1dp”/>
一切准备工作都已经就绪了,下面就进入编码环节,前面的内容其实和Kotlin的关系都不大,下面上正菜,Kotlin相比于Java来说的优势就是简洁,这一点会在下面的编码过程中体现。
1. 通知栏样式修改
首先修改状态栏的文字颜色,如果你现在运行这个项目在手机上时,你会发现状态栏是白色的背景以及白色的文字。如下图所示:
这样的用户体验是很不好的,而在Android6.0以后支持设置高亮状态栏样式。在之前我写Java版的时候特别弄了一个工具类,里面有针对性状态栏的一些样式和颜色改动,但实际上我只用了其中的一个方法,为了一个方法而去写一个工具类显然多此一举了。所以在Kotlin中我想到了更简单的办法,直接在MainActivity中修改状态栏样式。
代码如下:
//设置亮色状态栏模式 systemUiVisibility在Android11中弃用了,可以尝试一下。
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
放在onCreate方法中,然后运行。
是不是立竿见影,这个效果一行代码解决问题还不用写工具类,完全调用系统的方法,请注意我是Android10.0版本的手机,也是我自己用的手机。
下面写列表的适配器,因为你扫描蓝牙肯定会是一个列表,既然是一个列表那么肯定要有适配器。
2. 蓝牙设备列表适配器编写
创建一个adapter包,包下创建一个DeviceAdapter.kt文件,如下所示
DeviceAdapter的代码如下:
package com.llw.bluetooth.adapter
import android.bluetooth.BluetoothClass
import android.bluetooth.BluetoothClass.Device.*
import android.bluetooth.BluetoothClass.Device.Major.*
import android.bluetooth.BluetoothDevice
import android.widget.ImageView
import android.widget.TextView
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.BaseViewHolder
import com.llw.bluetooth.R
/**
- 蓝牙设备适配器
*/
class DeviceAdapter(layoutResId: Int, data: MutableList?) :
BaseQuickAdapter<BluetoothDevice, BaseViewHolder>(layoutResId, data) {
override fun convert(helper: BaseViewHolder?, item: BluetoothDevice?) {
val tvName = helper!!.getView(R.id.tv_name)
val icon = helper.getView(R.id.iv_device_type)
//根据设备类型设置图标
getDeviceType(item!!.bluetoothClass.majorDeviceClass, icon)
tvName.text = if (item.name == null) “无名” else item.name
//蓝牙设备绑定状态判断
val tvState = helper!!.getView(R.id.tv_bond_state)
tvState.text = when (item.bondState) {
10 -> “未配对”
11 -> “正在配对…”
12 -> “已配对”
else -> “未配对”
}
//添加item点击事件
helper.addOnClickListener(R.id.item_device)
}
/**
-
根据类型设置图标
-
@param type 类型码
-
@param icon 图标
*/
private fun getDeviceType(type: Int, icon: ImageView) {
when (type) {
AUDIO_VIDEO_HEADPHONES,//耳机
AUDIO_VIDEO_WEARABLE_HEADSET,//穿戴式耳机
AUDIO_VIDEO_HANDSFREE,//蓝牙耳机
AUDIO_VIDEO //音频设备
-> icon.setImageResource(R.mipmap.icon_headset)
COMPUTER //电脑
-> icon.setImageResource(R.mipmap.icon_computer)
PHONE //手机
-> icon.setImageResource(R.mipmap.icon_phone)
HEALTH //健康类设备
-> icon.setImageResource(R.mipmap.icon_health)
AUDIO_VIDEO_CAMCORDER, //照相机录像机
AUDIO_VIDEO_VCR //录像机
-> icon.setImageResource(R.mipmap.icon_vcr)
AUDIO_VIDEO_CAR_AUDIO //车载设备
-> icon.setImageResource(R.mipmap.icon_car)
AUDIO_VIDEO_LOUDSPEAKER //扬声器
-> icon.setImageResource(R.mipmap.icon_loudspeaker)
AUDIO_VIDEO_MICROPHONE //麦克风
-> icon.setImageResource(R.mipmap.icon_microphone)
AUDIO_VIDEO_PORTABLE_AUDIO //打印机
-> icon.setImageResource(R.mipmap.icon_printer)
AUDIO_VIDEO_SET_TOP_BOX //音频视频机顶盒
-> icon.setImageResource(R.mipmap.icon_top_box)
AUDIO_VIDEO_VIDEO_CONFERENCING //音频视频视频会议
-> icon.setImageResource(R.mipmap.icon_meeting)
AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER //显示器和扬声器
-> icon.setImageResource(R.mipmap.icon_tv)
AUDIO_VIDEO_VIDEO_GAMING_TOY //游戏
-> icon.setImageResource(R.mipmap.icon_game)
AUDIO_VIDEO_VIDEO_MONITOR //可穿戴设备
-> icon.setImageResource(R.mipmap.icon_wearable_devices)
else -> icon.setImageResource(R.mipmap.icon_bluetooth)
}
}
/**
- 刷新适配器
*/
fun changeBondDevice() {
notifyDataSetChanged()
}
}
代码讲解:
class DeviceAdapter(layoutResId: Int, data: MutableList?) :
BaseQuickAdapter<BluetoothDevice, BaseViewHolder>(layoutResId, data)
首先看这个类,在Kotlin继承和实现都是通过 :(英文下的冒号)来操作的,而Java中继承是extends,实现是implements。在上面的代码中DeviceAdapter继承了BaseQuickAdapter,这一点和Java的相似,如下图所示
而Kotlin的语法可以让你把构造方法的参数作为类参数使用,这样解释不知道是不是对的,这里传了一个布局id和数据源。
然后重写里面convert方法
override fun convert(helper: BaseViewHolder?, item: BluetoothDevice?) {
val tvName = helper!!.getView(R.id.tv_name)
val icon = helper.getView(R.id.iv_device_type)
getDeviceType(item!!.bluetoothClass.majorDeviceClass, icon)
tvName.text = if (item.name == null) “无名” else item.name
//蓝牙设备绑定状态判断
val tvState = helper!!.getView(R.id.tv_bond_state)
tvState.text = when (item.bondState) {
10 -> “未配对”
11 -> “正在配对…”
12 -> “已配对”
else -> “未配对”
}
//添加item点击事件
helper.addOnClickListener(R.id.item_device)
}
在代码中你会看到 !! 和 ? 这个你就不明所以了,因为Java中是没有的,这里解释一下,首先是Kotlin对于空安全做了处理, !! 表示当前对象不会空的情况下执行,而 ? 表示当前对象可以为空。
val tvName = helper!!.getView(R.id.tv_name)
val icon = helper.getView(R.id.iv_device_type)
而这两行代码,可以看到,第一行我给了!!,第二行没有给,这是因为在Kotlin中只要一开始做了处理之后后面就可以不用再次处理,当然你加上!!也没有问题。val 表示不可变量,而通过Kotlin的类型推导机制,tvName此时代表的就是一个通过R.id.tv_name实例化之后的TextView。
//根据设备类型设置图标
getDeviceType(item!!.bluetoothClass.majorDeviceClass, icon)
这行代码调用getDeviceType方法,传入两个参数,这两个参数都已经做了非空的处理,所以在getDeviceType的里面就不用在做空处理了。下面看这个方法的代码:
/**
-
根据类型设置图标
-
@param type 类型码
-
@param icon 图标
*/
private fun getDeviceType(type: Int, icon: ImageView) {
when (type) {
AUDIO_VIDEO_HEADPHONES,//耳机
AUDIO_VIDEO_WEARABLE_HEADSET,//穿戴式耳机
AUDIO_VIDEO_HANDSFREE,//蓝牙耳机
AUDIO_VIDEO //音频设备
-> icon.setImageResource(R.mipmap.icon_headset)
COMPUTER //电脑
-> icon.setImageResource(R.mipmap.icon_computer)
PHONE //手机
-> icon.setImageResource(R.mipmap.icon_phone)
HEALTH //健康类设备
-> icon.setImageResource(R.mipmap.icon_health)
AUDIO_VIDEO_CAMCORDER, //照相机录像机
AUDIO_VIDEO_VCR //录像机
-> icon.setImageResource(R.mipmap.icon_vcr)
AUDIO_VIDEO_CAR_AUDIO //车载设备
-> icon.setImageResource(R.mipmap.icon_car)
AUDIO_VIDEO_LOUDSPEAKER //扬声器
-> icon.setImageResource(R.mipmap.icon_loudspeaker)
AUDIO_VIDEO_MICROPHONE //麦克风
-> icon.setImageResource(R.mipmap.icon_microphone)
AUDIO_VIDEO_PORTABLE_AUDIO //打印机
-> icon.setImageResource(R.mipmap.icon_printer)
AUDIO_VIDEO_SET_TOP_BOX //音频视频机顶盒
-> icon.setImageResource(R.mipmap.icon_top_box)
AUDIO_VIDEO_VIDEO_CONFERENCING //音频视频视频会议
-> icon.setImageResource(R.mipmap.icon_meeting)
AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER //显示器和扬声器
-> icon.setImageResource(R.mipmap.icon_tv)
AUDIO_VIDEO_VIDEO_GAMING_TOY //游戏
-> icon.setImageResource(R.mipmap.icon_game)
AUDIO_VIDEO_VIDEO_MONITOR //可穿戴设备
-> icon.setImageResource(R.mipmap.icon_wearable_devices)
else -> icon.setImageResource(R.mipmap.icon_bluetooth)
}
}
其实就是条件分支,根据不同的条件做不同的事情,在Java中使用switch/case,而在Kotlin中使用when。when的语法结构更加的简洁明了,通过 -> 代替了 : ,冒号前面是条件,冒号后面是执行业务。而当多个条件对应一个执行业务时,条件之间用英文逗号隔开,一行代码完成一个条件分支,很简洁,但是不要忘了加上else,这是标准写法,你不加也没事,就如同你写switch/case不加default一样。相信这么一解释你已经理解了when的基本用法了,当然还有很多其他的用法由于业务的原因无法展示,自行百度吧。
tvName.text = if (item.name == null) “无名” else item.name
这行代码等同于
if (item.getName() == null) {
helper.setText(R.id.tv_name, “无名”);
} else {
helper.setText(R.id.tv_name, item.getName());
}
这么一看是不是觉得Kotlin的语法很简单,它允许你的返回值一致的判断进行直接赋值,比如这里判断设备名称为空则显示无名二字,不为空则显示设备名,这两个返回都是String类型,而tvName.text设置的就是String类型,所以就有了上面的简洁代码,有点像三目运算符。
//蓝牙设备绑定状态判断
val tvState = helper!!.getView(R.id.tv_bond_state)
tvState.text = when (item.bondState) {
10 -> “未配对”
11 -> “正在配对…”
12 -> “已配对”
else -> “未配对”
}
//添加item点击事件
helper.addOnClickListener(R.id.item_device)
这几行代码也没有什么好讲解的了,都讲过了,这也是when的另一种使用方法,可以直接赋值使用。
/**
- 刷新适配器
*/
fun changeBondDevice() {
notifyDataSetChanged()
}
最后一个方法就是刷新适配器,当页面的数据有变动是及时刷新。好了这个适配器就讲完了,应该够详细了吧。
3. 权限请求
不管你是用的什么语言来开发Android,你都得遵守Android制定的规则,因此也是要做Android版本大于6.0时动态请求权限。
于是就有了如下这个方法
/**
- 检查Android版本
*/
private fun checkVersion() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//6.0或6.0以上
permissionsRequest() //动态权限申请
} else {
//6.0以下
initBlueTooth() //初始化蓝牙配置
}
}
记得在OnCreate中调用喔!
然后来看看这个权限请求的方法
/**
- 动态权限申请
*/
private fun permissionsRequest() {
val rxPermissions = RxPermissions(this)
rxPermissions.request(Manifest.permission.ACCESS_FINE_LOCATION)
.subscribe {
if (it) {
initBlueTooth()
} else {
showMsg(“权限未开启”)
}
}
}
showMsg方法
/**
- 显示提示消息
*/
private fun showMsg(llw: String) {
Toast.makeText(this, llw, Toast.LENGTH_SHORT).show()
}
这个方法的代码很好理解,和Java的逻辑如出一辙,无非就是不知道这个it是什么意思,it就是它本身的意思,结合方法中的逻辑来看就容易理解了,权限请求自然会有两种结果,同意和不同意,也就是结果是true和false的结果,而这个it就代表这个请求的结果。
4. 初始化蓝牙
首先声明一些成员变量,这里用的是MutableList,表示可变列表,可以有很多方法。
//蓝牙广播接收器
private var bluetoothReceiver: BluetoothReceiver? = null
//蓝牙适配器
private var bluetoothAdapter: BluetoothAdapter? = null
//蓝牙设备适配器
private var mAdapter: DeviceAdapter? = null
//可变列表
private var list: MutableList = mutableListOf()
//请求码
private val REQUEST_ENABLE_BLUETOOTH = 1
BluetoothReceiver报红没有关系不要慌,下面会写的,先看这个初始化蓝牙的方法,比较简单,我想不用讲代码了。
/**
- 初始化蓝牙
*/
private fun initBlueTooth() {
var intentFilter = IntentFilter()
intentFilter.addAction(BluetoothDevice.ACTION_FOUND) //获得扫描结果
intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED) //绑定状态变化
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) //开始扫描
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) //扫描结束
bluetoothReceiver = BluetoothReceiver() //实例化广播接收器
registerReceiver(bluetoothReceiver, intentFilter) //注册广播接收器
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() //获取蓝牙适配器
}
5. 扫描蓝牙
在布局中底部我放了一个TextView,点击之后扫描蓝牙,
<TextView
android:id=“@+id/scan_devices”
android:layout_width=“match_parent”
android:layout_height=“50dp”
android:background=“?android:attr/selectableItemBackground”
android:gravity=“center”
android:onClick=“scanBluetooth”
android:text=“扫描蓝牙” />
注意看这一句话
android:onClick=“scanBluetooth”
通过在布局中点击触发MainActivity中的方法,在MainActivity中写入,或者Alt+Enter
然后你就会看到这样的一个方法
fun scanBluetooth(view: View) {}
首先想清楚这个里面要做什么?难道仅仅只有扫描蓝牙吗?当然不是,首先要看你的设备是否支持蓝牙,其次蓝牙是否打开,最后才是扫描蓝牙
于是里面的代码就可以这样写
/**
- 扫描蓝牙
*/
fun scanBluetooth(view: View) {
if (bluetoothAdapter != null) { //是否支持蓝牙
if (bluetoothAdapter!!.isEnabled) { //打开
//开始扫描周围的蓝牙设备,如果扫描到蓝牙设备,通过广播接收器发送广播
if (mAdapter != null) { //当适配器不为空时,这时就说明已经有数据了,所以清除列表数据,再进行扫描
list.clear()
mAdapter!!.notifyDataSetChanged()
}
bluetoothAdapter!!.startDiscovery()
} else { //未打开
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(intent, REQUEST_ENABLE_BLUETOOTH)
}
} else {
showMsg(“你的设备不支持蓝牙”)
}
}
6. 广播接收器
点击扫描蓝牙之后会这行扫描事件,会发送一个广播出去,发送出去了自然要有一个地方来接收,这就是广播接收器,在MainActivity定义一个内部类,通过inner关键字
/**
- 广播接收器
*/
inner class BluetoothReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
//显示蓝牙设备
BluetoothDevice.ACTION_FOUND -> showDevicesData(context, intent)
//当有蓝牙绑定状态发生改变时,刷新列表数据
BluetoothDevice.ACTION_BOND_STATE_CHANGED -> mAdapter?.changeBondDevice()
//开始扫描
BluetoothAdapter.ACTION_DISCOVERY_STARTED -> loading_lay.visibility = View.VISIBLE
//停止扫描
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> loading_lay.visibility = View.GONE
else -> showMsg(“未知”)
}
}
}
另外别忘了在页面销毁的时候解注册广播
/**
- 销毁
*/
override fun onDestroy() {
super.onDestroy()
//卸载广播接收器
unregisterReceiver(bluetoothReceiver)
}
当开始扫描的时候发送ACTION_FOUND,而作为接收方,自然要有相应的处理方法,这个时候注意到showDevicesData(context, intent),通过这个方法显示扫描到的蓝牙设备信息。
7. 显示蓝牙设备信息
/**
-
显示蓝牙设备信息
-
@param context 上下文参数
-
@param intent 意图
*/
private fun showDevicesData(context: Context?, intent: Intent) {
//获取已绑定的设备
getBondedDevice()
//获取周围蓝牙设备
val device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
if (list.indexOf(device) == -1) { //防止重复添加
if (device.name != null) { //过滤掉设备名称为null的设备
list.add(device)
}
}
mAdapter = DeviceAdapter(R.layout.item_device_list, list)
rv.layoutManager = LinearLayoutManager(context)
rv.adapter = mAdapter
//item的点击事件
mAdapter!!.setOnItemChildClickListener { adapter, view, position ->
//点击时获取状态,如果已经配对过了就不需要在配对
if (list[position].bondState == BluetoothDevice.BOND_NONE) {
createOrRemoveBond(1, list[position]) //开始匹配
} else {
showDialog(
“确定要取消配对吗?”,
DialogInterface.OnClickListener { dialog, which ->
//取消配对
createOrRemoveBond(2, list[position]) //取消匹配
})
}
}
}
在这里,有一个可以注意的地方
这句话的意思是,参数未被使用,可以使用_代替。然后来看里面的代码,首先第一句是getBondedDevice()
/**
- 获取已绑定设备
*/
private fun getBondedDevice() {
val pairedDevices = bluetoothAdapter!!.bondedDevices
if (pairedDevices.size > 0) { //如果获取的结果大于0,则开始逐个解析
for (device in pairedDevices) {
if (list.indexOf(device) == -1) { //防止重复添加
if (device.name != null) { //过滤掉设备名称为null的设备
list.add(device)
}
}
}
}
}
这里用到了in关键字,使用in来检查一个值是否在一个区间内。与他的代码已经有了注释了,就不过多的解释了。
剩下的代码分为两部分,一部分是数据的处理,点击处理。避免重复添加和添加null的设备进入列表,而点击item,根据绑定状态而定,绑定过蓝牙的点击就是取消绑定,这里调用了一个方法。显示一个弹窗
/**
-
弹窗
-
@param dialogTitle 标题
-
@param onClickListener 按钮的点击事件
*/
private fun showDialog(dialogTitle: String, onClickListener: DialogInterface.OnClickListener) {
val builder =
AlertDialog.Builder(this)
builder.setMessage(dialogTitle)
builder.setPositiveButton(“确定”, onClickListener)
builder.setNegativeButton(“取消”, null)
builder.create().show()
}
而未绑定的点击绑定,调用createOrRemoveBond方法,也可以说是配对,当你取消绑定是也会调用这个方法,只是传递的类型不同而已。
/**
- 创建或者取消匹配
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
学习福利
【Android 详细知识点思维脑图(技能树)】
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
减轻大家的负担。**
[外链图片转存中…(img-w5DIcD5V-1713198248954)]
[外链图片转存中…(img-6apu9O1q-1713198248954)]
[外链图片转存中…(img-3Q0YfMfN-1713198248955)]
[外链图片转存中…(img-ukSs2DUx-1713198248955)]
[外链图片转存中…(img-4XpP12WM-1713198248955)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
学习福利
【Android 详细知识点思维脑图(技能树)】
[外链图片转存中…(img-AYUIhlJj-1713198248955)]
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
[外链图片转存中…(img-DV00dOAU-1713198248956)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
更多推荐
所有评论(0)