鸿蒙基础入门与高频知识点梳理

介绍鸿蒙高频知识点,持续更新中

一、鸿蒙代码结构

├──entry/src/main/ets        // 代码区
│  ├──common
│  │  └──Constant.ets        // 常量类
│  ├──entryability            
│  │  └──EntryAbility.ts     // 程序入口类
│  ├──pages
│  │  ├──MainPage.ets        // 主页入口文件
│  │  └──WebPage.ets         // 抽奖页入口文件
│  └──viewmodel                          
│     └──NavigatorModel.ets  // 导航model
├──entry/src/main/resources  
│  ├──base
│  │  ├──element             // 尺寸、颜色、文字等资源文件存放位置
│  │  ├──media               // 媒体资源存放位置
│  │  └──profile             // 页面配置文件存放位置
│  ├──en_US                  // 国际化英文
│  ├──rawfile                // 本地html代码存放位置 
│  └──zh_CN                  // 国际化中文
└──HttpServerOfWeb           // 服务端代码

二、配置文件

1、module.json5

用于配置UIAbility页面模块信息。

位置:/entry/src/main/module.json5

{
  "module": {
    "name": "entry",//当前Module的名称
    "type": "entry",//Module的类型(entry:应用的主模块, feature:应用的动态特性模块)
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",//标识当前Module的入口UIAbility名称或者ExtensionAbility名称。
    "deviceTypes": [//运行设备
      "phone",
      "tablet"
    ],
    "deliveryWithInstall": true,//标识当前Module是否在用户主动安装的时候安装,表示该Module对应的HAP是否跟随应用一起安装。
    "installationFree": false,//是否支持免安装特性
    "pages": "$profile:main_pages",//页面配置文件json
    "abilities": [//UIAbility的配置信息
      {
        "name": "EntryAbility",//当前UIAbility组件的名称,该名称在整个应用要唯一
        "srcEntry": "./ets/entryability/EntryAbility.ts",//入口UIAbility的路径
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",//app图标
        "label": "$string:EntryAbility_label",//app
        "startWindowIcon": "$media:icon",//当前UIAbility组件启动页面图标(暂时没发现有啥用,与上面保持一致即可)
        "startWindowBackground": "$color:start_window_background",
        "exported": true,//当前UIAbility组件是否可以被其他应用调用
        "skills": [//能够接收的Want的特征集
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ]
  }
}

2、main_pages.json

页面列表json,对应上面module.json5的pages字段。

位置:/entry/src/main/resources/base/profile/main_pages.json

{
  "src": [
    "pages/SecondPage",
    "pages/SimpleVideoPlay",
    "pages/Index"
  ]
}

3、build-profile.json5

定制HAP多目标构建产物。

位置:entry/build-profile.json5

{
  "apiType": 'stageMode',
  "buildOption": {
  },
  "targets": [
    {
      "name": "default",
      "runtimeOS": "HarmonyOS"
    },
    {
      "name": "ohosTest",
    }
  ]
}

例如,以ArkTS Stage模型为例,定义一个免费版和付费版,示例如下。参考资料

{
  "apiType": 'stageMode',
  "buildOption": {
  },
  "targets": [
    {
      "name": "default"  //默认target名称default,未定义deviceType,默认支持config.json或module.json5中定义的设备类型
    },
    {
      "name": "free", //免费版target名称
      "config": {
        "deviceType": [  //定义free支持的设备类型为Tablet
          "tablet"
        ]
      }
    },
    {
      "name": "pay",//付费版target名称
      "config": {
        "deviceType": [  //定义pay支持的设备类型为Tablet
          "tablet"
        ]
      }
    }
  ]
}

4、oh-package.json5

描述项目基础信息

位置:entry/oh-package.json5

{
  "name": "entry",
  "version": "1.0.0",
  "description": "Please describe the basic information.",
  "main": "",
  "author": "",
  "license": "",
  "dependencies": {}
}

三、组件

1、Image

image-20231123184752283
  • 网络图片

需要在module.json5 文件中添加网络访问权限

"module": {
  "requestPermissions": [
    {"name": "ohos.permission.INTERNET"}
  ]
}
Image('https://gitcode.net/liuxingyuzaixian/csdn_img/-/raw/main/pictures/2023/11/17_10_51_42_image-20230518181509168.png')
  .width(78)
  .height(78)
  .objectFit(ImageFit.Cover)//设置缩放类型
  • PixelMap 图片

代码生成的色块图片,需要创建PixelMap对象

@State myPixelmap?: PixelMap = null

onPageShow() {
  // 创建PixelMap图片
  const color = new ArrayBuffer(56);
  let opts = { editable: true, pixelFormat: 3, size: { height: 4, width: 6 } }
  image.createPixelMap(color, opts, (err, pixelmap) => {
    if (pixelmap != undefined) {
      this.myPixelmap = pixelmap;
    }
  })
}

// 使用
if (this.myPixelmap != null)
  Image(this.myPixelmap).width(78).height(78)
  • Resource 图片

需要将图片添加到下面目录:/resources/base/media

// 使用
Image($r('app.media.icon')).width(78).height(78)

2、Text

Text($r('app.string.module_desc'))
  .fontSize(50)
  .fontWeight(FontWeight.Bold)
  .fontColor(0xFF0000)
  .maxLines(1)
  .textOverflow({ overflow: TextOverflow.Ellipsis })//单行...
  .decoration({ type: TextDecorationType.Underline, color: Color.Black })//文本装饰线

1、数字默认单位: vp

2、vp :屏幕密度相关像素

3、sp:文本推荐

3、TextInput

单行文本输入

TextInput({ placeholder: "账号" })
  .maxLength(11)
  .type(InputType.Number)
  .onChange((value: string) => {

  })

4、Button

Button("登录", { type: ButtonType.Capsule })
  .onClick(() => {

  })

5、Column、Row

用法语 flutter 一样,仅仅多了space参数方便添加间距

Column({ space: 10 }) {
  Text("asdf")
  Text("asdf")
}.alignItems(HorizontalAlign.Start)

6、List

Screenshot_2023-11-24T134447

如果长度超过容器高度,就会滚动

private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

List({ space: 10 }) {
  ForEach(this.arr, (item: number) => {
    ListItem() {
      Text(`${item}`)
        .width('100%')
        .height(100)
        .fontSize(20)
        .fontColor(Color.White)
        .textAlign(TextAlign.Center)
        .borderRadius(10)
        .backgroundColor(0x007DFF)
    }
  }, item => item)
}
.height('100%')

7、Grid

构建如下不可滚动网格示例

image-20231124114208959
Grid() {
  ForEach(this.arr, (item: string) => {
    GridItem() {
      Text(item)
        .fontSize(16)
        .fontColor(Color.White)
        .backgroundColor(0x007DFF)
        .width('100%')
        .height('100%')
        .textAlign(TextAlign.Center)
    }
  }, item => item)
}
.columnsTemplate('1fr 2fr 1fr 1fr') // 设置当前网格布局列的数量。
.rowsTemplate('1fr 2fr 1fr 1fr') // 设置当前网格布局行的数量。
.columnsGap(10) // 设置列与列的间距。
.rowsGap(10) // 设置行与行的间距。
.height(300)

如果需要垂直方向滚动,则关闭掉rowsTemplate即可,如下:

Grid() {
  ForEach(this.arr, (item: string) => {
    GridItem() {
      Text(item)
        .fontSize(16)
        .fontColor(Color.White)
        .backgroundColor(0x007DFF)
        .width(50)
        .height(50)
        .textAlign(TextAlign.Center)
    }
  }, item => item)
}
.direction(Direction.Ltr)
.columnsTemplate('1fr 2fr 1fr 1fr') // 设置当前网格布局列的数量。
// .rowsTemplate('1fr 2fr 1fr 1fr') // 设置当前网格布局行的数量。
.columnsGap(10) // 设置列与列的间距。
.rowsGap(10) // 设置行与行的间距。
.height(300)
.onScrollIndex((first: number) => {
  console.info('first:' + first)
})

8、Tabs

使用系统自带的样式:不带图片

Kapture 2023-11-24 at 14.04.14
private controller: TabsController = new TabsController()

Column() {
  Tabs({ barPosition: BarPosition.End, controller: this.controller }) {
    TabContent() {
      Column().width('100%').height('100%').backgroundColor(Color.Green)
    }
    .tabBar('首页')

    TabContent() {
      Column().width('100%').height('100%').backgroundColor(Color.Blue)
    }
    .tabBar('我的')
  }
  .barMode(BarMode.Fixed)//页签比较多的时候,可以设置滑动页签Scrollable
  .barWidth('100%') // 设置TabBar宽度
  .barHeight(60) // 设置TabBar高度
  .width('100%') // 设置Tabs组件宽度
  .height('100%') // 设置Tabs组件高度
  .backgroundColor(0xF5F5F5) // 设置Tabs组件背景颜色
  .vertical(false)//注意:这个表示底部的 tab 排列方向(与页面与 tab 的排列方向刚好相反)
}
.width('100%')
.height('100%')

自定义样式:带图片。tabBar组件支持@Builder装饰器修饰的函数

image-20231124141238336
struct Index {
  @State currentIndex: number = 0;
  private tabsController: TabsController = new TabsController();

  @Builder TabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
    Column() {
      Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
        .size({ width: 25, height: 25 })
      Text(title)
        .fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
    }
    .width('100%')
    .height(50)
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.currentIndex = targetIndex;
      this.tabsController.changeIndex(this.currentIndex);
    })
  }

  build() {
    Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
      TabContent() {
        Column().width('100%').height('100%').backgroundColor('#00CB87')
      }
      .tabBar(this.TabBuilder('首页', 0, $r('app.media.icon'), $r('app.media.test')))

      TabContent() {
        Column().width('100%').height('100%').backgroundColor('#007DFF')
      }
      .c(this.TabBuilder('我的', 1, $r('app.media.icon'), $r('app.media.test')))
    }
    .barWidth('100%')
    .barHeight(50)
    .onChange((index: number) => {
      this.currentIndex = index;
    })
  }
}

