Harmony Ble 蓝牙App (一)扫描
Harmony Ble 蓝牙App (一)扫描
- 前言
- 正文
- 一、创建工程
- 二、工程配置
- ① 权限配置
- ② Debug配置
- ③ UI配置
- 三、扫描
- ① 扫描接口
- ② 扫描类
- 四、业务处理
- ① Slice的生命周期
- ② 蓝牙开关和动态权限请求
- 五、扫描设备
- 六、显示设备
- ① 自定义蓝牙类
- ② 提供者
- ③ 显示设备
- 七、源码
前言
关于Android的低功耗蓝牙,我做了很多介绍了,那么对于Harmony来说这一块我没有做过介绍,而实际中我确实做过一个Harmony的BLE项目,所以这里分享一些内容出来。
正文
在Harmony中进行Ble的蓝牙开发实际上和Android中类似,但是又有一些不同,因为Harmony的SDK还在不断的完善。而这里我们使用的是API 6进行项目开发,使用的语言是Java,至于为什么使用API 6而不是最新的API 9,因为我买不起遥遥领先,所以只能用API 6的HUAWEI P30进行真机测试。蓝牙这种APP一定是要使用真机测试的,你用虚拟机是不行的,话不多说,我们开始吧。

一、创建工程
下面开始创建工程。

选择Empty Ability,点击Next。我们创建一个名为HarmonyBle的项目,语言为Java。

点击Finish完成创建。

默认的工程就是这个样子的,是不是很像Android创建的工程呢?
二、工程配置
① 权限配置
Harmony中同样有权限这个概念,也需要配置静态权限和动态权限,只不过配置静态权限的地方不一样。Harmony是在config.json中,里面的代码如下:
{"app": {"bundleName": "com.llw.ble","vendor": "example","version": {"code": 1000000,"name": "1.0.0"}},"deviceConfig": {},"module": {"package": "com.llw.ble","name": ".MyApplication","mainAbility": "com.llw.ble.MainAbility","deviceType": ["phone","tablet","tv","wearable","car"],"distro": {"deliveryWithInstall": true,"moduleName": "entry","moduleType": "entry","installationFree": false},"abilities": [{"skills": [{"entities": ["entity.system.home"],"actions": ["action.system.home"]}],"name": "com.llw.ble.MainAbility","description": "$string:mainability_description","icon": "$media:icon","label": "$string:entry_MainAbility","launchType": "standard","orientation": "unspecified","visible": true,"type": "page"}]}
}
你阅读一下,你就会发现,这和Android的AndroidManifest.xml配置文件好像差不多啊。只不过一个用的是json,一个用的是xml。
所以我们配置权限也是在config.json中,例如扫描蓝牙时我们需要定位权限。可以在里面增加如下代码:
"reqPermissions": [{"name": "ohos.permission.LOCATION"},{"name": "ohos.permission.USE_BLUETOOTH"},{"name": "ohos.permission.DISCOVER_BLUETOOTH"},{"name": "ohos.permission.MANAGE_BLUETOOTH"}]
如下图所示,注意json中标点符号。

② Debug配置
然后我们就应该要来写代码了,不过在此之前,我们先了解一下Ability和Slice的区别,Ability就像一个画框,而Slice就像一个画布。我们可以在一个画框里面加载多个画布,就好像多个页面之前的跳转,我们可以用Slice来进行,下面我们增加一个扫描的Slice,我们复制一下MainAbilitySlice,再粘贴一下,出现的弹窗中改名字

为什么要通过这种方式来创建Java文件呢?因为DevEco Studio我创建不了Java文件,可能是这个版本的DS没有这个选项亦或是我没有找到。
下面我们需要创建对应layout文件,再resources/base/layout下创建一个slice_scan.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayoutxmlns:ohos="http://schemas.huawei.com/res/ohos"ohos:height="match_parent"ohos:width="match_parent"ohos:alignment="center"ohos:orientation="vertical"><ListContainerohos:id="$+id:lc_device"ohos:height="match_parent"ohos:width="match_parent"/></DirectionalLayout>
然后我们再修改ScanSlice中的内容,让它加载我们刚写好的slice_scan.xml。修改onStart()方法,代码如下所示:
@Overridepublic void onStart(Intent intent) {super.onStart(intent);super.setUIContent(ResourceTable.Layout_slice_scan);}
现在App打开之后默认会运行MainAbility,我们看一下这个里面。
public class MainAbility extends Ability {@Overridepublic void onStart(Intent intent) {super.onStart(intent);super.setMainRoute(MainAbilitySlice.class.getName());}
}
可以看到,在setMainRoute()方法中,默认加载的是MainAbilitySlice,将它改为我们刚写好的ScanSlice,代码:super.setMainRoute(ScanSlice.class.getName());
然后我们先运行一下看看,通过USB链接到鸿蒙手机上。

