• [技术干货] 鸿蒙特效教程06-可拖拽网格实现教程
    鸿蒙特效教程06-可拖拽网格实现教程本教程适合 HarmonyOS Next 初学者,通过简单到复杂的步骤,一步步实现类似桌面APP中的可拖拽编辑效果。开发环境准备DevEco Studio 5.0.3HarmonyOS Next API 15下载代码仓库效果预览我们要实现的效果是一个 Grid 网格布局,用户可以通过长按并拖动来调整应用图标的位置顺序。拖拽完成后,底部会显示当前的排序结果。实现步骤步骤一:创建基本结构和数据模型首先,我们需要创建一个基本的页面结构和数据模型。我们将定义一个应用名称数组和一个对应的颜色数组。@Entry @Component struct DragGrid { // 应用名称数组 @State apps: string[] = [ '微信', '支付宝', 'QQ', '抖音', '快手', '微博', '头条', '网易云' ]; build() { Column() { // 这里将放置我们的应用网格 Text('应用网格示例') .fontSize(20) .fontColor(Color.White) } .width('100%') .height('100%') .backgroundColor('#121212') } } 这个基本结构包含一个应用名称数组和一个简单的Column容器。在这个阶段,我们只是显示一个标题文本。步骤二:使用Grid布局展示应用图标接下来,我们将使用Grid组件来创建网格布局,并使用ForEach遍历应用数组,为每个应用创建一个网格项。@Entry @Component struct DragGrid { @State apps: string[] = [ '微信', '支付宝', 'QQ', '抖音', '快手', '微博', '头条', '网易云' ]; build() { Column() { // 使用Grid组件创建网格布局 Grid() { ForEach(this.apps, (item: string) => { GridItem() { Column() { // 应用图标(暂用占位图) Image($r('app.media.startIcon')) .width(60) .aspectRatio(1) // 应用名称 Text(item) .fontSize(12) .fontColor(Color.White) } .padding(10) } }) } .columnsTemplate('1fr 1fr 1fr 1fr') // 4列等宽布局 .rowsGap(10) // 行间距 .columnsGap(10) // 列间距 .padding(20) // 内边距 } .width('100%') .height('100%') .backgroundColor('#121212') } } 在这一步,我们添加了Grid组件,它具有以下关键属性:columnsTemplate:定义网格的列模板,'1fr 1fr 1fr 1fr’表示四列等宽布局。rowsGap:行间距,设置为10。columnsGap:列间距,设置为10。padding:内边距,设置为20。每个GridItem包含一个Column布局,里面有一个Image(应用图标)和一个Text(应用名称)。步骤三:优化图标布局和样式现在我们有了基本的网格布局,接下来优化图标的样式和布局。我们将创建一个自定义的Builder函数来构建每个应用图标项,并添加一些颜色来区分不同应用。@Entry @Component struct DragGrid { @State apps: string[] = [ '微信', '支付宝', 'QQ', '抖音', '快手', '微博', '头条', '网易云', '腾讯视频', '爱奇艺', '优酷', 'B站' ]; // 定义应用图标颜色 private appColors: string[] = [ '#34C759', '#007AFF', '#5856D6', '#FF2D55', '#FF9500', '#FF3B30', '#E73C39', '#D33A31', '#38B0DE', '#39A5DC', '#22C8BD', '#00A1D6' ]; // 创建应用图标项的构建器 @Builder itemBuilder(name: string, index: number) { Column({ space: 2 }) { // 应用图标 Image($r('app.media.startIcon')) .width(80) .padding(10) .aspectRatio(1) .backgroundColor(this.appColors[index % this.appColors.length]) .borderRadius(16) // 应用名称 Text(name) .fontSize(12) .fontColor(Color.White) } } build() { Column() { Grid() { ForEach(this.apps, (item: string, index: number) => { GridItem() { this.itemBuilder(item, index) } }) } .columnsTemplate('1fr 1fr 1fr 1fr') .rowsGap(20) .columnsGap(20) .padding(20) } .width('100%') .height('100%') .backgroundColor('#121212') } } 在这一步,我们:添加了appColors数组,定义了各个应用图标的背景颜色。创建了@Builder itemBuilder函数,用于构建每个应用图标项,使代码更加模块化。为图标添加了背景颜色和圆角边框,使其更加美观。在ForEach中添加了index参数,用于获取当前项的索引,以便为不同应用使用不同的颜色。步骤四:添加拖拽功能现在我们有了美观的网格布局,下一步是添加拖拽功能。我们需要设置Grid的editMode属性为true,并添加相应的拖拽事件处理函数。@Entry @Component struct DragGrid { @State apps: string[] = [ '微信', '支付宝', 'QQ', '抖音', '快手', '微博', '头条', '网易云', '腾讯视频', '爱奇艺', '优酷', 'B站' ]; private appColors: string[] = [ '#34C759', '#007AFF', '#5856D6', '#FF2D55', '#FF9500', '#FF3B30', '#E73C39', '#D33A31', '#38B0DE', '#39A5DC', '#22C8BD', '#00A1D6' ]; @Builder itemBuilder(name: string) { Column({ space: 2 }) { Image($r('app.media.startIcon')) .draggable(false) // 禁止图片本身被拖拽 .width(80) .padding(10) .aspectRatio(1) Text(name) .fontSize(12) .fontColor(Color.White) } } // 交换两个应用的位置 changeIndex(a: number, b: number) { let temp = this.apps[a]; this.apps[a] = this.apps[b]; this.apps[b] = temp; } build() { Column() { Grid() { ForEach(this.apps, (item: string) => { GridItem() { this.itemBuilder(item) } }) } .columnsTemplate('1fr 1fr 1fr 1fr') .rowsGap(20) .columnsGap(20) .padding(20) .supportAnimation(true) // 启用动画 .editMode(true) // 启用编辑模式 // 拖拽开始事件 .onItemDragStart((_event: ItemDragInfo, itemIndex: number) => { return this.itemBuilder(this.apps[itemIndex]); }) // 拖拽放置事件 .onItemDrop((_event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => { if (!isSuccess || insertIndex >= this.apps.length) { return; } this.changeIndex(itemIndex, insertIndex); }) .layoutWeight(1) } .width('100%') .height('100%') .backgroundColor('#121212') } } 在这一步,我们添加了拖拽功能的关键部分:设置supportAnimation(true)来启用动画效果。设置editMode(true)来启用编辑模式,这是实现拖拽功能的必要设置。添加onItemDragStart事件处理函数,当用户开始拖拽时触发,返回被拖拽项的UI表示。添加onItemDrop事件处理函数,当用户放置拖拽项时触发,处理位置交换逻辑。创建changeIndex方法,用于交换数组中两个元素的位置。在Image上设置draggable(false),确保是整个GridItem被拖拽,而不是图片本身。步骤五:添加排序结果展示为了让用户更直观地看到排序结果,我们在网格下方添加一个区域,用于显示当前的应用排序结果。@Entry @Component struct DragGrid { // 前面的代码保持不变... build() { Column() { // 应用网格部分保持不变... // 添加排序结果展示区域 Column({ space: 10 }) { Text('应用排序结果') .fontSize(16) .fontColor(Color.White) Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) { ForEach(this.apps, (item: string, index: number) => { Text(item) .fontSize(12) .fontColor(Color.White) .backgroundColor(this.appColors[index % this.appColors.length]) .borderRadius(12) .padding({ left: 10, right: 10, top: 4, bottom: 4 }) .margin(4) }) } .width('100%') } .width('100%') .padding(10) .backgroundColor('#0DFFFFFF') // 半透明背景 .borderRadius({ topLeft: 20, topRight: 20 }) // 上方圆角 } .width('100%') .height('100%') .backgroundColor('#121212') } } 在这一步,我们添加了一个展示排序结果的区域:使用Column容器,顶部显示"应用排序结果"的标题。使用Flex布局,设置wrap: FlexWrap.Wrap允许内容换行,justifyContent: FlexAlign.Center使内容居中对齐。使用ForEach循环遍历应用数组,为每个应用创建一个带有背景色的文本标签。为结果区域添加半透明背景和上方圆角,使其更加美观。步骤六:美化界面最后,我们美化整个界面,添加渐变背景和一些视觉改进。@Entry @Component struct DragGrid { // 前面的代码保持不变... build() { Column() { // 网格和结果区域代码保持不变... } .width('100%') .height('100%') .expandSafeArea() // 扩展到安全区域 .linearGradient({ // 渐变背景 angle: 135, colors: [ ['#121212', 0], ['#242424', 1] ] }) } } 在这一步,我们:添加expandSafeArea()确保内容可以扩展到设备的安全区域。使用linearGradient创建渐变背景,角度为135度,从深色(#121212)渐变到稍浅的色调(#242424)。完整代码以下是完整的实现代码:@Entry @Component struct DragGrid { // 应用名称数组,用于显示和排序 @State apps: string[] = [ '微信', '支付宝', 'QQ', '抖音', '快手', '微博', '头条', '网易云', '腾讯视频', '爱奇艺', '优酷', 'B站', '小红书', '美团', '饿了么', '滴滴', '高德', '携程' ]; // 定义应用图标对应的颜色数组 private appColors: string[] = [ '#34C759', '#007AFF', '#5856D6', '#FF2D55', '#FF9500', '#FF3B30', '#E73C39', '#D33A31', '#38B0DE', '#39A5DC', '#22C8BD', '#00A1D6', '#FF3A31', '#FFD800', '#4290F7', '#FF7700', '#4AB66B', '#2A9AF1' ]; /** * 构建单个应用图标项 * @param name 应用名称 * @return 返回应用图标的UI组件 */ @Builder itemBuilder(name: string) { // 垂直布局,包含图标和文字 Column({ space: 2 }) { // 应用图标图片 Image($r('app.media.startIcon')) .draggable(false)// 禁止图片本身被拖拽,确保整个GridItem被拖拽 .width(80) .aspectRatio(1)// 保持1:1的宽高比 .padding(10) // 应用名称文本 Text(name) .fontSize(12) .fontColor(Color.White) } } /** * 交换两个应用在数组中的位置 * @param a 第一个索引 * @param b 第二个索引 */ changeIndex(a: number, b: number) { // 使用临时变量交换两个元素位置 let temp = this.apps[a]; this.apps[a] = this.apps[b]; this.apps[b] = temp; } /** * 构建组件的UI结构 */ build() { // 主容器,垂直布局 Column() { // 应用网格区域 Grid() { // 遍历所有应用,为每个应用创建一个网格项 ForEach(this.apps, (item: string) => { GridItem() { // 使用自定义builder构建网格项内容 this.itemBuilder(item) } }) } // 网格样式和行为设置 .columnsTemplate('1fr '.repeat(4)) // 设置4列等宽布局 .columnsGap(20) // 列间距 .rowsGap(20) // 行间距 .padding(20) // 内边距 .supportAnimation(true) // 启用动画支持 .editMode(true) // 启用编辑模式,允许拖拽 // 拖拽开始事件处理 .onItemDragStart((_event: ItemDragInfo, itemIndex: number) => { // 返回被拖拽项的UI return this.itemBuilder(this.apps[itemIndex]); }) // 拖拽放置事件处理 .onItemDrop((_event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => { // 如果拖拽失败或目标位置无效,则不执行操作 if (!isSuccess || insertIndex >= this.apps.length) { return; } // 交换元素位置 this.changeIndex(itemIndex, insertIndex); }) .layoutWeight(1) // 使网格区域占用剩余空间 // 结果显示区域 Column({ space: 10 }) { // 标题文本 Text('应用排序结果') .fontSize(16) .fontColor(Color.White) // 弹性布局,允许换行 Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) { // 遍历应用数组,为每个应用创建一个彩色标签 ForEach(this.apps, (item: string, index: number) => { Text(item) .fontSize(12) .fontColor(Color.White) .backgroundColor(this.appColors[index % this.appColors.length]) .borderRadius(12) .padding({ left: 10, right: 10, top: 4, bottom: 4 }) .margin(4) }) } .width('100%') } .width('100%') .padding(10) // 内边距 .backgroundColor('#0DFFFFFF') // 半透明背景 .expandSafeArea() // 扩展到安全区域 .borderRadius({ topLeft: 20, topRight: 20 }) // 上左右圆角 } // 主容器样式设置 .width('100%') .height('100%') .expandSafeArea() // 扩展到安全区域 .linearGradient({ angle: 135, // 渐变角度 colors: [ ['#121212', 0], // 起点色 ['#242424', 1] // 终点色 ] }) } } Grid组件的关键属性详解Grid是鸿蒙系统中用于创建网格布局的重要组件,它有以下关键属性:columnsTemplate: 定义网格的列模板。例如'1fr 1fr 1fr 1fr'表示四列等宽布局。'1fr’中的’fr’是fraction(分数)的缩写,表示按比例分配空间。rowsTemplate: 定义网格的行模板。如果不设置,行高将根据内容自动调整。columnsGap: 列之间的间距。rowsGap: 行之间的间距。editMode: 是否启用编辑模式。设置为true时启用拖拽功能。supportAnimation: 是否支持动画。设置为true时,拖拽过程中会有平滑的动画效果。拖拽功能的关键事件详解实现拖拽功能主要依赖以下事件:onItemDragStart: 当用户开始拖拽某个项时触发。参数:event(拖拽事件信息),itemIndex(被拖拽项的索引)返回值:被拖拽项的UI表示onItemDrop: 当用户放置拖拽项时触发。参数:event(拖拽事件信息),itemIndex(原始位置索引),insertIndex(目标位置索引),isSuccess(是否成功)功能:处理元素位置交换逻辑此外,还有一些可选的事件可以用于增强拖拽体验:onItemDragEnter: 当拖拽项进入某个位置时触发。onItemDragMove: 当拖拽项在网格中移动时触发。onItemDragLeave: 当拖拽项离开某个位置时触发。小结与进阶提示通过本教程,我们实现了一个功能完整的可拖拽应用网格界面。主要学习了以下内容:使用Grid组件创建网格布局使用@Builder创建可复用的UI构建函数实现拖拽排序功能优化UI和用户体验进阶提示:可以添加长按震动反馈,增强交互体验。可以实现数据持久化,保存用户的排序结果。可以添加编辑模式切换,只有在特定模式下才允许拖拽排序。可以为拖拽过程添加更丰富的动画效果,如缩放、阴影等。希望本教程对你有所帮助,让你掌握鸿蒙系统中Grid组件和拖拽功能的使用方法!
  • [技术干货] 鸿蒙特效教程05-鸿蒙很开门
    鸿蒙特效教程05-鸿蒙很开门本教程适合HarmonyOS初学者,通过简单到复杂的步骤,通过 Stack 层叠布局 + animation 动画,一步步实现这个"鸿蒙很开门"特效。开发环境准备DevEco Studio 5.0.3HarmonyOS Next API 15下载代码仓库最终效果预览屏幕上有一个双开门,点击中间的按钮后,两侧门会向打开,露出开门后面的内容。当用户再次点击按钮时,门会关闭。实现步骤我们将通过以下步骤逐步构建这个效果:用层叠布局搭建基础UI结构用层叠布局创建门的装饰实现开关门动画效果步骤1:搭建基础UI结构首先,我们需要创建一个基本的页面结构。在这个效果中,最关键的是使用Stack组件来实现层叠效果。@Entry @Component struct OpenTheDoor { build() { Stack() { // 背景层 Column() { Text('鸿蒙很开门') .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor(Color.White) } .width('100%') .height('100%') .backgroundColor('#1E2247') // 按钮 Button({ type: ButtonType.Circle }) { Text('开') .fontSize(20) .fontColor(Color.White) } .width(60) .height(60) .backgroundColor('#4CAF50') .position({ x: '50%', y: '85%' }) .translate({ x: '-50%', y: '-50%' }) } .width('100%') .height('100%') .backgroundColor(Color.Black) } } 代码说明:Stack组件是一个层叠布局容器,子组件会按照添加顺序从底到顶叠放。我们首先放置了一个背景层,它包含了将来门打开后要显示的内容。然后放置了一个圆形按钮,用于触发开门动作。使用position和translate组合定位按钮在屏幕底部中间。此时,只有一个简单的背景和按钮,还没有门的效果。步骤2:创建门的设计接下来,我们在Stack层叠布局中添加左右两扇门:@Entry @Component struct OpenTheDoor { build() { Stack() { // 背景层 Column() { Text('鸿蒙很开门') .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor(Color.White) } .width('100%') .height('100%') .backgroundColor('#1E2247') // 左门 Stack() { // 门本体 Column() .width('96%') .height('100%') .backgroundColor('#333333') .borderWidth({ right: 2 }) .borderColor('#444444') // 门上装饰 Column() { Circle() .width(40) .height(40) .fill('#666666') Rect() .width(120) .height(200) .radiusWidth(10) .stroke('#555555') .strokeWidth(2) .fill('none') .margin({ top: 40 }) } .width('80%') .alignItems(HorizontalAlign.Center) } .width('50%') .height('100%') // 右门 Stack() { // 门本体 Column() .width('96%') .height('100%') .backgroundColor('#333333') .borderWidth({ left: 2 }) .borderColor('#444444') // 门上装饰 Column() { Circle() .width(40) .height(40) .fill('#666666') Rect() .width(120) .height(200) .radiusWidth(10) .stroke('#555555') .strokeWidth(2) .fill('none') .margin({ top: 40 }) } .width('80%') .alignItems(HorizontalAlign.Center) } .width('50%') .height('100%') // 门框 Column() .width('100%') .height('100%') .border({ width: 8, color: '#666' }) // 按钮 Button({ type: ButtonType.Circle }) { Text('开') .fontSize(20) .fontColor(Color.White) } .width(60) .height(60) .backgroundColor('#4CAF50') .position({ x: '50%', y: '85%' }) .translate({ x: '-50%', y: '-50%' }) } .width('100%') .height('100%') .backgroundColor(Color.Black) } } 代码说明:我们添加了左右两扇门,每扇门占屏幕宽度的50%。每扇门自身是一个Stack,包含门本体和装饰元素。门本体使用Column组件,设置背景色和边框。装饰元素包括圆形"门把手"和矩形装饰。添加门框作为装饰元素,增强立体感。使用zIndex控制层叠顺序(虽然代码中未显示,但在最终代码中会用到)。此时我们有了一个静态的门的外观,但它还不能打开和关闭。步骤3:实现开关门动画现在我们需要添加状态变量和动画逻辑,使门能够打开和关闭:@Entry @Component struct OpenTheDoor { // 门打开的最大位移(百分比) private doorOpenMaxOffset: number = 110 // 当前门打开的位移 @State doorOpenOffset: number = 0 // 是否正在动画中 @State isAnimating: boolean = false // 切换门的状态 toggleDoor() { this.isAnimating = true if (this.doorOpenOffset <= 0) { // 开门动画 animateTo({ duration: 1500, curve: Curve.EaseInOut, iterations: 1, playMode: PlayMode.Normal, onFinish: () => { this.isAnimating = false } }, () => { this.doorOpenOffset = this.doorOpenMaxOffset }) } else { // 关门动画 animateTo({ duration: 1500, curve: Curve.EaseInOut, iterations: 1, playMode: PlayMode.Normal, onFinish: () => { this.isAnimating = false } }, () => { this.doorOpenOffset = 0 }) } } build() { Stack() { // 背景层(保持不变) ... // 左门 Stack() { // 门本体和装饰(保持不变) ... } .width('50%') .height('100%') .translate({ x: this.doorOpenOffset <= 0 ? '0%' : (-this.doorOpenOffset) + '%' }) // 右门 Stack() { // 门本体和装饰(保持不变) ... } .width('50%') .height('100%') .translate({ x: this.doorOpenOffset <= 0 ? '0%' : this.doorOpenOffset + '%' }) // 门框(保持不变) ... // 按钮 Button({ type: ButtonType.Circle }) { Text(this.doorOpenOffset > 0 ? '关' : '开') .fontSize(20) .fontColor(Color.White) } .width(60) .height(60) .backgroundColor(this.doorOpenOffset > 0 ? '#FF5252' : '#4CAF50') .position({ x: '50%', y: '85%' }) .translate({ x: '-50%', y: '-50%' }) .onClick(() => { if (!this.isAnimating) { this.toggleDoor() } }) } .width('100%') .height('100%') .backgroundColor(Color.Black) } } 代码说明:添加了状态变量:doorOpenMaxOffset: 门打开的最大位移doorOpenOffset: 当前门的位移状态isAnimating: 标记动画是否正在进行使用translate属性绑定到doorOpenOffset状态,实现门的移动效果:左门向左移动:translate({ x: (-this.doorOpenOffset) + '%' })右门向右移动:translate({ x: this.doorOpenOffset + '%' })实现toggleDoor方法,使用animateTo函数创建动画:animateTo是HarmonyOS中用于创建显式动画的API设置动画时长1500毫秒使用EaseInOut曲线使动画更加平滑通过改变doorOpenOffset状态触发UI更新按钮样式和文本随门的状态变化:门关闭时显示"开",背景绿色门打开时显示"关",背景红色添加点击事件调用toggleDoor方法使用isAnimating防止动画进行中重复触发此时,门可以通过动画打开和关闭,但门后的内容没有渐变效果。步骤4:添加门后内容和渐变效果现在我们为门后的内容添加渐变显示效果:@Entry @Component struct OpenTheDoor { // 已有的状态变量 private doorOpenMaxOffset: number = 110 @State doorOpenOffset: number = 0 @State isAnimating: boolean = false // 新增状态变量 @State showContent: boolean = false @State backgroundOpacity: number = 0 toggleDoor() { this.isAnimating = true if (this.doorOpenOffset <= 0) { // 开门动画 animateTo({ duration: 1500, curve: Curve.EaseInOut, iterations: 1, playMode: PlayMode.Normal, onFinish: () => { this.isAnimating = false this.showContent = true } }, () => { this.doorOpenOffset = this.doorOpenMaxOffset this.backgroundOpacity = 1 }) } else { // 关门动画 this.showContent = false animateTo({ duration: 1500, curve: Curve.EaseInOut, iterations: 1, playMode: PlayMode.Normal, onFinish: () => { this.isAnimating = false } }, () => { this.doorOpenOffset = 0 this.backgroundOpacity = 0 }) } } build() { Stack() { // 背景层 - 门后内容 Column() { Text('鸿蒙很开门') .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor(Color.White) .opacity(this.backgroundOpacity) .margin({ bottom: 20 }) Image($r('app.media.startIcon')) .width(100) .height(100) .objectFit(ImageFit.Contain) .opacity(this.backgroundOpacity) .animation({ duration: 800, curve: Curve.EaseOut, delay: 500, iterations: 1, playMode: PlayMode.Normal }) Text('探索无限可能') .fontSize(20) .fontColor(Color.White) .opacity(this.backgroundOpacity) .margin({ top: 20 }) .visibility(this.showContent ? Visibility.Visible : Visibility.Hidden) .animation({ duration: 800, curve: Curve.EaseOut, delay: 100, iterations: 1, playMode: PlayMode.Normal }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .backgroundColor('#1E2247') // 其他部分(左门、右门、按钮等)保持不变 ... } .width('100%') .height('100%') .backgroundColor(Color.Black) } } 代码说明:添加新的状态变量:showContent: 控制额外内容的显示与隐藏backgroundOpacity: 控制背景内容的透明度在toggleDoor方法中同时控制门的位移和内容的透明度:开门时,门位移增加到最大值,同时透明度从0变为1关门时,门位移减少到0,同时透明度从1变为0在开门动画完成后设置showContent为true,显示额外内容为内容元素添加动画效果:使用opacity属性绑定到backgroundOpacity状态为图片添加animation属性,设置渐入效果为第二段文本添加条件显示visibility属性两个元素使用不同的延迟时间,创造错落有致的动画效果这样,当门打开时,背景内容会平滑地渐入,创造更加连贯的用户体验。步骤5:优化交互体验最后,我们添加一些细节来增强交互体验:@Entry @Component struct OpenTheDoor { // 状态变量保持不变 private doorOpenMaxOffset: number = 110 @State doorOpenOffset: number = 0 @State isAnimating: boolean = false @State showContent: boolean = false @State backgroundOpacity: number = 0 // toggleDoor方法保持不变 ... build() { Stack() { // 背景层保持不变 ... // 左门和右门保持不变,但添加zIndex Stack() { ... } .width('50%') .height('100%') .translate({ x: this.doorOpenOffset <= 0 ? '0%' : (-this.doorOpenOffset) + '%' }) .zIndex(3) Stack() { ... } .width('50%') .height('100%') .translate({ x: this.doorOpenOffset <= 0 ? '0%' : this.doorOpenOffset + '%' }) .zIndex(3) // 门框 Column() .width('100%') .height('100%') .zIndex(5) .opacity(0.7) .border({ width: 8, color: '#666' }) // 按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { Stack() { Circle() .width(60) .height(60) .fill('#00000060') if (!this.isAnimating) { // 用文本替代图片 Text(this.doorOpenOffset > 0 ? '关' : '开') .fontSize(20) .fontColor(Color.White) .fontWeight(FontWeight.Bold) } else { // 加载动效 LoadingProgress() .width(30) .height(30) .color(Color.White) } } } .width(60) .height(60) .backgroundColor(this.doorOpenOffset > 0 ? '#FF5252' : '#4CAF50') .position({ x: '50%', y: '85%' }) .translate({ x: '-50%', y: '-50%' }) .zIndex(10) .onClick(() => { if (!this.isAnimating) { this.toggleDoor() } }) } .width('100%') .height('100%') .backgroundColor(Color.Black) .expandSafeArea() } } 代码说明:添加了zIndex属性来控制组件的层叠顺序:背景内容:默认层级最低左右门:zIndex为3门框:zIndex为5,确保在门的上层按钮:zIndex为10,确保始终在最上层改进按钮状态反馈:添加stateEffect: true使按钮有按下效果在动画过程中显示LoadingProgress加载指示器非动画状态下显示"开"或"关"文本添加expandSafeArea()以全屏显示效果,覆盖刘海屏、挖孔屏的安全区域完整代码以下是完整的实现代码:@Entry @Component struct OpenTheDoor { // 门打开的位移 private doorOpenMaxOffset: number = 110 // 门打开的幅度 @State doorOpenOffset: number = 0 // 是否正在动画 @State isAnimating: boolean = false // 是否显示内容 @State showContent: boolean = false // 背景透明度 @State backgroundOpacity: number = 0 toggleDoor() { this.isAnimating = true if (this.doorOpenOffset <= 0) { // 开门动画 animateTo({ duration: 1500, curve: Curve.EaseInOut, iterations: 1, playMode: PlayMode.Normal, onFinish: () => { this.isAnimating = false this.showContent = true } }, () => { this.doorOpenOffset = this.doorOpenMaxOffset this.backgroundOpacity = 1 }) } else { // 关门动画 this.showContent = false animateTo({ duration: 1500, curve: Curve.EaseInOut, iterations: 1, playMode: PlayMode.Normal, onFinish: () => { this.isAnimating = false } }, () => { this.doorOpenOffset = 0 this.backgroundOpacity = 0 }) } } build() { // 层叠布局 Stack() { // 背景层 - 门后内容 Column() { Text('鸿蒙很开门') .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor(Color.White) .opacity(this.backgroundOpacity) .margin({ bottom: 20 }) // 图片 Image($r('app.media.startIcon')) .width(100) .height(100) .objectFit(ImageFit.Contain) .opacity(this.backgroundOpacity) .animation({ duration: 800, curve: Curve.EaseOut, delay: 500, iterations: 1, playMode: PlayMode.Normal }) Text('探索无限可能') .fontSize(20) .fontColor(Color.White) .opacity(this.backgroundOpacity) .margin({ top: 20 }) .visibility(this.showContent ? Visibility.Visible : Visibility.Hidden) .animation({ duration: 800, curve: Curve.EaseOut, delay: 100, iterations: 1, playMode: PlayMode.Normal }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .backgroundColor('#1E2247') .expandSafeArea() // 左门 Stack() { // 门 Column() .width('96%') .height('100%') .backgroundColor('#333333') .borderWidth({ right: 2 }) .borderColor('#444444') // 装饰图案 Column() { // 简单的门把手和几何图案设计 Circle() .width(40) .height(40) .fill('#666666') .opacity(0.8) Rect() .width(120) .height(200) .radiusWidth(10) .stroke('#555555') .strokeWidth(2) .fill('none') .margin({ top: 40 }) // 添加门上的小装饰 Grid() { ForEach(Array.from({ length: 4 }), () => { GridItem() { Circle() .width(8) .height(8) .fill('#777777') } }) } .columnsTemplate('1fr 1fr') .rowsTemplate('1fr 1fr') .width(60) .height(60) .margin({ top: 20 }) } .width('80%') .alignItems(HorizontalAlign.Center) } .width('50%') .height('100%') .translate({ x: this.doorOpenOffset <= 0 ? '0%' : (-this.doorOpenOffset) + '%' }) .zIndex(3) // 右门 Stack() { // 门 Column() .width('96%') .height('100%') .backgroundColor('#333333') .borderWidth({ left: 2 }) .borderColor('#444444') // 装饰图案 Column() { // 简单的门把手和几何图案设计 Circle() .width(40) .height(40) .fill('#666666') .opacity(0.8) Rect() .width(120) .height(200) .radiusWidth(10) .stroke('#555555') .strokeWidth(2) .fill('none') .margin({ top: 40 }) // 添加门上的小装饰 Grid() { ForEach(Array.from({ length: 4 }), () => { GridItem() { Circle() .width(8) .height(8) .fill('#777777') } }) } .columnsTemplate('1fr 1fr') .rowsTemplate('1fr 1fr') .width(60) .height(60) .margin({ top: 20 }) } .width('80%') .alignItems(HorizontalAlign.Center) } .width('50%') .height('100%') .translate({ x: this.doorOpenOffset <= 0 ? '0%' : this.doorOpenOffset + '%' }) .zIndex(3) // 门框 Column() .width('100%') .height('100%') .zIndex(5) .opacity(0.7) .border({ width: 8, color: '#666' }) // 控制按钮 Button({ type: ButtonType.Circle, stateEffect: true }) { Stack() { Circle() .width(60) .height(60) .fill('#00000060') if (!this.isAnimating) { // 用文本替代图片 Text(this.doorOpenOffset > 0 ? '关' : '开') .fontSize(20) .fontColor(Color.White) .fontWeight(FontWeight.Bold) } else { // 加载动效 LoadingProgress() .width(30) .height(30) .color(Color.White) } } } .width(60) .height(60) .backgroundColor(this.doorOpenOffset > 0 ? '#FF5252' : '#4CAF50') .position({ x: '50%', y: '85%' }) .translate({ x: '-50%', y: '-50%' }) .zIndex(10) // 按钮位置在最上方 .onClick(() => { // 防止多点 if (!this.isAnimating) { this.toggleDoor() } }) } .width('100%') .height('100%') .backgroundColor(Color.Black) .expandSafeArea() } } 总结与技术要点涉及了以下HarmonyOS开发中的重要技术点:1. Stack布局Stack组件是实现这种叠加效果,允许子组件按照添加顺序从底到顶叠放。使用时有以下注意点:使用 zIndex 属性控制层叠顺序使用 alignContent 参数控制子组件对齐2. 动画系统本教程中使用了两种动画机制:animateTo:显式动画API,用于创建状态变化时的过渡效果 animateTo({ duration: 1500, curve: Curve.EaseInOut, iterations: 1, playMode: PlayMode.Normal, onFinish: () => { /* 动画完成回调 */ } }, () => { // 状态变化,触发动画 this.doorOpenOffset = this.doorOpenMaxOffset }) animation:属性动画,直接在组件上定义 .animation({ duration: 800, curve: Curve.EaseOut, delay: 500, iterations: 1, playMode: PlayMode.Normal }) 3. 状态管理我们使用以下几个状态来控制整个效果:doorOpenOffset:控制门的位移isAnimating:标记动画状态,防止重复触发backgroundOpacity:控制背景内容的透明度showContent:控制特定内容的显示与隐藏4. translate 位移使用translate属性实现门的移动效果:.translate({ x: this.doorOpenOffset <= 0 ? '0%' : this.doorOpenOffset + '%' }) 扩展与改进这个效果还有很多可以改进和扩展的地方:门的样式:可以添加更多细节,如纹理、把手、贴图等开门音效:添加音效增强3D效果:添加透视效果更多内容过渡:门后可以更有趣手势交互:添加滑动手势来开关门希望这个教程能够帮助你理解HarmonyOS中的层叠布局和动画系统!
  • [技术干货] 鸿蒙特效教程04-直播点赞动画效果实现教程
    鸿蒙特效教程04-直播点赞动画效果实现教程在时下流行的直播、短视频等应用中,点赞动画是提升用户体验的重要元素。当用户点击屏幕时,屏幕上会出现飘动的点赞图标。本教程适合HarmonyOS初学者,通过简单到复杂的步骤,通过HarmonyOS的Canvas组件,一步步实现时下流行的点赞动画效果。开发环境准备DevEco Studio 5.0.3HarmonyOS Next API 15下载代码仓库效果预览我们将实现的效果是:用户点击屏幕时,在点击位置生成一个emoji表情图标,逐步添加了以下动画效果:向上移动:让图标从点击位置向上飘移非线性运动:使用幂函数让移动更加自然渐隐效果:让图标在上升过程中逐渐消失放大效果:让图标从小变大左右摆动:增加水平方向的微妙摆动1. 基础结构搭建首先,我们创建一个基本的页面结构和数据模型,用于管理点赞图标和动画。定义图标数据结构// 定义点赞图标数据结构 interface LikeIcon { x: number // X坐标 y: number // Y坐标 initialX: number // 初始X坐标 initialY: number // 初始Y坐标 radius: number // 半径 emoji: string // emoji表情 fontSize: number // 字体大小 opacity: number // 透明度 createTime: number // 创建时间 lifespan: number // 生命周期(毫秒) scale: number // 当前缩放比例 initialScale: number // 初始缩放比例 maxScale: number // 最大缩放比例 maxOffset: number // 最大摆动幅度 direction: number // 摆动方向 (+1或-1) } 这个接口定义了每个点赞图标所需的所有属性,从位置到动画参数,为后续的动画实现提供了数据基础。组件基本结构@Entry @Component struct CanvasLike { // 用来配置CanvasRenderingContext2D对象的参数,开启抗锯齿 private settings: RenderingContextSettings = new RenderingContextSettings(true) // 创建CanvasRenderingContext2D对象 private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) @State likeIcons: LikeIcon[] = [] // 存储所有点赞图标 private animationId: number = 0 // 动画ID // emoji表情数组 private readonly emojis: string[] = [ '❤️', '🧡', '💛', '💚', '💙', '💜', '🐻', '🐼', '🐨', '🦁', '🐯', '🦊', '🎁', '🎀', '🎉', '🎊', '✨', '⭐' ] // 生命周期方法和核心功能将在后续步骤中添加 build() { Column() { Stack() { Text('直播点赞效果') Canvas(this.context) .width('100%') .height('100%') .onClick((event: ClickEvent) => { // 点击处理逻辑将在后续步骤中添加 }) } } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .backgroundColor(Color.White) } } 这里我们创建了基本的页面结构,包含一个标题和一个全屏的Canvas组件,用于绘制和响应点击事件。2. 实现静态图标绘制首先,我们实现最基础的功能:在Canvas上绘制一个静态的emoji图标。创建图标生成函数// 创建一个图标对象 createIcon(x: number, y: number, radius: number, emoji: string): LikeIcon { return { x: x, y: y, initialX: x, initialY: y, radius: radius, emoji: emoji, fontSize: Math.floor(radius * 1.2), opacity: 1.0, createTime: Date.now(), lifespan: 1000, // 1秒钟生命周期 scale: 1.0, // 暂时不缩放 initialScale: 1.0, maxScale: 1.0, maxOffset: 0, // 暂时不偏移 direction: 1 } } // 获取随机emoji getRandomEmoji(): string { return this.emojis[Math.floor(Math.random() * this.emojis.length)] } // 添加新的点赞图标 addLikeIcon(x: number, y: number) { const radius = 80 // 固定大小 const emoji = this.getRandomEmoji() this.likeIcons.push(this.createIcon(x, y, radius, emoji)) this.drawAllIcons() // 重新绘制所有图标 } 实现基本绘制函数// 绘制所有图标 drawAllIcons() { // 清除画布 this.context.clearRect(0, 0, this.context.width, this.context.height) // 绘制所有图标 for (let icon of this.likeIcons) { // 绘制emoji this.context.font = `${icon.fontSize}px` this.context.textAlign = 'center' this.context.textBaseline = 'middle' this.context.fillText(icon.emoji, icon.x, icon.y) } } 绑定点击事件.onClick((event: ClickEvent) => { console.info(`Clicked at: ${event.x}, ${event.y}`) this.addLikeIcon(event.x, event.y) }) 此时,每次点击Canvas,就会在点击位置绘制一个随机的emoji图标。但这些图标是静态的,不会移动或消失。3. 添加动画循环系统为了实现动画效果,我们需要一个动画循环系统,定期更新图标状态并重新绘制。aboutToAppear() { // 启动动画循环 this.startAnimation() } aboutToDisappear() { // 清除动画循环 clearInterval(this.animationId) } // 开始动画循环 startAnimation() { this.animationId = setInterval(() => { this.updateIcons() this.drawAllIcons() }, 16) // 约60fps的刷新率 } // 更新所有图标状态 updateIcons() { const currentTime = Date.now() const newIcons: LikeIcon[] = [] for (let icon of this.likeIcons) { // 计算图标已存在的时间 const existTime = currentTime - icon.createTime if (existTime < icon.lifespan) { // 保留未完成生命周期的图标 newIcons.push(icon) } } // 更新图标数组 this.likeIcons = newIcons } 现在,我们有了一个基本的动画系统,但图标仍然是静态的。接下来,我们将逐步添加各种动画效果。4. 实现向上移动效果让我们首先让图标动起来,实现一个简单的向上移动效果。// 更新所有图标状态 updateIcons() { const currentTime = Date.now() const newIcons: LikeIcon[] = [] for (let icon of this.likeIcons) { // 计算图标已存在的时间 const existTime = currentTime - icon.createTime if (existTime < icon.lifespan) { // 计算存在时间比例 const progress = existTime / icon.lifespan // 更新Y坐标 - 向上移动 icon.y = icon.initialY - 120 * progress // 保留未完成生命周期的图标 newIcons.push(icon) } } // 更新图标数组 this.likeIcons = newIcons } 现在,图标会在1秒内向上移动120像素,然后消失。这是一个简单的线性移动,看起来有些机械。5. 添加非线性运动效果为了让动画更加自然,我们可以使用幂函数来模拟非线性运动,使图标开始时移动较慢,然后加速。// 更新Y坐标 - 向上移动,速度变化更明显 const verticalDistance = 120 * Math.pow(progress, 0.7) // 使用幂函数让上升更快 icon.y = icon.initialY - verticalDistance幂指数0.7使得图标的上升速度随时间增加,创造出更加自然的加速效果。6. 添加渐隐效果接下来,让图标在上升过程中逐渐消失,增加视觉上的层次感。// 更新透明度 - 前60%保持不变,后40%逐渐消失 if (progress > 0.6) { // 在最后40%的生命周期内改变透明度,使消失更快 icon.opacity = 1.0 - ((progress - 0.6) / 0.4) } else { icon.opacity = 1.0 } // 在绘制时应用透明度 this.context.globalAlpha = icon.opacity这样,图标在生命周期的前60%保持完全不透明,后40%时间内逐渐变透明直到完全消失。这种设计让用户有足够的时间看清图标,然后它才开始消失。7. 实现放大效果现在,让我们添加图标从小变大的动画效果,这会让整个动画更加生动。// 创建图标时设置初始和最大缩放比例 createIcon(x: number, y: number, radius: number, emoji: string): LikeIcon { // 为图标生成随机属性 const initialScale = 0.4 + Math.random() * 0.2 // 初始缩放比例0.4-0.6 const maxScale = 1.0 + Math.random() * 0.3 // 最大缩放比例1.0-1.3 // ... 其他属性设置 ... return { // ... 其他属性 ... scale: initialScale, // 当前缩放比例 initialScale: initialScale, // 初始缩放比例 maxScale: maxScale, // 最大缩放比例 // ... 其他属性 ... } } // 在updateIcons中更新缩放比例 // 更新缩放比例 - 快速放大 // 在生命周期的前20%阶段(0.2s),缩放从initialScale增大到maxScale if (progress < 0.2) { // 平滑插值从initialScale到maxScale icon.scale = icon.initialScale + (icon.maxScale - icon.initialScale) * (progress / 0.2) } else { // 保持maxScale icon.scale = icon.maxScale } // 在绘制时应用缩放 // 设置缩放(从中心点缩放) this.context.translate(icon.x, icon.y) this.context.scale(icon.scale, icon.scale) this.context.translate(-icon.x, -icon.y) 现在,图标会在短时间内从小变大,然后保持大小不变,直到消失。为了确保变换正确,我们使用了translate和scale组合来实现从中心点缩放。8. 添加左右摆动效果最后,我们来实现图标左右摆动的效果,让整个动画更加生动自然。// 创建图标时设置摆动参数 createIcon(x: number, y: number, radius: number, emoji: string): LikeIcon { // ... 其他参数设置 ... // 减小摆动幅度,改为最大8-15像素 const maxOffset = 8 + Math.random() * 7 // 最大摆动幅度8-15像素 // 随机决定初始摆动方向 const direction = Math.random() > 0.5 ? 1 : -1 return { // ... 其他属性 ... maxOffset: maxOffset, // 最大摆动幅度 direction: direction // 初始摆动方向 } } // 在updateIcons中添加水平摆动逻辑 // 更新X坐标 - 快速的左右摆动 // 每0.25秒一个阶段,总共1秒4个阶段 let horizontalOffset = 0; if (progress < 0.25) { // 0-0.25s: 无偏移,专注于放大 horizontalOffset = 0; } else if (progress < 0.5) { // 0.25-0.5s: 向左偏移 const phaseProgress = (progress - 0.25) / 0.25; horizontalOffset = -icon.maxOffset * phaseProgress * icon.direction; } else if (progress < 0.75) { // 0.5-0.75s: 从向左偏移变为向右偏移 const phaseProgress = (progress - 0.5) / 0.25; horizontalOffset = icon.maxOffset * (2 * phaseProgress - 1) * icon.direction; } else { // 0.75-1s: 从向右偏移回到向左偏移 const phaseProgress = (progress - 0.75) / 0.25; horizontalOffset = icon.maxOffset * (1 - 2 * phaseProgress) * icon.direction; } icon.x = icon.initialX + horizontalOffset; 这个摆动算法将1秒的生命周期分为4个阶段:前25%时间:保持在原点,没有摆动,专注于放大效果25%-50%时间:向左偏移到最大值50%-75%时间:从向左偏移变为向右偏移75%-100%时间:从向右偏移变回向左偏移这样就形成了一个完整的"向左向右向左"摆动轨迹,非常符合物理世界中物体的运动规律。9. 优化绘制代码最后,我们需要优化绘制代码,正确处理状态保存和恢复,确保每个图标的绘制不会相互影响。// 绘制所有图标 drawAllIcons() { // 清除画布 this.context.clearRect(0, 0, this.context.width, this.context.height) // 绘制所有图标 for (let icon of this.likeIcons) { this.context.save() // 保存当前状态 // 设置透明度 this.context.globalAlpha = icon.opacity // 设置缩放(从中心点缩放) this.context.translate(icon.x, icon.y) this.context.scale(icon.scale, icon.scale) this.context.translate(-icon.x, -icon.y) // 绘制emoji this.context.font = `${icon.fontSize}px` this.context.textAlign = 'center' this.context.textBaseline = 'middle' this.context.fillText(icon.emoji, icon.x, icon.y) this.context.restore() // 恢复之前保存的状态 } } 每次绘制图标前调用save()方法,绘制完成后调用restore()方法,确保每个图标的绘制参数不会影响其他图标。10. 完整代码将上述所有步骤整合起来,我们就得到了一个完整的点赞动画效果。下面是完整的代码实现:@Entry @Component struct CanvasLike { // 用来配置CanvasRenderingContext2D对象的参数,开启抗锯齿 private settings: RenderingContextSettings = new RenderingContextSettings(true) // 正确创建CanvasRenderingContext2D对象 private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) @State likeIcons: LikeIcon[] = [] // 存储所有点赞图标 private animationId: number = 0 // 动画ID // emoji表情数组 private readonly emojis: string[] = [ '❤️', '🧡', '💛', '💚', '💙', '💜', '🐻', '🐼', '🐨', '🦁', '🐯', '🦊', '🎁', '🎀', '🎉', '🎊', '✨', '⭐' ] aboutToAppear() { // 启动动画循环 this.startAnimation() } aboutToDisappear() { // 清除动画循环 clearInterval(this.animationId) } // 创建一个图标对象 createIcon(x: number, y: number, radius: number, emoji: string): LikeIcon { // 为图标生成随机属性 const initialScale = 0.4 + Math.random() * 0.2 // 初始缩放比例0.4-0.6 const maxScale = 1.0 + Math.random() * 0.3 // 最大缩放比例1.0-1.3 // 减小摆动幅度,改为最大8-15像素 const maxOffset = 8 + Math.random() * 7 // 最大摆动幅度8-15像素 // 随机决定初始摆动方向 const direction = Math.random() > 0.5 ? 1 : -1 return { x: x, y: y, initialX: x, // 记录初始X坐标 initialY: y, // 记录初始Y坐标 radius: radius, emoji: emoji, fontSize: Math.floor(radius * 1.2), opacity: 1.0, createTime: Date.now(), lifespan: 1000, // 1秒钟生命周期 scale: initialScale, // 当前缩放比例 initialScale: initialScale, // 初始缩放比例 maxScale: maxScale, // 最大缩放比例 maxOffset: maxOffset, // 最大摆动幅度 direction: direction // 初始摆动方向 } } // 获取随机emoji getRandomEmoji(): string { return this.emojis[Math.floor(Math.random() * this.emojis.length)] } // 添加新的点赞图标 addLikeIcon(x: number, y: number) { const radius = 80 + Math.random() * 20 // 随机大小80-100 const emoji = this.getRandomEmoji() this.likeIcons.push(this.createIcon(x, y, radius, emoji)) } // 开始动画循环 startAnimation() { this.animationId = setInterval(() => { this.updateIcons() this.drawAllIcons() }, 16) // 约60fps的刷新率 } // 更新所有图标状态 updateIcons() { const currentTime = Date.now() const newIcons: LikeIcon[] = [] for (let icon of this.likeIcons) { // 计算图标已存在的时间 const existTime = currentTime - icon.createTime if (existTime < icon.lifespan) { // 计算存在时间比例 const progress = existTime / icon.lifespan // 1. 更新Y坐标 - 向上移动,速度变化更明显 const verticalDistance = 120 * Math.pow(progress, 0.7) // 使用幂函数让上升更快 icon.y = icon.initialY - verticalDistance // 2. 更新X坐标 - 快速的左右摆动 // 每0.25秒一个阶段,总共1秒4个阶段 let horizontalOffset = 0; if (progress < 0.25) { // 0-0.25s: 无偏移,专注于放大 horizontalOffset = 0; } else if (progress < 0.5) { // 0.25-0.5s: 向左偏移 const phaseProgress = (progress - 0.25) / 0.25; horizontalOffset = -icon.maxOffset * phaseProgress * icon.direction; } else if (progress < 0.75) { // 0.5-0.75s: 从向左偏移变为向右偏移 const phaseProgress = (progress - 0.5) / 0.25; horizontalOffset = icon.maxOffset * (2 * phaseProgress - 1) * icon.direction; } else { // 0.75-1s: 从向右偏移回到向左偏移 const phaseProgress = (progress - 0.75) / 0.25; horizontalOffset = icon.maxOffset * (1 - 2 * phaseProgress) * icon.direction; } icon.x = icon.initialX + horizontalOffset; // 3. 更新缩放比例 - 快速放大 // 在生命周期的前20%阶段(0.2s),缩放从initialScale增大到maxScale if (progress < 0.2) { // 平滑插值从initialScale到maxScale icon.scale = icon.initialScale + (icon.maxScale - icon.initialScale) * (progress / 0.2) } else { // 保持maxScale icon.scale = icon.maxScale } // 4. 更新透明度 - 前60%保持不变,后40%逐渐消失 if (progress > 0.6) { // 在最后40%的生命周期内改变透明度,使消失更快 icon.opacity = 1.0 - ((progress - 0.6) / 0.4) } else { icon.opacity = 1.0 } // 保留未完成生命周期的图标 newIcons.push(icon) } } // 更新图标数组 this.likeIcons = newIcons } // 绘制所有图标 drawAllIcons() { // 清除画布 this.context.clearRect(0, 0, this.context.width, this.context.height) // 绘制所有图标 for (let icon of this.likeIcons) { this.context.save() // 设置透明度 this.context.globalAlpha = icon.opacity // 设置缩放(从中心点缩放) this.context.translate(icon.x, icon.y) this.context.scale(icon.scale, icon.scale) this.context.translate(-icon.x, -icon.y) // 绘制emoji this.context.font = `${icon.fontSize}px` this.context.textAlign = 'center' this.context.textBaseline = 'middle' this.context.fillText(icon.emoji, icon.x, icon.y) this.context.restore() } } build() { Column() { Stack() { Text('直播点赞效果') Canvas(this.context) .width('100%') .height('100%') .onReady(() => { // Canvas已准备好,可以开始绘制 console.info(`Canvas size: ${this.context.width} x ${this.context.height}`) }) .onClick((event: ClickEvent) => { console.info(`Clicked at: ${event.x}, ${event.y}`) this.addLikeIcon(event.x, event.y) }) } } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .backgroundColor(Color.White) .expandSafeArea() } } 总结通过本教程,我们学习了如何使用HarmonyOS的Canvas组件实现直播点赞动画效果。我们从最基础的静态图标绘制开始,逐步形成了一个生动自然的点赞动画。在实现过程中,我们学习了以下重要知识点:Canvas的基本使用方法动画循环系统的实现图形变换(缩放、平移)透明度控制非线性动画实现状态管理的重要性通过这些技术,你可以创建出更多丰富多彩的动画效果,提升你的应用的用户体验。希望本教程对你有所帮助!
  • [技术干货] 鸿蒙特效教程03-水波纹动画效果实现教程
    鸿蒙特效教程03-水波纹动画效果实现教程本教程适合HarmonyOS初学者,通过简单到复杂的步骤,一步步实现漂亮的水波纹动画效果。开发环境准备DevEco Studio 5.0.3HarmonyOS Next API 15下载代码仓库最终效果预览我们将实现以下功能:点击屏幕任意位置,在点击处生成一个水波纹触摸并滑动屏幕,波纹会实时跟随手指位置生成波纹从小到大扩散,同时逐渐消失波纹颜色随机变化,增加视觉多样性一、创建基础布局首先,我们需要创建一个基础页面布局。这个布局包含一个占满屏幕的区域,用于展示水波纹动画。@Entry @Component struct WaterRipple { build() { Column() { // 水波纹动画效果区域 Stack() { // 提示文本 Column() { Text('点击屏幕任意位置产生水波纹') .fontSize(16) .fontColor('#333') .margin({ top: 20 }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } .width('100%') .height('100%') } .width('100%') .height('100%') .backgroundColor('#EDEDED') .expandSafeArea() } } 这段代码创建了一个简单的页面,包含以下元素:最外层是一个占满整个屏幕的Column内部是一个Stack组件,它允许我们将多个元素堆叠在一起在Stack中放置了一个包含提示文本的ColumnStack组件很重要,因为我们后续要在这里动态添加水波纹元素。二、定义水波纹数据结构在实现动画效果之前,我们需要先定义水波纹的数据结构。每个水波纹都有自己的位置、大小、颜色等属性:// 水波纹项目类型定义 interface RippleItem { id: number // 唯一标识 x: number // 中心点X坐标 y: number // 中心点Y坐标 size: number // 当前大小 maxSize: number // 最大大小 opacity: number // 透明度 color: string // 颜色 } @Entry @Component struct WaterRipple { // 水波纹数组,用于存储所有活动的水波纹 @State ripples: RippleItem[] = [] // 触摸状态 @State isTouching: boolean = false // 波纹生成定时器 private rippleTimer: number = -1 // 当前触摸位置 private touchX: number = 0 private touchY: number = 0 // 波纹生成间隔(毫秒) private readonly rippleInterval: number = 200 // 其余代码保持不变... } 使用@State装饰器标记ripples数组和isTouching状态,这样当它们的内容发生变化时,UI会自动更新。我们还定义了一些用于跟踪触摸状态和位置的变量。三、实现波纹绘制与点击事件接下来,我们需要实现波纹的绘制,并处理基本的点击事件:@Entry @Component struct WaterRipple { // 前面定义的属性不变... build() { Column() { // 水波纹动画效果 Stack() { // 水波纹展示区域 Column() { Text('点击屏幕任意位置产生水波纹') .fontSize(16) .fontColor('#333') .margin({ top: 20 }) Text('触摸并滑动,波纹会跟随手指') .fontSize(16) .fontColor('#333') .margin({ top: 10 }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) // 绘制所有波纹 ForEach(this.ripples, (item: RippleItem) => { Circle() .fill(item.color) .width(item.size) .height(item.size) .opacity(item.opacity) .position({ x: item.x - item.size / 2, y: item.y - item.size / 2 }) }) } .width('100%') .height('100%') // 触摸事件处理将在后面添加 } // 其余代码保持不变... } // 创建水波纹的方法(暂未实现) createRipple(x: number, y: number) { console.info(`点击位置: x=${x}, y=${y}`) } } 在这一步中,我们添加了以下内容:使用ForEach遍历ripples数组,为每个波纹创建一个Circle组件为提示文本添加了第二行,说明触摸滑动功能添加了createRipple方法的基本结构(目前只打印坐标)注意Circle组件的定位方式:由于圆形是以左上角为基准定位的,而我们希望以圆心定位,所以需要从坐标中减去圆形半径(即size/2)。四、实现水波纹创建逻辑下一步,我们来实现水波纹的创建逻辑:// 创建一个新的水波纹 createRipple(x: number, y: number) { // 创建随机颜色 const colors = ['#2196F3', '#03A9F4', '#00BCD4', '#4CAF50', '#8BC34A'] const color = colors[Math.floor(Math.random() * colors.length)] // 创建新波纹 const newRipple: RippleItem = { id: Date.now(), // 使用时间戳作为唯一标识 x: x, // X坐标 y: y, // Y坐标 size: 0, // 初始大小为0 maxSize: 300, // 最大扩散到300像素 opacity: 0.6, // 初始透明度 color: color // 随机颜色 } // 添加到波纹数组 this.ripples.push(newRipple) // 启动波纹动画 this.animateRipple(newRipple) } 在这个方法中:我们定义了一个颜色数组,每次随机选择一种颜色创建一个新的波纹对象,初始大小为0,最大扩散尺寸为300像素将新波纹添加到数组中,这会触发UI更新调用animateRipple方法开始动画(下一步实现)五、实现动画效果现在,我们来实现波纹的扩散和消失动画:// 动画处理波纹的扩散和消失 animateRipple(ripple: RippleItem) { let animationStep = 0 const totalSteps = 60 // 总动画帧数 const intervalTime = 16 // 每帧间隔时间(约60fps) const sizeStep = ripple.maxSize / totalSteps // 每帧增加的尺寸 const opacityStep = ripple.opacity / totalSteps // 每帧减少的透明度 const timer = setInterval(() => { animationStep++ // 更新波纹状态 const index = this.ripples.findIndex(item => item.id === ripple.id) if (index !== -1) { // 增加大小 this.ripples[index].size += sizeStep // 降低透明度 this.ripples[index].opacity -= opacityStep // 更新状态触发重绘 this.ripples = [...this.ripples] // 动画结束,移除波纹 if (animationStep >= totalSteps) { clearInterval(timer) this.ripples = this.ripples.filter(item => item.id !== ripple.id) } } else { // 波纹已被其他方式移除 clearInterval(timer) } }, intervalTime) } 这个方法使用了setInterval定时器来创建动画,主要逻辑包括:设置动画参数:总步数、帧间隔、每帧尺寸增量和透明度减量每帧更新波纹的大小和透明度,实现从小到大、从清晰到透明的效果使用[...this.ripples]创建数组的新实例,触发UI更新当动画完成(步数达到总步数)或波纹被移除时,清除定时器动画结束后从数组中移除该波纹,释放内存至此,我们已经实现了基本的水波纹效果。下一步将添加触摸滑动功能。六、实现触摸跟踪功能最后,我们来实现触摸跟踪功能,让波纹能够跟随手指移动:.onTouch((event: TouchEvent) => { // 获取当前触摸点坐标 this.touchX = event.touches[0].x this.touchY = event.touches[0].y // 根据触摸状态处理 switch (event.type) { case TouchType.Down: // 开始触摸,立即创建一个波纹 this.isTouching = true this.createRipple(this.touchX, this.touchY) // 启动定时器连续生成波纹 if (this.rippleTimer === -1) { this.rippleTimer = setInterval(() => { if (this.isTouching) { // 添加小偏移让效果更自然 const offsetX = Math.random() * 10 - 5 const offsetY = Math.random() * 10 - 5 this.createRipple(this.touchX + offsetX, this.touchY + offsetY) } }, this.rippleInterval) } break case TouchType.Move: // 移动时保持触摸状态,波纹会在定时器中根据新坐标创建 this.isTouching = true break case TouchType.Up: case TouchType.Cancel: // 触摸结束,停止生成波纹 this.isTouching = false if (this.rippleTimer !== -1) { clearInterval(this.rippleTimer) this.rippleTimer = -1 } break } }) 在这段代码中,我们使用onTouch事件监听器来处理触摸事件,主要功能包括:触摸开始(Down)时:记录触摸位置立即创建一个波纹启动定时器,以固定间隔创建波纹触摸移动(Move)时:更新触摸位置保持触摸状态,定时器会在新位置创建波纹触摸结束(Up)或取消(Cancel)时:停止触摸状态清除定时器,不再创建新波纹这样实现后,当用户触摸并滑动屏幕时,波纹会实时跟随手指位置生成,创造出一种水流般的视觉效果。完整代码下面是最终的完整代码:// 水波纹项目类型定义 interface RippleItem { id: number // 唯一标识 x: number // 中心点X坐标 y: number // 中心点Y坐标 size: number // 当前大小 maxSize: number // 最大大小 opacity: number // 透明度 color: string // 颜色 } @Entry @Component struct WaterRipple { // 水波纹数组 @State ripples: RippleItem[] = [] // 触摸状态 @State isTouching: boolean = false // 波纹生成定时器 private rippleTimer: number = -1 // 当前触摸位置 private touchX: number = 0 private touchY: number = 0 // 波纹生成间隔(毫秒) private readonly rippleInterval: number = 200 build() { Column() { // 水波纹动画效果 Stack() { // 水波纹展示区域 Column() { Text('点击屏幕任意位置产生水波纹') .fontSize(16) .fontColor('#333') .margin({ top: 20 }) Text('触摸并滑动,波纹会跟随手指') .fontSize(16) .fontColor('#333') .margin({ top: 10 }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) // 绘制所有波纹 ForEach(this.ripples, (item: RippleItem) => { Circle() .fill(item.color) .width(item.size) .height(item.size) .opacity(item.opacity) .position({ x: item.x - item.size / 2, y: item.y - item.size / 2 }) }) } .width('100%') .height('100%') .onTouch((event: TouchEvent) => { // 获取当前触摸点坐标 this.touchX = event.touches[0].x this.touchY = event.touches[0].y // 根据触摸状态处理 switch (event.type) { case TouchType.Down: // 开始触摸,立即创建一个波纹 this.isTouching = true this.createRipple(this.touchX, this.touchY) // 启动定时器连续生成波纹 if (this.rippleTimer === -1) { this.rippleTimer = setInterval(() => { if (this.isTouching) { // 添加小偏移让效果更自然 const offsetX = Math.random() * 10 - 5 const offsetY = Math.random() * 10 - 5 this.createRipple(this.touchX + offsetX, this.touchY + offsetY) } }, this.rippleInterval) } break case TouchType.Move: // 移动时保持触摸状态,波纹会在定时器中根据新坐标创建 this.isTouching = true break case TouchType.Up: case TouchType.Cancel: // 触摸结束,停止生成波纹 this.isTouching = false if (this.rippleTimer !== -1) { clearInterval(this.rippleTimer) this.rippleTimer = -1 } break } }) } .width('100%') .height('100%') .backgroundColor('#EDEDED') .expandSafeArea() } // 创建一个新的水波纹 createRipple(x: number, y: number) { // 创建随机颜色 const colors = ['#2196F3', '#03A9F4', '#00BCD4', '#4CAF50', '#8BC34A'] const color = colors[Math.floor(Math.random() * colors.length)] // 创建新波纹 const newRipple: RippleItem = { id: Date.now(), x: x, y: y, size: 0, maxSize: 300, opacity: 0.6, color: color } // 添加到波纹数组 this.ripples.push(newRipple) // 启动波纹动画 this.animateRipple(newRipple) } // 动画处理波纹的扩散和消失 animateRipple(ripple: RippleItem) { let animationStep = 0 const totalSteps = 60 const intervalTime = 16 // 约60fps const sizeStep = ripple.maxSize / totalSteps const opacityStep = ripple.opacity / totalSteps const timer = setInterval(() => { animationStep++ // 更新波纹状态 const index = this.ripples.findIndex(item => item.id === ripple.id) if (index !== -1) { // 增加大小 this.ripples[index].size += sizeStep // 降低透明度 this.ripples[index].opacity -= opacityStep // 更新状态触发重绘 this.ripples = [...this.ripples] // 动画结束,移除波纹 if (animationStep >= totalSteps) { clearInterval(timer) this.ripples = this.ripples.filter(item => item.id !== ripple.id) } } else { // 波纹已被其他方式移除 clearInterval(timer) } }, intervalTime) } } 总结通过这个教程,我们学习了如何一步步实现水波纹动画效果:ArkUI 搭建基础布局,创建用于展示水波纹的容器@State 定义水波纹数据结构,设计存储和管理波纹的方式实现基本的波纹绘制和触摸事件 onTouch创建水波纹生成逻辑,包括随机颜色 Math.random使用 setInterval 定时器实现波纹扩散和消失的动画效果添加 TouchEvent 触摸跟踪功能,让波纹能够跟随手指滑动这个简单而美观的水波纹效果可以应用在你的应用中的各种交互场景,例如按钮点击、图片查看、页面切换等。通过调整参数,你还可以创造出不同风格的波纹效果。希望这个教程对你有所帮助,祝你在鸿蒙应用开发中创造出更多精彩的交互体验!
  • [技术干货] 鸿蒙特效教程02-微信语音录制动画效果实现教程
    鸿蒙特效教程02-微信语音录制动画效果实现教程本教程适合HarmonyOS初学者,通过简单到复杂的步骤,一步步实现类似微信APP中的语音录制动画效果。开发环境准备DevEco Studio 5.0.3HarmonyOS Next API 15下载代码仓库最终效果预览我们将实现以下功能:长按"按住说话"按钮:显示录音界面和声波动画录音过程中显示实时时长手指上滑:取消录音发送松开手指:根据状态发送或取消录音一、基础布局实现首先,我们需要创建基本的界面布局,模拟微信聊天界面的结构。@Entry @Component struct WeChatRecorder { build() { Column() { // 聊天内容区域(模拟) Stack({ alignContent: Alignment.Center }) { } .layoutWeight(1) // 底部输入栏 Row() { // 录音按钮 Text('按住 说话') .fontSize(16) .fontColor('#333333') .backgroundColor('#F5F5F5') .borderRadius(4) .textAlign(TextAlign.Center) .width('100%') .height(40) .padding({ left: 10, right: 10 }) } .width('100%') .backgroundColor(Color.White) .expandSafeArea() .padding({ left: 15, right: 15, top: 15 }) .border({ width: { top: 1 }, color: '#E5E5E5' }) } .width('100%') .height('100%') .backgroundColor('#EDEDED') .expandSafeArea() } } 这一步我们创建了一个基本的聊天界面布局,包含两部分:顶部聊天内容区域:使用Stack布局,目前为空底部输入栏:包含一个"按住 说话"按钮二、添加状态变量接下来,我们需要添加一些状态变量来跟踪录音状态和动画效果。@Entry @Component struct WeChatRecorder { // 是否正在录音 @State isRecording: boolean = false // 是否显示取消提示(上滑状态) @State isCancel: boolean = false // 录音时长(秒) @State recordTime: number = 0 // 声波高度变化数组 @State waveHeights: number[] = [20, 30, 25, 40, 35, 28, 32, 37] // 计时器ID private timerId: number = 0 // 波形动画计时器ID private waveTimerId: number = 0 // 触摸起始位置 private touchStartY: number = 0 // 触摸移动阈值,超过该值显示取消提示 private readonly cancelThreshold: number = 50 build() { // 之前的布局代码 } } 我们添加了以下状态变量:isRecording:跟踪是否正在录音isCancel:跟踪是否处于取消录音状态(上滑)recordTime:记录录音时长(秒)waveHeights:存储声波高度数组,用于实现波形动画timerId:存储计时器ID,用于后续清除waveTimerId:存储波形动画计时器IDtouchStartY:记录触摸起始位置,用于计算上滑距离cancelThreshold:定义上滑多少距离触发取消状态三、添加基础方法在实现UI交互前,我们先添加一些基础方法来处理录音状态和动画效果。@Entry @Component struct WeChatRecorder { // 状态变量定义... /** * 开始录音,初始化状态及启动计时器 */ startRecording() { this.isRecording = true this.isCancel = false this.recordTime = 0 // 启动计时器,每秒更新录音时长 this.timerId = setInterval(() => { this.recordTime++ }, 1000) // 启动波形动画计时器,随机更新波形高度 this.waveTimerId = setInterval(() => { this.updateWaveHeights() }, 200) } /** * 结束录音,清理计时器和状态 */ stopRecording() { // 清除计时器 if (this.timerId !== 0) { clearInterval(this.timerId) this.timerId = 0 } if (this.waveTimerId !== 0) { clearInterval(this.waveTimerId) this.waveTimerId = 0 } // 如果是取消状态,则显示取消提示 if (this.isCancel) { console.info('录音已取消') } else if (this.recordTime > 0) { // 如果录音时长大于0,则模拟发送语音 console.info(`发送语音,时长: ${this.recordTime}秒`) } // 重置状态 this.isRecording = false this.isCancel = false this.recordTime = 0 } /** * 更新波形高度以产生动画效果 */ updateWaveHeights() { // 创建新的波形高度数组 const newHeights = this.waveHeights.map(() => { // 生成20-40之间的随机高度 return Math.floor(Math.random() * 20) + 20 }) this.waveHeights = newHeights } /** * 格式化时间显示,将秒转换为"00:00"格式 */ formatTime(seconds: number): string { const minutes = Math.floor(seconds / 60) const secs = seconds % 60 return `${minutes.toString() .padStart(2, '0')}:${secs.toString() .padStart(2, '0')}` } aboutToDisappear() { // 组件销毁时清除计时器 if (this.timerId !== 0) { clearInterval(this.timerId) this.timerId = 0 } if (this.waveTimerId !== 0) { clearInterval(this.waveTimerId) this.waveTimerId = 0 } } build() { // 之前的布局代码 } } 在这一步中,我们实现了以下方法:startRecording:开始录音,初始化状态并启动计时器stopRecording:结束录音,清理计时器和状态updateWaveHeights:更新波形高度数组,产生动画效果formatTime:将秒数格式化为"00:00"格式的时间显示aboutToDisappear:组件销毁时清理计时器,防止内存泄漏四、实现长按事件处理接下来,我们为"按住 说话"按钮添加触摸事件处理,实现长按开始录音的功能。@Entry @Component struct WeChatRecorder { // 之前的代码... build() { Column() { Stack({ alignContent: Alignment.Center }) { // 暂时留空,后面会添加录音界面 } .layoutWeight(1) // 底部输入栏 Row() { // 录音按钮 Text(this.isRecording ? '松开 发送' : '按住 说话') .fontSize(16) .fontColor(this.isRecording ? Color.White : '#333333') .backgroundColor(this.isRecording ? '#07C160' : '#F5F5F5') .borderRadius(4) .textAlign(TextAlign.Center) .width('100%') .height(40) .padding({ left: 10, right: 10 }) // 添加触摸事件 .onTouch((event) => { if (event.type === TouchType.Down) { // 按下时,记录起始位置,开始录音 this.touchStartY = event.touches[0].y this.startRecording() } else if (event.type === TouchType.Move) { // 移动时,检测是否上滑到取消区域 const moveDistance = this.touchStartY - event.touches[0].y this.isCancel = moveDistance > this.cancelThreshold } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) { // 松开或取消触摸时,结束录音 this.stopRecording() } }) } .width('100%') .backgroundColor(this.isRecording ? Color.Transparent : Color.White) .expandSafeArea() .padding({ left: 15, right: 15, top: 15 }) .border({ width: { top: 1 }, color: '#E5E5E5' }) } .width('100%') .height('100%') .backgroundColor('#EDEDED') .expandSafeArea() } } 在这一步中,我们:为按钮文本添加了动态内容,根据录音状态显示不同文字为按钮添加了触摸事件处理,包括按下、移动和松开/取消根据录音状态动态改变底部栏的背景色五、实现录音界面和声波动画最后,我们添加录音状态下的界面显示,包括上滑取消提示和声波动画。@Entry @Component struct WeChatRecorder { // 之前的代码... build() { Column() { // 聊天内容区域 Stack({ alignContent: Alignment.Center }) { // 录音状态提示 if (this.isRecording) { // 遮罩背景 Column() .width('100%') .height('100%') .backgroundColor('#80000000') .expandSafeArea() Column() { // 上滑取消提示 Text(this.isCancel ? '松开手指,取消发送' : '手指上滑,取消发送') .fontSize(14) .fontColor(this.isCancel ? Color.Red : '#999999') .backgroundColor(this.isCancel ? '#FFE9E9' : '#D1D1D1') .borderRadius(4) .padding({ left: 10, right: 10, top: 5, bottom: 5 }) .margin({ bottom: 20 }) // 录音界面容器 Column() { // 声波动画容器 Row() { ForEach(this.waveHeights, (height: number, index) => { Column() .width(4) .height(height) .backgroundColor('#7ED321') .borderRadius(2) .margin({ left: 3, right: 3 }) }) } .width(160) .height(100) .justifyContent(FlexAlign.Center) .margin({ bottom: 15 }) // 录音时间显示 Text(`${this.formatTime(this.recordTime)}`) .fontSize(16) .fontColor('#999999') } .width(180) .backgroundColor(Color.White) .borderRadius(8) .justifyContent(FlexAlign.Center) .padding(10) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } } .layoutWeight(1) // 底部输入栏 // 与之前的代码相同 } // 与之前的代码相同 } } 在这一步中,我们添加了:录音状态下的遮罩背景,使用半透明黑色背景上滑取消提示,根据 isCancel 状态显示不同内容和样式声波动画容器,使用 ForEach 循环遍历 waveHeights 数组创建多个柱状条录音时间显示,使用 formatTime 方法格式化时间六、完整实现下面是完整的实现代码:/** * 微信语音录制动画效果 * 实现功能: * 1. 长按按钮: 显示录音动画 * 2. 上滑取消: 模拟取消录音 * 3. 松开发送: 模拟发送语音 */ @Entry @Component struct WeChatRecorder { // 是否正在录音 @State isRecording: boolean = false // 是否显示取消提示(上滑状态) @State isCancel: boolean = false // 录音时长(秒) @State recordTime: number = 0 // 声波高度变化数组 @State waveHeights: number[] = [20, 30, 25, 40, 35, 28, 32, 37] // 计时器ID private timerId: number = 0 // 波形动画计时器ID private waveTimerId: number = 0 // 触摸起始位置 private touchStartY: number = 0 // 触摸移动阈值,超过该值显示取消提示 private readonly cancelThreshold: number = 50 /** * 开始录音,初始化状态及启动计时器 */ startRecording() { this.isRecording = true this.isCancel = false this.recordTime = 0 // 启动计时器,每秒更新录音时长 this.timerId = setInterval(() => { this.recordTime++ }, 1000) // 启动波形动画计时器,随机更新波形高度 this.waveTimerId = setInterval(() => { this.updateWaveHeights() }, 200) } /** * 结束录音,清理计时器和状态 */ stopRecording() { // 清除计时器 if (this.timerId !== 0) { clearInterval(this.timerId) this.timerId = 0 } if (this.waveTimerId !== 0) { clearInterval(this.waveTimerId) this.waveTimerId = 0 } // 如果是取消状态,则显示取消提示 if (this.isCancel) { console.info('录音已取消') } else if (this.recordTime > 0) { // 如果录音时长大于0,则模拟发送语音 console.info(`发送语音,时长: ${this.recordTime}秒`) } // 重置状态 this.isRecording = false this.isCancel = false this.recordTime = 0 } /** * 更新波形高度以产生动画效果 */ updateWaveHeights() { // 创建新的波形高度数组 const newHeights = this.waveHeights.map(() => { // 生成20-40之间的随机高度 return Math.floor(Math.random() * 20) + 20 }) this.waveHeights = newHeights } /** * 格式化时间显示,将秒转换为"00:00"格式 */ formatTime(seconds: number): string { const minutes = Math.floor(seconds / 60) const secs = seconds % 60 return `${minutes.toString() .padStart(2, '0')}:${secs.toString() .padStart(2, '0')}` } aboutToDisappear() { // 组件销毁时清除计时器 if (this.timerId !== 0) { clearInterval(this.timerId) this.timerId = 0 } if (this.waveTimerId !== 0) { clearInterval(this.waveTimerId) this.waveTimerId = 0 } } build() { Column() { // 聊天内容区域(模拟) Stack({ alignContent: Alignment.Center }) { // 录音状态提示 if (this.isRecording) { // 遮罩背景 Column() .width('100%') .height('100%') .backgroundColor('#80000000') .expandSafeArea() Column() { // 上滑取消提示 Text(this.isCancel ? '松开手指,取消发送' : '手指上滑,取消发送') .fontSize(14) .fontColor(this.isCancel ? Color.Red : '#999999') .backgroundColor(this.isCancel ? '#FFE9E9' : '#D1D1D1') .borderRadius(4) .padding({ left: 10, right: 10, top: 5, bottom: 5 }) .margin({ bottom: 20 }) // 录音界面容器 Column() { // 声波动画容器 Row() { ForEach(this.waveHeights, (height: number, index) => { Column() .width(4) .height(height) .backgroundColor('#7ED321') .borderRadius(2) .margin({ left: 3, right: 3 }) }) } .width(160) .height(100) .justifyContent(FlexAlign.Center) .margin({ bottom: 15 }) // 录音时间显示 Text(`${this.formatTime(this.recordTime)}`) .fontSize(16) .fontColor('#999999') } .width(180) .backgroundColor(Color.White) .borderRadius(8) .justifyContent(FlexAlign.Center) .padding(10) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } } .layoutWeight(1) // 底部输入栏 Row() { // 录音按钮 Text(this.isRecording ? '松开 发送' : '按住 说话') .fontSize(16) .fontColor(this.isRecording ? Color.White : '#333333') .backgroundColor(this.isRecording ? '#07C160' : '#F5F5F5') .borderRadius(4) .textAlign(TextAlign.Center) .width('100%') .height(40) .padding({ left: 10, right: 10 })// 添加触摸事件 .onTouch((event) => { if (event.type === TouchType.Down) { // 按下时,记录起始位置,开始录音 this.touchStartY = event.touches[0].y this.startRecording() } else if (event.type === TouchType.Move) { // 移动时,检测是否上滑到取消区域 const moveDistance = this.touchStartY - event.touches[0].y this.isCancel = moveDistance > this.cancelThreshold } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) { // 松开或取消触摸时,结束录音 this.stopRecording() } }) } .width('100%') .backgroundColor(this.isRecording ? Color.Transparent : Color.White) .expandSafeArea() .padding({ left: 15, right: 15, top: 15 }) .border({ width: { top: 1 }, color: '#E5E5E5' }) } .width('100%') .height('100%') .backgroundColor('#EDEDED') .expandSafeArea() } } 拓展与优化以上是基本的实现,如果想进一步优化,可以考虑:真实的录音功能:使用HarmonyOS的媒体录制API实现实际录音声音波形实时变化:根据实际录音音量调整波形高度振动反馈:在录音开始、取消或发送时添加振动反馈显示已录制的语音消息:将录制好的语音添加到聊天消息列表中录音时长限制:添加最长录音时间限制(如微信的60秒)总结通过这个教程,我们从零开始实现了类似微信的语音录制动画效果。主要用到了以下技术:HarmonyOS的ArkUI布局系统状态管理(@State)触摸事件处理定时器和动画条件渲染组件生命周期处理这些技术和概念不仅适用于这个特定效果,还可以应用于各种交互设计中。希望这个教程能帮助你更好地理解HarmonyOS开发,并创建出更加精美的应用界面!
  • [技术干货] 鸿蒙特效教程01-哔哩哔哩点赞与一键三连
    鸿蒙特效教程01-哔哩哔哩点赞与一键三连本教程适合HarmonyOS初学者,通过简单到复杂的步骤,一步步实现类似哔哩哔哩APP中的点赞及一键三连效果。开发环境准备DevEco Studio 5.0.3HarmonyOS Next API 15下载代码仓库最终效果预览我们将实现以下两个效果:点击点赞按钮:实现点赞/取消点赞的切换效果,包含图标变色和缩放动画长按点赞按钮:实现一键三连效果,包含旋转、缩放和粒子动画一、基础布局实现首先,我们需要创建基本的界面布局。这一步非常简单,只需要放置一个点赞图标。@Entry @Component struct BilibiliLike { build() { // 使用Stack布局,使元素居中对齐 Stack({ alignContent: Alignment.Center }) { // 点赞图标 Image($r('app.media.ic_public_like')) .width(50) // 设置图标宽度 .aspectRatio(1) // 保持宽高比为1:1 .objectFit(ImageFit.Contain) // 设置图片适应方式 } .width('100%') // 容器占满宽度 .height('100%') // 容器占满高度 .backgroundColor('#f1f1f1') // 设置背景颜色为浅灰色 } } 这一步实现了最基础的布局,我们使用了Stack布局使元素居中,并添加了一个点赞图标。点赞图标大家可以去 iconfont.cn 平台下载一个,通过 $() 函数导入资源$r('app.media.ic_public_like')。二、添加状态和颜色切换接下来,我们添加状态变量来跟踪点赞状态,并实现点击时的颜色切换效果。@Entry @Component struct BilibiliLike { // 是否已点赞状态 @State isLiked: boolean = false // 未点赞时的颜色(灰色) @State unlikeColor: string = '#757575' // 点赞后的颜色(蓝色) @State likeColor: string = '#4eabe6' build() { Stack({ alignContent: Alignment.Center }) { Image($r('app.media.ic_public_like')) .width(50) .aspectRatio(1) .objectFit(ImageFit.Contain) .fillColor(this.isLiked ? this.likeColor : this.unlikeColor) // 根据点赞状态设置颜色 .onClick(() => { // 点击时切换点赞状态 this.isLiked = !this.isLiked }) } .width('100%') .height('100%') .backgroundColor('#f1f1f1') } } 这一步我们添加了:isLiked 状态变量,用于跟踪是否已点赞两个颜色变量 unlikeColor 和 likeColor在图标上添加 fillColor 属性,根据点赞状态切换颜色为图标添加 onClick 事件,实现点击时切换点赞状态现在点击图标时,图标会在灰色和蓝色之间切换,实现了基本的点赞/取消点赞效果。三、添加点击动画效果点击时的颜色变化已经实现了,但用户体验还不够好。我们需要添加一些动画效果,让点赞操作更加生动。@Entry @Component struct BilibiliLike { @State isLiked: boolean = false @State unlikeColor: string = '#757575' @State likeColor: string = '#4eabe6' // 添加控制缩放的状态变量 @State scaleValue: number = 1 build() { Stack({ alignContent: Alignment.Center }) { Image($r('app.media.ic_public_like')) .width(50) .aspectRatio(1) .objectFit(ImageFit.Contain) .fillColor(this.isLiked ? this.likeColor : this.unlikeColor) .scale({ x: this.scaleValue, y: this.scaleValue }) // 添加缩放效果 .onClick(() => { this.toggleLike() // 调用点赞切换方法 }) } .width('100%') .height('100%') .backgroundColor('#f1f1f1') } // 处理点赞动画效果 toggleLike() { // 切换点赞状态 this.isLiked = !this.isLiked // 点赞动画 - 第一阶段:放大 animateTo({ duration: 300, // 动画持续300毫秒 curve: Curve.Friction, // 使用摩擦曲线,效果更自然 delay: 0 // 无延迟立即执行 }, () => { this.scaleValue = 1.5 // 图标放大到1.5倍 }) // 点赞动画 - 第二阶段:恢复原大小 animateTo({ duration: 100, // 动画持续100毫秒 curve: Curve.Friction, // 使用摩擦曲线 delay: 300 // 延迟300毫秒执行,等第一阶段完成 }, () => { this.scaleValue = 1 // 图标恢复到原始大小 }) } } 这一步我们添加了:scaleValue 状态变量,用于控制图标的缩放比例图标添加 scale 属性,绑定到 scaleValue 状态创建了 toggleLike 方法,实现点赞状态切换和动画效果使用 animateTo 方法创建两段动画:先放大,再恢复原大小现在点击图标时,不仅会变色,还会有一个放大再缩小的动画效果,让交互更加生动。四、添加长按手势识别接下来,我们要实现长按触发一键三连的功能,首先需要添加长按手势识别。@Entry @Component struct BilibiliLike { @State isLiked: boolean = false @State unlikeColor: string = '#757575' @State likeColor: string = '#4eabe6' @State scaleValue: number = 1 // 添加一个标记三连效果的状态 @State isTripleAction: boolean = false build() { Stack({ alignContent: Alignment.Center }) { Image($r('app.media.ic_public_like')) .width(50) .aspectRatio(1) .objectFit(ImageFit.Contain) .fillColor(this.isLiked ? this.likeColor : this.unlikeColor) .scale({ x: this.scaleValue, y: this.scaleValue }) .draggable(false) // 防止用户拖拽图片,导致点赞按钮无法长按 // 添加长按手势 .gesture( LongPressGesture({ repeat: false }) // 长按手势,不重复触发 .onAction(() => { console.info("检测到长按事件") // 设置状态 this.isTripleAction = true this.isLiked = true }) ) .onClick(() => { this.toggleLike() }) } .width('100%') .height('100%') .backgroundColor('#f1f1f1') } // 点赞动画方法保持不变 toggleLike() { // 代码同上... } } 这一步我们添加了:isTripleAction 状态变量,用于标记是否触发了一键三连效果使用 gesture 方法添加长按手势识别在长按触发时设置相关状态注意图片添加 draggable(false),防止用户拖拽图片,导致点赞按钮无法长按现在,长按图标后,会将点赞状态设为已点赞,并触发三连状态,但还没有具体的动画效果。五、添加长按动画效果下一步,我们为长按添加更丰富的动画效果,包括旋转和缩放。@Entry @Component struct BilibiliLike { @State isLiked: boolean = false @State isTripleAction: boolean = false @State scaleValue: number = 1 @State unlikeColor: string = '#757575' @State likeColor: string = '#4eabe6' // 添加旋转角度状态 @State rotation: number = 0 build() { Stack({ alignContent: Alignment.Center }) { Image($r('app.media.ic_public_like')) .width(50) .aspectRatio(1) .objectFit(ImageFit.Contain) .fillColor(this.isLiked ? this.likeColor : this.unlikeColor) .scale({ x: this.scaleValue, y: this.scaleValue }) .rotate({ z: 1, angle: this.rotation }) // 添加旋转效果 .draggable(false) .gesture( LongPressGesture({ repeat: false }) .onAction(() => { this.triggerTripleAction() // 调用三连效果方法 }) ) .onClick(() => { this.toggleLike() }) } .width('100%') .height('100%') .backgroundColor('#f1f1f1') } // 点赞动画方法保持不变 toggleLike() { // 代码同上... } // 处理一键三连动画效果 triggerTripleAction() { // 设置三连状态和点赞状态 this.isTripleAction = true this.isLiked = true // 阶段一:点赞按钮放大并旋转 animateTo({ duration: 200, // 动画持续200毫秒 curve: Curve.EaseInOut // 使用缓入缓出曲线 }, () => { this.scaleValue = 1.8 // 图标放大到1.8倍 this.rotation = 20 // 图标旋转20度 }) // 阶段二:点赞按钮缩小并恢复角度 animateTo({ duration: 100, // 动画持续100毫秒 curve: Curve.EaseInOut, // 使用缓入缓出曲线 delay: 200 // 延迟200毫秒,等阶段一完成 }, () => { this.scaleValue = 1.2 // 图标缩小到1.2倍 this.rotation = 0 // 图标恢复原始角度 }) // 阶段三:点赞按钮恢复原始大小 animateTo({ duration: 100, // 动画持续100毫秒 curve: Curve.EaseInOut, // 使用缓入缓出曲线 delay: 300 // 延迟300毫秒,等阶段二完成 }, () => { this.scaleValue = 1 // 图标恢复原始大小 }) // 重置三连状态 setTimeout(() => { this.isTripleAction = false }, 1000) // 延迟1000毫秒(1秒)执行 } } 这一步我们添加了:rotation 状态变量,用于控制图标的旋转角度图标添加 rotate 属性,实现旋转效果创建了 triggerTripleAction 方法,实现三连动画效果三段连续动画,实现放大旋转、缩小恢复的动画序列现在,长按图标会触发一系列动画:图标先放大并顺时针旋转,然后缩小并恢复原始角度,最后恢复原始大小,整个过程非常流畅。六、添加粒子爆炸效果最后,我们添加粒子爆炸效果,让一键三连的视觉效果更加丰富。@Entry @Component struct BilibiliLike { @State isLiked: boolean = false @State isTripleAction: boolean = false @State scaleValue: number = 1 @State rotation: number = 0 @State unlikeColor: string = '#757575' @State likeColor: string = '#4eabe6' // 添加粒子透明度状态 @State iconOpacity: number = 0 build() { Stack({ alignContent: Alignment.Center }) { // 粒子效果容器 Column() { // 当触发三连效果时显示粒子 if (this.isTripleAction) { // 创建8个粒子,均匀分布在圆周上 ForEach([1, 2, 3, 4, 5, 6, 7, 8], (item: number) => { Circle() .width(8) // 粒子宽度 .height(8) // 粒子高度 .fill(this.likeColor) // 使用点赞颜色填充粒子 // 使用三角函数计算粒子在圆周上的位置 .position({ x: 25 + 50 * Math.cos(item * Math.PI / 4), y: 25 + 50 * Math.sin(item * Math.PI / 4) }) .opacity(this.iconOpacity) // 控制粒子的透明度 }) } } .width(50) // 设置容器宽度 .height(50) // 设置容器高度 // 点赞按钮 Image($r('app.media.ic_public_like')) .width(50) .aspectRatio(1) .objectFit(ImageFit.Contain) .fillColor(this.isLiked ? this.likeColor : this.unlikeColor) .scale({ x: this.scaleValue, y: this.scaleValue }) .rotate({ z: 1, angle: this.rotation }) .draggable(false) .gesture( LongPressGesture({ repeat: false }) .onAction(() => { this.triggerTripleAction() }) ) .onClick(() => { this.toggleLike() }) } .width('100%') .height('100%') .backgroundColor('#f1f1f1') } // 点赞动画方法保持不变 toggleLike() { // 代码同上... } // 处理一键三连动画效果 triggerTripleAction() { // 设置三连状态和点赞状态 this.isTripleAction = true this.isLiked = true // 点赞按钮动画部分保持不变 // ... // 添加粒子出现动画 animateTo({ duration: 500, // 动画持续500毫秒 curve: Curve.EaseOut, // 使用缓出曲线,开始快结束慢 delay: 200 // 延迟200毫秒执行 }, () => { this.iconOpacity = 1 // 粒子透明度变为1,完全显示 }) // 粒子消失动画 animateTo({ duration: 300, // 动画持续300毫秒 curve: Curve.Linear, // 使用线性曲线,匀速变化 delay: 700 // 延迟700毫秒,等粒子完全显示一段时间 }, () => { this.iconOpacity = 0 // 粒子透明度变为0,完全消失 }) // 重置三连状态 setTimeout(() => { this.isTripleAction = false }, 1000) // 延迟1000毫秒(1秒)执行 } } 这一步我们添加了:iconOpacity 状态变量,用于控制粒子的透明度在Stack中添加粒子效果容器使用ForEach和条件渲染,当触发三连效果时创建并显示粒子使用三角函数计算粒子在圆周上的位置添加粒子的动画效果,包括出现和消失七、完整实现最后,我们来看一下最终的完整代码实现。这里整合了前面所有的步骤,并添加了一些额外的细节优化。/** * 哔哩哔哩点赞和一键三连效果组件 * 实现功能: * 1. 普通点击: 实现点赞/取消点赞效果 * 2. 长按: 触发一键三连效果(点赞+粒子动画) */ @Entry @Component struct BilibiliLike { // 是否已点赞状态 @State isLiked: boolean = false // 是否触发一键三连效果状态 @State isTripleAction: boolean = false // 控制图标缩放比例 @State scaleValue: number = 1 // 控制图标透明度 @State opacityValue: number = 1 // 控制图标X轴偏移量 @State translateXValue: number = 0 // 控制图标Y轴偏移量 @State translateYValue: number = 0 // 控制图标旋转角度 @State rotation: number = 0 // 控制粒子效果透明度 @State iconOpacity: number = 0 // 未点赞时的颜色(灰色) @State unlikeColor: string = '#757575' // 点赞后的颜色(蓝色) @State likeColor: string = '#4eabe6' build() { // 使用Stack布局,所有元素居中对齐 Stack({ alignContent: Alignment.Center }) { // 粒子效果容器 Column() { // 当触发三连效果时显示粒子 if (this.isTripleAction) { // 创建8个粒子,均匀分布在圆周上 ForEach([1, 2, 3, 4, 5, 6, 7, 8], (item: number) => { Circle() .width(8) // 粒子宽度 .height(8) // 粒子高度 .fill(this.likeColor) // 使用点赞颜色填充粒子 // 使用三角函数计算粒子在圆周上的位置 .position({ x: 25 + 50 * Math.cos(item * Math.PI / 4), y: 25 + 50 * Math.sin(item * Math.PI / 4) }) .opacity(this.iconOpacity) // 控制粒子的透明度 }) } } .width(50) // 设置容器宽度 .height(50) // 设置容器高度 // 点赞按钮 Image($r('app.media.ic_public_like')) // 使用点赞图标资源 .width(50) // 设置图标宽度 .aspectRatio(1) // 保持宽高比为1:1 .objectFit(ImageFit.Contain) // 设置图片适应方式 .fillColor(this.isLiked ? this.likeColor : this.unlikeColor) // 根据点赞状态设置颜色 .scale({ x: this.scaleValue, y: this.scaleValue }) // 设置缩放值 .opacity(this.opacityValue) // 设置透明度 .rotate({ z: 1, angle: this.rotation }) // 设置旋转角度,绕z轴旋转 .translate({ x: this.translateXValue, y: this.translateYValue }) // 设置平移值 .draggable(false) // 防止用户拖拽图片,导致点赞按钮无法长按 // 添加长按手势,触发一键三连效果 .gesture( LongPressGesture({ repeat: false }) // 长按手势,不重复触发 .onAction(() => { this.triggerTripleAction() // 调用三连效果方法 }) ) // 添加点击事件,触发普通点赞效果 .onClick(() => { this.toggleLike() // 调用点赞切换方法 }) } .width('100%') // 容器占满宽度 .height('100%') // 容器占满高度 .backgroundColor('#f1f1f1') // 设置背景颜色为浅灰色 .expandSafeArea() // 扩展到安全区域 } /** * 处理点赞/取消点赞动画效果 * 点击时切换点赞状态并播放放大缩小动画 */ toggleLike() { // 切换点赞状态 this.isLiked = !this.isLiked // 点赞动画 - 第一阶段:放大 animateTo({ duration: 300, // 动画持续300毫秒 curve: Curve.Friction, // 使用摩擦曲线,效果更自然 delay: 0 // 无延迟立即执行 }, () => { this.scaleValue = 1.5 // 图标放大到1.5倍 }) // 点赞动画 - 第二阶段:恢复原大小 animateTo({ duration: 100, // 动画持续100毫秒 curve: Curve.Friction, // 使用摩擦曲线 delay: 300 // 延迟300毫秒执行,等第一阶段完成 }, () => { this.scaleValue = 1 // 图标恢复到原始大小 }) } /** * 处理一键三连动画效果 * 包含多个动画阶段:放大旋转、粒子出现与消失 */ triggerTripleAction() { // 设置三连状态和点赞状态 this.isTripleAction = true // 启用三连效果 this.isLiked = true // 设置为已点赞状态 // 阶段一:点赞按钮放大并旋转 animateTo({ duration: 200, // 动画持续200毫秒 curve: Curve.EaseInOut // 使用缓入缓出曲线 }, () => { this.scaleValue = 1.8 // 图标放大到1.8倍 this.rotation = 20 // 图标旋转20度 }) // 阶段二:点赞按钮缩小并恢复角度 animateTo({ duration: 100, // 动画持续100毫秒 curve: Curve.EaseInOut, // 使用缓入缓出曲线 delay: 200 // 延迟200毫秒,等阶段一完成 }, () => { this.scaleValue = 1.2 // 图标缩小到1.2倍 this.rotation = 0 // 图标恢复原始角度 }) // 阶段三:点赞按钮恢复原始大小 animateTo({ duration: 100, // 动画持续100毫秒 curve: Curve.EaseInOut, // 使用缓入缓出曲线 delay: 300 // 延迟300毫秒,等阶段二完成 }, () => { this.scaleValue = 1 // 图标恢复原始大小 this.isLiked = true // 确保点赞状态为true }) // 阶段四:粒子出现动画 animateTo({ duration: 500, // 动画持续500毫秒 curve: Curve.EaseOut, // 使用缓出曲线,开始快结束慢 delay: 200 // 延迟200毫秒执行 }, () => { this.iconOpacity = 1 // 粒子透明度变为1,完全显示 }) // 阶段五:粒子消失动画 animateTo({ duration: 300, // 动画持续300毫秒 curve: Curve.Linear, // 使用线性曲线,匀速变化 delay: 700 // 延迟700毫秒,等粒子完全显示一段时间 }, () => { this.iconOpacity = 0 // 粒子透明度变为0,完全消失 }) // 延时重置三连状态,完成整个动画周期 setTimeout(() => { this.isTripleAction = false // 关闭三连效果状态 }, 1000) // 延迟1000毫秒(1秒)执行 } } 拓展与优化以上是基本的实现,如果想进一步优化,可以考虑:添加声音效果:在点赞和三连时添加声音反馈添加震动反馈:利用设备振动提供触觉反馈优化粒子效果:使用更复杂的粒子系统,例如随机大小、颜色和速度添加投币和收藏图标:真正实现完整的"一键三连"效果,显示投币和收藏图标总结通过这个教程,我们从零开始实现了哔哩哔哩的点赞和一键三连效果。主要用到了以下技术:HarmonyOS的ArkUI布局系统状态管理(@State)手势处理(点击、长按)动画系统(animateTo)条件渲染数学计算(用于粒子位置)这些技术和概念不仅适用于这个特定效果,还可以应用于各种交互设计中。希望这个教程能帮助你更好地理解HarmonyOS开发,并创建出更加精美的应用界面!
  • [技术干货] 鸿蒙NEXT开发案例:随机数生成
    【引言】本项目是一个简单的随机数生成器应用,用户可以通过设置随机数的范围和个数,并选择是否允许生成重复的随机数,来生成所需的随机数列表。生成的结果可以通过点击“复制”按钮复制到剪贴板。【环境准备】• 操作系统:Windows 10• 开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806• 目标设备:华为Mate60 Pro• 开发语言:ArkTS• 框架:ArkUI• API版本:API 12【关键技术点】1. 用户界面设计用户界面主要包括以下几个部分:• 标题栏:显示应用名称。• 输入框:用户可以输入随机数的起始值、结束值和生成个数。• 开关:用户可以选择生成的随机数是否允许重复。• 生成按钮:点击后生成随机数。• 结果显示区:显示生成的随机数,并提供复制功能。2. 随机数生成算法随机数生成是本项目的重点。根据用户是否允许生成重复的随机数,算法分为两种情况:2.1 不允许重复当用户选择不允许生成重复的随机数时,程序使用一个 Set 来存储生成的随机数,利用 Set 的特性自动去重。具体步骤如下:1)计算范围:计算用户指定的随机数范围 range = endValue - startValue + 1。2)生成随机数:使用一个临时数组 tempArray 来辅助生成不重复的随机数。每次生成一个随机索引 randomIndex,从 tempArray 中取出或计算一个新的随机数 randomNum,并将其添加到 Set 中。3)更新临时数组:将 tempArray 中末尾的元素移动到随机位置,以确保下次生成的随机数仍然是唯一的。if (!this.isUnique) { if (countValue > range) { // 显示错误提示 this.getUIContext().showAlertDialog({ title: '错误提示', message: `请求的随机数数量超过了范围内的总数`, confirm: { defaultFocus: true, value: '我知道了', fontColor: Color.White, backgroundColor: this.primaryColor, action: () => {} }, onWillDismiss: () => {}, alignment: DialogAlignment.Center, }); return; } for (let i = 0; i < countValue; i++) { let randomIndex = Math.floor(Math.random() * (range - i)); let randomNum = 0; if (tempArray[randomIndex] !== undefined) { randomNum = tempArray[randomIndex]; } else { randomNum = startValue + randomIndex; } generatedNumbers.add(randomNum); if (tempArray[range - 1 - i] === undefined) { tempArray[range - 1 - i] = startValue + range - 1 - i; } tempArray[randomIndex] = tempArray[range - 1 - i]; } this.generatedNumbers = JSON.stringify(Array.from(generatedNumbers)); }3. 剪贴板功能为了方便用户使用,程序提供了将生成的随机数复制到剪贴板的功能。具体实现如下:private copyToClipboard(text: string): void { const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); const systemPasteboard = pasteboard.getSystemPasteboard(); systemPasteboard.setData(pasteboardData); promptAction.showToast({ message: '已复制' }); }完整代码// 导入剪贴板服务模块,用于后续实现复制功能 import { pasteboard } from '@kit.BasicServicesKit'; // 导入用于显示提示信息的服务 import { promptAction } from '@kit.ArkUI'; // 使用装饰器定义一个入口组件,这是应用的主界面 @Entry @Component struct RandomNumberGenerator { // 定义基础间距,用于布局中的间距设置 @State private baseSpacing: number = 30; // 存储生成的随机数字符串 @State private generatedNumbers: string = ''; // 应用的主题色 @State private primaryColor: string = '#fea024'; // 文本的颜色 @State private fontColor: string = "#2e2e2e"; // 输入框是否获取焦点的状态变量 @State private isFocusStart: boolean = false; @State private isFocusEnd: boolean = false; @State private isFocusCount: boolean = false; // 是否允许生成的随机数重复 @State private isUnique: boolean = true; // 随机数生成的起始值 @State private startValue: number = 0; // 随机数生成的结束值 @State private endValue: number = 0; // 要生成的随机数个数 @State private countValue: number = 0; // 生成随机数的方法 private generateRandomNumbers(): void { const startValue = this.startValue; // 获取当前设定的起始值 const endValue = this.endValue; // 获取当前设定的结束值 const countValue = this.countValue; // 获取当前设定的生成个数 const range: number = endValue - startValue + 1; // 计算生成范围 // 用于存储生成的随机数 const generatedNumbers = new Set<number>(); // 使用Set来自动去重 const tempArray: number[] = []; // 临时数组,用于辅助生成不重复的随机数 // 如果不允许重复,则使用去重算法生成随机数 if (!this.isUnique) { // 如果请求的随机数数量超过了范围内的总数,则显示错误提示 if (countValue > range) { this.getUIContext().showAlertDialog({ title: '错误提示', message: `请求的随机数数量超过了范围内的总数`, confirm: { defaultFocus: true, value: '我知道了', fontColor: Color.White, backgroundColor: this.primaryColor, action: () => {} // 点击确认后的回调 }, onWillDismiss: () => {}, // 对话框即将关闭时的回调 alignment: DialogAlignment.Center, // 对话框的对齐方式 }); return; } for (let i = 0; i < countValue; i++) { let randomIndex = Math.floor(Math.random() * (range - i)); // 在剩余范围内选择一个随机索引 let randomNum = 0; if (tempArray[randomIndex] !== undefined) { // 如果索引位置已有值,则使用该值 randomNum = tempArray[randomIndex]; } else { randomNum = startValue + randomIndex; // 否则计算新的随机数 } generatedNumbers.add(randomNum); // 添加到Set中,自动去重 if (tempArray[range - 1 - i] === undefined) { // 更新末尾元素的位置 tempArray[range - 1 - i] = startValue + range - 1 - i; } tempArray[randomIndex] = tempArray[range - 1 - i]; // 将末尾元素移到随机位置 } // 将生成的随机数转换成JSON格式的字符串 this.generatedNumbers = JSON.stringify(Array.from(generatedNumbers)); } else { // 如果允许重复,则直接生成随机数 for (let i = 0; i < this.countValue; i++) { let randomNumber = this.startValue + Math.floor(Math.random() * (this.endValue - this.startValue)); tempArray.push(randomNumber); } // 将生成的随机数转换成JSON格式的字符串 this.generatedNumbers = JSON.stringify(tempArray); } } // 将生成的随机数复制到剪贴板的方法 private copyToClipboard(text: string): void { const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); // 创建剪贴板数据 const systemPasteboard = pasteboard.getSystemPasteboard(); // 获取系统剪贴板 systemPasteboard.setData(pasteboardData); // 设置剪贴板数据 // 显示复制成功的提示信息 promptAction.showToast({ message: '已复制' }); } // 构建页面布局的方法 build() { Column() { // 标题栏,展示应用名 Text("随机数生成") .width('100%') // 设置宽度为100% .height(54) // 设置高度为54 .fontSize(18) // 设置字体大小为18 .fontWeight(600) // 设置字体粗细为600 .backgroundColor(Color.White) // 设置背景颜色为白色 .textAlign(TextAlign.Center) // 设置文本居中对齐 .fontColor(this.fontColor); // 设置文本颜色 // 随机数范围设置区域 Column() { Row() { Text(`随机数范围`) .fontWeight(600) // 设置字体粗细为600 .fontSize(18) // 设置字体大小为18 .fontColor(this.fontColor); // 设置文本颜色 } .margin({ top: `${this.baseSpacing}lpx`, left: `${this.baseSpacing}lpx` }); // 设置边距 // 输入随机数范围的两个值 Row() { TextInput({ placeholder: '开始(>=)' }) // 输入框,显示占位符 .layoutWeight(1) // 设置布局权重为1 .type(InputType.Number) // 设置输入类型为数字 .placeholderColor(this.isFocusStart ? this.primaryColor : Color.Gray) // 设置占位符颜色 .fontColor(this.isFocusStart ? this.primaryColor : this.fontColor) // 设置文本颜色 .borderColor(this.isFocusStart ? this.primaryColor : Color.Gray) // 设置边框颜色 .borderWidth(1) // 设置边框宽度 .borderRadius(10) // 设置圆角半径 .backgroundColor(Color.White) // 设置背景颜色 .showUnderline(false) // 不显示下划线 .onBlur(() => this.isFocusStart = false) // 输入框失去焦点时的处理 .onFocus(() => this.isFocusStart = true) // 输入框获得焦点时的处理 .onChange((value: string) => this.startValue = Number(value)); // 输入值变化时的处理 // 分隔符 Line().width(10) // 设置分隔符宽度 TextInput({ placeholder: '结束(<=)' }) // 输入框,显示占位符 .layoutWeight(1) // 设置布局权重为1 .type(InputType.Number) // 设置输入类型为数字 .placeholderColor(this.isFocusEnd ? this.primaryColor : Color.Gray) // 设置占位符颜色 .fontColor(this.isFocusEnd ? this.primaryColor : this.fontColor) // 设置文本颜色 .borderColor(this.isFocusEnd ? this.primaryColor : Color.Gray) // 设置边框颜色 .borderWidth(1) // 设置边框宽度 .borderRadius(10) // 设置圆角半径 .backgroundColor(Color.White) // 设置背景颜色 .showUnderline(false) // 不显示下划线 .onBlur(() => this.isFocusEnd = false) // 输入框失去焦点时的处理 .onFocus(() => this.isFocusEnd = true) // 输入框获得焦点时的处理 .onChange((value: string) => this.endValue = Number(value)); // 输入值变化时的处理 } .margin({ left: `${this.baseSpacing}lpx`, // 左边距 right: `${this.baseSpacing}lpx`, // 右边距 top: `${this.baseSpacing}lpx`, // 上边距 }); // 输入生成随机数的个数 Text('生成随机数个数') .fontWeight(600) // 设置字体粗细为600 .fontSize(18) // 设置字体大小为18 .fontColor(this.fontColor) // 设置文本颜色 .margin({ left: `${this.baseSpacing}lpx`, top: `${this.baseSpacing}lpx` }); // 设置边距 Row() { TextInput({ placeholder: '' }) // 输入框,显示占位符 .layoutWeight(1) // 设置布局权重为1 .type(InputType.Number) // 设置输入类型为数字 .placeholderColor(this.isFocusCount ? this.primaryColor : Color.Gray) // 设置占位符颜色 .fontColor(this.isFocusCount ? this.primaryColor : this.fontColor) // 设置文本颜色 .borderColor(this.isFocusCount ? this.primaryColor : Color.Gray) // 设置边框颜色 .borderWidth(1) // 设置边框宽度 .borderRadius(10) // 设置圆角半径 .backgroundColor(Color.White) // 设置背景颜色 .showUnderline(false) // 不显示下划线 .onBlur(() => this.isFocusCount = false) // 输入框失去焦点时的处理 .onFocus(() => this.isFocusCount = true) // 输入框获得焦点时的处理 .onChange((value: string) => this.countValue = Number(value)); // 输入值变化时的处理 } .margin({ left: `${this.baseSpacing}lpx`, // 左边距 right: `${this.baseSpacing}lpx`, // 右边距 top: `${this.baseSpacing}lpx`, // 上边距 }); // 设置数字是否可重复的开关 Row() { Text('数字是否可重复') .fontWeight(400) // 设置字体粗细为400 .fontSize(16) // 设置字体大小为16 .fontColor(this.fontColor) // 设置文本颜色 .layoutWeight(1); // 设置布局权重为1 Toggle({ type: ToggleType.Checkbox, isOn: this.isUnique }) // 切换按钮 .width('100lpx') // 设置宽度 .height('50lpx') // 设置高度 .borderColor(Color.Gray) // 设置边框颜色 .selectedColor(this.primaryColor) // 设置选中时的颜色 .onChange((isOn: boolean) => this.isUnique = isOn) // 切换状态变化时的处理 .align(Alignment.End); // 设置对齐方式为右对齐 } .margin({ top: `${this.baseSpacing}lpx`, // 上边距 }) .width('100%') // 设置宽度为100% .padding({ left: `${this.baseSpacing}lpx`, // 左内边距 right: `${this.baseSpacing}lpx`, // 右内边距 top: `${this.baseSpacing / 3}lpx`, // 上内边距 }) .hitTestBehavior(HitTestMode.Block) // 设置点击测试行为 .onClick(() => this.isUnique = !this.isUnique); // 点击时切换状态 // 生成随机数的按钮 Text('开始生成') .fontColor(Color.White) // 设置文本颜色为白色 .backgroundColor(this.primaryColor) // 设置背景颜色为主题色 .height(54) // 设置高度为54 .textAlign(TextAlign.Center) // 设置文本居中对齐 .borderRadius(10) // 设置圆角半径 .fontSize(18) // 设置字体大小为18 .width(`${650 - this.baseSpacing * 2}lpx`) // 设置宽度 .margin({ top: `${this.baseSpacing}lpx`, // 上边距 left: `${this.baseSpacing}lpx`, // 左边距 right: `${this.baseSpacing}lpx`, // 右边距 bottom: `${this.baseSpacing}lpx` // 下边距 }) .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 }) // 设置点击效果 .onClick(() => this.generateRandomNumbers()); // 点击时生成随机数 } .width('650lpx') // 设置宽度 .margin({ top: 20 }) // 设置上边距 .backgroundColor(Color.White) // 设置背景颜色为白色 .borderRadius(10) // 设置圆角半径 .alignItems(HorizontalAlign.Start); // 设置水平对齐方式为左对齐 // 显示生成的随机数 Column() { Text(`生成的随机数为:`) .fontWeight(600) // 设置字体粗细为600 .fontSize(18) // 设置字体大小为18 .fontColor(this.fontColor) // 设置文本颜色 .margin({ top: `${this.baseSpacing}lpx`, // 上边距 left: `${this.baseSpacing}lpx`, // 左边距 }); Text(`${this.generatedNumbers}`) // 显示生成的随机数 .width('650lpx') // 设置宽度 .fontColor(this.primaryColor) // 设置文本颜色为主题色 .fontSize(18) // 设置字体大小为18 .textAlign(TextAlign.Center) // 设置文本居中对齐 .padding({ left: 5, right: 5 }) // 设置内边距 .margin({ top: `${this.baseSpacing / 3}lpx` // 上边距 }); // 复制生成的随机数到剪贴板的按钮 Text('复制') .enabled(this.generatedNumbers ? true : false) // 按钮是否可用 .fontColor(Color.White) // 设置文本颜色为白色 .backgroundColor(this.primaryColor) // 设置背景颜色为主题色 .height(54) // 设置高度为54 .textAlign(TextAlign.Center) // 设置文本居中对齐 .borderRadius(10) // 设置圆角半径 .fontSize(18) // 设置字体大小为18 .width(`${650 - this.baseSpacing * 2}lpx`) // 设置宽度 .margin({ top: `${this.baseSpacing}lpx`, // 上边距 left: `${this.baseSpacing}lpx`, // 左边距 right: `${this.baseSpacing}lpx`, // 右边距 bottom: `${this.baseSpacing}lpx` // 下边距 }) .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 }) // 设置点击效果 .onClick(() => this.copyToClipboard(this.generatedNumbers)); // 点击时复制随机数 } .width('650lpx') // 设置宽度 .backgroundColor(Color.White) // 设置背景颜色为白色 .borderRadius(10) // 设置圆角半径 .margin({ top: `${this.baseSpacing}lpx` }) // 设置上边距 .alignItems(HorizontalAlign.Start); // 设置水平对齐方式为左对齐 } .height('100%') // 设置高度为100% .width('100%') // 设置宽度为100% .backgroundColor("#f2f3f5"); // 设置背景颜色 } }转载自https://www.cnblogs.com/zhongcx/p/18556821
  • [技术干货] 鸿蒙NEXT开发案例:简体繁体转换器
     【引言】简体繁体转换器是一个实用的小工具,它可以帮助用户轻松地在简体中文和繁体中文之间进行转换。对于需要频繁处理两岸三地文档的用户来说,这样的工具无疑是提高工作效率的好帮手。本案例将展示如何利用鸿蒙NEXT提供的组件和服务,结合第三方库@nutpi/chinese_transverter,来实现这一功能。【环境准备】• 操作系统:Windows 10• 开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806• 目标设备:华为Mate60 Pro• 开发语言:ArkTS• 框架:ArkUI• API版本:API 12• 三方库:chinese_transverter【实现步骤】1. 项目初始化首先,确保你的开发环境已经安装了鸿蒙NEXT的相关工具链。然后,创建一个新的鸿蒙NEXT项目。2. 引入第三方库使用ohpm命令安装@nutpi/chinese_transverter库:1ohpm install @nutpi/chinese_transverter3. 编写核心逻辑接下来,在项目的主组件中引入所需的模块,并定义好状态变量和方法。这里的关键在于设置监听器以响应输入文本的变化,并调用转换函数来获取转换结果。import { transverter, TransverterType, TransverterLanguage } from "@nutpi/chinese_transverter"; @Entry @Component struct SimplifiedTraditionalConverter { @State @Watch('onInputTextChanged') inputText: string = ''; @State simplifiedResult: string = ''; @State traditionalResult: string = ''; @State isInputFocused: boolean = false; onInputTextChanged() { this.simplifiedResult = transverter({ type: TransverterType.SIMPLIFIED, str: this.inputText, language: TransverterLanguage.ZH_CN }); this.traditionalResult = transverter({ type: TransverterType.TRADITIONAL, str: this.inputText, language: TransverterLanguage.ZH_CN }); } private copyToClipboard(text: string): void { const clipboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); const systemClipboard = pasteboard.getSystemPasteboard(); systemClipboard.setData(clipboardData); promptAction.showToast({ message: '已复制到剪贴板' }); } build() { // UI构建代码... } }4. 构建用户界面在build方法中,我们构建了应用的用户界面。这里主要包括一个可滚动的容器、输入区域、结果展示区以及操作按钮。Scroll() { Column() { Text("简体繁体转换器") .width('100%') .height(54) .fontSize(18) .fontWeight(600) .backgroundColor(Color.White) .textAlign(TextAlign.Center) .fontColor(this.textColor); // 输入区域与清空按钮 Column() { // ...省略部分代码... Text('清空') .borderWidth(1) .borderColor(this.themeColor) .fontColor(this.themeColor) .height(50) .textAlign(TextAlign.Center) .borderRadius(10) .fontSize(18) .width(`${650 - this.basePadding * 2}lpx`) .margin({ top: `${this.basePadding}lpx` }) .onClick(() => this.inputText = ""); } .width('650lpx') .padding(`${this.basePadding}lpx`) .margin({ top: 20 }) .backgroundColor(Color.White) .borderRadius(10) .alignItems(HorizontalAlign.Start); // 结果展示区 // ...省略部分代码... } .width('100%') .height('100%') .backgroundColor("#f2f3f5") .align(Alignment.Top) .padding({ bottom: `${this.basePadding}lpx` }); } 完整代码// 导入必要的转换库,提供简体与繁体之间的转换功能 import { transverter, TransverterType, TransverterLanguage } from "@nutpi/chinese_transverter"; // 导入剪贴板服务,用于将文本复制到系统剪贴板 import { pasteboard } from '@kit.BasicServicesKit'; // 导入提示服务,用于向用户显示消息 import { promptAction } from '@kit.ArkUI'; // 使用@Entry装饰器标记此组件为应用的入口点 @Entry // 使用@Component装饰器定义一个名为SimplifiedTraditionalConverter的组件 @Component struct SimplifiedTraditionalConverter { // 定义状态变量inputText,存储用户输入的原始文本,当其值变化时触发onInputTextChanged方法 @State @Watch('onInputTextChanged') inputText: string = ''; // 定义状态变量simplifiedResult,存储转换后的简体结果 @State simplifiedResult: string = ''; // 定义状态变量traditionalResult,存储转换后的繁体结果 @State traditionalResult: string = ''; // 定义状态变量isInputFocused,表示输入框是否获得了焦点 @State isInputFocused: boolean = false; // 定义主题颜色 @State private themeColor: string = '#439fff'; // 定义文本颜色 @State private textColor: string = "#2e2e2e"; // 定义基础内边距大小 @State private basePadding: number = 30; // 定义最小文本区域高度 @State private minTextAreaHeight: number = 50; // 定义最大文本区域高度 @State private maxTextAreaHeight: number = 300; // 当inputText状态改变时触发的方法,用于更新转换结果 onInputTextChanged() { // 将inputText转换为简体,并更新simplifiedResult this.simplifiedResult = transverter({ type: TransverterType.SIMPLIFIED, str: this.inputText, language: TransverterLanguage.ZH_CN }); // 将inputText转换为繁体,并更新traditionalResult this.traditionalResult = transverter({ type: TransverterType.TRADITIONAL, str: this.inputText, language: TransverterLanguage.ZH_CN }); } // 将给定的文本复制到剪贴板,并显示提示信息 private copyToClipboard(text: string): void { const clipboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); // 创建剪贴板数据 const systemClipboard = pasteboard.getSystemPasteboard(); // 获取系统剪贴板 systemClipboard.setData(clipboardData); // 设置剪贴板数据 promptAction.showToast({ message: '已复制到剪贴板' }); // 显示复制成功的提示 } // 构建组件的UI build() { Scroll() { // 创建可滚动的容器 Column() { // 在滚动容器中创建垂直布局 // 创建标题文本 Text("简体繁体转换器") .width('100%') .height(54) .fontSize(18) .fontWeight(600) .backgroundColor(Color.White) .textAlign(TextAlign.Center) .fontColor(this.textColor); // 创建用户输入区域 Column() { // 创建多行文本输入框 TextArea({ text: $$this.inputText, placeholder: '请输入简体/繁体字(支持混合输入)' }) .fontSize(18) .placeholderColor(this.isInputFocused ? this.themeColor : Color.Gray) .fontColor(this.isInputFocused ? this.themeColor : this.textColor) .borderColor(this.isInputFocused ? this.themeColor : Color.Gray) .caretColor(this.themeColor) .onBlur(() => this.isInputFocused = false) // 当输入框失去焦点时,更新isInputFocused状态 .onFocus(() => this.isInputFocused = true) // 当输入框获得焦点时,更新isInputFocused状态 .borderWidth(1) .borderRadius(10) .backgroundColor(Color.White) .constraintSize({ minHeight: this.minTextAreaHeight, maxHeight: this.maxTextAreaHeight }); // 创建清空按钮 Text('清空') .borderWidth(1) .borderColor(this.themeColor) .fontColor(this.themeColor) .height(50) .textAlign(TextAlign.Center) .borderRadius(10) .fontSize(18) .width(`${650 - this.basePadding * 2}lpx`) .margin({ top: `${this.basePadding}lpx` }) .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 }) // 添加点击效果 .onClick(() => this.inputText = ""); // 清空输入框 } .width('650lpx') .padding(`${this.basePadding}lpx`) .margin({ top: 20 }) .backgroundColor(Color.White) .borderRadius(10) .alignItems(HorizontalAlign.Start); // 创建繁体结果展示与复制区域 Column() { // 创建繁体结果标题 Text(`繁体结果:`) .fontWeight(600) .fontSize(18) .fontColor(this.textColor); // 创建繁体结果展示文本 Text(`${this.traditionalResult}`) .constraintSize({ minHeight: this.minTextAreaHeight, maxHeight: this.maxTextAreaHeight }) .fontColor(this.themeColor) .fontSize(18) .textAlign(TextAlign.Start) .copyOption(CopyOptions.InApp) .margin({ top: `${this.basePadding / 3}lpx` }); // 创建复制繁体结果按钮 Text('复制') .enabled(this.traditionalResult ? true : false) // 只有当有繁体结果时,按钮才可用 .fontColor(Color.White) .backgroundColor(this.themeColor) .height(50) .textAlign(TextAlign.Center) .borderRadius(10) .fontSize(18) .width(`${650 - this.basePadding * 2}lpx`) .margin({ top: `${this.basePadding}lpx` }) .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 }) .onClick(() => this.copyToClipboard(this.traditionalResult)); // 复制繁体结果到剪贴板 } .width('650lpx') .padding(`${this.basePadding}lpx`) .backgroundColor(Color.White) .borderRadius(10) .margin({ top: `${this.basePadding}lpx` }) .alignItems(HorizontalAlign.Start); // 创建简体结果展示与复制区域 Column() { // 创建简体结果标题 Text(`简体结果:`) .fontWeight(600) .fontSize(18) .fontColor(this.textColor); // 创建简体结果展示文本 Text(`${this.simplifiedResult}`) .constraintSize({ minHeight: this.minTextAreaHeight, maxHeight: this.maxTextAreaHeight }) .fontColor(this.themeColor) .fontSize(18) .textAlign(TextAlign.Start) .copyOption(CopyOptions.InApp) .margin({ top: `${this.basePadding / 3}lpx` }); // 创建复制简体结果按钮 Text('复制') .enabled(this.simplifiedResult ? true : false) // 只有当有简体结果时,按钮才可用 .fontColor(Color.White) .backgroundColor(this.themeColor) .height(50) .textAlign(TextAlign.Center) .borderRadius(10) .fontSize(18) .width(`${650 - this.basePadding * 2}lpx`) .margin({ top: `${this.basePadding}lpx` }) .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 }) .onClick(() => this.copyToClipboard(this.simplifiedResult)); // 复制简体结果到剪贴板 } .width('650lpx') .padding(`${this.basePadding}lpx`) .backgroundColor(Color.White) .borderRadius(10) .margin({ top: `${this.basePadding}lpx` }) .alignItems(HorizontalAlign.Start); } } .width('100%') .height('100%') .backgroundColor("#f2f3f5") .align(Alignment.Top) .padding({ bottom: `${this.basePadding}lpx` }); } }转载自https://www.cnblogs.com/zhongcx/p/18559952 
  • [技术干货] 鸿蒙NEXT开发案例:血型遗传计算
     【引言】血型遗传计算器是一个帮助用户根据父母的血型预测子女可能的血型的应用。通过选择父母的血型,应用程序能够快速计算出孩子可能拥有的血型以及不可能拥有的血型。这个过程不仅涉及到了简单的数据处理逻辑,还涉及到UI设计与交互体验的设计。【环境准备】• 操作系统:Windows 10• 开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806• 目标设备:华为Mate60 Pro• 开发语言:ArkTS• 框架:ArkUI• API版本:API 12【开发步骤】1. 创建组件首先,我们使用@Entry和@Component装饰器创建一个名为BloodTypeCalculator的组件。这标志着我们的组件是鸿蒙NEXT应用的一个入口点。12345@Entry@Componentstruct BloodTypeCalculator {  // 组件的状态和方法定义}2. 定义状态为了控制组件的外观和行为,我们定义了一系列的状态变量,如主题颜色、文本颜色、边框颜色等。同时,我们也定义了两个数组来存储可能和不可能的血型结果。12345@State private themeColor: string | Color = Color.Orange;@State private textColor: string = "#2e2e2e";@State private lineColor: string = "#d5d5d5";@State private possibleBloodTypesText: string = "";@State private impossibleBloodTypesText: string = "";3. 血型逻辑实现接下来,实现了几个关键的方法来处理血型相关的逻辑。这些方法包括根据血型获取基因组合、组合父母的基因以获得子代可能的基因组合、根据基因组合确定血型等。123getGenes(bloodType: string): string[];combineGenes(fatherGenes: string[], motherGenes: string[]): string[];getBloodTypesFromGenes(genes: string[]): string[];4. 交互逻辑为了实现用户选择父母血型后自动计算子代血型的功能,我们使用了@Watch装饰器监听选择的变化,并在变化时调用计算方法更新结果显示。123456@State @Watch('capsuleSelectedIndexesChanged') fatherBloodTypeIndex: number[] = [0];@State @Watch('capsuleSelectedIndexesChanged') motherBloodTypeIndex: number[] = [0]; capsuleSelectedIndexesChanged() {  // 更新血型信息}5. UI设计最后,我们构建了用户界面,包括页面标题、工具介绍、父母血型选择区域以及结果显示区域。这里使用了Column、Row、Text和SegmentButton等组件来布局和美化界面。12345678910build() {  Column() {    // 页面标题    Text('血型遗传计算');    // 其他UI元素...  }  .height('100%')  .width('100%')  .backgroundColor("#f4f8fb");}总结通过上述步骤,我们成功地开发了一个基于鸿蒙NEXT的血型遗传计算器。这个案例不仅展示了鸿蒙NEXT框架下组件化开发的基本流程,同时也体现了通过合理的状态管理和逻辑处理,可以轻松实现复杂的业务需求。对于希望深入了解鸿蒙NEXT框架的开发者来说,这是一个很好的实践案例。希望这篇文章能为你提供灵感,鼓励你在鸿蒙NEXT的开发道路上继续前行。【完整代码】// 导入SegmentButton及其相关类型定义 import { SegmentButton, SegmentButtonItemTuple, SegmentButtonOptions } from '@kit.ArkUI'; // 使用@Entry装饰器标记此组件为入口点 @Entry // 使用@Component装饰器标记此结构体为一个组件 @Component // 定义一个名为BloodTypeCalculator的结构体,用于实现血型遗传计算功能 struct BloodTypeCalculator { // 定义主题颜色,默认为橙色 @State private themeColor: string | Color = Color.Orange; // 定义文本颜色,默认为深灰色 @State private textColor: string = "#2e2e2e"; // 定义边框颜色,默认为浅灰色 @State private lineColor: string = "#d5d5d5"; // 定义基础内边距大小,默认为30 @State private basePadding: number = 30; // 存储可能的血型结果 @State private possibleBloodTypesText: string = ""; // 存储不可能的血型结果 @State private impossibleBloodTypesText: string = ""; // 定义血型列表,包含A、B、AB、O四种血型 @State private bloodTypeList: object[] = [Object({ text: 'A' }), Object({ text: 'B' }), Object({ text: 'AB' }), Object({ text: 'O' })]; // 初始化单选胶囊按钮的配置项 @State singleSelectCapsuleOptions: SegmentButtonOptions | undefined = undefined; // 监听父亲血型选择变化 @State @Watch('capsuleSelectedIndexesChanged') fatherBloodTypeIndex: number[] = [0]; // 监听母亲血型选择变化 @State @Watch('capsuleSelectedIndexesChanged') motherBloodTypeIndex: number[] = [0]; // 根据血型获取其可能的基因组合 getGenes(bloodType: string): string[] { console.info(`bloodType:${bloodType}`); switch (bloodType) { case 'A': return ['A', 'O']; // A型血可能的基因组合 case 'B': return ['B', 'O']; // B型血可能的基因组合 case 'AB': return ['A', 'B']; // AB型血可能的基因组合 case 'O': return ['O']; // O型血可能的基因组合 default: throw new Error('Invalid blood type'); // 非法血型抛出错误 } } // 组合父母的基因以获得子代可能的基因组合 combineGenes(fatherGenes: string[], motherGenes: string[]): string[] { const possibleGenes: string[] = []; // 用于存储可能的基因组合 for (const fatherGene of fatherGenes) { for (const motherGene of motherGenes) { const combinedGene = [fatherGene, motherGene].sort().join(''); // 将父母的基因组合并排序后加入数组 if (!possibleGenes.includes(combinedGene)) { possibleGenes.push(combinedGene); // 如果组合尚未存在,则加入数组 } } } return possibleGenes; // 返回所有可能的基因组合 } // 根据基因组合确定血型 getBloodTypesFromGenes(genes: string[]): string[] { const bloodTypes: string[] = []; // 用于存储可能的血型 for (const gene of genes) { if (gene === 'AA' || gene === 'AO' || gene === 'OA') { bloodTypes.push('A'); // 基因组合为AA、AO或OA时,血型为A } else if (gene === 'BB' || gene === 'BO' || gene === 'OB') { bloodTypes.push('B'); // 基因组合为BB、BO或OB时,血型为B } else if (gene === 'AB' || gene === 'BA') { bloodTypes.push('AB'); // 基因组合为AB或BA时,血型为AB } else if (gene === 'OO') { bloodTypes.push('O'); // 基因组合为OO时,血型为O } } // 去除重复的血型 return bloodTypes.filter((value, index, self) => self.indexOf(value) === index); } // 计算孩子可能的血型及不可能的血型 calculatePossibleBloodTypes(father: string, mother: string) { const fatherGenes = this.getGenes(father); // 获取父亲的基因组合 const motherGenes = this.getGenes(mother); // 获取母亲的基因组合 const possibleGenes = this.combineGenes(fatherGenes, motherGenes); // 组合父母的基因 const possibleBloodTypes = this.getBloodTypesFromGenes(possibleGenes); // 从基因组合中获取可能的血型 const allBloodTypes: string[] = ['A', 'B', 'AB', 'O']; // 所有可能的血型列表 const impossibleBloodTypes = allBloodTypes.filter(bt => !possibleBloodTypes.includes(bt)); // 计算不可能的血型 console.log(this.possibleBloodTypesText = `孩子可能血型:${possibleBloodTypes.join('、')}`); // 显示可能的血型 console.log(this.impossibleBloodTypesText = `孩子不可能血型:${impossibleBloodTypes.join('、')}`); // 显示不可能的血型 } // 当胶囊按钮的选择发生变化时调用此函数 capsuleSelectedIndexesChanged() { let father: string = this.bloodTypeList[this.fatherBloodTypeIndex[0]]['text']; // 获取父亲选择的血型 let mother: string = this.bloodTypeList[this.motherBloodTypeIndex[0]]['text']; // 获取母亲选择的血型 this.calculatePossibleBloodTypes(father, mother); // 计算并更新血型信息 } // 在组件即将出现时调用此函数 aboutToAppear(): void { this.singleSelectCapsuleOptions = SegmentButtonOptions.capsule({ buttons: this.bloodTypeList as SegmentButtonItemTuple, // 设置胶囊按钮的选项 multiply: false, // 单选模式 fontColor: Color.White, // 字体颜色为白色 selectedFontColor: Color.White, // 选中时字体颜色为白色 selectedBackgroundColor: this.themeColor, // 选中背景色为主题色 backgroundColor: this.lineColor, // 背景色为边框颜色 backgroundBlurStyle: BlurStyle.BACKGROUND_THICK // 背景模糊效果 }); this.capsuleSelectedIndexesChanged(); // 初始化时调用选择变化处理函数 } // 构建用户界面 build() { Column() { // 页面标题 Text('血型遗传计算') .fontColor(this.textColor) // 文本颜色 .fontSize(18) // 字体大小 .width('100%') // 宽度为100% .height(50) // 高度为50 .textAlign(TextAlign.Center) // 文本居中对齐 .backgroundColor(Color.White) // 背景色为白色 .shadow({ // 添加阴影效果 radius: 2, // 阴影半径 color: this.lineColor, // 阴影颜色 offsetX: 0, // 水平偏移量 offsetY: 5 // 垂直偏移量 }); // 工具介绍部分 Column() { Text('工具介绍').fontSize(20).fontWeight(600).fontColor(this.textColor); Text('血型是以A、B、O三种遗传因子的组合而决定的,根据父母的血型,就可以判断出以后出生的孩子的血型。') .fontSize(18).fontColor(this.textColor).margin({ top: `${this.basePadding / 2}lpx` }); } .alignItems(HorizontalAlign.Start) .width('650lpx') .padding(`${this.basePadding}lpx`) .margin({ top: `${this.basePadding}lpx` }) .borderRadius(10) .backgroundColor(Color.White) .shadow({ radius: 10, color: this.lineColor, offsetX: 0, offsetY: 0 }); // 父亲血型选择部分 Column() { Row() { Text('父亲血型').fontColor(this.textColor).fontSize(18); SegmentButton({ options: this.singleSelectCapsuleOptions, // 胶囊按钮的配置项 selectedIndexes: this.fatherBloodTypeIndex // 当前选中的索引 }).width('400lpx'); }.height(45).justifyContent(FlexAlign.SpaceBetween).width('100%'); // 母亲血型选择部分 Row() { Text('母亲血型').fontColor(this.textColor).fontSize(18); SegmentButton({ options: this.singleSelectCapsuleOptions, // 胶囊按钮的配置项 selectedIndexes: this.motherBloodTypeIndex // 当前选中的索引 }).width('400lpx'); }.height(45).justifyContent(FlexAlign.SpaceBetween).width('100%'); } .alignItems(HorizontalAlign.Start) .width('650lpx') .padding(`${this.basePadding}lpx`) .margin({ top: `${this.basePadding}lpx` }) .borderRadius(10) .backgroundColor(Color.White) .shadow({ radius: 10, color: this.lineColor, offsetX: 0, offsetY: 0 }); // 显示计算结果 Column() { Row() { Text(this.possibleBloodTypesText).fontColor(this.textColor).fontSize(18); }.height(45).justifyContent(FlexAlign.SpaceBetween).width('100%'); Row() { Text(this.impossibleBloodTypesText).fontColor(this.textColor).fontSize(18); }.height(45).justifyContent(FlexAlign.SpaceBetween).width('100%'); } .alignItems(HorizontalAlign.Start) .width('650lpx') .padding(`${this.basePadding}lpx`) .margin({ top: `${this.basePadding}lpx` }) .borderRadius(10) .backgroundColor(Color.White) .shadow({ radius: 10, color: this.lineColor, offsetX: 0, offsetY: 0 }); } .height('100%') .width('100%') .backgroundColor("#f4f8fb"); // 页面背景色 } } 转载自https://www.cnblogs.com/zhongcx/p/18561407
  • [技术干货] 鸿蒙NEXT开发案例:数字转中文大小写
        【引言】本应用的主要功能是将用户输入的数字转换为中文的小写、大写及大写金额形式。用户可以在输入框中输入任意数字,点击“示例”按钮可以快速填充预设的数字,点击“清空”按钮则会清除当前输入。转换结果显示在下方的结果区域,每个结果旁边都有一个“复制”按钮,方便用户将结果复制到剪贴板。【环境准备】• 操作系统:Windows 10• 开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806• 目标设备:华为Mate60 Pro• 开发语言:ArkTS• 框架:ArkUI• API版本:API 12• 三方库:chinese-number-format(数字转中文)、chinese-finance-number(将数字转换成财务用的中文大写数字)12ohpm install @nutpi/chinese-finance-numberohpm install @nutpi/chinese-number-format【功能实现】• 输入监听:通过 @Watch 装饰器监听输入框的变化,一旦输入发生变化,即调用 inputChanged 方法更新转换结果。• 转换逻辑:利用 @nutpi/chinese-number-format 和 @nutpi/chinese-finance-number 库提供的方法完成数字到中文的各种转换。• 复制功能:使用 pasteboard 模块将结果显示的中文文本复制到剪贴板,通过 promptAction.showToast 提示用户复制成功。【完整代码】// 导入必要的模块 import { promptAction } from '@kit.ArkUI'; // 用于显示提示信息 import { pasteboard } from '@kit.BasicServicesKit'; // 用于处理剪贴板操作 import { toChineseNumber } from '@nutpi/chinese-finance-number'; // 将数字转换为中文大写金额 import { toChineseWithUnits, // 将数字转换为带单位的中文 toUpperCase, // 将中文小写转换为大写 } from '@nutpi/chinese-number-format'; @Entry // 标记此组件为入口点 @Component // 定义一个组件 struct NumberToChineseConverter { @State private exampleNumber: number = 88.8; // 示例数字 @State private textColor: string = "#2e2e2e"; // 文本颜色 @State private lineColor: string = "#d5d5d5"; // 分割线颜色 @State private basePadding: number = 30; // 基础内边距 @State private chineseLowercase: string = ""; // 转换后的小写中文 @State private chineseUppercase: string = ""; // 转换后的中文大写 @State private chineseUppercaseAmount: string = ""; // 转换后的中文大写金额 @State @Watch('inputChanged') private inputText: string = ""; // 监听输入文本变化 // 当输入文本改变时触发的方法 inputChanged() { this.chineseLowercase = toChineseWithUnits(Number(this.inputText), 'zh-CN'); // 转换为小写中文并带上单位 this.chineseUppercase = toUpperCase(this.chineseLowercase, 'zh-CN'); // 将小写中文转换为大写 this.chineseUppercaseAmount = toChineseNumber(Number(this.inputText)); // 转换为大写金额 } // 复制文本到剪贴板的方法 private copyToClipboard(text: string): void { const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); // 创建剪贴板数据 const systemPasteboard = pasteboard.getSystemPasteboard(); // 获取系统剪贴板 systemPasteboard.setData(pasteboardData); // 设置剪贴板数据 promptAction.showToast({ message: '已复制' }); // 显示复制成功的提示 } // 构建用户界面的方法 build() { Column() { // 主列容器 // 页面标题 Text('数字转中文大小写') .fontColor(this.textColor) // 设置字体颜色 .fontSize(18) // 设置字体大小 .width('100%') // 设置宽度 .height(50) // 设置高度 .textAlign(TextAlign.Center) // 文本居中对齐 .backgroundColor(Color.White) // 设置背景颜色 .shadow({ // 添加阴影效果 radius: 2, // 阴影半径 color: this.lineColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 5 // Y轴偏移量 }); Scroll() { // 滚动视图 Column() { // 内部列容器 // 工具介绍部分 Column() { Text('工具介绍').fontSize(20).fontWeight(600).fontColor(this.textColor); // 设置介绍文字样式 Text('将数字转换为中文格式,适用于票据填写、合同文书、财务报表等多种场景。支持从最小单位“分”到最大单位“千兆”的数字转换。') .textAlign(TextAlign.JUSTIFY) .fontSize(18).fontColor(this.textColor).margin({ top: `${this.basePadding / 2}lpx` }); // 设置介绍详情文字样式 } .alignItems(HorizontalAlign.Start) // 对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 设置内边距 .margin({ top: `${this.basePadding}lpx` }) // 设置外边距 .borderRadius(10) // 设置圆角 .backgroundColor(Color.White) // 设置背景颜色 .shadow({ // 添加阴影效果 radius: 10, // 阴影半径 color: this.lineColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 0 // Y轴偏移量 }); // 输入区 Column() { Row() { // 行容器 Text('示例') .fontColor("#5871ce") // 设置字体颜色 .fontSize(18) // 设置字体大小 .padding(`${this.basePadding / 2}lpx`) // 设置内边距 .backgroundColor("#f2f1fd") // 设置背景颜色 .borderRadius(5) // 设置圆角 .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 设置点击效果 .onClick(() => { // 点击事件 this.inputText = `${this.exampleNumber}`; // 设置输入框文本为示例数字 }); Blank(); // 占位符 Text('清空') .fontColor("#e48742") // 设置字体颜色 .fontSize(18) // 设置字体大小 .padding(`${this.basePadding / 2}lpx`) // 设置内边距 .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 设置点击效果 .backgroundColor("#ffefe6") // 设置背景颜色 .borderRadius(5) // 设置圆角 .onClick(() => { // 点击事件 this.inputText = ""; // 清空输入框 }); }.height(45) // 设置高度 .justifyContent(FlexAlign.SpaceBetween) // 子元素水平分布方式 .width('100%'); // 设置宽度 Divider().margin({ top: 5, bottom: 5 }); // 分割线 TextInput({ text: $$this.inputText, placeholder: `请输入数字,例如:${this.exampleNumber}` }) // 输入框 .width('100%') // 设置宽度 .fontSize(18) // 设置字体大小 .caretColor(this.textColor) // 设置光标颜色 .fontColor(this.textColor) // 设置字体颜色 .margin({ top: `${this.basePadding}lpx` }) // 设置外边距 .padding(0) // 设置内边距 .backgroundColor(Color.Transparent) // 设置背景颜色 .borderRadius(0) // 设置圆角 .type(InputType.NUMBER_DECIMAL); // 设置输入类型为数字 } .alignItems(HorizontalAlign.Start) // 对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 设置内边距 .margin({ top: `${this.basePadding}lpx` }) // 设置外边距 .borderRadius(10) // 设置圆角 .backgroundColor(Color.White) // 设置背景颜色 .shadow({ // 添加阴影效果 radius: 10, // 阴影半径 color: this.lineColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 0 // Y轴偏移量 }); // 结果区 Column() { Row() { Text(`小写:${this.chineseLowercase}`).fontColor(this.textColor).fontSize(18).layoutWeight(1); // 显示小写结果 Text('复制') .fontColor(Color.White) // 设置字体颜色 .fontSize(18) // 设置字体大小 .padding(`${this.basePadding / 2}lpx`) // 设置内边距 .backgroundColor("#0052d9") // 设置背景颜色 .borderRadius(5) // 设置圆角 .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 设置点击效果 .onClick(() => { // 点击事件 this.copyToClipboard(this.chineseLowercase); // 复制小写结果到剪贴板 }); }.constraintSize({ minHeight: 45 }) // 最小高度 .justifyContent(FlexAlign.SpaceBetween) // 子元素水平分布方式 .width('100%'); // 设置宽度 Divider().margin({ top: 5, bottom: 5 }); // 分割线 Row() { Text(`大写:${this.chineseUppercase}`).fontColor(this.textColor).fontSize(18).layoutWeight(1); // 显示大写结果 Text('复制') .fontColor(Color.White) // 设置字体颜色 .fontSize(18) // 设置字体大小 .padding(`${this.basePadding / 2}lpx`) // 设置内边距 .backgroundColor("#0052d9") // 设置背景颜色 .borderRadius(5) // 设置圆角 .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 设置点击效果 .onClick(() => { // 点击事件 this.copyToClipboard(this.chineseUppercase); // 复制大写结果到剪贴板 }); }.constraintSize({ minHeight: 45 }) // 最小高度 .justifyContent(FlexAlign.SpaceBetween) // 子元素水平分布方式 .width('100%'); // 设置宽度 Divider().margin({ top: 5, bottom: 5 }); // 分割线 Row() { Text(`大写金额:${this.chineseUppercaseAmount}`).fontColor(this.textColor).fontSize(18).layoutWeight(1); // 显示大写金额结果 Text('复制') .fontColor(Color.White) // 设置字体颜色 .fontSize(18) // 设置字体大小 .padding(`${this.basePadding / 2}lpx`) // 设置内边距 .backgroundColor("#0052d9") // 设置背景颜色 .borderRadius(5) // 设置圆角 .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 设置点击效果 .onClick(() => { // 点击事件 this.copyToClipboard(this.chineseUppercaseAmount); // 复制大写金额结果到剪贴板 }); }.constraintSize({ minHeight: 45 }) // 最小高度 .justifyContent(FlexAlign.SpaceBetween) // 子元素水平分布方式 .width('100%'); // 设置宽度 } .alignItems(HorizontalAlign.Start) // 对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 设置内边距 .margin({ top: `${this.basePadding}lpx` }) // 设置外边距 .borderRadius(10) // 设置圆角 .backgroundColor(Color.White) // 设置背景颜色 .shadow({ // 添加阴影效果 radius: 10, // 阴影半径 color: this.lineColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 0 // Y轴偏移量 }); } }.scrollBar(BarState.Off).clip(false); // 关闭滚动条,不允许裁剪 } .height('100%') // 设置高度 .width('100%') // 设置宽度 .backgroundColor("#f4f8fb"); // 设置页面背景颜色 } } 转载自https://www.cnblogs.com/zhongcx/p/18562512
  • [技术干货] 鸿蒙NEXT开发案例:字数统计
    【引言】本文将通过一个具体的案例——“字数统计”组件,来探讨如何在鸿蒙NEXT框架下实现这一功能。此组件不仅能够统计用户输入文本中的汉字、中文标点、数字、以及英文字符的数量,还具有良好的用户界面设计,使用户能够直观地了解输入文本的各种统计数据。【环境准备】• 操作系统:Windows 10• 开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806• 目标设备:华为Mate60 Pro• 开发语言:ArkTS• 框架:ArkUI• API版本:API 12【组件概述】“字数统计”组件基于鸿蒙NEXT框架构建,旨在提供一个简洁而强大的文本统计工具。组件的主要功能包括:• 文本输入:用户可以在提供的文本区域内输入或粘贴任何文本。• 实时统计:当用户输入或修改文本时,组件会实时更新并显示文本中汉字、中文标点、数字、英文字符等的具体数量。• 示例与清空:提供了“示例”按钮,点击后会自动填充预设的文本内容;“清空”按钮则用于清空当前的输入内容。【技术实现】1. 状态管理:使用@State装饰器来管理组件的状态,如输入文本、各种字符的数量统计等。通过@Watch装饰器监听输入文本的变化,触发相应的计算逻辑。2. 文本解析:当检测到输入文本发生变化时,组件会遍历文本中的每一个字符,根据正则表达式判断字符类型,并分别统计汉字、中文标点、数字、英文字符的数量。特别地,对于汉字和中文标点,每个字符被视为两个单位进行统计。3. 用户界面:组件的UI设计遵循了鸿蒙NEXT的设计规范,使用了Column、Row、Text、TextArea等基础组件来构建布局。通过设置字体颜色、大小、背景色、边距等属性,实现了美观且易于使用的界面。此外,组件还利用了阴影效果和圆角设计来提升视觉体验。4. 交互设计:为了增强用户体验,组件中加入了“示例”和“清空”按钮,用户可以通过简单的点击操作快速测试组件的功能或清空输入框。同时,组件支持文本输入区域的实时更新,保证了用户操作的即时反馈。【完整代码】// 定义一个组件,用于数字和文本统计 @Entry @Component struct NumberToChineseConverter { // 定义一个状态变量,存储示例数字字符串 @State private exampleNumber: string = '自从盘古破鸿蒙,开辟从兹清浊辨。\nare you ok?\n1234\n+-*/'; // 定义文本颜色的状态变量 @State private textColor: string = "#2e2e2e"; // 定义阴影边框颜色的状态变量 @State private shadowColor: string = "#d5d5d5"; // 定义基础内边距的状态变量 @State private basePadding: number = 30; // 定义汉字数量的状态变量 @State private chineseCharCount: string = "0"; // 定义中文标点数量的状态变量 @State private chinesePunctuationCount: string = "0"; // 定义汉字加中文标点总数的状态变量 @State private totalChineseCount: string = "0"; // 定义英文字符数量的状态变量 @State private englishCharCount: string = "0"; // 定义数字数量的状态变量 @State private digitCount: string = "0"; // 定义总字符数的状态变量 @State private charTotalCount: string = "0"; // 定义监听输入文本变化的状态变量 @State @Watch('inputChanged') private inputText: string = ""; // 当输入文本发生变化时调用的方法 inputChanged() { // 初始化计数器 let chineseChars = 0; // 汉字数量 let chinesePunctuation = 0; // 中文标点数量 let englishChars = 0; // 英文字符数量 let digits = 0; // 数字数量 let count = 0; // 总字符数 // 遍历输入文本的每个字符 for (let i = 0; i < this.inputText.length; i++) { let char = this.inputText.charAt(i); // 获取当前字符 count++; // 计数器加一 // 如果字符是数字,则数字计数器加一 if (/\d/.test(char)) { digits++; } // 如果字符是汉字,则汉字计数器加一,同时总字符数加二 if (/[\u4e00-\u9fa5]/.test(char)) { chineseChars++; count++; // 汉字和中文标点算两个字符,所以这里多+1 } // 如果字符是中文标点,则中文标点计数器加一,同时总字符数加二 if (/[\u3001-\u3002\uff01-\uff1a]/.test(char)) { chinesePunctuation++; count++; // 汉字和中文标点算两个字符,所以这里多+1 } // 如果字符是英文字符或英文标点,则英文字符计数器加一 if (/[a-zA-Z0-9\s!-/:-@[-`{-~]/.test(char)) { englishChars++; } } // 更新状态变量 this.chineseCharCount = `${chineseChars}`; this.chinesePunctuationCount = `${chinesePunctuation}`; this.totalChineseCount = `${chineseChars + chinesePunctuation}`; this.englishCharCount = `${englishChars}`; this.digitCount = `${digits}`; this.charTotalCount = `${count}`; } // 构建UI界面的方法 build() { // 创建一个列布局容器 Column() { // 添加标题 Text('字数统计') .fontColor(this.textColor) // 设置字体颜色 .fontSize(18) // 设置字体大小 .width('100%') // 设置宽度为100% .height(50) // 设置高度为50 .textAlign(TextAlign.Center) // 设置文本居中对齐 .backgroundColor(Color.White) // 设置背景色为白色 .shadow({ // 设置阴影效果 radius: 2, // 阴影半径 color: this.shadowColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 5 // Y轴偏移量 }); // 创建可滚动的容器 Scroll() { // 在可滚动容器内部创建列布局 Column() { // 添加工具介绍 Column() { Text('工具介绍').fontSize(18).fontWeight(600).fontColor(this.textColor); Text('本工具能够快速统计输入文本中的汉字、中文标点、数字、英文字符等的数量。具体规则如下:\n•汉字和中文标点各算作两个字符。\n•数字、空格、英文字母及英文标点各算作一个字符。') .textAlign(TextAlign.JUSTIFY) // 设置文本两端对齐 .fontSize(13) // 设置字体大小 .fontColor(this.textColor) // 设置字体颜色 .margin({ top: `${this.basePadding / 2}lpx` }); // 设置上边距 } // 设置样式 .alignItems(HorizontalAlign.Start) // 设置水平对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 设置内边距 .margin({ top: `${this.basePadding}lpx` }) // 设置外边距 .borderRadius(10) // 设置圆角 .backgroundColor(Color.White) // 设置背景色 .shadow({ // 设置阴影效果 radius: 10, // 阴影半径 color: this.shadowColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 0 // Y轴偏移量 }); // 添加示例和清空按钮 Column() { Row() { Text('示例') .fontColor("#5871ce") // 设置字体颜色 .fontSize(16) // 设置字体大小 .padding(`${this.basePadding / 2}lpx`) // 设置内边距 .backgroundColor("#f2f1fd") // 设置背景色 .borderRadius(5) // 设置圆角 .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 设置点击效果 .onClick(() => { // 设置点击事件 this.inputText = this.exampleNumber; // 将示例文本赋值给输入框 }); Blank(); // 添加空白间隔 Text('清空') .fontColor("#e48742") // 设置字体颜色 .fontSize(16) // 设置字体大小 .padding(`${this.basePadding / 2}lpx`) // 设置内边距 .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 设置点击效果 .backgroundColor("#ffefe6") // 设置背景色 .borderRadius(5) // 设置圆角 .onClick(() => { // 设置点击事件 this.inputText = ""; // 清空输入框 }); } .height(45) // 设置高度 .justifyContent(FlexAlign.SpaceBetween) // 设置子项之间间距均匀分布 .width('100%'); // 设置宽度 Divider(); // 添加分割线 // 添加文本输入区域 TextArea({ text: $$this.inputText, placeholder: `请输入内容` }) .width(`${650 - this.basePadding * 2}lpx`) // 设置宽度 .height(100) // 设置高度 .fontSize(16) // 设置字体大小 .caretColor(this.textColor) // 设置光标颜色 .fontColor(this.textColor) // 设置字体颜色 .margin({ top: `${this.basePadding}lpx` }) // 设置上边距 .padding(0) // 设置内边距 .backgroundColor(Color.Transparent) // 设置背景色 .borderRadius(0) // 设置圆角 .textAlign(TextAlign.JUSTIFY); // 设置文本两端对齐 } // 设置样式 .alignItems(HorizontalAlign.Start) // 设置水平对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 设置内边距 .margin({ top: `${this.basePadding}lpx` }) // 设置外边距 .borderRadius(10) // 设置圆角 .backgroundColor(Color.White) // 设置背景色 .shadow({ // 设置阴影效果 radius: 10, // 阴影半径 color: this.shadowColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 0 // Y轴偏移量 }); // 添加统计结果展示 Column() { // 汉字数量 Row() { Text() { Span(`汉字:`) Span(`${this.chineseCharCount} `).fontColor(Color.Red) // 汉字数量以红色显示 Span('个') } .fontColor(this.textColor) // 设置字体颜色 .fontSize(16) // 设置字体大小 .layoutWeight(1); // 设置布局权重 } .constraintSize({ minHeight: 45 }) // 设置最小高度 .justifyContent(FlexAlign.SpaceBetween) // 设置子项之间间距均匀分布 .width('100%'); // 设置宽度 Divider(); // 添加分割线 // 中文标点数量 Row() { Text() { Span(`中文标点:`) Span(`${this.chinesePunctuationCount} `).fontColor(Color.Red) // 中文标点数量以红色显示 Span('个') } .fontColor(this.textColor) // 设置字体颜色 .fontSize(16) // 设置字体大小 .layoutWeight(1); // 设置布局权重 } .constraintSize({ minHeight: 45 }) // 设置最小高度 .justifyContent(FlexAlign.SpaceBetween) // 设置子项之间间距均匀分布 .width('100%'); // 设置宽度 Divider(); // 添加分割线 // 汉字加中文标点总数 Row() { Text() { Span(`汉字+中文标点:`) Span(`${this.totalChineseCount} `).fontColor(Color.Red) // 汉字加中文标点总数以红色显示 Span('个') } .fontColor(this.textColor) // 设置字体颜色 .fontSize(16) // 设置字体大小 .layoutWeight(1); // 设置布局权重 } .constraintSize({ minHeight: 45 }) // 设置最小高度 .justifyContent(FlexAlign.SpaceBetween) // 设置子项之间间距均匀分布 .width('100%'); // 设置宽度 Divider(); // 添加分割线 // 英文字符数量 Row() { Text() { Span(`英文:`) Span(`${this.englishCharCount} `).fontColor(Color.Red) // 英文字符数量以红色显示 Span('个') Span('(含英文状态下的数字、符号、标点)').fontSize(13) // 附加说明 } .fontColor(this.textColor) // 设置字体颜色 .fontSize(16) // 设置字体大小 .layoutWeight(1); // 设置布局权重 } .constraintSize({ minHeight: 45 }) // 设置最小高度 .justifyContent(FlexAlign.SpaceBetween) // 设置子项之间间距均匀分布 .width('100%'); // 设置宽度 Divider(); // 添加分割线 // 数字数量 Row() { Text() { Span(`数字:`) Span(`${this.digitCount} `).fontColor(Color.Red) // 数字数量以红色显示 Span('个') } .fontColor(this.textColor) // 设置字体颜色 .fontSize(16) // 设置字体大小 .layoutWeight(1); // 设置布局权重 } .constraintSize({ minHeight: 45 }) // 设置最小高度 .justifyContent(FlexAlign.SpaceBetween) // 设置子项之间间距均匀分布 .width('100%'); // 设置宽度 Divider(); // 添加分割线 // 总字符数 Row() { Text() { Span(`字符总数:`) Span(`${this.charTotalCount} `).fontColor(Color.Red) // 总字符数以红色显示 Span('个字符') } .fontColor(this.textColor) // 设置字体颜色 .fontSize(16) // 设置字体大小 .layoutWeight(1); // 设置布局权重 } .constraintSize({ minHeight: 45 }) // 设置最小高度 .justifyContent(FlexAlign.SpaceBetween) // 设置子项之间间距均匀分布 .width('100%'); // 设置宽度 } // 设置样式 .alignItems(HorizontalAlign.Start) // 设置水平对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 设置内边距 .margin({ top: `${this.basePadding}lpx` }) // 设置外边距 .borderRadius(10) // 设置圆角 .backgroundColor(Color.White) // 设置背景色 .shadow({ // 设置阴影效果 radius: 10, // 阴影半径 color: this.shadowColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 0 // Y轴偏移量 }); } } .scrollBar(BarState.Off) // 关闭滚动条 .clip(false); // 不裁剪超出部分,这里是允许内部组件阴影可以向父布局外扩散 } .height('100%') // 设置高度为100% .width('100%') // 设置宽度为100% .backgroundColor("#f4f8fb"); // 设置背景色 } }转载自https://www.cnblogs.com/zhongcx/p/18563304
  • [技术干货] 鸿蒙NEXT开发案例:二维码的生成与识别
    【引言】在本篇文章中,我们将探讨如何在鸿蒙NEXT平台上实现二维码的生成与识别功能。通过使用ArkUI组件库和相关的媒体库,我们将创建一个简单的应用程序,用户可以生成二维码并扫描识别。【环境准备】• 操作系统:Windows 10• 开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806• 目标设备:华为Mate60 Pro• 开发语言:ArkTS• 框架:ArkUI• API版本:API 12• 权限:ohos.permission.WRITE_IMAGEVIDEO(为实现将图片保存至相册功能)【项目介绍】1. 项目结构我们首先定义一个名为QrCodeGeneratorAndScanner的组件,使用@Component装饰器进行标记。该组件包含多个状态变量和方法,用于处理二维码的生成、识别和剪贴板操作。2. 组件状态组件的状态包括:buttonOptions: 定义分段按钮的选项,用于切换生成和识别二维码的功能。inputText: 用户输入的文本,用于生成二维码。scanResult: 扫描结果文本。scanResultObject: 存储扫描结果的对象。3. 用户界面构建在build方法中,我们使用Column和Row布局来构建用户界面。主要包含以下部分:分段按钮:用户可以选择生成二维码或识别二维码。输入区域:用户可以输入文本并生成二维码。二维码显示:根据输入文本生成二维码。扫描区域:用户可以通过相机扫描二维码或从图库选择图片进行识别。4. 二维码生成二维码生成使用QRCode组件,输入文本通过this.inputText传递。用户输入后,二维码会实时更新。5. 二维码识别二维码识别功能通过scanBarcode模块实现。用户可以点击“扫一扫”按钮,启动相机进行扫描,或选择图库中的图片进行识别。识别结果将显示在界面上,并提供复制功能。6. 剪贴板操作用户可以将扫描结果复制到剪贴板,使用pasteboard模块实现。点击“复制”按钮后,扫描结果将被复制,用户会收到提示。【完整代码】填写权限使用声明字符串:src/main/resources/base/element/string.json{ "string": [ { "name": "module_desc", "value": "module description" }, { "name": "EntryAbility_desc", "value": "description" }, { "name": "EntryAbility_label", "value": "label" }, { "name": "WRITE_IMAGEVIDEO_info", "value": "保存功能需要该权限" } ] }配置权限:src/main/module.json5{ "module": { "requestPermissions": [ { "name": 'ohos.permission.WRITE_IMAGEVIDEO', "reason": "$string:WRITE_IMAGEVIDEO_info", "usedScene": { } } ], //... ...示例代码:src/main/ets/pages/Index.etsimport { componentSnapshot, // 组件快照 promptAction, // 提示操作 SegmentButton, // 分段按钮 SegmentButtonItemTuple, // 分段按钮项元组 SegmentButtonOptions // 分段按钮选项 } from '@kit.ArkUI'; // 引入 ArkUI 组件库 import { photoAccessHelper } from '@kit.MediaLibraryKit'; // 引入 MediaLibraryKit 中的照片访问助手 import { common } from '@kit.AbilityKit'; // 引入 AbilityKit 中的通用功能 import { fileIo as fs } from '@kit.CoreFileKit'; // 引入 CoreFileKit 中的文件 I/O 模块 import { image } from '@kit.ImageKit'; // 引入 ImageKit 中的图像处理模块 import { BusinessError, pasteboard } from '@kit.BasicServicesKit'; // 引入 BasicServicesKit 中的业务错误和剪贴板操作 import { hilog } from '@kit.PerformanceAnalysisKit'; // 引入 PerformanceAnalysisKit 中的性能分析模块 import { detectBarcode, scanBarcode } from '@kit.ScanKit'; // 引入 ScanKit 中的条形码识别模块 @Entry // 入口标记 @Component // 组件标记 struct QrCodeGeneratorAndScanner { // 定义二维码生成与识别组件 @State private buttonOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({ // 定义分段按钮选项 buttons: [{ text: '生成二维码' }, { text: '识别二维码' }] as SegmentButtonItemTuple, // 按钮文本 multiply: false, // 不允许多选 fontColor: Color.White, // 字体颜色为白色 selectedFontColor: Color.White, // 选中字体颜色为白色 selectedBackgroundColor: Color.Orange, // 选中背景颜色为橙色 backgroundColor: "#d5d5d5", // 背景颜色 backgroundBlurStyle: BlurStyle.BACKGROUND_THICK // 背景模糊样式 }) @State private sampleText: string = 'hello world'; // 示例文本 @State private inputText: string = ""; // 输入文本 @State private scanResult: string = ""; // 扫描结果 @State @Watch('selectIndexChanged') selectIndex: number = 0 // 选择索引 @State @Watch('selectedIndexesChanged') selectedIndexes: number[] = [0]; // 选中索引数组 private qrCodeId: string = "qrCodeId" // 二维码 ID @State private scanResultObject: scanBarcode.ScanResult = ({} as scanBarcode.ScanResult) // 扫描结果对象 @State private textColor: string = "#2e2e2e"; // 文本颜色 @State private shadowColor: string = "#d5d5d5"; // 阴影颜色 @State private basePadding: number = 30; // 基础内边距 selectedIndexesChanged() { // 选中索引改变事件 console.info(`this.selectedIndexes[0]:${this.selectedIndexes[0]}`) this.selectIndex = this.selectedIndexes[0] } selectIndexChanged() { // 选择索引改变事件 console.info(`selectIndex:${this.selectIndex}`) this.selectedIndexes[0] = this.selectIndex } private copyToClipboard(text: string): void { // 复制文本到剪贴板 const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); // 创建剪贴板数据 const systemPasteboard = pasteboard.getSystemPasteboard(); // 获取系统剪贴板 systemPasteboard.setData(pasteboardData); // 设置数据 promptAction.showToast({ message: '已复制' }); // 弹出提示消息 } build() { // 构建界面 Column() { // 列布局 SegmentButton({ // 分段按钮 options: this.buttonOptions, // 选项 selectedIndexes: this.selectedIndexes // 选中索引 }).width('400lpx').margin({ top: 20 }) // 设置宽度和外边距 Tabs({ index: this.selectIndex }) { // 选项卡 TabContent() { // 选项卡内容 Scroll() { // 滚动视图 Column() { // 列布局 Column() { // 列布局 Row() { // 行布局 Text('示例') // 文本 .fontColor("#5871ce") // 字体颜色 .fontSize(16) // 字体大小 .padding(`${this.basePadding / 2}lpx`) // 内边距 .backgroundColor("#f2f1fd") // 背景颜色 .borderRadius(5) // 边框圆角 .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 点击效果 .onClick(() => { // 点击事件 this.inputText = this.sampleText; // 设置输入文本为示例文本 }); Blank(); // 空白占位 Text('清空') // 清空按钮 .fontColor("#e48742") // 字体颜色 .fontSize(16) // 字体大小 .padding(`${this.basePadding / 2}lpx`) // 内边距 .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 点击效果 .backgroundColor("#ffefe6") // 背景颜色 .borderRadius(5) // 边框圆角 .onClick(() => { // 点击事件 this.inputText = ""; // 清空输入文本 }); } .height(45) // 设置高度 .justifyContent(FlexAlign.SpaceBetween) // 主轴对齐方式 .width('100%'); // 设置宽度 Divider(); // 分隔线 TextArea({ text: $$this.inputText, placeholder: `请输入内容` }) // 文本输入框 .width(`${650 - this.basePadding * 2}lpx`) // 设置宽度 .height(100) // 设置高度 .fontSize(16) // 字体大小 .caretColor(this.textColor) // 光标颜色 .fontColor(this.textColor) // 字体颜色 .margin({ top: `${this.basePadding}lpx` }) // 外边距 .padding(0) // 内边距 .backgroundColor(Color.Transparent) // 背景颜色 .borderRadius(0) // 边框圆角 .textAlign(TextAlign.JUSTIFY); // 文本对齐方式 } .alignItems(HorizontalAlign.Start) // 交叉轴对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 内边距 .borderRadius(10) // 边框圆角 .backgroundColor(Color.White) // 背景颜色 .shadow({ // 阴影 radius: 10, // 阴影半径 color: this.shadowColor, // 阴影颜色 offsetX: 0, // X 轴偏移 offsetY: 0 // Y 轴偏移 }); Row() { // 行布局 QRCode(this.inputText) // 二维码组件 .width('300lpx') // 设置宽度 .aspectRatio(1) // 设置宽高比 .id(this.qrCodeId) // 设置 ID 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 { // 尝试 let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg'); // 创建图片资源 let file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); // 打开文件 componentSnapshot.get(this.qrCodeId).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 // 持续时间 }); } }) } .visibility(this.inputText ? Visibility.Visible : Visibility.Hidden) // 根据输入文本设置可见性 .justifyContent(FlexAlign.SpaceBetween) // 主轴对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 内边距 .margin({ top: `${this.basePadding}lpx` }) // 外边距 .borderRadius(10) // 边框圆角 .backgroundColor(Color.White) // 背景颜色 .shadow({ // 阴影 radius: 10, // 阴影半径 color: this.shadowColor, // 阴影颜色 offsetX: 0, // X 轴偏移 offsetY: 0 // Y 轴偏移 }); }.padding({ top: 20, bottom: 20 }) // 设置内边距 .width('100%') // 设置宽度 }.scrollBar(BarState.Off) // 禁用滚动条 .align(Alignment.Top) // 顶部对齐 .height('100%') // 设置高度 } TabContent() { // 第二个选项卡内容 Scroll() { // 滚动视图 Column() { // 列布局 Row() { // 行布局 Text('扫一扫') // 扫一扫文本 .fontSize(20) // 字体大小 .textAlign(TextAlign.Center) // 文本居中对齐 .fontColor("#5871ce") // 字体颜色 .backgroundColor("#f2f1fd") // 背景颜色 .clickEffect({ scale: 0.8, level: ClickEffectLevel.LIGHT }) // 点击效果 .borderRadius(10) // 边框圆角 .height('250lpx') // 设置高度 .layoutWeight(1) // 布局权重 .onClick(() => { // 点击事件 if (canIUse('SystemCapability.Multimedia.Scan.ScanBarcode')) { // 检查是否支持扫描 try { // 尝试 scanBarcode.startScanForResult(getContext(this), { // 开始扫描 enableMultiMode: true, // 启用多模式 enableAlbum: true // 启用相册选择 }, (error: BusinessError, result: scanBarcode.ScanResult) => { // 扫描结果回调 if (error) { // 如果发生错误 hilog.error(0x0001, '[Scan CPSample]', // 记录错误日志 `Failed to get ScanResult by callback with options. Code: ${error.code}, message: ${error.message}`); return; // 退出 } hilog.info(0x0001, '[Scan CPSample]', // 记录成功日志 `Succeeded in getting ScanResult by callback with options, result is ${JSON.stringify(result)}`); this.scanResultObject = result; // 设置扫描结果对象 this.scanResult = result.originalValue ? result.originalValue : '无法识别'; // 设置扫描结果文本 }); } catch (error) { // 捕获错误 hilog.error(0x0001, '[Scan CPSample]', // 记录错误日志 `Failed to start the scanning service. Code:${error.code}, message: ${error.message}`); } } else { // 如果不支持扫描 promptAction.showToast({ message: '当前设备不支持二维码扫描' }); // 弹出提示消息 } }); Line().width(`${this.basePadding}lpx`).aspectRatio(1); // 分隔线 Text('图库选') // 图库选择文本 .fontSize(20) // 字体大小 .textAlign(TextAlign.Center) // 文本居中对齐 .fontColor("#e48742") // 字体颜色 .backgroundColor("#ffefe6") // 背景颜色 .borderRadius(10) // 边框圆角 .clickEffect({ scale: 0.8, level: ClickEffectLevel.LIGHT }) // 点击效果 .height('250lpx') // 设置高度 .layoutWeight(1) // 布局权重 .onClick(() => { // 点击事件 if (canIUse('SystemCapability.Multimedia.Scan.ScanBarcode')) { // 检查是否支持扫描 let photoOption = new photoAccessHelper.PhotoSelectOptions(); // 创建照片选择选项 photoOption.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 设置 MIME 类型 photoOption.maxSelectNumber = 1; // 设置最大选择数量 let photoPicker = new photoAccessHelper.PhotoViewPicker(); // 创建照片选择器 photoPicker.select(photoOption).then((result) => { // 选择照片 let inputImage: detectBarcode.InputImage = { uri: result.photoUris[0] }; // 获取选中的图片 URI try { // 尝试 detectBarcode.decode(inputImage, // 解码条形码 (error: BusinessError, result: Array<scanBarcode.ScanResult>) => { // 解码结果回调 if (error && error.code) { // 如果发生错误 hilog.error(0x0001, '[Scan Sample]', // 记录错误日志 `Failed to get ScanResult by callback. Code: ${error.code}, message: ${error.message}`); return; // 退出 } hilog.info(0x0001, '[Scan Sample]', // 记录成功日志 `Succeeded in getting ScanResult by callback, result is ${JSON.stringify(result, null, '\u00A0\u00A0')}`); if (result.length > 0) { // 如果有结果 this.scanResultObject = result[0]; // 设置扫描结果对象 this.scanResult = result[0].originalValue ? result[0].originalValue : '无法识别'; // 设置扫描结果文本 } else { // 如果没有结果 this.scanResult = '不存在二维码'; // 设置结果文本 } }); } catch (error) { // 捕获错误 hilog.error(0x0001, '[Scan Sample]', // 记录错误日志 `Failed to detect Barcode. Code: ${error.code}, message: ${error.message}`); } }); } else { // 如果不支持扫描 promptAction.showToast({ message: '当前设备不支持二维码扫描' }); // 弹出提示消息 } }); } .justifyContent(FlexAlign.SpaceEvenly) // 主轴对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 内边距 .borderRadius(10) // 边框圆角 .backgroundColor(Color.White) // 背景颜色 .shadow({ // 阴影 radius: 10, // 阴影半径 color: this.shadowColor, // 阴影颜色 offsetX: 0, // X 轴偏移 offsetY: 0 // Y 轴偏移 }); Column() { // 列布局 Row() { // 行布局 Text(`解析结果:\n${this.scanResult}`) // 显示解析结果文本 .fontColor(this.textColor) // 设置字体颜色 .fontSize(18) // 设置字体大小 .layoutWeight(1) // 设置布局权重 Text('复制') // 复制按钮文本 .fontColor(Color.White) // 设置字体颜色为白色 .fontSize(16) // 设置字体大小 .padding(`${this.basePadding / 2}lpx`) // 设置内边距 .backgroundColor("#0052d9") // 设置背景颜色 .borderRadius(5) // 设置边框圆角 .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 设置点击效果 .onClick(() => { // 点击事件 this.copyToClipboard(this.scanResult); // 复制扫描结果到剪贴板 }); }.constraintSize({ minHeight: 45 }) // 设置最小高度约束 .justifyContent(FlexAlign.SpaceBetween) // 设置主轴对齐方式 .width('100%'); // 设置宽度为100% } .visibility(this.scanResult ? Visibility.Visible : Visibility.Hidden) // 根据扫描结果设置可见性 .alignItems(HorizontalAlign.Start) // 设置交叉轴对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 设置内边距 .margin({ top: `${this.basePadding}lpx` }) // 设置外边距 .borderRadius(10) // 设置边框圆角 .backgroundColor(Color.White) // 设置背景颜色为白色 .shadow({ // 设置阴影 radius: 10, // 设置阴影半径 color: this.shadowColor, // 设置阴影颜色 offsetX: 0, // 设置X轴偏移 offsetY: 0 // 设置Y轴偏移 }); Column() { // 列布局 Row() { // 行布局 Text(`完整结果:`).fontColor(this.textColor).fontSize(18).layoutWeight(1) // 显示完整结果文本 }.constraintSize({ minHeight: 45 }) // 设置最小高度约束 .justifyContent(FlexAlign.SpaceBetween) // 设置主轴对齐方式 .width('100%'); // 设置宽度为100% Divider().margin({ top: 2, bottom: 15 }); // 添加分隔线并设置外边距 Row() { // 行布局 Text(`${JSON.stringify(this.scanResultObject, null, '\u00A0\u00A0')}`) // 显示完整扫描结果 .fontColor(this.textColor) // 设置字体颜色 .fontSize(18) // 设置字体大小 .layoutWeight(1) // 设置布局权重 .copyOption(CopyOptions.LocalDevice); // 设置复制选项 }.constraintSize({ minHeight: 45 }) // 设置最小高度约束 .justifyContent(FlexAlign.SpaceBetween) // 设置主轴对齐方式 .width('100%'); // 设置宽度为100% } .visibility(this.scanResult ? Visibility.Visible : Visibility.Hidden) // 根据扫描结果设置可见性 .alignItems(HorizontalAlign.Start) // 设置交叉轴对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 设置内边距 .margin({ top: `${this.basePadding}lpx` }) // 设置外边距 .borderRadius(10) // 设置边框圆角 .backgroundColor(Color.White) // 设置背景颜色为白色 .shadow({ // 设置阴影 radius: 10, // 设置阴影半径 color: this.shadowColor, // 设置阴影颜色 offsetX: 0, // 设置X轴偏移 offsetY: 0 // 设置Y轴偏移 }); } .padding({ top: 20, bottom: 20 }) // 设置内边距 .width('100%') // 设置宽度为100% }.scrollBar(BarState.Off) // 禁用滚动条 .align(Alignment.Top) // 顶部对齐 .height('100%') // 设置高度为100% } } .barHeight(0) // 设置选项卡条高度为0 .tabIndex(this.selectIndex) // 设置当前选中的索引 .width('100%') // 设置宽度为100% .layoutWeight(1) // 设置布局权重 .onChange((index: number) => { // 选项卡变化事件 this.selectIndex = index; // 更新选择的索引 }); } .height('100%') // 设置高度为100% .width('100%') // 设置宽度为100% .backgroundColor("#f4f8fb"); // 设置背景颜色 } }转载自https://www.cnblogs.com/zhongcx/p/18566067
  • [技术干货] 鸿蒙NEXT开发案例:亲戚关系计算器
     【引言】在快节奏的现代生活中,人们往往因为忙碌而忽略了与亲戚间的互动,特别是在春节期间,面对众多的长辈和晚辈时,很多人会感到困惑,不知道该如何正确地称呼每一位亲戚。针对这一问题,我们开发了一款基于鸿蒙NEXT平台的“亲戚关系计算器”应用,旨在帮助用户快速、准确地识别和称呼他们的亲戚。【环境准备】• 操作系统:Windows 10• 开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806• 目标设备:华为Mate60 Pro• 开发语言:ArkTS• 框架:ArkUI• API版本:API 12• 三方库:@nutpi/relationship(核心算法)【应用背景】中国社会有着深厚的家庭观念,亲属关系复杂多样。从血缘到姻亲,从直系到旁系,每一种关系都有其独特的称呼方式。然而,随着社会的发展,家庭成员之间的联系逐渐变得疏远,尤其是对于年轻人来说,准确地称呼每一位亲戚成了一项挑战。为了应对这一挑战,“亲戚关系计算器”应运而生。【核心功能】1. 关系输入:用户可以通过界面输入或选择具体的亲戚关系描述,例如“爸爸的哥哥的儿子”。2. 性别及称呼选择:考虑到不同地区的习俗差异,应用允许用户选择自己的性别和希望使用的称呼方式,比如“哥哥”、“姐夫”等。3. 关系计算:利用@nutpi/relationship库,根据用户提供的信息,精确计算出正确的亲戚称呼。4. 示例与清空:提供示例按钮供用户测试应用功能,同时也设有清空按钮方便用户重新开始。5. 个性化设置:支持多种方言和地方习惯的称呼方式,让应用更加贴近用户的实际需求。【用户界面】应用的用户界面简洁明了,主要由以下几个部分组成:• 选择性别:通过分段按钮让用户选择自己的性别。• 选择称呼方式:另一个分段按钮让用户选择希望的称呼方式。• 输入关系描述:提供一个文本输入框,用户可以在此处输入具体的关系描述。• 结果显示区:在用户提交信息后,这里会显示出正确的亲戚称呼。• 操作按钮:包括示例按钮、清空按钮等,方便用户操作。【完整代码】导包1ohpm install @nutpi/relationship代码// 导入关系计算模块 import relationship from "@nutpi/relationship" // 导入分段按钮组件及配置类型 import { SegmentButton, SegmentButtonItemTuple, SegmentButtonOptions } from '@kit.ArkUI'; // 使用 @Entry 和 @Component 装饰器标记这是一个应用入口组件 @Entry @Component // 定义一个名为 RelationshipCalculator 的结构体,作为组件主体 struct RelationshipCalculator { // 用户输入的关系描述,默认值为“爸爸的堂弟” @State private userInputRelation: string = "爸爸的堂弟"; // 应用的主题颜色,设置为橙色 @State private themeColor: string | Color = Color.Orange; // 文字颜色 @State private textColor: string = "#2e2e2e"; // 边框颜色 @State private lineColor: string = "#d5d5d5"; // 基础内边距大小 @State private paddingBase: number = 30; // 性别选项数组 @State private genderOptions: object[] = [Object({ text: '男' }), Object({ text: '女' })]; // 称呼方式选项数组 @State private callMethodOptions: object[] = [Object({ text: '我叫ta' }), Object({ text: 'ta叫我' })]; // 性别选择按钮的配置 @State private genderButtonOptions: SegmentButtonOptions | undefined = undefined; // 称呼方式选择按钮的配置 @State private callMethodButtonOptions: SegmentButtonOptions | undefined = undefined; // 当前选中的性别索引 @State @Watch('updateSelections') selectedGenderIndex: number[] = [0]; // 当前选中的称呼方式索引 @State @Watch('updateSelections') selectedCallMethodIndex: number[] = [0]; // 用户输入的关系描述 @State @Watch('updateSelections') userInput: string = ""; // 计算结果显示 @State calculationResult: string = ""; // 输入框是否获得焦点 @State isInputFocused: boolean = false; // 当选择发生改变时,更新关系计算 updateSelections() { // 根据索引获取选中的性别(0为男,1为女) const gender = this.selectedGenderIndex[0] === 0 ? 1 : 0; // 判断是否需要反转称呼方向 const reverse = this.selectedCallMethodIndex[0] === 0 ? false : true; // 调用关系计算模块进行计算 const result: string[] = relationship({ text: this.userInput, reverse: reverse, sex: gender }) as string[]; // 如果有计算结果,则更新显示;否则显示默认提示 if (result && result.length > 0) { this.calculationResult = `${reverse ? '对方称呼我' : '我称呼对方'}:${result[0]}`; } else { this.calculationResult = this.userInput ? '当前信息未查到关系' : ''; } } // 组件即将显示时,初始化性别和称呼方式选择按钮的配置 aboutToAppear(): void { this.genderButtonOptions = SegmentButtonOptions.capsule({ buttons: this.genderOptions as SegmentButtonItemTuple, multiply: false, fontColor: Color.White, selectedFontColor: Color.White, selectedBackgroundColor: this.themeColor, backgroundColor: this.lineColor, backgroundBlurStyle: BlurStyle.BACKGROUND_THICK }); this.callMethodButtonOptions = SegmentButtonOptions.capsule({ buttons: this.callMethodOptions as SegmentButtonItemTuple, multiply: false, fontColor: Color.White, selectedFontColor: Color.White, selectedBackgroundColor: this.themeColor, backgroundColor: this.lineColor, backgroundBlurStyle: BlurStyle.BACKGROUND_THICK }); } // 构建组件界面 build() { // 创建主列布局 Column() { // 标题栏 Text('亲戚关系计算器') .fontColor(this.textColor) .fontSize(18) .width('100%') .height(50) .textAlign(TextAlign.Center) .backgroundColor(Color.White) .shadow({ radius: 2, color: this.lineColor, offsetX: 0, offsetY: 5 }); // 内部列布局 Column() { // 性别选择行 Row() { Text('我的性别').fontColor(this.textColor).fontSize(18); // 性别选择按钮 SegmentButton({ options: this.genderButtonOptions, selectedIndexes: this.selectedGenderIndex }).width('400lpx'); }.height(45).justifyContent(FlexAlign.SpaceBetween).width('100%'); // 称呼方式选择行 Row() { Text('称呼方式').fontColor(this.textColor).fontSize(18); // 称呼方式选择按钮 SegmentButton({ options: this.callMethodButtonOptions, selectedIndexes: this.selectedCallMethodIndex }).width('400lpx'); }.height(45).justifyContent(FlexAlign.SpaceBetween).width('100%'); // 示例与清空按钮行 Row() { // 示例按钮 Text('示例') .fontColor("#5871ce") .fontSize(18) .padding(`${this.paddingBase / 2}lpx`) .backgroundColor("#f2f1fd") .borderRadius(5) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) .onClick(() => { this.userInput = this.userInputRelation; }); // 空白间隔 Blank(); // 清空按钮 Text('清空') .fontColor("#e48742") .fontSize(18) .padding(`${this.paddingBase / 2}lpx`) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) .backgroundColor("#ffefe6") .borderRadius(5) .onClick(() => { this.userInput = ""; }); }.height(45) .justifyContent(FlexAlign.SpaceBetween) .width('100%'); // 用户输入框 TextInput({ text: $$this.userInput, placeholder: !this.isInputFocused ? `请输入称呼。如:${this.userInputRelation}` : '' }) .placeholderColor(this.isInputFocused ? this.themeColor : Color.Gray) .fontColor(this.isInputFocused ? this.themeColor : this.textColor) .borderColor(this.isInputFocused ? this.themeColor : Color.Gray) .caretColor(this.themeColor) .borderWidth(1) .borderRadius(10) .onBlur(() => this.isInputFocused = false) .onFocus(() => this.isInputFocused = true) .height(45) .width('100%') .margin({ top: `${this.paddingBase / 2}lpx` }); } .alignItems(HorizontalAlign.Start) .width('650lpx') .padding(`${this.paddingBase}lpx`) .margin({ top: `${this.paddingBase}lpx` }) .borderRadius(10) .backgroundColor(Color.White) .shadow({ radius: 10, color: this.lineColor, offsetX: 0, offsetY: 0 }); // 结果显示区 Column() { Row() { // 显示计算结果 Text(this.calculationResult).fontColor(this.textColor).fontSize(18); }.justifyContent(FlexAlign.SpaceBetween).width('100%'); } .visibility(this.calculationResult ? Visibility.Visible : Visibility.None) .alignItems(HorizontalAlign.Start) .width('650lpx') .padding(`${this.paddingBase}lpx`) .margin({ top: `${this.paddingBase}lpx` }) .borderRadius(10) .backgroundColor(Color.White) .shadow({ radius: 10, color: this.lineColor, offsetX: 0, offsetY: 0 }); } .height('100%') .width('100%') .backgroundColor("#f4f8fb"); } } 转载自https://www.cnblogs.com/zhongcx/p/18568197
  • [技术干货] 鸿蒙NEXT开发案例:文字转拼音
     【引言】在鸿蒙NEXT开发中,文字转拼音是一个常见的需求,本文将介绍如何利用鸿蒙系统和pinyin-pro库实现文字转拼音的功能。【环境准备】• 操作系统:Windows 10• 开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806• 目标设备:华为Mate60 Pro• 开发语言:ArkTS• 框架:ArkUI• API版本:API 12• 三方库:pinyin-pro@3.18.3(核心算法)【开始步骤】首先,我们引入pinyin-pro库中的pinyin函数,用于将中文转换为拼音。然后定义一个PinyinBean类来存储字符和其对应的拼音,以便后续展示转换结果。接着,我们使用装饰器定义一个PinyinConverter组件,该组件实现了文字转拼音的功能。通过用户输入文本,调用convertToPinyin方法将文本转换成拼音数组,并将拼音和字符对应存储在conversionResult数组中。在UI方面,我们通过鸿蒙系统提供的布局组件和样式设置,构建了一个用户友好的界面。用户可以输入文本,点击示例按钮填充默认文本,点击清空按钮清空输入内容。转换结果会以拼音和字符的形式展示在界面上。整个开发案例涵盖了鸿蒙NEXT开发中的组件定义、状态管理、事件处理、UI构建等方面,展示了如何利用鸿蒙系统和第三方库实现文字转拼音的功能。【完整代码】导包1ohpm install pinyin-pro@3.18.3代码// 引入pinyin-pro库中的pinyin函数,用于将中文转换为拼音 import { pinyin } from "pinyin-pro"; // 定义一个类来存储字符和其对应的拼音 class PinyinBean { pinyin: string; // 拼音 character: string; // 对应的汉字 // 构造器,初始化拼音和字符 constructor(pinyin: string, character: string) { this.pinyin = pinyin; this.character = character; } } // 使用装饰器定义一个组件,该组件用于实现文字转拼音功能 @Entry @Component struct PinyinConverter { // 默认的用户输入内容 @State private defaultInput: string = "混沌未分天地乱,茫茫渺渺无人见。自从盘古破鸿蒙,开辟从兹清浊辨。"; // 组件的主题颜色 @State private themeColor: string | Color = Color.Orange; // 组件的文字颜色 @State private fontColor: string = "#2e2e2e"; // 组件的边框颜色 @State private lineColor: string = "#d5d5d5"; // 基础内边距值 @State private basePadding: number = 30; // 用户输入的内容,当这个状态改变时会触发convertToPinyin方法 @State @Watch('convertToPinyin') userInput: string = ""; // 转换结果显示,存储了转换后的拼音和对应字符 @State conversionResult: PinyinBean[] = []; // 输入框是否获得了焦点 @State isInputFocused: boolean = false; // 方法:将用户输入的文本转换成拼音 convertToPinyin() { // 使用pinyin-pro库将输入的文本转换成拼音数组 const pinyinArray: string[] = pinyin(this.userInput, { type: "array" }); // 将输入的文本分割成单个字符的数组 const charArray: string[] = this.userInput.split(""); // 清空转换结果数组 this.conversionResult.length = 0; // 遍历拼音数组,创建PinyinBean对象,并将其添加到转换结果数组中 for (let i = 0; i < pinyinArray.length; i++) { this.conversionResult.push(new PinyinBean(pinyinArray[i], charArray[i])); } } // 构建UI的方法 build() { // 创建一个垂直布局的容器 Column() { // 添加标题栏 Text('文字转拼音') .fontColor(this.fontColor) // 设置字体颜色 .fontSize(18) // 设置字体大小 .width('100%') // 设置宽度为100% .height(50) // 设置高度为50 .textAlign(TextAlign.Center) // 文本居中对齐 .backgroundColor(Color.White) // 设置背景色为白色 .shadow({ // 添加阴影效果 radius: 2, // 阴影圆角 color: this.lineColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 5 // Y轴偏移量 }); // 内部垂直布局 Column() { // 示例与清空按钮行 Row() { // 示例按钮 Text('示例') .fontColor("#5871ce") // 设置字体颜色 .fontSize(18) // 设置字体大小 .padding(`${this.basePadding / 2}lpx`) // 设置内边距 .backgroundColor("#f2f1fd") // 设置背景色 .borderRadius(5) // 设置圆角 .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 设置点击效果 .onClick(() => { // 点击事件处理 this.userInput = this.defaultInput; // 将默认输入设置为用户输入 }); // 空白间隔 Blank(); // 清空按钮 Text('清空') .fontColor("#e48742") // 设置字体颜色 .fontSize(18) // 设置字体大小 .padding(`${this.basePadding / 2}lpx`) // 设置内边距 .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 设置点击效果 .backgroundColor("#ffefe6") // 设置背景色 .borderRadius(5) // 设置圆角 .onClick(() => { // 点击事件处理 this.userInput = ""; // 清空用户输入 }); }.height(45) // 设置高度 .justifyContent(FlexAlign.SpaceBetween) // 子元素之间等间距分布 .width('100%'); // 设置宽度为100% // 用户输入框 Row() { TextArea({ text: $$this.userInput, // 绑定用户输入 placeholder: !this.isInputFocused ? `请输入内容。如:${this.defaultInput}` : '' // 设置占位符 }) .backgroundColor(Color.Transparent) // 设置背景色为透明 .padding(0) // 设置内边距 .height('100%') // 设置高度为100% .placeholderColor(this.isInputFocused ? this.themeColor : Color.Gray) // 设置占位符颜色 .fontColor(this.isInputFocused ? this.themeColor : this.fontColor) // 设置字体颜色 .caretColor(this.themeColor) // 设置光标颜色 .borderRadius(0) // 设置圆角 .onBlur(() => this.isInputFocused = false) // 当失去焦点时更新状态 .onFocus(() => this.isInputFocused = true) // 当获得焦点时更新状态 .width('100%'); // 设置宽度为100% } .padding(`${this.basePadding / 2}lpx`) // 设置内边距 .backgroundColor("#f2f1fd") // 设置背景色 .width('100%') // 设置宽度为100% .height(120) // 设置高度 .borderWidth(1) // 设置边框宽度 .borderRadius(10) // 设置圆角 .borderColor(this.isInputFocused ? this.themeColor : Color.Gray) // 设置边框颜色 .margin({ top: `${this.basePadding / 2}lpx` }); // 设置上边距 } .alignItems(HorizontalAlign.Start) // 设置子元素水平对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 设置内边距 .margin({ top: `${this.basePadding}lpx` }) // 设置上边距 .borderRadius(10) // 设置圆角 .backgroundColor(Color.White) // 设置背景色 .shadow({ // 设置阴影 radius: 10, // 阴影圆角 color: this.lineColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 0 // Y轴偏移量 }); // 结果显示区域 Column() { Row() { Flex({ wrap: FlexWrap.Wrap }) { // 允许子元素换行 ForEach(this.conversionResult, (item: PinyinBean, index: number) => { // 遍历转换结果 Column() { // 显示计算结果(拼音) Text(`${item.pinyin}`).fontColor(this.fontColor).fontSize(18); // 显示计算结果(字符) Text(`${item.character}`).fontColor(this.fontColor).fontSize(18); }.padding(3); // 设置内边距 }) } }.justifyContent(FlexAlign.SpaceBetween) // 子元素之间等间距分布 .width('100%'); // 设置宽度为100% } .visibility(this.conversionResult.length != 0 ? Visibility.Visible : Visibility.None) // 根据是否有转换结果决定是否显示 .alignItems(HorizontalAlign.Start) // 设置子元素水平对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 设置内边距 .margin({ top: `${this.basePadding}lpx` }) // 设置上边距 .borderRadius(10) // 设置圆角 .backgroundColor(Color.White) // 设置背景色 .shadow({ // 设置阴影 radius: 10, // 阴影圆角 color: this.lineColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 0 // Y轴偏移量 }); } .height('100%') // 设置高度为100% .width('100%') // 设置宽度为100% .backgroundColor("#f4f8fb"); // 设置背景色 } } 转载自https://www.cnblogs.com/zhongcx/p/18568438
  • [技术干货] 鸿蒙NEXT开发案例:温度转换
     【引言】温度是日常生活中常见的物理量,但不同国家和地区可能使用不同的温度单位,如摄氏度(Celsius)、华氏度(Fahrenheit)、开尔文(Kelvin)、兰氏度(Rankine)和列氏度(Reaumur)。为了方便用户在这些温度单位之间进行快速准确的转换,我们开发了一款温度转换工具。该工具支持五种温度单位之间的相互转换,旨在为用户提供便捷的服务。【环境准备】• 操作系统:Windows 10• 开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806• 目标设备:华为Mate60 Pro• 开发语言:ArkTS• 框架:ArkUI• API版本:API 12• 三方库:@nutpi/temperature-converter(核心算法)【项目结构】项目的核心组件是 TemperatureConverterApp,它负责构建整个应用的用户界面,并处理用户输入及温度单位之间的转换逻辑。1. 温度单位类定义我们定义了一个温度单位类 TemperatureUnit,用于封装温度单位的基本信息及其操作方法。每个温度单位都有一个标题、当前温度值和输入框的焦点状态。通过 setValue 方法,可以设置温度值并保留三位小数。2. 温度单位类型枚举为了更好地管理和使用温度单位,我们定义了一个温度单位类型对象 TemperatureUnitType,列出了五种温度单位的名称。3. 应用程序主组件TemperatureConverterApp 组件是整个应用的入口,它定义了应用的样式属性,并实现了UI的构建逻辑。组件中包含了多个状态变量,用于设置应用的颜色、字体大小等样式。在UI构建逻辑中,我们使用了鸿蒙NEXT提供的布局组件,如 Column 和 Row,来组织页面的布局。页面顶部有一个标题 "温度转换",下方是一个垂直布局的容器,动态生成每个温度单位的输入框。每个输入框都绑定了 onChange 事件,当用户输入或更改温度值时,会触发相应的转换逻辑,更新其他温度单位的值。4. 温度转换逻辑温度转换逻辑通过调用 @nutpi/temperature-converter 库中的方法实现。当用户在某个温度单位的输入框中输入温度后,程序会根据当前输入的温度单位,调用相应的转换方法,计算出其他温度单位对应的值,并更新界面上的显示。例如,如果用户在摄氏度输入框中输入温度,程序会自动计算出华氏度、开尔文、兰氏度和列氏度的值,并更新相应的输入框。【用户体验】为了提升用户体验,我们在输入框上添加了焦点状态的处理。当输入框获得焦点时,背景颜色和边框颜色会发生变化,以提示用户当前的操作位置。此外,输入框还支持输入过滤,只允许输入数字和小数点,确保输入的有效性。【完整代码】导包1ohpm install @nutpi/temperature-converter代码// 引入温度转换器模块 import { TemperatureConverter } from "@nutpi/temperature-converter" // 定义温度单位类型对象,用于存储各温度单位的名称 const TemperatureUnitType: object = Object({ Celsius: '摄氏度', // 摄氏度 Fahrenheit: '华氏度', // 华氏度 Kelvin: '开尔文', // 开尔文 Rankine: '兰氏度', // 兰氏度 Reaumur: '列氏度' // 列氏度 }) // 使用装饰器定义一个温度单位类 @ObservedV2 class TemperatureUnit { title: string // 温度单位的标题 @Trace value: string = "" // 当前温度值,使用@Trace装饰器追踪变化 @Trace isInputFocused: boolean = false // 输入框是否获得焦点,同样使用@Trace追踪变化 // 构造函数,初始化时传入温度单位的标题 constructor(title: string) { this.title = title } // 设置温度值的方法,保留三位小数 setValue(value: number) { this.value = `${parseFloat(value.toFixed(3))}` // 将数值转换成字符串,保留三位小数 console.info(`温度值:${this.value}`) // 打印当前温度值到控制台 } } // 定义温度转换器应用程序的入口组件 @Entry @Component struct TemperatureConverterApp { // 定义一系列的状态变量,用于设置应用的颜色、字体大小等样式 @State private primaryColor: string = "#080808" // 主色调 @State private secondaryColor: string = "#f7f7f7" // 次要色调 @State private bgColor: string = "#f4f8fb" // 背景颜色 @State private placeholderColor: string = "#2f9b6c" // 占位符颜色 @State private textColor: string = "#a3a3a3" // 文本颜色 @State private fontSizeSmall: number = 16 // 较小的字体大小 @State private fontSizeLarge: number = 18 // 较大的字体大小 @State private basePadding: number = 30 // 基础内边距 // 初始化温度单位数组,创建每个温度单位的实例 @State private temperatureUnits: TemperatureUnit[] = Object.keys(TemperatureUnitType).map(unit => new TemperatureUnit(TemperatureUnitType[unit])) // 构建应用程序的UI build() { Column() { // 创建一个垂直布局容器 // 添加标题 Text('温度转换') .fontColor(this.primaryColor) // 设置字体颜色 .fontSize(this.fontSizeSmall) // 设置字体大小 .width('100%') // 设置宽度 .height(50) // 设置高度 .textAlign(TextAlign.Center) // 设置文本对齐方式 .backgroundColor(Color.White) // 设置背景颜色 .shadow({ // 添加阴影效果 radius: 2, // 阴影半径 color: this.secondaryColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 5 // Y轴偏移量 }); // 循环遍历温度单位数组,动态生成每个温度单位的输入框 Column() { ForEach(this.temperatureUnits, (unit: TemperatureUnit, index: number) => { Row() { // 创建一个水平布局容器 // 显示温度单位的标题 Text(`${unit.title}`).fontSize(this.fontSizeSmall).fontColor(this.primaryColor) // 创建输入框 Row() { TextInput({ text: unit.value, // 输入框的初始值 placeholder: !unit.isInputFocused ? `请输入${unit.title}` : '' // 输入框的占位符文本 }) .inputFilter('[0-9.-]', (e) => console.log(JSON.stringify(e))) // 过滤输入内容,只允许数字和小数点 .fontSize(this.fontSizeSmall) // 设置字体大小 .backgroundColor(Color.Transparent) // 设置背景颜色 .padding(0) // 设置内边距 .width('100%') // 设置宽度 .height('100%') // 设置高度 .placeholderColor(unit.isInputFocused ? this.placeholderColor : this.textColor) // 设置占位符颜色 .fontColor(unit.isInputFocused ? this.placeholderColor : this.primaryColor) // 设置字体颜色 .caretColor(this.placeholderColor) // 设置光标颜色 .borderRadius(0) // 设置圆角 .onBlur(() => unit.isInputFocused = false) // 失去焦点时的处理 .onFocus(() => unit.isInputFocused = true) // 获得焦点时的处理 .onChange((value: string) => { // 输入内容改变时的处理 if (!unit.isInputFocused) { // 如果输入框未获得焦点,则不处理数据 console.info(`当前位置${index}没有焦点,不处理数据内容`) return } if (unit.value == value) { // 如果新旧值相同,则不处理 console.info(`当前位置${index}内容与修改内容相同,不需要继续处理`) return } console.info(`onChange, unit.value:${unit.value}, value:${value}`) // 打印变更信息 const tempValue = Number(value); // 将输入的字符串转换成数字 unit.setValue(tempValue) // 更新当前温度单位的值 // 根据用户输入的温度单位,计算并更新其他温度单位的值 switch (index) { case 0: this.temperatureUnits[1].setValue(TemperatureConverter.celsiusToFahrenheit(tempValue)) this.temperatureUnits[2].setValue(TemperatureConverter.celsiusToKelvin(tempValue)) this.temperatureUnits[3].setValue(TemperatureConverter.celsiusToRankine(tempValue)) this.temperatureUnits[4].setValue(TemperatureConverter.celsiusToReaumur(tempValue)) break; case 1: this.temperatureUnits[0].setValue(TemperatureConverter.fahrenheitToCelsius(tempValue)) this.temperatureUnits[2].setValue(TemperatureConverter.fahrenheitToKelvin(tempValue)) this.temperatureUnits[3].setValue(TemperatureConverter.fahrenheitToRankine(tempValue)) this.temperatureUnits[4].setValue(TemperatureConverter.fahrenheitToReaumur(tempValue)) break; case 2: this.temperatureUnits[0].setValue(TemperatureConverter.kelvinToCelsius(tempValue)) this.temperatureUnits[1].setValue(TemperatureConverter.kelvinToFahrenheit(tempValue)) this.temperatureUnits[3].setValue(TemperatureConverter.kelvinToRankine(tempValue)) this.temperatureUnits[4].setValue(TemperatureConverter.kelvinToReaumur(tempValue)) break; case 3: this.temperatureUnits[0].setValue(TemperatureConverter.rankineToCelsius(tempValue)) this.temperatureUnits[1].setValue(TemperatureConverter.rankineToFahrenheit(tempValue)) this.temperatureUnits[2].setValue(TemperatureConverter.rankineToKelvin(tempValue)) this.temperatureUnits[4].setValue(TemperatureConverter.rankineToReaumur(tempValue)) break; case 4: this.temperatureUnits[0].setValue(TemperatureConverter.reaumurToCelsius(tempValue)) this.temperatureUnits[1].setValue(TemperatureConverter.reaumurToFahrenheit(tempValue)) this.temperatureUnits[2].setValue(TemperatureConverter.reaumurToKelvin(tempValue)) this.temperatureUnits[3].setValue(TemperatureConverter.reaumurToRankine(tempValue)) break; } }); } .padding(`${this.basePadding / 2}lpx`) // 设置内边距 .backgroundColor(unit.isInputFocused ? this.bgColor : Color.Transparent) // 设置背景颜色 .layoutWeight(1) // 设置布局权重 .height(40) // 设置高度 .borderWidth(1) // 设置边框宽度 .borderRadius(10) // 设置圆角 .borderColor(unit.isInputFocused ? this.placeholderColor : this.secondaryColor) // 设置边框颜色 .margin({ left: `${this.basePadding / 2}lpx`, right: `${this.basePadding / 2}lpx` }); // 设置外边距 }.margin({ top: `${this.basePadding / 2}lpx`, bottom: `${this.basePadding / 2}lpx` }); // 设置外边距 }) } .alignItems(HorizontalAlign.Start) // 设置水平对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 设置内边距 .margin({ top: `${this.basePadding}lpx` }) // 设置外边距 .borderRadius(10) // 设置圆角 .backgroundColor(Color.White) // 设置背景颜色 .shadow({ // 添加阴影效果 radius: 10, // 阴影半径 color: this.secondaryColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 0 // Y轴偏移量 }); // 添加工具介绍部分 Column() { // 添加标题 Text('工具介绍').fontSize(this.fontSizeLarge).fontWeight(600).fontColor(this.primaryColor); // 添加工具介绍的文本 Text('这款温度单位转换工具专为满足您在科学研究、日常生活及工作中的需求而设计。借助此工具,您可以轻松实现摄氏度(Celsius)、华氏度(Fahrenheit)和开尔文(Kelvin)之间的无缝切换。无论是学术研究、日常应用还是专业工作,都能为您提供精准便捷的温度换算服务。') .textAlign(TextAlign.JUSTIFY) // 设置文本对齐方式 .fontSize(this.fontSizeSmall) // 设置字体大小 .fontColor(this.primaryColor) // 设置字体颜色 .margin({ top: `${this.basePadding / 2}lpx` }); // 设置外边距 } .alignItems(HorizontalAlign.Start) // 设置水平对齐方式 .width('650lpx') // 设置宽度 .padding(`${this.basePadding}lpx`) // 设置内边距 .margin({ top: `${this.basePadding}lpx` }) // 设置外边距 .borderRadius(10) // 设置圆角 .backgroundColor(Color.White) // 设置背景颜色 .shadow({ // 添加阴影效果 radius: 10, // 阴影半径 color: this.secondaryColor, // 阴影颜色 offsetX: 0, // X轴偏移量 offsetY: 0 // Y轴偏移量 }); } .height('100%') // 设置高度 .width('100%') // 设置宽度 .backgroundColor(this.bgColor); // 设置背景颜色 } } 转载自https://www.cnblogs.com/zhongcx/p/18570831
总条数:462 到第
上滑加载中