9、Swiper

Swiper() {
  Image($r('app.media.video_list0'))
    .borderRadius(12).objectFit(ImageFit.Contain)
  Image($r('app.media.video_list0'))
    .borderRadius(12).objectFit(ImageFit.Contain)
  Image($r('app.media.video_list0'))
    .borderRadius(12).objectFit(ImageFit.Contain)
}
.autoPlay(true)

10、Slider进度条

image-20231127105246602
@State slidingProgress: number = 0;

// 样式 1
Slider({
  value: this.slidingProgress,
  style: SliderStyle.InSet,
})
  .onChange((value: number, mode: SliderChangeMode) => {
    this.slidingProgress = Math.floor(value);
  })
// 样式 2
Slider({
  value: this.slidingProgress,
  style: SliderStyle.OutSet,
})
  .onChange((value: number, mode: SliderChangeMode) => {
    this.slidingProgress = Math.floor(value);
  })

11、Video

1、加载本地

需要先在rawfile中添加videoTest.mp4文件

image-20231128165455140
Video({
  src: $rawfile('videoTest.mp4'),
  previewUri: $r('app.media.icon'),
})

效果图如下

Kapture 2023-11-28 at 16.56.29

2、加载网络视频

src换成网络视频即可,并且添加网络权限。

需要注意的是:

