前言
在上一篇中我们完成了连接和发现服务两个动作,那么再发现服务之后要做什么呢?发现服务只是让你知道设备有什么服务,可以做什么事情。
正文
本篇要做的是显示服务下的特性,首先我们了解一下特性的基本知识。在蓝牙低功耗(BLE)中,特性(Characteristic)
是蓝牙设备提供的一种数据单元,用于描述设备的某个属性或功能。特性包含了一系列的属性和值,可以用于读取、写入和通知数据。
BLE特性相关的关键概念和说明:
UUID(Universally Unique Identifier)
:每个特性都会有一个唯一的UUID,用于标识该特性。值(Value)
:特性包含一个值,可以是字节数组、字符串或其他数据类型。该值代表特性的当前状态或数据内容。属性(Properties)
:特性具有一组属性,包括读、写、通知等。属性决定了可以对特性进行哪些操作。读(Read)
:允许外部设备从特性中读取当前的值。写(Write)
:允许外部设备向特性写入一个新的值。通知(Notify)
:当特性的值发生变化时,可以通过通知方式将新的值发送给订阅该特性的外部设备。描述符(Descriptor)
:特性可以附带一个或多个描述符,用于提供关于特性的额外信息或配置。
使用BLE特性,可以实现各种功能和数据交互,例如传感器数据的读取、设备状态的监控、远程控制等。特性的读写和通知操作可以通过与蓝牙设备的交互来实现。需要注意的是,BLE特性的操作和功能是由设备的厂商定义的,并在设备的GATT(Generic Attribute Profile)配置文件中进行描述。
首先理清一下思路,我们现在知道服务下面有特性,特性下面有一些属性值,其中属性(Properties)
尤为重要,因为它决定了你的特性可以进行那些操作。用一个图来说明服务,特性,属性之间的关系。
一、获取属性列表
下面我们先获取最下面的属性,这是一个列表,属性值的处理有一些不同,首先我们在BleUtils中增加一个函数,代码如下所示:
/** * 获取属性 */ public static List<String> getProperties(int property) { List<String> properties = new ArrayList<>(); for (int i = 0; i < 8; i++) { switch (property & (1 << i)) { case 0x01: properties.add("Broadcast"); break; case 0x02: properties.add("Read"); break; case 0x04: properties.add("Write No Response"); break; case 0x08: properties.add("Write"); break; case 0x10: properties.add("Notify"); break; case 0x20: properties.add("Indicate"); break; case 0x40: properties.add("Authenticated Signed Writes"); break; case 0x80: properties.add("Extended Properties"); break; } } return properties; }
这里是通过位运算进行计算属性的值,首先是循环遍历,先左移再按位与,得到最终的值,根据值得到属性描述,这些描述就是具体的功能操作。会返回一个属性列表,有了列表我们就可以写一个适配器了。
二、属性提供者
首先我们在layout下创建一个item_property.xml
,代码如下所示:
<?xml version="1.0" encoding="utf-8"?> <Text xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:id="$+id:tx_property" ohos:height="match_content" ohos:width="match_content" ohos:end_margin="8vp" ohos:text="property" ohos:text_color="$color:blue" ohos:text_size="14fp"/>
因为是String类型,所以我们就直接用一个Text显示即可,下面我们写提供者,在provider
包下新建一个PropertyProvider
类,代码如下所示:
public class PropertyProvider extends BaseItemProvider { private final List<String> propertyList; private final AbilitySlice slice; public PropertyProvider(List<String> list, AbilitySlice slice) { this.propertyList = list; this.slice = slice; } @Override public int getCount() { return propertyList == null ? 0 : propertyList.size(); } @Override public Object getItem(int position) { if (propertyList != null && position >= 0 && position < propertyList.size()) { return propertyList.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; ServiceHolder holder; if (component == null) { cpt = LayoutScatter.getInstance(slice).parse(ResourceTable.Layout_item_property, null, false); holder = new ServiceHolder(cpt); //将获取到的子组件信息绑定到列表项的实例中 cpt.setTag(holder); } else { cpt = component; // 从缓存中获取到列表项实例后,直接使用绑定的子组件信息进行数据填充。 holder = (ServiceHolder) cpt.getTag(); } holder.txProperty.setText(propertyList.get(position)); return cpt; } /** * 用于保存列表项的子组件信息 */ public static class ServiceHolder { Text txProperty; public ServiceHolder(Component component) { txProperty = (Text) component.findComponentById(ResourceTable.Id_tx_property); } } }
这里进行了属性的点击监听,我们可以回调到特性适配器中去处理,下面我们要处理的就是特性了。
三、获取特性名称
首先是特性名称,同样是根据UUID,同样是那个PDF文档,在BleUtils中增加一个getCharacteristicsName()
函数,代码有点多,如下所示:
/** * 获取特性名称 * @param uuid UUID */ public static String getCharacteristicsName(UUID uuid) { String targetUuid = getShortUUID(uuid); switch (targetUuid) { case "0x2A00": return "Device Name"; case "0x2A01": return "Appearance"; case "0x2A02": return "Peripheral Privacy Flag"; case "0x2A03": return "Reconnection Address"; case "0x2A04": return "Peripheral Preferred Connection Parameters"; case "0x2A05": return "Service Changed"; case "0x2A06": return "Alert Level"; case "0x2A07": return "Tx Power Level"; case "0x2A08": return "Date Time"; case "0x2A09": return "Day of Week"; case "0x2A0A": return "Day Date Time"; case "0x2A0C": return "Exact Time 256"; case "0x2A0D": return "DST Offset"; case "0x2A0E": return "Time Zone"; case "0x2A0F": return "Local Time Information"; case "0x2A11": return "Time with DST"; case "0x2A12": return "Time Accuracy"; case "0x2A13": return "Time Source"; case "0x2A14": return "Reference Time Information"; case "0x2A16": return "Time Update Control Point"; case "0x2A17": return "Time Update State"; case "0x2A18": return "Glucose Measurement"; case "0x2A19": return "Battery Level"; case "0x2A1C": return "Temperature Measurement"; case "0x2A1D": return "Temperature Type"; case "0x2A1E": return "Intermediate Temperature"; case "0x2A21": return "Measurement Interval"; case "0x2A22": return "Boot Keyboard Input Report"; case "0x2A23": return "System ID"; case "0x2A24": return "Model Number String"; case "0x2A25": return "Serial Number String"; case "0x2A26": return "Firmware Revision String"; case "0x2A27": return "Hardware Revision String"; case "0x2A28": return "Software Revision String"; case "0x2A29": return "Manufacturer Name String"; case "0x2A2A": return "IEEE 11073-20601 Regulatory Certification Data List"; case "0x2A2B": return "Current Time"; case "0x2A2C": return "Magnetic Declination"; case "0x2A31": return "Scan Refresh"; case "0x2A32": return "Boot Keyboard Output Report"; case "0x2A33": return "Boot Mouse Input Report"; case "0x2A34": return "Glucose Measurement Context"; case "0x2A35": return "Blood Pressure Measurement"; case "0x2A36": return "Intermediate Cuff Pressure"; case "0x2A37": return "Heart Rate Measurement"; case "0x2A38": return "Body Sensor Location"; case "0x2A39": return "Heart Rate Control Point"; case "0x2A3F": return "Alert Status"; case "0x2A40": return "Ringer Control Point"; case "0x2A41": return "Ringer Setting"; case "0x2A42": return "Alert Category ID Bit Mask"; case "0x2A43": return "Alert Category ID"; case "0x2A44": return "Alert Notification Control Point"; case "0x2A45": return "Unread Alert Status"; case "0x2A46": return "New Alert"; case "0x2A47": return "Supported New Alert Category"; case "0x2A48": return "Supported Unread Alert Category"; case "0x2A49": return "Blood Pressure Feature"; case "0x2A4A": return "HID Information"; case "0x2A4B": return "Report Map"; case "0x2A4C": return "HID Control Point"; case "0x2A4D": return "Report"; case "0x2A4E": return "Protocol Mode"; case "0x2A4F": return "Scan Interval Window"; case "0x2A50": return "PnP ID"; case "0x2A51": return "Glucose Feature"; case "0x2A52": return "Record Access Control Point"; case "0x2A53": return "RSC Measurement"; case "0x2A54": return "RSC Feature"; case "0x2A55": return "SC Control Point"; case "0x2A5A": return "Aggregate"; case "0x2A5B": return "CSC Measurement"; case "0x2A5C": return "CSC Feature"; case "0x2A5D": return "Sensor Location"; case "0x2A5E": return "PLX Spot-Check Measurement"; case "0x2A5F": return "PLX Continuous Measurement"; case "0x2A60": return "PLX Features"; case "0x2A63": return "Cycling Power Measurement"; case "0x2A64": return "Cycling Power Vector"; case "0x2A65": return "Cycling Power Feature"; case "0x2A66": return "Cycling Power Control Point"; case "0x2A67": return "Location and Speed"; case "0x2A68": return "Navigation"; case "0x2A69": return "Position Quality"; case "0x2A6A": return "LN Feature"; case "0x2A6B": return "LN Control Point"; case "0x2A6C": return "Elevation"; case "0x2A6D": return "Pressure"; case "0x2A6E": return "Temperature"; case "0x2A6F": return "Humidity"; case "0x2A70": return "True Wind Speed"; case "0x2A71": return "True Wind Direction"; case "0x2A72": return "Apparent Wind Speed"; case "0x2A73": return "Apparent Wind Direction"; case "0x2A74": return "Gust Factor"; case "0x2A75": return "Pollen Concentration"; case "0x2A76": return "UV Index"; case "0x2A77": return "Irradiance"; case "0x2A78": return "Rainfall"; case "0x2A79": return "Wind Chill"; case "0x2A7A": return "Heat Index"; case "0x2A7B": return "Dew Point"; case "0x2A7D": return "Descriptor Value Changed"; case "0x2A7E": return "Aerobic Heart Rate Lower Limit"; case "0x2A7F": return "Aerobic Threshold"; case "0x2A80": return "Age"; case "0x2A81": return "Anaerobic Heart Rate Lower Limit"; case "0x2A82": return "Anaerobic Heart Rate Upper Limit"; case "0x2A83": return "Anaerobic Threshold"; case "0x2A84": return "Aerobic Heart Rate Upper Limit"; case "0x2A85": return "Date of Birth"; case "0x2A86": return "Date of Threshold Assessment"; case "0x2A87": return "Email Address"; case "0x2A88": return "Fat Burn Heart Rate Lower Limit"; case "0x2A89": return "Fat Burn Heart Rate Upper Limit"; case "0x2A8A": return "First Name"; case "0x2A8B": return "Five Zone Heart Rate Limits"; case "0x2A8C": return "Gender"; case "0x2A8D": return "Heart Rate Max"; case "0x2A8E": return "Height"; case "0x2A8F": return "Hip Circumference"; case "0x2A90": return "Last Name"; case "0x2A91": return "Maximum Recommended Heart Rate"; case "0x2A92": return "Resting Heart Rate"; case "0x2A93": return "Sport Type for Aerobic and Anaerobic Thresholds"; case "0x2A94": return "Three Zone Heart Rate Limits"; case "0x2A95": return "Two Zone Heart Rate Limits"; case "0x2A96": return "VO2 Max"; case "0x2A97": return "Waist Circumference"; case "0x2A98": return "Weight"; case "0x2A99": return "Database Change Increment"; case "0x2A9A": return "User Index"; case "0x2A9B": return "Body Composition Feature"; case "0x2A9C": return "Body Composition Measurement"; case "0x2A9D": return "Weight Measurement"; case "0x2A9E": return "Weight Scale Feature"; case "0x2A9F": return "User Control Point"; case "0x2AA0": return "Magnetic Flux Density - 2D"; case "0x2AA1": return "Magnetic Flux Density - 3D"; case "0x2AA2": return "Language"; case "0x2AA3": return "Barometric Pressure Trend"; case "0x2AA4": return "Bond Management Control Point"; case "0x2AA5": return "Bond Management Feature"; case "0x2AA6": return "Central Address Resolution"; case "0x2AA7": return "CGM Measurement"; case "0x2AA8": return "CGM Feature"; case "0x2AA9": return "CGM Status"; case "0x2AAA": return "CGM Session Start Time"; case "0x2AAB": return "CGM Session Run Time"; case "0x2AAC": return "CGM Specific Ops Control Point"; case "0x2AAD": return "Indoor Positioning Configuration"; case "0x2AAE": return "Latitude"; case "0x2AAF": return "Longitude"; case "0x2AB0": return "Local North Coordinate"; case "0x2AB1": return "Local East Coordinate"; case "0x2AB2": return "Floor Number"; case "0x2AB3": return "Altitude"; case "0x2AB4": return "Uncertainty"; case "0x2AB5": return "Location Name"; case "0x2AB6": return "URI"; case "0x2AB7": return "HTTP Headers"; case "0x2AB8": return "HTTP Status Code"; case "0x2AB9": return "HTTP Entity Body"; case "0x2ABA": return "HTTP Control Point"; case "0x2ABB": return "HTTPS Security"; case "0x2ABC": return "TDS Control Point"; case "0x2ABD": return "OTS Feature"; case "0x2ABE": return "Object Name"; case "0x2ABF": return "Object Type"; case "0x2AC0": return "Object Size"; case "0x2AC1": return "Object First -Created"; case "0x2AC2": return "Object Last - Modified"; case "0x2AC3": return "Object ID"; case "0x2AC4": return "Object Properties"; case "0x2AC5": return "Object Action Control Point"; case "0x2AC6": return "Object List Control Point"; case "0x2AC7": return "Object List Filter"; case "0x2AC8": return "Object Changed"; case "0x2AC9": return "Resolvable Private Address Only"; case "0x2ACC": return "Fitness Machine Feature"; case "0x2ACD": return "Treadmill Data"; case "0x2ACE": return "Cross Trainer Data"; case "0x2ACF": return "Step Climber Data"; case "0x2AD0": return "Stair Climber Data"; case "0x2AD1": return "Rower Data"; case "0x2AD2": return "Indoor Bike Data"; case "0x2AD3": return "Training Status"; case "0x2AD4": return "Supported Speed Range"; case "0x2AD5": return "Supported Inclination Range"; case "0x2AD6": return "Supported Resistance Level Range"; case "0x2AD7": return "Supported Heart Rate Range"; case "0x2AD8": return "Supported Power Range"; case "0x2AD9": return "Fitness Machine Control Point"; case "0x2ADA": return "Fitness Machine Status"; case "0x2ADB": return "Mesh Provisioning Data In"; case "0x2ADC": return "Mesh Provisioning Data Out"; case "0x2ADD": return "Mesh Proxy Data In"; case "0x2ADE": return "Mesh Proxy Data Out"; case "0x2AE0": return "Average Current"; case "0x2AE1": return "Average Voltage"; case "0x2AE2": return "Boolean"; case "0x2AE3": return "Chromatic Distance from Planckian"; case "0x2AE4": return "Chromaticity Coordinates"; case "0x2AE5": return "Chromaticity in CCT and Duv Values"; case "0x2AE6": return "Chromaticity Tolerance"; case "0x2AE7": return "CIE 13.3 - 1995 Color Rendering Index"; case "0x2AE8": return "Coefficient"; case "0x2AE9": return "Correlated Color Temperature"; case "0x2AEA": return "Count 16"; case "0x2AEB": return "Count 24"; case "0x2AEC": return "Country Code"; case "0x2AED": return "Date UTC"; case "0x2AEE": return "Electric Current"; case "0x2AEF": return "Electric Current Range"; case "0x2AF0": return "Electric Current Specification"; case "0x2AF1": return "Electric Current Statistics"; case "0x2AF2": return "Energy"; case "0x2AF3": return "Energy in a Period of Day"; case "0x2AF4": return "Event Statistics"; case "0x2AF5": return "Fixed String 16"; case "0x2AF6": return "Fixed String 24"; case "0x2AF7": return "Fixed String 36"; case "0x2AF8": return "Fixed String 8"; case "0x2AF9": return "Generic Level"; case "0x2AFA": return "Global Trade Item Number"; case "0x2AFB": return "Illuminance"; case "0x2AFC": return "Luminous Efficacy"; case "0x2AFD": return "Luminous Energy"; case "0x2AFE": return "Luminous Exposure"; case "0x2AFF": return "Luminous Flux"; case "0x2B00": return "Luminous Flux Range"; case "0x2B01": return "Luminous Intensity"; case "0x2B02": return "Mass Flow"; case "0x2B03": return "Perceived Lightness"; case "0x2B04": return "Percentage 8"; case "0x2B05": return "Power"; case "0x2B06": return "Power Specification"; case "0x2B07": return "Relative Runtime in a Current Range"; case "0x2B08": return "Relative Runtime in a Generic Level Range"; case "0x2B09": return "Relative Value in a Voltage Range"; case "0x2B0A": return "Relative Value in an Illuminance Range"; case "0x2B0B": return "Relative Value in a Period of Day"; case "0x2B0C": return "Relative Value in a Temperature Range"; case "0x2B0D": return "Temperature 8"; case "0x2B0E": return "Temperature 8 in a Period of Day"; case "0x2B0F": return "Temperature 8 Statistics"; case "0x2B10": return "Temperature Range"; case "0x2B11": return "Temperature Statistics"; case "0x2B12": return "Time Decihour 8"; case "0x2B13": return "Time Exponential 8"; case "0x2B14": return "Time Hour 24"; case "0x2B15": return "Time Millisecond 24"; case "0x2B16": return "Time Second 16"; case "0x2B17": return "Time Second 8"; case "0x2B18": return "Voltage"; case "0x2B19": return "Voltage Specification"; case "0x2B1A": return "Voltage Statistics"; case "0x2B1B": return "Volume Flow"; case "0x2B1C": return "Chromaticity Coordinate"; case "0x2B1D": return "RC Feature"; case "0x2B1E": return "RC Settings"; case "0x2B1F": return "Reconnection Configuration Control Point"; case "0x2B20": return "IDD Status Changed"; case "0x2B21": return "IDD Status"; case "0x2B22": return "IDD Annunciation Status"; case "0x2B23": return "IDD Features"; case "0x2B24": return "IDD Status Reader Control Point"; case "0x2B25": return "IDD Command Control Point"; case "0x2B26": return "IDD Command Data"; case "0x2B27": return "IDD Record Access Control Point"; case "0x2B28": return "IDD History Data"; case "0x2B29": return "Client Supported Features"; case "0x2B2A": return "Database Hash"; case "0x2B2B": return "BSS Control Point"; case "0x2B2C": return "BSS Response"; case "0x2B2D": return "Emergency ID"; case "0x2B2E": return "Emergency Text"; case "0x2B2F": return "ACS Status"; case "0x2B30": return "ACS Data In"; case "0x2B31": return "ACS Data Out Notify"; case "0x2B32": return "ACS Data Out Indicate"; case "0x2B33": return "ACS Control Point"; case "0x2B34": return "Enhanced Blood Pressure Measurement"; case "0x2B35": return "Enhanced Intermediate Cuff Pressure"; case "0x2B36": return "Blood Pressure Record"; case "0x2B37": return "Registered User"; case "0x2B38": return "BR - EDR Handover Data"; case "0x2B39": return "Bluetooth SIG Data"; case "0x2B3A": return "Server Supported Features"; case "0x2B3B": return "Physical Activity Monitor Features"; case "0x2B3C": return "General Activity Instantaneous Data"; case "0x2B3D": return "General Activity Summary Data"; case "0x2B3E": return "CardioRespiratory Activity Instantaneous Data"; case "0x2B3F": return "CardioRespiratory Activity Summary Data"; case "0x2B40": return "Step Counter Activity Summary Data"; case "0x2B41": return "Sleep Activity Instantaneous Data"; case "0x2B42": return "Sleep Activity Summary Data"; case "0x2B43": return "Physical Activity Monitor Control Point"; case "0x2B44": return "Activity Current Session"; case "0x2B45": return "Physical Activity Session Descriptor"; case "0x2B46": return "Preferred Units"; case "0x2B47": return "High Resolution Height"; case "0x2B48": return "Middle Name"; case "0x2B49": return "Stride Length"; case "0x2B4A": return "Handedness"; case "0x2B4B": return "Device Wearing Position"; case "0x2B4C": return "Four Zone Heart Rate Limits"; case "0x2B4D": return "High Intensity Exercise Threshold"; case "0x2B4E": return "Activity Goal"; case "0x2B4F": return "Sedentary Interval Notification"; case "0x2B50": return "Caloric Intake"; case "0x2B51": return "TMAP Role"; case "0x2B77": return "Audio Input State"; case "0x2B78": return "Gain Settings Attribute"; case "0x2B79": return "Audio Input Type"; case "0x2B7A": return "Audio Input Status"; case "0x2B7B": return "Audio Input Control Point"; case "0x2B7C": return "Audio Input Description"; case "0x2B7D": return "Volume State"; case "0x2B7E": return "Volume Control Point"; case "0x2B7F": return "Volume Flags"; case "0x2B80": return "Volume Offset State"; case "0x2B81": return "Audio Location"; case "0x2B82": return "Volume Offset Control Point"; case "0x2B83": return "Audio Output Description"; case "0x2B84": return "Set Identity Resolving Key"; case "0x2B85": return "Coordinated Set Size"; case "0x2B86": return "Set Member Lock"; case "0x2B87": return "Set Member Rank"; case "0x2B88": return "Encrypted Data Key Material"; case "0x2B89": return "Apparent Energy 32"; case "0x2B8A": return "Apparent Power"; case "0x2B8B": return "Live Health Observations"; case "0x2B8C": return "CO \\{} text-subscript { 2 } Concentration"; case "0x2B8D": return "Cosine of the Angle"; case "0x2B8E": return "Device Time Feature"; case "0x2B8F": return "Device Time Parameters"; case "0x2B90": return "Device Time"; case "0x2B91": return "Device Time Control Point"; case "0x2B92": return "Time Change Log Data"; case "0x2B93": return "Media Player Name"; case "0x2B94": return "Media Player Icon Object ID"; case "0x2B95": return "Media Player Icon URL"; case "0x2B96": return "Track Changed"; case "0x2B97": return "Track Title"; case "0x2B98": return "Track Duration"; case "0x2B99": return "Track Position"; case "0x2B9A": return "Playback Speed"; case "0x2B9B": return "Seeking Speed"; case "0x2B9C": return "Current Track Segments Object ID"; case "0x2B9D": return "Current Track Object ID"; case "0x2B9E": return "Next Track Object ID"; case "0x2B9F": return "Parent Group Object ID"; case "0x2BA0": return "Current Group Object ID"; case "0x2BA1": return "Playing Order"; case "0x2BA2": return "Playing Orders Supported"; case "0x2BA3": return "Media State"; case "0x2BA4": return "Media Control Point"; case "0x2BA5": return "Media Control Point Opcodes Supported"; case "0x2BA6": return "Search Results Object ID"; case "0x2BA7": return "Search Control Point"; case "0x2BA8": return "Energy 32"; case "0x2BA9": return "Media Player Icon Object Type"; case "0x2BAA": return "Track Segments Object Type"; case "0x2BAB": return "Track Object Type"; case "0x2BAC": return "Group Object Type"; case "0x2BAD": return "Constant Tone Extension Enable"; case "0x2BAE": return "Advertising Constant Tone Extension Minimum Length"; case "0x2BAF": return "Advertising Constant Tone Extension Minimum Transmit Count"; case "0x2BB0": return "Advertising Constant Tone Extension Transmit Duration"; case "0x2BB1": return "Advertising Constant Tone Extension Interval"; case "0x2BB2": return "Advertising Constant Tone Extension PHY"; case "0x2BB3": return "Bearer Provider Name"; case "0x2BB4": return "Bearer UCI"; case "0x2BB5": return "Bearer Technology"; case "0x2BB6": return "Bearer URI Schemes Supported List"; case "0x2BB7": return "Bearer Signal Strength"; case "0x2BB8": return "Bearer Signal Strength Reporting Interval"; case "0x2BB9": return "Bearer List Current Calls"; case "0x2BBA": return "Content Control ID"; case "0x2BBB": return "Status Flags"; case "0x2BBC": return "Incoming Call Target Bearer URI"; case "0x2BBD": return "Call State"; case "0x2BBE": return "Call Control Point"; case "0x2BBF": return "Call Control Point Optional Opcodes"; case "0x2BC0": return "Termination Reason"; case "0x2BC1": return "Incoming Call"; case "0x2BC2": return "Call Friendly Name"; case "0x2BC3": return "Mute"; case "0x2BC4": return "Sink ASE"; case "0x2BC5": return "Source ASE"; case "0x2BC6": return "ASE Control Point"; case "0x2BC7": return "Broadcast Audio Scan Control Point"; case "0x2BC8": return "Broadcast Receive State"; case "0x2BC9": return "Sink PAC"; case "0x2BCA": return "Sink Audio Locations"; case "0x2BCB": return "Source PAC"; case "0x2BCC": return "Source Audio Locations"; case "0x2BCD": return "Available Audio Contexts"; case "0x2BCE": return "Supported Audio Contexts"; case "0x2BCF": return "Ammonia Concentration"; case "0x2BD0": return "Carbon Monoxide Concentration"; case "0x2BD1": return "Methane Concentration"; case "0x2BD2": return "Nitrogen Dioxide Concentration"; case "0x2BD3": return "Non -Methane Volatile Organic Compounds Concentration"; case "0x2BD4": return "Ozone Concentration"; case "0x2BD5": return "Particulate Matter - PM1 Concentration"; case "0x2BD6": return "Particulate Matter - PM2.5 Concentration"; case "0x2BD7": return "Particulate Matter - PM10 Concentration"; case "0x2BD8": return "Sulfur Dioxide Concentration"; case "0x2BD9": return "Sulfur Hexafluoride Concentration"; case "0x2BDA": return "Hearing Aid Features"; case "0x2BDB": return "Hearing Aid Preset Control Point"; case "0x2BDC": return "Active Preset Index"; case "0x2BDD": return "Stored Health Observations"; case "0x2BDE": return "Fixed String 64"; case "0x2BDF": return "High Temperature"; case "0x2BE0": return "High Voltage"; case "0x2BE1": return "Light Distribution"; case "0x2BE2": return "Light Output"; case "0x2BE3": return "Light Source Type"; case "0x2BE4": return "Noise"; case "0x2BE5": return "Relative Runtime in a Correlated Color Temperature Range"; case "0x2BE6": return "Time Second 32"; case "0x2BE7": return "VOC Concentration"; case "0x2BE8": return "Voltage Frequency"; case "0x2BE9": return "Battery Critical Status"; case "0x2BEA": return "Battery Health Status"; case "0x2BEB": return "Battery Health Information"; case "0x2BEC": return "Battery Information"; case "0x2BED": return "Battery Level Status"; case "0x2BEE": return "Battery Time Status"; case "0x2BEF": return "Estimated Service Date"; case "0x2BF0": return "Battery Energy Status"; case "0x2BF1": return "Observation Schedule Changed"; case "0x2BF2": return "Current Elapsed Time"; case "0x2BF3": return "Health Sensor Features"; case "0x2BF4": return "GHS Control Point"; case "0x2BF5": return "LE GATT Security Levels"; case "0x2BF6": return "ESL Address"; case "0x2BF7": return "AP Sync Key Material"; case "0x2BF8": return "ESL Response Key Material"; case "0x2BF9": return "ESL Current Absolute Time"; case "0x2BFA": return "ESL Display Information"; case "0x2BFB": return "ESL Image Information"; case "0x2BFC": return "ESL Sensor Information"; case "0x2BFD": return "ESL LED Information"; case "0x2BFE": return "ESL Control Point"; case "0x2BFF": return "UDI for Medical Devices"; default: return "Unknown Characteristics"; } }
同修改一下原有的getServiceUUID()
为getShortUUID()
,只改名字而已,之前命名有点不太严谨,记得改一下使用的地方,如下图所示,在ServiceProvider
中
四、特性提供者
首先我们在layout下创建一个item_characteristic.xml
,代码如下所示:
<?xml version="1.0" encoding="utf-8"?> <DependentLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="match_content" ohos:width="match_parent" ohos:background_element="#FFFFFF" ohos:bottom_margin="2vp" ohos:bottom_padding="8vp" ohos:end_padding="16vp" ohos:start_padding="16vp" ohos:top_padding="8vp"> <Text ohos:id="$+id:tx_character_name" ohos:height="match_content" ohos:width="match_content" ohos:background_element="$color:black" ohos:text="服务" ohos:text_size="16fp"/> <Text ohos:id="$+id:tx_uuid_title" ohos:height="match_content" ohos:width="match_content" ohos:below="$id:tx_character_name" ohos:text="UUID:" ohos:text_color="$color:gray" ohos:text_size="16fp" ohos:top_margin="2vp"/> <Text ohos:id="$+id:tx_uuid" ohos:height="match_content" ohos:width="match_content" ohos:background_element="$color:black" ohos:below="$id:tx_character_name" ohos:end_of="$id:tx_uuid_title" ohos:text="UUID" ohos:text_size="16fp" ohos:top_margin="2vp"/> <Text ohos:id="$+id:tx_property_title" ohos:height="match_content" ohos:width="match_content" ohos:below="$id:tx_uuid_title" ohos:text="Properties:" ohos:text_color="$color:gray" ohos:text_size="16fp" ohos:top_margin="2vp"/> <ListContainer ohos:id="$+id:lc_property" ohos:height="match_content" ohos:width="match_parent" ohos:orientation="horizontal" ohos:align_bottom="$id:tx_property_title" ohos:align_top="$id:tx_property_title" ohos:end_of="$id:tx_property_title"/> </DependentLayout>
这个布局中唯一说明的一点就是ListContainer
的orientation="horizontal"
属性,这表示列表是横向,默认是纵向的。这里显示特性的名称和UUIID,同时加载属性列表,然后写适配器,因为需要操作属性的缘故,这些写一个接口,在provider
包下新建一个OperateCallback
接口,代码如下所示:
public interface OperateCallback { /** * 属性操作 */ void onPropertyOperate(GattCharacteristic characteristic, String operateName); }
通过这个接口可以知道当前操作的是那个特性和属性名称。下面我们写适配器,在provider
包下新建一个CharacteristicProvider
类,代码如下所示:
public class CharacteristicProvider extends BaseItemProvider { private final List<GattCharacteristic> characteristicList; private final AbilitySlice slice; private final OperateCallback operateCallback; public CharacteristicProvider(List<GattCharacteristic> list, AbilitySlice slice, OperateCallback operateCallback) { this.characteristicList = list; this.slice = slice; this.operateCallback = operateCallback; } @Override public int getCount() { return characteristicList == null ? 0 : characteristicList.size(); } @Override public Object getItem(int position) { if (characteristicList != null && position >= 0 && position < characteristicList.size()) { return characteristicList.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; ServiceHolder holder; GattCharacteristic characteristic = characteristicList.get(position); if (component == null) { cpt = LayoutScatter.getInstance(slice).parse(ResourceTable.Layout_item_characteristic, null, false); holder = new ServiceHolder(cpt); //将获取到的子组件信息绑定到列表项的实例中 cpt.setTag(holder); } else { cpt = component; // 从缓存中获取到列表项实例后,直接使用绑定的子组件信息进行数据填充。 holder = (ServiceHolder) cpt.getTag(); } holder.txCharacterName.setText(BleUtils.getCharacteristicsName(characteristic.getUuid())); holder.txUuid.setText(BleUtils.getShortUUID(characteristic.getUuid())); List<String> properties = BleUtils.getProperties(characteristic.getProperties()); //加载属性 holder.lcProperty.setItemProvider(new PropertyProvider(properties, slice)); //属性列表点击 holder.lcProperty.setItemClickedListener((listContainer, component1, propertyPosition, l) -> { if (operateCallback != null) { //属性操作回调 operateCallback.onPropertyOperate(characteristic, properties.get(propertyPosition)); } }); return cpt; } /** * 用于保存列表项的子组件信息 */ public static class ServiceHolder { Text txCharacterName; Text txUuid; ListContainer lcProperty; public ServiceHolder(Component component) { txCharacterName = (Text) component.findComponentById(ResourceTable.Id_tx_character_name); txUuid = (Text) component.findComponentById(ResourceTable.Id_tx_uuid); lcProperty = (ListContainer) component.findComponentById(ResourceTable.Id_lc_property); } } }
在这里我们就可以处理特性的名称和UUID显示,同时加载属性提供者,显示出来。
五、加载特性
因为特性是在服务下的,所以我们可以在服务适配器中加载特性适配器。首先我们修改一下item_service.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:background_element="#FFFFFF" ohos:bottom_margin="2vp" ohos:orientation="vertical"> <DependentLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:id="$+id:item_service" ohos:height="match_content" ohos:width="match_parent" ohos:background_element="#FFFFFF" ohos:bottom_padding="8vp" ohos:end_padding="16vp" ohos:start_padding="16vp" ohos:top_padding="8vp"> <Text ohos:id="$+id:tx_service_name" ohos:height="match_content" ohos:width="match_content" ohos:background_element="$color:black" ohos:text="服务" ohos:text_size="16fp"/> <Text ohos:id="$+id:tx_uuid_title" ohos:height="match_content" ohos:width="match_content" ohos:below="$id:tx_service_name" ohos:text="UUID:" ohos:text_color="$color:gray" ohos:text_size="16fp" ohos:top_margin="2vp"/> <Text ohos:id="$+id:tx_uuid" ohos:height="match_content" ohos:width="match_content" ohos:background_element="$color:black" ohos:below="$id:tx_service_name" ohos:end_of="$id:tx_uuid_title" ohos:text="UUID" ohos:text_size="16fp" ohos:top_margin="2vp"/> <Text ohos:id="$+id:tx_service_info" ohos:height="match_content" ohos:width="match_content" ohos:below="$id:tx_uuid_title" ohos:text="PRIMARY SERVICE" ohos:text_color="$color:gray" ohos:text_size="16fp" ohos:top_margin="2vp"/> <Image ohos:id="$+id:iv_state" ohos:height="24vp" ohos:width="24vp" ohos:align_parent_end="true" ohos:end_margin="16vp" ohos:background_element="$graphic:ic_right_24" ohos:vertical_center="true"/> </DependentLayout> <ListContainer ohos:id="$+id:lc_characteristics" ohos:height="match_content" ohos:width="match_parent" ohos:start_padding="16vp" ohos:visibility="hide"/> </DirectionalLayout>
整体上变化不大,只是加了一个ListContainer
,同时增加了一个Image
,用于显示当前的服务下的特性列表是否显示,可以通过当前的服务item的方式控制是否显示特性列表,这里用到两个图标,在graphic
下创建ic_right_24.xml
,代码如下所示:
<?xml version="1.0" encoding="UTF-8"?> <vector xmlns:ohos="http://schemas.android.com/apk/res/android" ohos:height="24vp" ohos:tint="#000000" ohos:viewportHeight="24" ohos:viewportWidth="24" ohos:width="24vp"> <path ohos:fillColor="#000000" ohos:pathData="M10,17l5,-5 -5,-5v10z"/> </vector>
还有一个ic_down_24.xml
<?xml version="1.0" encoding="utf-8"?> <vector xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="24vp" ohos:width="24vp" ohos:tint="#000000" ohos:viewportHeight="24" ohos:viewportWidth="24"> <path ohos:fillColor="#000000" ohos:pathData="M7,10l5,5 5,-5z"/> </vector>
下面修改一下ServiceAdapter,代码如下所示:
public class ServiceProvider extends BaseItemProvider { private final List<GattService> serviceList; private final AbilitySlice slice; private OperateCallback operateCallback; public void setOperateCallback(OperateCallback operateCallback) { this.operateCallback = operateCallback; } public ServiceProvider(List<GattService> list, AbilitySlice slice) { this.serviceList = list; this.slice = slice; } @Override public int getCount() { return serviceList == null ? 0 : serviceList.size(); } @Override public Object getItem(int position) { if (serviceList != null && position >= 0 && position < serviceList.size()) { return serviceList.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; ServiceHolder holder; GattService service = serviceList.get(position); if (component == null) { cpt = LayoutScatter.getInstance(slice).parse(ResourceTable.Layout_item_service, null, false); holder = new ServiceHolder(cpt); //将获取到的子组件信息绑定到列表项的实例中 cpt.setTag(holder); } else { cpt = component; // 从缓存中获取到列表项实例后,直接使用绑定的子组件信息进行数据填充。 holder = (ServiceHolder) cpt.getTag(); } holder.itemService.setClickedListener(component1 -> { boolean isShow = holder.lcCharacteristics.getVisibility() == Component.VISIBLE; //显示特性列表 holder.lcCharacteristics.setVisibility(isShow ? Component.HIDE : Component.VISIBLE); //更换图标 VectorElement vectorElement = new VectorElement(slice.getContext(), isShow ? ResourceTable.Graphic_ic_right_24 : ResourceTable.Graphic_ic_down_24); holder.ivState.setBackground(vectorElement); //刷新Item高度,这个很重要,不加会造成内容覆盖。 notifyDataSetItemChanged(position); }); //加载特性 设置属性回调 holder.lcCharacteristics.setItemProvider(new CharacteristicProvider(service.getCharacteristics(), slice, operateCallback)); holder.txServiceName.setText(BleUtils.getServiceName(service.getUuid())); holder.txUuid.setText(BleUtils.getShortUUID(service.getUuid())); return cpt; } /** * 用于保存列表项的子组件信息 */ public static class ServiceHolder { DependentLayout itemService; Text txServiceName; Text txUuid; Image ivState; ListContainer lcCharacteristics; public ServiceHolder(Component component) { itemService = (DependentLayout) component.findComponentById(ResourceTable.Id_item_service); txServiceName = (Text) component.findComponentById(ResourceTable.Id_tx_service_name); txUuid = (Text) component.findComponentById(ResourceTable.Id_tx_uuid); ivState = (Image) component.findComponentById(ResourceTable.Id_iv_state); lcCharacteristics = (ListContainer) component.findComponentById(ResourceTable.Id_lc_characteristics); } } }
和之前的区别就在于构造的时候增加了一个回调,并且在getComponent()
方法中就处理了服务Item的点击事件,而不是像之前一样回调到页面中,在服务Item的点击事件中判断是否显示特性列表同时修改图标资源。最后再将接口回调到页面中。
另外还需要注意一点,那就是Image我在xml中是设置的背景,而不是图片资源,因为在java代码中无法设置矢量图的资源,所以我就改成使用背景资源,需要注意的是背景资源要设置Image具体的大小,否则不会显示,在Java代码中通过VectorElement
来加载矢量图资源,然后设置背景。同时notifyDataSetItemChanged(position)
这样代码也很重要,因为我们的服务Item实际上有两部分内容,服务本身内容和特性列表内容,默认情况下显示服务内容,当点击服务Item时显示特性列表内容,此时如果你按照Android的习惯去搞,你就会发现,Item展开的内容会被其他Item遮挡,所以我们需要加上这样一行代码,让Item进行刷新,这样就不会被其他Item遮挡了。
最后我们在MainAbilitySlice中添加serviceProvider.setOperateCallback(this);
,然后再实现接口
重写里面的onPropertyOperate()
方法。这个方法在下一篇文章中会用到。
@Override public void onPropertyOperate(GattCharacteristic characteristic, String operateName) { }
运行一下看看效果。
六、源码
如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~
源码地址:HarmonyBle-Java