这里会提示报错,浏览一下错误信息。

这里是说我们需要配置Signing。点击Run下面或者右侧弹窗的Open signing configs,会打开一个配置窗口,如下图所示:

我们点击Signing Configs选项,需要你进行登录,如下图所示:

这里就需要你登录华为的帐号了,我们当前在本地运行所以是Debug模式,旁边有一个Release表示发布版本,它里面配置的东西和Debug模式一致,区别在于Debug模式下的配置信息只要我们登录之后,DevEco Studio会帮助我们自动生成,而Release中的信息则需要开发者去华为开发者官网上去创建应用并申请配置文件和证书,比较麻烦,但是如果你要上架应用则必须做这一步,在国内,华为应用市场上架应用是最严格的。华为的你搞得定,其他的都是小趴菜,不值一提。
下面我们先登录,会打开一个网页,登录成功之后,你会看到这样的页面。

然后我们回到DS,就会自动配置Debug模式下的证书和配置文件,如下图所示:

点击OK,会在DS中进行一个配置,配置好之后你可以在工程目录下的build.gradle中看到debug的相关信息,如下图所示。

然后我们再运行一下看看,这一次毫无疑问是可以运行成功的。如下图所示:

③ UI配置
可以看到默认的标题栏就如同Android默认的ActionBar,丑的很特别,我们去掉它,在config.json中添加如下代码:
"metaData": {"customizeData": [{"extra": "","name": "hwc-theme","value": "androidhwext:style/Theme.Emui.Light.NoTitleBar"}]},
添加位置如下所示:

下面我们再运行一下看看。

是不是Unbelievable! 同样为了标题好看,我们在element下创建一个color.json,里面的代码如下所示:
{"color": [{"name": "white","value": "#FFF"},{"name": "black","value": "#000"},{"name": "blue","value": "#FFA7D3FF"},{"name": "bg_color","value": "#F8F8F8"},{"name": "gray","value": "#989898"}]
}
我们再修改一下scan_slice.xml中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayoutxmlns:ohos="http://schemas.huawei.com/res/ohos"ohos:height="match_parent"ohos:width="match_parent"ohos:alignment="center"ohos:background_element="$color:bg_color"ohos:orientation="vertical"><DirectionalLayoutohos:height="50vp"ohos:width="match_parent"ohos:alignment="vertical_center"ohos:background_element="$color:blue"ohos:orientation="horizontal"ohos:start_padding="12vp"><Textohos:id="$+id:title"ohos:height="match_content"ohos:width="match_content"ohos:text="选择设备"ohos:text_color="#FFF"ohos:text_font="HwChinese-medium"ohos:text_size="18fp"ohos:weight="1"/><Textohos:id="$+id:tx_scan_status"ohos:height="match_content"ohos:width="match_content"ohos:end_margin="6vp"ohos:padding="8vp"ohos:text="搜索"ohos:text_color="#FFF"ohos:text_size="14fp"/></DirectionalLayout><ListContainerohos:id="$+id:lc_device"ohos:height="match_parent"ohos:width="match_parent"/></DirectionalLayout>
这个DirectionalLayout布局就是线性布局,我们可以点击右侧导航栏的Previewer进行布局预览,如下图所示。

右上角的T图标,点击之后可以查看当前布局的层级。

这里说明一下,有时候在通过资源使用颜色值的时候会无法生效,所以就会直接使用#FFF,在代码里也是如此,这应该属于编译器的Bug。
标题栏就写好了,还有状态栏我们没有改,状态栏我们在MainAbility中进行修改,代码如下所示:
@Overridepublic void onStart(Intent intent) {Window window = WindowManager.getInstance().getTopWindow().get();window.setStatusBarColor(Color.getIntColor("#A7D3FF"));super.onStart(intent);super.setMainRoute(ScanSlice.class.getName());}
还是修改onStart()方法,然后我们运行一下看看。