1、目前我使用鸿蒙模拟器对网络视频的加载体验并不好

2、网络加载器点击播放的时候需要一段下载时间,最好加上loading

Video({
  src: "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4",
  previewUri: $r('app.media.icon'),
})
  .objectFit(ImageFit.Contain)
image-20231128174112323

3、自定义Video

Kapture 2023-11-29 at 10.44.23
Button("dianji").onClick(()=>{
  router.pushUrl({
    url: 'pages/SimpleVideoPlay',
    params: { source: $rawfile('videoTest.mp4') }//添加视频资源
  });
})

自定义Video页面SimpleVideoPlay.ets

需要额外不上这些icon
ic_back.png
ic_pause.png
ic_play.png
ic_public_play.png

import router from '@ohos.router';
import { VideoPlayer } from './VideoPlayer';

/**
 * 自定义Video页面
 */
@Entry
@Component
struct Play {
  private source: string = (router.getParams() as Record<string, Object>).source as string;
  private startIconResource: Resource = $r('app.media.ic_public_play');
  private backIconResource: Resource = $r('app.media.ic_back');
  @Provide isPlay: boolean = false;
  @Provide isOpacity: boolean = false;
  controller: VideoController = new VideoController();
  @Provide isLoading: boolean = false;
  @Provide progressVal: number = 0;
  @Provide flag: boolean = false;

  aboutToAppear() {
    this.source;
  }

  onPageHide() {
    this.controller.pause();
  }

  build() {
    Column() {
      Row() {
        Image(this.backIconResource)
          .width(24)
          .height(24)
          .margin({ left: 24 })
          .onClick(() => {
            router.back();
          })
        Text('返回')
          .fontColor(Color.White)
          .fontSize(24)
          .fontWeight(500)
          .margin({ left: 12 })
      }
      .width('100%')
      .margin({
        left: 12,
        top: 12
      })
      .justifyContent(FlexAlign.Start)

      Stack() {
        if (!this.isPlay && !this.isLoading) {
          Image(this.startIconResource)
            .width(50)
            .height(50)
            .zIndex(2)
        }
        if (this.isLoading) {
          Progress({
            value: 0,
            total: 100,
            type: ProgressType.ScaleRing
          })
            .color(Color.Grey)
            .value(this.progressVal)
            .width(80)
            .style({
              strokeWidth: 15,
              scaleCount: 15,
              scaleWidth: 5
            })
            .zIndex(1)
        }
        VideoPlayer({
          source: this.source,
          controller: this.controller
        })
          .zIndex(0)
      }
    }
    .height('100%')
    .backgroundColor(Color.Black)
  }
}

滑块VideoPlaySlider.ets

/**
 * video slider component
 */
@Component
export struct VideoSlider {
  @Consume isOpacity: boolean;
  private controller: VideoController = new VideoController();
  @Consume currentStringTime: string;
  @Consume currentTime: number;
  @Consume durationTime: number;
  @Consume durationStringTime: string;
  @Consume isPlay: boolean;
  @Consume flag: boolean;
  @Consume isLoading: boolean;
  @Consume progressVal: number;

  build() {
    Row({ space: 12 }) {
      Image(this.isPlay ? $r('app.media.ic_pause') : $r('app.media.ic_play'))
        .width(24)
        .height(24)
        .margin({ left: 12 })
        .onClick(() => {
          this.iconOnclick();
        })
      Text(this.currentStringTime)
        .fontSize(16)
        .fontColor(Color.White)
        .margin({ left: 12 })
      Slider({
        value: this.currentTime,
        min: 0,
        max: this.durationTime,
        step: 1,
        style: SliderStyle.OutSet
      })
        .blockColor("#FFFFFF")
        .width('46.7%')
        .trackColor(Color.Gray)
        .selectedColor("#FFFFFF")
        .showSteps(true)
        .showTips(true)
        .trackThickness(this.isOpacity ? 2 : 4)
        .onChange((value: number, mode: SliderChangeMode) => {
          this.sliderOnchange(value, mode);
        })
      Text(this.durationStringTime)
        .fontSize(16)
        .margin({ right: 12 })
        .fontColor(Color.White)
    }
    .opacity(this.isOpacity ? Number.parseFloat('0.2') : 1)
    .width('100%')
    .alignItems(VerticalAlign.Center)
    .justifyContent(FlexAlign.Center)
  }

