表盘 (TSDial)
表盘模块提供完整的外设表盘管理功能,包括表盘信息获取、推送、删除、切换以及变化监听。支持内置表盘、云端表盘和自定义表盘的全生命周期管理。
前提条件
- 设备已连接并处于可用状态
- 获取
TSPeripheralDialInterface协议实例 - 对于自定义表盘操作,需要获取设备屏幕信息(
TSPeripheralScreen)以确保图片尺寸正确
数据模型
TSDialModel(表盘信息模型)
| 属性名 | 类型 | 说明 |
|---|---|---|
dialId | NSString * | 表盘唯一标识符 |
dialName | NSString * | 表盘显示名称 |
dialType | TSDialType | 表盘类型(内置/自定义/云端) |
isCurrent | BOOL | 是否为当前表盘 |
locationIndex | UInt8 | 表盘在设备上的位置索引 |
version | NSString * | 表盘版本号 |
TSDialTime(时间配置)
| 属性 | 类型 | 说明 |
|---|---|---|
timeImage | UIImage * | 时间样式图片,优先于路径 |
timeImagePath | NSString * | 本地或 Bundle 图片路径 |
timePosition | TSDialTimePosition | timeRect 未指定时的备用位置 |
timeRect | CGRect | 背景图像素坐标中的时间区域 |
timeColor | UIColor * | 单色时间图片的可选着色 |
style | TSDialTimeStyle | 内置时间样式标识 |
TSDialDraft 与 TSDialDraftItem
TSDialDraft 通过 initWithDraftType:templateFilePath:items: 创建。draftType、templateFilePath 和 items 为只读;dialId、previewImage 和 multiplePlayIntervalMillis 为可选输入。资源项使用 itemWithImage:time: 或 itemWithVideoFilePath:time: 创建,图片尺寸必须匹配设备屏幕像素尺寸。TSDialTime 为不可变对象,须使用指定初始化方法创建。
TSDialDanMuItem
通过 initWithImage: 创建滚动图片。仅 TSDialDraftTypeDanMu 使用,背景资源项的 danMuItems 至少包含一项。静态图片必填;animationFilePath 可选,指向本地可读 GIF。walkSpeed 为有限正数,单位像素/秒,默认 60;leftToRight 默认 NO。
imageX、imageY、animationX、animationY 使用包含 anchor 和 offset 的 TSDialDanMuCoordinate,通过 TSDialDanMuCoordinateMake(anchor, offset) 创建。偏移量为 -524288…524287 的有符号像素值;绝对坐标直接使用偏移量,其他锚点先按内容原始尺寸对齐,再加偏移量。
TSDialDanMuAnchor | 值 | 含义 |
|---|---|---|
TSDialDanMuAnchorAbsolute | 0 | 左上角绝对坐标 |
TSDialDanMuAnchorStart | 1 | 横轴左对齐、纵轴顶对齐 |
TSDialDanMuAnchorCenter | 2 | 当前轴居中 |
TSDialDanMuAnchorEnd | 3 | 横轴右对齐、纵轴底对齐 |
当前统一表盘模型
| 模型 | 关键属性 | 说明 |
|---|---|---|
TSDialCapability | supportsCustom, supportsVideo, maxVideoDuration, supportsSlideshow, supportsDanMu, maxSlideshowImages, colorTintable, supportsComponent, shouldRenderTimeInPreview, maxInstallCount, maxInnerCount, screenWidth, screenHeight, deviceCornerRadius, previewWidth, previewHeight, previewCornerRadius, shape | 只读的当前设备静态能力快照 |
TSStorageSpace | available, total | 表盘存储空间,单位字节;total == 0 表示未知 |
TSDialDraft | dialId, draftType, templateFilePath, items, previewImage, multiplePlayIntervalMillis | 自定义表盘构建输入 |
TSDialDraftItem | itemType, image, videoFilePath, time, danMuItems | 单张图片或视频素材 |
TSDialDanMuItem | image, imageX, imageY, walkSpeed, leftToRight, animationFilePath, animationX, animationY | 一条静态图片弹幕及可选关联 GIF |
TSDialArtifact | dialId, dialType, filePath | 唯一安装输入,可由自定义造包生成,或使用已下载的云端包构造 |
TSComposePreviewInput | backgroundImage, time, maxKBSize, keepTransparentBackground | 预览合成输入 |
TSCustomDialStyleOption | style, previewImageURL, size | 可用时间样式 |
TSCustomDialPositionOption | position, frame | 可用时间位置 |
TSCustomDialStyleConstraint | screenSize, screenShape, styles, positions, allowColorTint | Provider 无关的自定义样式约束 |
构建与预览规则
构建前先查询 dialCapability;设备缓存信息不可用时可能返回 nil。maxInstallCount == -1 表示无限制,0 表示不支持安装云端/自定义表盘。maxSlideshowImages == 0 表示无明确图片数量上限。选择草稿类型前应检查相应能力标志。
单图、视频和弹幕草稿必须只有一个资源项,多图草稿需要一个或多个图片项。除弹幕外均需提供时间配置。模板路径为 nil 时由 Provider 解析兼容模板。multiplePlayIntervalMillis 非正数使用 Provider 默认值,正数在构建时量化;视频和 GIF 源文件在构建完成前须保持可读且不变。
shouldRenderTimeInPreview 表示 Provider 合成安装预览图时是否叠加时间,与 supportsComponent 独立;仍需时间配置,不控制手表实际时间显示,也不修改 App 提供的成品预览图。自行设置 draft.previewImage 时,其像素尺寸必须等于 TSPeripheralScreen.dialPreviewSize。
fetchDialStorage: 仅返回表盘区域空间;total 未知时仍可使用 available。TSStorageSpace 提供 used、usagePercentage、hasEnoughSpaceForSize: 和 formattedStorageInfo。总量未知时,已用空间和使用率返回 0,不代表实际使用情况。
枚举与常量
TSDialType(表盘类型)
| 值 | 说明 |
|---|---|
eTSDialTypeBuiltIn (0) | 设备自带的内置表盘 |
eTSDialTypeCustomer (1) | 用户创建的自定义表盘 |
eTSDialTypeCloud (2) | 从云服务器下载的表盘 |
TSDialDraftType(表盘草稿类型)
| 值 | 说明 |
|---|---|
TSDialDraftTypeSingleImage (1) | 单图片自定义表盘 |
TSDialDraftTypeMultipleImage (2) | 多图片自定义表盘 |
TSDialDraftTypeVideo (3) | 视频自定义表盘 |
TSDialDraftTypeDanMu (4) | 弹幕图片表盘 |
TSDialTimePosition(时间显示位置)
| 值 | 说明 |
|---|---|
eTSDialTimePositionTop (0) | 上方 |
eTSDialTimePositionBottom (1) | 下方 |
eTSDialTimePositionLeft (2) | 左方 |
eTSDialTimePositionRight (3) | 右方 |
eTSDialTimePositionTopLeft (4) | 左上 |
eTSDialTimePositionBottomLeft (5) | 左下 |
eTSDialTimePositionTopRight (6) | 右上 |
eTSDialTimePositionBottomRight (7) | 右下 |
eTSDialTimePositionCenter (8) | 中间 |
TSDialTimeStyle(时间显示样式)
| 值 | 说明 |
|---|---|
eTSDialTimeStyleNone (0) | 无样式 |
eTSDialTimeStyle1 (1) | 时间样式1 |
eTSDialTimeStyle2 (2) | 时间样式2 |
eTSDialTimeStyle3 (3) | 时间样式3 |
eTSDialTimeStyle4 (4) | 时间样式4 |
eTSDialTimeStyle5 (5) | 时间样式5 |
eTSDialTimeStyle6 (6) | 时间样式6 |
eTSDialTimeStyle7 (7) | 时间样式7 |
TSDialInstallResult(表盘安装结果)
| 值 | 说明 |
|---|---|
eTSDialInstallResultStart (0) | 开始 |
eTSDialInstallResultProgress (1) | 安装中 |
eTSDialInstallResultSuccess (2) | 安装成功 |
eTSDialInstallResultFailed (3) | 安装失败 |
eTSDialInstallResultCompleted (4) | 安装流程结束 |
构建类型与错误
TSDialDraftType:TSDialDraftTypeSingleImage、TSDialDraftTypeMultipleImage、TSDialDraftTypeVideo、TSDialDraftTypeDanMu。TSDialDraftItemType:TSDialDraftItemTypeImage、TSDialDraftItemTypeVideo。TSDialErrorCode:TSDialErrorInvalidDraftType、TSDialErrorMissingTemplateFilePath、TSDialErrorTemplateFileNotFound、TSDialErrorEmptyDraftItems、TSDialErrorInvalidDraftItemCount、TSDialErrorInvalidDraftItemType、TSDialErrorMissingDraftItemTime、TSDialErrorMissingImageResource、TSDialErrorMissingVideoFilePath、TSDialErrorVideoFileNotFound、TSDialErrorInvalidScreenSize、TSDialErrorImageSizeMismatch、TSDialErrorMissingArtifactDialId、TSDialErrorMissingArtifactFilePath、TSDialErrorArtifactFileNotFound、TSDialErrorMissingPreviewBackgroundImage、TSDialErrorInvalidArtifactDialType,TSDialErrorInvalidDanMuItem。
回调类型
| 回调类型 | 说明 |
|---|---|
TSDialInstallCompletionBlock | 必传的安装最终结果与错误信息;须根据 result 判断结果 |
TSDialInstallProgressBlock | 安装阶段与 0–100 进度 |
TSDialListBlock | 表盘列表与错误信息 |
TSDialWidgetsBlock | Fw 系列设备支持的挂件字典与错误信息 |
TSCustomDialStyleConstraintBlock | 自定义表盘样式约束与错误信息 |
当前接口方法
能力、样式与查询
- (nullable TSDialCapability *)dialCapability;
- (BOOL)isSupportCustomDialStyleConstraint;
- (void)fetchCustomDialStyleConstraint:(TSCustomDialStyleConstraintBlock)completion;
- (void)fetchAllDials:(TSDialListBlock)completion;
- (void)fetchCurrentDial:(void (^)(TSDialModel *_Nullable dial, NSError *_Nullable error))completion;
- (void)fetchDialStorage:(void (^)(TSStorageSpace *_Nullable storage, NSError *_Nullable error))completion;
能力快照来自本地设备信息;样式约束和列表/当前表盘/存储通过异步回调返回。
选择、构建与预览
- (void)selectDial:(NSString *)dialId completion:(TSCompletionBlock)completion;
- (void)buildDialWithDraft:(TSDialDraft *)draft
completion:(void (^)(TSDialArtifact *_Nullable artifact, NSError *_Nullable error))completion;
- (void)composeDialPreview:(TSComposePreviewInput *)input
completion:(void (^)(UIImage *_Nullable previewImage, NSError *_Nullable error))completion;
App 负责素材裁剪、视频截取与屏幕尺寸适配;构建成功后把 TSDialArtifact 交给安装接口。
安装、卸载与事件
- (void)installDial:(TSDialArtifact *)artifact
progressBlock:(nullable TSDialInstallProgressBlock)progressBlock
completion:(TSDialInstallCompletionBlock)completion;
- (void)cancelDialInstall:(TSCompletionBlock)completion;
- (void)uninstallDial:(NSString *)dialId completion:(TSCompletionBlock)completion;
- (void)registerDialListDidChangeHandler:(void (^)(NSArray<TSDialModel *> *_Nullable allDials,
NSError *_Nullable error))handler;
- (void)fetchSupportedDialWidgets:(TSDialWidgetsBlock)completion;
安装进度范围为 0–100;内置表盘不能卸载;列表监听仅保留最后一次注册。
现有接口示例
以下片段在 SDK 初始化并连接设备后,按操作分别放入 App 方法中执行。资源名、路径和 ID 需替换为实际值。先获取接口实例:
id<TSPeripheralDialInterface> dialInterface = [TopStepComKit sharedInstance].dial;
if (dialInterface == nil) {
TSLog(@"[Dial] 表盘接口不可用");
return;
}
查询设备能力与表盘列表
TSDialCapability *capability = [dialInterface dialCapability];
if (capability == nil) {
TSLog(@"[Dial] 设备能力信息尚未就绪");
return;
}
TSLog(@"支持自定义:%@,视频:%@,最大视频时长:%ld秒",
capability.supportsCustom ? @"是" : @"否",
capability.supportsVideo ? @"是" : @"否",
(long)capability.maxVideoDuration);
[dialInterface fetchAllDials:^(NSArray<TSDialModel *> *dials, NSError *error) {
if (error) {
TSLog(@"[Dial] 获取表盘列表失败: %@", error.localizedDescription);
return;
}
for (TSDialModel *dial in dials) {
TSLog(@"%@ %@", dial.dialName, dial.isCurrent ? @"(当前)" : @"");
}
}];
[dialInterface fetchCurrentDial:^(TSDialModel *dial, NSError *error) {
if (!error && dial) {
TSLog(@"[Dial] 当前表盘: %@", dial.dialName);
}
}];
查询样式约束和存储空间
if ([dialInterface isSupportCustomDialStyleConstraint]) {
[dialInterface fetchCustomDialStyleConstraint:^(TSCustomDialStyleConstraint *constraint,
NSError *error) {
if (!error && constraint) {
TSLog(@"[Dial] 可用时间样式: %lu", (unsigned long)constraint.styles.count);
}
}];
}
[dialInterface fetchDialStorage:^(TSStorageSpace *storage, NSError *error) {
if (error || !storage) {
TSLog(@"[Dial] 查询表盘空间失败: %@", error.localizedDescription);
return;
}
TSLog(@"[Dial] 可用空间: %llu 字节", storage.available);
if (storage.total > 0) {
TSLog(@"[Dial] 总空间: %llu 字节", storage.total);
}
}];
构建并预览单图表盘
UIImage *image = [UIImage imageNamed:@"dial_background"];
if (!image || ![dialInterface dialCapability].supportsCustom) {
TSLog(@"[Dial] 请提供屏幕尺寸的图片,并确认设备支持自定义表盘");
return;
}
TSDialTime *time = [[TSDialTime alloc] initWithTimeImage:[UIImage imageNamed:@"time_style"]
timeImagePath:nil
timePosition:eTSDialTimePositionTop
timeRect:CGRectZero
timeColor:nil
style:eTSDialTimeStyle1];
TSDialDraftItem *item = [TSDialDraftItem itemWithImage:image time:time];
TSDialDraft *draft = [[TSDialDraft alloc] initWithDraftType:TSDialDraftTypeSingleImage
templateFilePath:nil
items:@[item]];
[dialInterface composeDialPreview:[[TSComposePreviewInput alloc]
initWithBackgroundImage:image time:time maxKBSize:300 keepTransparentBackground:NO]
completion:^(UIImage *preview, NSError *error) {
if (!error && preview) {
TSLog(@"[Dial] 预览图已生成: %@", preview);
}
}];
[dialInterface buildDialWithDraft:draft completion:^(TSDialArtifact *artifact, NSError *error) {
if (error || !artifact) {
TSLog(@"[Dial] 造包失败: %@", error.localizedDescription);
return;
}
[dialInterface installDial:artifact progressBlock:^(TSDialInstallResult result, NSInteger progress) {
TSLog(@"安装进度: %ld%%", (long)progress);
} completion:^(TSDialInstallResult result, NSError *error) {
TSLog(@"[Dial] 安装结果: %ld,错误: %@", (long)result, error.localizedDescription);
}];
}];
构建视频表盘
复用单图示例中的 time 配置。视频需匹配设备屏幕尺寸,时长不超过 maxVideoDuration,构建完成前保持文件可读。
if (![dialInterface dialCapability].supportsVideo) {
TSLog(@"[Dial] 设备不支持视频表盘");
return;
}
TSDialDraftItem *item = [TSDialDraftItem itemWithVideoFilePath:@"/path/to/watchface.mp4"
time:time];
TSDialDraft *draft = [[TSDialDraft alloc] initWithDraftType:TSDialDraftTypeVideo
templateFilePath:nil
items:@[item]];
[dialInterface buildDialWithDraft:draft completion:^(TSDialArtifact *artifact, NSError *error) {
if (error || !artifact) {
TSLog(@"[Dial] 造包失败: %@", error.localizedDescription);
return;
}
[dialInterface installDial:artifact progressBlock:nil completion:^(TSDialInstallResult result, NSError *installError) {
TSLog(@"[Dial] 视频安装结果: %ld,错误: %@", (long)result, installError.localizedDescription);
}];
}];
切换、取消和卸载表盘
下列为独立操作,不应在发起安装后连续执行。切换 ID 来自 fetchAllDials:,仅云端或自定义表盘允许卸载。
[dialInterface selectDial:@"dial-id" completion:^(BOOL success, NSError *error) {
TSLog(@"切换结果: %@", success ? @"成功" : error.localizedDescription);
}];
[dialInterface cancelDialInstall:^(BOOL success, NSError *error) {
TSLog(@"取消安装: %@", success ? @"成功" : error.localizedDescription);
}];
[dialInterface uninstallDial:@"custom-dial-id" completion:^(BOOL success, NSError *error) {
TSLog(@"卸载结果: %@", success ? @"成功" : error.localizedDescription);
}];
监听表盘列表变化与获取挂件
[dialInterface registerDialListDidChangeHandler:^(NSArray<TSDialModel *> *dials, NSError *error) {
if (!error && dials) {
TSLog(@"[Dial] 表盘列表已更新,共 %lu 个", (unsigned long)dials.count);
}
}];
[dialInterface fetchSupportedDialWidgets:^(NSDictionary *widgets, NSError *error) {
if (!error && widgets) {
TSLog(@"[Dial] 支持的挂件: %@", widgets);
}
}];
注意事项
TSDialDraft、TSDialDraftItem和TSDialTime是当前自定义表盘构建入口;图片像素尺寸必须匹配设备屏幕尺寸。TSStorageSpace的available和total均以字节计;安装前检查available,total == 0表示未知。- 单图、视频和弹幕草稿需要一个资源项,多图草稿可包含多个图片项;弹幕草稿还必须提供非空
danMuItems。 - 安装回调中的进度范围为 0–100;安装完成后可在回调中刷新表盘列表。
- 内置表盘不能卸载;
registerDialListDidChangeHandler:传入新回调会替换之前的监听,事件不区分安装、卸载或切换来源。