【共创季稿事节】HarmonyOS 7.0 尝鲜:轻触即定位,把分享变成一次直觉操作
HarmonyOS 7.0 尝鲜:轻触即定位,把分享变成一次直觉操作
刚参加完 HDC2026 回来,有个演示视频让我印象挺深:手机轻触电脑或平板屏幕,系统不止完成跨设备分享,还能识别目标窗口和触碰坐标,把手机里的图片、素材精准插到 PC 或平板的指定位置。
以前聊“碰一碰分享”,更多是说“把文件从 A 设备送到 B 设备”。但 HarmonyOS 7.0(26.0.0)开始,轻碰事件里带的坐标信息把这事儿往前推了一步:它不光知道“传给谁”,还知道“碰到了哪里”。这意味着应用可以把一次分享变成一个明确的业务动作——比如把图片插到文档光标附近、把素材投到画布坐标点、或者替换窗口里某个被碰区域的内容。
这篇文章我们就来体验一下碰一碰·精准分享:先跑通基础的碰一碰文件分享,再在 PC/平板接收侧读轻碰坐标,最后把手机分享过来的图片插到目标位置。
1. 从 HDC2026 的演示说起:碰一碰不只是传文件
HDC2026 现场那个“碰一碰精准分享”的演示,交互链路非常短,用户不需要经历传统文件流转里的“发送、接收、打开、拖拽、插入”,实现了我想把这张图放这儿,手机触碰对应位置就可以分享了。
从开发角度看,背后其实是两段能力拼起来的:
- 碰一碰文件分享:用 Share Kit 把手机端图片、视频、文件传到目标设备
- 轻碰坐标获取:在 PC、二合一设备或平板侧,从回调事件里拿到基于屏幕左上角的
screenX、screenY
let receivableTargetInfo = receivableTarget.getInfo();
let screenX = receivableTargetInfo.coordinate?.screenX;
let screenY = receivableTargetInfo.coordinate?.screenY;
拿到坐标之后,分享就不再是“扔进下载目录”。它可以进入编辑器、文档、画板、白板、看板这些更具体的业务位置。
2. 工程利旧:画板应用迭代升级思路

我们基于一个6.0版本的demo进行更新适配,最终完成我们的迭代体验。
entry/src/main/ets
├── controller
│ └── KnockController.ets # 碰一碰分享与接收核心控制器
├── entryability
│ └── EntryAbility.ets # 应用入口、页面加载、窗口断点更新
├── model
│ └── FileData.ets # 文件数据模型和示例资源
├── pages
│ └── Index.ets # 首页、文件初始化、选择列表、监听注册
└── utils
├── BreakpointSystem.ets # 响应式断点工具
└── FileUtil.ets # MIME 判断、视频缩略图生成
要按 HarmonyOS 7.0(26.0.0)来做精准坐标插图,得关注三个改造点。
第一,版本能力要对齐。原示例配置里可能还是 6.0.0(20),要用 26.0.0 的轻碰坐标能力,需要把 SDK、DevEco Studio 和设备系统能力都切到对应版本。
第二,要支持平板,需要在模块设备类型里补上 tablet:
"deviceTypes": [
"phone",
"2in1",
"tablet"
]
第三,业务层要从“接收后展示文件列表”升级成“接收后按坐标插入”。这就需要在接收端维护一个目标区域,比如文档编辑区、画布区域,或者一组可插入的容器。
我这次实操的最小闭环是:
手机端选图
-> 轻碰 PC/平板屏幕
-> Share Kit 传图
-> PC/平板 dataReceive 回调
-> 读 screenX/screenY
-> 文件落盘到沙箱
-> 把屏幕坐标换算成窗口/画布坐标
-> 在指定位置插图片
3. 跑通基础链路:文件从手机分享到 PC/平板