  /**
   * icon onclick callback
   */
  iconOnclick() {
    if (this.isPlay === true) {
      this.controller.pause()
      this.isPlay = false;
      this.isOpacity = false;
      return;
    }
    if (this.flag === true) {
      this.controller.start();
      this.isPlay = true;
      this.isOpacity = true;
    } else {
      this.isLoading = true;
      // The video loading is not complete. The loading action is displayed.
      let intervalLoading = setInterval(() => {
        if (this.progressVal >= 100) {
          this.progressVal = 0;
        } else {
          this.progressVal += 10;
        }
      }, 100)
      // The scheduled task determines whether the video loading is complete.
      let intervalFlag = setInterval(() => {
        if (this.flag === true) {
          this.controller.start();
          this.isPlay = true;
          this.isOpacity = true;
          this.isLoading = false;
          clearInterval(intervalFlag);
          clearInterval(intervalLoading);
        }
      }, 100);
    }
  }

  /**
   * video slider component onchange callback
   */
  sliderOnchange(value: number, mode: SliderChangeMode) {
    this.currentTime = Number.parseInt(value.toString());
    this.controller.setCurrentTime(Number.parseInt(value.toString()), SeekMode.Accurate);
    if (mode === SliderChangeMode.Begin || mode === SliderChangeMode.Moving) {
      this.isOpacity = false;
    }
    if (mode === SliderChangeMode.End) {
      this.isOpacity = true;
    }
  }
}

Video组件封装VideoPlayer.ets

import prompt from '@ohos.promptAction';
import { VideoSlider } from './VideoPlaySlider';
export function changeSliderTime(value: number): string {
  let second: number = value % 60;
  let min: number = Number.parseInt((value / 60).toString());
  let head = min < 10 ? `${'0'}${min}` : min;
  let end = second < 10 ? `${'0'}${second}` : second;
  let nowTime = `${head}${':'}${end}`;
  return nowTime;
}

/**
 * video controller component
 */
@Component
export struct VideoPlayer {
  private source: string | Resource = '';
  private controller: VideoController = new VideoController();
  private previewUris: Resource = $r('app.media.icon');
  @Provide currentTime: number = 0;
  @Provide durationTime: number = 0;
  @Provide durationStringTime: string = '00:00';
  @Provide currentStringTime: string = '00:00';
  @Consume isPlay: boolean;
  @Consume isOpacity: boolean;
  @Consume flag: boolean;
  @Consume isLoading: boolean;
  @Consume progressVal: number;

  build() {
    Column() {
      Video({
        src: this.source,
        previewUri: this.previewUris,
        controller: this.controller
      })
        .width('100%')
        .height('88%')
        .controls(false)
        .autoPlay(false)
        .objectFit(ImageFit.Contain)
        .loop(false)
        .onUpdate((event) => {
          if (event) {
            this.currentTime = event.time;
            this.currentStringTime = changeSliderTime(this.currentTime);
          }
        })
        .onPrepared((event) => {
          this.prepared(event?.duration);
        })
        .onFinish(() => {
          this.finish();
        })
        .onError(() => {
          prompt.showToast({
            duration: 5000,
            message: '请检查网络'
          });
        })
      VideoSlider({ controller: this.controller })
    }
  }

  /**
   * video component prepared callback
   */
  prepared(duration: number) {
    this.durationTime = duration;
    let second: number = duration % 60;
    let min: number = Number.parseInt((duration / 60).toString());
    let head = min < 10 ? `${'0'}${min}` : min;
    let end = second < 10 ? `${'0'}${second}` : second;
    this.durationStringTime = `${head}${':'}${end}`;
    this.flag = true;
  }

  /**
   * video component finish callback
   */
  finish() {
    this.isPlay = false;
    this.isOpacity = false;
  }
}

12、Web

1、Web组件使用

struct Index {
  controller: WebController = new WebController();

  build() {
    Column() {
      // 加载网页
      Web({ src: 'https://developer.harmonyos.com/', controller: this.controller })
      // 加载本地html
      // Web({ src: $rawfile('index.html'), controller: this.controller })
    }
  }
}

2、Web与js交互

下面示例中:

1、打开App,html回调confirm方法

2、点击按钮,app调用html的test方法

Kapture 2023-12-02 at 18.56.17

鸿蒙页面使用如下

struct Index {
  controller: WebController = new WebController();

  build() {
    Column() {
      // 鸿蒙调用html的方法
      Button("鸿蒙按钮").onClick(() => {
        this.controller.runJavaScript({
          script: 'test()',
          callback: (result: string) => {
            prompt.showToast({
              duration: 5000,
              message: result
            });
          } });
      })
      Web({ src: $rawfile('index.html'), controller: this.controller })
        .javaScriptAccess(true)
        // 鸿蒙对外方法
        .onConfirm((event) => {
          AlertDialog.show({
            title: 'title',
            message: event.message,
            confirm: {
              value: 'onAlert',
              action: () => {
                event.result.handleConfirm();
              }
            },
            cancel: () => {
              event.result.handleCancel();
            }
          })
          return true;
        })
        // 输出js的日志
        .onConsole((event) => {
          console.log('getMessage:' + event.message.getMessage());
          console.log('getMessageLevel:' + event.message.getMessageLevel());
          return false;
        })
    }
  }
}

html使用如下

<!DOCTYPE html>
<html>
<meta charset="utf-8">
<body>
</body>
<script type="text/javascript">
  <!--js回调鸿蒙的方法-->
  confirm("confirm message from html")
  <!--js对外方法-->
  function test() {
      return "This value is from index.html"
  }

</script>
</html>

四、鸿蒙api

1、UIAbility启动模式

UIAbility当前支持singleton(单实例模式)、multiton(多实例模式)和specified(指定实例模式)3种启动模式

  • singleton(单实例模式)
img

