• [技术干货] 【HarmonyOS】抖动动画方案
    HarmonyOs-demo-抖动动画实现方案import curves from "@ohos.curves"/** * 1. 手机号输入框: * - 提供一个用于输入手机号码的文本框。 * - 输入类型限制为电话号码,仅接受数字输入。 * - 最大允许输入长度为13位,符合中国手机号码标准。 * - 显示占位提示文字“请输入手机号”,当输入框为空时可见。 * - 自定义样式包括背景色、字体颜色、大小、边框圆角、光标颜色等。 * - 用户输入时,实时更新`textPhone`状态变量存储输入的手机号码。 * * 2. 动画测试按钮: * - 点击此按钮触发`startAnimation`方法,启动输入框的动画效果。 * * 3. 动画实现: * - `startAnimation`方法使用`animateTo`函数创建一个动画。 * - 动画参数说明: * - `duration`: 动画持续时间设为800毫秒。 * - `curve`: 应用`curves.springCurve`生成的Spring Curve动画曲线,模拟物理弹簧效果。 * - `iterations`: 动画只执行一次。 * - `onEnded`: 动画结束后,通过回调函数更新`doScale`状态,将输入框永久放大10%(从1到1.1)。 * - 在启动动画之前,先将`doScale`重置为初始值(1, 1),确保动画从原尺寸开始计算。 * * 注意事项: * * - **动画逻辑**: * - 动画结束后,输入框的缩放状态将永久保留放大后的尺寸。这是通过`onEnded`回调函数实现的。 * - 为了保证动画正确起始,先将`doScale`恢复到初始尺寸(1, 1)再启动动画。 * * - **输入框行为**: * - 用户目前只能在输入框末尾插入或删除字符,无法在中间位置插入光标。这可能是由于`onChange`事件处理方式导致的限制。 */@Entry@Componentexport struct test { @State textPhone: string = '' @State doScale: ScaleOptions = { x: 1, y: 1 } startAnimation() { animateTo({ duration: 800, curve: curves.springCurve(0, 10, 80, 10), iterations: 1, }, () => { this.doScale = { x: 1.1, y: 1.1 }; }) this.doScale = { x: 1, y: 1 }; } build() { Column() { TextInput({ text: this.textPhone, placeholder: '请输入手机号' }) .margin({ top: 30 }) // .padding({ left: 0 }) .width('658lpx') .height('96lpx') .scale(this.doScale) .backgroundColor(Color.White) .type(InputType.PhoneNumber) .maxLength(13) .placeholderColor("#CBCBCB") .fontColor("#2E2E2E") .fontSize('36lpx') .caretColor('#FF1919') //设置输入框光标颜色。 // .stateStyles({ //设置按下背景颜色 ,一旦设置上,会导致点击空白输入框区域就清空内容 // pressed: this.txtClcik, // focused: this.txtClcik, // }) .onChange((value: string) => { //解决了会导致点击空白输入框区域就清空内容的问题,但有新问题,用户没办法在中间插入光标 this.textPhone = value }) .borderRadius(40) Button('动画测试').margin({ top: 30 }).onClick(() => { this.startAnimation() }) }.width('100%').height('100%') .backgroundColor("#f5f5f5") }}转载自https://www.cnblogs.com/zhongcx/articles/18433328
  • [技术干货] 【HarmonyOS】九宫格拼图游戏
    构建一个简易九宫格拼图游戏应用程序,利用picker从相册选择图片、使用fs拷贝路径、使用PixelMap切分图片import picker from '@ohos.file.picker';import fs from '@ohos.file.fs';import image from '@ohos.multimedia.image';import { common } from '@kit.AbilityKit';import { promptAction } from '@kit.ArkUI';// 定义拼图组件接口interface PuzzlePiece { // 拼图块的像素地图 pixelMap: image.PixelMap; // 原始图片中的索引位置 originalIndex: number;}// 使用装饰器定义页面组件@Entry@Componentstruct Page30 { // 状态变量:选中图片的URI @State imgUri: string = ''; // 状态变量:原始图片的URI @State imgOriginal: string = ''; // 状态变量:存储拼图块的数组 @State puzzlePieces: Array<PuzzlePiece> = []; // 状态变量:记录当前选中的拼图块索引 @State selectedPieceIndex: number = -1; // 弹出图片选择器方法 async openPicker() { try { // 设置图片选择器选项 const photoSelectOptions = new picker.PhotoSelectOptions(); // 限制只能选择一张图片 photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; photoSelectOptions.maxSelectNumber = 1; // 创建并实例化图片选择器 const photoViewPicker = new picker.PhotoViewPicker(); // 选择图片并获取图片URI let uris: picker.PhotoSelectResult = await photoViewPicker.select(photoSelectOptions); if (!uris || uris.photoUris.length === 0) return; // 获取选中图片的第一张URI let uri: string = uris.photoUris[0]; // 打开文件读取流 let file = fs.openSync(uri, fs.OpenMode.READ_ONLY); // 获取当前上下文 let context = getContext(this) as common.UIAbilityContext; // 新建一个保存裁剪后图片的路径 let newUrl = context.filesDir + '/test' + new Date().getTime() + '.jpg'; // 复制图片到新的路径 fs.copyFileSync(file.fd, newUrl); // 关闭文件读取流 fs.closeSync(file); // 更新状态变量:设置显示图片的URI this.imgUri = newUrl; // 更新状态变量:保存原始图片的URI this.imgOriginal = uri; // 图片更改时触发的方法 this.imgChange(); } catch (e) { console.error('openPicker', JSON.stringify(e)); } } // 图片更改处理方法 async imgChange() { try { // 创建图片源对象 const imageSource: image.ImageSource = image.createImageSource(this.imgUri); // 图片解码选项 let decodingOptions: image.DecodingOptions = { editable: true, desiredPixelFormat: 3, }; // 创建像素地图 let mPixelMap: image.PixelMap = await imageSource.createPixelMap(decodingOptions); // 获取图片信息 let mImageInfo: image.ImageInfo = await mPixelMap.getImageInfo(); // 计算每个拼图块的大小 const cropSize: image.Size = { width: mImageInfo.size.width / 3, height: mImageInfo.size.height / 3, }; // 清空已有拼图块数据 this.puzzlePieces.splice(0); // 遍历图片生成9个拼图块 let count = 0; for (let row = 0; row < 3; row++) { for (let col = 0; col < 3; col++) { // 创建基于原图的新图片源 const imageSource = image.createImageSource(this.imgUri); // 创建新像素地图 let mPixelMap = await imageSource.createPixelMap(decodingOptions); // 计算裁剪区域 const cropRect: image.Region = { x: col * cropSize.width, y: row * cropSize.height, size: cropSize, }; // 裁剪像素地图 await mPixelMap.crop(cropRect); // 创建并添加拼图块至数组 const piece: PuzzlePiece = { pixelMap: mPixelMap, originalIndex: count++, }; this.puzzlePieces.push(piece); } } // 打乱拼图块顺序 for (let i = this.puzzlePieces.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); let temp: PuzzlePiece = this.puzzlePieces[i]; this.puzzlePieces[i] = this.puzzlePieces[j]; this.puzzlePieces[j] = temp; } } catch (e) { console.error('imgChange', JSON.stringify(e)); } } // 构建UI界面 build() { Column() { // 添加选择图片按钮,点击后调用打开图片选择器方法 Button('选择图片→').onClick(() => { this.openPicker(); }); // 显示原始图片(如果已选择) if (this.imgOriginal) { Text('原始图片↓'); Image(this.imgOriginal) .width('180lpx') .height('180lpx') .objectFit(ImageFit.Contain); } // 如果有拼图块,则显示游戏区 if (this.puzzlePieces.length > 0) { Text('游戏图片↓'); // 游戏区域采用网格布局 Grid() { // 遍历所有拼图块并创建网格项 ForEach(this.puzzlePieces, (item: PuzzlePiece, index: number) => { GridItem() { // 显示拼图块图像 Image(item.pixelMap) .width('200lpx') .height('200lpx') .margin('5lpx') // 根据是否选中调整缩放比例 .scale(this.selectedPieceIndex == index ? { x: 0.5, y: 0.5 } : { x: 1, y: 1 }) // 添加点击事件处理 .onClick(() => { // 处理拼图交换逻辑 if (this.selectedPieceIndex == -1) { this.selectedPieceIndex = index; } else if (this.selectedPieceIndex == index) { this.selectedPieceIndex = -1; } else { let temp: PuzzlePiece = this.puzzlePieces[this.selectedPieceIndex]; this.puzzlePieces[this.selectedPieceIndex] = this.puzzlePieces[index]; this.puzzlePieces[index] = temp; this.selectedPieceIndex = -1; // 检查拼图是否完成 let isSucc: boolean = true; for (let i = 0; i < this.puzzlePieces.length; i++) { console.info('====item', this.puzzlePieces[i].originalIndex, i); if (this.puzzlePieces[i].originalIndex !== i) { isSucc = false; break; } } // 如果拼图完成,弹出提示对话框 if (isSucc) { promptAction.showDialog({ message: '拼图完成!', }); } } }); } }) // End of ForEach } // End of Grid .backgroundColor("#fafafa"); // 设置网格背景色 } } // End of Column .width('100%'); // 设置列宽度为100% }}转载自https://www.cnblogs.com/zhongcx/articles/18433327
  • [技术干货] 【HarmonyOS】Web组件同步与异步数据获取
    Web组件交互同步与异步获取数据的方式示例【html测试文件】src/main/resources/rawfile/Page04.html<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <script> let isEnvSupported = '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${isEnvSupported ? ', viewport-fit=cover' : ''}">`); </script> <title>Page Title</title> <link rel="stylesheet" href="mycss.css"> <link rel="icon" href="./static/favicon.ico"></head><body><button onclick="fetchSyncData()">获取同步数据</button><button onclick="fetchAsyncData()">获取异步数据</button><p id="dataDisplay"></p></body><script> function fetchSyncData() { console.log('开始获取同步数据'); const result = window.hm.getTestData("测试数据"); document.getElementById("dataDisplay").textContent = result; console.log('完成获取同步数据'); } function fetchAsyncData() { console.log('开始获取异步数据'); window.hm.getTestDataAsync("测试数据").then(value => { document.getElementById("dataDisplay").textContent = value; console.log('完成获取异步数据'); }); }</script></html>【使用示例】src/main/ets/pages/Page04.etsimport web_webview from '@ohos.web.webview';import dataPreferences from '@ohos.data.preferences';class WebService { context: Context constructor(context: Context) { this.context = context } getTestData = (input: string): string => { console.info('输入数据:', input); const resultMap = new Map<string, string>(); resultMap[input] = "我是value"; return JSON.stringify(resultMap); } getTestDataAsync = async (input: string): Promise<string> => { console.info('输入数据:', input); const preferences = await dataPreferences.getPreferences(this.context, 'DATA_STORE'); const value = await preferences.get('KEY', '默认值'); console.info('读取到的值:', value); const resultMap = new Map<string, string>(); resultMap[input] = value; return JSON.stringify(resultMap); }}@Entry@Componentstruct Page04 { controller: web_webview.WebviewController = new web_webview.WebviewController(); webService: WebService = new WebService(getContext(this)); methodList: Array<string> = [] 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) }); } build() { Column() { Web({ src: $rawfile('Page04.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混合 .javaScriptProxy({ name: "hm", object: this.webService, methodList: this.methodList, controller: this.controller, }) } .width('100%') .height('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433324
  • [技术干货] 【HarmonyOS】图片下载加载方案
    【harmonyOS】如果有些图片url用Image组件加载不显示,可以request下载后利用PixelMap加载。需要网络权限:src/main/module.json5 "requestPermissions": [ { "name": "ohos.permission.INTERNET" },src/main/ets/pages/Page010.etsimport { http } from '@kit.NetworkKit'import { image } from '@kit.ImageKit'@Entry@Componentstruct Page010 { @State pixelMap: PixelMap | undefined = undefined @State imgUrl: string = 'https://img0.baidu.com/it/u=3129379276,3231297819&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500' build() { Column() { Text('加载url显示') Image(this.imgUrl).width('200lpx').height('200lpx') .onError((error: ImageError) => { console.info('error', JSON.stringify(error)) }) .onComplete((event) => { console.info('event', JSON.stringify(event)) }) Button('下载图片').onClick(() => { http.createHttp().request( this.imgUrl, { expectDataType: http.HttpDataType.ARRAY_BUFFER } ).then(async (res) => { console.info('res', JSON.stringify(res)) // 将图片资源转为像素图(PixelMap) this.pixelMap = await image.createImageSource(res.result as ArrayBuffer).createPixelMap() }).catch(() => { console.info('catch') }) }) Text('下载图片后显示') Image(this.pixelMap).width('200lpx') } .width('100%') .height('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433322
  • [技术干货] 【HarmonyOS】动画旋转方式比较
    【HarmonyOS】学习笔记 比较animation动画三种rotate旋转方式的不同import { curves } from '@kit.ArkUI'@Entry@Componentstruct Page030 { @State rotateValue_1: number = 0 @State rotateValue_2: number = 0 build() { Column({ space: 10 }) { Image($r("app.media.app_icon")) .width('100lpx') .height('100lpx') .rotate({ angle: this.rotateValue_1 }) .animation({ duration: 2000, curve: curves.springMotion(), playMode: PlayMode.Normal, }) Button('切换旋转 (往返)').onClick(() => { //先正向转,再转回来 this.rotateValue_1 = this.rotateValue_1 == 360 ? 0 : 360 }) Button('持续加速旋转').onClick(() => { //持续正转,动画未执行完成就继续加速旋转 this.rotateValue_1 += 360 }) Image($r("app.media.app_icon")) .width('100lpx') .height('100lpx') .rotate({ angle: this.rotateValue_2 }) .animation(this.rotateValue_2 != 0 ? { duration: 2000, curve: curves.springMotion(), playMode: PlayMode.Normal, onFinish: (() => { console.info('----onFinish this.rotateValue', this.rotateValue_2) if (this.rotateValue_2 == 360) { this.rotateValue_2 = 0 } }) } : undefined) Button('条件旋转 (一次性)').onClick(() => { //持续正转,动画未执行完成点击无效 this.rotateValue_2 = 360 }) } .width('100%') .height('100%') }}1. 切换旋转 (往返):• 这种方式允许图像在用户点击时进行一个完整的360度旋转,然后回到初始位置。它适用于需要展示一个“完整循环”动画效果的场景。2. 持续加速旋转:• 在这种方式下,每次用户点击按钮时,图像都会额外旋转360度。这模拟了一种连续加速的效果,适合于创建动态的、无限旋转的视觉效果。3.条件旋转 (一次性):• 这种方式只有在图像没有正在执行动画时才响应用户的点击,从而进行一次360度旋转。它防止了动画的叠加,确保每个动画周期都是独立的,适用于需要精确控制动画触发的情况。转载自https://www.cnblogs.com/zhongcx/articles/18433319
  • [技术干货] 【HarmonyOS】Flex布局文本位置
    【HarmonyOS】使用Flex布局和onAreaChange事件计算并记录多行文本位置的实现方案class PosItem { x: number y: number constructor(x: number, y: number) { this.x = x this.y = y }}@Entry@Componentstruct Page021 { // 原始数据 @State historyValueArr: Array<string> = ['张三', '李四', '举头望明月', '低头思故乡', 'HarmonyOS', '不可能,绝对不可能', '张三和李四', 'city不city'] @State result: string[][] | undefined = undefined private map: Map<string, PosItem> = new Map<string, PosItem>() processPositions(key: string, value: PosItem) { this.map.set(key, value) if (this.map.size == this.historyValueArr.length) { this.convertTo2DArray() } } convertTo2DArray() { console.info('创建一个空的对象来存储行数据'); const rows: ESObject = {}; this.map.forEach((value, key) => { const rowKey = Math.floor(value.y / 26.923076923076923).toString(); if (!rows[rowKey]) { rows[rowKey] = []; } rows[rowKey].push(key); }); // 对每一行中的元素按x值排序 Object.keys(rows).forEach(rowKey => { rows[rowKey].sort((a: string, b: string) => { const posA = this.map.get(a); const posB = this.map.get(b); return posA!.x - posB!.x; }); }); this.result = Object.values(rows); } build() { Column() { Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap, }) { ForEach(this.historyValueArr, (item: string, value: number) => { Text(item) .padding({ left: '15lpx', right: '15lpx', top: '7lpx', bottom: '7lpx' }) .backgroundColor("#EFEFEF") .borderRadius(10) .margin('11lpx') .onAreaChange((previousArea: Area, currentArea: Area) => { console.info(`child currentArea item ${item}`); console.info(`child currentArea ${JSON.stringify(currentArea)}`); this.processPositions(item, new PosItem(currentArea.position.x as number, currentArea.position.y as number)); }) }) } .width('100%') .padding({ left: '26lpx', top: '11lpx', bottom: '11lpx', right: '26lpx' }) .backgroundColor("#F8F8F8") ForEach(this.result, (item: Object, index: number) => { Text(`第${index}组:${JSON.stringify(item)}`).backgroundColor(Color.Pink) }) }.width('100%') }}转载自https://www.cnblogs.com/zhongcx/articles/18433315
  • [技术干货] 【HarmonyOS】横向List高度适配
    【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
    【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日期选择
    【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保存图片
    【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,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAADAFBMVEX///8BAgIDAwP9+/v+/v02ODwCBAUoKy4iJik7PUL+/P00NTkgIycoLDAZGx8mKi0sLzIkKCwdHyImJysnKS0FBAX8+/0bHB8FBgb89vMHCQokJikvMDT69vQ5Oj4MDQ8rLTEPERMFBgkdISUxMzgxMjUkNWsKCwwTFRciJCYfISUrLC8XGBsaHSIXGR0REhX6+vqRdX/+/f+OdH0nOG4HCg8sMTUbHyTo5uwKDhP59/n89vXs6u7j4eH39vXn4+Pb2dnHxsfv7e3q5+YrO29YX2ciM2YTFxvx8PDn5efMzNLd2N/g3dzPz9CqrK7V0dgPDg/08/Tj3ePNycqOdn44ODj08/r88/Hk4efW1tX08vHb1NjV0dIXFhjx7vTp6fHt6ekxP3NFQkO5urs0Qnjs7fT58/Lw6/DAub2GgYfEvMLUzM++v763srqrpKwbGxzKxc6wrq1+fH+JcXn49v3v7/qcnaFPWGYjMmBISE0LER3y8fjW1+CEbnYsPHTDwsSmpaapn6RcW19MTlUaJTsSITAhICH77+7x7OvHwMiQlp1zYWYhLVf37Ou2tMKYl5hlb3tJUWJARU0kKzaAaXFiY2QxNkdGr8ygl6ZweYU7SHtraWvg4+vx4uDi1tZApsa0rLF1dHNXVlY4QU1QS0tCOjq7ucSdnayWkJ99g4hubXNjanJfZm4cU21RU1wcJkjO0titsbivprWztLQxf6CLhJqAh5FsaItHUX9UWX4vNz4QGSXa3OVRvtq9sbWLkZaSjJWOiI1eYIR3dXpRYnBBS1YfJTE1LTD35+UukMGUhZFvdHtfaXV/5PFOttO8wciio7Euh6yinZ4haJmCepMfXocYRV5jU1lZTFIWOE4PLkKo3udWxuLAxdM8lbSajJRfbYMnaH1lvNE2oL5Wkq0fdKsnd4yQkIocU3xFTHUsKCej+/u76eh7q7yLprqklJt5dZB2wtqhr7+QnaiKeYJAXWpjp8EiervRu7kfLkAni7VFbX5cfZZk0ucTQW51j7diYMDRAAAbuElEQVRo3pTVS6wLURgH8DIzLnOIQxgSRw3OUYzHZFIN1Y5qq+qZlHThETF0gSCIyBCpVwlJxaORUIkghESiHqEeiUeFCDuxt5Su2Nr5zpkZl9EF/+Rm7p2bfr/5vm9mGukZbbcSJBr1johSxANnIENCgVObtqty5H9yqIx+R0AghDCK4DdEUQ8EmPIFuzci9z5dulzmSoAgw0ACMkYZICACSA+FLbT+B9mxqYwCBIozBD+6jhmlFPrh7ShDegzsWipcXsTqiSSPwmACBAQozBfCcN9wHY6E8u7CBt/KQzlM8KhqD8PcJ3bsERQRDH/pI4Yehzw/cGDmMGAQKEo0jJSv5+SQ0Y+E+MxdRP1JMYp1hvDQd7VWve4WXLdeb9XeURghZ1AYKV9LWr8LqkgPxLJW0mfPPESnCYzo4Octt1Ao1AtTCiL1WkNRwjMDQymXTyz8RyR5FFGMgGA6xZgNq3ECgPqUKaD4TAKaVECJ/o6AssfugWhhQ9bOUDp4KBgEw8LJuxZ0wOO64sClKSA2KIwrGjBRbnDkxnrZEwJCkiT14F/ICkRGTiQKIjpMqtEqeEXjU9y4SCC5rUYCls9rBwioZbZVDuITgOwLG8nXdOTYaYqCYeOs5ndRyFfdfLWaz+eFwyWYWQtWg8TMRMBAsCVbtYRhCUSTJM284/cWGPKSC31jY0ghfRjh596E4vm447gVHicPmRI0VJ8s3giICUME4e0C4YDXh6al9oeRhdE5k6YjbBAyrAYGL5jPO80n59qfIe3Ok26z6jcESq2BRAdg+AjbY3sIKD6yhvDCluU78M8zswfNZgwTMqJVEF1UnW736edHr5YtyOUWrNv14tv3p82K4zMu3GcKpQwFjSB2IxMgUJ8b2nYUUUVkP8khgyb1IZ2SmTXRRrXypP3hyIt1y5Z5DcuytHbd1UftJw60I3bTSiC4qMCghOwDxJKCYZmmtnVyCMnMnjSS6siYeNwFo1ptdj5sy61bl06l5f77f/zG9Q863UpVMIUfXGEeQgG5bgpEChDzwhBApN+Q3aMnoARjE+e4oo1m+8h7yUovWG4uDxCvI+nUo07FEYrbgE1gFCDMWC39iRy7Hzm6GToLHHPPpDEIo1lz6nmx7/aL03AR6dQae3no3ZBd9eL7E1CAaekKIQwI/o1AGLm+VoaKgaItHbEoEtuj9iOpY3P6hiXGjT2ez1dh4+1tScuypCX29rUbs38ixWIyc7vrxKvxeKGGEcb9CF74CzE1M/lx2NtIrGx6CL/HVo+bpeO+2AG3Cut4+uWKBjAg6sJ0KSPBV39AaObaoqbZOztwLdBMDVY/CnkIIfhNsb8PLd1YdDQSm5rxXM4cnj2tD80Y23IqTqVzZKPqPbpLtNL6bDobkUvB42SWSkW4ruKDpw5ff72BdIx9BOMbawIDsv352ZmRWOyr9AvZM2GUwcYO4o/300857RciZfj9JWVVf/N2sZgCRLMvdpsOvANqNKEbjFIPQQ9/Q16+u7Q4Mi9282SAaNdn63jqgHlOpdJ9dGW+apuyJKv2AknNZTbcK2obLctDktls2oKL0lKPOxVQ3BHUMAhlHoL39COlO43zsci7CVNX+POyssYYPG3swOOVSrOza6Oq5mwzCQUXJFU7k0otLOUsCHzSSs5dsgKOe01zy60nDoysRqih++PS8f61/t5Nbcn+OfMm/aSx7qOaKuM4gI+Rd8ODOJzgHNJ4PUAjWJ2zcbcGYxAHtgteuTtnjLY2BMfe3BrGXGwrHJgyivHiaQ0RGq6A0AhIEJQIFEhIk0ArI4JKT51E8a20956Bff+4557nj+dznuf3+91zcV52XP3/96WPJ8bGQ8w3jx4dHsjMeMI/nZGWFpqTycv3989B5Co0FXR9uS9iQeEBuLxcLhaLsK7hPbtfe2UqZkfEjpjHCH3vRl98UyIMCKQm4VzZtJZ1eGPoW/EEAhVv2T00fMdQk/MEQ8fI4TAEAh54ciSIFcvjcMrXAl5bKuXlKrlYVKS7MgRu7MOwWAIJTD5QIkgRGg74fPmui9FX3RP3JM41RXuXt47kpyTtCILwrx8dOm1Q5+SE5oj8ueXliATmcsC+eVx5XianXAxSrkKlM8USOVdh4gu46uWjQLEQIgg+hAQQ0nEe6Py1muyvYDIDcAFHdn0qWi+TjRm9I84P7xo6NyDjZ+SHpoo4sNgklNgQMdhbLkRhGSy2Wq1KMZcv1Y2PwTBsqhQoBf3nju7e/WZKBGFHbGzM1giABPD9HyPvb6DFV+GqXeyXytYRPZNOxvudHT532pku4+YzdHyOxKTUj2i1YjHYWt6hR1AUILBVUiLlXbDrAaKWKJUKUJY9uz8kRmwCCnkrQEj1/s+G+hDxCRaT1YQLcll2ta8ZG8e30RPxtOHTpweQ9Fw4n+Pgy2HlMc2IVm21yq0ShUOr7xBWogjKRbTSosM1ZQ5M4QGIydg1tGfP868TglmsWHognU4nHE8DJQGIpDqJmXUGNxVzlvYuY22lmx5Gg2KuDNxxK5BUOIfjcCgUyp1LAr5IIBfkSLhGBEONRiGGyhGR9IWCTFkZqrBjCqXJhp4eOrr7YPbmQBaLFLiNQCDEFq7PCb86m30yGPc70UX7JcO3kl9FTqCwT9gGrvSLMQk3h2PsgJWKFhGvMJUvx3IkAiEGq1QqOYzKpTKkiK97Id0xsoopFKYR+QBQDjKJwZtJrIjkLVu3hv20hvj3sdhsNhVnzu55+kWZb0XSE50INTn77wyoOQIJaE9hP6owrralSvNLMgUZMCpEinwIgqpEWrW0qLS0xmibAScBSNfAnWFXVDadSGKREpITNhNjM0LBnmn3XaTqLcm4pvhgb1KZDyks3kw79JGh54rBweHCcrlYYhces2vVukoelqeCuUahNEMlUKkcakf9gfTSolxpm0i4JIMVJpuiq/9y15UN7G1hW1kkQnJycPChvT6EO3gmOpDdhPNutriI7aVgpYS0qfrA+Zc+1aiNYiUsBnH3YtpSnqykxFB/QM1FtVKVAFZx6w0GtV6n9sz91K7Z91V9OhfUxO20dS4u7m+pqiiOIcRHJZLJ+xhAGWluCmJSA3ANPRYW63gmQHa+pWkvjgt5UWMUWpUSsUlsdQt1jjadSK0TjRpEMKZHYEwFC7S6B/ax7mrXtWvfmE82kTUSOerodNo0y//evte5ryKGHkSlbiNXccHPivBiI4vJTMbdr2UGEd9L92cU7XtvVwgeH0KdQYXWDInVZFLOGstkPB5mX1312AUSrjY1V6JCUK39VU3FwZ/nT92Y/2vR2TSngh3OTr1tpnH+msUSSCSRnoqDaAnFMnCUyxNmL5sZhQuMBvX/hQ8+mdU0PzxI1BhilAswE4j+o+7jxRt6shovdnerERXKk8KYVjtqt7fXn5peWHjnj9u9zqpcVUml0+0ZWTLf+AcPbQujR4RFQuDnbS9AZrPMLjaTimNbsi3JiXZ/Rj6Zisf74UN2uZFKLooAQ9k9Tu9ufuXq9eun5ifsUlSKYFKDZ1Srm9O9sPD3JZCRtL37VfCY0dnVb/MhEBQZRqZvzYbwlJfqwZgMZvVMMQFyonZ2AzGynRHKT4L8/ADypBvRc7VGpUnhbDkv5MCnpu8ufP7tXZ1Di0gd9jmPLLPmcEH4wqWbN2+upKUaMLlUY3RqDL2r5oc/QxAlIYFOCKRCUNxx0FwTWdVnfScxdzqzNmUXp4XWx/uBhESecEv7FXqtQqlQIvvRjQzb3UsgC/landFpWPLw6gq2by8oUKwsLKyklmnGYZW+q2Pxgbv3k+8BgscnhoXRyewoCuVQmn9vY9akxYdMTTZtIj4dJmCcp60h5Dtu2eyxDj2mhJVyvZ3BOHbpJsgxnlpvcPd7SreDAKQud3T15feKx9US2D3rXNR02RrmAeKHp5G3EMjJUZSQkHT/zsYsF0CYOFbAyayp6MS93CqqD0mubpqr7Bwx7pQpuFJYdn60xrqysLKywtV61B6PWs+vWUcO17W2PXo06pFiurG+vo4HzY4/Hz68GuWHpzwVFrw5iArhQw5wGswnXWd9SFZDbe2ZlKD2wgoKMKICnozT6t0oaiiBYUke1/7qG0tlZSWFPJG2LbOsrUamzqt5jFwAaVMjEoOms8Mw/tet69M35pkQHoonE4OjI0GbtqeaJ7onj/iQwcGse2bvZNhOEkAohxIpsZhB0wH31SIoLMzDdI8ePWprBTlcJ2orLS0tKtTVFPiYw4BpdWAo+mD8k8UZ83fvTE9fmz8IEGoCkUhMioKg4tnh5oYJlw/prDXfa/ZObGkhAAT0MHVONrZhv6rvtlqEIgZM5wMuXKgDexaFh9fVASazdHt4eIFPyc3Voe6ZMWen9/rb09M32NeuMvFgRIiBxJRkCEpoONc8sXwxm83G3RscrP3EtZxAoof4+ZIg0Fcka+Qdt+7LujD9mMDw2CgoCA8Pb2t7DgS8AKQAIJkex9fPjC1+dv3zt/+48XNI1Jff4MGIRAcSo7cxIWrjxeblZa8FILMN95trz0yQEiIoPoPSUlgVGXkeNt6+e1mvF+5Tt+la69aM7WDr1tY1A2StLjU6ZMn86617v3379jsfzH9JoVBSqKAYSdGBKUHxUZTJicblxteftrBx5knvGbM3KyA4grp2kPqKOL8Xq4S2+3dv9Roud2lGRdJ1w4cU1IQDYx0JLyjFdA7Nrd9++O3bj3/8wnL1G3xICASBG6dtSUnZkkSFvMter+vIkbOW/5gy19CkwjCOn4yORrqLdvJ0tI7nrDjHS7Mhc6zyOqQyFcfcB23CUJAiyUowFsnWxYhJtZXLIkiqVdhmVFt9KNaKLiMW1YjaihptqxVBQVfoBj1H1+WP7oL6/t7/87zP857ziohmFs+03Wq6DDsaZ6TpLW/aNMWMtVsffP01/hEK9djVNev2ACOvpSFgFAT/7TavXtM13nfv2bPRiccQKgwDAjghisRlVVUsqm9rO3z4bvvmdqS49MBg6w4+3Ldr4HXKzzXigLbJ/uDl+LMP7x+cnr/NcdSwOz9oIVT/ID5zQ+Bl3710X3ro+1M88ZTGYIJAocs5iJQQdLe13d3c3t6O6A8cuHz5cllJyWxoXfAmYKiluOzUhZ+jz56Nv3//6YgpNM8IgwID9JcBctQc+zLel8ncGx17mMCYAwSMwFHIqqqyKrGULLe1tbV1t7ffQvYf3GEbzO7XF1PT/kBYFPOfvvClN933bPzduxcb580tGJnSH0hoRcuPe32ZoYmJyZ2PExjGcBCIFyMVC4VlYnF59ebDh9vutrdvRu52nT/vys7UsygQ/oq8eOj22GT6Xt/415c3vUuNU5DQ7t2h3SZTgRIKfsncSw9NjvY+rkwkcIw35UMsFgo4iJg6cSkLlLZu5NatLputa78Ihzn8ZfDwG4N0YrJ3NN3Xl878cNUUICFDo2/p3JCPW2Jza14rR9Pp3lyu9w2N0tVoAcLTqoRQ8XkrRYuyl+7a7nbZkP23DvdfynaISPR/K/iZ/WhlbmxoLA2JHf3RFfTll/DSpXOnFPJ21F/OZDK5p09yj6BlYVMMSikoLi4FjKpMXE4Ntto227ptyKWszWbrGOwoAso/CNGtwugtuTtbJgEyBAG5DpD/GPMcnW9TZycyE4krT6vffEO5lZuPlUzI5xeXlnJeltNV/QA5e3YH8nxvV3f3ppirQ6pl/kGkTZWwAh5P4olMOjM2OTT0xrbREPrno+H4yVT05p07vZWVCdhzcR4Ph3jj4EMkAgjkpIiiqUFXa7cNhNw/1T842B+LuXaQrJ/hFYTN0sDksNwTFB/LTDzNDT3sfRIJrF3NFTrHWB84mfREb36/k0NRGt6J4iSDw74uK+UDRKIspxgcpXe0BFvPdAEGeX7/fDYbc8VagjsIjNK4iTwE47xXfaMxcmxijHgy9PhRTsDg7iMbzSvmmVoC/oGkpyL65dWrx4VM4BQxDWWUEn6JQi2LUHT+iiQQu3++Ndt66fAZxL7hfmt/S9AVtDfPB8NujYbDFLIPtMu9k1fQxKPqJzu3d0YwzF+/fVOn3/12uKKiIn7iUe5RNQoMgsIh5QK+Ql8i0xLQ7bl5zo71BF37+lthWSF7728FG81Be515IwMGMLeKhT43VbxlX3LV09ArWzCqcuRzagDHoBww9y7wARB9Droi7Lg0i01DKYVCXqIu+lP1PPZasOe8K+Zyxfb1I+fO2c+3uCwWp9NgjUV4KEUTfg2JTa00iuVCTtM0itV7PPFAxM24dwVSHl20oiLa+Y2svFKN4SSNMmI471CLyXwQ4IFJN4XD4WCPy25vtluQHpfLtbGnzuv1mny1ayl0uZTGyYiGKEC4NsEJ5bkHPDpdfDgwcHI4DgCA6IY7/ThdSWkpgpoth7tFbpEVyoVocjmdFks4GN56zm63Iy2ACIadJrOhpnZJ4w2CkfkhpIxfy7UAHMdQDEdRXDuQHPHoABNNxT0eCJWuosKTjKdGJBqWpJQLZi1QFMG8prpf5LXD4PA2NEB8VkGk7iM9gGiuc5pMKxtrlyw21NOskgQKzmi1DMYqyxaVa6hdTcm4ZxgQIA/H0KV0oGj8c3IXRQnlcPQoJvECA571Lp+1xuQwm511Tm+dpTmIhMPNFmeD19BotdYunj6nhUVVUhQE1buLJQR6vUKhGIkCIOnRxWHkCi7n0XgBeMRNU3LuyC4CtvMm4DHfMX2pbzdQHF6ns645HAwiTiCYHY4a6wqfcfF0Y08JqpVoGYIgGKpIrVwu0JfAcfpwVBeHyafAgy4PieYhybeUGGzMkkvBBqgAkr82LJ4HFIPD7G1wWpqbw4gXEAZTTaPPZ1yyxLFJSWC0lC9TKpWSYgWcLpQK+Xq9Xj6SHzUFPjwcJBWFH57UgETOfdWiZ/PpBhcFkrupbt48iBhQzA1OiBcCBEBYV9QarcEjfgzSgeIaCdyLi0QlQhVfL1fzS/QL5QEwAlkHBAhonniycyF3wqXXc9fw3McIBi9AMEzbEW7k8lIwU4esbFxmhVDVGs0dkT+ljjKRYpFIpCAwspwPxz58KIKTw2AGFhWneCo1clJeKlTNXqBfKORKnNBqpBoGihiqRstqNH5ZR53P2phPv9eJLMsTjLUblTzQv15fBhAtzI+WwnG3Cp7qk8PJeDQPSQ53KqAJVlfB2UAxgcJmqJarWZoHLKlMBrGWCQTSXRYrUAycGWSFL2Q0rmgJUH8ZhV80KyyWUDyUKVEL4PCTr54tCSSTyShXHyOdEi1JkFKFSFDOylUyuVxWTtKMXwwbFlxuC/JSLdxkMYAXoCDGJSB7Pc3jBARcq9GQNPcXTUplfvK408VqYKJKlVI5kIzHISMjAyyBEqxAxC9iyrPOGwskGoLQimXQ5fn82aVTEPBTv8lk5TC/yzSTFyeCKIwnrXRG7ekZe6GbbrVS3UpXelVhjIq7hzguY1wiLsQBRVxQoyMIouAGc9CDIHgyBxUVFRHx7EHxIOjNkzePgn+FX72OQfRjMulOut+vXr2XqnqVVGj39pNQywWRK7SFFpoTp9zDaVD0ju3alNiRkyQGZ6z3/dKK/U9RJLsCU3jOlKo/NTWPMe7UVo3XEUaClBhNa8z6jw5vA6Yit573LOBYB0DMQQUDyBiaFLdpzI6mNncjL+jWaiMisNvPLt1t2zYTguGjhMVvdGsmXuNY46sASZISMjaEFI8Xg7JFfh03fxejiLiOaVkSQKlVH+MeOrG9b1OBOdPLzQZGNdv+PKsGvTRA1roRQOL0zKLxUqPkCVoIVwiihUUHKXV4W2Wl/K7Ma8uSNEYrSjcIUk9y5qqNI7vadtUThiPEbFoYjSjiURBxzjDDuiK5fHqJJODyUSmCmGYJMVrOzuVrN+yhzf0nAVOCrlzLjEnGaI0wSWIZE+qT7c+5jdrIME1dGGbNMoqCF3ED8VA8J1l2/uO8Rav+g/gEcdLujTmgSMjmBmvzeCFEnkBEgWpx/uX5ZT+NvAmhWYnhBRFjUcAaJsMIGjWwF3zx8rwy5mVvAWJZcIQgQjceyH17CblXiLRRMsgVC6oNdOzHu5ejlqGL1PBr9ThCLtiFP9aqylE6qdeXXZyat2gAkQRikCMQ18Njm7DbLjffHxR6ppkg4EEEaKxkWI+/3HxVxyoHuWJayTg8UNpmLcRQwhyLIKcIQoy/IXGohYCMn8WeOCDbOrnjGP6guyCTXJKqjd/48uIWWTAX4rRet3rCrJncDVjPryXYoJ/ad2XAkPYlwZKMkTDTdJE1tXvz6auwzbrhtJooJmO8h6CBMYTMu/7h45XR+qgM6yAbkBfCCzgYo3VQbux4Q16QBwSRjIZm5JrIMkN/OEdClj8pQmcNKlbeG5HyhxDo1uSLmSXSxjC0iYX9hogbvinTMDHvrXuI98AYQHwfDEAybrTyrJn/2roSkIOco4rM4txONYhAJiRZ1uM95z8tlZCECNTgkVYhMr9MjaTx8+pMHQxcXN4kAQigpk/oKQeE3ZCeHA/WNFOXO15VhGEcEgbNIYj5c8+FV/XaUDK5rVh3miMmzmQSzs4cuGOBQRAfotIk0+I0EMUaJ2u5neVzKys/BmsyYbtdpcrD0AhjDSKM9Ojinr0PrbEhAxDLbKDMWWgNIM7DA+d8QEagkhGGTS7CWKgYdfRMuO6hufjVRTChF7aCpSYLM6HHcUggQjU2Hz35CVlDKtOT2rtQHpN6Tw7sHl9ILaJQgBHmXhEaBaZjW2SFqnybW1nfsb2UKWrbqwaGE03kstoDiVCzO0+ceEm9PUgHiaDcMEv5aWfb9C36fEMxvMA+AVOE0ZxQUUx4LUBubahsnsXkEGGMiBRVdyLVSzNkntGEwtCfnJ7+OgLD0LBDSP7gtXR244kpjRxAQJtQpgdK1zACtY3Jm3NV6eysXGSYDD3MT3iNOayqBrmug9MEyRhfN736tQaLWCLJ3iBpgyPKorQ4dGL1aCghuD7LAEFdmBoo02HQZmuq1d71ymkbvECWrfBG5yr+p5JC/nQmV69+o2mwF2v4i6XQKXSk0WnqnZ2evgLTQ7XUqprqXGGoWRQvwuNb5QqKsQiQKibVIBWA2EwnOY7zbHLy4G1KA9kbEJ7oAVCp1P62evUrguB6HWJYJ+S5p5TzICD2g4qrgGUD4iL6RculqiTPS8ixyf77+83YkH0REsL4R1175mD/Dhi6FDACFrxcqC6VubYH86JCPJtWl7bKUJfgPOA5MNCzfv/a/cwYKGuWT1JG5pC67oMd/SlL1+kOR8851h9RzlWPqkJbBkKp0LqyhKBgSpkqS56gSHMofXaQIE6pTHcG0ulINr0bPD3Uf74gb7VaaZriFmmVtZjKbAlRaKMDkIGQxGogOFWLyIU0x00dQK7oA8NDDSA61OsGz7b3L8wjiKQIKjrERMAlgyDVvyFupNq8CFSq+lQuIb8O9t+/yfX/RaQcEsHnM+/f3m6R0laKSKgqrLSjP+Ug9Bt1kOLeW0+v0QAAAABJRU5ErkJggg==' 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】组件下划线与边框
    【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
  • [技术干货] 【HarmonyOS】H5页面调用图库
    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
  • [技术干货] 【HarmonyOS】自定义圆点进度条
    实现一个自定义带圆点的进度条效果【起因】论坛里有人问进度条如何带圆点,参考: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
  • [技术干货] 【HarmonyOS】Text组件两端对齐
    实现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
  • [技术干货] 【HarmonyOS】分页滚动文本组件
    实现分页滚动文本组件:为何选择 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
总条数:88 到第
上滑加载中