“精准插入”的前提,是文件的可靠传输。
3.1 先把资源变成可分享的 URI
在 rawfile 或 media 资源里的图片和视频,不能直接拿来跨设备分享。页面启动时,Index.ets 会先执行 handleFileInit(),把资源复制到应用沙箱目录,再用 fileUri.getUriFromPath() 转成 Share Kit 能用的 URI。
核心代码是这段:
let url = this.context.filesDir + '/' + item.url;
file = fileIo.openSync(url, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
fileIo.writeSync(file.fd, (this.context.resourceManager.getRawFileContentSync(item.url) as Uint8Array).buffer);
item.url = fileUri.getUriFromPath(url);
缩略图也一样:
let url = this.context.filesDir + '/' + item.thumbnail;
file = fileIo.openSync(url, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
fileIo.writeSync(file.fd, (this.context.resourceManager.getMediaContentSync(IMAGE_DATA[item.index].id) as Uint8Array).buffer);
item.thumbnail = fileUri.getUriFromPath(url);
有个容易踩的坑:分享文件时别把工程资源名直接塞给 SharedRecord.uri。Share Kit 需要的是接收端能处理的文件 URI,所以示例先把资源写入沙箱,再转 fileUri。
3.2 页面勾选文件,AppStorage 存分享状态
页面把文件列表和已选文件索引放在 AppStorage 里:
@StorageLink('KnockFileShare_videoDataList') dataList: FileData[] = VIDEO_SOURCES;
@StorageLink('KnockFileShare_fileShare') fileShare: number[] = [];
勾选时只存文件下标:
Checkbox({ name: index + '', group: 'checkboxGroup' })
.onChange((value: boolean) => {
if (value) {
this.fileShare.push(index);
} else {
this.fileShare.splice(this.fileShare.indexOf(index), 1);
}
})
这么做的优势是,页面只负责 UI 状态,真正分享时由 KnockController 统一读 fileShare 和 videoDataList。这个分层很适合继续扩展——后面要加坐标,只要在控制器回调里加就行。
3.3 注册 knockShare 监听
普通设备可以直接注册 knockShare:
public immersiveListening() {
if (canIUse('SystemCapability.Collaboration.HarmonyShare')) {
harmonyShare.on('knockShare', (target: harmonyShare.SharableTarget) => {
this.immersiveCallback(target);
});
}
}
PC/二合一侧建议绑定 windowId,让系统知道当前窗口参与轻碰:
window.getLastWindow(this.context).then((data) => {
let mainWindowID: number = data.getWindowProperties().id;
harmonyShare.on('knockShare', { windowId: mainWindowID }, (target: harmonyShare.SharableTarget) => {
this.immersiveCallback(target);
});
});
3.4 组装 SharedData 并分享
轻碰触发后,发送端从 AppStorage 里拿到已选文件,逐个转成 SharedRecord,再组装成 SharedData:
public immersiveCallback(target: harmonyShare.SharableTarget) {
let fileShare = AppStorage.get('KnockFileShare_fileShare') as number[];
let videoDataList = AppStorage.get('KnockFileShare_videoDataList') as FileData[];
if (!fileShare || fileShare.length === 0) {
return;
}
let shareData: systemShare.SharedData =
new systemShare.SharedData(this.getShareRecord(videoDataList[fileShare[0]]));
for (let i = 1; i < fileShare.length; i++) {
shareData.addRecord(this.getShareRecord(videoDataList[fileShare[i]]));
}
target.share(shareData);
}
每个文件记录这样生成:
getShareRecord(data: FileData): systemShare.SharedRecord {
let suffix = '.' + data.url.split('.').pop();
let utd = uniformTypeDescriptor.getUniformDataTypeByFilenameExtension(suffix);
return {
utd: utd,
uri: data.url,
thumbnailUri: data.thumbnail,
title: data.name,
description: data.description
};
}
这一步有两个重点:
uri是真实文件 URIutd建议通过扩展名推导
做到这儿,碰一碰文件分享的发送端就跑通了。
4. 接收端落盘:dataReceive 是精准插图的入口

PC/二合一或平板要实现“收到文件并插到指定位置”,入口是 harmonyShare.on('dataReceive')。
接收监听是这样注册的:
harmonyShare.on('dataReceive', {
windowId: mainWindowID,
capabilities: [
{
'utd': uniformTypeDescriptor.UniformDataType.MEDIA,
'maxSupportedCount': 5
},
{
'utd': uniformTypeDescriptor.UniformDataType.FILE,
'maxSupportedCount': 5
}
]
}, (receiveTarget: harmonyShare.ReceivableTarget) => {
receiveTarget.receive(fileUri.getUriFromPath(this.context.filesDir), {
onDataReceived: (shareData: systemShare.SharedData) => {
// 处理收到的数据
},
onResult(resultCode: harmonyShare.ShareResultCode) {
// 处理接收结果
}
});
});
capabilities 表示接收端支持什么类型、最多收几个。比如这里只声明支持媒体和普通文件,最多 5 个。如果只做“手机传一张图到 PC 指定位置”,可以收窄成图片能力:
capabilities: [
{
utd: uniformTypeDescriptor.UniformDataType.IMAGE,
maxSupportedCount: 1
}
]
接收后,通过 shareData.getRecords() 拿到文件记录。原示例会按类型处理缩略图,然后追加到页面列表:
let shareRecords = shareData.getRecords();
let videoDataList = AppStorage.get('KnockFileShare_videoDataList') as FileData[];
shareRecords.forEach(async (record: systemShare.SharedRecord) => {
if (!record.uri) {
return;
}
let fileName = record.uri.split('/').pop()?.split('.')[0];
let thumbPath: string = videoDataList[0].thumbnail;
if (record.uri.endsWith('mp4') || record.uri.endsWith('mkv')) {
thumbPath = record.uri.slice(0, record.uri.lastIndexOf('.')) + 'thumb.png';
let result = await new FileUtil().getVideoThumbnail(record.uri, thumbPath);
if (!result) {
thumbPath = videoDataList[0].thumbnail;
}
} else if (record.uri.endsWith('png') || record.uri.endsWith('jpg') || record.uri.endsWith('jpeg')) {
thumbPath = record.uri;
}
videoDataList.push({
url: record.uri,
name: fileName,
description: record.description,
thumbnail: thumbPath,
index: videoDataList.length
});
});
如果目标是“文档指定位置插图”,这段逻辑可以拆成两步:
- 文件落盘和类型处理照常保留
- 不再只是
videoDataList.push(),而是调用insertImageAt(uri, x, y)
也就是说,dataReceive 是文件进应用的入口,coordinate 是业务落位的入口。两者合在一起,才是精准分享。
5. 精准定位:从 screenX/screenY 到指定位置插图
现在进入 26.0.0 的重点。
从 26.0.0 版本开始,手机与 PC/二合一、手机与平板触发轻碰事件时,在 PC/二合一或平板侧可以从回调事件里获取轻碰位置。坐标以屏幕左上角为原点:
let receivableTargetInfo = receivableTarget.getInfo();
let screenX = receivableTargetInfo.coordinate?.screenX;
let screenY = receivableTargetInfo.coordinate?.screenY;
发送侧也能从 SharableTarget 获取信息:
let sharableTargetInfo = sharableTarget.getInfo();
let screenX = sharableTargetInfo.coordinate?.screenX;
let screenY = sharableTargetInfo.coordinate?.screenY;
但在“手机到 PC 指定位置插图”这个场景里,我更关心接收侧坐标,因为最终插入动作发生在 PC/平板侧。
5.1 在 dataReceive 回调里先缓存落点
可以把接收回调改成这样:
private dataReceiveCallback = (receivableTarget: harmonyShare.ReceivableTarget) => {
let targetInfo = receivableTarget.getInfo();
let screenX = targetInfo.coordinate?.screenX;
let screenY = targetInfo.coordinate?.screenY;
if (screenX === undefined || screenY === undefined) {
// 兜底策略:插到当前光标、画布中心或最近一次触点
screenX = this.lastInsertX;
screenY = this.lastInsertY;
}
receivableTarget.receive(this.sandboxUri, {
onDataReceived: (sharedData: systemShare.SharedData) => {
this.handleReceivedData(sharedData, screenX, screenY);
},
onResult: (resultCode: harmonyShare.ShareResultCode) => {
if (resultCode === harmonyShare.ShareResultCode.SHARE_SUCCESS) {
// 接收成功,刷新 UI 或上报埋点
}
}
});
}
这里把坐标和 sharedData 绑在同一个处理函数里,避免后面异步处理时丢掉落点。
5.2 屏幕坐标不能直接当组件坐标用
screenX/screenY 是屏幕坐标,图片要插入的是窗口内部的某个区域。中间至少要做一次坐标转换:
screenX/screenY
-> 减去窗口左上角 windowLeft/windowTop
-> 得到窗口内坐标 windowX/windowY
-> 减去编辑区左上角 editorLeft/editorTop
-> 得到编辑区坐标 editorX/editorY
伪代码可以这样写:
interface Point {
x: number;
y: number;
}
interface Rect {
left: number;
top: number;
width: number;
height: number;
}
function toEditorPoint(screenX: number, screenY: number, windowRect: Rect, editorRect: Rect): Point {
const windowX = screenX - windowRect.left;
const windowY = screenY - windowRect.top;
return {
x: windowX - editorRect.left,
y: windowY - editorRect.top
};
}
换算完还要做边界判断:
function isInRect(point: Point, rect: Rect): boolean {
return point.x >= 0 &&
point.y >= 0 &&
point.x <= rect.width &&
point.y <= rect.height;
}
这一步决定业务体验。坐标落在编辑区,就插图片;落在工具栏,就弹导入菜单;落在侧边栏,就加到素材库。
5.3 把收到的图片插到指定位置
收到文件后,先取记录:
private handleReceivedData(sharedData: systemShare.SharedData, screenX: number, screenY: number) {
let records = sharedData.getRecords();
records.forEach((record: systemShare.SharedRecord) => {
if (!record.uri) {
return;
}
this.insertImageFromKnock(record.uri, screenX, screenY);
});
}
然后做坐标转换并插入:
private insertImageFromKnock(uri: string, screenX: number, screenY: number) {
const editorPoint = toEditorPoint(screenX, screenY, this.windowRect, this.editorRect);
if (!isInRect(editorPoint, this.editorRect)) {
this.addToMaterialPanel(uri);
return;
}
this.images.push({
uri,
x: editorPoint.x,
y: editorPoint.y,
width: 160,
height: 120
});
}
如果是 ArkUI 页面,最简演示可以用 Stack 承载图片,通过 position() 或相对布局放到指定位置:
Stack() {
ForEach(this.images, (item: InsertedImage) => {
Image(item.uri)
.width(item.width)
.height(item.height)
.position({ x: item.x, y: item.y })
})
}
.width('100%')
.height('100%')
这样,一个“手机轻碰 PC 指定位置插图”的最小闭环就跑通了。
5.4 整理核心调用链
把它放回当前工程,核心调用链可以整理成:
Index.aboutToAppear()
-> handleFileInit()
-> KnockController.getInstance(context)
-> 二合一/平板注册 knockShare + dataReceive
手机端勾选图片
-> fileShare 存已选 index
-> 轻碰触发 knockShare
-> SharedData 带图片 URI
-> target.share(shareData)
PC/平板侧接收
-> dataReceive 回调
-> receivableTarget.getInfo()
-> 读 coordinate.screenX / screenY
-> receive(sandboxUri)
-> onDataReceived(sharedData)
-> getRecords()
-> 坐标转换
-> insertImageFromKnock(uri, x, y)
这条链路比单纯文件分享多了三步:读坐标、转坐标、执行业务插入。
在实操的过程中,需要注意:
第一,坐标可能为空。
this.dataReceiveScreenX = receivableTargetInfo.coordinate?.screenX;
this.dataReceiveScreenY = receivableTargetInfo.coordinate?.screenY;
真实业务不能假设它永远有值。比较稳的兜底是:没有坐标时插当前光标位置、编辑区中心,或者回退成普通文件接收。
第二,坐标是屏幕坐标。 屏幕坐标要经过窗口偏移、组件偏移、滚动偏移,才能变成业务坐标。文档编辑器尤其要留意滚动位置。
第三,能力声明要收敛。 接收端如果只做图片插入,就不要上来就声明支持所有文件:
capabilities: [
{
utd: uniformTypeDescriptor.UniformDataType.IMAGE,
maxSupportedCount: 1
}
]
这样用户体验更明确,异常处理也简单。
总结
最后总结一下这次实操:
碰一碰精准分享不是一个单点 API,而是一套新的跨设备交互方式。它的基础是文件分享,核心是目标识别和坐标,最终价值落在业务动作上。
如果说传统分享解决的是“内容怎么跨设备传输”,那 HarmonyOS 7.0 的轻碰坐标能力解决的就是“内容应该如何场景化传递”。这也是我看完 HDC2026 演示后,第一时间想把它跑通的原因。
更多推荐



所有评论(0)