如果应用进程中该类型的UIAbility实例已经存在,则复用系统中的UIAbility实例.

在module.json5配置文件中的"launchType"字段配置为"singleton"即可。

{
  "module": {
    // ...
    "abilities": [
      {
        "launchType": "singleton",
        // ...
      }
    ]
  }
}
  • standard(标准实例模式)
img

每次调用startAbility()方法时,都会在应用进程中创建一个新的该类型UIAbility实例。即在最近任务列表中可以看到有多个该类型的UIAbility实例。

 "launchType": "standard",
  • specified(指定实例模式)
img

针对一些特殊场景使用(例如文档应用中每次新建文档希望都能新建一个文档实例,重复打开一个已保存的文档希望打开的都是同一个文档实例)

"launchType": "specified",

2、UIAbility组件生命周期

UIAbility的生命周期包括Create、Foreground、Background、Destroy四个状态

Ability-Life-Cycle

需要注意的是:UIAbility没有WindowStageCreate、WindowStageDestroy,这两个是WindowStage的生命周期。

UIAbility实例创建完成之后,在进入Foreground之前,系统会创建一个WindowStage。WindowStage创建完成后会进入onWindowStageCreate()回调,可以在该回调中设置UI界面加载、设置WindowStage的事件订阅。

img

3、toast

import prompt from '@ohos.promptAction';

Button("点击toast").onClick(() => {
  prompt.showToast({
    duration: 5000,
    message: '点击toast'
  });
})

4、Preferences存储

注意:初始化需要await,并且需要context参数,建议在EntryAbility的onCreate方法中

await sharePreferenceUtil.init(this.context);
import dataPreferences from '@ohos.data.preferences';

const KEY_APP_FONT_SIZE = 'appFontSize';
/**
 * SP工具类
 */
export class SharePreferenceUtil {
  preferences: dataPreferences.Preferences;

  // 初始化(注意:初始化是异步方法,需要await)
  async init(context: Context) {
    this.preferences = await dataPreferences.getPreferences(context, 'myPreferences');
  }

  // 存储
  saveDefaultFontSize(fontSize: number) {
    this.preferences.has(KEY_APP_FONT_SIZE).then(async (isExist: boolean) => {
      if (!isExist) {
        await this.preferences.put(KEY_APP_FONT_SIZE, fontSize);
        this.preferences.flush();
      }
    }).catch((err: Error) => {
    });
  }

  // 更新
  async saveChangeFontSize(fontSize: number) {
    await this.preferences.put(KEY_APP_FONT_SIZE, fontSize);
    this.preferences.flush();
  }

  // 获取
  async getChangeFontSize() {
    let fontSize: number = 0;
    fontSize = await this.preferences.get(KEY_APP_FONT_SIZE, fontSize) as number;
    return fontSize;
  }

  // 删除
  async deleteChangeFontSize() {
    let deleteValue = this.preferences.delete(KEY_APP_FONT_SIZE);
    deleteValue.then(() => {
    }).catch((err: Error) => {
    });
  }
}

const sharePreferenceUtil = new SharePreferenceUtil();

export default sharePreferenceUtil;

五、状态管理与数据同步

1、组件状态管理装饰器和@Builder装饰器:

组件状态管理装饰器用来管理组件中的状态,它们分别是:@State、@Prop、@Link。

  • @State装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新。
  • @Prop与@State有相同的语义,但初始化方式不同。@Prop装饰的变量必须使用其父组件提供的@State变量进行初始化,允许组件内部修改@Prop变量,但更改不会通知给父组件,即@Prop属于单向数据绑定。
  • @Link装饰的变量可以和父组件的@State变量建立双向数据绑定,需要注意的是:@Link变量不能在组件内部进行初始化。
  • @Builder装饰的方法用于定义组件的声明式UI描述,在一个自定义组件内快速生成多个布局内容。

组件内的状态管理:@State
从父组件单向同步状态:@Prop
与父组件双向同步状态:@Link
监听状态变化:@Watch
跨组件层级双向同步状态:@Provide和@Consume

1、父页面同步数据给子页面:@Prop

2、子页面同步数据给父页面:@Link

下面示例中

1、父组件把clickIndex通过 Props 传递给子页面

2、点击子组件后,通过 Link 把修改后的clickIndex值传递给页面

3、其余子组件 Watch 了clickIndex,并同时修改组件中的isExpanded值

Kapture 2023-12-03 at 10.04.34

页面Index.ets

import TestItem from './TestItem';

@Entry
@Component
struct Index {
  private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  @State clickIndex: number = -1;

  build() {
    Column() {
      ForEach(this.arr, (item: number, index: number) => {
        TestItem({
          index: this.arr[index], //@Prop传递给子组件数据
          clickIndex: $clickIndex, //@Link双向绑定数据
        })
      }, item => item)
    }
    .width('100%')
    .height('100%')
  }
}

组件TestItem.ets

@Component
export default struct TestItem {
  @Prop index: number; //当前 item 序号
  @State isExpanded: boolean = false; //当前是否展开
  // @Link修饰是为了同步数据到父组件,@Watch是为了监听回调给onClickIndexChanged
  @Link @Watch('onClickIndexChanged') clickIndex: number; //点击的序号

  onClickIndexChanged() {
    this.isExpanded = this.clickIndex == this.index;
  }

