普通视图

发现新文章,点击刷新页面。
昨天 — 2025年6月7日首页

HarmonyOS运动语音开发:如何让运动开始时的语音播报更温暖

2025年6月6日 16:00

##鸿蒙核心技术##运动开发##Core Speech Kit(基础语音服务)#

前言

在运动类应用中,语音播报功能不仅可以提升用户体验,还能让运动过程更加生动有趣。想象一下,当你准备开始运动时,一个温暖的声音提醒你“3,2,1,运动开始了”,是不是比冷冰冰的文字提示更有动力呢?本文将结合鸿蒙(HarmonyOS)开发实战经验,深入解析如何实现运动开始时的语音播报功能,让每一次运动都充满活力。

IMG_20250606155608468.gif

一、语音合成功能简介

鸿蒙系统提供了强大的语音合成(Text-to-Speech,TTS)功能,可以将文字转换为语音。通过调用鸿蒙的 TTS API,我们可以轻松实现语音播报功能。以下是实现语音播报功能的核心代码:

1.初始化 TTS 引擎

在使用 TTS 功能之前,我们需要初始化 TTS 引擎。以下是初始化 TTS 引擎的代码:

private ttsEngine?: textToSpeech.TextToSpeechEngine;

private async initTtsEngine() {
  try {
    // 设置创建引擎参数
    let extraParam: Record<string, Object> = {"style": 'interaction-broadcast', "locate": 'CN', "name": 'EngineName'};
    let initParamsInfo: textToSpeech.CreateEngineParams = {
      language: 'zh-CN',
      person: 0,
      online: 1,
      extraParams: extraParam
    };

    // 调用createEngine方法
    textToSpeech.createEngine(initParamsInfo, (err: BusinessError, textToSpeechEngine: textToSpeech.TextToSpeechEngine) => {
      if (!err) {
        console.info('Succeeded in creating engine');
        // 接收创建引擎的实例
        this.ttsEngine = textToSpeechEngine;
        // 设置speak的回调信息
        let speakListener: textToSpeech.SpeakListener = {
          // 开始播报回调
          onStart(requestId: string, response: textToSpeech.StartResponse) {
            console.info(`onStart, requestId: ${requestId} response: ${JSON.stringify(response)}`);
          },
          // 合成完成及播报完成回调
          onComplete(requestId: string, response: textToSpeech.CompleteResponse) {
            console.info(`onComplete, requestId: ${requestId} response: ${JSON.stringify(response)}`);
          },
          // 停止播报回调
          onStop(requestId: string, response: textToSpeech.StopResponse) {
            console.info(`onStop, requestId: ${requestId} response: ${JSON.stringify(response)}`);
          },
          // 返回音频流
          onData(requestId: string, audio: ArrayBuffer, response: textToSpeech.SynthesisResponse) {
            console.info(`onData, requestId: ${requestId} sequence: ${JSON.stringify(response)} audio: ${JSON.stringify(audio)}`);
          },
          // 错误回调
          onError(requestId: string, errorCode: number, errorMessage: string) {
            console.error(`onError, requestId: ${requestId} errorCode: ${errorCode} errorMessage: ${errorMessage}`);
          }
        };
        // 设置回调
        this.ttsEngine?.setListener(speakListener);
      } else {
        console.error(`Failed to create engine. Code: ${err.code}, message: ${err.message}.`);
      }
    });

  } catch (error) {
    console.error('Failed to initialize TTS engine:', error);
  }
}

2.语音播报

初始化 TTS 引擎后,我们可以使用speak方法进行语音播报。以下是语音播报的代码:

private async speak(text: string) {
  if (!this.ttsEngine) {
    await this.initTtsEngine();
  }
  try {
    let extraParam: Record<string, Object> =
      {"queueMode": 0,
        "speed": 1,
        "volume": 2,
        "pitch": 1,
        "languageContext": 'zh-CN',
        "audioType": "pcm", "soundChannel": 3, "playType": 1 };
    let speakParams: textToSpeech.SpeakParams = {
      requestId: USystem.generateRandomString(5), // requestId在同一实例内仅能用一次,请勿重复设置
      extraParams: extraParam
    };
    // 调用播报方法
    // 开发者可以通过修改speakParams主动设置播报策略
    this.ttsEngine?.speak(text, speakParams);
  } catch (error) {
    console.error('TTS speak error:', error);
  }
}

3.调用语音播报

在运动开始时,我们可以通过定时器调用speak方法进行倒计时播报。以下是倒计时播报的代码:

private startCountdown() {
  let timer = setInterval(() => {
    if (this.countdownValue > 0) {
      this.speak(this.countdownValue.toString());
      this.countdownValue--;
    } else {
      clearInterval(timer);
      this.isCountdownFinished = true;
      this.speak('运动开始了');
      this.runTracker.start();
    }
  }, 1000);
}

二、代码核心点解析

1.初始化 TTS 引擎

createEngine:创建 TTS 引擎实例。需要设置语言、发音人、在线模式等参数。

setListener:设置语音播报的回调监听器,包括开始、完成、停止、错误等回调。

2.语音播报

speak:调用 TTS 引擎的speak方法进行语音播报。可以通过extraParams设置播报参数,如语速、音量、音调等。

3.倒计时播报

setInterval:使用定时器实现倒计时功能。

clearInterval:倒计时结束后,清除定时器,避免资源浪费。

三、优化与改进

1.语音播报参数优化

可以通过调整extraParams中的参数,优化语音播报的效果。例如,调整语速、音量、音调等参数,让语音播报更符合用户需求。

let extraParam: Record<string, Object> =
  {"queueMode": 0,
    "speed": 1.2, // 语速稍快
    "volume": 2, // 音量稍大
    "pitch": 1.1, // 音调稍高
    "languageContext": 'zh-CN',
    "audioType": "pcm", "soundChannel": 3, "playType": 1 };

2.语音播报内容优化

可以将倒计时播报内容从简单的数字改为更友好的提示语,提升用户体验。例如:

private startCountdown() {
  let timer = setInterval(() => {
    if (this.countdownValue > 0) {
      this.speak(`倒计时 ${this.countdownValue} 秒`);
      this.countdownValue--;
    } else {
      clearInterval(timer);
      this.isCountdownFinished = true;
      this.speak('运动正式开始,请做好准备');
      this.runTracker.start();
    }
  }, 1000);
}

3.语音播报异常处理

在实际开发中,可能会遇到 TTS 引擎初始化失败、语音播报失败等问题。可以通过监听错误回调,及时处理异常情况,提升应用的健壮性。

onError(requestId: string, errorCode: number, errorMessage: string) {
  console.error(`onError, requestId: ${requestId} errorCode: ${errorCode} errorMessage: ${errorMessage}`);
  // 可以在这里处理错误,例如重新初始化 TTS 引擎
}

四、总结

通过鸿蒙的 TTS 功能,我们可以轻松实现运动开始时的语音播报功能。

昨天以前首页

HarmonyOS5ArkTS常见数据类型认识

作者 大胖子101
2025年6月5日 17:41

在 ArkTS(基于 TypeScript 的 HarmonyOS 开发语言)中,常见数据类型主要分为基础类型和复杂类型两大类,以下是核心整理:


⚙️ 一、基础类型(Primitive Types)

  1. number

    • 数值类型,包含整数和浮点数(无单独的 int/float 区分
    typescript
    复制
    let age: number = 30;       // 整数
    let price: number = 9.99;   // 浮点数
    
  2. string

    • 文本类型,支持单引号/双引号/模板字符串
    typescript
    复制
    let name: string = "Alice";
    let msg: string = `Hello ${name}`;  // 模板字符串
    
  3. boolean

    • 布尔值,仅 true 或 false
    typescript
    复制
    let isActive: boolean = true;
    
  4. void

    • 函数无返回值
    typescript
    复制
    function logError(): void {
      console.error("Error!");
    }
    
  5. null 和 undefined

    • null:显式空值
    • undefined:变量未初始化默认值
    typescript
    复制
    let data: string | null = null;  // 联合类型允许赋值为 null
    

🧩 二、复杂类型(Complex Types)

集合类型

  1. 数组 Array<T>

    • 相同类型元素集合
    typescript
    复制
    let numbers: number[] = [1, 2, 3];
    let strings: Array<string> = ["a", "b"]; // 泛型语法
    
  2. 元组 Tuple

    • 固定长度类型顺序的数组
    typescript
    复制
    let userInfo: [string, number] = ["Alice", 25];
    let firstElement: string = userInfo[0]; // 类型安全访问
    
  3. 枚举 enum

    • 命名常量集合(自动赋值或手动指定)
    typescript
    复制
    enum Color { Red = 1, Green, Blue }; // Green=2, Blue=3
    let bg: Color = Color.Green;
    

对象类型

  1. 接口 Interface

    • 定义对象结构(最常用
    typescript
    复制
    interface User {
      id: number;
      name: string;
      isAdmin?: boolean;  // 可选属性
    }
    
    let user: User = { id: 1, name: "Bob" };
    
  2. 类 Class

    • 面向对象编程(封装/继承/多态)
    typescript
    复制
    class Person {
      constructor(public name: string) {}
      greet() { console.log(`Hello ${this.name}`) }
    }
    

特殊类型

  1. 联合类型 |

    • 变量可多选类型
    typescript
    复制
    let id: string | number;
    id = "001";  // 合法
    id = 100;    // 合法
    
  2. 字面量类型

    • 固定值作为类型(常与联合类型配合)
    typescript
    复制
    type Direction = "left" | "right" | "up" | "down";
    let move: Direction = "left"; // 只能赋值指定值
    
  3. any

    • 关闭类型检查(慎用!失去TS优势)
    typescript
    复制
    let unknownData: any = fetchExternalData(); 
    
  4. Resource(ArkUI特有)

    • 引用本地资源文件
    typescript
    复制
    Image($r('app.media.logo'))  // 加载resources中的图片
    

🛡️ 三、类型守卫与类型推断

  1. 类型守卫

    • 运行时检查类型(typeof/instanceof
    typescript
    复制
    function printId(id: number | string) {
      if (typeof id === "string") {
        console.log(id.toUpperCase()); // 自动识别为string
      } else {
        console.log(id.toFixed(2));    // 自动识别为number
      }
    }
    
  2. 类型别名 type

    • 为复杂类型命名
    typescript
    复制
    type UserID = string | number;
    type Callback = () => void;
    

💎 核心数据类型总结表

类型 示例 典型场景
number let count: number = 5; 数值计算
string let msg: string = "Hi"; 文本展示
boolean let isDone: boolean = false; 条件判断
Array<T> let list: number[] = [1,2,3]; 数据集合
Interface interface User { id: number } 结构化数据定义
Union Types `let id: string number;`
Resource $r('app.string.title') 本地化资源引用

实际开发建议:

  1. 优先使用接口定义对象结构
  2. 避免 any 充分利用TS静态检查
  3. 联合类型+类型守卫提高安全性
  4. 善用资源类型加载本地文件

HarmonyOS5鸿蒙开发常用组件介绍

作者 大胖子101
2025年6月5日 17:23

一、文本类组件

  1. Text

    • 用途:显示纯文本内容,支持样式自定义(字体、颜色、对齐等)

    • 示例

      Text("Hello HarmonyOS")
        .fontSize(20)
        .fontColor(Color.Black)
        .textAlign(TextAlign.Center)
      
  2. TextInput

    • 用途:单行文本输入框,支持密码模式、占位符提示等

    • 关键属性

      TextInput({ placeholder: "请输入内容" })
        .type(InputType.Password)  // 密码输入
        .placeholderColor(Color.Gray)
      
  3. TextArea

    • 用途:多行文本输入框,支持自动换行和字数统计

    • 示例

      TextArea()
        .height(100)
        .maxLength(200)  // 限制输入长度
        .showCounter(true)  // 显示字数统计
      

🕹️ 二、交互类组件

  1. Button

    • 用途:触发操作的按钮,支持样式定制和点击事件

    • 示例

      Button("提交", { type: ButtonType.Normal })
        .backgroundColor(0x317aff)
        .onClick(() => { /* 处理逻辑 */ })
      
  2. Toggle

    • 用途:开关控件,支持勾选框/开关样式切换

    • 示例

      Toggle({ type: ToggleType.Switch, isOn: true })
        .onChange((isOn) => { console.log(`开关状态: ${isOn}`) })
      
  3. CheckBox & Radio

    • 用途:多选框(CheckBox)和单选框(Radio),用于选项选择

    • 示例

      // 多选框
      CheckBox({ name: 'option1' }).select(true)
      // 单选框
      Radio({ value: 'A', group: 'choices' }).checked(true)
      

🧩 三、布局容器组件

  1. Column & Row

    • 用途:纵向(Column)和横向(Row)排列子组件,支持主轴/交叉轴对齐

    • 示例

      Column() {
        Text("顶部文本")
        Button("底部按钮")
      }.justifyContent(FlexAlign.SpaceBetween)  // 两端对齐
      
  2. List

    • 用途:滚动列表容器,适用于长列表数据展示(需配合 ListItem 使用)

    • 示例

      List() {
        ForEach(dataArray, (item) => {
          ListItem() { Text(item.name) }
        })
      }.scrollBar(BarState.Off)  // 隐藏滚动条
      
  3. Swiper

    • 用途:轮播图容器,支持自动播放和循环切换

    • 示例

      Swiper() {
        ForEach(bannerList, (item) => {
          Image(item.imageSrc).objectFit(ImageFit.Contain)
        })
      }.autoPlay(true).loop(true)
      
  4. Scroll

    • 用途:可滚动容器,当内容超出可视区域时支持滚动

    • 注意:嵌套 List 时需明确指定子组件宽高以优化性能。


🖼️ 四、媒体与图像组件

  1. Image

    • 用途:显示本地或网络图片,支持缩放和裁剪模式

    • 示例

      Image($r('app.media.logo'))  // 加载资源图片
        .objectFit(ImageFit.Cover)  // 覆盖整个容器
        .borderRadius(10)  // 圆角
      

📊 五、高级容器组件

  1. Grid & GridItem

    • 用途:网格布局(Grid)及其子项容器(GridItem),用于瀑布流展示

    • 限制GridItem 必须作为 Grid 的子组件使用。

  2. WaterFlow & FlowItem

    • 用途:瀑布流布局(WaterFlow)及其子项(FlowItem),适用于不规则内容排列

⚙️ 六、自定义组件

通过 @Component 装饰器创建可复用的 UI 单元,组合系统组件实现业务逻辑

@Component
struct CustomCard {
  build() {
    Column() {
      Text("自定义卡片标题").fontSize(18)
      Button("详情").onClick(() => { /* 跳转逻辑 */ })
    }
  }
}

💎 核心组件总结表

类别 组件 核心功能 典型场景
文本输入 TextTextInput 显示/编辑文本内容 表单输入、内容展示
交互控制 ButtonToggle 触发操作、状态切换 提交表单、开关设置
布局容器 ColumnList 排列子组件、支持滚动 列表页、详情页布局
媒体展示 ImageSwiper 图片/轮播图展示 Banner 广告、商品图集
高级容器 GridWaterFlow 网格/瀑布流布局 图片墙、商品分类页

更多组件用法详见 HarmonyOS 官方文档 或参考 ArkUI 组件指南。实际开发中可组合使用(如 List + ListItem 构建动态列表,Swiper + Image 实现轮播图)

HarmonyOS5鸿蒙开发常用装饰器

作者 大胖子101
2025年6月5日 17:14

 一、UI 组件装饰器

  1. @Component

    • 标记 struct 为自定义组件,必须实现 build() 方法描述 UI 结构

    • 示例:

      @Component
      struct MyButton {
        build() {
          Button('Click')
        }
      }
      
  2. @Entry

    • 声明页面入口组件,每个页面仅允许一个 @Entry 装饰的组件

    • 常与 @Component 组合使用:

      @Entry
      @Component
      struct HomePage { ... }
      
  3. @Reusable

    • 使组件具备复用能力,优化频繁创建/销毁场景的性能(如列表滑动、条件渲染)

    • 限制:仅能在同一父组件下复用实例。


🔄 二、状态管理装饰器

1. 组件内状态

  • @State
    组件内部私有状态变量,变化触发 UI 刷新

    @State count: number = 0;
    
  • @Prop
    单向同步父组件的参数,子组件不可修改

    @Prop title: string; // 父组件传递的只读数据
    
  • @Link
    父子组件双向数据绑定,修改同步至父组件

2. 跨层级状态

  • @Provide 和 @Consume
    实现祖先与后代组件的双向同步,无需逐层传递参数

    // 祖先组件
    @Provide('theme') theme: string = 'dark';
    
    // 后代组件
    @Consume('theme') theme: string;
    
  • @Observed 和 @ObjectLink
    用于嵌套对象或数组属性的双向同步 @Observed 装饰类,@ObjectLink 装饰子组件中接收该类的变量。

3. 应用级状态

  • @LocalStorageProp / @LocalStorageLink
    与页面级 LocalStorage 中的属性建立单向/双向同步

  • @StorageProp / @StorageLink
    与全局 AppStorage 中的属性建立单向/双向同步


🛠️ 三、UI 复用与样式装饰器

  1. @Builder

    • 封装可复用的 UI 描述块,支持引用状态变量($$ 语法)

      @Builder customButton($$: { text: string }) {
        Button($$.text).width(100)
      }
      
  2. @BuilderParam

    • 作为占位符,接收外部传入的 @Builder 方法

      @BuilderParam action: () => void;
      
  3. @Styles 和 @Extend

    • @Styles:封装多条样式属性,支持全局或组件内复用

      @Styles fancy() {
        .width(200).backgroundColor(Color.Red)
      }
      
    • @Extend:扩展原生组件样式,支持参数传递

      @Extend(Text) function textStyle(size: number) {
        .fontSize(size).fontColor('#333')
      }
      
  4. stateStyles

    • 依据组件内部状态(如按下、禁用)动态切换样式

⚙️ 四、辅助功能装饰器

  1. @Watch

    • 监听状态变量变化,触发回调逻辑

      @State @Watch('onCountChange') count: number = 0;
      onCountChange() { ... }
      
  2. @Track

    • 标记类对象的属性,仅当该属性变化时触发 UI 更新(优化性能)

      class User {
        @Track name: string; // 仅 name 变化时更新 UI
      }
      

💎 关键总结

类别 装饰器 核心用途
组件定义 @Entry@Component 标记入口和自定义组件
状态管理 @State@Prop@Link 组件内状态与父子同步
跨组件通信 @Provide/@Consume 祖先-后代数据共享
UI 复用 @Builder@BuilderParam 封装与动态注入 UI 逻辑
样式扩展 @Styles@Extend 样式复用与原生组件扩展
性能优化 @Reusable@Track 组件复用与精准更新

更多实践案例可参考:HarmonyOS 开发文档 developer.huawei.com/consumer/cn… 。实际开发中需根据场景组合使用,例如 @State + @Watch 实现状态监听,@Provide + @Consume 替代深层参数传递。

探秘鸿蒙 HarmonyOS NEXT:实战用 CodeGenie 构建鸿蒙应用页面

作者 lucky志
2025年6月5日 08:50

在开发鸿蒙应用时,你是否也曾为一个页面的布局反复调整?是否还在为查 API、写模板代码而浪费大量时间?今天带大家实战体验一下鸿蒙官方的 AI 编程助手——CodeGenie(代码精灵) ,如何从 0 到 1 快速构建一个完整页面!帮助入门开发者快速上手,了解如何通过 AI 辅助完成页面开发的整个流程。

本文将带你用自然语言和 CodeGenie 聊聊天,就能自动生成一个完整的新闻列表页面,体验一下CodeGenie的高效性!


目标

我们打算做一个新闻类 App 的首页页面,包含以下内容:

  • 一个顶部标题栏
  • 一个搜索框,支持输入关键词
  • 一个可滑动的新闻卡片列表(带标题、图片、描述)
  • 下拉刷新功能

这个页面是大部分内容类应用的常见模板,CodeGenie 可以轻松帮我们构建出来。


环境准备

在开始之前,请确保你已经准备好以下开发环境:

项目 状态
DevEco Studio 已安装(建议使用 4.0+ 版本)
CodeGenie 插件 已启用(可在设置中打开)
项目类型 ArkTS 应用项目

创建一个新的项目(例如叫 NewsApp),并进入主页面代码文件(例如 NewsListPage.ets)。


第一步:打开 CodeGenie 聊天界面,输入需求描述

打开 DevEco Studio,点击工具栏右上角的 CodeGenie 图标,进入聊天界面,输入我们的页面需求:

请帮我用 ArkTS 创建一个完整的新闻列表页面demo,包含标题栏、搜索框、可滑动的新闻卡片列表(含标题、图片、描述),并支持下拉刷新。

几秒钟后,CodeGenie 就会自动生成一个完整的页面代码。如果没有达到预期效果,那么可能是描述的不优雅,我们可以换个问法,需求描述很重要!


第二步:查看并理解生成的代码

image.png

CodeGenie 给出的页面代码结构清晰,具备完整的 UI 和逻辑,例如:

@Entry
@Component
struct NewsListPage {
  @State newsList: Array<NewsItem> = [
    { title: '新闻标题1', image: $r('app.media.news1'), description: '新闻描述1' },
    { title: '新闻标题2', image: $r('app.media.news2'), description: '新闻描述2' },
    { title: '新闻标题3', image: $r('app.media.news3'), description: '新闻描述3' },
    // 更多新闻...
  ]

  build() {
    Column() {
      // 标题栏
      NavigationBar() {
        Text('新闻列表')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
      }

      // 搜索框
      SearchBar()

      // 可滑动的新闻卡片列表
      List() {
        ForEach(this.newsList, (newsItem: NewsItem) => {
          NewsCard(newsItem)
        })
      }
      .onScrollIndex((start: number) => {
        console.info(`开始索引: ${start}`)
      })
      .listDirection(Axis.Vertical)
      .edgeEffect(EdgeEffect.Spring)
      .scrollBar(BarState.Off)
      .friction(0.6)
      .onScroll((scrollOffset: number) => {
        console.info(`滚动偏移量: ${scrollOffset}`)
      })
      .onScrollEdge((edge: Edge) => {
        console.info(`滚动到边缘: ${edge}`)
      })
      .onScrollStop(() => {
        console.info('滚动停止')
      })
      .enableScrollInteraction(true)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)
  }
}

interface NewsItem {
  title: string
  image: Resource
  description: string
}

@Component
struct NavigationBar {
  build() {
    Row() {
      Text('新闻列表')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
    }
    .height(50)
    .backgroundColor(Color.Blue)
    .justifyContent(FlexAlign.Center)
    .alignItems(VerticalAlign.Center)
  }
}

@Component
struct SearchBar {
  build() {
    Row() {
      Image($r('app.media.search'))
        .width(20)
        .height(20)
        .margin({ left: 10 })

      TextInput()
        .placeholder('搜索新闻...')
        .width('80%')
        .height(40)
        .backgroundColor(Color.White)
        .fontSize(16)
        .fontColor(Color.Black)
        .borderRadius(20)
        .margin({ left: 10 })
    }
    .height(40)
    .backgroundColor(Color.White)
    .justifyContent(FlexAlign.Center)
    .alignItems(VerticalAlign.Center)
    .padding(10)
  }
}

@Component
struct NewsCard {
  @Prop newsItem: NewsItem

  build() {
    Column() {
      Image(this.newsItem.image)
        .width('100%')
        .height(200)
        .objectFit(ImageFit.Cover)

      Text(this.newsItem.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 10 })

      Text(this.newsItem.description)
        .fontSize(16)
        .margin({ top: 5 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)
    .padding(10)
    .borderRadius(10)
    .margin({ bottom: 10 })
  }
}

以下是对代码的解释:
在这个示例中:

  • 使用@Entry和@Component装饰器定义了一个名为NewsListPage的组件,作为页面的入口。
  • 使用@State装饰器定义了一个newsList状态,用于存储新闻列表数据。
  • 使用Column布局组件来组织页面内容,包括标题栏、搜索框和新闻卡片列表。
  • 使用NavigationBar组件来显示页面的标题。
  • 使用SearchBar组件来显示搜索框。
  • 使用List组件来创建一个可滑动的新闻卡片列表,并使用ForEach循环渲染每个新闻卡片。
  • 使用NewsCard组件来显示每个新闻卡片的内容,包括标题、图片和描述。
  • 使用各种属性和事件处理函数来控制组件的样式和行为。

CodeGenie 回答的还是很完善的,非常的棒!


第三步:调整图片资源或组件API代码

CodeGenie 帮我们生成的代码,还是有一些小瑕疵的,需要我们手动去调整,例如一些图片资源或者组件的API方法不支持等等

生成的代码中默认使用了本地图片 app.media.news1。你可以按如下方式替换为真实的网络图片或资源文件:

✅ 方法一:放入本地资源目录

将图片放到 resources/base/media/ 目录中,并确保路径正确,例如:

image: '/resources/base/media/news1.png'

✅ 方法二:使用网络图片链接

image: 'https://example.com/news-image1.jpg'

第四步:运行并预览页面

在 DevEco Studio 中点击“预览”或运行到模拟器,页面应当呈现如下结构:

  • 顶部:“新闻列表”标题
  • 中部:可输入的搜索框
  • 下方:可滚动的新闻卡片列表,每项展示图片、标题与描述
  • 下拉操作:触发刷新逻辑

恭喜你,短短几分钟就完成了一个页面的搭建!


拓展功能建议

有了基础页面,你可以继续和 CodeGenie 聊天,添加新功能。例如:

想法 示例指令
增加分页加载 “为新闻列表添加分页加载逻辑,滑到底部时加载更多”
优化样式 “请让卡片增加圆角和阴影,更加卡片化”
跳转到详情页 “点击新闻项跳转到详情页,传递新闻数据”
网络请求替代模拟数据 “把 newsList 替换为从远程接口请求的新闻列表”

CodeGenie 会像开发搭档一样,持续帮你完善页面!


总结:AI + 开发 = 高效创作

通过这次实战我们可以看到,CodeGenie 不仅可以理解自然语言,还能真正参与到项目搭建的每一步

  • 快速生成结构清晰、功能完整的页面
  • 理解鸿蒙 ArkTS 的开发模式与组件体系
  • 帮助开发者省去大量重复劳动,专注业务逻辑
  • 对初学者友好,对资深开发者更是降本增效

如果你还没有试过 CodeGenie,现在就打开 DevEco Studio,动动嘴皮子或者敲几行字,就能把一个页面做出来!

我们又上架了一个鸿蒙项目-止欲

作者 万少
2025年6月4日 16:21

我们又上架了一个鸿蒙项目-止欲

止欲介绍

止欲是一款休闲类的鸿蒙元服务,希望可以通过冥想让繁杂的生活慢下来、静下来。

image-20250604154144296

《止欲》从立项到上架总过程差不多两个月,主要都是我们青蓝的小伙伴在工作止欲抽空完成的,已经实属不易了,我们主要开发者都是 00 后,最年轻的开发者也是才 19 岁。

立项时间是:2025-04-08

image-20250604154712749

上架时间是:2025-06-03

image-20250604154654173

止欲同时也是我们青蓝逐码组织上架的第三个作品了,每个作品都是由初入职场、甚至大学还没有毕业的小伙伴高度参与!

image-20250604161153167

git 日志一览

image-20250604155808917

项目技术细节

项目架构

Serenity/Application/
├── entry/                          # 主模块
│   ├── src/main/
│   │   ├── ets/                    # TypeScript源码
│   │   │   ├── entryability/       # 应用入口能力
│   │   │   ├── entryformability/   # 服务卡片能力
│   │   │   ├── pages/              # 页面文件
│   │   │   ├── view/               # UI组件
│   │   │   ├── utils/              # 工具类
│   │   │   ├── model/              # 数据模型
│   │   │   ├── const/              # 常量定义
│   │   │   └── navigationStack/    # 导航栈管理
│   │   └── resources/              # 资源文件
│   └── module.json5                # 模块配置
├── EntryCard/                      # 服务卡片模块
├── AppScope/                       # 应用级配置
└── oh-package.json5               # 依赖管理

技术栈

  • 开发语言: ArkTS (TypeScript)
  • UI 框架: ArkUI
  • 构建工具: Hvigor
  • 包管理: ohpm

核心开发套件 (Kit)

本项目使用了多个 HarmonyOS 官方开发套件:

套件名称 用途 主要 API
@kit.ArkUI UI 框架和导航 AtomicServiceNavigation, window
@kit.BasicServicesKit 基础服务 BusinessError, request
@kit.MediaLibraryKit 媒体库访问 photoAccessHelper
@kit.CoreFileKit 文件操作 fileIo
@kit.ImageKit 图像处理 image.createImageSource
@kit.PerformanceAnalysisKit 性能分析 hilog
@kit.AbilityKit 应用能力 UIAbility, abilityAccessCtrl

开发环境要求

  • HarmonyOS SDK: 5.0.1(13) 或更高版本
  • DevEco Studio: 5.0 或更高版本
  • 编译目标: HarmonyOS

开发细节

开始立项

image-20250604160132698

分析如何选型

image-20250604160239603

image-20250604160259889

暴躁起来了

image-20250604160336458

成功上架

image-20250604160436312

后续计划

  1. 接入登录
  2. 接入端云一体
  3. 增加趣味性功能
  4. 代码开源-分享教程

总结

关于青蓝逐码组织

如果你兴趣想要了解更多的鸿蒙应用开发细节和最新资讯甚至你想要做出一款属于自己的应用!欢迎在评论区留言或者私信或者看我个人信息,可以加入技术交流群。

image-20250604160620575

Harmony OS5—封装一个日志工具

作者 半路下车
2025年6月3日 10:33

注:这是一个功能完善的日志工具类封装,支持不同日志级别、日志存储和日志文件管理功能

一、设计思路

1. 分层架构

-   **控制层**:LogLevel枚举控制日志过滤
-   **输出层**:双通道输出(控制台+文件)
-   **管理层**:文件轮转和清理机制

2. SOLID原则应用

-   单一职责:每个方法只做一件事(如formatMessage只负责格式化)
-   开闭原则:通过LogLevel枚举方便扩展新级别
-   依赖倒置:依赖抽象的hilog接口而非具体实现

3. 生命周期管理

-   显式初始化(initialize)
-   资源自动回收(文件流自动关闭)
-   异常安全(所有IO操作try-catch包裹)

二、关键实现

1. 文件日志核心逻辑

private async writeToFile(message: string) {
  // 1. 大小检查
  const stat = await fs.stat(this.currentLogFile); 
  // 2. 触发轮转条件
  if(stat.size > MAX_LOG_FILE_SIZE) {
    await this.rotateLogFile(); // 3. 文件轮转
  }
  // 4. 写入新日志
  await fs.appendFile(this.currentLogFile, message + '\n');
}

2. 日志轮转算法

private async rotateLogFile() {
  // 1. 生成带时间戳的新文件名
  const newFile = `${this.logDir}/app_${Date.now()}.log`; 
  // 2. 复制当前日志
  await fs.copyFile(this.currentLogFile, newFile);
  // 3. 清空当前文件
  await fs.truncate(this.currentLogFile); 
  // 4. 触发清理
  this.cleanOldLogs(); 
}

3. 线程安全设计

  • 所有文件操作使用async/await
  • 写操作通过appendFile保证原子性
  • 使用单例模式避免多实例竞争

三、优化策略

1. 性能优化

-   **批量写入**:积累多条日志后批量写入(未展示,可扩展)
-   **内存缓存**:使用LRU缓存最近日志(适合高频日志场景)
-   **空闲写入**:通过IdleHandler在系统空闲时执行IO

2. 可观测性增强

// 在initialize()中添加:
this.d(TAG, `Log system config: 
  Level=${LogLevel[this.logLevel]}, 
  MaxSize=${MAX_LOG_FILE_SIZE/1024}KB,
  MaxFiles=${MAX_LOG_FILES}`);

3. 生产环境建议扩展

  • 日志压缩:对历史日志进行gzip压缩
  • 加密存储:敏感日志AES加密
  • 远程上报:异常日志自动上传到服务器
  • 日志分析:内置关键词过滤/统计功能

4. 调试模式优化

// 开发阶段增加彩色日志
private getColor(level: string): string {
  const colors = {
    DEBUG: '\x1b[36m', // 青色
    ERROR: '\x1b[31m'  // 红色
  };
  return colors[level] || '';
}

四、设计模式应用

1. 单例模式:全局唯一日志实例

export function getLogger(context?: common.UIAbilityContext): Logger {
  if (!globalLogger && context) {
    globalLogger = new Logger(context);
  }
  return globalLogger!;
}

2. 策略模式:不同级别采用不同处理策略

e(tag: string, message: string, error?: Error) {
  if (this.logLevel > LogLevel.ERROR) return;
  // 错误级别特殊处理:包含堆栈
  const fullMsg = error ? `${message}\n${error.stack}` : message;
  this.writeToFile(this.formatMessage('ERROR', tag, fullMsg));
}

3. 观察者模式(可扩展):注册日志监听器

interface LogListener {
  onLog(level: LogLevel, message: string): void;
}
// 在Logger类中添加addListener方法

五、异常处理体系

1. 分级处理策略

try {
  // 主要逻辑
} catch (ioError) {
  // 1. 本地写入失败转存内存缓存
} catch (serializeError) {
  // 2. 数据序列化失败降级处理
} finally {
  // 保证资源释放
}

2. 错误恢复机制

  • 文件写入失败时自动重试3次
  • 最终失败后转存到临时文件
  • 下次初始化时尝试恢复

六、完整代码

如下:

// Logger.ets
import fs from '@ohos.file.fs';
import hilog from '@ohos.hilog';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import common from '@ohos.app.ability.common';

const TAG = 'AppLogger';
const MAX_LOG_FILE_SIZE = 1024 * 1024 * 2; // 2MB
const MAX_LOG_FILES = 5;

enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3,
  NONE = 4
}

class Logger {
  private context: common.UIAbilityContext;
  private logDir: string = '';
  private currentLogFile: string = '';
  private logLevel: LogLevel = LogLevel.DEBUG;
  private isInitialized: boolean = false;

  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }

  /**
   * 初始化日志系统
   * @param level 日志级别
   * @param logDir 日志存储目录(可选)
   */
  async initialize(level: LogLevel = LogLevel.DEBUG, logDir?: string): Promise<void> {
    if (this.isInitialized) return;

    this.logLevel = level;
    
    // 设置日志目录
    if (logDir) {
      this.logDir = logDir;
    } else {
      this.logDir = this.context.filesDir + '/logs';
    }

    // 创建日志目录
    try {
      await fs.ensureDir(this.logDir);
      this.currentLogFile = `${this.logDir}/app_${this.getCurrentDate()}.log`;
      this.isInitialized = true;
      
      // 检查并清理旧日志
      this.cleanOldLogs();
      
      this.i(TAG, 'Logger initialized successfully');
    } catch (error) {
      hilog.error(TAG, 'Failed to initialize logger: %{public}s', error.message);
    }
  }

  /**
   * 设置日志级别
   * @param level 日志级别
   */
  setLogLevel(level: LogLevel): void {
    this.logLevel = level;
  }

  /**
   * 调试日志
   * @param tag 日志标签
   * @param message 日志内容
   * @param data 附加数据(可选)
   */
  d(tag: string, message: string, data?: object): void {
    if (this.logLevel > LogLevel.DEBUG) return;
    const logMsg = this.formatMessage('DEBUG', tag, message, data);
    hilog.debug(tag, logMsg);
    this.writeToFile(logMsg);
  }

  /**
   * 信息日志
   * @param tag 日志标签
   * @param message 日志内容
   * @param data 附加数据(可选)
   */
  i(tag: string, message: string, data?: object): void {
    if (this.logLevel > LogLevel.INFO) return;
    const logMsg = this.formatMessage('INFO', tag, message, data);
    hilog.info(tag, logMsg);
    this.writeToFile(logMsg);
  }

  /**
   * 警告日志
   * @param tag 日志标签
   * @param message 日志内容
   * @param data 附加数据(可选)
   */
  w(tag: string, message: string, data?: object): void {
    if (this.logLevel > LogLevel.WARN) return;
    const logMsg = this.formatMessage('WARN', tag, message, data);
    hilog.warn(tag, logMsg);
    this.writeToFile(logMsg);
  }

  /**
   * 错误日志
   * @param tag 日志标签
   * @param message 日志内容
   * @param error 错误对象(可选)
   * @param data 附加数据(可选)
   */
  e(tag: string, message: string, error?: Error, data?: object): void {
    if (this.logLevel > LogLevel.ERROR) return;
    const fullMessage = error ? `${message}: ${error.message}\n${error.stack}` : message;
    const logMsg = this.formatMessage('ERROR', tag, fullMessage, data);
    hilog.error(tag, logMsg);
    this.writeToFile(logMsg);
  }

  /**
   * 获取所有日志文件
   */
  async getLogFiles(): Promise<Array<string>> {
    try {
      const files = await fs.listFile(this.logDir);
      return files.filter(file => file.endsWith('.log'))
                 .sort()
                 .reverse()
                 .map(file => `${this.logDir}/${file}`);
    } catch (error) {
      this.e(TAG, 'Failed to get log files', error);
      return [];
    }
  }

  /**
   * 清理日志文件
   */
  async clearLogs(): Promise<void> {
    try {
      const files = await this.getLogFiles();
      for (const file of files) {
        await fs.unlink(file);
      }
      this.i(TAG, 'All log files cleared');
    } catch (error) {
      this.e(TAG, 'Failed to clear log files', error);
    }
  }

  // 格式化日志消息
  private formatMessage(level: string, tag: string, message: string, data?: object): string {
    const timestamp = this.getCurrentDateTime();
    let logMsg = `[${timestamp}] [${level}] [${tag}] ${message}`;
    
    if (data) {
      try {
        logMsg += ` | Data: ${JSON.stringify(data)}`;
      } catch (error) {
        logMsg += ` | Data: [Unable to stringify]`;
      }
    }
    
    return logMsg;
  }

  // 写入日志文件
  private async writeToFile(message: string): Promise<void> {
    if (!this.isInitialized || !this.currentLogFile) return;

    try {
      // 检查文件大小
      const fileExists = await fs.access(this.currentLogFile);
      if (fileExists) {
        const stat = await fs.stat(this.currentLogFile);
        if (stat.size > MAX_LOG_FILE_SIZE) {
          this.rotateLogFile();
        }
      }

      // 写入日志
      await fs.appendFile(this.currentLogFile, message + '\n');
    } catch (error) {
      hilog.error(TAG, 'Failed to write log to file: %{public}s', error.message);
    }
  }

  // 轮转日志文件
  private async rotateLogFile(): Promise<void> {
    const newFile = `${this.logDir}/app_${this.getCurrentDate()}_${Date.now()}.log`;
    try {
      await fs.copyFile(this.currentLogFile, newFile);
      await fs.truncate(this.currentLogFile);
      this.cleanOldLogs();
    } catch (error) {
      this.e(TAG, 'Failed to rotate log file', error);
    }
  }

  // 清理旧日志
  private async cleanOldLogs(): Promise<void> {
    try {
      const files = await this.getLogFiles();
      if (files.length > MAX_LOG_FILES) {
        for (let i = MAX_LOG_FILES; i < files.length; i++) {
          await fs.unlink(files[i]);
        }
      }
    } catch (error) {
      this.e(TAG, 'Failed to clean old logs', error);
    }
  }

  // 获取当前日期时间
  private getCurrentDateTime(): string {
    const now = new Date();
    return now.toISOString().replace('T', ' ').replace(/\..+/, '');
  }

  // 获取当前日期
  private getCurrentDate(): string {
    return new Date().toISOString().split('T')[0];
  }
}

// 全局单例
let globalLogger: Logger | null = null;

export function getLogger(context?: common.UIAbilityContext): Logger {
  if (!globalLogger) {
    if (!context) {
      throw new Error('Context is required for first time logger initialization');
    }
    globalLogger = new Logger(context);
  }
  return globalLogger;
}

export { LogLevel };

七、使用实例

// 在Ability中初始化
import { getLogger, LogLevel } from './Logger';

@Entry
@Component
struct MyComponent {
  private logger = getLogger(getContext(this) as common.UIAbilityContext);

  aboutToAppear() {
    // 初始化日志系统,设置日志级别为INFO
    this.logger.initialize(LogLevel.INFO);
  }

  build() {
    Column() {
      Button('Test Logging')
        .onClick(() => {
          // 记录不同级别的日志
          this.logger.d('MyTag', 'This is a debug message', { key: 'value' });
          this.logger.i('MyTag', 'This is an info message');
          this.logger.w('MyTag', 'This is a warning message');
          this.logger.e('MyTag', 'This is an error message', new Error('Something went wrong'));
        })
    }
  }
}

八、配置说明

module.json5中添加所需权限:

{
  "requestPermissions": [
    {
      "name": "ohos.permission.READ_MEDIA",
      "reason": "需要读取日志文件"
    },
    {
      "name": "ohos.permission.WRITE_MEDIA",
      "reason": "需要写入日志文件"
    }
  ]
}

HarmonyOS Next 弹窗系列教程(1)

作者 万少
2025年6月3日 10:24

HarmonyOS Next 弹窗系列教程(1)

弹窗的介绍

弹窗概述

弹窗一般指打开应用时自动弹出或者用户行为操作时弹出的 UI 界面,用于短时间内展示用户需关注的信息或待处理的操作。

比如以下这些效果:

image-20241226195728205

弹窗的种类

根据用户交互操作场景,弹窗可分为模态弹窗非模态弹窗两种类型,其区别在于用户是否必须对其做出响应。

  • 模态弹窗: 为强交互形式,会中断用户当前的操作流程,要求用户必须做出响应才能继续其他操作,通常用于需要向用户传达重要信息的场景。
  • 非模态弹窗: 为弱交互形式,不会影响用户当前操作行为,用户可以不对其进行回应,通常都有时间限制,出现一段时间后会自动消失。一般用于告诉用户信息内容外还需要用户进行功能操作的场景。

模态弹窗

为强交互形式,会中断用户当前的操作流程,要求用户必须做出响应才能继续其他操作,通常用于需要向用户传达重要信息的场景。image-20241226195844696

非模态弹窗

PixPin_2024-12-26_20-02-31

弹窗的分类

HarmonyOS Next 中将弹窗进行了如下这些分类

弹窗名称 应用场景
弹出框(Dialog) 当需要展示用户当前需要或必须关注的信息内容或操作时,例如二次退出应用等,应优先考虑使用此弹出框。
菜单控制(Menu) 当需要给指定的组件绑定用户可执行的操作时,例如长按图标展示操作选项等,应优先考虑使用此弹窗。
气泡提示(Popup) 当需要给指定的组件提示时,例如点击一个问号图标弹出一段帮助提示等,应优先考虑使用此弹窗。
绑定模态页面(bindContentCover/bindSheet) 当需要新的界面覆盖在旧的界面上,且旧的界面不消失的一种转场方式时,例如缩略图片点击后查看大图等,应优先考虑使用此弹窗。
即时反馈(Toast) 当需要在一个小的窗口中提供用户当前操作的简单反馈时,例如提示文件保存成功等,应用优先考虑使用此弹窗。
设置浮层(OverlayManager) 当需要完全自定义内容、行为、样式时,可以使用浮层将 UI 展示在页面之上,例如音乐/语音播放悬浮球/胶囊等,应优先考虑使用此弹窗。

弹出框概述

用户需在模态弹出框内完成相关交互任务之后,才能退出模态模式。弹出框可以不与任何组件绑定,其内容通常由多种组件组成,如文本、列表、输入框、图片等,以实现布局。ArkUI 当前提供了固定样式自定义两类弹出框组件。

  • 自定义弹出框: 开发者需要根据使用场景,传入自定义组件填充在弹出框中实现自定义的弹出框内容。主要包括基础自定义弹出框 (CustomDialog)、不依赖 UI 组件的自定义弹出框 (openCustomDialog)。
  • 固定样式弹出框: 开发者可使用固定样式弹出框,指定需要显示的文本内容和按钮操作,完成简单的交互效果。主要包括警告弹窗 (AlertDialog)、列表选择弹窗 (ActionSheet)、选择器弹窗 (PickerDialog)、对话框 (showDialog)、操作菜单 (showActionMenu)。

openCustomDialog 使用教程

我们分成 3 步来使用openCustomDialog弹窗

  1. 创建要显示的内容
  2. 声明 UiContext
  3. 声明 promptAction
  4. 创建要弹窗显示的节点
  5. 显示弹窗

创建要显示的内容

使用 @Builder 修饰的不是普通的函数,而是负责渲染节点的内容构造函数。

@Builder
function buildText() {
  Column() {
    Text("自定义标题")
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      .margin({ bottom: 36 })
  }.backgroundColor('#FFF0F0F0')
}

声明 UiContext

通过getUIContext()接口获取 UI 上下文实例UIContext对象

let uiContext = this.getUIContext();

声明 promptAction

promptAction用于创建并显示文本提示框、对话框和操作菜单。

let promptAction = uiContext.getPromptAction();

创建要弹窗显示的节点

omponentContent表示组件内容的实体封装,其对象支持在非 UI 组件中创建与传递,便于开发者对弹窗类组件进行解耦封装

wrapBuilder 是一个全局函数,功能是用来封装全局@Builder,方便@Builder 传递和调用

let contentNode = new ComponentContent(uiContext, wrapBuilder(buildText));

显示弹窗

最后,调用的 promptAction 的 openCustomDialog 方法,传入 contentNode 便可实现弹窗效果

promptAction.openCustomDialog(contentNode);

PixPin_2024-12-26_21-05-37

完整代码

import { ComponentContent } from '@kit.ArkUI';


@Builder
function buildText() {
  Column() {
    Text("自定义标题")
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      .margin({ bottom: 36 })
  }.backgroundColor('#FFF0F0F0')
}

@Entry
@Component
struct Index {
  build() {
    Row() {
      Column() {
        Button("click me")
          .onClick(() => {
            let uiContext = this.getUIContext();
            let promptAction = uiContext.getPromptAction();
            let contentNode = new ComponentContent(uiContext, wrapBuilder(buildText));
            promptAction.openCustomDialog(contentNode);
          })
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }
}

参考链接

  1. wrapBuilder
  2. ComponentContent
  3. getUIContext
  4. getPromptAction
❌
❌