好了,下面我们来写扫描需要的内容代码。
三、扫描
首先我们在com.llw.ble包下新建一个core包,core包下创建一个BleCore类,这里面就是控制Ble蓝牙相关的一切,比如扫描,连接,读写数据等操作,我们先不写代码。下面在core包下创建一个scan包。
① 扫描接口
scan包下新建一个ScanCallback接口,代码如下:
public interface ScanCallback {void onScanResult(BleScanResult result);default void onGroupScanResultsEvent(List<BleScanResult> results){}default void onScanFailed(String failed){}
}
② 扫描类
然后在scan包下新建一个BleScan类,代码如下所示:
public class BleScan {private final BleCentralManager centralManager;private boolean isScanning = false;private ScanCallback scanCallback;// 创建扫描过滤器然后开始扫描private List<BleScanFilter> filters;private static volatile BleScan mInstance;//初始化public static BleScan getInstance(Context context) {if (mInstance == null) {synchronized (BleScan.class) {if (mInstance == null) {mInstance = new BleScan(context);}}}return mInstance;}public BleScan(Context context) {BleScanCallback centralManagerCallback = new BleScanCallback();centralManager = new BleCentralManager(context, centralManagerCallback);}/*** 当前是否正在扫描* @return true 扫描中,false 未扫描*/public boolean isScanning() {return isScanning;}/*** 设置过滤信息* @param filters 蓝牙扫描过滤列表*/public void setFilters(List<BleScanFilter> filters) {this.filters = filters;}/*** 设置扫描回调,页面需要实现才能获取扫描到的设备* @param scanCallback 扫描回调*/public void setScanCallback(ScanCallback scanCallback) {this.scanCallback = scanCallback;}/*** 开始扫描*/public void startScan() {if (centralManager == null) {localScanFailed("Bluetooth not turned on.");return;}centralManager.startScan(filters);isScanning = true;}/*** 停止扫描*/public void stopScan() {if (!isScanning) {localScanFailed("Not currently scanning, your stop has no effect.");return;}centralManager.stopScan();isScanning = false;}/*** 实现扫描回调*/public class BleScanCallback implements BleCentralManagerCallback {@Overridepublic void scanResultEvent(BleScanResult bleScanResult) {if (scanCallback != null) {scanCallback.onScanResult(bleScanResult);}}@Overridepublic void scanFailedEvent(int resultCode) {if (scanCallback != null) {scanCallback.onScanFailed(String.valueOf(resultCode));}}@Overridepublic void groupScanResultsEvent(final List<BleScanResult> scanResults) {// 对扫描结果进行处理if (scanCallback != null) {scanCallback.onGroupScanResultsEvent(scanResults);}}}/*** 本地扫描失败处理* @param failed 错误信息*/private void localScanFailed(String failed) {if (scanCallback != null) {scanCallback.onScanFailed(failed);}}
}
这里面采用单例模式,在初始化之后直接调用,然后再实现扫描回调接口,返回扫描信息,有开始、停止扫描和是否正在扫描方法。这个类你可以直接用,也可以再封装到BleCore中,这里我们封装到BleCore中,修改BleCore中的代码,如下所示:
public class BleCore {private static volatile BleCore mInstance;private final BleScan bleScan;public BleCore(Context context) {//蓝牙扫描bleScan = BleScan.getInstance(context);}public static BleCore getInstance(Context context) {if (mInstance == null) {synchronized (BleCore.class) {if (mInstance == null) {mInstance = new BleCore(context);}}}return mInstance;}public void setPhyScanCallback(ScanCallback scanCallback) {bleScan.setScanCallback(scanCallback);}public boolean isScanning() {return bleScan.isScanning();}public void startScan() {bleScan.startScan();}public void stopScan() {bleScan.stopScan();}
}
四、业务处理
这里的业务处理主要是两个,第一个是蓝牙开关监听,第二个动态权限申请。
再进行业务处理之前,我们先修改一下MyApplication类的名字,修改为BleApp,修改后再改动里面的代码,如下所示:
public class BleApp extends AbilityPackage {private static BleCore bleCore;@Overridepublic void onInitialize() {super.onInitialize();bleCore = BleCore.getInstance(getContext());}public static BleCore getBleCore() {return bleCore;}
}
① Slice的生命周期
首先我们来看一下Slice的生命周期,这个就比较重要,下面我们首先在com.llw.ble下创建一个utils包,utils包下创建一个LogUtils类,代码如下所示:
public class LogUtils {static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00201, "HarmonyBle");private static HiLogLabel logLabel;public static void setLogLabel(HiLogLabel logLabel) {LogUtils.logLabel = logLabel;}public static void Log(String content) {HiLog.info(LABEL, content);}public static void LogI(String TAG, String content) {HiLogLabel label = new HiLogLabel(HiLog.LOG_APP, 0x00201, TAG);HiLog.info(label, content);}public static void LogD(String TAG, String content) {HiLogLabel label = new HiLogLabel(HiLog.LOG_APP, 0x00201, TAG);HiLog.debug(label, content);}public static void LogE(String TAG, String content) {HiLogLabel label = new HiLogLabel(HiLog.LOG_APP, 0x00201, TAG);HiLog.error(label, content);}public static void LogW(String TAG, String content) {HiLogLabel label = new HiLogLabel(HiLog.LOG_APP, 0x00201, TAG);HiLog.warn(label, content);}}
这是因为Harmony中打印日志比较麻烦,所以写一个工具类,封装一下,下面我们修改一下ScanSlice类中的代码,如下所示:
public class ScanSlice extends AbilitySlice {private final String TAG = ScanSlice.class.getSimpleName();@Overridepublic void onStart(Intent intent) {super.onStart(intent);super.setUIContent(ResourceTable.Layout_slice_scan);LogUtils.LogD(TAG, "onStart");}@Overridepublic void onActive() {LogUtils.LogD(TAG, "onActive");}@Overrideprotected void onInactive() {LogUtils.LogD(TAG, "onInactive");}@Overridepublic void onForeground(Intent intent) {LogUtils.LogD(TAG, "onForeground");}@Overrideprotected void onBackground() {LogUtils.LogD(TAG, "onBackground");}@Overrideprotected void onStop() {LogUtils.LogD(TAG, "onStop");}
}
然后我们运行一下看看,检查控制台日志:

然后我们通过Home键回到桌面,看看日志:

然后我们点击桌面上的图标回到应用中,看看日志:

再回到桌面,然后我们通过后台的运行程序进入应用,看看日志:

这两种回到应用的方式日志一样,然后我们按返回键回到桌面,看看日志:

那么现在你对于Slice的生命周期就比较了解了,下面我们进行代码的编写。
② 蓝牙开关和动态权限请求
首先处理蓝牙相关的,在BleCore中添加如下代码:
private final BluetoothHost mBluetoothHost;
在构造方法中实例化
public BleCore(Context context) {...// 获取蓝牙本机管理对象mBluetoothHost = BluetoothHost.getDefaultHost(context);}
然后我们再写两个方法:
public boolean isEnableBt() {return mBluetoothHost.getBtState() == BluetoothHost.STATE_ON;}public void enableBt() {mBluetoothHost.enableBt();}
用于判断是否打开蓝牙和打开蓝牙,回到ScanSlice中我们需要使用BleCore来处理蓝牙相关的工作,代码如下所示:
public class ScanSlice extends AbilitySlice {private final String TAG = ScanSlice.class.getSimpleName();private BleCore bleCore;private Text txScanStatus;private ListContainer lcDevice;@Overridepublic void onStart(Intent intent) {super.onStart(intent);super.setUIContent(ResourceTable.Layout_slice_scan);bleCore = BleApp.getBleCore();txScanStatus = (Text) findComponentById(ResourceTable.Id_tx_scan_status);lcDevice = (ListContainer) findComponentById(ResourceTable.Id_lc_device);//点击监听txScanStatus.setClickedListener(component -> {});}@Overridepublic void onActive() {// 判断是否打开蓝牙if (!bleCore.isEnableBt()) {//打开蓝牙bleCore.enableBt();return;}}
}
首先在onStart()中对BleCore进行实例化,findComponentById就如同findViewById,然后在onActive()中调用刚才我们所写的方法。
@Overridepublic void onActive() {// 判断是否打开蓝牙if (!bleCore.isEnableBt()) {//打开蓝牙bleCore.enableBt();return;}}
然后是定位权限的处理,同样在onActive()中,增加代码如下所示:
@Overridepublic void onActive() {// 判断是否打开蓝牙...// 是否获取定位权限String locationPermission = "ohos.permission.LOCATION";if (verifySelfPermission(locationPermission) != IBundleManager.PERMISSION_GRANTED) {requestPermissionsFromUser(new String[]{locationPermission}, 100);return;}}
这里首先我们定义一个权限,然后判断是否授予,没有授予则进行请求,下面运行一下看看:

那么我们就完成了蓝牙打开和定位权限动态申请,你可以在运行一次,你会发现,你还需要请求权限的,因为DS默认安装时不会保留应用的数据,而蓝牙打开了属于系统层面的,所以你可以不用再打开蓝牙,而需要重新请求定位权限,为了避免这一点,我们点击Run→ Edit Configurations...

在弹出的窗口上勾选Keep Application Data

点击OK,再运行即可。
五、扫描设备
接下来我们进行扫描的处理,在ScanSlice中增加如下方法代码:
private void startScan() {bleCore.startScan();txScanStatus.setText("停止");}private void stopScan() {bleCore.stopScan();txScanStatus.setText("搜索");}
这里就是扫描和停止方法,同时修改一下Text文本,在onStart()中首先实现扫描回调监听,然后处理再处理txScanStatus文本的点击事件,代码如下所示:
@Overridepublic void onStart(Intent intent) {...bleCore.setPhyScanCallback(this);//点击监听txScanStatus.setClickedListener(component -> {if (bleCore.isScanning()) {stopScan();//扫描开关停止扫描} else {startScan();//开始扫描}});}
这里this会报错,鼠标放在上面,Alt + Enter,出现弹窗。

选择最后一个,就会给你实现ScanCallback中的onScanResult()方法,代码如下所示:
@Overridepublic void onScanResult(BleScanResult result) {LogUtils.LogD(TAG, result.getPeripheralDevice().getDeviceAddr());}
我们在里面打印一下扫描到的设备Mac地址,最后我们在onActive()中增加如下所示代码:
@Overridepublic void onActive() {...// 是否在扫描中if (!bleCore.isScanning()) {startScan();}}
下面运行一下,看看控制台日志:

扫描出来了,只不过目前还看不到,所以我们要渲染一下,让它可以看到。
六、显示设备
要显示设备,首先我们需要写一个Bean。
① 自定义蓝牙类
在core包下新建一个BleDevice类,里面的代码如下所示:
public class BleDevice {private String realName = "Unknown device"; //蓝牙设备真实名称private String macAddress; //地址private int rssi; //信号强度private BlePeripheralDevice device; //设备public BleDevice(BleScanResult scanResult) {this.device = scanResult.getPeripheralDevice();this.macAddress = device.getDeviceAddr();String name = device.getDeviceName().get();if (name != null || !name.isEmpty()) {this.realName = name;}this.rssi = scanResult.getRssi();}public String getRealName() {return realName;}public void setRealName(String realName) {this.realName = realName;}public String getMacAddress() {return macAddress;}public void setMacAddress(String macAddress) {this.macAddress = macAddress;}public int getRssi() {return rssi;}public void setRssi(int rssi) {this.rssi = rssi;}public BlePeripheralDevice getDevice() {return device;}public void setDevice(BlePeripheralDevice device) {this.device = device;}
}
这个Bean没有什么好说的,下面要做的就是列表Item的渲染,在Android中我们使用的是适配器Adapter,而在Harmony中使用的是提供者Provider。
② 提供者
同样我们先写布局,在layout下新建一个item_scan_device.xml,代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayoutxmlns:ohos="http://schemas.huawei.com/res/ohos"ohos:height="match_content"ohos:width="match_parent"ohos:alignment="vertical_center"ohos:background_element="#FFF"ohos:bottom_padding="8vp"ohos:orientation="horizontal"ohos:right_padding="16vp"ohos:top_margin="1vp"ohos:top_padding="8vp"><Imageohos:height="match_content"ohos:width="match_content"ohos:end_margin="16vp"ohos:image_src="$graphic:ic_bluetooth"ohos:start_margin="16vp"/><DirectionalLayoutohos:height="match_content"ohos:width="match_content"ohos:orientation="vertical"ohos:weight="1"><Textohos:id="$+id:device_name"ohos:height="match_content"ohos:width="match_content"ohos:text="设备名称"ohos:text_size="16fp"/><Textohos:id="$+id:device_address"ohos:height="match_content"ohos:width="match_content"ohos:text="设备地址"ohos:text_color="$color:gray"ohos:text_size="14fp"ohos:top_margin="4vp"/></DirectionalLayout><Textohos:id="$+id:rssi"ohos:height="match_content"ohos:width="match_content"ohos:align_parent_end="true"ohos:text_color="#000000"ohos:text_size="10fp"/></DirectionalLayout>
几个主要内容,设备名称、Mac地址、Rssi信号强度,然后这里有一个图标,在graphic下创建一个ic_bluetooth.xml,代码如下所示:
<?xml version="1.0" encoding="UTF-8"?><vectorxmlns:ohos="http://schemas.huawei.com/res/ohos"ohos:height="48vp"ohos:width="48vp"ohos:viewportHeight="1024"ohos:viewportWidth="1024"><pathohos:fillColor="#A7D3FF"ohos:pathData="M53.31,512a458.69,458.69 0,1 1,917.38 0A458.69,458.69 0,0 1,53.31 512zM584.96,301.82a356.16,356.16 0,0 0,-39.81 -26.69c-12.1,-6.34 -32,-13.89 -52.74,-3.01 -20.48,10.82 -25.86,31.23 -27.78,44.67 -1.92,13.18 -1.92,30.21 -1.92,48.45v77.18l-57.92,-49.6a32,32 0,0 0,-41.6 48.64L445.44,512 363.2,582.4a32,32 0,1 0,41.6 48.64l57.92,-49.6v77.18c0,18.24 0,35.33 1.92,48.51 1.92,13.44 7.23,33.86 27.78,44.61 20.74,10.88 40.64,3.33 52.74,-2.94a356.48,356.48 0,0 0,39.81 -26.69l39.42,-28.8c10.62,-7.74 21.31,-15.55 29.06,-23.1 8.64,-8.58 18.56,-21.57 18.56,-40.06 0,-18.56 -9.92,-31.55 -18.56,-40.06 -7.68,-7.55 -18.43,-15.36 -29.06,-23.17L548.99,512l75.39,-54.98c10.62,-7.74 21.31,-15.55 29.06,-23.17 8.64,-8.51 18.56,-21.5 18.56,-40 0,-18.56 -9.92,-31.55 -18.56,-40.06 -7.68,-7.62 -18.43,-15.36 -29.06,-23.17l-39.42,-28.8zM526.72,367.36v64.77c0,7.36 0,11.01 2.37,12.16 2.3,1.28 5.25,-0.9 11.2,-5.25l44.8,-32.7 8.32,-6.08c3.97,-2.94 5.95,-4.42 5.95,-6.53 0,-2.18 -1.98,-3.65 -5.95,-6.53l-8.32,-6.14 -36.1,-26.3a3344.06,3344.06 0,0 0,-9.34 -6.78c-5.44,-3.97 -8.19,-5.95 -10.5,-4.8 -2.37,1.15 -2.37,4.54 -2.37,11.33v12.86zM526.72,656.45L526.72,591.74c0,-7.36 0,-11.01 2.37,-12.16 2.3,-1.22 5.25,0.96 11.2,5.25l44.8,32.7 8.32,6.14c3.97,2.88 5.95,4.35 5.95,6.53 0,2.11 -1.98,3.58 -5.95,6.53l-8.32,6.08 -36.1,26.37 -9.34,6.78c-5.44,3.97 -8.19,5.95 -10.5,4.74 -2.37,-1.15 -2.37,-4.48 -2.37,-11.33v-12.8z"></path>
</vector>
下面我们写提供者,在com.llw.ble下创建一个provider包,包下创建一个ScanDeviceProvider类,代码如下所示:
public class ScanDeviceProvider extends BaseItemProvider {private final List<BleDevice> deviceList;private final AbilitySlice slice;public ScanDeviceProvider(List<BleDevice> list, AbilitySlice slice) {this.deviceList = list;this.slice = slice;}@Overridepublic int getCount() {return deviceList == null ? 0 : deviceList.size();}@Overridepublic Object getItem(int position) {if (deviceList != null && position >= 0 && position < deviceList.size()) {return deviceList.get(position);}return null;}@Overridepublic long getItemId(int position) {return position;}@Overridepublic Component getComponent(int position, Component component, ComponentContainer componentContainer) {final Component cpt;ScanDeviceHolder holder;BleDevice device = deviceList.get(position);if (component == null) {cpt = LayoutScatter.getInstance(slice).parse(ResourceTable.Layout_item_scan_device, null, false);holder = new ScanDeviceHolder(cpt);//将获取到的子组件信息绑定到列表项的实例中cpt.setTag(holder);} else {cpt = component;// 从缓存中获取到列表项实例后,直接使用绑定的子组件信息进行数据填充。holder = (ScanDeviceHolder) cpt.getTag();}holder.deviceName.setText(device.getRealName());holder.deviceAddress.setText(device.getMacAddress());holder.rssi.setText(String.format(Locale.getDefault(), "%d dBm", device.getRssi()));return cpt;}/*** 用于保存列表项的子组件信息*/public static class ScanDeviceHolder {Text deviceName;Text deviceAddress;Text rssi;public ScanDeviceHolder(Component component) {deviceName = (Text) component.findComponentById(ResourceTable.Id_device_name);deviceAddress = (Text) component.findComponentById(ResourceTable.Id_device_address);rssi = (Text) component.findComponentById(ResourceTable.Id_rssi);}}
}
通过提供者的代码,可以看到它和适配器的写法差不多,不同的是你得注意getComponent()方法中的处理,另外提供者默认提供了Item的点击方法,所以我们不用再自己去写了。
③ 显示设备
我们回到ScanSlice中使用,首先我们创建几个变量,代码如下所示:
private final List<BleDevice> mList = new ArrayList<>();private ScanDeviceProvider provider;
然后在onStart()方法中进行初始化:
@Overridepublic void onStart(Intent intent) {...provider = new ScanDeviceProvider(mList, this);lcDevice.setItemProvider(provider);//列表item点击监听lcDevice.setItemClickedListener((listContainer, component, position, id) -> {});}
这里设置了列表提供者,然后添加item点击监听,最后我们在扫描回调中渲染数据,修改代码如下所示:
private int findIndex(BleDevice bleDevice, List<BleDevice> deviceList) {int index = 0;for (final BleDevice devi : deviceList) {if (bleDevice.getMacAddress().equals(devi.getDevice().getDeviceAddr())) return index;index += 1;}return -1;}@Overridepublic void onScanResult(BleScanResult result) {BleDevice bleDevice = new BleDevice(result);int index = findIndex(bleDevice, mList);if (index == -1) {//添加新设备mList.add(bleDevice);} else {//更新已有设备的rssi和时间戳mList.get(index).setRssi(bleDevice.getRssi());}getUITaskDispatcher().syncDispatch(() -> provider.notifyDataChanged());}
这里添加一个findIndex()方法,用于添加设备和更新设备,最终通过UI线程同步刷新提供者,再修改一个开始扫描和停止扫描的方法代码:
private void startScan() {mList.clear();provider.notifyDataChanged();bleCore.startScan();txScanStatus.setText("停止");LogUtils.LogD(TAG,"开始扫描设备!");}private void stopScan() {bleCore.stopScan();txScanStatus.setText("搜索");LogUtils.LogD(TAG,"已经停止扫描设备!");}
运行一下看看:

七、源码
如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~
源码地址:HarmonyBle-Java
相关文章:
Harmony Ble 蓝牙App (一)扫描
Harmony Ble 蓝牙App (一)扫描 前言正文一、创建工程二、工程配置① 权限配置② Debug配置③ UI配置 三、扫描① 扫描接口② 扫描类 四、业务处理① Slice的生命周期② 蓝牙开关和动态权限请求 五、扫描设备六、显示设备① 自定义蓝牙类② 提供者③ 显示…...
录制第一个jmeter性能测试脚本2(http协议)——webtour
我们手工编写了一个测试计划,现在我们通过录制的方式来实现那个测试计划。也就是说‘’测试计划目标和上一节类似:让5个用户在2s内登录webtour,然后进入 页面进行查看。 目录 欢迎访问我的免费课程 PPT、安装包、视频应有尽有! …...
时间序列与 Statsmodels:预测所需的基本概念(1)
后文:时间序列与 statsmodels:预测所需的基本概念(2)-CSDN博客 一、说明 本博客解释了理解时间序列的基本概念:趋势、季节性、白噪声、平稳性,并使用自回归、差分和移动平均参数进行预测示例。这是理解任何…...
计算机网络(持续更新…)
文章目录 一、概述1. 计网概述⭐ 发展史⭐ 基本概念⭐ 分类⭐ 数据交换方式🥰 小练 2. 分层体系结构⭐ OSI 参考模型⭐TCP/IP 参考模型🥰 小练 二、物理层1. 物理层概述⭐ 四个特性 2. 通信基础⭐ 重点概念⭐ 极限数据传输率⭐ 信道复用技术🎉…...
BetterDisplay Pro for Mac(显示器校准软件)
BetterDisplay Pro是一款由waydabber开发的Mac平台上的显示器校准软件,可以帮助用户调整显示器的颜色和亮度,以获得更加真实、清晰和舒适的视觉体验。 以下是BetterDisplay Pro的主要特点: - 显示器校准:可以根据不同的需求和环境…...
Node.js之TCP(net)
Hi I’m Shendi Node.js之TCP(net) 最近使用Nodejs编写程序,需要用到自己编写的分布式工具,于是需要将Java版的用NodeJs重新写一遍,需要使用到TCP通信,于是在这里记录下Node.js TCP 的使用方法 依赖 需要使…...
何时使用Elasticsearch而不是MySql?
何时使用Elasticsearch而不是MySql? MySQL 和 Elasticsearch 是两种不同的数据管理系统,它们各有优劣,适用于不同的场景。本文将从以下几个方面对它们进行比较和分析: 数据模型查询语言索引和搜索分布式和高可用性能和扩展性使用…...
nodejs微信小程序 +python+PHP+图书销售管理系统的设计与实现-网上书店-图书商城-计算机毕业设计
目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性:…...
云原生周刊:Istio 1.20.0 发布 | 2023.11.20
开源项目推荐 DevPod DevPod 是一款纯客户端工具,可在任何后端基于 devcontainer.json 创建可重现的开发人员环境。每个开发者环境都在一个容器中运行,并通过 devcontainer.json 进行指定。通过 DevPod 提供商,这些环境可以在任何后端创建&…...
Ajax基础(应用场景|jquery实现Ajax|注意事项)
文章目录 一、Ajax简介二、基于jquery实现Ajax三、使用Ajax注意的问题1.Ajax不要与form表单同时提交2.后端响应格式问题3、使用了Ajax作为请求后的注意事项 一、Ajax简介 AJAX(Asynchronous Javascript And XML)翻译成中文就是“异步Javascript和XML”。…...
【SpringCloud】Eureka基于Ribbon负载均衡的调用链路流程分析
文章目录 前言1.调用形式2.LoadBalancerInterceptor3.负载均衡流程分析3.1 调用流程图3.2 intercept()方法3.3 execute()方法3.4 getServer()方法3.4 子类的chooseServer()方法3.5 getLoadBalancerStats().…...
Springboot和Vue+MYSQL项目(基本介绍+前后端结合初步项目)+maven+mybatis
一、基本知识 当我们谈论全栈开发时,通常指的是一个开发者能够处理整个应用程序的开发,包括前端(Front-End)和后端(Back-End)的所有层面。这三个基本的领域是: 前端开发(Front-End …...
基于单片机K型热电偶温度采集报警系统
**单片机设计介绍, 基于单片机K型热电偶温度采集报警系统 文章目录 一 概要简介系统特点系统组成工作原理应用领域 二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 # 基于单片机K型热电偶温度采集报警系统介绍 简介 该系统是基于单片…...
利用OpenCV做个熊猫表情包 二
之前写了一篇 利用OpenCV做个熊猫表情包吧_Leen的博客-CSDN博客 回想起来觉得有点太弱了,意犹未尽,每次使用需要自己去手动截取人脸,清除黑边什么的才能使用demo去合成表情,无奈之前由于安装的vs,opencv版本都比较低…...
华纳云服务器怎么清理cdn缓存?
清理 CDN(内容分发网络)缓存通常需要通过 CDN 提供商的管理界面或 API 进行操作。不同的 CDN 提供商可能有不同的方法和步骤,以下是一个通用的清理 CDN 缓存的一般步骤: 1. 登录到 CDN 提供商的管理界面: 打开你所使用的 CDN 提供商的网站。 …...
python functools.wraps保留被装饰函数属性
作用 普通装饰器 ,会覆盖函数名称,并且 会替换 函数 文档字符串 介绍 functools.wraps(wrapped[, assigned][, updated]) This is a convenience function for invoking partial(update_wrapper, wrappedwrapped, assignedassigned, updatedupdated) …...
【多线程 - 11、死锁】
死锁 1、介绍 在 Java 中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,程序不再往下执行。只能通过中止并重启的方式来让程序重新执行。要尽可能避免死锁的情况发生 2、造成死锁的原因 互斥条件: 同一资源同时只能由一个线程读…...
flask实现session开发
要在Flask应用中实现会话(session)开发,你可以使用Flask内置的session模块。以下是一个示例代码,演示在Flask应用中启用和使用会话功能: from flask import Flask, session, redirect, url_for, requestapp Flask(__…...
paddle dataset
paddle实现图像旋转 import numpy as np from PIL import Image from matplotlib import pyplot as plt from paddle.vision.transforms import functional as F import cv2imagecv2.imread(./1.jpg) imagecv2.cvtColor(image,cv2.COLOR_BGR2RGB)# 图像旋转 opencv # imgR90 …...
接口自动化测试实战:JMeter+Ant+Jenkins+钉钉机器人群通知完美结合
前言 一、本地JAVA环境安装配置,安装JAVA8和JAVA17 二、安装和配置Jmeter 三、安装和配置ant 四、jmeter + ant配置 五、jenkins安装和配置持续构建项目 六、jenkins配置流程 前言 搭建jmeter+ant+jenkins环境有些前提条件,那就是要先配置好java环境,本地java环境…...
RestClient
什么是RestClient RestClient 是 Elasticsearch 官方提供的 Java 低级 REST 客户端,它允许HTTP与Elasticsearch 集群通信,而无需处理 JSON 序列化/反序列化等底层细节。它是 Elasticsearch Java API 客户端的基础。 RestClient 主要特点 轻量级ÿ…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...
2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...
TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案
一、TRS收益互换的本质与业务逻辑 (一)概念解析 TRS(Total Return Swap)收益互换是一种金融衍生工具,指交易双方约定在未来一定期限内,基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...
Xen Server服务器释放磁盘空间
disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...
现有的 Redis 分布式锁库(如 Redisson)提供了哪些便利?
现有的 Redis 分布式锁库(如 Redisson)相比于开发者自己基于 Redis 命令(如 SETNX, EXPIRE, DEL)手动实现分布式锁,提供了巨大的便利性和健壮性。主要体现在以下几个方面: 原子性保证 (Atomicity)ÿ…...
uniapp 开发ios, xcode 提交app store connect 和 testflight内测
uniapp 中配置 配置manifest 文档:manifest.json 应用配置 | uni-app官网 hbuilderx中本地打包 下载IOS最新SDK 开发环境 | uni小程序SDK hbulderx 版本号:4.66 对应的sdk版本 4.66 两者必须一致 本地打包的资源导入到SDK 导入资源 | uni小程序SDK …...