  build() {
    Button(this.index + '、是否展开:' + this.isExpanded)
      .width('100%')
      .height(this.isExpanded ? 80 : 40)
      .fontSize(20)
      .fontColor(Color.White)
      .borderRadius(10)
      .backgroundColor(0x007DFF)
      .margin({ top: 10 })
      .onClick(() => {
        this.clickIndex = this.index;
      })
  }
}

2、子组件callback 回调父页面

子组件声明callback 方法

// 组件
@Component
export default struct TestItem {
  callback?: (index: number) => void;

  build() {
    Button('子组件')
      .width('100%')
      .height(40)
      .fontSize(20)
      .fontColor(Color.White)
      .borderRadius(10)
      .backgroundColor(0x007DFF)
      .onClick(() => {
        // this.clickIndex = this.index;
        if (this.callback !== undefined) {
          this.callback(123)
        }
      })
  }
}

父页面传入callback方法

TestItem({
  callback: (index:number): void => {
    console.warn("index:",index)
  }
})

六、弹窗

1、警告弹窗AlertDialog

img
AlertDialog.show(
  {
    title: '删除联系人', // 标题
    message: '是否需要删除所选联系人?', // 内容
    autoCancel: false, // 点击遮障层时,是否关闭弹窗。
    alignment: DialogAlignment.Bottom, // 弹窗在竖直方向的对齐方式
    offset: { dx: 0, dy: -20 }, // 弹窗相对alignment位置的偏移量
    primaryButton: {
      value: '取消',
      action: () => {
      }
    },
    secondaryButton: {
      value: '删除',
      fontColor: '#D94838',
      action: () => {
      }
    },
    cancel: () => { // 点击遮障层关闭dialog时的回调
    }
  }
)

2、文本选择弹窗TextPickerDialog

img
@State select: number = 2;
private fruits: string[] = ['苹果', '橘子', '香蕉', '猕猴桃', '西瓜'];

TextPickerDialog.show({
  range: this.fruits, // 设置文本选择器的选择范围
  selected: this.select, // 设置初始选中项的索引值。
  onAccept: (value: TextPickerResult) => { // 点击弹窗中的“确定”按钮时触发该回调。
    // 设置select为按下确定按钮时候的选中项index,这样当弹窗再次弹出时显示选中的是上一次确定的选项
    this.select = value.index;
    console.info("TextPickerDialog:onAccept()" + JSON.stringify(value));
  },
  onCancel: () => { // 点击弹窗中的“取消”按钮时触发该回调。
    console.info("TextPickerDialog:onCancel()");
  },
  onChange: (value: TextPickerResult) => { // 滑动弹窗中的选择器使当前选中项改变时触发该回调。
    console.info("TextPickerDialog:onChange()" + JSON.stringify(value));
  }
})

3、日期滑动选择弹窗DatePickerDialog

image-20231129200521350
selectedDate: Date = new Date("2010-1-1")

DatePickerDialog.show({
  start: new Date("1900-1-1"), // 设置选择器的起始日期
  end: new Date("2023-12-31"), // 设置选择器的结束日期
  selected: this.selectedDate, // 设置当前选中的日期
  lunar: false,
  onAccept: (value: DatePickerResult) => { // 点击弹窗中的“确定”按钮时触发该回调
    // 通过Date的setFullYear方法设置按下确定按钮时的日期,这样当弹窗再次弹出时显示选中的是上一次确定的日期
    this.selectedDate.setFullYear(value.year, value.month, value.day)
    console.info("DatePickerDialog:onAccept()" + JSON.stringify(value))
  },
  onCancel: () => { // 点击弹窗中的“取消”按钮时触发该回调
    console.info("DatePickerDialog:onCancel()")
  },
  onChange: (value: DatePickerResult) => { // 滑动弹窗中的滑动选择器使当前选中项改变时触发该回调
    console.info("DatePickerDialog:onChange()" + JSON.stringify(value))
  }
})

4、自定义弹窗

通过装饰器@CustomDialog定义的组件来实现,然后结合CustomDialogController来控制自定义弹窗的显示和隐藏。

image-20231127102353922

弹窗组件AddTargetDialog.ets绘制

@CustomDialog
export default struct AddTargetDialog {
  @State subtaskName: string = '';
  private controller?: CustomDialogController;
  onClickOk?: (value: string) => void;

  build() {
    Column() {
      Text('添加子目标')
        .width('100%')
        .fontSize('20fp')
        .fontWeight(500)
        .fontColor('#182431')
        .textAlign(TextAlign.Start)
      TextInput({ placeholder: '请输入子目标名称'})
        .placeholderColor(Color.Grey)
        .placeholderFont({ size: '16fp'})
        .caretColor(Color.Blue)
        .backgroundColor('#0D182431')
        .width('100%')
        .height('40%')
        .margin({ top: '6%' })
        .fontSize('16fp')
        .fontColor("#182431")
        .onChange((value: string) => {
          this.subtaskName = value;
        })
      Blank()
      Row() {
        Button('取消')
          .dialogButtonStyle()
          .onClick(() => {
            this.controller?.close();
          })
        Divider()
          .vertical(true)
        Button('确定')
          .dialogButtonStyle()
          .onClick(() => {
            if (this.onClickOk !== undefined) {
              this.onClickOk(this.subtaskName);
            }
          })
      }
      .width('70%')
      .height('10%')
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .padding('24vp')
    .height('168vp')
    .width('90.3%')
    .borderRadius(32)
    .backgroundColor(Color.White)
  }
}

/**
 * Custom button style.
 */
@Extend(Button) function dialogButtonStyle() {
  .fontSize('16fp')
  .height('32vp')
  .width('96vp')
  .backgroundColor(Color.White)
  .fontColor('#007DFF')
}

页面使用

@Entry
@Component
struct Index {
  dialogController: CustomDialogController = new CustomDialogController({
    builder: AddTargetDialog({
      onClickOk: (value: string): void => {
        console.warn("value:",value)
        this.dialogController.close();// 关闭
      }
    }),
    alignment: DialogAlignment.Bottom,
    offset: {
      dx: 0,
      dy: '-16vp'
    },
    customStyle: true,
    autoCancel: false
  });

