关于 uni-push 小米厂商推送“个人订阅”迁移至“公信消息”的适配通知
关于push,近期小米推送将上线订阅消息功能,原私信通道下的个人订阅类目将统一调整至公信消息。仅接入小米个人订阅消息的 App 开发者需关注,未使用无需处理。
1、订阅类消息仅支持使用 8 个专属订阅类目,规则详见小米推送2026年消息分类新规:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2321
2、2026年8月1日起原有私信渠道下的【个人订阅】通道将关闭申请入口,于2026年12月31日正式关停使用。
3、2026年8月1日之前已申请接入过【个人订阅】分类的应用,需要在2026年12月31日前完成订阅消息功能接入和订阅授权弹窗适配。具体接入流程请参考:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2320
4、2026年8月1日之后新申请订阅分类的需要按照新规接入发送
对接个推侧需要进行以下适配:
1、服务端:接入订阅类消息后推送提醒方式与私信消息相同,新订阅消息的channel_id与原有【个人订阅】分类的channel_id不同,在调用个推服务推送API下发订阅类消息时,必须同时设置小米消息分类和私信模板相关参数,接口设置参照文档【小米】私信模板id:https://docs.getui.com/getui/server/rest_v2/third_party/#doc-title-15
2、客户端:
- Android小米厂商SDK 需要升级到 com.getui.opt:xmp:3.3.7 版本
- 客户端需完成订阅消息功能接入,同步适配订阅授权弹窗,统一采用系统原生 API 调用实现,完整接入规范可参考小米推送订阅消息官方文档:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2320
以上请尽快完成适配,根据应用业务场景,发送不同类别的消息。如有疑问,可以添加微信客服群进行咨询,加群方式参考文档:https://uniapp.dcloud.net.cn/unipush-v2.html#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98
关于push,近期小米推送将上线订阅消息功能,原私信通道下的个人订阅类目将统一调整至公信消息。仅接入小米个人订阅消息的 App 开发者需关注,未使用无需处理。
1、订阅类消息仅支持使用 8 个专属订阅类目,规则详见小米推送2026年消息分类新规:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2321
2、2026年8月1日起原有私信渠道下的【个人订阅】通道将关闭申请入口,于2026年12月31日正式关停使用。
3、2026年8月1日之前已申请接入过【个人订阅】分类的应用,需要在2026年12月31日前完成订阅消息功能接入和订阅授权弹窗适配。具体接入流程请参考:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2320
4、2026年8月1日之后新申请订阅分类的需要按照新规接入发送
对接个推侧需要进行以下适配:
1、服务端:接入订阅类消息后推送提醒方式与私信消息相同,新订阅消息的channel_id与原有【个人订阅】分类的channel_id不同,在调用个推服务推送API下发订阅类消息时,必须同时设置小米消息分类和私信模板相关参数,接口设置参照文档【小米】私信模板id:https://docs.getui.com/getui/server/rest_v2/third_party/#doc-title-15
2、客户端:
- Android小米厂商SDK 需要升级到 com.getui.opt:xmp:3.3.7 版本
- 客户端需完成订阅消息功能接入,同步适配订阅授权弹窗,统一采用系统原生 API 调用实现,完整接入规范可参考小米推送订阅消息官方文档:https://dev.mi.com/xiaomihyperos/documentation/detail?pId=2320
以上请尽快完成适配,根据应用业务场景,发送不同类别的消息。如有疑问,可以添加微信客服群进行咨询,加群方式参考文档:https://uniapp.dcloud.net.cn/unipush-v2.html#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98
收起阅读 »怎么用 BeeWare(Briefcase)把 Python 应用打包成 iOS App 并上架
'''BeeWare 走的是和 Flet 不一样的路子:它的 Toga 用的是各平台的原生控件,iOS 上就是真的 UIKit,不套 Flutter 或者 WebView。打包工具叫 Briefcase,briefcase create iOS 会生成一个 Xcode 工程,briefcase build iOS 编译,briefcase package iOS 出包。
因为最终还是 Xcode 在干活,这几条命令都得在 macOS 上跑。Windows 上 Briefcase 只能处理 Windows/Android 那些目标。这一步没有变通。
我的分工是:Python 代码在 Windows 上写,iOS 出包丢给 macOS 机器;证书、描述文件、IPA 上传用开心上架(AppUploader)在 Windows 上处理。
先说结论:哪一步真绕不开 Mac
| 环节 | 我实际怎么做的 |
|---|---|
| 注册 Bundle ID、签发证书、生成描述文件 | Windows 上做完 |
| 写 Python 代码 | Windows |
briefcase create / build / package iOS |
要 macOS + Xcode |
| 提交 App Store | Windows 上传 |
| 装到 iPhone 测试 | 扫码注册 UDID,再扫码装 |
说明一下:我用的签证书工具本身在 macOS 上也能用(网页版不挑系统,桌面客户端有 macOS 版)。表里"不需要 Mac"是指这一步不再需要 Xcode、钥匙串访问或 Transporter。
第一步:在 pyproject.toml 里定好 bundle
Briefcase 的配置全在 pyproject.toml 里。iOS 的包标识由 bundle 加 app 名拼出来:
[tool.briefcase]
project_name = "My App"
bundle = "com.yourcompany"
version = "1.0.0"
[tool.briefcase.app.myapp]
formal_name = "My App"
这样最终的 Bundle Identifier 就是 com.yourcompany.myapp。拼出来的这个值要和 Apple 注册的 Bundle ID、描述文件绑定的 App ID 完全一致——注意是拼出来的结果,不是 bundle 字段本身,这点第一次配的时候容易搞错。
第二步:在 Windows 上签出证书和描述文件
注册 Bundle ID:开心上架的「Bundle ID」页面点「新建 Bundle ID」,Identifier 填拼出来的那个完整标识。创建后改不了,所以先把 bundle 和 app 名定下来再注册。
签发证书:「证书」页面点「新建证书」,上架选发布签名证书,真机调试选开发签名证书。绑定上面的 Bundle ID,填名称邮箱,设一个 .p12 保护密码——导入 Mac 钥匙串时要输,服务器不保存它,忘了只能吊销重签。勾上「使用开心上架服务同步证书」的话,出包用的那台 Mac 直接登录下载就行。
生成描述文件:「描述文件」页面点「新建描述文件」,上架选 App Store 类型,内测选 Ad Hoc 并勾上测试设备,关联上面的 Bundle ID 和证书,提交后下载 .mobileprovision。
私钥和 CSR 都在你本机生成后封进 .p12,不用在 Mac 上走「钥匙串访问 → 从证书颁发机构请求证书」那一套——对只写 Python、平时压根不碰 Xcode 的人来说,这一步省下来挺值的。
第三步:在 macOS 上导入签名材料
security import dist.p12 -k ~/Library/Keychains/login.keychain-db -P "你的p12密码" -T /usr/bin/codesign
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp AppStore.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
第四步:生成工程、编译、出包
在 macOS 上依次跑:
briefcase create iOS
briefcase build iOS
briefcase package iOS
briefcase create iOS 会在 build/ 下生成一个 Xcode 工程。package 这一步会做签名,Briefcase 会让你选用哪个签名身份(identity),也可以用参数直接指定,具体参数名以 briefcase package iOS --help 输出的为准——Briefcase 各版本的命令行选项变动比较频繁。
签名这块有个实操建议:别让 Xcode 自动管理签名。生成的工程里如果开着 Automatically manage signing,Xcode 会跑去 Apple 那边替你申请新证书,而同类型证书一共就 3 个名额,几轮下来会占满。用 Xcode 打开生成的工程,把它关掉,手动选第二步那套证书和描述文件,再回来跑 briefcase package iOS。
改了 pyproject.toml 里跟原生工程相关的配置之后,记得重新 briefcase create iOS(或者带 -r 更新),不然生成的工程里还是旧配置。
第五步:把 IPA 传上去
拿到包之后就跟 Mac 无关了。我一般先在开心上架的「文件&提交」页面把 IPA 打开看一眼签名信息——Bundle ID 是不是拼出来的那个、描述文件类型对不对、证书有没有过期,确认了再点「提交 App Store」。
命令行版本,在 Windows 上直接跑:
appuploader-cli upload -f MyApp.ipa -u dev@example.com -p abcd-efgh-ijkl-mnop
-p 是 Apple 生成的 App 专用密码(xxxx-xxxx-xxxx-xxxx),不是 Apple ID 登录密码。也可以用 API 密钥:
appuploader-cli upload -f MyApp.ipa --api-key <KeyID> --api-issuer <IssuerID> --private-key Auth_Key.p8
密钥方式只收 .ipa,且 App 要先在 App Store Connect 里建好。
第六步:发内测包
「设备」页面点「注册设备」,二维码发出去让人扫上报 UDID(一年 100 台,免费账号 3 台)。回描述文件那边点「重建」,生成一份包含这些设备的 Ad Hoc 描述文件,拷到 Mac 上替换掉旧的。
重新 package 一次,然后在「文件&提交」页面生成二维码让人扫码装。
写在最后
BeeWare 这条路对纯 Python 背景的人最友好的地方,是你几乎不用学 Objective-C 或 Swift。但签名这一环是躲不掉的 Apple 规则,和你用什么语言无关——把证书和描述文件在 Windows 上准备好,Mac 那边就只剩三条 briefcase 命令。'''
'''BeeWare 走的是和 Flet 不一样的路子:它的 Toga 用的是各平台的原生控件,iOS 上就是真的 UIKit,不套 Flutter 或者 WebView。打包工具叫 Briefcase,briefcase create iOS 会生成一个 Xcode 工程,briefcase build iOS 编译,briefcase package iOS 出包。
因为最终还是 Xcode 在干活,这几条命令都得在 macOS 上跑。Windows 上 Briefcase 只能处理 Windows/Android 那些目标。这一步没有变通。
我的分工是:Python 代码在 Windows 上写,iOS 出包丢给 macOS 机器;证书、描述文件、IPA 上传用开心上架(AppUploader)在 Windows 上处理。
先说结论:哪一步真绕不开 Mac
| 环节 | 我实际怎么做的 |
|---|---|
| 注册 Bundle ID、签发证书、生成描述文件 | Windows 上做完 |
| 写 Python 代码 | Windows |
briefcase create / build / package iOS |
要 macOS + Xcode |
| 提交 App Store | Windows 上传 |
| 装到 iPhone 测试 | 扫码注册 UDID,再扫码装 |
说明一下:我用的签证书工具本身在 macOS 上也能用(网页版不挑系统,桌面客户端有 macOS 版)。表里"不需要 Mac"是指这一步不再需要 Xcode、钥匙串访问或 Transporter。
第一步:在 pyproject.toml 里定好 bundle
Briefcase 的配置全在 pyproject.toml 里。iOS 的包标识由 bundle 加 app 名拼出来:
[tool.briefcase]
project_name = "My App"
bundle = "com.yourcompany"
version = "1.0.0"
[tool.briefcase.app.myapp]
formal_name = "My App"
这样最终的 Bundle Identifier 就是 com.yourcompany.myapp。拼出来的这个值要和 Apple 注册的 Bundle ID、描述文件绑定的 App ID 完全一致——注意是拼出来的结果,不是 bundle 字段本身,这点第一次配的时候容易搞错。
第二步:在 Windows 上签出证书和描述文件
注册 Bundle ID:开心上架的「Bundle ID」页面点「新建 Bundle ID」,Identifier 填拼出来的那个完整标识。创建后改不了,所以先把 bundle 和 app 名定下来再注册。
签发证书:「证书」页面点「新建证书」,上架选发布签名证书,真机调试选开发签名证书。绑定上面的 Bundle ID,填名称邮箱,设一个 .p12 保护密码——导入 Mac 钥匙串时要输,服务器不保存它,忘了只能吊销重签。勾上「使用开心上架服务同步证书」的话,出包用的那台 Mac 直接登录下载就行。
生成描述文件:「描述文件」页面点「新建描述文件」,上架选 App Store 类型,内测选 Ad Hoc 并勾上测试设备,关联上面的 Bundle ID 和证书,提交后下载 .mobileprovision。
私钥和 CSR 都在你本机生成后封进 .p12,不用在 Mac 上走「钥匙串访问 → 从证书颁发机构请求证书」那一套——对只写 Python、平时压根不碰 Xcode 的人来说,这一步省下来挺值的。
第三步:在 macOS 上导入签名材料
security import dist.p12 -k ~/Library/Keychains/login.keychain-db -P "你的p12密码" -T /usr/bin/codesign
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp AppStore.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
第四步:生成工程、编译、出包
在 macOS 上依次跑:
briefcase create iOS
briefcase build iOS
briefcase package iOS
briefcase create iOS 会在 build/ 下生成一个 Xcode 工程。package 这一步会做签名,Briefcase 会让你选用哪个签名身份(identity),也可以用参数直接指定,具体参数名以 briefcase package iOS --help 输出的为准——Briefcase 各版本的命令行选项变动比较频繁。
签名这块有个实操建议:别让 Xcode 自动管理签名。生成的工程里如果开着 Automatically manage signing,Xcode 会跑去 Apple 那边替你申请新证书,而同类型证书一共就 3 个名额,几轮下来会占满。用 Xcode 打开生成的工程,把它关掉,手动选第二步那套证书和描述文件,再回来跑 briefcase package iOS。
改了 pyproject.toml 里跟原生工程相关的配置之后,记得重新 briefcase create iOS(或者带 -r 更新),不然生成的工程里还是旧配置。
第五步:把 IPA 传上去
拿到包之后就跟 Mac 无关了。我一般先在开心上架的「文件&提交」页面把 IPA 打开看一眼签名信息——Bundle ID 是不是拼出来的那个、描述文件类型对不对、证书有没有过期,确认了再点「提交 App Store」。
命令行版本,在 Windows 上直接跑:
appuploader-cli upload -f MyApp.ipa -u dev@example.com -p abcd-efgh-ijkl-mnop
-p 是 Apple 生成的 App 专用密码(xxxx-xxxx-xxxx-xxxx),不是 Apple ID 登录密码。也可以用 API 密钥:
appuploader-cli upload -f MyApp.ipa --api-key <KeyID> --api-issuer <IssuerID> --private-key Auth_Key.p8
密钥方式只收 .ipa,且 App 要先在 App Store Connect 里建好。
第六步:发内测包
「设备」页面点「注册设备」,二维码发出去让人扫上报 UDID(一年 100 台,免费账号 3 台)。回描述文件那边点「重建」,生成一份包含这些设备的 Ad Hoc 描述文件,拷到 Mac 上替换掉旧的。
重新 package 一次,然后在「文件&提交」页面生成二维码让人扫码装。
写在最后
BeeWare 这条路对纯 Python 背景的人最友好的地方,是你几乎不用学 Objective-C 或 Swift。但签名这一环是躲不掉的 Apple 规则,和你用什么语言无关——把证书和描述文件在 Windows 上准备好,Mac 那边就只剩三条 briefcase 命令。'''
收起阅读 »如何让Canvas绘制内容能超出父组件的限制
问题现象
如何让Canvas绘制内容能超出父组件的限制,“按下”的响应区域不超出父组件范围?组件中.clip属性能实现此效果吗?
背景知识
HarmonyOS组件中.clip属性:是否对子组件超出当前组件范围外的区域进行裁剪。参考链接:clip。
HarmonyOS组件中.responseRegion属性可以实现组件的响应区域范围的变化,响应区域范围可以超出或者小于组件的布局范围。参考链接:自定义控制的多层级手势事件。
Canvas:Canvas组件提供画布,用于自定义绘制图形。
CanvasRenderingContext2D:使用CanvasRenderingContext2D在Canvas画布组件上进行绘制,绘制对象可以是矩形、文本、图片等。
解决方案
在HarmonyOS中,无法通过.clip属性实现所需效果。当.clip属性设置为true时,子组件超出当前组件范围的区域将不会响应绑定的手势事件;当设置为undefined时,系统将不再对超出部分进行裁剪,但这并不意味着可以控制内容超出组件本身的绘制范围。可以通过以下方案实现让Canvas绘制内容能超出父组件限制的功能,步骤如下:
使用Column父组件包含子组件Canvas,Column父组件宽高为(150,150)区域,子组件Canvas宽高为(300,300)。
对Canvas组件设置.responseRegion属性触摸热区为.responseRegion({ x: 75, y: 0, width: 150, height: 150 })。实现功能:蓝色区域(150150)可响应Canvas绘制内容时“按下(down)”的触摸操作,红色区域(300300去除蓝色区域的范围)不可响应“按下(down)”的触摸操作,红色区域只可响应从蓝色区域“按下(down)”后的“移动(move)”触摸操作。
说明
当组件Canvas绑定了.responseRegion(Rect),所有落在Rect区域范围的触摸事件和手势可被组件Canvas对应的回调响应。
@Entry
@Component
struct DrawBankCom {
paintSize: number = 5; // 当前画笔大小
private settings: RenderingContextSettings = new RenderingContextSettings(true);
canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
tempPath: Path2D = new Path2D();
@State @Watch('onChange') pathArray: Array<Path2D> = []; // 所有画图路径信息
isUpdate: boolean = true;
removeArray: Array<Path2D> = []; // 回退的路径集合
@State text: string = '';
@State eventType: string = '';
build() {
Column() {
if (this.isUpdate) {
Row() {
Column() {
Canvas(this.canvasContext)
// Canvas(子组件)宽高设置为300300
.width(300)
.height(300)
.onReady(() => {
this.canvasContext.lineWidth = this.paintSize;
this.canvasContext.stroke(this.tempPath);
for (let index = 0; index < this.pathArray.length; index++) {
this.canvasContext.stroke(this.pathArray[index]);
}
})
.onTouch((event?: TouchEvent) => {
if (event) {
if (event.type === TouchType.Down) {
this.eventType = 'Down';
this.canvasContext.lineWidth = this.paintSize;
this.canvasContext.beginPath();
this.tempPath = new Path2D();
this.pathArray.push(this.tempPath);
this.tempPath.moveTo(event.touches[0].x, event.touches[0].y);
this.canvasContext.moveTo(event.touches[0].x, event.touches[0].y);
}
if (event.type === TouchType.Up) {
this.eventType = 'Up';
}
if (event.type === TouchType.Move) {
this.eventType = 'Move';
this.tempPath.lineTo(event.touches[0].x, event.touches[0].y);
this.canvasContext.stroke(this.tempPath);
this.canvasContext.lineTo(event.touches[0].x, event.touches[0].y);
this.canvasContext.stroke();
}
this.text = 'TouchType:' + this.eventType + '\n touch point and touch element:\nx: ' +
event.touches[0].x + '\n' + 'y: ' + event.touches[0].y + '\nwidth:' +
event.target.area.width + '\nheight:' + event.target.area.height + '\pathArray size:' +
this.pathArray.length;
}
})
.renderFit(RenderFit.TOP_RIGHT)
// 设置触摸热区,蓝色区域可响应“down”操作,红色不可响应“down”操作,只能响应“move”操作
.responseRegion({
x: 75,
y: 0,
width: 150,
height: 150
});
}
// Column(父组件)宽高设置为150150
.width(150)
.height(150)
.backgroundColor('#ffbdf3e9');
};
}
Column() {
Row() {
Button('回退').onClick(() => {
this.pathArray.pop();
});
};
Text(this.text);
};
}
.width(300)
.height(300)
.clip(false)
.borderRadius(10)
.backgroundColor('#ff0000');
}
onChange() {
this.canvasContext.reset();
this.canvasContext.lineWidth = this.paintSize;
for (let index = 0; index < this.pathArray.length; index++) {
this.canvasContext.stroke(this.pathArray[index]);
}
}
}
具体表现为从蓝色区域开始画线,可以延伸到红色区域,但是不可从红色区域开始画线。
总结
使用.responseRegion属性可以实现组件的响应区域范围的变化,能实现让Canvas绘制内容能超出父组件的限制。
https://sites.google.com/view/1svbrd24/home
https://sites.google.com/view/2jlhml67/home
https://sites.google.com/view/0ojkvo59/home
https://sites.google.com/view/5erbeg51/home
https://sites.google.com/view/7txhqp00/home
https://sites.google.com/view/9ahuri30/home
https://sites.google.com/view/9brhff50/home
https://sites.google.com/view/7usmzs19/home
https://sites.google.com/view/7fkyls54/home
https://sites.google.com/view/4yfngr91/home
https://sites.google.com/view/7bibkd36/home
https://sites.google.com/view/7rgsbk35/home
https://sites.google.com/view/5tjymn45/home
https://sites.google.com/view/0qxruy91/home
https://sites.google.com/view/6xlnnt32/home
https://sites.google.com/view/9ekbqp32/home
https://sites.google.com/view/5vwcmf35/home
https://sites.google.com/view/2xhdlr45/home
https://sites.google.com/view/0giwkx12/home
https://sites.google.com/view/9cqmye26/home
https://sites.google.com/view/0rxdat90/home
https://sites.google.com/view/3yywdt98/home
https://sites.google.com/view/1lbijc54/home
https://sites.google.com/view/4ilgby72/home
https://sites.google.com/view/3lexut78/home
https://sites.google.com/view/1dsgmu66/home
https://sites.google.com/view/7lsxyd26/home
https://sites.google.com/view/2ulade56/home
https://sites.google.com/view/1tipkq60/home
https://sites.google.com/view/4jhjbn51/home
https://sites.google.com/view/2bpvrp99/home
https://sites.google.com/view/7hyotm19/home
https://sites.google.com/view/1sxxqc05/home
https://sites.google.com/view/7bzjsk57/home
https://sites.google.com/view/0rvozy96/home
https://sites.google.com/view/4lqeqc80/home
https://sites.google.com/view/5xnhok81/home
https://sites.google.com/view/9piiqm40/home
https://sites.google.com/view/2opvsv00/home
https://sites.google.com/view/5sdsim60/home
https://sites.google.com/view/6qrjip83/home
https://sites.google.com/view/8wzpjp01/home
https://sites.google.com/view/2rbzfb85/home
https://sites.google.com/view/8yjxua98/home
https://sites.google.com/view/5asfix82/home
https://sites.google.com/view/5myuib41/home
https://sites.google.com/view/9arbno01/home
https://sites.google.com/view/9fqrsq43/home
https://sites.google.com/view/2lgtzy82/home
https://sites.google.com/view/4detht79/home
https://sites.google.com/view/9yezyo00/home
https://sites.google.com/view/0nofnm30/home
https://sites.google.com/view/2vfhym56/home
https://sites.google.com/view/1rivml82/home
https://sites.google.com/view/7ohgjy23/home
https://sites.google.com/view/1gbrst25/home
https://sites.google.com/view/8zevsj79/home
https://sites.google.com/view/1tfuhl96/home
https://sites.google.com/view/8fmixj07/home
https://sites.google.com/view/3yprml67/home
https://sites.google.com/view/4vzytf83/home
https://sites.google.com/view/0zydqk84/home
https://sites.google.com/view/8onjpb17/home
https://sites.google.com/view/5qbsvc57/home
https://sites.google.com/view/2uqmyg64/home
https://sites.google.com/view/6hnwoa03/home
https://sites.google.com/view/2bdovq40/home
https://sites.google.com/view/3ykzjl65/home
https://sites.google.com/view/3ypjlx51/home
https://sites.google.com/view/2vecse29/home
https://sites.google.com/view/8wjmoz24/home
https://sites.google.com/view/6anaaz10/home
https://sites.google.com/view/6vztlx87/home
https://sites.google.com/view/9hxqwy10/home
https://sites.google.com/view/6emmnf53/home
https://sites.google.com/view/9qgmon96/home
https://sites.google.com/view/8vqblh76/home
https://sites.google.com/view/5repzi61/home
https://sites.google.com/view/6iqxbj96/home
https://sites.google.com/view/4fiyto32/home
https://sites.google.com/view/1uimwi96/home
https://sites.google.com/view/5ibqaj17/home
https://sites.google.com/view/9nqdyq84/home
https://sites.google.com/view/4lwyyd65/home
https://sites.google.com/view/3gawuk38/home
https://sites.google.com/view/5lgzag04/home
https://sites.google.com/view/1imonc99/home
https://sites.google.com/view/9xyukm01/home
https://sites.google.com/view/6nalbh77/home
https://sites.google.com/view/2ypecr06/home
https://sites.google.com/view/5zdody20/home
https://sites.google.com/view/6wkgom88/home
https://sites.google.com/view/8zwqqi53/home
https://sites.google.com/view/0qyluw74/home
https://sites.google.com/view/5aphjd68/home
https://sites.google.com/view/2sacyn38/home
https://sites.google.com/view/8dbjrm35/home
https://sites.google.com/view/6unuwk46/home
https://sites.google.com/view/5hrlie99/home
https://sites.google.com/view/6mxmyz56/home
https://sites.google.com/view/9gdzzy11/home
https://sites.google.com/view/3wkfon30/home
https://sites.google.com/view/7jesbl97/home
https://sites.google.com/view/7xllml61/home
https://sites.google.com/view/3mpkjx30/home
https://sites.google.com/view/6idizy90/home
https://sites.google.com/view/1czsip30/home
https://sites.google.com/view/3ezdwf97/home
https://sites.google.com/view/1ydubh84/home
https://sites.google.com/view/4cvedy61/home
https://sites.google.com/view/7yyumz73/home
https://sites.google.com/view/5pkquw00/home
https://sites.google.com/view/7dceex86/home
https://sites.google.com/view/3gnvyg34/home
https://sites.google.com/view/2ehwht60/home
https://sites.google.com/view/3ocyay63/home
https://sites.google.com/view/6uizgx22/home
https://sites.google.com/view/3cgblb20/home
https://sites.google.com/view/3guucg59/home
https://sites.google.com/view/9cdybx22/home
https://sites.google.com/view/7giaxy95/home
https://sites.google.com/view/3qkkfi06/home
https://sites.google.com/view/9goqyg50/home
https://sites.google.com/view/6sfntm95/home
https://sites.google.com/view/8hekup65/home
https://sites.google.com/view/8dypsx49/home
https://sites.google.com/view/1emhrm38/home
https://sites.google.com/view/1yyqhu29/home
https://sites.google.com/view/5heoek19/home
https://sites.google.com/view/3tvkkw85/home
https://sites.google.com/view/4dqdwv87/home
https://sites.google.com/view/2tsotl60/home
https://sites.google.com/view/7mpwib24/home
https://sites.google.com/view/9dlwvj94/home
https://sites.google.com/view/3hggdc60/home
https://sites.google.com/view/3vhyat21/home
https://sites.google.com/view/7vyebf30/home
https://sites.google.com/view/4wnnbn37/home
https://sites.google.com/view/6mjlkw77/home
https://sites.google.com/view/0jcihr79/home
https://sites.google.com/view/6lpxba81/home
https://sites.google.com/view/7nmcwv30/home
https://sites.google.com/view/3ztaut28/home
https://sites.google.com/view/1ecdeq22/home
https://sites.google.com/view/0dgpba93/home
https://sites.google.com/view/1aqqqo25/home
https://sites.google.com/view/1raxlw38/home
https://sites.google.com/view/3snsha82/home
https://sites.google.com/view/5uwlrm29/home
https://sites.google.com/view/4feaqq70/home
https://sites.google.com/view/3nlhqj52/home
https://sites.google.com/view/1pidkm14/home
https://sites.google.com/view/2wqdnc45/home
https://sites.google.com/view/1iwyhj71/home
https://sites.google.com/view/8evxkf88/home
https://sites.google.com/view/5zbhhn62/home
https://sites.google.com/view/9hjcnp07/home
https://sites.google.com/view/3mxnek99/home
https://sites.google.com/view/9bmsbt04/home
https://sites.google.com/view/2lezmt81/home
https://sites.google.com/view/6qmqey84/home
https://coub.com/view/scsevfdzaa
https://coub.com/view/ghy3aenpkf
https://coub.com/view/xfyiqequqh
https://coub.com/view/45ky2yyzja
https://coub.com/view/v7lv7a0sy1
https://coub.com/view/e09y796sai
https://coub.com/view/z9dgue55fb
https://coub.com/view/4v9yryaq7b
https://coub.com/view/rjakrzmprm
https://coub.com/view/443k6aufbo
https://coub.com/view/adxz7xc4zj
https://coub.com/view/gontyw4afu
https://coub.com/view/67wn0hy74w
https://coub.com/view/p7i0qsp0th
https://coub.com/view/bu18agb0cw
https://coub.com/view/u453dsg9rs
https://coub.com/view/0xh6401od2
https://coub.com/view/s8ivp1bnms
https://coub.com/view/q7d0nkso0h
https://coub.com/view/gadau0rb64
https://coub.com/view/79ww1tgrud
https://coub.com/view/grqy2h2n44
https://coub.com/view/dvjar8c2ng
https://coub.com/view/365566dfec
https://coub.com/view/szouxmw105
https://coub.com/view/yucl2gqgiq
https://coub.com/view/mcb2rze9ox
https://coub.com/view/0j5cdu3ezv
https://coub.com/view/023m21y9ex
https://coub.com/view/0tvarwqbh6
https://coub.com/view/ks6hpuqzv7
https://coub.com/view/390avxe2u9
https://coub.com/view/vqb725jiar
https://coub.com/view/9ekoylsaw1
https://coub.com/view/n5wdecqayc
https://coub.com/view/lu3z4p5rah
https://coub.com/view/6lu4dpcva8
https://coub.com/view/l3dai5ah2r
https://coub.com/view/6rged9l4g5
https://coub.com/view/u4gupyey94
https://coub.com/view/9oyqag6yr3
https://coub.com/view/3b6ywmk5lt
https://coub.com/view/t5mm2naqc1
问题现象
如何让Canvas绘制内容能超出父组件的限制,“按下”的响应区域不超出父组件范围?组件中.clip属性能实现此效果吗?
背景知识
HarmonyOS组件中.clip属性:是否对子组件超出当前组件范围外的区域进行裁剪。参考链接:clip。
HarmonyOS组件中.responseRegion属性可以实现组件的响应区域范围的变化,响应区域范围可以超出或者小于组件的布局范围。参考链接:自定义控制的多层级手势事件。
Canvas:Canvas组件提供画布,用于自定义绘制图形。
CanvasRenderingContext2D:使用CanvasRenderingContext2D在Canvas画布组件上进行绘制,绘制对象可以是矩形、文本、图片等。
解决方案
在HarmonyOS中,无法通过.clip属性实现所需效果。当.clip属性设置为true时,子组件超出当前组件范围的区域将不会响应绑定的手势事件;当设置为undefined时,系统将不再对超出部分进行裁剪,但这并不意味着可以控制内容超出组件本身的绘制范围。可以通过以下方案实现让Canvas绘制内容能超出父组件限制的功能,步骤如下:
使用Column父组件包含子组件Canvas,Column父组件宽高为(150,150)区域,子组件Canvas宽高为(300,300)。
对Canvas组件设置.responseRegion属性触摸热区为.responseRegion({ x: 75, y: 0, width: 150, height: 150 })。实现功能:蓝色区域(150150)可响应Canvas绘制内容时“按下(down)”的触摸操作,红色区域(300300去除蓝色区域的范围)不可响应“按下(down)”的触摸操作,红色区域只可响应从蓝色区域“按下(down)”后的“移动(move)”触摸操作。
说明
当组件Canvas绑定了.responseRegion(Rect),所有落在Rect区域范围的触摸事件和手势可被组件Canvas对应的回调响应。
@Entry
@Component
struct DrawBankCom {
paintSize: number = 5; // 当前画笔大小
private settings: RenderingContextSettings = new RenderingContextSettings(true);
canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
tempPath: Path2D = new Path2D();
@State @Watch('onChange') pathArray: Array<Path2D> = []; // 所有画图路径信息
isUpdate: boolean = true;
removeArray: Array<Path2D> = []; // 回退的路径集合
@State text: string = '';
@State eventType: string = '';
build() {
Column() {
if (this.isUpdate) {
Row() {
Column() {
Canvas(this.canvasContext)
// Canvas(子组件)宽高设置为300300
.width(300)
.height(300)
.onReady(() => {
this.canvasContext.lineWidth = this.paintSize;
this.canvasContext.stroke(this.tempPath);
for (let index = 0; index < this.pathArray.length; index++) {
this.canvasContext.stroke(this.pathArray[index]);
}
})
.onTouch((event?: TouchEvent) => {
if (event) {
if (event.type === TouchType.Down) {
this.eventType = 'Down';
this.canvasContext.lineWidth = this.paintSize;
this.canvasContext.beginPath();
this.tempPath = new Path2D();
this.pathArray.push(this.tempPath);
this.tempPath.moveTo(event.touches[0].x, event.touches[0].y);
this.canvasContext.moveTo(event.touches[0].x, event.touches[0].y);
}
if (event.type === TouchType.Up) {
this.eventType = 'Up';
}
if (event.type === TouchType.Move) {
this.eventType = 'Move';
this.tempPath.lineTo(event.touches[0].x, event.touches[0].y);
this.canvasContext.stroke(this.tempPath);
this.canvasContext.lineTo(event.touches[0].x, event.touches[0].y);
this.canvasContext.stroke();
}
this.text = 'TouchType:' + this.eventType + '\n touch point and touch element:\nx: ' +
event.touches[0].x + '\n' + 'y: ' + event.touches[0].y + '\nwidth:' +
event.target.area.width + '\nheight:' + event.target.area.height + '\pathArray size:' +
this.pathArray.length;
}
})
.renderFit(RenderFit.TOP_RIGHT)
// 设置触摸热区,蓝色区域可响应“down”操作,红色不可响应“down”操作,只能响应“move”操作
.responseRegion({
x: 75,
y: 0,
width: 150,
height: 150
});
}
// Column(父组件)宽高设置为150150
.width(150)
.height(150)
.backgroundColor('#ffbdf3e9');
};
}
Column() {
Row() {
Button('回退').onClick(() => {
this.pathArray.pop();
});
};
Text(this.text);
};
}
.width(300)
.height(300)
.clip(false)
.borderRadius(10)
.backgroundColor('#ff0000');
}
onChange() {
this.canvasContext.reset();
this.canvasContext.lineWidth = this.paintSize;
for (let index = 0; index < this.pathArray.length; index++) {
this.canvasContext.stroke(this.pathArray[index]);
}
}
}
具体表现为从蓝色区域开始画线,可以延伸到红色区域,但是不可从红色区域开始画线。
总结
使用.responseRegion属性可以实现组件的响应区域范围的变化,能实现让Canvas绘制内容能超出父组件的限制。
https://sites.google.com/view/1svbrd24/home
https://sites.google.com/view/2jlhml67/home
https://sites.google.com/view/0ojkvo59/home
https://sites.google.com/view/5erbeg51/home
https://sites.google.com/view/7txhqp00/home
https://sites.google.com/view/9ahuri30/home
https://sites.google.com/view/9brhff50/home
https://sites.google.com/view/7usmzs19/home
https://sites.google.com/view/7fkyls54/home
https://sites.google.com/view/4yfngr91/home
https://sites.google.com/view/7bibkd36/home
https://sites.google.com/view/7rgsbk35/home
https://sites.google.com/view/5tjymn45/home
https://sites.google.com/view/0qxruy91/home
https://sites.google.com/view/6xlnnt32/home
https://sites.google.com/view/9ekbqp32/home
https://sites.google.com/view/5vwcmf35/home
https://sites.google.com/view/2xhdlr45/home
https://sites.google.com/view/0giwkx12/home
https://sites.google.com/view/9cqmye26/home
https://sites.google.com/view/0rxdat90/home
https://sites.google.com/view/3yywdt98/home
https://sites.google.com/view/1lbijc54/home
https://sites.google.com/view/4ilgby72/home
https://sites.google.com/view/3lexut78/home
https://sites.google.com/view/1dsgmu66/home
https://sites.google.com/view/7lsxyd26/home
https://sites.google.com/view/2ulade56/home
https://sites.google.com/view/1tipkq60/home
https://sites.google.com/view/4jhjbn51/home
https://sites.google.com/view/2bpvrp99/home
https://sites.google.com/view/7hyotm19/home
https://sites.google.com/view/1sxxqc05/home
https://sites.google.com/view/7bzjsk57/home
https://sites.google.com/view/0rvozy96/home
https://sites.google.com/view/4lqeqc80/home
https://sites.google.com/view/5xnhok81/home
https://sites.google.com/view/9piiqm40/home
https://sites.google.com/view/2opvsv00/home
https://sites.google.com/view/5sdsim60/home
https://sites.google.com/view/6qrjip83/home
https://sites.google.com/view/8wzpjp01/home
https://sites.google.com/view/2rbzfb85/home
https://sites.google.com/view/8yjxua98/home
https://sites.google.com/view/5asfix82/home
https://sites.google.com/view/5myuib41/home
https://sites.google.com/view/9arbno01/home
https://sites.google.com/view/9fqrsq43/home
https://sites.google.com/view/2lgtzy82/home
https://sites.google.com/view/4detht79/home
https://sites.google.com/view/9yezyo00/home
https://sites.google.com/view/0nofnm30/home
https://sites.google.com/view/2vfhym56/home
https://sites.google.com/view/1rivml82/home
https://sites.google.com/view/7ohgjy23/home
https://sites.google.com/view/1gbrst25/home
https://sites.google.com/view/8zevsj79/home
https://sites.google.com/view/1tfuhl96/home
https://sites.google.com/view/8fmixj07/home
https://sites.google.com/view/3yprml67/home
https://sites.google.com/view/4vzytf83/home
https://sites.google.com/view/0zydqk84/home
https://sites.google.com/view/8onjpb17/home
https://sites.google.com/view/5qbsvc57/home
https://sites.google.com/view/2uqmyg64/home
https://sites.google.com/view/6hnwoa03/home
https://sites.google.com/view/2bdovq40/home
https://sites.google.com/view/3ykzjl65/home
https://sites.google.com/view/3ypjlx51/home
https://sites.google.com/view/2vecse29/home
https://sites.google.com/view/8wjmoz24/home
https://sites.google.com/view/6anaaz10/home
https://sites.google.com/view/6vztlx87/home
https://sites.google.com/view/9hxqwy10/home
https://sites.google.com/view/6emmnf53/home
https://sites.google.com/view/9qgmon96/home
https://sites.google.com/view/8vqblh76/home
https://sites.google.com/view/5repzi61/home
https://sites.google.com/view/6iqxbj96/home
https://sites.google.com/view/4fiyto32/home
https://sites.google.com/view/1uimwi96/home
https://sites.google.com/view/5ibqaj17/home
https://sites.google.com/view/9nqdyq84/home
https://sites.google.com/view/4lwyyd65/home
https://sites.google.com/view/3gawuk38/home
https://sites.google.com/view/5lgzag04/home
https://sites.google.com/view/1imonc99/home
https://sites.google.com/view/9xyukm01/home
https://sites.google.com/view/6nalbh77/home
https://sites.google.com/view/2ypecr06/home
https://sites.google.com/view/5zdody20/home
https://sites.google.com/view/6wkgom88/home
https://sites.google.com/view/8zwqqi53/home
https://sites.google.com/view/0qyluw74/home
https://sites.google.com/view/5aphjd68/home
https://sites.google.com/view/2sacyn38/home
https://sites.google.com/view/8dbjrm35/home
https://sites.google.com/view/6unuwk46/home
https://sites.google.com/view/5hrlie99/home
https://sites.google.com/view/6mxmyz56/home
https://sites.google.com/view/9gdzzy11/home
https://sites.google.com/view/3wkfon30/home
https://sites.google.com/view/7jesbl97/home
https://sites.google.com/view/7xllml61/home
https://sites.google.com/view/3mpkjx30/home
https://sites.google.com/view/6idizy90/home
https://sites.google.com/view/1czsip30/home
https://sites.google.com/view/3ezdwf97/home
https://sites.google.com/view/1ydubh84/home
https://sites.google.com/view/4cvedy61/home
https://sites.google.com/view/7yyumz73/home
https://sites.google.com/view/5pkquw00/home
https://sites.google.com/view/7dceex86/home
https://sites.google.com/view/3gnvyg34/home
https://sites.google.com/view/2ehwht60/home
https://sites.google.com/view/3ocyay63/home
https://sites.google.com/view/6uizgx22/home
https://sites.google.com/view/3cgblb20/home
https://sites.google.com/view/3guucg59/home
https://sites.google.com/view/9cdybx22/home
https://sites.google.com/view/7giaxy95/home
https://sites.google.com/view/3qkkfi06/home
https://sites.google.com/view/9goqyg50/home
https://sites.google.com/view/6sfntm95/home
https://sites.google.com/view/8hekup65/home
https://sites.google.com/view/8dypsx49/home
https://sites.google.com/view/1emhrm38/home
https://sites.google.com/view/1yyqhu29/home
https://sites.google.com/view/5heoek19/home
https://sites.google.com/view/3tvkkw85/home
https://sites.google.com/view/4dqdwv87/home
https://sites.google.com/view/2tsotl60/home
https://sites.google.com/view/7mpwib24/home
https://sites.google.com/view/9dlwvj94/home
https://sites.google.com/view/3hggdc60/home
https://sites.google.com/view/3vhyat21/home
https://sites.google.com/view/7vyebf30/home
https://sites.google.com/view/4wnnbn37/home
https://sites.google.com/view/6mjlkw77/home
https://sites.google.com/view/0jcihr79/home
https://sites.google.com/view/6lpxba81/home
https://sites.google.com/view/7nmcwv30/home
https://sites.google.com/view/3ztaut28/home
https://sites.google.com/view/1ecdeq22/home
https://sites.google.com/view/0dgpba93/home
https://sites.google.com/view/1aqqqo25/home
https://sites.google.com/view/1raxlw38/home
https://sites.google.com/view/3snsha82/home
https://sites.google.com/view/5uwlrm29/home
https://sites.google.com/view/4feaqq70/home
https://sites.google.com/view/3nlhqj52/home
https://sites.google.com/view/1pidkm14/home
https://sites.google.com/view/2wqdnc45/home
https://sites.google.com/view/1iwyhj71/home
https://sites.google.com/view/8evxkf88/home
https://sites.google.com/view/5zbhhn62/home
https://sites.google.com/view/9hjcnp07/home
https://sites.google.com/view/3mxnek99/home
https://sites.google.com/view/9bmsbt04/home
https://sites.google.com/view/2lezmt81/home
https://sites.google.com/view/6qmqey84/home
https://coub.com/view/scsevfdzaa
https://coub.com/view/ghy3aenpkf
https://coub.com/view/xfyiqequqh
https://coub.com/view/45ky2yyzja
https://coub.com/view/v7lv7a0sy1
https://coub.com/view/e09y796sai
https://coub.com/view/z9dgue55fb
https://coub.com/view/4v9yryaq7b
https://coub.com/view/rjakrzmprm
https://coub.com/view/443k6aufbo
https://coub.com/view/adxz7xc4zj
https://coub.com/view/gontyw4afu
https://coub.com/view/67wn0hy74w
https://coub.com/view/p7i0qsp0th
https://coub.com/view/bu18agb0cw
https://coub.com/view/u453dsg9rs
https://coub.com/view/0xh6401od2
https://coub.com/view/s8ivp1bnms
https://coub.com/view/q7d0nkso0h
https://coub.com/view/gadau0rb64
https://coub.com/view/79ww1tgrud
https://coub.com/view/grqy2h2n44
https://coub.com/view/dvjar8c2ng
https://coub.com/view/365566dfec
https://coub.com/view/szouxmw105
https://coub.com/view/yucl2gqgiq
https://coub.com/view/mcb2rze9ox
https://coub.com/view/0j5cdu3ezv
https://coub.com/view/023m21y9ex
https://coub.com/view/0tvarwqbh6
https://coub.com/view/ks6hpuqzv7
https://coub.com/view/390avxe2u9
https://coub.com/view/vqb725jiar
https://coub.com/view/9ekoylsaw1
https://coub.com/view/n5wdecqayc
https://coub.com/view/lu3z4p5rah
https://coub.com/view/6lu4dpcva8
https://coub.com/view/l3dai5ah2r
https://coub.com/view/6rged9l4g5
https://coub.com/view/u4gupyey94
https://coub.com/view/9oyqag6yr3
https://coub.com/view/3b6ywmk5lt
https://coub.com/view/t5mm2naqc1
JavaScript `this` 指南
JavaScript this 指南
一、核心心法
this不是在函数定义时确定的,而是在函数被调用时确定的。
判断this,只需要看一件事:这个函数是怎么被调用的(调用点)。
唯一的例外是箭头函数——它根本没有自己的 this。
二、五种绑定规则
规则 1:默认绑定(独立函数调用)
function foo() {
console.log(this);
}
foo(); // 非严格模式: globalThis (浏览器 window / Node global)
// 严格模式: undefined
'use strict';
function foo() { console.log(this); }
foo(); // undefined
顶层 this 的值取决于环境:
| 环境 | 顶层 this |
|---|---|
浏览器 <script> |
window |
ES Module (.mjs / type="module") |
undefined |
CommonJS 模块 (Node .js) |
module.exports(即 {}) |
| 函数内(非严格) | globalThis |
| 函数内(严格) | undefined |
| class 内部 | 永远严格模式 |
规则 2:隐式绑定(方法调用)
看调用时最后一个点前面是谁。
const obj = {
name: 'obj',
foo() { console.log(this.name); }
};
obj.foo(); // 'obj'
多层嵌套只看最后一层:
const a = { name: 'a', b: { name: 'b', foo() { console.log(this.name); } } };
a.b.foo(); // 'b' ← 不是 'a'
方括号访问同理:
a.b['foo'](); // 'b'
规则 3:显式绑定(call / apply / bind)
function greet(greeting, punct) {
console.log(greeting + ', ' + this.name + punct);
}
const me = { name: 'Alice' };
greet.call(me, 'Hi', '!'); // 逐个传参
greet.apply(me, ['Hi', '!']); // 数组传参 (Apply → Array)
const bound = greet.bind(me, 'Hi'); // 返回新函数 + 支持柯里化
bound('!');
bind 的三个特性:
- 返回一个新函数,不立即执行
- 支持参数预置(柯里化)
- 硬绑定:绑定后无法再被
call/apply改变
const hard = greet.bind(me);
hard.call({ name: 'Bob' }, 'Yo', '!'); // 依然是 Alice
传 null / undefined 的坑:
function foo() { console.log(this); }
foo.call(null); // 非严格: globalThis(被替换了!) 严格: null
安全做法——DMZ 空对象:
const DMZ = Object.create(null); // 比 {} 更干净,无原型
foo.apply(DMZ, [1, 2]);
原始值会被装箱(仅非严格模式):
function f() { console.log(typeof this); }
f.call(1); // 'object' (Number 包装对象)
// 严格模式下: 'number'
规则 4:new 绑定
new Foo() 做了四件事:
- 创建一个新的空对象
- 将该对象的
[[Prototype]]指向Foo.prototype - 以该对象为
this执行Foo - 如果
Foo没有返回一个对象,则返回这个新对象
function Person(name) {
this.name = name;
// return { name: 'hacked' }; // 若返回对象,则 new 的结果是它
// return 42; // 返回原始值 → 被忽略,仍返回 this
}
const p = new Person('Alice'); // p.name === 'Alice'
规则 5:箭头函数(词法绑定)
箭头函数没有自己的 this,它继承定义时所在外层作用域的 this,且永久不可改变。
const obj = {
name: 'obj',
regular() {
const arrow = () => console.log(this.name);
arrow(); // 'obj' ← 继承 regular 的 this
},
arrowMethod: () => console.log(this?.name) // ⚠️ 继承的是模块/全局的 this
};
obj.regular(); // 'obj'
obj.arrowMethod(); // undefined ← 经典陷阱!
箭头函数无法被改变:
const arrow = () => console.log(this);
arrow.call({ a: 1 }); // call 无效
arrow.bind({ a: 1 })(); // bind 无效
new arrow(); // TypeError: arrow is not a constructor
箭头函数也没有 arguments、super、new.target。
三、优先级
new 绑定 > 显式绑定(bind/call/apply) > 隐式绑定(obj.fn) > 默认绑定
箭头函数不在此列——它压根不参与,直接看词法作用域。
验证 new > bind:
function Foo(a) { this.a = a; }
const obj = {};
const B = Foo.bind(obj);
B(2); // obj.a === 2 (bind 生效)
const b = new B(3);// b.a === 3, obj.a 仍是 2 ← new 赢了
这正是
bindpolyfill 里必须判断new.target的原因。
四、隐式丢失:最常见的 Bug 来源
1. 赋值给变量
const obj = { name: 'obj', foo() { console.log(this?.name); } };
const fn = obj.foo; // 只拿到了函数引用,丢掉了 obj
fn(); // undefined
2. 作为回调传递
setTimeout(obj.foo, 100); // 丢失
[1].forEach(obj.foo); // 丢失
btn.addEventListener('click', obj.foo); // this 变成 btn
3. 解构
const { foo } = obj;
foo(); // 丢失
这就是 React 中
const { setState } = this会出问题、
以及 Vue3 用ref而非this的部分原因。
4. 间接引用
const o1 = { name: 'o1', foo() { console.log(this.name); } };
const o2 = { name: 'o2' };
o2.foo = o1.foo;
o2.foo(); // 'o2' (正常隐式绑定)
(o2.foo = o1.foo)(); // 赋值表达式返回函数本身 → 默认绑定 → undefined/报错
5. 解决方案汇总
// ① bind
setTimeout(obj.foo.bind(obj), 100);
// ② 箭头函数包裹(推荐,可读性好)
setTimeout(() => obj.foo(), 100);
// ③ 数组方法的 thisArg 参数
[1, 2].forEach(obj.foo, obj);
// ④ class field 箭头函数(见下文)
支持 thisArg 的数组方法:
forEach map filter some every find findIndex findLast findLastIndex flatMap
不支持的: reduce reduceRight sort(它们的第二参数另有用途)
五、Class 中的 this
类体内永远是严格模式
class Counter {
count = 0;
inc() { this.count++; } // 原型方法 → this 会丢失
dec = () => { this.count--; }; // 类字段箭头函数 → this 永久绑定实例
}
const c = new Counter();
const { inc, dec } = c;
dec(); // ✅ 正常
inc(); // ❌ TypeError: Cannot read properties of undefined
两者的本质区别:
原型方法 inc() |
类字段 dec = () => {} |
|
|---|---|---|
| 存放位置 | Counter.prototype |
每个实例上 |
| 内存 | 共享一份 | 每个实例一份(内存开销大) |
this |
调用时决定 | 定义时锁死为实例 |
可被 call 改变 |
是 | 否 |
可被子类 super 调用 |
是 | 否 |
推荐做法: 默认用原型方法,只在需要作为回调传递时用箭头字段,或在构造器中 bind:
class Counter {
constructor() {
this.inc = this.inc.bind(this); // 等价效果
}
}
静态方法中的 this
class A {
static create() { return new this(); } // this === 类本身,支持子类继承
}
class B extends A {}
B.create(); // 返回 B 的实例,而非 A
继承与 super
class Base {
constructor() { this.type = 'base'; }
hello() { console.log('base hello'); }
}
class Child extends Base {
constructor() {
// console.log(this); // ❌ ReferenceError: 必须先调用 super()
super(); // 派生类中,super() 之前 this 处于 TDZ
console.log(this.type);// 'base'
}
hello() {
super.hello(); // super 方法调用时,this 仍是当前实例
}
}
私有字段的坑
class A {
#secret = 1;
get() { return this.#secret; }
}
const a = new A();
a.get.call({}); // TypeError: Cannot read private member #secret
// 私有字段是"品牌检查",this 必须是真实实例
六、各类场景速查
DOM 事件
btn.addEventListener('click', function () {
console.log(this); // === event.currentTarget (btn)
});
btn.addEventListener('click', () => {
console.log(this); // 外层作用域的 this,不是 btn
});
handleEvent 特殊接口(传对象而非函数):
const handler = {
count: 0,
handleEvent(e) { console.log(this.count++, e.type); } // this === handler
};
btn.addEventListener('click', handler);
内联 onclick="..." 属性中的 this 也是元素本身。
定时器
setTimeout(function () {
console.log(this);
// 浏览器非严格: window
// Node: Timeout 对象
}, 0);
getter / setter
const obj = {
_v: 1,
get v() { return this._v; } // this 是访问该属性的对象
};
Proxy 中的 this 泄漏
const target = { greet() { return this; } };
const proxy = new Proxy(target, {});
proxy.greet() === proxy; // true —— this 是 proxy,不是 target
// 若 target 用了私有字段或内部槽(如 Map/Set),会报错
标签模板 / 生成器 / async
async function f() { return this; } // 与普通函数规则一致
function* g() { yield this; } // 同上
七、手写实现(面试高频)
Function.prototype.myCall = function (context, ...args) {
if (typeof this !== 'function') throw new TypeError('not a function');
context = context ?? globalThis;
if (Object(context) !== context) context = Object(context); // 原始值装箱
const key = Symbol('fn'); // Symbol 避免属性名冲突
context[key] = this;
try {
return context[key](...args);
} finally {
delete context[key];
}
};
Function.prototype.myApply = function (context, args = []) {
return this.myCall(context, ...args);
};
Function.prototype.myBind = function (context, ...preArgs) {
const fn = this;
if (typeof fn !== 'function') throw new TypeError('not a function');
function bound(...args) {
// 关键:new 调用时忽略 context,使用新建的 this
return fn.apply(
new.target ? this : context,
[...preArgs, ...args]
);
}
// 维持原型链,使 instanceof 正确
bound.prototype = Object.create(fn.prototype || null);
return bound;
};
// 手写 new
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype);
const ret = Ctor.apply(obj, args);
return (ret !== null && (typeof ret === 'object' || typeof ret === 'function'))
? ret
: obj;
}
八、综合练习(先自己作答)
var name = 'global'; // 注意:必须是 var,let/const 不挂载到 window
const obj = {
name: 'obj',
f1() { console.log(this.name); },
f2: () => console.log(this?.name),
f3() {
return function () { console.log(this?.name); };
},
f4() {
return () => console.log(this.name);
}
};
obj.f1(); // ?
obj.f2(); // ?
obj.f3()(); // ?
obj.f4()(); // ?
const g = obj.f1;
g(); // ?
obj.f1.call({ name: 'x' }); // ?
new obj.f1(); // ?
答案
obj.f1() → 'obj' 隐式绑定
obj.f2() → 'global'/undefined 箭头函数继承顶层 this
obj.f3()() → 'global'/undefined 返回的函数独立调用,默认绑定
obj.f4()() → 'obj' 箭头函数继承 f4 的 this
g() → 'global'/undefined 隐式丢失
obj.f1.call({...})→ 'x' 显式绑定
new obj.f1() → undefined new 绑定,新对象上没有 name
(浏览器非严格模式下顶层 this 是 window,故为 'global';ESM/严格模式下为 undefined 或报错)
九、终极判断流程图
函数是箭头函数?
├─ 是 → this = 定义时外层作用域的 this(向上查找,不可改变)
└─ 否 ↓
用 new 调用? ──→ 是 → this = 新创建的对象
↓ 否
用 call/apply/bind 调用? ──→ 是 → this = 指定的对象
↓ 否 (非严格下 null/undefined 会替换为 globalThis)
通过 obj.fn() 调用? ──→ 是 → this = obj(只看最后一个点)
↓ 否
严格模式? ──→ 是 → this = undefined
└→ 否 → this = globalThis
十、最佳实践
- 优先用箭头函数写回调,避免
this丢失 - 对象方法用简写语法
foo() {},不要用箭头函数(除非确实想要外层this) - class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器
bind - 全项目开启严格模式 / 使用 ESM,让
this错误尽早暴露为undefined报错 - TypeScript 用户:开启
noImplicitThis,并善用显式this参数:function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ } - 不确定时,打印
console.log(this)—— 比推理快十倍
JavaScript this 指南
一、核心心法
this不是在函数定义时确定的,而是在函数被调用时确定的。
判断this,只需要看一件事:这个函数是怎么被调用的(调用点)。
唯一的例外是箭头函数——它根本没有自己的 this。
二、五种绑定规则
规则 1:默认绑定(独立函数调用)
function foo() {
console.log(this);
}
foo(); // 非严格模式: globalThis (浏览器 window / Node global)
// 严格模式: undefined
'use strict';
function foo() { console.log(this); }
foo(); // undefined
顶层 this 的值取决于环境:
| 环境 | 顶层 this |
|---|---|
浏览器 <script> |
window |
ES Module (.mjs / type="module") |
undefined |
CommonJS 模块 (Node .js) |
module.exports(即 {}) |
| 函数内(非严格) | globalThis |
| 函数内(严格) | undefined |
| class 内部 | 永远严格模式 |
规则 2:隐式绑定(方法调用)
看调用时最后一个点前面是谁。
const obj = {
name: 'obj',
foo() { console.log(this.name); }
};
obj.foo(); // 'obj'
多层嵌套只看最后一层:
const a = { name: 'a', b: { name: 'b', foo() { console.log(this.name); } } };
a.b.foo(); // 'b' ← 不是 'a'
方括号访问同理:
a.b['foo'](); // 'b'
规则 3:显式绑定(call / apply / bind)
function greet(greeting, punct) {
console.log(greeting + ', ' + this.name + punct);
}
const me = { name: 'Alice' };
greet.call(me, 'Hi', '!'); // 逐个传参
greet.apply(me, ['Hi', '!']); // 数组传参 (Apply → Array)
const bound = greet.bind(me, 'Hi'); // 返回新函数 + 支持柯里化
bound('!');
bind 的三个特性:
- 返回一个新函数,不立即执行
- 支持参数预置(柯里化)
- 硬绑定:绑定后无法再被
call/apply改变
const hard = greet.bind(me);
hard.call({ name: 'Bob' }, 'Yo', '!'); // 依然是 Alice
传 null / undefined 的坑:
function foo() { console.log(this); }
foo.call(null); // 非严格: globalThis(被替换了!) 严格: null
安全做法——DMZ 空对象:
const DMZ = Object.create(null); // 比 {} 更干净,无原型
foo.apply(DMZ, [1, 2]);
原始值会被装箱(仅非严格模式):
function f() { console.log(typeof this); }
f.call(1); // 'object' (Number 包装对象)
// 严格模式下: 'number'
规则 4:new 绑定
new Foo() 做了四件事:
- 创建一个新的空对象
- 将该对象的
[[Prototype]]指向Foo.prototype - 以该对象为
this执行Foo - 如果
Foo没有返回一个对象,则返回这个新对象
function Person(name) {
this.name = name;
// return { name: 'hacked' }; // 若返回对象,则 new 的结果是它
// return 42; // 返回原始值 → 被忽略,仍返回 this
}
const p = new Person('Alice'); // p.name === 'Alice'
规则 5:箭头函数(词法绑定)
箭头函数没有自己的 this,它继承定义时所在外层作用域的 this,且永久不可改变。
const obj = {
name: 'obj',
regular() {
const arrow = () => console.log(this.name);
arrow(); // 'obj' ← 继承 regular 的 this
},
arrowMethod: () => console.log(this?.name) // ⚠️ 继承的是模块/全局的 this
};
obj.regular(); // 'obj'
obj.arrowMethod(); // undefined ← 经典陷阱!
箭头函数无法被改变:
const arrow = () => console.log(this);
arrow.call({ a: 1 }); // call 无效
arrow.bind({ a: 1 })(); // bind 无效
new arrow(); // TypeError: arrow is not a constructor
箭头函数也没有 arguments、super、new.target。
三、优先级
new 绑定 > 显式绑定(bind/call/apply) > 隐式绑定(obj.fn) > 默认绑定
箭头函数不在此列——它压根不参与,直接看词法作用域。
验证 new > bind:
function Foo(a) { this.a = a; }
const obj = {};
const B = Foo.bind(obj);
B(2); // obj.a === 2 (bind 生效)
const b = new B(3);// b.a === 3, obj.a 仍是 2 ← new 赢了
这正是
bindpolyfill 里必须判断new.target的原因。
四、隐式丢失:最常见的 Bug 来源
1. 赋值给变量
const obj = { name: 'obj', foo() { console.log(this?.name); } };
const fn = obj.foo; // 只拿到了函数引用,丢掉了 obj
fn(); // undefined
2. 作为回调传递
setTimeout(obj.foo, 100); // 丢失
[1].forEach(obj.foo); // 丢失
btn.addEventListener('click', obj.foo); // this 变成 btn
3. 解构
const { foo } = obj;
foo(); // 丢失
这就是 React 中
const { setState } = this会出问题、
以及 Vue3 用ref而非this的部分原因。
4. 间接引用
const o1 = { name: 'o1', foo() { console.log(this.name); } };
const o2 = { name: 'o2' };
o2.foo = o1.foo;
o2.foo(); // 'o2' (正常隐式绑定)
(o2.foo = o1.foo)(); // 赋值表达式返回函数本身 → 默认绑定 → undefined/报错
5. 解决方案汇总
// ① bind
setTimeout(obj.foo.bind(obj), 100);
// ② 箭头函数包裹(推荐,可读性好)
setTimeout(() => obj.foo(), 100);
// ③ 数组方法的 thisArg 参数
[1, 2].forEach(obj.foo, obj);
// ④ class field 箭头函数(见下文)
支持 thisArg 的数组方法:
forEach map filter some every find findIndex findLast findLastIndex flatMap
不支持的: reduce reduceRight sort(它们的第二参数另有用途)
五、Class 中的 this
类体内永远是严格模式
class Counter {
count = 0;
inc() { this.count++; } // 原型方法 → this 会丢失
dec = () => { this.count--; }; // 类字段箭头函数 → this 永久绑定实例
}
const c = new Counter();
const { inc, dec } = c;
dec(); // ✅ 正常
inc(); // ❌ TypeError: Cannot read properties of undefined
两者的本质区别:
原型方法 inc() |
类字段 dec = () => {} |
|
|---|---|---|
| 存放位置 | Counter.prototype |
每个实例上 |
| 内存 | 共享一份 | 每个实例一份(内存开销大) |
this |
调用时决定 | 定义时锁死为实例 |
可被 call 改变 |
是 | 否 |
可被子类 super 调用 |
是 | 否 |
推荐做法: 默认用原型方法,只在需要作为回调传递时用箭头字段,或在构造器中 bind:
class Counter {
constructor() {
this.inc = this.inc.bind(this); // 等价效果
}
}
静态方法中的 this
class A {
static create() { return new this(); } // this === 类本身,支持子类继承
}
class B extends A {}
B.create(); // 返回 B 的实例,而非 A
继承与 super
class Base {
constructor() { this.type = 'base'; }
hello() { console.log('base hello'); }
}
class Child extends Base {
constructor() {
// console.log(this); // ❌ ReferenceError: 必须先调用 super()
super(); // 派生类中,super() 之前 this 处于 TDZ
console.log(this.type);// 'base'
}
hello() {
super.hello(); // super 方法调用时,this 仍是当前实例
}
}
私有字段的坑
class A {
#secret = 1;
get() { return this.#secret; }
}
const a = new A();
a.get.call({}); // TypeError: Cannot read private member #secret
// 私有字段是"品牌检查",this 必须是真实实例
六、各类场景速查
DOM 事件
btn.addEventListener('click', function () {
console.log(this); // === event.currentTarget (btn)
});
btn.addEventListener('click', () => {
console.log(this); // 外层作用域的 this,不是 btn
});
handleEvent 特殊接口(传对象而非函数):
const handler = {
count: 0,
handleEvent(e) { console.log(this.count++, e.type); } // this === handler
};
btn.addEventListener('click', handler);
内联 onclick="..." 属性中的 this 也是元素本身。
定时器
setTimeout(function () {
console.log(this);
// 浏览器非严格: window
// Node: Timeout 对象
}, 0);
getter / setter
const obj = {
_v: 1,
get v() { return this._v; } // this 是访问该属性的对象
};
Proxy 中的 this 泄漏
const target = { greet() { return this; } };
const proxy = new Proxy(target, {});
proxy.greet() === proxy; // true —— this 是 proxy,不是 target
// 若 target 用了私有字段或内部槽(如 Map/Set),会报错
标签模板 / 生成器 / async
async function f() { return this; } // 与普通函数规则一致
function* g() { yield this; } // 同上
七、手写实现(面试高频)
Function.prototype.myCall = function (context, ...args) {
if (typeof this !== 'function') throw new TypeError('not a function');
context = context ?? globalThis;
if (Object(context) !== context) context = Object(context); // 原始值装箱
const key = Symbol('fn'); // Symbol 避免属性名冲突
context[key] = this;
try {
return context[key](...args);
} finally {
delete context[key];
}
};
Function.prototype.myApply = function (context, args = []) {
return this.myCall(context, ...args);
};
Function.prototype.myBind = function (context, ...preArgs) {
const fn = this;
if (typeof fn !== 'function') throw new TypeError('not a function');
function bound(...args) {
// 关键:new 调用时忽略 context,使用新建的 this
return fn.apply(
new.target ? this : context,
[...preArgs, ...args]
);
}
// 维持原型链,使 instanceof 正确
bound.prototype = Object.create(fn.prototype || null);
return bound;
};
// 手写 new
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype);
const ret = Ctor.apply(obj, args);
return (ret !== null && (typeof ret === 'object' || typeof ret === 'function'))
? ret
: obj;
}
八、综合练习(先自己作答)
var name = 'global'; // 注意:必须是 var,let/const 不挂载到 window
const obj = {
name: 'obj',
f1() { console.log(this.name); },
f2: () => console.log(this?.name),
f3() {
return function () { console.log(this?.name); };
},
f4() {
return () => console.log(this.name);
}
};
obj.f1(); // ?
obj.f2(); // ?
obj.f3()(); // ?
obj.f4()(); // ?
const g = obj.f1;
g(); // ?
obj.f1.call({ name: 'x' }); // ?
new obj.f1(); // ?
答案
obj.f1() → 'obj' 隐式绑定
obj.f2() → 'global'/undefined 箭头函数继承顶层 this
obj.f3()() → 'global'/undefined 返回的函数独立调用,默认绑定
obj.f4()() → 'obj' 箭头函数继承 f4 的 this
g() → 'global'/undefined 隐式丢失
obj.f1.call({...})→ 'x' 显式绑定
new obj.f1() → undefined new 绑定,新对象上没有 name
(浏览器非严格模式下顶层 this 是 window,故为 'global';ESM/严格模式下为 undefined 或报错)
九、终极判断流程图
函数是箭头函数?
├─ 是 → this = 定义时外层作用域的 this(向上查找,不可改变)
└─ 否 ↓
用 new 调用? ──→ 是 → this = 新创建的对象
↓ 否
用 call/apply/bind 调用? ──→ 是 → this = 指定的对象
↓ 否 (非严格下 null/undefined 会替换为 globalThis)
通过 obj.fn() 调用? ──→ 是 → this = obj(只看最后一个点)
↓ 否
严格模式? ──→ 是 → this = undefined
└→ 否 → this = globalThis
十、最佳实践
- 优先用箭头函数写回调,避免
this丢失 - 对象方法用简写语法
foo() {},不要用箭头函数(除非确实想要外层this) - class 中默认用原型方法,需要传递为回调时才用箭头字段或构造器
bind - 全项目开启严格模式 / 使用 ESM,让
this错误尽早暴露为undefined报错 - TypeScript 用户:开启
noImplicitThis,并善用显式this参数:function handler(this: HTMLButtonElement, e: MouseEvent) { /* ... */ } - 不确定时,打印
console.log(this)—— 比推理快十倍
使用animateTo循环播放动画时刷新状态变量无法刷新UI
问题现象
在使用animateTo方法实现循环动画的过程中,如果希望在每次动画播放时通过更新状态变量来刷新UI,可能会遇到UI无法动态响应更新的问题。例如,即使在事件中配置状态变量changeNum每次播放增加1,并设置动画循环播放3次,实际的UI显示仅更新一次,而onFinish的回调中changeNum仅记录1。
@Entry
@Component
struct AnimateToExample {
@State widthSize: number = 250;
@State heightSize: number = 100;
uiContext: UIContext | undefined = undefined;
@State changeNum: number = 0;
aboutToAppear() {
this.uiContext = this.getUIContext();
if (!this.uiContext) {
console.warn('no uiContext');
return;
}
}
playLrc() {
this.uiContext?.animateTo({
duration: 2000,
curve: Curve.EaseOut,
iterations: 3,
playMode: PlayMode.Normal,
onFinish: () => {
console.info('play end');
}
}, () => {
this.widthSize = 300;
this.heightSize = 60;
this.changeNum++;
});
}
build() {
Column() {
Button(this.changeNum.toString())
.width(this.widthSize)
.height(this.heightSize)
.margin(30)
.onClick(() => {
this.playLrc();
});
}.width('100%').margin({ top: 5 });
}
}
问题效果预览:
点击放大
背景知识
animateTo:提供animateTo接口来指定由于闭包代码导致的状态变化插入过渡动效。
关键帧动画 (keyframeAnimateTo):在UIContext中提供keyframeAnimateTo接口来指定若干个关键帧状态,实现分段的动画。
解决方案
显示动画的参数iterations表示动画的执行次数,并不代表闭包函数里面的逻辑执行次数,所以问题代码中iterations:3并不会使得修改宽高动画执行3次,需要使用关键帧动画。
使用关键帧动画,可以分段执行动画逻辑。在动画结束后在onFinish回调中递归执行动画,从而实现预期效果。
@Entry
@Component
struct AnimateToExample {
@State widthSize: number = 250;
@State heightSize: number = 100;
uiContext: UIContext | undefined = undefined;
@State changeNum: number = 0;
aboutToAppear() {
this.uiContext = this.getUIContext();
};
playLrc() {
// 使用关键帧动画
this.uiContext?.keyframeAnimateTo({
iterations: 1,
onFinish: () => {
if (this.changeNum % 3 !== 0) {
this.playLrc();
}
}
}, [
{
// 第一段关键帧动画
duration: 800,
event: () => {
this.widthSize = 300;
this.heightSize = 60;
this.changeNum++;
}
},
// 第二段关键帧动画
{
duration: 500,
event: () => {
this.widthSize = 250;
this.heightSize = 100;
}
}
]);
}
build() {
Column() {
Button(this.changeNum.toString())
.width(this.widthSize)
.height(this.heightSize)
.margin(30)
.onClick(() => {
this.playLrc();
});
}.width('100%').margin({ top: 5 });
}
}
https://sites.google.com/view/6hnwoa03/home
https://sites.google.com/view/2bdovq40/home
https://sites.google.com/view/3ykzjl65/home
https://sites.google.com/view/3ypjlx51/home
https://sites.google.com/view/2vecse29/home
https://sites.google.com/view/8wjmoz24/home
https://sites.google.com/view/6anaaz10/home
https://sites.google.com/view/6vztlx87/home
https://sites.google.com/view/9hxqwy10/home
https://sites.google.com/view/6emmnf53/home
https://sites.google.com/view/9qgmon96/home
https://sites.google.com/view/8vqblh76/home
https://sites.google.com/view/5repzi61/home
https://sites.google.com/view/6iqxbj96/home
https://sites.google.com/view/4fiyto32/home
https://sites.google.com/view/1uimwi96/home
https://sites.google.com/view/5ibqaj17/home
https://sites.google.com/view/9nqdyq84/home
https://sites.google.com/view/4lwyyd65/home
https://sites.google.com/view/3gawuk38/home
https://sites.google.com/view/5lgzag04/home
https://sites.google.com/view/1imonc99/home
https://sites.google.com/view/9xyukm01/home
https://sites.google.com/view/6nalbh77/home
https://sites.google.com/view/2ypecr06/home
https://sites.google.com/view/5zdody20/home
https://sites.google.com/view/6wkgom88/home
https://sites.google.com/view/8zwqqi53/home
https://sites.google.com/view/0qyluw74/home
https://sites.google.com/view/5aphjd68/home
https://sites.google.com/view/2sacyn38/home
https://sites.google.com/view/8dbjrm35/home
https://sites.google.com/view/6unuwk46/home
https://sites.google.com/view/5hrlie99/home
https://sites.google.com/view/6mxmyz56/home
https://sites.google.com/view/9gdzzy11/home
https://sites.google.com/view/3wkfon30/home
https://sites.google.com/view/7jesbl97/home
https://sites.google.com/view/7xllml61/home
https://sites.google.com/view/3mpkjx30/home
https://sites.google.com/view/6idizy90/home
https://sites.google.com/view/1czsip30/home
https://sites.google.com/view/3ezdwf97/home
https://sites.google.com/view/1ydubh84/home
https://sites.google.com/view/4cvedy61/home
https://sites.google.com/view/7yyumz73/home
https://sites.google.com/view/5pkquw00/home
https://sites.google.com/view/7dceex86/home
https://sites.google.com/view/3gnvyg34/home
https://sites.google.com/view/2ehwht60/home
https://sites.google.com/view/3ocyay63/home
https://sites.google.com/view/6uizgx22/home
https://sites.google.com/view/3cgblb20/home
https://sites.google.com/view/3guucg59/home
https://sites.google.com/view/9cdybx22/home
https://sites.google.com/view/7giaxy95/home
https://sites.google.com/view/3qkkfi06/home
https://sites.google.com/view/9goqyg50/home
https://sites.google.com/view/6sfntm95/home
https://sites.google.com/view/8hekup65/home
https://sites.google.com/view/8dypsx49/home
https://sites.google.com/view/1emhrm38/home
https://sites.google.com/view/1yyqhu29/home
https://sites.google.com/view/5heoek19/home
https://sites.google.com/view/3tvkkw85/home
https://sites.google.com/view/4dqdwv87/home
https://sites.google.com/view/2tsotl60/home
https://sites.google.com/view/7mpwib24/home
https://sites.google.com/view/9dlwvj94/home
https://sites.google.com/view/3hggdc60/home
https://sites.google.com/view/3vhyat21/home
https://sites.google.com/view/7vyebf30/home
https://sites.google.com/view/4wnnbn37/home
https://sites.google.com/view/6mjlkw77/home
https://sites.google.com/view/0jcihr79/home
https://sites.google.com/view/6lpxba81/home
https://sites.google.com/view/7nmcwv30/home
https://sites.google.com/view/3ztaut28/home
https://sites.google.com/view/1ecdeq22/home
https://sites.google.com/view/0dgpba93/home
https://sites.google.com/view/1aqqqo25/home
https://sites.google.com/view/1raxlw38/home
https://sites.google.com/view/3snsha82/home
https://sites.google.com/view/5uwlrm29/home
https://sites.google.com/view/4feaqq70/home
https://sites.google.com/view/3nlhqj52/home
https://sites.google.com/view/1pidkm14/home
https://sites.google.com/view/2wqdnc45/home
https://sites.google.com/view/1iwyhj71/home
https://sites.google.com/view/8evxkf88/home
https://sites.google.com/view/5zbhhn62/home
https://sites.google.com/view/9hjcnp07/home
https://sites.google.com/view/3mxnek99/home
https://sites.google.com/view/9bmsbt04/home
https://sites.google.com/view/2lezmt81/home
https://sites.google.com/view/6qmqey84/home
https://coub.com/view/kvpoylkwik
https://coub.com/view/i77t3xznqp
https://coub.com/view/f7trec03b8
https://coub.com/view/3n90r2j0rd
https://coub.com/view/y57xd610co
https://coub.com/view/eaephw8q6a
https://coub.com/view/hrrjoj4h9l
https://coub.com/view/wsjrcxld3n
https://coub.com/view/npkzo9bhsy
https://coub.com/view/e684ftu7dh
https://coub.com/view/3o0ro925t2
https://coub.com/view/fnl2q6kiht
https://coub.com/view/n99qs782yl
https://coub.com/view/1ondlagd1i
https://coub.com/view/9z6hsd1v17
https://coub.com/view/10d32yhsna
https://coub.com/view/ouwb3ocnhh
https://coub.com/view/xdc8nn4avc
https://coub.com/view/09w1hw0sip
https://coub.com/view/zlwigv6w8f
https://coub.com/view/j3r71lim3x
https://coub.com/view/2430o5fdrc
https://coub.com/view/ujctsdcdwy
https://coub.com/view/0buxtgmwxv
https://coub.com/view/377gnonlc2
https://coub.com/view/2nxu03hy9c
https://coub.com/view/qc7d9vj1v0
https://coub.com/view/3mqfz4b3fw
https://coub.com/view/g7ijtiaan7
https://coub.com/view/5inj7wcs41
https://coub.com/view/re695cu3l0
https://coub.com/view/qk2gxc95lg
https://coub.com/view/1g97y8qqtc
https://coub.com/view/wbze3qybuc
https://coub.com/view/ht8lrsgyup
https://coub.com/view/uwy6yesicp
https://coub.com/view/8xz2oua5mz
https://coub.com/view/va9tyn2k4l
https://coub.com/view/5kamzbr4mi
https://coub.com/view/9slmznc3ol
https://coub.com/view/dnhe4x7otv
https://coub.com/view/libk78392s
https://coub.com/view/2splvx7ais
https://coub.com/view/0fqk03fwlz
https://coub.com/view/u3but76ukt
https://coub.com/view/2xrg8kdx5o
https://coub.com/view/iqeb7cwj8k
https://coub.com/view/lx86s7cfpr
https://coub.com/view/qin8evf0sk
https://coub.com/view/ul1dmz1db2
问题现象
在使用animateTo方法实现循环动画的过程中,如果希望在每次动画播放时通过更新状态变量来刷新UI,可能会遇到UI无法动态响应更新的问题。例如,即使在事件中配置状态变量changeNum每次播放增加1,并设置动画循环播放3次,实际的UI显示仅更新一次,而onFinish的回调中changeNum仅记录1。
@Entry
@Component
struct AnimateToExample {
@State widthSize: number = 250;
@State heightSize: number = 100;
uiContext: UIContext | undefined = undefined;
@State changeNum: number = 0;
aboutToAppear() {
this.uiContext = this.getUIContext();
if (!this.uiContext) {
console.warn('no uiContext');
return;
}
}
playLrc() {
this.uiContext?.animateTo({
duration: 2000,
curve: Curve.EaseOut,
iterations: 3,
playMode: PlayMode.Normal,
onFinish: () => {
console.info('play end');
}
}, () => {
this.widthSize = 300;
this.heightSize = 60;
this.changeNum++;
});
}
build() {
Column() {
Button(this.changeNum.toString())
.width(this.widthSize)
.height(this.heightSize)
.margin(30)
.onClick(() => {
this.playLrc();
});
}.width('100%').margin({ top: 5 });
}
}
问题效果预览:
点击放大
背景知识
animateTo:提供animateTo接口来指定由于闭包代码导致的状态变化插入过渡动效。
关键帧动画 (keyframeAnimateTo):在UIContext中提供keyframeAnimateTo接口来指定若干个关键帧状态,实现分段的动画。
解决方案
显示动画的参数iterations表示动画的执行次数,并不代表闭包函数里面的逻辑执行次数,所以问题代码中iterations:3并不会使得修改宽高动画执行3次,需要使用关键帧动画。
使用关键帧动画,可以分段执行动画逻辑。在动画结束后在onFinish回调中递归执行动画,从而实现预期效果。
@Entry
@Component
struct AnimateToExample {
@State widthSize: number = 250;
@State heightSize: number = 100;
uiContext: UIContext | undefined = undefined;
@State changeNum: number = 0;
aboutToAppear() {
this.uiContext = this.getUIContext();
};
playLrc() {
// 使用关键帧动画
this.uiContext?.keyframeAnimateTo({
iterations: 1,
onFinish: () => {
if (this.changeNum % 3 !== 0) {
this.playLrc();
}
}
}, [
{
// 第一段关键帧动画
duration: 800,
event: () => {
this.widthSize = 300;
this.heightSize = 60;
this.changeNum++;
}
},
// 第二段关键帧动画
{
duration: 500,
event: () => {
this.widthSize = 250;
this.heightSize = 100;
}
}
]);
}
build() {
Column() {
Button(this.changeNum.toString())
.width(this.widthSize)
.height(this.heightSize)
.margin(30)
.onClick(() => {
this.playLrc();
});
}.width('100%').margin({ top: 5 });
}
}
https://sites.google.com/view/6hnwoa03/home
https://sites.google.com/view/2bdovq40/home
https://sites.google.com/view/3ykzjl65/home
https://sites.google.com/view/3ypjlx51/home
https://sites.google.com/view/2vecse29/home
https://sites.google.com/view/8wjmoz24/home
https://sites.google.com/view/6anaaz10/home
https://sites.google.com/view/6vztlx87/home
https://sites.google.com/view/9hxqwy10/home
https://sites.google.com/view/6emmnf53/home
https://sites.google.com/view/9qgmon96/home
https://sites.google.com/view/8vqblh76/home
https://sites.google.com/view/5repzi61/home
https://sites.google.com/view/6iqxbj96/home
https://sites.google.com/view/4fiyto32/home
https://sites.google.com/view/1uimwi96/home
https://sites.google.com/view/5ibqaj17/home
https://sites.google.com/view/9nqdyq84/home
https://sites.google.com/view/4lwyyd65/home
https://sites.google.com/view/3gawuk38/home
https://sites.google.com/view/5lgzag04/home
https://sites.google.com/view/1imonc99/home
https://sites.google.com/view/9xyukm01/home
https://sites.google.com/view/6nalbh77/home
https://sites.google.com/view/2ypecr06/home
https://sites.google.com/view/5zdody20/home
https://sites.google.com/view/6wkgom88/home
https://sites.google.com/view/8zwqqi53/home
https://sites.google.com/view/0qyluw74/home
https://sites.google.com/view/5aphjd68/home
https://sites.google.com/view/2sacyn38/home
https://sites.google.com/view/8dbjrm35/home
https://sites.google.com/view/6unuwk46/home
https://sites.google.com/view/5hrlie99/home
https://sites.google.com/view/6mxmyz56/home
https://sites.google.com/view/9gdzzy11/home
https://sites.google.com/view/3wkfon30/home
https://sites.google.com/view/7jesbl97/home
https://sites.google.com/view/7xllml61/home
https://sites.google.com/view/3mpkjx30/home
https://sites.google.com/view/6idizy90/home
https://sites.google.com/view/1czsip30/home
https://sites.google.com/view/3ezdwf97/home
https://sites.google.com/view/1ydubh84/home
https://sites.google.com/view/4cvedy61/home
https://sites.google.com/view/7yyumz73/home
https://sites.google.com/view/5pkquw00/home
https://sites.google.com/view/7dceex86/home
https://sites.google.com/view/3gnvyg34/home
https://sites.google.com/view/2ehwht60/home
https://sites.google.com/view/3ocyay63/home
https://sites.google.com/view/6uizgx22/home
https://sites.google.com/view/3cgblb20/home
https://sites.google.com/view/3guucg59/home
https://sites.google.com/view/9cdybx22/home
https://sites.google.com/view/7giaxy95/home
https://sites.google.com/view/3qkkfi06/home
https://sites.google.com/view/9goqyg50/home
https://sites.google.com/view/6sfntm95/home
https://sites.google.com/view/8hekup65/home
https://sites.google.com/view/8dypsx49/home
https://sites.google.com/view/1emhrm38/home
https://sites.google.com/view/1yyqhu29/home
https://sites.google.com/view/5heoek19/home
https://sites.google.com/view/3tvkkw85/home
https://sites.google.com/view/4dqdwv87/home
https://sites.google.com/view/2tsotl60/home
https://sites.google.com/view/7mpwib24/home
https://sites.google.com/view/9dlwvj94/home
https://sites.google.com/view/3hggdc60/home
https://sites.google.com/view/3vhyat21/home
https://sites.google.com/view/7vyebf30/home
https://sites.google.com/view/4wnnbn37/home
https://sites.google.com/view/6mjlkw77/home
https://sites.google.com/view/0jcihr79/home
https://sites.google.com/view/6lpxba81/home
https://sites.google.com/view/7nmcwv30/home
https://sites.google.com/view/3ztaut28/home
https://sites.google.com/view/1ecdeq22/home
https://sites.google.com/view/0dgpba93/home
https://sites.google.com/view/1aqqqo25/home
https://sites.google.com/view/1raxlw38/home
https://sites.google.com/view/3snsha82/home
https://sites.google.com/view/5uwlrm29/home
https://sites.google.com/view/4feaqq70/home
https://sites.google.com/view/3nlhqj52/home
https://sites.google.com/view/1pidkm14/home
https://sites.google.com/view/2wqdnc45/home
https://sites.google.com/view/1iwyhj71/home
https://sites.google.com/view/8evxkf88/home
https://sites.google.com/view/5zbhhn62/home
https://sites.google.com/view/9hjcnp07/home
https://sites.google.com/view/3mxnek99/home
https://sites.google.com/view/9bmsbt04/home
https://sites.google.com/view/2lezmt81/home
https://sites.google.com/view/6qmqey84/home
https://coub.com/view/kvpoylkwik
https://coub.com/view/i77t3xznqp
https://coub.com/view/f7trec03b8
https://coub.com/view/3n90r2j0rd
https://coub.com/view/y57xd610co
https://coub.com/view/eaephw8q6a
https://coub.com/view/hrrjoj4h9l
https://coub.com/view/wsjrcxld3n
https://coub.com/view/npkzo9bhsy
https://coub.com/view/e684ftu7dh
https://coub.com/view/3o0ro925t2
https://coub.com/view/fnl2q6kiht
https://coub.com/view/n99qs782yl
https://coub.com/view/1ondlagd1i
https://coub.com/view/9z6hsd1v17
https://coub.com/view/10d32yhsna
https://coub.com/view/ouwb3ocnhh
https://coub.com/view/xdc8nn4avc
https://coub.com/view/09w1hw0sip
https://coub.com/view/zlwigv6w8f
https://coub.com/view/j3r71lim3x
https://coub.com/view/2430o5fdrc
https://coub.com/view/ujctsdcdwy
https://coub.com/view/0buxtgmwxv
https://coub.com/view/377gnonlc2
https://coub.com/view/2nxu03hy9c
https://coub.com/view/qc7d9vj1v0
https://coub.com/view/3mqfz4b3fw
https://coub.com/view/g7ijtiaan7
https://coub.com/view/5inj7wcs41
https://coub.com/view/re695cu3l0
https://coub.com/view/qk2gxc95lg
https://coub.com/view/1g97y8qqtc
https://coub.com/view/wbze3qybuc
https://coub.com/view/ht8lrsgyup
https://coub.com/view/uwy6yesicp
https://coub.com/view/8xz2oua5mz
https://coub.com/view/va9tyn2k4l
https://coub.com/view/5kamzbr4mi
https://coub.com/view/9slmznc3ol
https://coub.com/view/dnhe4x7otv
https://coub.com/view/libk78392s
https://coub.com/view/2splvx7ais
https://coub.com/view/0fqk03fwlz
https://coub.com/view/u3but76ukt
https://coub.com/view/2xrg8kdx5o
https://coub.com/view/iqeb7cwj8k
https://coub.com/view/lx86s7cfpr
https://coub.com/view/qin8evf0sk
https://coub.com/view/ul1dmz1db2
如何实现一个自定义高度的底部弹窗
问题现象
需要实现的底部弹窗可以设置一个最大高度。同时,实现的底部弹窗需要从下往上出现。
如果弹窗内部视图的高度超过了这个最大高度,弹窗内部的视图就以这个自定义最大高度进行滚动展示;
如果弹窗内部视图的高度没有超过这个最大高度,那么弹窗内部视图就完全展示;
背景知识
组件的constraintSize属性用于设置约束尺寸,组件布局时,进行尺寸范围限制。
对于组件的显示方式,转场主要通过transition属性配置转场参数,在组件插入和删除时显示过渡动效,主要用于容器组件中的子组件插入和删除时,提升用户体验。
解决方案
首先,通过使用容器组件的constraintSize属性对视图进行高度限制,constraintSize的优先级高于Width和Height,constraintSize里面的maxHeight小于Height时,就会滚动展示。接着,实现弹窗从底部向上显示,通过设置组件transition属性的转场参数。
示例代码如下:
@CustomDialog
struct CustomDialogExample {
// 最大可滚动区域高度
maxScrollHeight: number = 70;
// 视图是否显示,用于底部弹窗出现与消失时的动效处理
@Link showFlag: Visibility;
controller: CustomDialogController;
scroller: Scroller = new Scroller;
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
build() {
Column({ space: 5 }) {
RelativeContainer() {
Text('标题')
.fontSize(17)
.fontColor(Color.Black)
.fontWeight(FontWeight.Bold)
.alignRules({
middle: { anchor: 'container', align: HorizontalAlign.Center },
center: { anchor: 'container', align: VerticalAlign.Center }
});
}
.width('100%')
.height(48);
Scroll(this.scroller) {
Column() {
ForEach(this.arr, (item: number) => {
Text(item.toString())
.width('90%')
.height(50)
.backgroundColor(0xFFFFFF)
.borderColor(Color.Black)
.fontSize(16)
.textAlign(TextAlign.Center);
Divider()
.vertical(false)
.strokeWidth(0.5)
.width('60%');
}, (item: string) => item);
}.width('100%');
}
.edgeEffect(EdgeEffect.Spring)
.scrollSnap({
snapAlign: ScrollSnapAlign.START,
snapPagination: 400,
enableSnapToStart: true,
enableSnapToEnd: true
})
// 使用constraintSize方法可以设置约束尺寸,组件布局时,进行尺寸范围限制
.constraintSize({ maxHeight: this.maxScrollHeight + '%' });
}
.justifyContent(FlexAlign.Center)
.backgroundColor('#f5f5f5')
.borderRadius({
topLeft: '16',
topRight: '16'
})
.visibility(this.showFlag)
.transition(TransitionEffect.OPACITY.animation({ duration: 200 }) // 弹窗出现与消失的动效
.combine(TransitionEffect.translate({ y: 100 })),
(transitionIn: boolean) => {
if (!transitionIn) {
this.controller.close();
}
}
);
}
}
@Entry
@Component
struct CustomDialogDemo {
// 最大可滚动区域高度
@State maxScrollHeight: number = 70;
@State settingDialogShowFlag: Visibility = Visibility.Visible;
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
maxScrollHeight: this.maxScrollHeight,
showFlag: this.settingDialogShowFlag
}),
alignment: DialogAlignment.Bottom,
width: '100%',
// customStyle需要设置为true,否则底部弹窗出现的动效会有问题
customStyle: true,
autoCancel: true,
// 修改点击弹窗外部区域和返回操作时弹窗消失的方式,这里的处理会有一个动效。否则弹窗会以默认的方式消失
onWillDismiss: () => {
this.settingDialogShowFlag = Visibility.Hidden;
}
});
build() {
Column() {
Row({ space: 5 }) {
Row() {
Button('减10')
.onClick(() => {
this.maxScrollHeight -= 10;
});
};
Row() {
Text(this.maxScrollHeight + '%')
.width('40%')
.height(45)
.textAlign(TextAlign.Center)
.backgroundColor('#f5f5f5');
};
Row() {
Button('加10')
.onClick(() => {
this.maxScrollHeight += 10;
});
};
};
Button('打开底部弹窗')
.onClick(() => {
this.settingDialogShowFlag = Visibility.Visible;
this.dialogController.open();
});
}.justifyContent(FlexAlign.Center).width('100%').height('100%');
}
}
https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home
问题现象
需要实现的底部弹窗可以设置一个最大高度。同时,实现的底部弹窗需要从下往上出现。
如果弹窗内部视图的高度超过了这个最大高度,弹窗内部的视图就以这个自定义最大高度进行滚动展示;
如果弹窗内部视图的高度没有超过这个最大高度,那么弹窗内部视图就完全展示;
背景知识
组件的constraintSize属性用于设置约束尺寸,组件布局时,进行尺寸范围限制。
对于组件的显示方式,转场主要通过transition属性配置转场参数,在组件插入和删除时显示过渡动效,主要用于容器组件中的子组件插入和删除时,提升用户体验。
解决方案
首先,通过使用容器组件的constraintSize属性对视图进行高度限制,constraintSize的优先级高于Width和Height,constraintSize里面的maxHeight小于Height时,就会滚动展示。接着,实现弹窗从底部向上显示,通过设置组件transition属性的转场参数。
示例代码如下:
@CustomDialog
struct CustomDialogExample {
// 最大可滚动区域高度
maxScrollHeight: number = 70;
// 视图是否显示,用于底部弹窗出现与消失时的动效处理
@Link showFlag: Visibility;
controller: CustomDialogController;
scroller: Scroller = new Scroller;
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
build() {
Column({ space: 5 }) {
RelativeContainer() {
Text('标题')
.fontSize(17)
.fontColor(Color.Black)
.fontWeight(FontWeight.Bold)
.alignRules({
middle: { anchor: 'container', align: HorizontalAlign.Center },
center: { anchor: 'container', align: VerticalAlign.Center }
});
}
.width('100%')
.height(48);
Scroll(this.scroller) {
Column() {
ForEach(this.arr, (item: number) => {
Text(item.toString())
.width('90%')
.height(50)
.backgroundColor(0xFFFFFF)
.borderColor(Color.Black)
.fontSize(16)
.textAlign(TextAlign.Center);
Divider()
.vertical(false)
.strokeWidth(0.5)
.width('60%');
}, (item: string) => item);
}.width('100%');
}
.edgeEffect(EdgeEffect.Spring)
.scrollSnap({
snapAlign: ScrollSnapAlign.START,
snapPagination: 400,
enableSnapToStart: true,
enableSnapToEnd: true
})
// 使用constraintSize方法可以设置约束尺寸,组件布局时,进行尺寸范围限制
.constraintSize({ maxHeight: this.maxScrollHeight + '%' });
}
.justifyContent(FlexAlign.Center)
.backgroundColor('#f5f5f5')
.borderRadius({
topLeft: '16',
topRight: '16'
})
.visibility(this.showFlag)
.transition(TransitionEffect.OPACITY.animation({ duration: 200 }) // 弹窗出现与消失的动效
.combine(TransitionEffect.translate({ y: 100 })),
(transitionIn: boolean) => {
if (!transitionIn) {
this.controller.close();
}
}
);
}
}
@Entry
@Component
struct CustomDialogDemo {
// 最大可滚动区域高度
@State maxScrollHeight: number = 70;
@State settingDialogShowFlag: Visibility = Visibility.Visible;
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
maxScrollHeight: this.maxScrollHeight,
showFlag: this.settingDialogShowFlag
}),
alignment: DialogAlignment.Bottom,
width: '100%',
// customStyle需要设置为true,否则底部弹窗出现的动效会有问题
customStyle: true,
autoCancel: true,
// 修改点击弹窗外部区域和返回操作时弹窗消失的方式,这里的处理会有一个动效。否则弹窗会以默认的方式消失
onWillDismiss: () => {
this.settingDialogShowFlag = Visibility.Hidden;
}
});
build() {
Column() {
Row({ space: 5 }) {
Row() {
Button('减10')
.onClick(() => {
this.maxScrollHeight -= 10;
});
};
Row() {
Text(this.maxScrollHeight + '%')
.width('40%')
.height(45)
.textAlign(TextAlign.Center)
.backgroundColor('#f5f5f5');
};
Row() {
Button('加10')
.onClick(() => {
this.maxScrollHeight += 10;
});
};
};
Button('打开底部弹窗')
.onClick(() => {
this.settingDialogShowFlag = Visibility.Visible;
this.dialogController.open();
});
}.justifyContent(FlexAlign.Center).width('100%').height('100%');
}
}
https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home
如何解决打断无限循环动画后,动画失效问题
问题现象
第一次触发无限循环动画后,快速连续多次点击打断该无限循环动画,再次点击就无法看到动画了。
背景知识
animateTo:指定由于闭包代码导致的状态变化插入过渡动效。接口参数有两个,分别是value和event,其中value指定AnimateParam对象(包括时长、Curve等)event为动画的闭包函数,闭包内变量改变产生的属性动画将遵循相同的动画参数。
问题定位
对动画的执行机制进行排查,确认在动画打断过程中是否存在多次叠加的情况,从而导致动画未能正常显示。
分析结论
动画并未真正消失,而是在每次打断过程中被持续叠加,由于叠加的动画实例过多,彼此之间相互覆盖或干扰,导致在视觉上表现不明显,从而给人以动画消失的错觉。
修改结论
首先,应设置一个duration为0的动画,用于确保在每次动画被中断时,能够清除前一次的动画实例,避免动画叠加;该动画的属性值需设置为一个与上一次动画最终状态不同的指定值,以确保状态的更新和正确清除。随后,再创建一个用于实现所需初始动画效果的动画实例,以确保动画表现符合预期。
if (this.isRecording) {
// 设置一个duration为0的动画停掉上一次动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
// 这里的opacityValue不可以和上一次设置的终止0.1相同
this.opacityValue = 0.2;
});
this.isRecording = false;
// 再创建需要的动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
this.opacityValue = 1;
});
} else {
this.isRecording = true;
this.getUIContext().animateTo({ duration: 1500, iterations: -1, }, () => {
this.opacityValue = 0.1;
});
}
完整示例参考如下:
@Entry
@Component
struct Page {
@State opacityValue: number = 1;
@State isRecording: boolean = false;
build() {
Row() {
Column() {
Text(this.isRecording ? 'Hello World' : 'Welcome')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.opacity(this.opacityValue)
.textAlign(TextAlign.Center)
.fontColor('#000')
.onClick(() => {
if (this.isRecording) {
// 设置一个duration为0的动画停掉上一次动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
// 这里的opacityValue不可以和上一次设置的终止0.1相同
this.opacityValue = 0.2;
});
this.isRecording = false;
// 再创建需要的动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
this.opacityValue = 1;
});
} else {
this.isRecording = true;
this.getUIContext().animateTo({ duration: 1500, iterations: -1, }, () => {
this.opacityValue = 0.1;
});
}
});
}
.width('100%');
}
.height('100%');
}
}
https://sites.google.com/view/0kqtje63/home
https://sites.google.com/view/2yuici15/home
https://sites.google.com/view/2xujlh37/home
https://sites.google.com/view/3ahalq09/home
https://sites.google.com/view/3tqjcf61/home
https://sites.google.com/view/6jajrr88/home
https://sites.google.com/view/9ghecz74/home
https://sites.google.com/view/8qlhiy40/home
https://sites.google.com/view/6sknwb20/home
https://sites.google.com/view/2bwlws24/home
https://sites.google.com/view/2blczd15/home
https://sites.google.com/view/1lmiwz57/home
https://sites.google.com/view/7htwsc76/home
https://sites.google.com/view/3chktr81/home
https://sites.google.com/view/1rejbt07/home
https://sites.google.com/view/6zfyyu19/home
https://sites.google.com/view/9pcdic70/home
https://sites.google.com/view/0wcdbm94/home
https://sites.google.com/view/2yddpg19/home
https://sites.google.com/view/7sevuo10/home
https://sites.google.com/view/6incag10/home
https://sites.google.com/view/4bbfak54/home
https://sites.google.com/view/1wioqp25/home
https://sites.google.com/view/8vhssf95/home
https://sites.google.com/view/9eekvo81/home
https://sites.google.com/view/8qprfl23/home
https://sites.google.com/view/4wvrav40/home
https://sites.google.com/view/8legom73/home
https://sites.google.com/view/2kwmgy35/home
https://sites.google.com/view/0nkzdh51/home
https://sites.google.com/view/2kucvr93/home
https://sites.google.com/view/3csonu11/home
https://sites.google.com/view/3cajfi21/home
https://sites.google.com/view/2ysddp77/home
https://sites.google.com/view/5cqzyg62/home
https://sites.google.com/view/6waqdl39/home
https://sites.google.com/view/9qwaeq72/home
https://sites.google.com/view/1yknjw85/home
https://sites.google.com/view/6vaarx10/home
https://sites.google.com/view/7ltamt18/home
https://sites.google.com/view/3nrfrt86/home
https://sites.google.com/view/8rzfds48/home
https://sites.google.com/view/2vrecv27/home
https://sites.google.com/view/8pgryd86/home
https://sites.google.com/view/9ukcxw90/home
https://sites.google.com/view/6szkbf52/home
https://sites.google.com/view/1mvila90/home
https://sites.google.com/view/9eytcv61/home
https://sites.google.com/view/4cnnur37/home
https://sites.google.com/view/5uvpgz70/home
https://sites.google.com/view/9wnykm74/home
https://sites.google.com/view/1maebc71/home
https://sites.google.com/view/7bjvba88/home
https://sites.google.com/view/6yxdgq20/home
https://sites.google.com/view/6oplhm39/home
https://sites.google.com/view/0gslqp42/home
https://sites.google.com/view/2fobzs04/home
https://sites.google.com/view/5geezf47/home
https://sites.google.com/view/6damyk90/home
https://sites.google.com/view/8txifb96/home
https://sites.google.com/view/4uycua91/home
https://sites.google.com/view/4qahte14/home
https://sites.google.com/view/3hmwmu69/home
https://sites.google.com/view/2dqgbe78/home
https://sites.google.com/view/4ogacg47/home
https://sites.google.com/view/0zvppg85/home
https://sites.google.com/view/8bsjqs81/home
https://sites.google.com/view/0yhduz67/home
https://sites.google.com/view/0nvpde36/home
https://sites.google.com/view/4svrfc10/in%C3%ADcio
https://sites.google.com/view/4gjwxj46/in%C3%ADcio
https://sites.google.com/view/0xejpo69/in%C3%ADcio
https://sites.google.com/view/4eizzb39/in%C3%ADcio
https://sites.google.com/view/8cglkg37/in%C3%ADcio
https://sites.google.com/view/2poltf08/in%C3%ADcio
https://sites.google.com/view/6ggdzs74/in%C3%ADcio
https://sites.google.com/view/7rpmpu57/in%C3%ADcio
https://sites.google.com/view/6gkcjo69/in%C3%ADcio
https://sites.google.com/view/5jgxxq89/in%C3%ADcio
问题现象
第一次触发无限循环动画后,快速连续多次点击打断该无限循环动画,再次点击就无法看到动画了。
背景知识
animateTo:指定由于闭包代码导致的状态变化插入过渡动效。接口参数有两个,分别是value和event,其中value指定AnimateParam对象(包括时长、Curve等)event为动画的闭包函数,闭包内变量改变产生的属性动画将遵循相同的动画参数。
问题定位
对动画的执行机制进行排查,确认在动画打断过程中是否存在多次叠加的情况,从而导致动画未能正常显示。
分析结论
动画并未真正消失,而是在每次打断过程中被持续叠加,由于叠加的动画实例过多,彼此之间相互覆盖或干扰,导致在视觉上表现不明显,从而给人以动画消失的错觉。
修改结论
首先,应设置一个duration为0的动画,用于确保在每次动画被中断时,能够清除前一次的动画实例,避免动画叠加;该动画的属性值需设置为一个与上一次动画最终状态不同的指定值,以确保状态的更新和正确清除。随后,再创建一个用于实现所需初始动画效果的动画实例,以确保动画表现符合预期。
if (this.isRecording) {
// 设置一个duration为0的动画停掉上一次动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
// 这里的opacityValue不可以和上一次设置的终止0.1相同
this.opacityValue = 0.2;
});
this.isRecording = false;
// 再创建需要的动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
this.opacityValue = 1;
});
} else {
this.isRecording = true;
this.getUIContext().animateTo({ duration: 1500, iterations: -1, }, () => {
this.opacityValue = 0.1;
});
}
完整示例参考如下:
@Entry
@Component
struct Page {
@State opacityValue: number = 1;
@State isRecording: boolean = false;
build() {
Row() {
Column() {
Text(this.isRecording ? 'Hello World' : 'Welcome')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.opacity(this.opacityValue)
.textAlign(TextAlign.Center)
.fontColor('#000')
.onClick(() => {
if (this.isRecording) {
// 设置一个duration为0的动画停掉上一次动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
// 这里的opacityValue不可以和上一次设置的终止0.1相同
this.opacityValue = 0.2;
});
this.isRecording = false;
// 再创建需要的动画
this.getUIContext().animateTo({ duration: 0, iterations: 1, playMode: PlayMode.Normal }, () => {
this.opacityValue = 1;
});
} else {
this.isRecording = true;
this.getUIContext().animateTo({ duration: 1500, iterations: -1, }, () => {
this.opacityValue = 0.1;
});
}
});
}
.width('100%');
}
.height('100%');
}
}
https://sites.google.com/view/0kqtje63/home
https://sites.google.com/view/2yuici15/home
https://sites.google.com/view/2xujlh37/home
https://sites.google.com/view/3ahalq09/home
https://sites.google.com/view/3tqjcf61/home
https://sites.google.com/view/6jajrr88/home
https://sites.google.com/view/9ghecz74/home
https://sites.google.com/view/8qlhiy40/home
https://sites.google.com/view/6sknwb20/home
https://sites.google.com/view/2bwlws24/home
https://sites.google.com/view/2blczd15/home
https://sites.google.com/view/1lmiwz57/home
https://sites.google.com/view/7htwsc76/home
https://sites.google.com/view/3chktr81/home
https://sites.google.com/view/1rejbt07/home
https://sites.google.com/view/6zfyyu19/home
https://sites.google.com/view/9pcdic70/home
https://sites.google.com/view/0wcdbm94/home
https://sites.google.com/view/2yddpg19/home
https://sites.google.com/view/7sevuo10/home
https://sites.google.com/view/6incag10/home
https://sites.google.com/view/4bbfak54/home
https://sites.google.com/view/1wioqp25/home
https://sites.google.com/view/8vhssf95/home
https://sites.google.com/view/9eekvo81/home
https://sites.google.com/view/8qprfl23/home
https://sites.google.com/view/4wvrav40/home
https://sites.google.com/view/8legom73/home
https://sites.google.com/view/2kwmgy35/home
https://sites.google.com/view/0nkzdh51/home
https://sites.google.com/view/2kucvr93/home
https://sites.google.com/view/3csonu11/home
https://sites.google.com/view/3cajfi21/home
https://sites.google.com/view/2ysddp77/home
https://sites.google.com/view/5cqzyg62/home
https://sites.google.com/view/6waqdl39/home
https://sites.google.com/view/9qwaeq72/home
https://sites.google.com/view/1yknjw85/home
https://sites.google.com/view/6vaarx10/home
https://sites.google.com/view/7ltamt18/home
https://sites.google.com/view/3nrfrt86/home
https://sites.google.com/view/8rzfds48/home
https://sites.google.com/view/2vrecv27/home
https://sites.google.com/view/8pgryd86/home
https://sites.google.com/view/9ukcxw90/home
https://sites.google.com/view/6szkbf52/home
https://sites.google.com/view/1mvila90/home
https://sites.google.com/view/9eytcv61/home
https://sites.google.com/view/4cnnur37/home
https://sites.google.com/view/5uvpgz70/home
https://sites.google.com/view/9wnykm74/home
https://sites.google.com/view/1maebc71/home
https://sites.google.com/view/7bjvba88/home
https://sites.google.com/view/6yxdgq20/home
https://sites.google.com/view/6oplhm39/home
https://sites.google.com/view/0gslqp42/home
https://sites.google.com/view/2fobzs04/home
https://sites.google.com/view/5geezf47/home
https://sites.google.com/view/6damyk90/home
https://sites.google.com/view/8txifb96/home
https://sites.google.com/view/4uycua91/home
https://sites.google.com/view/4qahte14/home
https://sites.google.com/view/3hmwmu69/home
https://sites.google.com/view/2dqgbe78/home
https://sites.google.com/view/4ogacg47/home
https://sites.google.com/view/0zvppg85/home
https://sites.google.com/view/8bsjqs81/home
https://sites.google.com/view/0yhduz67/home
https://sites.google.com/view/0nvpde36/home
https://sites.google.com/view/4svrfc10/in%C3%ADcio
https://sites.google.com/view/4gjwxj46/in%C3%ADcio
https://sites.google.com/view/0xejpo69/in%C3%ADcio
https://sites.google.com/view/4eizzb39/in%C3%ADcio
https://sites.google.com/view/8cglkg37/in%C3%ADcio
https://sites.google.com/view/2poltf08/in%C3%ADcio
https://sites.google.com/view/6ggdzs74/in%C3%ADcio
https://sites.google.com/view/7rpmpu57/in%C3%ADcio
https://sites.google.com/view/6gkcjo69/in%C3%ADcio
https://sites.google.com/view/5jgxxq89/in%C3%ADcio
如何解决拖拽功能和长按功能的冲突问题
问题现象
在List组件中,单个ListItem在同时设置拖拽功能与长按功能时,实际运行之后会产生冲突,如何解决单个ListItem拖拽功能和长按功能的冲突问题?
效果预览
点击放大
背景知识
支持统一拖拽提供了一种通过鼠标或手势触屏传递数据的机制,即从一个组件位置拖出数据并将其拖入到另一个组件位置,以触发响应。
长按手势通过长按屏幕触发长按手势事件。
由于拖拽事件和长按手势都需要通过长按来触发,因此这种组合手势类型会出现冲突,例如List组件中每个ListItem都设置单独的长按事件时,拖拽功能和长按手势功能就会出现冲突。
Grid网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。
滑动手势PanGesture,能够提供自定义拖拽事件的功能。
解决方案
由于在ListItem中设置单独的长按事件会与拖拽事件产生冲突,因此建议放弃List本身的拖拽事件,使用滑动手势PanGesture,自行实现拖拽过程的逻辑,然后再通过组合手势的功能,去实现其他的长按事件。
import curves from '@ohos.curves';
@Entry
@Component
struct Page {
// 元素数组
@State numbers: number[] = [];
// 多列
private str: string = '';
row: number = 4;
// 元素数组中最后一个元素的索引
@State lastIndex: number = 0;
@State dragItem: number = -1;
@State scaleItem: number = -1;
item: number = -1;
private dragRefOffsetX: number = 0;
private dragRefOffsetY: number = 0;
@State offsetX: number = 0;
@State offsetY: number = 0;
private FIX_VP_X: number = 108;
private FIX_VP_Y: number = 120;
aboutToAppear() {
for (let i = 1; i <= 36; i++) {
this.numbers.push(i);
}
this.lastIndex = this.numbers.length - 1;
// 多列
for (let i = 0; i < this.row; i++) {
this.str = this.str + '1fr ';
}
}
itemMove(index: number, newIndex: number): void {
console.info('index:' + index + ' newIndex:' + newIndex);
if (!this.isDraggable(newIndex)) {
return;
}
let tmp = this.numbers.splice(index, 1);
this.numbers.splice(newIndex, 0, tmp[0]);
}
// 向下滑
down(index: number): void {
if (!this.isDraggable(index + this.row)) {
return;
}
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
// 多列
this.itemMove(index, index + this.row);
}
// 向下滑(右下角为空)
down2(index: number): void {
if (!this.isDraggable(index + 3)) {
return;
}
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + 3);
}
// 向上滑
up(index: number): void {
if (!this.isDraggable(index - this.row)) {
return;
}
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - this.row);
}
// 向左滑
left(index: number): void {
if (!this.isDraggable(index - 1)) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.itemMove(index, index - 1);
}
// 向右滑
right(index: number): void {
if (!this.isDraggable(index + 1)) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.itemMove(index, index + 1);
}
// 向右下滑
lowerRight(index: number): void {
if (!this.isDraggable(index + this.row + 1)) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + this.row + 1);
}
// 向右上滑
upperRight(index: number): void {
if (!this.isDraggable(index - (this.row - 1))) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - (this.row - 1));
}
// 向左下滑
lowerLeft(index: number): void {
if (!this.isDraggable(index + (this.row - 1))) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + (this.row - 1));
}
// 向左上滑
upperLeft(index: number): void {
if (!this.isDraggable(index - (this.row + 1))) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - (this.row + 1));
}
// 通过元素的索引,控制对应元素是否能移动排序
isDraggable(index: number): boolean {
console.info(index: ${index});
return index > -1; // 恒成立,所有元素均可移动排序
}
build() {
Column() {
Grid() {
ForEach(this.numbers, (item: number) => {
GridItem() {
Text(item + '')
.fontSize(16)
.width('100%')
.textAlign(TextAlign.Center)
.height(100)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
.shadow(this.scaleItem == item ? {
radius: 70,
color: '#15000000',
offsetX: 0,
offsetY: 0
} :
{
radius: 0,
color: '#15000000',
offsetX: 0,
offsetY: 0
})
.animation({ curve: Curve.Sharp, duration: 300 });
}
// 添加震动
.onTouch(() => {
})
.onAreaChange((oldVal, newVal) => {
// 多列
this.FIX_VP_X = Math.round(newVal.width as number);
this.FIX_VP_Y = Math.round(newVal.height as number);
console.info(oldVal:${JSON.stringify(oldVal)});
})
// 指定固定GridItem不响应事件
.hitTestBehavior(this.isDraggable(this.numbers.indexOf(item)) ? HitTestMode.Default : HitTestMode.None)
.scale({ x: this.scaleItem == item ? 1.05 : 1, y: this.scaleItem == item ? 1.05 : 1 })
.zIndex(this.dragItem == item ? 1 : 0)
.translate(this.dragItem == item ? { x: this.offsetX, y: this.offsetY } : { x: 0, y: 0 })
.padding(10)
.gesture(
GestureGroup(GestureMode.Sequence,
LongPressGesture({ repeat: true, duration: 50 })
.onAction((event?: GestureEvent) => {
console.info(event: ${event});
this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
this.scaleItem = item;
});
})
.onActionEnd(() => {
this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
this.scaleItem = -1;
});
}),
PanGesture({ fingers: 1, direction: null, distance: 0 })
.onActionStart(() => {
this.dragItem = item;
this.dragRefOffsetX = 0;
this.dragRefOffsetY = 0;
})
.onActionUpdate((event: GestureEvent) => {
this.offsetY = event.offsetY - this.dragRefOffsetY;
this.offsetX = event.offsetX - this.dragRefOffsetX;
console.info(移动过程中event.offsetY:${event.offsetY},
this.dragRefOffsetY: ${this.dragRefOffsetY}, this.offsetY: ${this.offsetY});
console.info(移动过程中event.offsetX:${event.offsetX},
this.dragRefOffsetX: ${this.dragRefOffsetX}, this.offsetX: ${this.offsetX});
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
let index = this.numbers.indexOf(this.dragItem);
console.info('index= ', index);
if (this.offsetY >= this.FIX_VP_Y / 2 &&
(this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
&& (index + this.row <= this.lastIndex)) {
// 向下滑
this.down(index);
} else if (this.offsetY <= -this.FIX_VP_Y / 2 &&
(this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
&& index - this.row >= 0) {
// 向上滑
this.up(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 &&
(this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
&& !(((index - (this.row - 1)) % this.row == 0) || index == this.lastIndex)) {
// 向右滑
this.right(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 &&
(this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
&& !(index % this.row == 0)) {
// 向左滑
this.left(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& ((index + this.row + 1 <= this.lastIndex && !((index - (this.row - 1)) % this.row == 0)) ||
!((index - (this.row - 1)) % this.row == 0))) {
// 向右下滑
this.lowerRight(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
&& !((index - this.row < 0) || ((index - (this.row - 1)) % this.row == 0))) {
// 向右上滑
this.upperRight(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& (!(index % this.row == 0) && (index + (this.row - 1) <= this.lastIndex))) {
// 向左下滑
this.lowerLeft(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
&& !((index <= this.row - 1) || (index % this.row == 0))) {
// 向左上滑
this.upperLeft(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& (index == this.lastIndex)) {
// 向右下滑(右下角为空)
this.down2(index);
}
});
})
.onActionEnd(() => {
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
this.dragItem = -1;
});
this.getUIContext().animateTo({
curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150
}, () => {
this.scaleItem = -1;
});
})
)
.onCancel(() => {
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
this.dragItem = -1;
});
this.getUIContext().animateTo({
curve: curves.interpolatingSpring(14, 1, 170, 17)
}, () => {
this.scaleItem = -1;
});
})
);
}, (item: number) => item.toString());
}
.width('90%')
.editMode(true)
.scrollBar(BarState.Off)
// 多列
.columnsTemplate(this.str);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
.width('100%').height('100%').backgroundColor('#f1f3f5').padding({ top: 5 });
}
}
https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home
问题现象
在List组件中,单个ListItem在同时设置拖拽功能与长按功能时,实际运行之后会产生冲突,如何解决单个ListItem拖拽功能和长按功能的冲突问题?
效果预览
点击放大
背景知识
支持统一拖拽提供了一种通过鼠标或手势触屏传递数据的机制,即从一个组件位置拖出数据并将其拖入到另一个组件位置,以触发响应。
长按手势通过长按屏幕触发长按手势事件。
由于拖拽事件和长按手势都需要通过长按来触发,因此这种组合手势类型会出现冲突,例如List组件中每个ListItem都设置单独的长按事件时,拖拽功能和长按手势功能就会出现冲突。
Grid网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。
滑动手势PanGesture,能够提供自定义拖拽事件的功能。
解决方案
由于在ListItem中设置单独的长按事件会与拖拽事件产生冲突,因此建议放弃List本身的拖拽事件,使用滑动手势PanGesture,自行实现拖拽过程的逻辑,然后再通过组合手势的功能,去实现其他的长按事件。
import curves from '@ohos.curves';
@Entry
@Component
struct Page {
// 元素数组
@State numbers: number[] = [];
// 多列
private str: string = '';
row: number = 4;
// 元素数组中最后一个元素的索引
@State lastIndex: number = 0;
@State dragItem: number = -1;
@State scaleItem: number = -1;
item: number = -1;
private dragRefOffsetX: number = 0;
private dragRefOffsetY: number = 0;
@State offsetX: number = 0;
@State offsetY: number = 0;
private FIX_VP_X: number = 108;
private FIX_VP_Y: number = 120;
aboutToAppear() {
for (let i = 1; i <= 36; i++) {
this.numbers.push(i);
}
this.lastIndex = this.numbers.length - 1;
// 多列
for (let i = 0; i < this.row; i++) {
this.str = this.str + '1fr ';
}
}
itemMove(index: number, newIndex: number): void {
console.info('index:' + index + ' newIndex:' + newIndex);
if (!this.isDraggable(newIndex)) {
return;
}
let tmp = this.numbers.splice(index, 1);
this.numbers.splice(newIndex, 0, tmp[0]);
}
// 向下滑
down(index: number): void {
if (!this.isDraggable(index + this.row)) {
return;
}
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
// 多列
this.itemMove(index, index + this.row);
}
// 向下滑(右下角为空)
down2(index: number): void {
if (!this.isDraggable(index + 3)) {
return;
}
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + 3);
}
// 向上滑
up(index: number): void {
if (!this.isDraggable(index - this.row)) {
return;
}
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - this.row);
}
// 向左滑
left(index: number): void {
if (!this.isDraggable(index - 1)) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.itemMove(index, index - 1);
}
// 向右滑
right(index: number): void {
if (!this.isDraggable(index + 1)) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.itemMove(index, index + 1);
}
// 向右下滑
lowerRight(index: number): void {
if (!this.isDraggable(index + this.row + 1)) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + this.row + 1);
}
// 向右上滑
upperRight(index: number): void {
if (!this.isDraggable(index - (this.row - 1))) {
return;
}
this.offsetX -= this.FIX_VP_X;
this.dragRefOffsetX += this.FIX_VP_X;
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - (this.row - 1));
}
// 向左下滑
lowerLeft(index: number): void {
if (!this.isDraggable(index + (this.row - 1))) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.offsetY -= this.FIX_VP_Y;
this.dragRefOffsetY += this.FIX_VP_Y;
this.itemMove(index, index + (this.row - 1));
}
// 向左上滑
upperLeft(index: number): void {
if (!this.isDraggable(index - (this.row + 1))) {
return;
}
this.offsetX += this.FIX_VP_X;
this.dragRefOffsetX -= this.FIX_VP_X;
this.offsetY += this.FIX_VP_Y;
this.dragRefOffsetY -= this.FIX_VP_Y;
this.itemMove(index, index - (this.row + 1));
}
// 通过元素的索引,控制对应元素是否能移动排序
isDraggable(index: number): boolean {
console.info(index: ${index});
return index > -1; // 恒成立,所有元素均可移动排序
}
build() {
Column() {
Grid() {
ForEach(this.numbers, (item: number) => {
GridItem() {
Text(item + '')
.fontSize(16)
.width('100%')
.textAlign(TextAlign.Center)
.height(100)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
.shadow(this.scaleItem == item ? {
radius: 70,
color: '#15000000',
offsetX: 0,
offsetY: 0
} :
{
radius: 0,
color: '#15000000',
offsetX: 0,
offsetY: 0
})
.animation({ curve: Curve.Sharp, duration: 300 });
}
// 添加震动
.onTouch(() => {
})
.onAreaChange((oldVal, newVal) => {
// 多列
this.FIX_VP_X = Math.round(newVal.width as number);
this.FIX_VP_Y = Math.round(newVal.height as number);
console.info(oldVal:${JSON.stringify(oldVal)});
})
// 指定固定GridItem不响应事件
.hitTestBehavior(this.isDraggable(this.numbers.indexOf(item)) ? HitTestMode.Default : HitTestMode.None)
.scale({ x: this.scaleItem == item ? 1.05 : 1, y: this.scaleItem == item ? 1.05 : 1 })
.zIndex(this.dragItem == item ? 1 : 0)
.translate(this.dragItem == item ? { x: this.offsetX, y: this.offsetY } : { x: 0, y: 0 })
.padding(10)
.gesture(
GestureGroup(GestureMode.Sequence,
LongPressGesture({ repeat: true, duration: 50 })
.onAction((event?: GestureEvent) => {
console.info(event: ${event});
this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
this.scaleItem = item;
});
})
.onActionEnd(() => {
this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
this.scaleItem = -1;
});
}),
PanGesture({ fingers: 1, direction: null, distance: 0 })
.onActionStart(() => {
this.dragItem = item;
this.dragRefOffsetX = 0;
this.dragRefOffsetY = 0;
})
.onActionUpdate((event: GestureEvent) => {
this.offsetY = event.offsetY - this.dragRefOffsetY;
this.offsetX = event.offsetX - this.dragRefOffsetX;
console.info(移动过程中event.offsetY:${event.offsetY},
this.dragRefOffsetY: ${this.dragRefOffsetY}, this.offsetY: ${this.offsetY});
console.info(移动过程中event.offsetX:${event.offsetX},
this.dragRefOffsetX: ${this.dragRefOffsetX}, this.offsetX: ${this.offsetX});
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
let index = this.numbers.indexOf(this.dragItem);
console.info('index= ', index);
if (this.offsetY >= this.FIX_VP_Y / 2 &&
(this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
&& (index + this.row <= this.lastIndex)) {
// 向下滑
this.down(index);
} else if (this.offsetY <= -this.FIX_VP_Y / 2 &&
(this.offsetX <= this.FIX_VP_X / 2 && this.offsetX >= -this.FIX_VP_X / 2)
&& index - this.row >= 0) {
// 向上滑
this.up(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 &&
(this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
&& !(((index - (this.row - 1)) % this.row == 0) || index == this.lastIndex)) {
// 向右滑
this.right(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 &&
(this.offsetY <= this.FIX_VP_Y / 2 && this.offsetY >= -this.FIX_VP_Y / 2)
&& !(index % this.row == 0)) {
// 向左滑
this.left(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& ((index + this.row + 1 <= this.lastIndex && !((index - (this.row - 1)) % this.row == 0)) ||
!((index - (this.row - 1)) % this.row == 0))) {
// 向右下滑
this.lowerRight(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
&& !((index - this.row < 0) || ((index - (this.row - 1)) % this.row == 0))) {
// 向右上滑
this.upperRight(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& (!(index % this.row == 0) && (index + (this.row - 1) <= this.lastIndex))) {
// 向左下滑
this.lowerLeft(index);
} else if (this.offsetX <= -this.FIX_VP_X / 2 && this.offsetY <= -this.FIX_VP_Y / 2
&& !((index <= this.row - 1) || (index % this.row == 0))) {
// 向左上滑
this.upperLeft(index);
} else if (this.offsetX >= this.FIX_VP_X / 2 && this.offsetY >= this.FIX_VP_Y / 2
&& (index == this.lastIndex)) {
// 向右下滑(右下角为空)
this.down2(index);
}
});
})
.onActionEnd(() => {
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
this.dragItem = -1;
});
this.getUIContext().animateTo({
curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150
}, () => {
this.scaleItem = -1;
});
})
)
.onCancel(() => {
this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
this.dragItem = -1;
});
this.getUIContext().animateTo({
curve: curves.interpolatingSpring(14, 1, 170, 17)
}, () => {
this.scaleItem = -1;
});
})
);
}, (item: number) => item.toString());
}
.width('90%')
.editMode(true)
.scrollBar(BarState.Off)
// 多列
.columnsTemplate(this.str);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
.width('100%').height('100%').backgroundColor('#f1f3f5').padding({ top: 5 });
}
}
https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home
Tab组件内Navigation跳转,TabBar导航栏隐藏失败如何解决
问题现象
在“我的”页面点击“跳转设置页”按钮跳转到设置页面,底部的TabBar导航栏仍然存在。
点击放大点击放大
主页面有“首页”、“我的”两个Tab页面,部分代码如下:
@Entry
@Component
struct TabsNavPage {
build() {
Tabs() {
TabContent() {
MinePage()
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '我的'))
TabContent() {
Text('首页')
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '首页'))
}
.barPosition(BarPosition.End)
}
}
@Component
struct MinePage {
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
@Builder
PagesMap(name: string) {
if (name === 'Settings') {
Settings();
}
}
build() {
Navigation(this.pathStack) {
Row(){
Column() {
Button('跳转设置页')
.onClick(() => {
this.pathStack.pushPathByName('Settings', null);
})
}
}
}
.hideTitleBar(true)
.navDestination(this.PagesMap)
}
}
@Component
struct Settings {
@Consume('pathStack') pathStack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Button('返回')
.onClick(() => {
this.pathStack.pop();
});
}.title('设置')
}
}
效果预览
点击放大
背景知识
Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(Navigation的子组件)或非首页显示(NavDestination的子组件),首页和非首页通过路由进行切换。
@Provide装饰器和@Consume装饰器:应用于与后代组件的双向数据同步、状态数据在多个层级之间传递的场景。
解决方案
问题现象中是用Tab组件包裹Navigation组件,跳转Navigation子组件并不会影响到外层的Tab组件。
将Navigation组件作为根容器包括Tab组件,为保证子组件都共享一个NavPathStack实例,可以使用@Provide和@Consume装饰器将导航控制器对象NavPathStack传递给Tabs内的子组件使用。
@Entry
@Component
struct TabsNavPage {
// 使用@Provide将路由栈对象传递给TabContent内的组件
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
@Builder
PagesMap(name: string) {
if (name === 'Settings') {
Settings();
}
}
build() {
// 使用Navigation包裹Tabs,Tabs子页使用同一个路由栈对象
Navigation(this.pathStack) {
Tabs() {
TabContent() {
MinePage();
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '我的'))
TabContent() {
Text('首页').fontColor('40fp');
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '首页'))
}
.barPosition(BarPosition.End);
}
.hideTitleBar(true)
.navDestination(this.PagesMap)
}
}
@Component
struct MinePage {
// 获取Navigation的路由栈对象
@Consume('pathStack') pathStack: NavPathStack;
build() {
Column() {
Button('跳转设置页')
.onClick(() => {
this.pathStack.pushPathByName('Settings', null);
});
};
}
}
@Component
struct Settings {
@Consume('pathStack') pathStack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Button('返回')
.onClick(() => {
this.pathStack.pop();
});
}.title('设置')
}
}
https://coub.com/view/5kamzbr4mi
https://coub.com/view/9slmznc3ol
https://coub.com/view/dnhe4x7otv
https://coub.com/view/libk78392s
https://coub.com/view/2splvx7ais
https://coub.com/view/0fqk03fwlz
https://coub.com/view/u3but76ukt
https://coub.com/view/2xrg8kdx5o
https://coub.com/view/iqeb7cwj8k
https://coub.com/view/lx86s7cfpr
https://coub.com/view/qin8evf0sk
https://coub.com/view/ul1dmz1db2
https://coub.com/view/1n6f59dch3
https://coub.com/view/mz789w4pbg
https://coub.com/view/trb9jcog9p
https://coub.com/view/j3i3lac4yx
https://coub.com/view/84l11twj2s
https://coub.com/view/ut50q4erj3
https://coub.com/view/w6uuiu7ru3
https://coub.com/view/iun2hgfzqm
https://coub.com/view/hc6d3b3sts
https://coub.com/view/7l7wd758hs
https://coub.com/view/sshi8p319r
https://coub.com/view/2y21fnu5t4
https://coub.com/view/3dpvlq1hxa
https://coub.com/view/7tefrrju9a
https://coub.com/view/kr0zw5ow09
https://coub.com/view/vze4xn6lxk
https://coub.com/view/gp0hoyjbev
https://coub.com/view/8ukozyn2pf
https://coub.com/view/gne8uf8fun
https://coub.com/view/o1ljlnaqrl
https://coub.com/view/6pe00kuw3p
https://coub.com/view/3ys8ww2kf5
https://coub.com/view/ky54919n96
https://coub.com/view/ovvfpcdgiy
https://coub.com/view/h1g7qx1ibk
https://coub.com/view/4ytxeco099
https://coub.com/view/z8grd6tisg
https://coub.com/view/ihcl3n786e
https://coub.com/view/e48vma3hes
https://coub.com/view/rgk8us00m7
https://coub.com/view/mxmu9agwxl
https://coub.com/view/jjq7sf2887
https://coub.com/view/rtlotmcb2o
https://coub.com/view/2xzrkf1j13
https://coub.com/view/datluk4gwo
https://coub.com/view/5ld5j0uc5o
https://coub.com/view/cp081k64fp
https://coub.com/view/0sgmugss1z
https://coub.com/view/ynwmj064s7
https://coub.com/view/y8atenamwg
https://coub.com/view/acqfefno2e
https://coub.com/view/hmwvsetfho
https://coub.com/view/n59qhb108b
https://coub.com/view/946i56xqgw
https://coub.com/view/fu355xmxbd
https://coub.com/view/9gfbdnyv3z
https://coub.com/view/i16nn052at
https://coub.com/view/s2o2prg0rl
https://coub.com/view/f7m17v6t3x
https://coub.com/view/6bmqboq0w9
https://coub.com/view/tvj9e2712i
https://coub.com/view/b5kbn4qpgx
https://coub.com/view/hh77rcnz58
https://coub.com/view/8w0hoxvhxu
https://coub.com/view/ogtrlf718w
https://coub.com/view/dcy0w8eeq1
https://coub.com/view/foo2zaj4nz
https://coub.com/view/ffai2oxjgj
https://coub.com/view/h7llkwvo4t
https://coub.com/view/fxf6iote20
https://coub.com/view/zr4atw33g7
https://coub.com/view/w6ksjzfgri
https://coub.com/view/6wlivajplu
https://coub.com/view/m3p9mpc87g
https://coub.com/view/1svk68qjxi
https://coub.com/view/jn78pmgtsy
https://coub.com/view/ru4kxd02ca
https://coub.com/view/u2qm3zht04
https://coub.com/view/07vrswbr9o
https://coub.com/view/z53dkteu04
https://coub.com/view/r3kczagbat
https://coub.com/view/ebkii1w2uy
https://coub.com/view/gu2ovmapto
https://coub.com/view/6s1y6nkktx
https://coub.com/view/2mzsrq6bnh
https://coub.com/view/bmh3bh21dl
https://coub.com/view/fdiekfi3y1
https://coub.com/view/ex4dtaj1ri
https://coub.com/view/tsgmmorhu1
https://coub.com/view/gh01h6433n
https://coub.com/view/3zfrusqatl
https://coub.com/view/u27l9exjjr
https://coub.com/view/wx8rybzzdd
https://coub.com/view/u13mvzb6i9
https://coub.com/view/lw5mv3n3vp
https://coub.com/view/tn7jo1khbm
https://coub.com/view/hr9h8qk489
https://coub.com/view/dfx72ezl4k
https://coub.com/view/xfnivvh59q
https://coub.com/view/6dmijjf9eu
https://coub.com/view/fi22iqa3iv
https://coub.com/view/57fq1sjhpa
https://coub.com/view/9xii5apv7o
https://coub.com/view/e5fhxwb6re
https://coub.com/view/wuc96hdc6z
https://coub.com/view/bvp1embwd5
https://coub.com/view/u1lurshbkm
https://coub.com/view/nih2qy00t8
https://coub.com/view/mrx826u951
https://coub.com/view/kes50gj32a
https://coub.com/view/fcv3394eq2
https://coub.com/view/4qc0x8z64l
https://coub.com/view/n2k5itdn4w
https://coub.com/view/t988vol3yp
https://coub.com/view/synur4gqha
https://coub.com/view/bd6fqj7ej9
https://coub.com/view/crwhjs6qgk
https://coub.com/view/vde66uoo7x
https://coub.com/view/68md6egw0p
https://coub.com/view/2fgvpgk4ir
https://coub.com/view/4ygf27wtro
https://coub.com/view/0qdbv1ptnp
https://coub.com/view/b5ii094vls
https://coub.com/view/o8fq8rif3n
https://coub.com/view/3odrc01a85
https://coub.com/view/8k08cynikg
问题现象
在“我的”页面点击“跳转设置页”按钮跳转到设置页面,底部的TabBar导航栏仍然存在。
点击放大点击放大
主页面有“首页”、“我的”两个Tab页面,部分代码如下:
@Entry
@Component
struct TabsNavPage {
build() {
Tabs() {
TabContent() {
MinePage()
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '我的'))
TabContent() {
Text('首页')
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '首页'))
}
.barPosition(BarPosition.End)
}
}
@Component
struct MinePage {
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
@Builder
PagesMap(name: string) {
if (name === 'Settings') {
Settings();
}
}
build() {
Navigation(this.pathStack) {
Row(){
Column() {
Button('跳转设置页')
.onClick(() => {
this.pathStack.pushPathByName('Settings', null);
})
}
}
}
.hideTitleBar(true)
.navDestination(this.PagesMap)
}
}
@Component
struct Settings {
@Consume('pathStack') pathStack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Button('返回')
.onClick(() => {
this.pathStack.pop();
});
}.title('设置')
}
}
效果预览
点击放大
背景知识
Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(Navigation的子组件)或非首页显示(NavDestination的子组件),首页和非首页通过路由进行切换。
@Provide装饰器和@Consume装饰器:应用于与后代组件的双向数据同步、状态数据在多个层级之间传递的场景。
解决方案
问题现象中是用Tab组件包裹Navigation组件,跳转Navigation子组件并不会影响到外层的Tab组件。
将Navigation组件作为根容器包括Tab组件,为保证子组件都共享一个NavPathStack实例,可以使用@Provide和@Consume装饰器将导航控制器对象NavPathStack传递给Tabs内的子组件使用。
@Entry
@Component
struct TabsNavPage {
// 使用@Provide将路由栈对象传递给TabContent内的组件
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
@Builder
PagesMap(name: string) {
if (name === 'Settings') {
Settings();
}
}
build() {
// 使用Navigation包裹Tabs,Tabs子页使用同一个路由栈对象
Navigation(this.pathStack) {
Tabs() {
TabContent() {
MinePage();
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '我的'))
TabContent() {
Text('首页').fontColor('40fp');
}
.tabBar(BottomTabBarStyle.of($r('sys.media.ohos_app_icon'), '首页'))
}
.barPosition(BarPosition.End);
}
.hideTitleBar(true)
.navDestination(this.PagesMap)
}
}
@Component
struct MinePage {
// 获取Navigation的路由栈对象
@Consume('pathStack') pathStack: NavPathStack;
build() {
Column() {
Button('跳转设置页')
.onClick(() => {
this.pathStack.pushPathByName('Settings', null);
});
};
}
}
@Component
struct Settings {
@Consume('pathStack') pathStack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Button('返回')
.onClick(() => {
this.pathStack.pop();
});
}.title('设置')
}
}
https://coub.com/view/5kamzbr4mi
https://coub.com/view/9slmznc3ol
https://coub.com/view/dnhe4x7otv
https://coub.com/view/libk78392s
https://coub.com/view/2splvx7ais
https://coub.com/view/0fqk03fwlz
https://coub.com/view/u3but76ukt
https://coub.com/view/2xrg8kdx5o
https://coub.com/view/iqeb7cwj8k
https://coub.com/view/lx86s7cfpr
https://coub.com/view/qin8evf0sk
https://coub.com/view/ul1dmz1db2
https://coub.com/view/1n6f59dch3
https://coub.com/view/mz789w4pbg
https://coub.com/view/trb9jcog9p
https://coub.com/view/j3i3lac4yx
https://coub.com/view/84l11twj2s
https://coub.com/view/ut50q4erj3
https://coub.com/view/w6uuiu7ru3
https://coub.com/view/iun2hgfzqm
https://coub.com/view/hc6d3b3sts
https://coub.com/view/7l7wd758hs
https://coub.com/view/sshi8p319r
https://coub.com/view/2y21fnu5t4
https://coub.com/view/3dpvlq1hxa
https://coub.com/view/7tefrrju9a
https://coub.com/view/kr0zw5ow09
https://coub.com/view/vze4xn6lxk
https://coub.com/view/gp0hoyjbev
https://coub.com/view/8ukozyn2pf
https://coub.com/view/gne8uf8fun
https://coub.com/view/o1ljlnaqrl
https://coub.com/view/6pe00kuw3p
https://coub.com/view/3ys8ww2kf5
https://coub.com/view/ky54919n96
https://coub.com/view/ovvfpcdgiy
https://coub.com/view/h1g7qx1ibk
https://coub.com/view/4ytxeco099
https://coub.com/view/z8grd6tisg
https://coub.com/view/ihcl3n786e
https://coub.com/view/e48vma3hes
https://coub.com/view/rgk8us00m7
https://coub.com/view/mxmu9agwxl
https://coub.com/view/jjq7sf2887
https://coub.com/view/rtlotmcb2o
https://coub.com/view/2xzrkf1j13
https://coub.com/view/datluk4gwo
https://coub.com/view/5ld5j0uc5o
https://coub.com/view/cp081k64fp
https://coub.com/view/0sgmugss1z
https://coub.com/view/ynwmj064s7
https://coub.com/view/y8atenamwg
https://coub.com/view/acqfefno2e
https://coub.com/view/hmwvsetfho
https://coub.com/view/n59qhb108b
https://coub.com/view/946i56xqgw
https://coub.com/view/fu355xmxbd
https://coub.com/view/9gfbdnyv3z
https://coub.com/view/i16nn052at
https://coub.com/view/s2o2prg0rl
https://coub.com/view/f7m17v6t3x
https://coub.com/view/6bmqboq0w9
https://coub.com/view/tvj9e2712i
https://coub.com/view/b5kbn4qpgx
https://coub.com/view/hh77rcnz58
https://coub.com/view/8w0hoxvhxu
https://coub.com/view/ogtrlf718w
https://coub.com/view/dcy0w8eeq1
https://coub.com/view/foo2zaj4nz
https://coub.com/view/ffai2oxjgj
https://coub.com/view/h7llkwvo4t
https://coub.com/view/fxf6iote20
https://coub.com/view/zr4atw33g7
https://coub.com/view/w6ksjzfgri
https://coub.com/view/6wlivajplu
https://coub.com/view/m3p9mpc87g
https://coub.com/view/1svk68qjxi
https://coub.com/view/jn78pmgtsy
https://coub.com/view/ru4kxd02ca
https://coub.com/view/u2qm3zht04
https://coub.com/view/07vrswbr9o
https://coub.com/view/z53dkteu04
https://coub.com/view/r3kczagbat
https://coub.com/view/ebkii1w2uy
https://coub.com/view/gu2ovmapto
https://coub.com/view/6s1y6nkktx
https://coub.com/view/2mzsrq6bnh
https://coub.com/view/bmh3bh21dl
https://coub.com/view/fdiekfi3y1
https://coub.com/view/ex4dtaj1ri
https://coub.com/view/tsgmmorhu1
https://coub.com/view/gh01h6433n
https://coub.com/view/3zfrusqatl
https://coub.com/view/u27l9exjjr
https://coub.com/view/wx8rybzzdd
https://coub.com/view/u13mvzb6i9
https://coub.com/view/lw5mv3n3vp
https://coub.com/view/tn7jo1khbm
https://coub.com/view/hr9h8qk489
https://coub.com/view/dfx72ezl4k
https://coub.com/view/xfnivvh59q
https://coub.com/view/6dmijjf9eu
https://coub.com/view/fi22iqa3iv
https://coub.com/view/57fq1sjhpa
https://coub.com/view/9xii5apv7o
https://coub.com/view/e5fhxwb6re
https://coub.com/view/wuc96hdc6z
https://coub.com/view/bvp1embwd5
https://coub.com/view/u1lurshbkm
https://coub.com/view/nih2qy00t8
https://coub.com/view/mrx826u951
https://coub.com/view/kes50gj32a
https://coub.com/view/fcv3394eq2
https://coub.com/view/4qc0x8z64l
https://coub.com/view/n2k5itdn4w
https://coub.com/view/t988vol3yp
https://coub.com/view/synur4gqha
https://coub.com/view/bd6fqj7ej9
https://coub.com/view/crwhjs6qgk
https://coub.com/view/vde66uoo7x
https://coub.com/view/68md6egw0p
https://coub.com/view/2fgvpgk4ir
https://coub.com/view/4ygf27wtro
https://coub.com/view/0qdbv1ptnp
https://coub.com/view/b5ii094vls
https://coub.com/view/o8fq8rif3n
https://coub.com/view/3odrc01a85
https://coub.com/view/8k08cynikg
Android离线打包,plus.runtime.install导致应用闪退
logcat日志如下:
Unresolved exception class when finding catch block: net.lingala.zip4j.exception.ZipException
2026-08-28 15:40:18.591 29792-30261 AndroidRuntime com.gdxinyue.newusmp
E FATAL EXCEPTION: Thread-10
Process: com.gdxinyue.newusmp, PID: 29792
java.lang.NullPointerException: Attempt to read from null array
at io.dcloud.feature.pdr.RuntimeFeatureImpl$a.run
参考博客:https://blog.csdn.net/qq_32058147/article/details/155034674
在Android Stadio主项目的 build.gradle > dependencies中增加以下依赖:
dependencies {
implementation 'net.lingala.zip4j:zip4j:2.11.5'
}
logcat日志如下:
Unresolved exception class when finding catch block: net.lingala.zip4j.exception.ZipException
2026-08-28 15:40:18.591 29792-30261 AndroidRuntime com.gdxinyue.newusmp
E FATAL EXCEPTION: Thread-10
Process: com.gdxinyue.newusmp, PID: 29792
java.lang.NullPointerException: Attempt to read from null array
at io.dcloud.feature.pdr.RuntimeFeatureImpl$a.run
参考博客:https://blog.csdn.net/qq_32058147/article/details/155034674
在Android Stadio主项目的 build.gradle > dependencies中增加以下依赖:
dependencies {
implementation 'net.lingala.zip4j:zip4j:2.11.5'
}
uni-starter模板代码更正
创建uni-app项目时选择uni-starter模板,我发现其中pages/list/search/search.vue中的“搜索联想”部分代码<uni-list-item>的:title="item.name"可能写错了,应该改为:title="item.title",具体代码如下:
<!-- 搜索联想 -->
<view class="search-associative" v-if="associativeShow">
<uni-list>
<!-- :title="item.name"可能写错了,文章表没有name字段,associativeList只是获取_id、title字段! -->
<!-- <uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.name" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item> -->
<!-- 正确改为:title="item.title",在搜索框输入文章记录的标题子串时,会显示篇文章的标题 -->
<uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item>
</uni-list>
</view>
这个搜索联想的功能应该是在搜索框输入文章记录标题的子串时,提示有没有这篇文章,若存在,则显示所有标题带有此子串的文章标题列表,这样就可以直接点击找到此文章,因为模板的代码写错了"item.name",导致完全没有提示,希望可以改正过来。
创建uni-app项目时选择uni-starter模板,我发现其中pages/list/search/search.vue中的“搜索联想”部分代码<uni-list-item>的:title="item.name"可能写错了,应该改为:title="item.title",具体代码如下:
<!-- 搜索联想 -->
<view class="search-associative" v-if="associativeShow">
<uni-list>
<!-- :title="item.name"可能写错了,文章表没有name字段,associativeList只是获取_id、title字段! -->
<!-- <uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.name" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item> -->
<!-- 正确改为:title="item.title",在搜索框输入文章记录的标题子串时,会显示篇文章的标题 -->
<uni-list-item v-for="(item,index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title" @click="associativeClick(item)" show-extra-icon
clickable :extra-icon="{size:18,color:iconColor,type:'search'}" >
</uni-list-item>
</uni-list>
</view>
这个搜索联想的功能应该是在搜索框输入文章记录标题的子串时,提示有没有这篇文章,若存在,则显示所有标题带有此子串的文章标题列表,这样就可以直接点击找到此文章,因为模板的代码写错了"item.name",导致完全没有提示,希望可以改正过来。
收起阅读 »组件实现随手拖动松手后贴边效果
问题现象
如何实现手指长按组件时,组件能够跟随手指拖动,手指抬起后,组件贴边停靠的效果。
背景知识
onTouch是用于处理触摸事件的组件属性,支持TouchType.Down(按下)、TouchType.Move(移动)、TouchType.Up(抬起),用于多模交互场景(如拖拽、点击、滑动等)。
animateTo是用于实现动画效果的核心函数,支持平滑过渡、弹性动画、曲线控制等。
解决方案
为实现上述功能,可通过如下步骤实现:
通过组件的触摸事件onTouch,其中使用TouchType.Down记录手指按下时的初始坐标,使用TouchType.Move获取手指移动时的位置信息,通过获取的位置信息修改组件的position属性实现元素的拖拽功能:
// 指针按下事件:记录初始坐标
case TouchType.Down: {
this.windowStartX = event.touches[0].windowX;
this.windowStartY = event.touches[0].windowY;
break;
}
case TouchType.Move: {
const windowX: number = event.touches[0].windowX;
const windowY: number = event.touches[0].windowY;
// 左边距吸附时:更新左边缘位置
if (this.edge.left !== undefined) {
this.edge.left = this.edge.left as number + (windowX - this.windowStartX);
}
// 右边距吸附时:更新右边缘位置
else {
this.edge.right = this.edge.right as number - (windowX - this.windowStartX);
}
// 更新顶部位置
this.edge.top = this.edge.top as number + (windowY - this.windowStartY);
// 更新起始坐标用于下次计算
this.windowStartX = windowX;
this.windowStartY = windowY;
break;
}
再通过TouchType.Up(手指抬起),对比手指抬起时组件所处位置,与容器组件的中心位置,判断出需要向哪边进行贴边操作,或与容器元素宽度进行对比,判断元素是否超出容器,选择将组件position属性的left或right设置为undefined并将其对立方向设置为0,且位置的调整设置于animateTo中,实现将元素从容器内部或容器外部缓慢调整至容器边缘,从而实现组件拖拽后的贴边效果:
case TouchType.Up: {
// 计算悬浮窗中心点在父组件中的水平坐标
let centerX: number;
if (this.edge.left !== undefined) {
centerX = this.edge.left as number + this.floatWindowWidth / 2;
} else {
centerX = this.containerWidth - (this.edge.right as number) - this.floatWindowWidth / 2;
}
// 使用阻尼动画实现吸附效果
this.uiContext.animateTo({ curve: curves.springMotion() }, () => {
// 获取屏幕宽度
let width = this.uiContext.px2vp(display.getDefaultDisplaySync().width);
// 启用吸附功能时
if (this.openAdsorb) {
// 根据中心点位置决定吸附方向
if (centerX > (this.containerWidth / 2)) {
// 右侧吸附
this.edge.right = this.pagePadding;
this.edge.left = undefined;
} else {
// 左侧吸附
this.edge.right = undefined;
this.edge.left = this.pagePadding;
}
} else {
// 检查是否超出左侧屏幕
let overflowLeft = this.edge.right! > width - this.floatWindowWidth;
// 检查是否超出右侧屏幕
let overflowRight = this.edge.right! < 0;
// 处理超出屏幕边界的情况
if (overflowLeft) { // 超出左边屏幕
this.edge.left = undefined;
this.edge.right = width - this.floatWindowWidth;
} else if (overflowRight) { // 超出右边屏幕
this.edge.right = this.pagePadding;
this.edge.left = undefined;
}
}
})
break;
}
完整实现如下:
import { curves, display } from '@kit.ArkUI';
@Component
export struct FloatWindowView {
@State edge: Edges = { top: 200, left: 0 };
@Link containerWidth: number;
@Link containerHeight: number;
private windowStartX: number = 0;
private windowStartY: number = 0;
// 是否启用吸附功能
openAdsorb: boolean = true;
@Prop pagePadding: number = 0; // 页面内容内边距,用于悬浮窗位置计算
@State floatWindowWidth: number = 100; // 悬浮窗宽度
@State floatWindowHeight: number = 50; // 悬浮窗高度
uiContext: UIContext = this.getUIContext();
// 触摸回调,实现悬浮窗跟手拖拽和贴边吸附动画
onTouchEvent(event: TouchEvent): void {
switch (event.type) {
// 指针按下事件:记录初始坐标
case TouchType.Down: {
this.windowStartX = event.touches[0].windowX;
this.windowStartY = event.touches[0].windowY;
break;
}
case TouchType.Move: {
const windowX: number = event.touches[0].windowX;
const windowY: number = event.touches[0].windowY;
// 左边距吸附时:更新左边缘位置
if (this.edge.left !== undefined) {
this.edge.left = this.edge.left as number + (windowX - this.windowStartX);
}
// 右边距吸附时:更新右边缘位置
else {
this.edge.right = this.edge.right as number - (windowX - this.windowStartX);
}
// 更新顶部位置
this.edge.top = this.edge.top as number + (windowY - this.windowStartY);
// 更新起始坐标用于下次计算
this.windowStartX = windowX;
this.windowStartY = windowY;
break;
}
// 指针抬起事件:实现吸附动画和边界限制
case TouchType.Up: {
// 计算悬浮窗中心点在父组件中的水平坐标
let centerX: number;
if (this.edge.left !== undefined) {
centerX = this.edge.left as number + this.floatWindowWidth / 2;
} else {
centerX = this.containerWidth - (this.edge.right as number) - this.floatWindowWidth / 2;
}
// 使用阻尼动画实现吸附效果
this.uiContext.animateTo({ curve: curves.springMotion() }, () => {
// 获取屏幕宽度
let width = this.uiContext.px2vp(display.getDefaultDisplaySync().width);
// 启用吸附功能时
if (this.openAdsorb) {
// 根据中心点位置决定吸附方向
if (centerX > (this.containerWidth / 2)) {
// 右侧吸附
this.edge.right = this.pagePadding;
this.edge.left = undefined;
} else {
// 左侧吸附
this.edge.right = undefined;
this.edge.left = this.pagePadding;
}
} else {
// 检查是否超出左侧屏幕
let overflowLeft = this.edge.right! > width - this.floatWindowWidth;
// 检查是否超出右侧屏幕
let overflowRight = this.edge.right! < 0;
// 处理超出屏幕边界的情况
if (overflowLeft) { // 超出左边屏幕
this.edge.left = undefined;
this.edge.right = width - this.floatWindowWidth;
} else if (overflowRight) { // 超出右边屏幕
this.edge.right = this.pagePadding;
this.edge.left = undefined;
}
}
})
break;
}
default: {
break;
}
}
}
build() {
Column() {
}
.clip(true)
.backgroundColor('#0A59F7')
.width(this.floatWindowWidth)
.height(this.floatWindowHeight)
.position(this.edge)
.onTouch((event: TouchEvent) => {
this.onTouchEvent(event);
})
}
}
@Entry
@Component
struct Index {
// 父组件宽度
@State containerWidth: number = 0;
// 父组件高度
@State containerHeight: number = 0;
build() {
Stack() {
FloatWindowView({
containerWidth: this.containerWidth, // 传递父容器宽度
containerHeight: this.containerHeight, // 传递父容器高度
})
.width('100%')
}
// 设置外层Stack容器的尺寸
.height('100%')
.width('100%')
.onAreaChange((oldValue: Area, newValue: Area) => {
// 记录父组件的宽高
if (oldValue.width !== newValue.width) {
this.containerWidth = newValue.width as number;
}
if (oldValue.height !== newValue.height) {
this.containerHeight = newValue.height as number;
}
})
}
}
https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home
问题现象
如何实现手指长按组件时,组件能够跟随手指拖动,手指抬起后,组件贴边停靠的效果。
背景知识
onTouch是用于处理触摸事件的组件属性,支持TouchType.Down(按下)、TouchType.Move(移动)、TouchType.Up(抬起),用于多模交互场景(如拖拽、点击、滑动等)。
animateTo是用于实现动画效果的核心函数,支持平滑过渡、弹性动画、曲线控制等。
解决方案
为实现上述功能,可通过如下步骤实现:
通过组件的触摸事件onTouch,其中使用TouchType.Down记录手指按下时的初始坐标,使用TouchType.Move获取手指移动时的位置信息,通过获取的位置信息修改组件的position属性实现元素的拖拽功能:
// 指针按下事件:记录初始坐标
case TouchType.Down: {
this.windowStartX = event.touches[0].windowX;
this.windowStartY = event.touches[0].windowY;
break;
}
case TouchType.Move: {
const windowX: number = event.touches[0].windowX;
const windowY: number = event.touches[0].windowY;
// 左边距吸附时:更新左边缘位置
if (this.edge.left !== undefined) {
this.edge.left = this.edge.left as number + (windowX - this.windowStartX);
}
// 右边距吸附时:更新右边缘位置
else {
this.edge.right = this.edge.right as number - (windowX - this.windowStartX);
}
// 更新顶部位置
this.edge.top = this.edge.top as number + (windowY - this.windowStartY);
// 更新起始坐标用于下次计算
this.windowStartX = windowX;
this.windowStartY = windowY;
break;
}
再通过TouchType.Up(手指抬起),对比手指抬起时组件所处位置,与容器组件的中心位置,判断出需要向哪边进行贴边操作,或与容器元素宽度进行对比,判断元素是否超出容器,选择将组件position属性的left或right设置为undefined并将其对立方向设置为0,且位置的调整设置于animateTo中,实现将元素从容器内部或容器外部缓慢调整至容器边缘,从而实现组件拖拽后的贴边效果:
case TouchType.Up: {
// 计算悬浮窗中心点在父组件中的水平坐标
let centerX: number;
if (this.edge.left !== undefined) {
centerX = this.edge.left as number + this.floatWindowWidth / 2;
} else {
centerX = this.containerWidth - (this.edge.right as number) - this.floatWindowWidth / 2;
}
// 使用阻尼动画实现吸附效果
this.uiContext.animateTo({ curve: curves.springMotion() }, () => {
// 获取屏幕宽度
let width = this.uiContext.px2vp(display.getDefaultDisplaySync().width);
// 启用吸附功能时
if (this.openAdsorb) {
// 根据中心点位置决定吸附方向
if (centerX > (this.containerWidth / 2)) {
// 右侧吸附
this.edge.right = this.pagePadding;
this.edge.left = undefined;
} else {
// 左侧吸附
this.edge.right = undefined;
this.edge.left = this.pagePadding;
}
} else {
// 检查是否超出左侧屏幕
let overflowLeft = this.edge.right! > width - this.floatWindowWidth;
// 检查是否超出右侧屏幕
let overflowRight = this.edge.right! < 0;
// 处理超出屏幕边界的情况
if (overflowLeft) { // 超出左边屏幕
this.edge.left = undefined;
this.edge.right = width - this.floatWindowWidth;
} else if (overflowRight) { // 超出右边屏幕
this.edge.right = this.pagePadding;
this.edge.left = undefined;
}
}
})
break;
}
完整实现如下:
import { curves, display } from '@kit.ArkUI';
@Component
export struct FloatWindowView {
@State edge: Edges = { top: 200, left: 0 };
@Link containerWidth: number;
@Link containerHeight: number;
private windowStartX: number = 0;
private windowStartY: number = 0;
// 是否启用吸附功能
openAdsorb: boolean = true;
@Prop pagePadding: number = 0; // 页面内容内边距,用于悬浮窗位置计算
@State floatWindowWidth: number = 100; // 悬浮窗宽度
@State floatWindowHeight: number = 50; // 悬浮窗高度
uiContext: UIContext = this.getUIContext();
// 触摸回调,实现悬浮窗跟手拖拽和贴边吸附动画
onTouchEvent(event: TouchEvent): void {
switch (event.type) {
// 指针按下事件:记录初始坐标
case TouchType.Down: {
this.windowStartX = event.touches[0].windowX;
this.windowStartY = event.touches[0].windowY;
break;
}
case TouchType.Move: {
const windowX: number = event.touches[0].windowX;
const windowY: number = event.touches[0].windowY;
// 左边距吸附时:更新左边缘位置
if (this.edge.left !== undefined) {
this.edge.left = this.edge.left as number + (windowX - this.windowStartX);
}
// 右边距吸附时:更新右边缘位置
else {
this.edge.right = this.edge.right as number - (windowX - this.windowStartX);
}
// 更新顶部位置
this.edge.top = this.edge.top as number + (windowY - this.windowStartY);
// 更新起始坐标用于下次计算
this.windowStartX = windowX;
this.windowStartY = windowY;
break;
}
// 指针抬起事件:实现吸附动画和边界限制
case TouchType.Up: {
// 计算悬浮窗中心点在父组件中的水平坐标
let centerX: number;
if (this.edge.left !== undefined) {
centerX = this.edge.left as number + this.floatWindowWidth / 2;
} else {
centerX = this.containerWidth - (this.edge.right as number) - this.floatWindowWidth / 2;
}
// 使用阻尼动画实现吸附效果
this.uiContext.animateTo({ curve: curves.springMotion() }, () => {
// 获取屏幕宽度
let width = this.uiContext.px2vp(display.getDefaultDisplaySync().width);
// 启用吸附功能时
if (this.openAdsorb) {
// 根据中心点位置决定吸附方向
if (centerX > (this.containerWidth / 2)) {
// 右侧吸附
this.edge.right = this.pagePadding;
this.edge.left = undefined;
} else {
// 左侧吸附
this.edge.right = undefined;
this.edge.left = this.pagePadding;
}
} else {
// 检查是否超出左侧屏幕
let overflowLeft = this.edge.right! > width - this.floatWindowWidth;
// 检查是否超出右侧屏幕
let overflowRight = this.edge.right! < 0;
// 处理超出屏幕边界的情况
if (overflowLeft) { // 超出左边屏幕
this.edge.left = undefined;
this.edge.right = width - this.floatWindowWidth;
} else if (overflowRight) { // 超出右边屏幕
this.edge.right = this.pagePadding;
this.edge.left = undefined;
}
}
})
break;
}
default: {
break;
}
}
}
build() {
Column() {
}
.clip(true)
.backgroundColor('#0A59F7')
.width(this.floatWindowWidth)
.height(this.floatWindowHeight)
.position(this.edge)
.onTouch((event: TouchEvent) => {
this.onTouchEvent(event);
})
}
}
@Entry
@Component
struct Index {
// 父组件宽度
@State containerWidth: number = 0;
// 父组件高度
@State containerHeight: number = 0;
build() {
Stack() {
FloatWindowView({
containerWidth: this.containerWidth, // 传递父容器宽度
containerHeight: this.containerHeight, // 传递父容器高度
})
.width('100%')
}
// 设置外层Stack容器的尺寸
.height('100%')
.width('100%')
.onAreaChange((oldValue: Area, newValue: Area) => {
// 记录父组件的宽高
if (oldValue.width !== newValue.width) {
this.containerWidth = newValue.width as number;
}
if (oldValue.height !== newValue.height) {
this.containerHeight = newValue.height as number;
}
})
}
}
https://sites.google.com/view/8ojcyz21/home
https://sites.google.com/view/8votdo34/home
https://sites.google.com/view/2glnsx28/home
https://sites.google.com/view/5cljni07/home
https://sites.google.com/view/8prkhh07/home
https://sites.google.com/view/7khplo30/home
https://sites.google.com/view/1iwwoj29/home
https://sites.google.com/view/5lokbt20/home
https://sites.google.com/view/8ucjap35/home
https://sites.google.com/view/3vmwqk28/home
https://sites.google.com/view/4hqmfe46/home
https://sites.google.com/view/8jjhsd05/home
https://sites.google.com/view/2ajqbl31/home
https://sites.google.com/view/8eroul90/home
https://sites.google.com/view/8whpgb50/home
https://sites.google.com/view/7fhwgy79/home
https://sites.google.com/view/8otxzd32/home
https://sites.google.com/view/0skklh79/home
https://sites.google.com/view/7epixg32/home
https://sites.google.com/view/2fdymh76/home
https://sites.google.com/view/1qzbbd10/home
https://sites.google.com/view/3betru91/home
https://sites.google.com/view/0fjymy02/home
https://sites.google.com/view/1tuyht61/home
https://sites.google.com/view/3lqsci93/home
https://sites.google.com/view/4moosi57/home
https://sites.google.com/view/0ebmiw84/home
https://sites.google.com/view/4usaol83/home
https://sites.google.com/view/0hknci80/home
https://sites.google.com/view/7ldcgt68/home
https://sites.google.com/view/8emzek60/home
https://sites.google.com/view/9oaldi75/home
https://sites.google.com/view/1hujlx46/home
https://sites.google.com/view/9krrdx96/home
https://sites.google.com/view/0rdwwl74/home
https://sites.google.com/view/4oalem69/home
https://sites.google.com/view/7tpgsy97/home
https://sites.google.com/view/4sjvkh25/home
https://sites.google.com/view/6ixmrs26/home
https://sites.google.com/view/7jjcws23/home
https://sites.google.com/view/4entvi31/home
https://sites.google.com/view/0hqtsy80/home
https://sites.google.com/view/2vthve29/home
https://sites.google.com/view/2wjwhq06/home
https://sites.google.com/view/7lnlrg38/home
https://sites.google.com/view/3jwoff92/home
https://sites.google.com/view/9exfcx28/home
https://sites.google.com/view/6ayynm56/home
https://sites.google.com/view/2mkert04/home
https://sites.google.com/view/9bfpha34/home
https://sites.google.com/view/7kwinp20/home
https://sites.google.com/view/0kzpsl33/home
https://sites.google.com/view/7vqzug72/home
https://sites.google.com/view/8qybcb91/home
https://sites.google.com/view/1manil34/home
https://sites.google.com/view/9zlzlw68/home
https://sites.google.com/view/7ytpgf83/home
https://sites.google.com/view/0figtg65/home
https://sites.google.com/view/3nphmi02/home
https://sites.google.com/view/0qkeih69/home
https://sites.google.com/view/1qqdjc96/home
https://sites.google.com/view/2ocmzl47/home
https://sites.google.com/view/0rszvm31/home
https://sites.google.com/view/8twpcf39/home
https://sites.google.com/view/1tmhcz01/home
https://sites.google.com/view/1fsimv87/home
https://sites.google.com/view/0krela31/home
https://sites.google.com/view/8jhshd10/home
https://sites.google.com/view/5tseou06/home
https://sites.google.com/view/2msyts76/home
https://sites.google.com/view/6dxuri73/home
https://sites.google.com/view/1pmboa56/home
https://sites.google.com/view/9iueuv34/home
https://sites.google.com/view/8pubxw30/home
https://sites.google.com/view/1tlyco56/home
https://sites.google.com/view/8srvbu36/home
https://sites.google.com/view/4gsnto37/home
https://sites.google.com/view/6akswp56/home
https://sites.google.com/view/2inxol47/home
https://sites.google.com/view/3oojsn55/home
https://sites.google.com/view/5mdqwb17/home
https://sites.google.com/view/7xuice12/home
https://sites.google.com/view/0qzfci43/home
https://sites.google.com/view/6ohonc26/home
https://sites.google.com/view/4tbmuv32/home
https://sites.google.com/view/1rbxss83/home
https://sites.google.com/view/2niebx01/home
https://sites.google.com/view/3qaeqy35/home
https://sites.google.com/view/0cbewx04/home
https://sites.google.com/view/3csyuh21/home
https://sites.google.com/view/8wylyx13/home
https://sites.google.com/view/1kpxse38/home
https://sites.google.com/view/8ovanb09/home
https://sites.google.com/view/6rmoih50/home
https://sites.google.com/view/5cfhmg25/home
https://sites.google.com/view/4ivxrt19/home
https://sites.google.com/view/4ltzqb02/home
https://sites.google.com/view/3vdlsy06/home
https://sites.google.com/view/5vwlbj78/home
https://sites.google.com/view/4naisw60/home




