Harmony Ble 蓝牙App (一)扫描(上)https://developer.aliyun.com/article/1407933
三、扫描
首先我们在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 { @Override public void scanResultEvent(BleScanResult bleScanResult) { if (scanCallback != null) { scanCallback.onScanResult(bleScanResult); } } @Override public void scanFailedEvent(int resultCode) { if (scanCallback != null) { scanCallback.onScanFailed(String.valueOf(resultCode)); } } @Override public 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; @Override public 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(); @Override public void onStart(Intent intent) { super.onStart(intent); super.setUIContent(ResourceTable.Layout_slice_scan); LogUtils.LogD(TAG, "onStart"); } @Override public void onActive() { LogUtils.LogD(TAG, "onActive"); } @Override protected void onInactive() { LogUtils.LogD(TAG, "onInactive"); } @Override public void onForeground(Intent intent) { LogUtils.LogD(TAG, "onForeground"); } @Override protected void onBackground() { LogUtils.LogD(TAG, "onBackground"); } @Override protected 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; @Override public 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 -> { }); } @Override public void onActive() { // 判断是否打开蓝牙 if (!bleCore.isEnableBt()) { //打开蓝牙 bleCore.enableBt(); return; } } }
首先在onStart()
中对BleCore进行实例化,findComponentById
就如同findViewById
,然后在onActive()
中调用刚才我们所写的方法。
@Override public void onActive() { // 判断是否打开蓝牙 if (!bleCore.isEnableBt()) { //打开蓝牙 bleCore.enableBt(); return; } }
然后是定位权限的处理,同样在onActive()中,增加代码如下所示:
@Override public 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
文本的点击事件,代码如下所示:
@Override public void onStart(Intent intent) { ... bleCore.setPhyScanCallback(this); //点击监听 txScanStatus.setClickedListener(component -> { if (bleCore.isScanning()) { stopScan();//扫描开关停止扫描 } else { startScan();//开始扫描 } }); }
这里this
会报错,鼠标放在上面,Alt + Enter,出现弹窗。
选择最后一个,就会给你实现ScanCallback
中的onScanResult()
方法,代码如下所示:
@Override public void onScanResult(BleScanResult result) { LogUtils.LogD(TAG, result.getPeripheralDevice().getDeviceAddr()); }
我们在里面打印一下扫描到的设备Mac地址,最后我们在onActive()
中增加如下所示代码:
@Override public 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"?> <DirectionalLayout xmlns: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"> <Image ohos:height="match_content" ohos:width="match_content" ohos:end_margin="16vp" ohos:image_src="$graphic:ic_bluetooth" ohos:start_margin="16vp"/> <DirectionalLayout ohos:height="match_content" ohos:width="match_content" ohos:orientation="vertical" ohos:weight="1"> <Text ohos:id="$+id:device_name" ohos:height="match_content" ohos:width="match_content" ohos:text="设备名称" ohos:text_size="16fp"/> <Text ohos: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> <Text ohos: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"?> <vector xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="48vp" ohos:width="48vp" ohos:viewportHeight="1024" ohos:viewportWidth="1024"> <path ohos: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; } @Override public int getCount() { return deviceList == null ? 0 : deviceList.size(); } @Override public Object getItem(int position) { if (deviceList != null && position >= 0 && position < deviceList.size()) { return deviceList.get(position); } return null; } @Override public long getItemId(int position) { return position; } @Override public 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()方法中进行初始化:
@Override public 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; } @Override public 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