  build() {
    Button("点击打开弹窗").onClick(()=>{
      this.dialogController.open()// 打开
    })
  }
}

七、动画

添加animation属性就好,由State驱动。

Kapture 2023-11-30 at 20.19.59
struct Index {
  @State iconWidth: number = 30;

  onPageShow() {
    this.iconWidth = 90;
  }

  build() {
    Column() {
      Image($r('app.media.icon'))
        .width(this.iconWidth)
        .margin(10)
        .objectFit(ImageFit.Contain)
        .animation({
          duration: 2000,
          tempo: 3.0, //动画的播放速度
          delay: 0,
          curve: Curve.Linear,
          playMode: PlayMode.Normal,
          iterations: -1, //播放次数,默认一次,设置为-1时表示无限次播放。
        })
    }
  }
}

八、网络请求

注意:多个请求可以使用同一个httpRequest对象,httpRequest对象不能复用,因为它支持request、destroy、on和off方法,例如取消网络请求httpRequest.destroy();

import http from '@ohos.net.http';

let httpRequest = http.createHttp();
let promise = httpRequest.request(
  "http://www.baidu.com",
  {
    // 请求方式
    method: http.RequestMethod.POST,
    // 请求的额外数据。
    extraData: {
      "param1": "value1",
      "param2": "value2",
    },
    // 可选,默认为60s
    connectTimeout: 60000,
    // 可选,默认为60s
    readTimeout: 60000,
    // 开发者根据自身业务需要添加header字段
    header: {
      'Content-Type': 'application/json'
    }
  });
promise.then((data) => {
  if (data.responseCode === http.ResponseCode.OK) {
    console.info('Result:' + data.result);
    console.info('code:' + data.responseCode);
  }
}).catch((err) => {
  console.info('error:' + JSON.stringify(err));
});

九、路由

在如下目录下注册页面

/entry/src/main/resources/base/profile/main_pages.json

跳转代码

import router from '@ohos.router';

router.pushUrl({
  url: 'pages/SecondPage',
  params: {
    src: 'Index页面传来的数据',
  }
}, router.RouterMode.Single)

鸿蒙参考资料

鸿蒙第一课视频,对应代码Codelabs

完整版的功能demo

官方文档

HarmonyOS点石成金

鸿蒙系统系列教程6-鸿蒙系统项目结构解析

鸿蒙开发者学习笔记

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/215355.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序自定义数据实现级联省市区组件

前言 在微信小程序中&#xff0c;官方文档提供的省市区组件&#xff0c;可以让用户更加方便快捷地选择省市区&#xff0c;但是官方提供的组件有一个缺点&#xff0c;无法自定义数据&#xff0c;但如果项目中需要使用自己的数据&#xff0c;显然就得寻找其它的组件实现。 官方组…

CTF特训日记day3

复现一下RWCTF5th shellfind题目 题目描述如下&#xff1a; Hello Hacker. You dont know me, but I know you. I want to play a game. Heres what happens if you lose. The device you are watching is hooked into your Saturday and Sunday. When the timer in the back …

颠覆性语音识别:单词级时间戳和说话人分离

vbenjs/vue-vben-admin[1] Stars: 19.7k License: MIT Vue Vben Admin 是一个免费开源的中后台模板&#xff0c;使用最新的 vue3、vite4 和 TypeScript 等主流技术进行开发。该项目提供了现成的中后台前端解决方案&#xff0c;并可用于学习参考。 使用先进的前端技术如 Vue3/…

简单可行的SeruatV4的安装方案

目前Seurat的版本从V4升级到了V5&#xff0c;由于一些变化&#xff0c;导致当年取巧&#xff0c;使用获取数据的方法都无法在V5中使用。 建议在操作前重启下Rstudio&#xff08;或更确切的说是R&#xff09;&#xff01;&#xff01;&#xff01; 那么如何确保自己能够安装V4的…

python之ddddocr快速识别

1. 安装模块 pip install ddddocr -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com2. 编写代码 import ddddocr # 导入orc模块 import logging # 导入日志 logging.getLogger().setLevel(logging.INFO) # 设置日志级别 def ComputeCode(path):try:logg…

基于轻量级模型GHoshNet开发构建眼球眼疾识别分析系统,构建全方位多层次参数对比分析实验

工作中经常会使用到轻量级的网络模型来进行开发&#xff0c;所以平时也会常常留意使用和记录&#xff0c;在前面的博文中有过很多相关的实践工作&#xff0c;感兴趣的话可以自行移步阅读即可。 《移动端轻量级模型开发谁更胜一筹&#xff0c;efficientnet、mobilenetv2、mobil…

浅谈安科瑞ASJ继电器在马尔代夫环岛水上排屋的应用

摘要&#xff1a;对电气线路进行接地故障保护&#xff0c;方式接地故障电流引起的设备和电气火灾事故越来越成为日常所需。针对用户侧主要的用能节点&#xff0c;设计安装剩余电流继电器&#xff0c;实时监控各用能回路的剩余电流状态。通过实时监控用能以及相关电力参数、提高…

ESP32-Web-Server编程-简单的照片浏览器

ESP32-Web-Server编程-简单的照片浏览器 概述 从本节开始我们开始制作一些有趣的多媒体 Web 的示例。 当你希望在网页上展示一些广告、照片&#xff0c;或者你的开发板带摄像头&#xff0c;能够采集一些图片&#xff0c;这时你希望可以通过手头的浏览器查看图片&#xff0c;…

Mover Creator 用户界面

1 “开始”对话框 首次打开 Mover Creator 时&#xff0c;出现的第一个页面是“开始”对话框&#xff0c;如下所示。从这里开始&#xff0c;用户可以选择开始设计飞机、武器或发动机。在上述每种情况下&#xff0c;用户都可以创建新模型或编辑现有模型。 1.1 新建模型 如果用…

线上超市小程序可以做什么活动_提升用户参与度与购物体验

标题&#xff1a;线上超市小程序&#xff1a;精心策划活动&#xff0c;提升用户参与度与购物体验 一、引言 随着移动互联网的普及&#xff0c;线上购物已经成为人们日常生活的一部分。线上超市作为线上购物的重要组成部分&#xff0c;以其便捷、快速、丰富的商品种类和个性化…

金蝶云星空单据体明细权限和表单插件操作事件的先后顺序

文章目录 金蝶云星空单据体明细权限和表单插件操作事件的先后顺序顺序说明结论 金蝶云星空单据体明细权限和表单插件操作事件的先后顺序 顺序说明 先分录菜单单击事件EntryBarItemClick 再验权 后表单操作执行事件BeforeDoOperation 结论 如果是需要鉴权通过才允许操作的逻辑…

浅谈用户体验测试的主要功能

用户体验(User Experience&#xff0c;简称UX)在现代软件和产品开发中变得愈发重要。为了确保产品能够满足用户期望&#xff0c;提高用户满意度&#xff0c;用户体验测试成为不可或缺的环节。本文将详细探讨用户体验测试的主要功能&#xff0c;以及它在产品开发过程中的重要性。…

得帆云助力容百科技构建CRM系统,实现LTC全流程管理

宁波容百新能源科技股份有限公司 宁波容百新能源科技股份有限公司&#xff08;以下简称“容百科技”&#xff09;于2014年9月建立&#xff0c;是高科技新能源材料行业的跨国型集团公司。专业从事锂电池正极材料的研发、生产和销售&#xff0c;于2019年登陆上交所科创板&#x…

YouTube Premium 会员白嫖教程

前言 YouTube是美国Alphabet旗下的视频分享网站&#xff0c;也是目前全球最大的视频搜索和分享平台&#xff0c;同时允许用户上传、观看、分享及评论视频 1、点击自己的头像&#xff0c;点击购买内容与会员 2、点击免费试订 3、这里选择个人 4、点击开始试用一个月 5、添加一…

力扣543. 二叉树的直径(java DFS解法)

Problem: 543. 二叉树的直径 文章目录 题目描述思路解题方法复杂度Code 题目描述 给你一棵二叉树的根节点&#xff0c;返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的 长度 由它们…

Python----练习:使用面向对象实现报名系统开发

第一步&#xff1a;分析哪些动作是由哪些实体发出的 学生提出报名 学生提供相关资料 学生缴费 机构收费 教师分配教室 班级增加学生信息 于是&#xff0c;在整个过程中&#xff0c;一共有四个实体&#xff1a;学生、机构、教师、班级&#xff01;在现实中的一个具体的实…

Kali Linux三种网络攻击方法总结(DDoS、CC和ARP欺骗)

本文章使用的是Kali Linux的2020-4-installer-amd64版本 Kali Linux的安装过程本文章不做过多说明&#xff0c;请自行百度 请正确使用DDos和CC攻击&#xff0c;不要用来做违反当地法律法规的事情&#xff0c;否则后果自负 CSDN大礼包&#xff1a;《黑客&网络安全入门&am…

使用DevEco Studio时遇见的错误情况与问题

第一个 问题:打开项目文件,控制台报错 hvigor ERROR: Unable to find sdk.dir in local.properties or OHOS_BASE_SDK_HOME in the system environment path. 解决办法:在项目根目录中打开local.properties。如果没有local.properties,自己创建。 在local.properties中填…

tNavigator 23.2 x64

Rock Flow Dynamics&#xff08;RFD&#xff09;很高兴地宣布发布我们旗舰产品tNavigator的最新版本。版本 23.2 现在可供用户使用。 tNavigator长期以来一直被认为是油藏工程师和地质学家的强大工具&#xff0c;可为复杂的油藏行为提供准确的建模和模拟。最新版本为所有模块带…

numpy知识库:基于numpy绘制灰度直方图

前言 对于灰度图像而言&#xff0c;灰度直方图可以统计灰度图像内各个灰度级出现的次数。 灰度直方图的横坐标是灰度图像中各像素点的灰度级。灰度的数值范围为[0, 255]。因此&#xff0c;如果将图像分为256个灰度级&#xff0c;那么每个灰度级唯一对应一个灰度&#xff1b;如…