-
【HarmonyOS】当list设置横向布局时,list高度默认撑满没有达到预期的高度自适应,可以通过onAreaChange动态修改高度。@Entry@Componentstruct Page148 { build() { Column() { List() { ForEach(['北京', '杭州', '上海'], (item: string, index: number) => { ListItem() { Text(item).fontSize(24) .height(100 * (Math.floor(Math.random() * 3) + 1))//生成一个1到3 随机数,然后+100高度 测试 .backgroundColor(Color.Pink) .margin(10) } }) } .listDirection(Axis.Horizontal) .backgroundColor('#FFF1F3F5') }.width('100%') .height('100%') }}@Entry@Componentstruct Page148 { @State maxItemHeight: number = -1 build() { Column() { List() { ForEach(['北京', '杭州', '上海'], (item: string, index: number) => { ListItem() { Text(item).fontSize(24) .height(100 * (Math.floor(Math.random() * 3) + 1))//生成一个1到3 随机数,然后+100高度 测试 .backgroundColor(Color.Pink) .margin(10) }.onAreaChange((oldArea: Area, newArea: Area) => { if (this.maxItemHeight < newArea.height) { this.maxItemHeight = newArea.height as number } }) }) } .listDirection(Axis.Horizontal) .backgroundColor('#FFF1F3F5') .height(this.maxItemHeight == -1 ? undefined : this.maxItemHeight) }.width('100%') .height('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433314
-
【HarmonyOS】TaskPool方法不会阻塞UI,如果做上传图片的功能加载Loading记得使用TaskPool,Promise、Async/Await都会阻塞UI【引言】 源于一个论坛帖子:https://developer.huawei.com/consumer/cn/forum/topic/0209156179937828001?fid=0109140870620153026&pid=0308156182059545349发现Promise可能会阻塞UI,尝试使用async或await,但发现它们仍然会导致阻塞。后来看到chaoxiaoshu回复的TaskPool方法,发现使用该方法后UI不再阻塞。因此,我特意编写了一个加载弹窗进行测试,结果同样显示,只有TaskPool方法不会阻塞UI。【代码示例】import { taskpool } from '@kit.ArkTS';@Componentexport struct MyDialog_1 { @Prop dialogID: string @State title: string = '加载中...' build() { Stack() { Column() { LoadingProgress() .color(Color.White).width(100).height(100) Text(this.title) .fontSize(18).fontColor(0xffffff).margin({ top: 8 }) .visibility(this.title ? Visibility.Visible : Visibility.None) } } .onClick(() => { getContext(this).eventHub.emit(this.dialogID, "关闭弹窗") }) .width(180) .height(180) .backgroundColor(0x88000000) .borderRadius(10) .shadow({ radius: 10, color: Color.Gray, offsetX: 3, offsetY: 3 }) }}@Entry@Componentstruct Page28 { @State time3: string = "" @State isShowLoading: boolean = false build() { Stack() { Column({ space: 20 }) { Button("【方案一】测试Promise") .type(ButtonType.Capsule) .onClick(() => { this.isShowLoading = true this.time3 = 'loading...' console.log("start call promise") testPromise(100000000).then((time) => { this.time3 = `耗时:${time}` console.log("promise then") this.isShowLoading = false }) console.log("end call promise") }) Button("【方案二】测试async await") .type(ButtonType.Capsule) .onClick(() => { this.isShowLoading = true this.time3 = 'loading...' console.log("start call promise") this.testPromise() console.log("end call promise") }) Button("【方案三】测试taskpool") .type(ButtonType.Capsule) .onClick(() => { this.isShowLoading = true this.time3 = 'loading...' let task: taskpool.Task = new taskpool.Task(concurrentFunc, 100000000); taskpool.execute(task); task.onReceiveData((time: number) => { this.time3 = `耗时:${time}`; console.log("====end") this.isShowLoading = false }) }) Text(this.time3) }.alignItems(HorizontalAlign.Start) MyDialog_1().visibility(this.isShowLoading ? Visibility.Visible : Visibility.None) }.width('100%').height('100%') } //耗时操作 async testPromise() { let time = await testPromise(100000000) time = new Date().getTime() - time this.time3 = `耗时:${time}毫秒` console.log("promise then") this.isShowLoading = false }}function testPromise(count: number): Promise<number> { return new Promise<number>((resolve) => { let time = Date.now().valueOf() let num = 0 for (let i = 0; i < count; i++) { +num } time = Date.now().valueOf() - time resolve(time) })}@Concurrentfunction concurrentFunc(count: number): void { let time = Date.now().valueOf() let num = 0 for (let i = 0; i < count; i++) { +num } time = Date.now().valueOf() - time taskpool.Task.sendData(time);}【方案一:Promise】优点:易于理解:Promise的语法简单,易于理解和使用。链式调用:可以通过.then进行链式调用,处理多个异步操作。缺点:阻塞UI:在执行耗时任务时,Promise会阻塞UI线程,导致Loading弹窗不能及时显示。【方案二:Async/Await】优点:同步写法:Async/Await 使异步代码看起来像同步代码,更加直观。错误处理:可以使用try/catch块处理错误,使代码更加清晰。缺点:阻塞UI:与Promise类似,Async/Await在执行耗时任务时仍会阻塞UI线程,导致Loading弹窗不能及时显示。【方案三:TaskPool】优点:真正的异步:TaskPool可以将耗时任务放到独立的线程中执行,不会阻塞UI线程,保证了UI的流畅性。数据通信:通过task.onReceiveData可以方便地接收任务结果。缺点:复杂度增加:引入了多线程处理,增加了代码的复杂度和维护成本。【使用注意事项】任务复杂度:如果任务较为简单且不会长时间阻塞UI,可以考虑使用Promise或Async/Await。如果任务较为复杂且耗时较长,建议使用TaskPool以保证UI的流畅性(例如,上传图片时显示加载中)。代码可读性:Promise和Async/Await的语法较为简单,适合初学者使用。TaskPool需要对多线程有一定了解,适合有经验的开发者。性能考虑:TaskPool在处理大量或耗时任务时表现更优,可以显著提升应用性能。Promise和Async/Await在小任务场景下更简洁高效。【总结】选择合适的异步操作方案至关重要。Promise和Async/Await适合处理简单的异步任务,而TaskPool则在处理复杂耗时任务时表现出色。根据实际需求,选择最适合的方案,能有效提升开发效率和用户体验。希望本文对您在异步操作的选择和使用上有所帮助。转载自https://www.cnblogs.com/zhongcx/articles/18433309
-
【HarmonyOS】利用TextPicker实现日期选择框只有【年】或者【年月】或【月日】@Entry@Componentstruct Page39 { @State generateYearMonth: TextCascadePickerRangeContent [] = [] @State generateMonthDay: TextCascadePickerRangeContent [] = [] @State generateYear: TextCascadePickerRangeContent [] = [] generateYearMonthRange(startYear: number, endYear: number): TextCascadePickerRangeContent[] { const range: TextCascadePickerRangeContent[] = []; for (let year = startYear; year <= endYear; year++) { const months: TextCascadePickerRangeContent[] = []; for (let month = 1; month <= 12; month++) { months.push({ text: `${month.toString().padStart(2, '0')}月` // 确保月份是两位数 }); } // 只有当月份数组不为空时,才添加到range中 if (months.length > 0) { range.push({ text: `${year}年`, // 使用年份作为文本 children: months // 只有当月份不为空时,才设置children属性 }); } } return range; // 返回一维数组 } generateMonthDayRange(year: number): TextCascadePickerRangeContent[] { const range: TextCascadePickerRangeContent[] = []; // 生成月份 for (let month = 1; month <= 12; month++) { const days: TextCascadePickerRangeContent[] = []; // 计算每个月的天数 let daysInMonth = new Date(year, month, 0).getDate(); for (let day = 1; day <= daysInMonth; day++) { days.push({ text: `${day.toString().padStart(2, '0')}日` // 确保天数是两位数 }); } range.push({ text: `${month.toString().padStart(2, '0')}月`, // 使用月份作为文本 children: days }); } return range; } generateYearRange(startYear: number, endYear: number): TextCascadePickerRangeContent[] { const range: TextCascadePickerRangeContent[] = []; for (let year = startYear; year <= endYear; year++) { range.push({ text: `${year}年` }); } return range; } aboutToAppear(): void { this.generateYear = this.generateYearRange(2000, 2024); this.generateYearMonth = this.generateYearMonthRange(2000, 2024); this.generateMonthDay = this.generateMonthDayRange(2024); } build() { Column() { Button('指定【年】区间列表') TextPicker({ range: this.generateYear }) .onChange((value: string | string[], index: number | number[]) => { console.info('TextPicker 多列联动:onChange ' + JSON.stringify(value) + ', ' + 'index: ' + JSON.stringify(index)) }) Button('指定【年】【月】区间列表') TextPicker({ range: this.generateYearMonth }) .onChange((value: string | string[], index: number | number[]) => { console.info('TextPicker 多列联动:onChange ' + JSON.stringify(value) + ', ' + 'index: ' + JSON.stringify(index)) }) Button('【月】【日】区间列表') TextPicker({ range: this.generateMonthDay }) .onChange((value: string | string[], index: number | number[]) => { console.info('TextPicker 多列联动:onChange ' + JSON.stringify(value) + ', ' + 'index: ' + JSON.stringify(index)) }) } .height('100%') .width('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433308
-
【HarmonyOS】SaveButton组件把图片显示到相册中的方法demo,支持组件截图、url网络图片、base64格式图片。注意事项:1、不支持自定义SaveButton样式。2、下载按钮被遮挡一部分,也无法保存到相册。import photoAccessHelper from '@ohos.file.photoAccessHelper';import fs from '@ohos.file.fs';import { common } from '@kit.AbilityKit';import { componentSnapshot, promptAction } from '@kit.ArkUI';import { image } from '@kit.ImageKit';import { BusinessError } from '@kit.BasicServicesKit';import { http } from '@kit.NetworkKit';import { util } from '@kit.ArkTS';@Entry@Componentstruct Page09 { inviteQrCodeID: string = "inviteQrCodeID" @State imageUrl: string = "https://img1.baidu.com/it/u=1268271089,1175168242&fm=253&fmt=auto&app=120&f=JPEG?w=506&h=500" @State base64Str: string = 'data:image/png;base64,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' build() { Scroll() { Column({ space: 10 }) { Column({ space: 10 }) { Text('下载组件截图图片到相册') Column() { Column() { Text('标题测试').fontSize('36lpx').height('120lpx').fontColor("#2E2E2E") QRCode('https://www.huawei.com/') .width('300lpx') .height('300lpx') .margin({ top: '25lpx' }) .draggable(false) .width(140) .height(140) }.padding({ left: '42lpx', right: '42lpx', bottom: '20lpx' }) Text('点按下载保存至相册') .textAlign(TextAlign.Center) .padding({ left: '125lpx', right: '125lpx' }) .fontColor("#4B4B4B") .fontSize('32lpx') .margin({ bottom: '70lpx' }) } .id(this.inviteQrCodeID) .padding('20lpx') .margin({ top: '30lpx', bottom: '30lpx' }) .backgroundColor(Color.White) .borderRadius('30lpx') .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) SaveButton().onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => { if (result === SaveButtonOnClickResult.SUCCESS) { const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // 免去权限申请和权限请求等环节,获得临时授权,保存对应图片 let helper = photoAccessHelper.getPhotoAccessHelper(context); try { // onClick触发后5秒内通过createAsset接口创建图片文件,5秒后createAsset权限收回。 let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg'); // 使用uri打开文件,可以持续写入内容,写入过程不受时间限制 let file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); componentSnapshot.get(this.inviteQrCodeID).then((pixelMap) => { let packOpts: image.PackingOption = { format: 'image/png', quality: 100 } const imagePacker: image.ImagePacker = image.createImagePacker(); return imagePacker.packToFile(pixelMap, file.fd, packOpts).finally(() => { imagePacker.release(); //释放 fs.close(file.fd); promptAction.showToast({ message: '图片已保存至相册', duration: 2000 }); }); }) } catch (error) { const err: BusinessError = error as BusinessError; console.error(`Failed to save photo. Code is ${err.code}, message is ${err.message}`); } } else { promptAction.showToast({ message: '设置权限失败!', duration: 2000 }); } }) }.borderWidth(1).borderStyle(BorderStyle.Dotted).backgroundColor(Color.Pink).padding('50lpx') Column({ space: 10 }) { Text('下载网络图片到相册') /** * 需要在 src/main/module.json5 * 添加网络权限 * { "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" }, ], */ Image(this.imageUrl).width('100lpx') SaveButton().onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => { if (result === SaveButtonOnClickResult.SUCCESS) { const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // 免去权限申请和权限请求等环节,获得临时授权,保存对应图片 // savePhotoToGallery(context); let helper = photoAccessHelper.getPhotoAccessHelper(context); try { http.createHttp().request( this.imageUrl, { expectDataType: http.HttpDataType.ARRAY_BUFFER } ).then(async (res) => { console.info('res', JSON.stringify(res)) // 将图片资源转为像素图(PixelMap) let pixelMap = await image.createImageSource(res.result as ArrayBuffer).createPixelMap() // onClick触发后5秒内通过createAsset接口创建图片文件,5秒后createAsset权限收回。 let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg'); // 使用uri打开文件,可以持续写入内容,写入过程不受时间限制 let file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); let packOpts: image.PackingOption = { format: 'image/png', quality: 100 } const imagePacker: image.ImagePacker = image.createImagePacker(); return imagePacker.packToFile(pixelMap, file.fd, packOpts).finally(() => { imagePacker.release(); //释放 fs.close(file.fd); promptAction.showToast({ message: '图片已保存至相册', duration: 2000 }); }); }).catch(() => { console.info('catch') }) } catch (error) { const err: BusinessError = error as BusinessError; console.error(`Failed to save photo. Code is ${err.code}, message is ${err.message}`); } } else { promptAction.showToast({ message: '设置权限失败!', duration: 2000 }); } }) }.borderWidth(1).borderStyle(BorderStyle.Dotted).backgroundColor(Color.Pink).padding('50lpx') Column({ space: 10 }) { Text('下载base64图片到相册') Text('注意1:有些base64的格式图片显示不出来,\n是因为前缀没加data:image/png;base64,').textAlign(TextAlign.Center) Text("注意2:下载到相册的base64字符串不能有'data:image/jpeg;base64,'这样的前缀。所以我这里用正则去掉了前缀").textAlign(TextAlign.Center) SaveButton().onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => { if (result === SaveButtonOnClickResult.SUCCESS) { const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // 免去权限申请和权限请求等环节,获得临时授权,保存对应图片 // savePhotoToGallery(context); let helper = photoAccessHelper.getPhotoAccessHelper(context); try { // 正则表达式用于匹配 "data:image/*;base64," 这样的前缀 const prefixRegex = /^data:image\/[a-zA-Z]+;base64,/; // 使用 replace 方法去除匹配到的前缀 let base64String = this.base64Str.replace(prefixRegex, '') let buffer: ArrayBuffer = new util.Base64Helper().decodeSync(base64String, util.Type.MIME).buffer as ArrayBuffer; let imageSource = image.createImageSource(buffer); let pixelMap = await imageSource.createPixelMap({ editable: true }); let opts: image.PackingOption = { format: "image/jpeg", quality: 100 }; // onClick触发后5秒内通过createAsset接口创建图片文件,5秒后createAsset权限收回。 let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg'); // 使用uri打开文件,可以持续写入内容,写入过程不受时间限制 let file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); let packOpts: image.PackingOption = { format: 'image/png', quality: 100 } const imagePacker: image.ImagePacker = image.createImagePacker(); return imagePacker.packToFile(pixelMap, file.fd, packOpts).finally(() => { imagePacker.release(); //释放 fs.close(file.fd); promptAction.showToast({ message: '图片已保存至相册', duration: 2000 }); }); } catch (error) { const err: BusinessError = error as BusinessError; console.error(`Failed to save photo. Code is ${err.code}, message is ${err.message}`); } } else { console.info(`result:${JSON.stringify(result)}`) promptAction.showToast({ message: '设置权限失败!', duration: 2000 }); } }) } .borderWidth(1).borderStyle(BorderStyle.Dotted).backgroundColor(Color.Pink).padding('50lpx') }.width('100%') }.width('100%') }}注意事项:1、样式不支持自定义图标,试过用opacity修改透明度,然后添加背景来实现自定义样式,结果也失败了,opacity(1)点击生效,opacity(0.9)后点击按钮就不生效了。参考:cid:link_12、如果带Scroll时,下载按钮被遮挡一部分,也无法保存到相册。比如下面这样遮挡一点点下载按钮后,点击就没办法保存到相册了。【参考方案】1、官方文档:cid:link_02、PixelMap和base64的相互转换参考文档:cid:link_2转载自https://www.cnblogs.com/zhongcx/articles/18433304
-
【HarmonyOS】给组件添加下划线或者画虚线边框可以使用.borderStyle和.borderWidth@Entry@Componentstruct Page51 { build() { Column() { Text('虚线边框测试') .width(200) .height(150) .borderStyle(BorderStyle.Dashed) .borderWidth(1) .backgroundColor('#F5F5F5') Row() { Text('利用底部边框实现添加下划线效果') .height('48lpx') .margin({ top: '66lpx' }) .fontSize('28lpx') .fontColor("#FF1919") .textAlign(TextAlign.Center) .borderWidth({ bottom: 1 }) .borderStyle(BorderStyle.Solid) .borderColor("#FF1919") }.width('100%').justifyContent(FlexAlign.Center) } .width('100%') .height('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433301
-
web组件的h5页面调用鸿蒙app图库和拍照示例 1、添加权限:entry/src/main/module.json5 2、测试文件:src/main/resources/rawfile/page107.html<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>更换头像示例</title> <style> body { /* 确保图片不会超出屏幕宽度 */ margin: 0; padding: 0; overflow-x: hidden; } #avatarPreview { max-width: 50vw; /* 设置图片最大宽度为屏幕宽度的50% */ display: block; /* 确保图片作为块级元素显示 */ margin: 10px auto; /* 居中图片 */ } #avatarPreview img { width: 100%; /* 图片宽度自动适应其容器宽度 */ height: auto; /* 高度自适应保持图片比例 */ } </style></head><body><h2>点击选择新头像</h2><input type="file" id="avatarInput" accept="image/*" style="margin-bottom:10px;"><button onclick="uploadAvatar()">上传头像至OSS</button><button onclick="changeAvatar()">显示头像</button><div id="avatarPreview"></div><script> async function uploadAvatar() { const input = document.getElementById('avatarInput'); if (input.files && input.files[0]) { // 模拟异步上传图片至OSS的过程 try { const formData = new FormData(); formData.append('file', input.files[0]); // 这里使用fetch API模拟上传请求,实际应用中需要替换为真实的服务端API地址 const response = await fetch('https://your-fake-oss-api.com/upload', { method: 'POST', body: formData }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); alert('图片上传成功!服务器响应:' + JSON.stringify(result)); } catch (error) { alert('图片上传失败:' + error); } } } function changeAvatar() { const input = document.getElementById('avatarInput'); if (input.files && input.files[0]) { const reader = new FileReader(); reader.onload = function(e) { document.getElementById('avatarPreview').innerHTML = `<img src="${e.target.result}" alt="头像预览">`; }; reader.readAsDataURL(input.files[0]); } }</script></body></html>3、示例代码:src/main/ets/pages/Page107.etsimport web_webview from '@ohos.web.webview';import picker from '@ohos.file.picker';import fs from '@ohos.file.fs';import { common } from '@kit.AbilityKit';interface MyEvent { result: FileSelectorResult, fileSelector: FileSelectorParam}@Entry@Componentstruct Page107 { controller: web_webview.WebviewController = new web_webview.WebviewController(); handleFileSelection(event: MyEvent) { const PhotoSelectOptions = new picker.PhotoSelectOptions(); PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; PhotoSelectOptions.maxSelectNumber = 1; const photoPicker = new picker.PhotoViewPicker(); photoPicker.select(PhotoSelectOptions) .then((PhotoSelectResult) => { if (PhotoSelectResult.photoUris.length === 0) { console.warn('No image selected.'); return; } const srcUri = PhotoSelectResult.photoUris[0]; const context = getContext(this) as common.UIAbilityContext; const destPath = `${context.filesDir}/test${new Date().getTime()}.jpg`; try { let file = fs.openSync(srcUri, fs.OpenMode.READ_ONLY); fs.copyFileSync(file.fd, destPath); event?.result.handleFileList([destPath]); } catch (copyError) { console.error('Copying the file failed:', JSON.stringify(copyError)); } }) .catch((selectError: object) => { console.error('Failed to invoke photo picker:', JSON.stringify(selectError)); }); return true; } build() { Column() { Web({ src: $rawfile('page107.html'), // src: 'https://xxx', controller: this.controller }) .width('100%') .height('100%') .domStorageAccess(true)//设置是否开启文档对象模型存储接口(DOM Storage API)权限。 .javaScriptAccess(true)//设置是否允许执行JavaScript脚本,默认允许执行。 .databaseAccess(true)//设置是否开启数据库存储API权限,默认不开启。 .mixedMode(MixedMode.All)//HTTP和HTTPS混合 .fileAccess(true)//设置是否开启应用中文件系统的访问,默认启用。 .imageAccess(true)//设置是否允许自动加载图片资源,默认允许。 .geolocationAccess(true)//设置是否开启获取地理位置权限,默认开启。 .onlineImageAccess(true)//设置是否允许从网络加载图片资源(通过HTTP和HTTPS访问的资源),默认允许访问。 .mediaPlayGestureAccess(true)//设置有声视频播放是否需要用户手动点击,静音视频播放不受该接口管控,默认需要。 .onShowFileSelector(this.handleFileSelection.bind(this)) } .width('100%') .height('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433297
-
实现一个自定义带圆点的进度条效果【起因】论坛里有人问进度条如何带圆点,参考:https://developer.huawei.com/consumer/cn/forum/topic/0208157230875079489?fid=0101587866109860105&pid=0301157237673947937方案就是做一个圆角组件,然后利用rotate旋转,至于动画效果,我查了一下文档,只要设置enableSmoothEffect:false,就可以关闭动画,然后自己开个定时器,判断实际进度与动画进度的差值每隔10毫秒执行一次就行了。上面的gif图比较卡是因为录屏转gif掉帧了哈,实际代码执行很流畅。【代码】@Entry@Componentstruct Page03 { @State value: number = 70 //实际进度,单位% @State valueAnim: number = 10 //动画进度,单位% progressId: number = 0 aboutToAppear(): void { this.progressId = setInterval(() => { if (this.value > this.valueAnim) { this.valueAnim += 1 } else if (this.value < this.valueAnim) { this.valueAnim -= 1 } }, 10) } aboutToDisappear(): void { clearInterval(this.progressId) } build() { Column() { Button('设置为0%').onClick(() => { this.value = 0 }) Button('设置为50%').onClick(() => { this.value = 50 }) Button('设置为68%').onClick(() => { this.value = 68 }) Button('设置为100%').onClick(() => { this.value = 100 }) Stack() { // Image() //这里展示向内渐变的圆形图片做北京 Text(`${this.valueAnim}%`) //这里展示进度 Text('本月任务进度').fontSize('15lpx').margin({ top: '100lpx' }) Progress({ value: this.valueAnim, total: 100, type: ProgressType.Ring }) .width('200lpx').color(Color.Orange) .style({ strokeWidth: 5, shadow: false ,enableSmoothEffect:false}) //这里系统进度条,可以实现无圆点进度 Text().width('50lpx') //重点来了,这里的图片是一个png,但只有正上方有一个白芯的圆点,然后根据进度计算角度把图片进行旋转,也就是Image().rotate(根据进度计算角度哈) Stack() { Text() .backgroundColor(Color.White) .borderColor(Color.Orange) .borderWidth(5) .width('27lpx') .height('27lpx') .borderRadius('50lpx') }.width('220lpx').height('220lpx').align(Alignment.Top).rotate({angle:this.valueAnim / 100 * 360}) }.width('300lpx').height('300lpx').backgroundColor(Color.Pink) } .height('100%') .width('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433293
-
实现Text组件中文本两端对齐方案@Entry@Componentstruct Page29 { @State str_1 :string = '文本内容' @State str_2 :string = '文本内容文本内容abcd1234也对也不对,八荣八耻。' build() { Column() { Text('单行效果') Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) { ForEach(this.str_1.split(''), (item: string, index: number) => { Text(item) .fontSize(14) }) }.width(100) .backgroundColor(Color.Pink) Text('多行效果') Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) { ForEach(this.str_2.split(''), (item: string, index: number) => { Text(item) .fontSize(14) }) }.width(100) .backgroundColor(Color.Pink) } .height('100%') .width('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433285
-
实现分页滚动文本组件:为何选择 Scroll + Text 而非 textOverflowimport { promptAction } from '@kit.ArkUI'@Entry@Componentstruct Page37 { @State lineHeight: number = 0 // 单行文本的高度 @State pageHeight: number = 0 // 每页的最大高度 @State totalContentHeight: number = 0 // 整个文本内容的高度 @State textContent: string = " " // 文本内容,默认一个空格是为了计算单行文本的高度 @State scrollOffset: number = 0 // 当前滚动偏移量 @State totalPages: number = 1 // 总页数 @State currentPage: number = 1 // 当前页数 scroller: Scroller = new Scroller() // 滚动条实例 resetMaxLineHeight() { if (this.lineHeight > 0 && this.pageHeight > 0 && this.totalContentHeight > 0) { this.pageHeight = (Math.floor(this.pageHeight / this.lineHeight)) * this.lineHeight this.totalPages = Math.ceil(this.totalContentHeight / this.pageHeight) //向上取整得到总页数 } } build() { Column() { Text('第一章') .margin({ top: 10, bottom: 10 }) .backgroundColor(Color.Pink) .width('100%') .textAlign(TextAlign.Center) Column() { Scroll(this.scroller) { Column() { Text(this.textContent) .backgroundColor(Color.Orange) .fontSize(20) .lineHeight(40) .fontColor(Color.Black)// .textOverflow({ overflow: TextOverflow.Clip }) .margin({ top: this.scrollOffset }) .onAreaChange((oldArea: Area, newArea: Area) => { if (this.lineHeight == 0 && newArea.height > 0) { this.lineHeight = newArea.height as number this.resetMaxLineHeight() //添加数据测试 let str = "" for (let i = 1; i <= 20; i++) { str += ` ${i}、荣誉和耻辱,是荣辱观中的一对基本范畴,是指社会对人们行为褒贬评价以及人们对这种评价的自我感受。知荣辱,是人性的标志,是人区别于动物、人之为人的重要标准。` } this.textContent = str return } if (this.totalContentHeight != newArea.height) { console.info(`newArea.height:${newArea.height}`) this.totalContentHeight = newArea.height as number this.resetMaxLineHeight() } }) }.hitTestBehavior(HitTestMode.Block) //禁止滑动 }.scrollBar(BarState.Off) .constraintSize({ maxHeight: this.pageHeight == 0 ? 1000 : this.pageHeight }) } .width('100%') .layoutWeight(1) .onAreaChange((oldArea: Area, newArea: Area) => { if (this.pageHeight == 0 && newArea.height > 0) { this.pageHeight = newArea.height as number this.resetMaxLineHeight() } }) Row() { Button('上一页').onClick(() => { if (this.currentPage == 1) { promptAction.showToast({ message: "没有上一页了" }) return; } this.scrollOffset += this.pageHeight this.currentPage--; }) Text(`${this.currentPage}/${this.totalPages}`) Button('下一页').onClick(() => { if (this.currentPage == this.totalPages) { promptAction.showToast({ message: "没有下一页了" }) return; } this.scrollOffset -= this.pageHeight this.currentPage++; }) }.margin({ top: 10, bottom: 10 }).backgroundColor(Color.Pink).width('100%').justifyContent(FlexAlign.SpaceAround) } .width('100%') .height('100%') .backgroundColor(Color.Gray) }}【实现思路】目标是实现在HarmonyOS应用中的分页滚动文本效果,使得用户能够通过“上一页”和“下一页”按钮来浏览不同的页面。我们选择使用 Scroll 组件结合 Text 组件来实现这一功能,而不是采用 textOverflow 的方式,原因在于 textOverflow 无法直接获取到文本控件被截断后的内容。具体实现过程如下:初始化状态:利用 @State 装饰器定义状态变量来存储单行文本的高度 (lineHeight)、每页的最大高度 (pageHeight)、文本内容的总高度 (totalContentHeight)、文本内容 (textContent)、滚动偏移量 (scrollOffset)、总页数 (totalPages) 和当前页数 (currentPage)。计算单行高度:通过监听 Text 组件的 onAreaChange 事件,当首次获取到文本元素的高度时,将其赋值给 lineHeight 并调用 resetMaxLineHeight 方法来计算每页的最大高度。生成内容:初始时,textContent 中包含一个空格,以便能够计算出单行文本的高度。一旦单行高度计算完成,通过循环生成多个段落填充文本内容。分页逻辑:① 当 totalContentHeight 发生变化时,调用 resetMaxLineHeight 方法更新总页数。② “上一页”和“下一页”按钮通过修改 scrollOffset 和 currentPage 来实现翻页效果。UI 布局与滚动控制:① 使用 Column 和 Row 布局来组织界面元素。② Scroll 组件用于创建滚动区域,而 Text 组件则用于显示文本内容。③ 通过设置 hitTestBehavior 为 HitTestMode.Block 来阻止文本区域的滑动行为,确保滚动仅发生在父级滚动区域中。适配不同屏幕尺寸:① 为了确保组件在不同设备上的表现一致,可以考虑使用百分比布局或者动态计算容器尺寸的方法来适应不同屏幕尺寸。② 通过设置 Scroll 组件的 constraintSize 属性,限制其最大高度为 pageHeight 或默认值 1000,以确保内容不会超出当前页面的高度。动态计算内容高度:① 通过监听 Scroll 组件的 onAreaChange 事件,当容器高度发生变化时,重新计算 pageHeight 和 totalPages。② 这样可以确保组件能够动态地适应不同的屏幕尺寸和内容长度,避免内容溢出或遮挡问题。【为何不使用 textOverflow?】① 无法直接获取截断后的内容: textOverflow 主要用于处理文本过长时的显示问题,但不能直接获取到文本被截断后的内容。这使得在分页时难以准确判断当前页面显示的是文本的哪一部分。② 难以实现分页逻辑: 由于 textOverflow 不提供获取截断文本内容的API,因此难以实现精确的分页逻辑,比如计算每页显示的内容范围。③ 用户体验: 使用 Scroll 和 Text 的组合可以更好地控制文本的显示和分页,从而提供更平滑的阅读体验。【总结】通过上述步骤,构建了一个简单但功能完备的分页滚动文本组件,可用于展示长文本内容,适用于多种场景。用户可以方便地通过“上一页”和“下一页”按钮浏览不同页面,而无需担心内容的显示问题。转载自https://www.cnblogs.com/zhongcx/articles/18433276
-
【HarmonyOS】普通组件与web组件长截屏方案:原则是利用Scroll内的组件可以使用componentSnapshot完整的截屏【普通组件长截屏】import { componentSnapshot, promptAction } from '@kit.ArkUI'import { common } from '@kit.AbilityKit'import { photoAccessHelper } from '@kit.MediaLibraryKit'import fs from '@ohos.file.fs';import { image } from '@kit.ImageKit';import { BusinessError } from '@kit.BasicServicesKit';@Entry@Componentstruct Page37 { @State lineHeight: number = 0 // 单行文本的高度 @State pageHeight: number = 0 // 每页的最大高度 @State totalContentHeight: number = 0 // 整个文本内容的高度 @State textContent: string = " " // 文本内容,默认一个空格是为了计算单行文本的高度 @State scrollOffset: number = 0 // 当前滚动偏移量 @State totalPages: number = 1 // 总页数 @State currentPage: number = 1 // 当前页数 scroller: Scroller = new Scroller() // 滚动条实例 resetMaxLineHeight() { if (this.lineHeight > 0 && this.pageHeight > 0 && this.totalContentHeight > 0) { this.pageHeight = (Math.floor(this.pageHeight / this.lineHeight)) * this.lineHeight this.totalPages = Math.ceil(this.totalContentHeight / this.pageHeight) //向上取整得到总页数 } } build() { Column() { SaveButton().onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => { if (result === SaveButtonOnClickResult.SUCCESS) { const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // 免去权限申请和权限请求等环节,获得临时授权,保存对应图片 let helper = photoAccessHelper.getPhotoAccessHelper(context); try { // onClick触发后5秒内通过createAsset接口创建图片文件,5秒后createAsset权限收回。 let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg'); // 使用uri打开文件,可以持续写入内容,写入过程不受时间限制 let file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); componentSnapshot.get("aaaa").then((pixelMap) => { let packOpts: image.PackingOption = { format: 'image/png', quality: 100 } const imagePacker: image.ImagePacker = image.createImagePacker(); return imagePacker.packToFile(pixelMap, file.fd, packOpts).finally(() => { imagePacker.release(); //释放 fs.close(file.fd); promptAction.showToast({ message: '图片已保存至相册', duration: 2000 }); }); }) } catch (error) { const err: BusinessError = error as BusinessError; console.error(`Failed to save photo. Code is ${err.code}, message is ${err.message}`); } } else { promptAction.showToast({ message: '设置权限失败!', duration: 2000 }); } }) Text('第一章') .margin({ top: 10, bottom: 10 }) .backgroundColor(Color.Pink) .width('100%') .textAlign(TextAlign.Center) Column() { Scroll(this.scroller) { Column() { Text(this.textContent) .id('aaaa') .backgroundColor(Color.Orange) .fontSize(20) .lineHeight(40) .fontColor(Color.Black)// .textOverflow({ overflow: TextOverflow.Clip }) .margin({ top: this.scrollOffset }) .onAreaChange((oldArea: Area, newArea: Area) => { if (this.lineHeight == 0 && newArea.height > 0) { this.lineHeight = newArea.height as number this.resetMaxLineHeight() //添加数据测试 let str = "" for (let i = 1; i <= 20; i++) { str += ` ${i}、荣誉和耻辱,是荣辱观中的一对基本范畴,是指社会对人们行为褒贬评价以及人们对这种评价的自我感受。知荣辱,是人性的标志,是人区别于动物、人之为人的重要标准。` } this.textContent = str return } if (this.totalContentHeight != newArea.height) { console.info(`newArea.height:${newArea.height}`) this.totalContentHeight = newArea.height as number this.resetMaxLineHeight() } }) }.hitTestBehavior(HitTestMode.Block) //禁止滑动 }.scrollBar(BarState.Off) .constraintSize({ maxHeight: this.pageHeight == 0 ? 1000 : this.pageHeight }) } .width('100%') .layoutWeight(1) .onAreaChange((oldArea: Area, newArea: Area) => { if (this.pageHeight == 0 && newArea.height > 0) { this.pageHeight = newArea.height as number this.resetMaxLineHeight() } }) Row() { Button('上一页').onClick(() => { if (this.currentPage == 1) { promptAction.showToast({ message: "没有上一页了" }) return; } this.scrollOffset += this.pageHeight this.currentPage--; }) Text(`${this.currentPage}/${this.totalPages}`) Button('下一页').onClick(() => { if (this.currentPage == this.totalPages) { promptAction.showToast({ message: "没有下一页了" }) return; } this.scrollOffset -= this.pageHeight this.currentPage++; }) }.margin({ top: 10, bottom: 10 }).backgroundColor(Color.Pink).width('100%').justifyContent(FlexAlign.SpaceAround) } .width('100%') .height('100%') .backgroundColor(Color.Gray) }}【web组件长截屏】src/main/resources/rawfile/test.html<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" /> <script> //用于根据浏览器对 CSS.supports 和 env/constant 的支持情况,动态地调整视口元标签的内容,以达到最佳的页面显示效果。 var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || CSS.supports('top: constant(a)')) document.write( '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + (coverSupport ? ', viewport-fit=cover' : '') + '" />') </script> <title></title> <!--用于设置浏览器页签上显示的小图标 start--> <!-- <link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" /> --> <link rel="stylesheet" href="mycss.css" /> <link rel="icon" href="./static/favicon.ico" /> <!--用于设置浏览器页签上显示的小图标 end--> <!--preload-links--> <!--app-context--></head><body><div>测试测试</div><div>测试测试</div><div>测试测试</div><div>测试测试</div><div>测试测试</div><div>测试测试</div><div>测试测试</div><div>测试测试</div><div>哈哈哈哈</div><div>哈哈哈哈</div><div>哈哈哈哈</div><div>哈哈哈哈</div><div>哈哈哈哈</div><div>哈哈哈哈</div><div>哈哈哈哈</div><div>哈哈哈哈</div><div>1111111</div><div>1111111</div><div>1111111</div><div>1111111</div><div>1111111</div><div>1111111</div><div>1111111</div><div>1111111</div><div>1111111</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>2222222</div><div>aaaaaaa</div><div>aaaaaaa</div><div>aaaaaaa</div><div>aaaaaaa</div><div>aaaaaaa</div><div>aaaaaaa</div><div>aaaaaaa</div><div>aaaaaaa</div><div>aaaaaaa</div><div>aaaaaaa</div><div>aaaaaaa</div><div>aaaaaaa</div><div>aaaaaaa</div><div>bbbbbbb</div><div>bbbbbbb</div><div>bbbbbbb</div><div>bbbbbbb</div><div>bbbbbbb</div><div>bbbbbbb</div><div>bbbbbbb</div><div>bbbbbbb</div><div>bbbbbbb</div><div>bbbbbbb</div><div>bbbbbbb</div><div>bbbbbbb</div><div>bbbbbbb</div><div>bbbbbbb</div><div>到底了</div><div id="webBottom"></div></body><script> //Android禁止微信调整字体大小 (function() { if (typeof WeixinJSBridge == "object" && typeof WeixinJSBridge.invoke == "function") { handleFontSize(); } else { if (document.addEventListener) { document.addEventListener("WeixinJSBridgeReady", handleFontSize, false); } else if (document.attachEvent) { document.attachEvent("WeixinJSBridgeReady", handleFontSize); document.attachEvent("onWeixinJSBridgeReady", handleFontSize); } } function handleFontSize() { WeixinJSBridge.invoke('setFontSizeCallback', { 'fontSize': 0 }); WeixinJSBridge.on('menu:setfont', function() { WeixinJSBridge.invoke('setFontSizeCallback', { 'fontSize': 0 }); }); } })(); function setWebHeight() { window.hm.setWebHeight(document.getElementById('webBottom').offsetTop); } // 在文档加载完成后执行 setWebHeight 函数 window.onload = function() { setWebHeight(); };</script></html>src/main/ets/pages/Page42.etsimport { webview } from '@kit.ArkWeb';import web_webview from '@ohos.web.webview';import dataPreferences from '@ohos.data.preferences';import { common } from '@kit.AbilityKit';import { photoAccessHelper } from '@kit.MediaLibraryKit';import { componentSnapshot, promptAction } from '@kit.ArkUI';import fs from '@ohos.file.fs';import { image } from '@kit.ImageKit';import { BusinessError } from '@kit.BasicServicesKit';class WebService { setWebHeight = (height: string) => { console.info('web高度:', height); getContext().eventHub.emit("设置web高度",height) }}@Entry@Componentstruct Page42 { controller: webview.WebviewController = new webview.WebviewController(); webService: WebService = new WebService( ); methodList: Array<string> = [] @State isShort: boolean = true @State webHeight: number | undefined = undefined aboutToAppear(): void { this.methodList.splice(0) //清空原数组 console.info('====this.testObjtest', JSON.stringify(this.webService)) Object.keys(this.webService).forEach((key) => { this.methodList.push(key) console.info('====key', key) }); getContext().eventHub.on("设置web高度",(height:number)=>{ this.webHeight = height }) } build() { Scroll() { Column() { SaveButton().onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => { if (result === SaveButtonOnClickResult.SUCCESS) { const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // 免去权限申请和权限请求等环节,获得临时授权,保存对应图片 let helper = photoAccessHelper.getPhotoAccessHelper(context); try { // onClick触发后5秒内通过createAsset接口创建图片文件,5秒后createAsset权限收回。 let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg'); // 使用uri打开文件,可以持续写入内容,写入过程不受时间限制 let file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); componentSnapshot.get("aaaa").then((pixelMap) => { let packOpts: image.PackingOption = { format: 'image/png', quality: 100 } const imagePacker: image.ImagePacker = image.createImagePacker(); return imagePacker.packToFile(pixelMap, file.fd, packOpts).finally(() => { imagePacker.release(); //释放 fs.close(file.fd); promptAction.showToast({ message: '图片已保存至相册', duration: 2000 }); }); }) } catch (error) { const err: BusinessError = error as BusinessError; console.error(`Failed to save photo. Code is ${err.code}, message is ${err.message}`); } } else { promptAction.showToast({ message: '设置权限失败!', duration: 2000 }); } }) Text('1234测试顶部').backgroundColor(Color.Red).width('100%').height('800lpx') Web({ src: $rawfile('test.html'), controller: this.controller, renderMode: RenderMode.SYNC_RENDER }) .width('100%') .height(this.webHeight) .layoutMode(WebLayoutMode.FIT_CONTENT) .javaScriptAccess(true)//设置是否允许执行JavaScript脚本,默认允许执行。 .mixedMode(MixedMode.All)//HTTP和HTTPS混合 .javaScriptProxy({ name: "hm", object: this.webService, methodList: this.methodList, controller: this.controller, }).id("aaaa") Text('测试底部').backgroundColor(Color.Blue).width('100%').height('800lpx') } }.width('100%').height('100%').align(Alignment.Top) }}转载自https://www.cnblogs.com/zhongcx/articles/18433271
-
华为开发者布道师技术沙龙西安站的活动中,来自西安交通大学的谢涛老师作了题为“产学合作背景下的西安交通大学鸿蒙实践”的课程改革经验分享。谢涛老师分别从产学合作背景、西安交通大学鸿蒙实践、成效与反馈、未来展望等方面阐述了产学合作背景下西安交通大学的鸿蒙实践及效果。
-
鸿蒙开发实战:分析服务(HarmonyOS-JavaScript)介绍鸿蒙操作系统(HarmonyOS)是由华为开发的一款分布式操作系统,旨在实现跨设备的无缝连接。其核心设计理念是“一个内核支持多种设备”,包括智能手机、可穿戴设备、智慧屏等。本文将重点介绍如何在HarmonyOS中使用JavaScript进行分析服务的开发。应用使用场景分析服务可以用于以下场景:用户行为分析:收集应用程序中的用户交互数据,以优化用户体验。性能监控:实时获取设备性能指标,提高应用程序响应速度和稳定性。故障诊断:捕获并分析异常事件,以快速定位和修复问题。原理解释分析服务的核心是数据的收集与处理。通过仪表化代码(Instrumentation Code)来收集特定的信息,并将这些信息上传到服务器进行进一步分析。算法原理流程图[Start] --> [Instrumentation Code] --> [Data Collection] --> [Data Transmission] --> [Server Analysis] --> [Result Feedback] --> [End]算法原理解释Instrumentation Code:在应用源代码中嵌入代码,以便记录用户交互、性能指标或异常事件。Data Collection:收集来自不同设备的数据,可能涉及日志、计数器或指标。Data Transmission:通过网络将收集的数据发送到后端服务器。Server Analysis:在服务器上对数据进行清洗和分析,生成有意义的报告或采取自动反应措施。Result Feedback:将分析结果反馈给应用开发者,帮助优化应用。实际详细应用代码示例实现以下是一个简单的JavaScript代码示例,用于在HarmonyOS应用中实现基本的用户行为分析。import analytics from '@system.analytics'; // 初始化分析服务 analytics.enableAnalytics(true); // 定义一个函数来记录点击事件 function logClickEvent(buttonName) { analytics.record({ event: 'button_click', params: { button_name: buttonName, timestamp: new Date().toISOString() } }); } // 绑定按钮点击事件 document.getElementById('myButton').addEventListener('click', function() { logClickEvent('myButton'); });测试代码测试代码应该确保点击事件被正确记录,并且数据被成功传输到服务器。// 模拟点击事件 document.getElementById('myButton').click(); // 检查控制台日志以验证记录 console.log('Check if button click event is recorded.');部署场景移动应用:分析用户在App上的行为和偏好。智慧家居:监测设备使用情况及性能,为用户提供优化建议。企业系统:追踪员工在内部系统中的操作,提升工作效率。材料链接HarmonyOS 官方文档JavaScript 开发指南总结分析服务在HarmonyOS的生态体系中扮演着重要角色,它不仅有助于理解用户需求,还能够提高系统的稳定性和响应速度。通过合理的分析策略,开发者可以持续优化应用的用户体验和性能。未来展望随着物联网设备的普及和技术的不断进步,分析服务的作用将愈发重要。未来,我们期待看到更智能化、更自适应的分析工具,帮助开发者更有效地利用数据,推动创新应用的实现。
-
HarmonyOS 鸿蒙开发测试:分布式UI测试框架介绍HarmonyOS 是由华为开发的操作系统,旨在提供跨设备的无缝体验。分布式 UI 测试框架是其中一个重要组件,它使开发者能够在不同设备上进行 UI 的自动化测试,以确保一致性和可靠性。应用使用场景多设备交互测试: 检测应用在手机、平板、智能电视等多个设备上的表现。UI 一致性验证: 确保用户界面在不同尺寸屏幕上的呈现一致。性能测试: 评估应用在分布式场景下的性能表现。原理解释分布式 UI 测试框架依赖于 HarmonyOS 的分布式特性,通过统一接口将测试案例分发到各个设备上执行。每个设备独立运行测试并返回结果,最终综合分析以确保应用的稳定性和一致性。算法原理流程图+------------------------+ | Start Test Framework | +-----------+------------+ | v +-----------v------------+ | Load Test Cases | +-----------+------------+ | v +-----------v------------+ | Distribute Test | | Cases to Devices | +-----------+------------+ | v +-----------v------------+ | Execute Tests on Each | | Device | +-----------+------------+ | v +-----------v------------+ | Collect Results from | | All Devices | +-----------+------------+ | v +-----------v------------+ | Analyze and Report | | Results | +-----------+------------+ | v +-----------v------------+ | End Test Framework | +------------------------+算法原理解释加载测试用例: 从配置文件或数据库中获取需要执行的测试用例。分发测试用例: 通过网络连接(如 Wi-Fi、Bluetooth)将测试用例分配到各个目标设备。执行测试: 各设备独立执行所分配的测试用例,并记录结果。收集结果: 测试结果上传至中央服务器进行汇总。分析与报告: 生成详细的测试报告,包括错误日志和性能指标。实际详细应用代码示例实现下面是一个简单的 Python 伪代码,演示如何实现一个基本的分布式 UI 测试框架:def load_test_cases(): # 假设从某个配置源加载 return ["test_case_1", "test_case_2", "test_case_3"] def distribute_tests_to_devices(test_cases, devices): for device in devices: # 将测试用例发送到设备 send_tests(device, test_cases) def execute_tests_on_device(device, test_cases): results = [] for test in test_cases: result = run_single_test(device, test) results.append(result) return results def collect_results_from_devices(devices): all_results = [] for device in devices: results = get_results_from_device(device) all_results.extend(results) return all_results def analyze_and_report(results): # 简单分析和报告生成 pass # 主程序入口 if __name__ == "__main__": devices = ["device_1", "device_2"] test_cases = load_test_cases() distribute_tests_to_devices(test_cases, devices) results = collect_results_from_devices(devices) analyze_and_report(results)测试代码、部署场景测试代码应在真实设备环境下进行,以模拟实际使用场景。可以使用 docker 或虚拟机来搭建测试环境,但最有效的是直接在物理设备上部署。材料链接HarmonyOS 官方文档鸿蒙开源项目 GitHub总结HarmonyOS 分布式 UI 测试框架是一个强大的工具,用于确保不同设备间应用的一致性和性能。其核心是利用 HarmonyOS 的分布式能力,实现跨设备自动化测试,从而提升开发效率和产品质量。未来展望随着 IoT 设备的普及,分布式 UI 测试的重要性将会越来越高。在未来,该框架可能会进一步扩展支持,更好地适应新设备和新的交互方式,同时随着 AI 技术的发展,可能引入更智能的分析和优化算法。
-
HarmonyOS 鸿蒙开发测试: 安全测试介绍HarmonyOS(鸿蒙操作系统)是由华为开发的分布式操作系统,旨在提供一种跨设备的统一体验。安全测试在HarmonyOS中至关重要,因为一个漏洞可能会影响多个连接设备。本次讨论将涉及HarmonyOS中的安全测试,包括应用使用场景、原理解释、算法流程及实现。应用使用场景智能家居设备:确保所有联网设备的通信安全。移动设备:保护用户数据和隐私。物联网设备:保证传感器与控制器的安全交互。汽车系统:维护车载信息娱乐系统的安全性。原理解释HarmonyOS的安全框架包括:权限管理:应用程序需要申请适当的权限才能访问敏感数据。数据加密:对数据进行加密以防止未经授权的访问。沙箱机制:每个应用运行在隔离的环境中,减少相互干扰风险。算法流程图以下是基本的安全测试算法流程图:┌───────────────────────┐ │ 开始 │ ├───────────────────────┤ │ 初始化测试环境 │ ├───────────────────────┤ │ 加载待测应用 │ ├───────────────────────┤ │ 检查权限设置 │ ├───────────────────────┤ │ 执行安全扫描 │ ├───────────────────────┤ │ 分析扫描结果 │ ├────────────┬──────────────┤ │ │ 无漏洞 │ 有漏洞 │ │ └──────────────┘ ┌───────────────┘ └──────────────▲──────────────┘ 修复并重测 │ │ │ └─────────────────────────────┘算法原理解释初始化:设定测试基础设施,包括网络模拟器、设备模拟器等。加载 & 权限检查:确保应用所宣称的权限合理,不超出实际需求。安全扫描:使用自动化工具检测已知的安全漏洞。分析和修复:根据扫描报告分析漏洞并进行修复,再次测试直至通过。实际详细应用代码示例实现// A simplified example of checking permissions in a HarmonyOS app public void checkPermissions(Context context) { if (context.verifySelfPermission("ohos.permission.INTERNET") != IBundleManager.PERMISSION_GRANTED) { throw new SecurityException("Internet permission is not granted"); } }测试代码@Test public void testCheckPermissions() { Context mockContext = Mockito.mock(Context.class); Mockito.when(mockContext.verifySelfPermission("ohos.permission.INTERNET")) .thenReturn(IBundleManager.PERMISSION_DENIED); try { checkPermissions(mockContext); fail("SecurityException expected but not thrown"); } catch (SecurityException e) { // Test passes } }部署场景生产部署:在发布应用之前,通过CI/CD流水线集成安全测试。开发阶段:作为开发者本地测试的一部分,以便及时发现并解决问题。材料链接HarmonyOS 官方文档OWASP 移动应用安全测试指南总结在HarmonyOS中的安全测试是确保应用和系统健壮性的关键步骤。通过良好的权限管理、数据加密、沙箱机制以及自动化工具的结合,可以有效降低安全风险。未来展望随着物联网设备数量的激增,安全问题将变得更加复杂。未来,HarmonyOS的安全测试可能会更多地引入人工智能技术,以提高漏洞检测的效率和精准度。同时,加强与开源社区和安全研究机构的合作也将有助于构建更为安全的生态系统。
-
HarmonyOS 鸿蒙开发测试: 游戏测试介绍HarmonyOS(鸿蒙)是由华为开发的分布式操作系统,旨在为全场景智慧生态提供统一的平台。它支持多设备协同工作,使得应用可以跨设备无缝运行。在游戏开发和测试方面,HarmonyOS 提供了丰富的接口和工具,使开发者能够创建出更为智能和高效的游戏体验。应用使用场景跨设备游戏体验: 在电视、手机、平板等设备间无缝切换游戏进程。增强现实游戏: 利用 HarmonyOS 的分布式能力,将游戏与现实世界相结合。多人在线游戏: 通过设备互联实现实时多人互动。原理解释HarmonyOS 的游戏测试主要依托于其分布式架构和轻量化虚拟机(Ark Compiler),这使得代码能够高效地在多个平台上执行。通过分布式数据管理和任务调度,确保游戏在不同设备之间流畅运行。算法原理流程图Start | v Initialize Game Environment | v Load Game Assets | v Initialize Network Components | v Enter Main Game Loop |---> Check Input Events ----| | | v v Update Game Logic Render Graphics | | |<------ Synchronize State -| | v End Game Loop | v Terminate Game Session算法原理解释初始化游戏环境: 设置屏幕大小、音频等配置信息。加载游戏资源: 包含纹理、声音、关卡数据等。初始化网络组件: 准备好用于多设备通信的网络模块。进入主游戏循环: 持续检测玩家输入、更新游戏状态和渲染画面。同步状态: 确保分布式环境下所有设备的游戏状态一致。实际详细应用代码示例实现// 简单的 JS 游戏元素移动示例 for HarmonyOS let playerPosition = { x: 0, y: 0 }; function movePlayer(direction) { switch (direction) { case 'left': playerPosition.x -= 1; break; case 'right': playerPosition.x += 1; break; case 'up': playerPosition.y -= 1; break; case 'down': playerPosition.y += 1; break; } renderPlayer(); } function renderPlayer() { console.log(`Player is at (${playerPosition.x}, ${playerPosition.y})`); } // 模拟游戏循环 setInterval(() => { let randomDirection = ['left', 'right', 'up', 'down'][Math.floor(Math.random() * 4)]; movePlayer(randomDirection); }, 1000);测试代码、部署场景在实验室环境中进行测试,使用多台设备模拟现实场景。需考虑网速、延迟等对分布式游戏的影响。材料链接HarmonyOS 官方文档Ark Compiler 介绍总结HarmonyOS 为游戏开发者提供了强大的工具和平台,简化了跨设备应用的开发难度,同时提升了用户体验。未来展望随着 HarmonyOS 生态的逐步完善,预计将看到更多基于该平台的创新游戏应用涌现。未来可能会有更多关于AI和AR技术的集成,进一步增强交互性和沉浸感。
上滑加载中
推荐直播
-
华为云码道 × 仓颉编程:工程化AI编码探索2026/05/27 周三 19:00-21:00
刘俊杰-华为云仓颉语言专家/李炎-华为云码道技术专家/王智鹏-OpenCangjie开源社区发起人
本场直播围绕华为云仓颉语言与华为云码道的深度结合,展示华为云智能编程从零基础到高效落地的完整生态能力。以华为云码道为引擎,仓颉语言为载体,带给大家日常提效、趣味创新到极速量产的开发体验。
回顾中
热门标签