Skip to content

鸿蒙

学习Stage模型(FA模型会逐步淘汰)

应用结构

项目目录

https://hedaodao-1256075778.cos.ap-beijing.myqcloud.com/Essay/2023111712031122p3lL.png

Module

一个鸿蒙应用包含多个Module (每个Module可以单独编译为.hap后缀的文件,所有.hap文件整体称为Bundle,可以打包为一个.app后缀的文件用于上架)

Module分为3种类型:

  • Ability(HAP)

    • entry类型: 应用的主模块(项目可以包含一个或多个Entry模块,如果同一类型的设备包含多个Entry模块,需要配置distroFilter分发规则,使得应用市场在做应用的云端分发时,对该设备类型下不同规格的设备进行精确分发)

    • feature类型:类似于分包,甚至配置应用市场的分发策略实现按设备下发分包(按照设备一次性加载好,不是类似微信触发后再去下载)

    • 创建:需要选择模块类型

      entry类型的设备类型可以是1个或多个 (Phone、2in1、Tablet),假如存在一个Phone+Tablet类型的entry了,再次创建只能选剩下的2in1了,即确定设备上只能有一个entry入口

      feature类型可选择的设备类型只能是entry覆盖的类型

      image-20240311104821513

  • Shared Library(HSP)

    需要选择设备类型

    image-20240311104717132

  • Static Library Module(HAR)

    静态共享包(Harmony Archive,HAR),包含代码、C++库、资源和配置文件。通过HAR可以实现多个模块或多个工程共享ArkUI组件、资源等相关代码。HAR不同于HAP,不能独立安装运行在设备上,只能作为应用模块的依赖项被引用

目录右键 -> New Module,可以看到下面的菜单。处理框中的两个,其他都是Ability

image-20240311104518726

多入口

在entry中可以定义多个ability,然后在entry模块的module.json中配置module.abilities字段

该字段是个数组类型,可以配置多个入口

用户安装后在桌面会展示多个应用图标入口

Stage模型

image-20240311103834673

Window:当前窗口实例,窗口管理器管理的基本单元

WindowStage:窗口管理器。管理各个基本窗口单元

抓包

请参考如下流程:
当前网络模块已支持适配Charles工具抓包,配置方式如下:
一、导出证书,点击 Help--->SSL Proxying--->Save Charles Root Certificate
(1)导入证书到手机执行命令参考如下:
hdc file send charles.pem(pc上证书路径) /storage/media/100/local/files/Download(工程机指定路径)
(2)连接工程机后执行命令启动证书安装界面
hdc shell aa start -a MainAbility -b com.ohos.certmanager
(3) 选择从存储设备安装,选择指定pem证书
二、安装Charles证书到系统可信目录,操作步骤:
点击 Help--->SSL Proxying--->Install Charles Root Certificate--->安装证书--->选择证书存储路径为:受信任的根证书颁发机构
三、设置代理操作步骤:
1)点击 Proxy--->SSL Proxy Settings--->在Include添加 *:* 和 *:443
2)点击 Proxy--->Proxy Settings--->勾选Enable transparent HTTP proxying
四、Wifi代理设置:
将手机与PC同一局域网下连接,手机连接WiFi时,点击代理设置为手动,修改设置代理IP,端口为Charles监听的端口,默认为8888,可在上一步Proxy Settings中查看和修改
五、应用抓取http包: App开发时,HTTP请求HttpRequestOptions参数设置,可参考文档
1)设置usingProxy为true,表示使用HTTP代理(该字段默认为false 不使用代理)
2)设置caPath(可根据环境使用设置,默认使用系统预设CA)
http参数设置可参考:https://docs.openharmony.cn/pages/v4.0/zh-cn/application-dev/reference/apis/js-apis-http.md/#httprequestoptions%E6%94%AF%E6%8C%81%E8%B7%A8%E5%B9%B3%E5%8F%B0
charles工具配置可参考博客:https://juejin.cn/post/6844904182588112904

类型标记

类型描述 需 使用 interface、class

ts
interface yyy =Record<string,string>

@Component
export default struct xxxx {
	@Status obj:yyy = {} //必须初始化
  
  @Prop 不必初始化,但是需要从外部传入
}

如果用明确类型

ts
interface yyy {
	name:string
}
@Component
export default struct xxxx {
	@Status obj:yyy = {
    name:''
  } //必须初始化,且初始值必须明确字段
  
  @Prop 不必初始化,但是需要从外部传入
}

组件

Row、Column

默认布局方向:

  • Row主轴水平方向,默认子元素 水平靠左,竖直居中

  • Column主轴竖直方向,默认子元素 竖直靠上,水平居中

【也可以说两者,主轴方向靠Start、纵轴居中】

Flex

Flex默认水平方向为主轴,可通过direction参数改变

默认布局方向:

  • 主轴靠Start
  • 纵轴靠Start

记得一般居中布局要设置下

ts
Flex({ direction: FlexDirection.xxx }) { 
		// 子组件
}

Scroll

  • 默认子组件竖直、水平方向都居中,一般要使用align可以靠顶部、左侧开始
  • 必须指定scrollable属性,设置滚动方向,否则不可滚动
ts
Scroll() {
    Column(){
      
    }
}
.scrollable(ScrollDirection.Vertical) // ScrollDirection.Horizontal
.align(Alignment.Top)

// 无论滚动方向。Alignment轴是固定的
// 竖直方向,Alignment.Top
// 水平方向,Alignment.Start

align设置子元素与父元素对齐的方式(不受滚动方向的影响)

image-20240307110301479

Grid

Grid通过columnsTemplate指定列数、列的宽度,通过rowsTemplate指定行数、行的高度(如果宽度使用fr比例的话,不能确定Grid的宽高了,这时候需要参考Grid的width、height,如果还没有则认为不能确定)

注意:Grid最坑的一点是不能确定宽或高,宽或高就会继承父级(不是由内容撑起来)

Grid布局的三种情况:

  • 仅仅指定列(最常见)

    • 高继承父级

    主轴为竖直

  • 仅仅指定行

    • 宽继承父级

    主轴为水平

  • 指定列、行

    • 需要判断是否能确定宽或高,不能才会就会继承父级

    • Grid只展示固定行列数的元素,多余元素不展示,且Grid不可滚动

  • 不指定列、行

    • 宽高继承父级

    • 主轴方向需要layoutDirection指定,主轴方向元素minCount、maxCount决定是否换行

ts
Grid() {
    GridItem() {
            
    }
}

.columnsTemplate('1fr 1fr')
.columnsGap('14lpx')
.rowsGap('16lpx')
.maxCount(2)

目前发现了一个Grid撑起来高度的方案,下面绿框中的是Gird,成功撑起来下拉框

image-20240307140706419

ts
Column() {
      Grid() {
        ForEach(this.data, (item: SpecificGroupColorItem) => {
          GridItem() {
            Row() {
              ColorItem({ color: item.colorRgb, isActive: false })
              Text(item.colorName)
                .fontSize('28lpx')
                .fontColor(this.activeColor.colorId === item.colorId ? '#2567ff' : '#0f1d37')
                .fontWeight(this.activeColor.colorId === item.colorId ? FontWeight.Medium : FontWeight.Regular)
                .margin({ left: '8lpx' })
            }
            .width('100%')
            .height('80lpx')
            .backgroundColor(this.activeColor.colorId === item.colorId ? '#f2f6ff' : '#f5f7fb')
            .borderWidth('2lpx')
            .borderColor(this.activeColor.colorId === item.colorId ? '#d6e3ff' : Color.Transparent)
            .borderRadius('8lpx')
            .padding({ top: '24lpx', bottom: '24lpx', left: '24lpx', right: '24lpx' })
            .onClick(() => {
              this?.onChange?.({
                colorId: item.colorId as number,
                colorName: item.colorName as string
              })
            })
          }
        })
      }

      .columnsTemplate('1fr 1fr')
      .columnsGap('14lpx')
      .rowsGap('16lpx')
      // 重点
      .maxCount(2)
    }
		// 重点 ,加margin也行
    .padding({ top: '24lpx', bottom: '32lpx', left: '32lpx', right: '32lpx' })

Toggle组件

hoverEffect可以去掉切换开关时背景的遮罩

Toggle({ type: ToggleType.Switch, isOn: false })
.margin({ left: '8lpx' })
.selectedColor('#12C2CE')
.switchPointColor('#FFFFFF')
.onChange((isOn: boolean) => {
console.info('Component status:' + isOn)
})
.hoverEffect(HoverEffect.None)

Blank

空白填充组件,在容器主轴方向上,空白填充组件具有自动填充容器空余部分的能力。仅当父组件为Row/Column/Flex时生效

image-20240110173049993

TabContent组件

高度=屏幕高度 - Tab高度

image-20240110163020832

layoutWeight属性

通用属性,仅在Row/Column/Flex布局中生效。

设置占据权重

Divider

设置颜色用color,不要用backgroundColor,使用backgroundColor颜色会有问题

Divider().vertical(true).color('#eeeeee').strokeWidth('2lpx').height('66lpx')

盒子模型

border

占据宽高范围,radius可裁剪盒子形状

image-20240325180036002

ts
Row()
    .width(100)
    .height(100)
    .backgroundColor(Color.Yellow)
    .border({
        width: 20,
        color: Color.Red,
        radius: 50
    })

outline

不占据宽高范围

盒子被裁剪,outline仍然是正方形,如果需要裁剪outline请使用它的radius属性

image-20240325190557961

Row()
    .width(100)
    .height(100)
    .backgroundColor(Color.Yellow)
    .outline({
        width: 20,
        color: Color.Red,

    })
    .border({
        color: Color.Red,
        radius: 50

    })

定位偏移

属性

position({x:number,y:number}) // 以父组件左上角为基础

offset({x:number,y:number}) // 在当前位置的基础上,进行偏移

zIndex(value:number) // 设置层级 ,组件Stack、RelativeContainer都是按顺序叠加层级,但是zIndex可以改变

组件

Stack

  • 只能设置向一个方向对齐

  • 子组件按顺序,层级从低到高

  • Stack不设置宽高默认为所有子元素中最大的宽、最大的高

  • offset设置偏移量,子组件可以偏移出Stack的范围

image-20240325161243363

ts
@Entry
@Component
struct Index {
    build() {
      	// 布局只能设置一个对齐方向,这里是设置了所有子组件都向下边框对齐
        Stack({ alignContent: Alignment.Bottom }) {
            // 层级低
            Row().width('100lpx').height('100lpx').backgroundColor(Color.Red)
            // 层级高
            Row().width('50lpx').height('50lpx').backgroundColor(Color.Blue)
						// 层级高+1
            Row().width('20lpx').height('20lpx').backgroundColor(Color.Yellow)
          	
        }.backgroundColor(Color.Black).margin({ left: '200lpx' })
    }
}

虽然Stack只能指定一个方向对齐,但是我们可以通过嵌套Stack实现多个方向对齐

image-20240325161911944

ts
@Entry
@Component
export default struct Index {
    build() {
        Stack({ alignContent: Alignment.Bottom }) {
            Stack({ alignContent: Alignment.TopEnd }) {
                Row()
                    .width('97lpx')
                    .height('97lpx')
                    .backgroundColor('#eeeeee')


                Image($r('sys.media.ohos_ic_public_close'))
                    .width('36lpx')
                    .height('36lpx')
                    .offset({ x: '10lpx', y: '-10lpx' })

            }

            Text('你好你好你好你好你好').fontSize('20lpx')
        }
    }
}

RelativeContainer

RelativeContainer可以实现多个方向对齐,比Stack功能更强大

但是其缺点更明显:必须设置宽高,否则就是父级宽高。内部内容不能撑起宽高

image-20240325163509800

目前是使用嵌套的Stack实现了多个方向对齐(关闭按钮右上角对齐,文案下边中心对齐)

注意:

  • 每个元素必须设置id,RelativeContainer的id默认为__container__
  • alignRules可设置相对谁布局
  • 内部子组件按顺序,层级从低到高
ts
@Entry
@Component
export default struct Index {
    build() {
        RelativeContainer() {
						// 文本
            Text('标题') {
            }
            .alignRules({
              	// 垂直方向  当前组件位置:{ id: 相对的组件id , align:相对的组件位置}
                center: { anchor: "__container__", align: VerticalAlign.Center },
              	// 水平方向
                middle: { anchor: "__container__", align: HorizontalAlign.Center }
            })
            .id("a")

            // 红色方块
            Row()
                .width('48lpx')
                .height('48lpx')
                .backgroundColor(Color.Red)
                .alignRules({
                    top: { anchor: "a", align: VerticalAlign.Bottom },
                    left: { anchor: "a", align: HorizontalAlign.Start }
                })
                .id("b")


            // 麦克风图片
            Image($r('sys.media.ohos_ic_public_voice'))
                .width('48lpx')
                .height('48lpx')
                .alignRules({
                    top: { anchor: "a", align: VerticalAlign.Bottom },
                    left: { anchor: "a", align: HorizontalAlign.Start }
                })
                .id("c")
                .offset({ x: '300lpx' })
        }
        .width('500lpx')
        .height('500lpx')
        .backgroundColor(Color.Yellow)
    }
}

事件机制

手势没有冒泡的概念

事件:

父子均绑定事件,如果子响应,父级就不会响应。如果自己没有绑定,父级响应

可以用hitTestBehavior(HitTestMode.Block),阻止父级响应

状态管理最佳实践

官方目前还不稳定,从@track、@observed到现在的@ObservedV2还在变动

待稳定后补充这部分

组件状态管理

无状态修饰符

变量可以显示在页面上,但是变量变更不会更新页面

必须赋初始值

从目前的官方例子来看,定义的变量如果没有状态修饰符,都用private修饰

父级组件是否传入这个字段可选

ts
// 必须赋值
private isShowTitle: boolean = true

// 可以不赋值,使用时必须判断是否存在
private isShowTitle?: boolean

箭头函数

js
// 防止this指向变化。如果将方法传入子组件,必须使用这种方式声明函数
private navToBrandListTop = () => {
    // xxxx
}

@Status

@Status修饰的变量,必须赋初值,不允许使用any、undefined和null

变量类型

  • 简单类型,直接改就能触发刷新页面。tabsIndex变量使用@Status修饰,onchange方法修改这个变量,页面才会更新(tab会切换)

    image-20240110160322017

    ts
    @Entry
    @Component
    struct Home {
      // 初始化tabsBar的index值为0
      @State tabsIndex: number = 0;
    
      build() {
        Tabs() {
          ForEach(HOME_TABS, (item: FirstLevelCategory, index: number) => {
            TabContent() {
              xxxx
            }
            .tabBar(this.CustomTabBar(item, index))
          })
        }
        .onChange((index: number) => {
          this.tabsIndex = index;
        })
      }
    
      @Builder CustomTabBar(item: FirstLevelCategory, index: number) {
        Column() {
          Image(this.tabsIndex === index ? item.iconSelected : item.icon)
            .width(24)
            .height(24)
            .margin({ bottom: 4 })
    
          Text(item.tabBarName)
            .fontSize(10)
            .fontFamily('HarmonyHeiTi-Medium')
            .fontColor(this.tabsIndex === index ? $r('app.color.tab_bar_select') : $r('app.color.tab_bar_unselect'))
        }
        .width('100%')
        .id(`tabBar${index}`)
        .padding({ top: 6, bottom: 6 })
        .alignItems(HorizontalAlign.Center)
      }
    }
  • 数组类型,必须通过数组方法修改才能触发更新,例如splice、push。如果数组元素为对象,例如 let arr=[{count:1},{count:2}],如果修改第一个元素的count,必须整个元素赋值 arr[0]={count:2},不能 arr[0].count=2

  • 对象类型,修改第一层数据才可以触发更新,但是深层次必须整体赋值到第一层才能更新

    ts
    class FirstLevelClass {
        public a: string;
        public child:SecondLevelClass
    
        constructor(a:string,child:SecondLevelClass) {
            this.a = a;
            this.child=child;
        }
    }
    
    
    class SecondLevelClass {
        public b: string;
    
        constructor(b:string) {
            this.b=b
        }
    }
    
    // 父组件
    @Component
    export struct Parent{
        @State instance:FirstLevelClass=new FirstLevelClass('a',new SecondLevelClass('c'))
        build(){
            Column(){
                Text('父级:')
                TextInput({text:this.instance.a}).onChange((value:string)=>{
                    this.instance.a=value
                })
                Child({ instance: this.instance })
    
            }
    
        }
    }
    
    // 子组件
    @Component
    export struct Child{
        @Link instance:FirstLevelClass
        build(){
            Column(){
                Text('子级:')
                TextInput({text:this.instance.a}).onChange((value:string)=>{
                    this.instance.a=value
                })
            }
    
        }
    }

@Status与@Prop

@Prop(父 -> 子 )修饰的变量不必赋初始值

即使父组件不使用circleColor渲染页面,也必须使用 @Status 修饰

@Status circleColor='red'

// 单向同步子组件  组件内部  @Prop circleColor:string
ChildCompProp({ circleColor: this.circleColor })
  • 是否可以多级传递。父级State修饰,子、孙子都使用Prop(子级不必再使用@Status修饰,就可以被孙子级别识别到)

    父级变动会引起子级、孙子级一起变动

    ts
    PersistentStorage.persistProp('PropA', 47)
    
    @Entry
    @Component
    struct Parent {
        @State a: number = 1;
    
        build() {
            Column() {
                Button(`父级:${this.a}`).onClick(() => {
                    this.a++
                })
                Child({ childA: this.a })
            }.height('100%').alignItems(HorizontalAlign.Center)
        }
    }
    
    @Component
    struct Child {
        @Prop childA: number
    
        build() {
            Column() {
                Button(`子级 :${this.childA}`)
                Grandson({ GrandsonA: this.childA })
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
        }
    }
    
    @Component
    struct Grandson {
        @Prop GrandsonA: number
    
        build() {
            Column({ space: 20 }) {
                Button(`孙子: ${this.GrandsonA}`)
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
        }
    }
  • 孙子级别使用Link

    ts
    PersistentStorage.persistProp('PropA', 47)
    
    @Entry
    @Component
    struct Parent {
        @State a: number = 1;
    
        build() {
            Column() {
                Button(`父级:${this.a}`).onClick(() => {
                    this.a++
                })
                Child({ childA: this.a })
            }.height('100%').alignItems(HorizontalAlign.Center)
        }
    }
    
    @Component
    struct Child {
        @Prop childA: number
    
        build() {
            Column() {
                Button(`子级 :${this.childA}`)
                Grandson({ GrandsonA: this.childA })
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
        }
    }
    
    @Component
    struct Grandson {
        @Link GrandsonA: number
    
        build() {
            Column({ space: 20 }) {
                Button(`孙子: ${this.GrandsonA}`).onClick(() => {
                  	// 可以修改子级数据,无法修改父级数据
                    this.GrandsonA++
                })
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
        }
    }

@Link(父 <->子)修饰的变量不必赋初始值,但是他们只能接受父组件的 @Status 修饰的变量

@Status circleColor='red'

// 双向同步子组件  组件内部  @Link circleColor:string
ChildCompLink({ circleColor: this.circleColor }) //还有一种写法是 circleColor:$circleColor

对象修改一层数据,也可以用。如果是嵌套的对象,需要整个赋值子对象,才能触发

ts
class FirstLevelClass {
    public a: string;
    public child:SecondLevelClass

    constructor(a:string,child:SecondLevelClass) {
        this.a = a;
        this.child=child;
    }
}


class SecondLevelClass {
    public b: string;

    constructor(b:string) {
        this.b=b
    }
}

// 父组件
@Component
export struct Parent{
    @State instance:FirstLevelClass=new FirstLevelClass('a',new SecondLevelClass('b'))
    build(){
        Column(){
            Text('父级:')
            TextInput({text:this.instance.a}).onChange((value:string)=>{
                this.instance.a=value
            })
            Child({ instance: this.instance })

        }

    }
}

// 子组件
@Component
export struct Child{
    @Link instance:FirstLevelClass
    build(){
        Column(){
            Text('子级:')
            TextInput({text:this.instance.a}).onChange((value:string)=>{
                this.instance.a=value
            })
        }

    }
}

@Status与@ObjectLink、@observe

@ObjectLink与@Link共功能一样,都是父子同步

@ObjectLink修饰的变量,其类型必须是@observe修饰的,可以不初始化值

当父组件修饰的是数组,但是传入子组件的不是这个数组,而是其中的元素。子组件想要改父级的大数组需要用@ObjectLink

@Status arr=[{count:1},{count:2}]

// 双向同步子组件  组件内部  @ObjectLink circleColor:xxx
ChildCompLink({ circleColor: this.arr[0] })

还有一种情况 嵌套对象,但是只将其子对象传入组件,使用@ObjectLink能同步父级与子级的数据

但是有一个问题:虽然父级的子对象数据更新了,但是由于是深层对象,父级页面没法触发更新

ts
class Student {
    public name: string
    public score: Score

    constructor(name: string, score: Score) {
        this.name = name
        this.score = score
    }
}

@Observed
class Score {
    public math: number

    constructor(math: number) {
        this.math = math
    }
}

@Entry
@Component
struct Parent {
    @State stu: Student = new Student('张三', new Score(50))

    build() {
        Column() {
            Text('父级')
            Column() {
                Text(`数学:${this.stu.score.math}`)
                Button(`数学 +1`).onClick(() => {
                    this.stu.score.math += 1 // 不使用@ObjectLink、@observe,这里操作不能触发子级更新
                })
            }

            Child({ score: this.stu.score })
        }.height('100%').alignItems(HorizontalAlign.Center).borderWidth('10lpx')
    }
}


@Component
struct Child {
    @ObjectLink score: Score

    build() {
        Column() {
            Text('子级')
            Column() {
                Text(`数学:${this.score.math}`)
                Button(`数学+1`).onClick(() => {
                    this.score.math += 1
                })
            }
        }
        .width('100%')
        .justifyContent(FlexAlign.Center).borderWidth('10lpx')
    }
}

内存存储

页面级存储

运行时存吃在内存中,并不是持久化的,在应用退出再次启动后会丢失

@LocalStorageProp(key) 单项、@LocalStorageLink(key)双向

因为,不确定初始化时 LocalStorage是否存在某个属性key,所以一定要初始化。如果LocalStorage没有对应key的话,将使用本地默认值初始化

  • 页面与其组件共享

    ts
    // 创建新实例并使用给定对象初始化
    let para:Record<string,number> = { 'PropA': 47 };
    let storage: LocalStorage = new LocalStorage(para);
    
    
    // 页面都是@Entry修饰的,可以传入一个LocalStorage实例。这样页面、和页面中的组件都可以通过@LocalStorageProp(key)、@LocalStorageLink(key)来访问到这个实例
    
    @Entry(storage)
    @Component
    struct CompA {
      // storLink1与LocalStorage中的'PropA'属性建立双向绑定
      @LocalStorageLink('PropA') storLink1: number = 1;
    
      build() {
        Column() {
          Child()
        }
      }
    }
    
    @Component
    struct Child {
      // storLink2与LocalStorage中的'PropA'属性建立双向绑定
      @LocalStorageLink('PropA') storLink2: number = 1;
    
      build() {
        Button(`Child from LocalStorage ${this.storLink2}`)
          // 更改将同步至LocalStorage中的'PropA'以及Parent.storLink1
          .onClick(() => this.storLink2 += 1)
      }
    }
  • 一个Ability中多个页面共享一个LocalStorage

    ts
    // EntryAbility.ts
    import UIAbility from '@ohos.app.ability.UIAbility';
    import window from '@ohos.window';
    
    
    export default class EntryAbility extends UIAbility {
    	private storage: LocalStorage = new LocalStorage();
      
      onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        // 设置当前内容字体为18
        this.storage.setOrCreate<number>('contentFontSize', FONT_SIZE);
      }
    
    	onWindowStageCreate(windowStage: window.WindowStage) {
      	// loadContent第二个参数可以注入LocalStorage
      	windowStage.loadContent('pages/Index', this.storage);
      }
    }

    这个Ability持有的页面,都可以通过getShared函数访问到实例。再通过@Entry将LocalStorage实例注入到页面中

    ts
    // 通过getShared接口获取stage共享的LocalStorage实例
    let storage = LocalStorage.getShared()
    
    @Entry(storage)
    @Component
    struct CompA {
      // can access LocalStorage instance using 
      // @LocalStorageLink/Prop decorated variables
      @LocalStorageLink('PropA') varA: number = 1;
    
      build() {
        Column() {
          Text(`${this.varA}`).fontSize(50)
        }
      }
    }

应用级存储

运行时存在内存中,并不是持久化的,在应用退出再次启动后会丢失

但是可以借助下一节的【PersistentStorage】实现持久化

@StorageProp(单向)、@StorageLink(双向)

因为,不确定初始化时 AppStorage是否存在某个属性key,所以一定要初始化

用法基本相同,区别:不用@Entry()注入,全局多个Ability都可以共享

ts
AppStorage.setOrCreate('PropA', 47);

@Entry
@Component
struct CompA {
  @StorageLink('PropA') storLink: number = 1;

  build() {
    Column({ space: 20 }) {
      Text(`From AppStorage ${this.storLink}`)
        .onClick(() => this.storLink += 1)
    }
  }
}

疑问:

  • 未创建设置 AppStorage ,直接使用@StorageLink(key)是否会报错

    不会报错,会自动创建AppStorage

持久化存储

在应用退出再次启动后,依然能保存选定的结果,是应用开发中十分常见的现象

PersistentStorage的作用是持久化指定的AppStorage属性,

PersistentStorage不能直接和页面通讯,页面需要通过AppStorage访问到PersistentStorage持久化的数据

PersistentStorage不支持存储嵌套对象(对象数组,对象的属性是对象等),这种数据数据变化后,无法回到存储中

持久化是一个同步操作,大量数据持久化会阻塞UI,所以大量数据持久化建议使用数据库api

ts
// 指定 持久化AppStorage中的PropA字段
//     如果PersistentStorage不存在PropA字段,则创建AppStorage并设置其PropA为47
//     如果PersistentStorage存在PropA字段,则创建AppStorage设置PropA为持久化存储的值
PersistentStorage.persistProp('PropA', 47)

@Entry
@Component
struct Index {
  // 这里的storLink修改,就直接修改AppStorage中PropA的值,进一步修改PersistentStorage存在PropA字段的值
  @StorageLink('PropA') storLink: number = 1;

  build() {
    Column({ space: 20 }) {
      Text(`AppStorage 的PropA ${this.storLink}`)
        .onClick(() => this.storLink += 1)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

首选项

Preferences会将该数据缓存在内存中,当用户读取时能够快速从内存中快速获取数据,当需要持久化时可以使用flush接口将内存中的数据写入持久化文件中

Preferences不适合存放过多的数据,适用的场景一般为应用保存用户的个性化设置(字体大小,是否开启夜间模式)等

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/data-persistence-by-preferences-0000001774120070

页面数据通讯

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/uiability-data-sync-with-ui-0000001774119978

UIContext

很多api的使用和具体UI实例的执行上下文相关的,在调用时,会追溯调用链跟踪到UI的上下文,来确定具体的UI实例。但是,若在非UI页面中或者一些异步回调中调用这类api,可能无法跟踪到当前UI的上下文,导致接口执行失败

这里分3中情况讨论:

  • ability中

    WindowStage/Window可以通过loadContent接口加载页面并创建UI的实例,并将页面内容渲染到关联的窗口中,所以UI实例和窗口是一一关联的。api会自动获取上下文,例如router跳转

    获取UI实例:windowClass.getUIContext

    js
    import UIAbility from '@ohos.app.ability.UIAbility';
    import window from '@ohos.window';
    import { BusinessError } from '@ohos.base';
    import { UIContext } from '@ohos.arkui.UIContext';
    
    export default class EntryAbility extends UIAbility {
      onWindowStageCreate(windowStage: window.WindowStage) {
        // 为主窗口加载对应的目标页面。
        windowStage.loadContent("pages/page2", (err: BusinessError) => {
          let errCode: number = err.code;
          if (errCode) {
            console.error('Failed to load the content. Cause:' + JSON.stringify(err));
            return;
          }
          console.info('Succeeded in loading the content.');
          // 获取应用主窗口。
          let windowClass: window.Window | undefined = undefined;
          windowStage.getMainWindow((err: BusinessError, data) => {
            let errCode: number = err.code;
            if (errCode) {
              console.error('Failed to obtain the main window. Cause: ' + JSON.stringify(err));
              return;
            }
            windowClass = data;
            console.info('Succeeded in obtaining the main window. Data: ' + JSON.stringify(data));
            // 获取UIContext实例。
            let uiContext: UIContext | null = null;
            uiContext = windowClass.getUIContext();
          })
        });
      }
    };
  • 页面中

    api会自动获取上下文,例如router跳转

    获取UI实例getContext(this)

    ts
    @Component
    export default struct xxx {
      bundleName: string = (getContext(this) as common.UIAbilityContext).applicationInfo.name
    }
  • 封装ets工具函数中

    // 这种建议函数通过入参的形式传入UI实例,这样函数既可以在页面使用,也可以在ability使用

    目前看社区很多工具函数,都是这样做的

    image-20240410104106461

获取UI实例后可使用runScopedTask方法,在回调函数内部是明确的UI上下文

ts
// cityLocationUtils.ets
uiContext?.runScopedTask(() => {
        // 持久化存储用户选择的城市信息
        PersistentStorage.persistProp('userSelectedCity', '')
        // 持久化存储 GPS 定位的城市信息
        PersistentStorage.persistProp('gpsLocationCity', '')

        const userCityLocal = AppStorage.get<string>('userCity')
        if (userCityLocal) {
          this.userCityLocal = JSON.parse(userCityLocal)
        }
      })
    })

t

文件

文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/app-file-overview-0000001820880057

窗口概念

文档

Stage模型中:

  • WindowStage:窗口管理器,管理各个Window

  • Window:当前窗口实例

窗口分为:

  • 主窗口、子窗口
  • 窗口

主窗口

核心是通过WindowStage控制窗口

窗口

流程

  • 获取窗口实例(window对象
    • 创建窗口(createWindow)
    • 获取当前应用内最上层的子窗口,若无应用子窗口,则返回应用主窗口 (getLastWindow)
    • 查找name所对应的窗口(findWindow,注:每个窗口创建时都有唯一的name)
  • 指定窗口位置(moveWindowTo)
  • 指定窗口大小(resize)
  • 窗口加载内容(setUIContent)
  • 窗口显示(showWindow)

例子:本例子是创建实例

注意,创建时指定不同windowType的区别:

  • TYPE_DIALOG 模态窗,只在当前应用内显示

  • TYPE_FLOAT 全局悬浮窗 ,应用切换到后台,仍然可以在屏幕中显示。需要申请ohos.permission.SYSTEM_FLOAT_WINDOW权限,而且仅在2in1设备使用,需要ACL跨级别申请。官方推荐使用画中画替代

注意,使用不同窗口加载同一页面会发现,窗口内容彼此独立,互不影响

ts
import window from '@ohos.window';
import { BusinessError } from '@ohos.base';

@Entry
@Component
struct test {
  	destroyWin(){
      this.win.destroyWindow((err: BusinessError) => {
        let errCode: number = err.code;
        if (errCode) {
          console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err));
          return;
        }
        console.info('Succeeded in destroying the window.');
      });
    }
    async createSubWin() {
        const win = await window.createWindow({
            name: '1',
            windowType: window.WindowType.TYPE_SYSTEM_ALERT,
            ctx: getContext(this)
        })

        win.moveWindowTo(500, 800, (err: BusinessError) => {
            let errCode: number = err.code;
            if (errCode) {
                console.error('Failed to move the window. Cause:' + JSON.stringify(err));
                return;
            }
            console.info('Succeeded in moving the window.');
        });
        win.resize(500, 700, (err: BusinessError) => {
            let errCode: number = err.code;
            if (errCode) {
                console.error('Failed to change the window size. Cause:' + JSON.stringify(err));
                return;
            }
            console.info('Succeeded in changing the window size.');
        });
        // 3.为子窗口加载对应的目标页面。
        win.setUIContent("pages/Index2", (err: BusinessError) => {
            let errCode: number = err.code;
            if (errCode) {
                console.error('Failed to load the content. Cause:' + JSON.stringify(err));
                return;
            }
            console.info('Succeeded in loading the content.');
            // 3.显示子窗口。
            (win as window.Window).showWindow((err: BusinessError) => {
                let errCode: number = err.code;
                if (errCode) {
                    console.error('Failed to show the window. Cause: ' + JSON.stringify(err));
                    return;
                }
                console.info('Succeeded in showing the window.');
            });
        });
      
    }

    build() {
        Button('创建').onClick(() => {
            this.createWin()
        })
       
      	Button('销毁').onClick(() => {
            this.destroyWin()
        })
    }
}

图像(待补充)

图像分为 三种类型 : uri、Resource、PixelMap

大纲:

文件(待补充)

关于Builder函数

基础用法

ts
@Builder
function navBuilder() {
  	// 不用build函数,直接写布局
    Column(){
      
    }
}

Builder函数内部可以是自定义组件

ts
@Builder
function navBuilder() {
  	// import进来的自定义组件
    NavBar()
}

Builder支持全局、局部(写在struct中)两种作用域

ts
@Builder
function navBuilder() {
  		
}
ts
@Component
struct xxx{
  @Builder
	navBuilder() {
  		
	}
}

Builder参数

参考

无参数(上面例子就是无参数的)

有参数

默认参数是值传递,无论入参是基本类型还是引用类型,Builder组件不会更新

ts
@Builder
function navBuilder(title: string) {// 这里形参是拷贝了一份副本
    Text(title)
}

@Entry
struct Index {
    @State title: string = '标题1'

    build() {
        Column() {
            // 使用builder,传入参数
            navBuilder(this.title)
          
          	// 这里不会改变Builder内的数据
            Button('修改标题').onClick(() => {
                this.title = '标题2'
            })
        }
    }
}

使用引用传参

这个比较绕,所以解释下。builder入参必须是 { key: 这里才是数据 } ,改变数据才会更新页面

这个数据是简单类型、对象都会更新页面

ts
interface BuilderData {
    data: Info
}

interface Info {
    title: string
}

@Builder
function navBuilder(builderData: BuilderData) { //这个builderData是 {key:数据}
    Text(builderData.data.title)
}

@Entry
struct Index {
    @State data: Info = { title: '标题1' }

    build() {
        Column() {
            // 使用builder,传入参数,参数形式必须是 {key: 数据}
            navBuilder({ data: this.data })
          
            // 修改数据
            Button('修改标题').onClick(() => {
                this.data.title = '标题2'
            })
        }
    }
}

使用Builder

作为组件

ts
@Builder
navBuilder(title:string) {
   //xxx
}

@component
struct xx{
  build(){
    this.navBuilder()
  }
}

作为参数(插槽用法)

子组件使用@BuilderParam定义的参数,父级可以将Builder传入子组件

子组件数据变更,Builder更新,这需要Builder使用引用传递

关于Builder中的this指向子组件(比如,在Builder中的元素的onClick回调中使用this)

如何访问父级this?

答:使用bind绑定父级的this

ts
Child({ customOverBuilderParam: this.overBuilder.bind(this) })

例子:

ts
class Info {
    label: string = ''
}

interface BuilderData {
    info: Info
}


@Entry
@Component
struct Parent {
    label: string = 'Parent'

    @Builder
    overBuilder($$: BuilderData) {
        Text($$.info.label)

    }

    build() {
        Column() {
            Child({ customOverBuilderParam: this.overBuilder })
        }
    }
}

@Component
struct Child {
    @State info: Info = { label: '初始值' }
    // 有参数类型
    @BuilderParam customOverBuilderParam: ($$: BuilderData) => void;

    @Builder
    customBuilder() {
    }

    build() {
        Column() {
            Button('修改内部的值').onClick(() => {
                this.info.label = '测试2'
            })
          
          	// 使用引用传递 {key:数据}
            this.customOverBuilderParam({ info: this.info })
        }
    }
}

image-20240308101119719

image-20240308101047347

Emitter

ts
import emitter from '@ohos.events.emitter';

struct Parent{
  build(){
    Button('按钮')
    	.onClick(()=>{
      	// 事件名
      	let innerEvent: emitter.InnerEvent = { eventId: 'event1' }
        // 数据
        let eventData: emitter.EventData = {
              data: {
                "colorTag": 1
              }
            }
        emitter.emit(innerEvent, eventData)
    	})
  }
}

struct Child{
  private index: number = 0;
  
  aboutToAppear() {
    //定义事件ID
    let innerEvent: emitter.InnerEvent = { eventId:'event1'}
    emitter.on(innerEvent, data => {
    	this.onTapIndexChange(data)
    })
  }
}

经验

初始值零值

  • 对于简单类型,初始类型零值可以设置为null,最好是一个有意义的值

  • 对枚举类型,可以设置None为零值

    ts
    enum xx{
      None
      Type1,
      Type2
    }
  • 最重要的是对于对象,如果设置为null,极有出现空指针,导致闪退现象

    对应对象建议使用class,

    ts
    class Person {
      name: string = '' //设置零值
    
      constructor(name?: string) {
        this.name = name|'' //如果是可选属性,必须在这里设置零值
      }
    }

对象key必须明确

比如,一个组件接收入参,点击组件返回参数

但是,返参字段与入参保持一致,不同入参会导致返参结果不确定,这需要通过if一个个取判断

if(backParam.a){
	// 存在a,就xxx
}

if(backParam.b){
	// 存在b,就xxx
}

组件入参或者函数入参,如果是对象。一定要保证对象的key是固定的,否则会很难写

比如,一个下拉框组件,传入了对象结构如下。复用组件时可能传入的是动态的配置,在组件每部不确定传入的对象的key到底可能是什么(arkts中不能for循环key,通过索引的方式取出对象的value)

{
	dataType:{xxx}
	weekType:{xxx}
}


{
	yearType:{xxx}
}

{
	dayType:{xxx}
}

建议可变数据,传入数组,组件内可以forEach读取

[
	{type:dataType,value:xxx}
	{type:weekType,value:xxx}
]


[
	{type:yearType,value:xxx}
]

[
	{type:dayType,value:xxx}
]

关于滚动事件

List、Scroll组件onScroll、onScrollFrameBegin区别

手指上滑,页面标签折叠;下滑展开

必须用onScrollFrameBegin,如果用onScroll下滑到底会向上回弹下,onScroll也会触发

ts
.onScroll((scrollOffset, scrollState) => {
    // 滚动触发,注意 滚动的回弹也会触发
})
.onScrollFrameBegin((offset: number) => {
  // 手指滑动触发
   return { offsetRemain: offset }
})

过渡

transition过渡 :https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-enter-exit-transition-0000001820879809

过渡需要加上animation指定动画(过渡时间,动画曲线),否则过渡会一闪而过

ts
xxx.transition(TransitionEffect.translate({ y: 1000 }).animation({ curve: curves.springMotion(0.6, 0.8) }))

if控制组件显隐,产生过渡

过渡出现消失是对称的,我们指定是出现的过渡效果

ts
if (this.isPresent) {
        Column() {
         	// xxx
        }.transition(TransitionEffect.translate({ x: '580lpx' }).animation({ duration: 300 }))
  			//  transition 指定过渡的方式,比如平移、旋转、放大
  			//  animation 指定动画的时长、动画的运动函数
}

也可以自定义不对称的过渡效果,使用asymmetric函数,第一个参数是出现的过渡、第二个参数是消失的过渡

ts
if (this.isPresent) {
        Column() {
         	// xxx
        }.transition(
          TransitionEffect.asymmetric(TransitionEffect.move(TransitionEdge.START), TransitionEffect.move(TransitionEdge.END))
        ).animation({ duration: 300 })
  			
}

动画

事件触发,动画跟变量绑定

ts
xxx.onClick(()=>{
	animateTo({ curve: curves.springMotion(),duration: 500 }, () => {
	  // 所有绑定isPresent的元素都会发生动画
  	this.isPresent = false;
	})
})

动画和组件绑定

模态转场

页面切换、半弹层弹出

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-modal-transition-0000001774120166

获取布局信息

xxx().id('id名')

注意:

  • 组件和获取布局代码可以不在一个est文件中
  • 单位是px
ts
import componentUtils from '@ohos.arkui.componentUtils'
import inspector from '@ohos.arkui.inspector'

struct xxx{
	listener: inspector.ComponentObserver = inspector.createComponentObserver('组件id')
  
  aboutToAppear(): void {
    this.listener.on('draw', () => {
      this.spaceHeight = componentUtils.getRectangleById('RankCarItem').size.height
      console.log('高度', String(componentUtils.getRectangleById('RankCarItem').size.height))
    })

  }
}

手势处理

需求:二级城市选择弹层,点击弹层中的城市则选中,右滑弹层则关闭

image-20240226162906075

ts
@Component
export default struct CitySecondLevelSelectList {
  onSelectSecondLevelCity?: (cityInfo: CityInfo) => void
  onClose?: () => void

  build() {
    Scroll() {
      Column() {
        if (this.data !== null) {
          ForEach(this.data, (cityName: string) => {
            Column() {
              Row() {
                Text(cityName)
              }
              .width('100%')
              .height('102lpx')

              Divider().width('540lpx').strokeWidth('1lpx').color('#eeeeee')
            }
            .width('100%')
            .padding({ left: '40lpx', right: '40lpx' })
            	// 绑定点击手势(TapGesture),点击选中城市
            .gesture(TapGesture().onAction(() => {
              this?.onSelectSecondLevelCity?.(cityName)
            }))

          })
        }

      }
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)
    .align(Alignment.Top)
    .scrollBar(BarState.Off)
    .scrollable(ScrollDirection.Vertical)
    // 绑定滑动手势(SwipeGesture),点击选中城市
    .priorityGesture(SwipeGesture({ direction: SwipeDirection.Horizontal }).onAction(() => {
      this?.onClose?.()
    }))

  }
}
  • 使用priorityGesture,priorityGesture优先级大于gesture,响应了priorityGesture的事件,gesture就不会响应了

  • parallelGesture,parallelGesture、gesture手势都会触发。在这个例子里,会出现滑动关闭时,同时选中城市的Bug

编译模式

可以区分是否release、debug模式

import BuildProfile from 'BuildProfile';

@State message: string = BuildProfile.BUILD_MODE_NAME

样式封装

开发时,经常对Text组件设置多个样式,每个Text组件都要写很繁琐

可以将多个样式封装成一个

注意:只支持全局作用域,即不能写在Struct里

ts
// 支持传参
@Extend(Text) function newTextStyle (fontWeight: number,
  textAttribute: number, fontSize: Resource, fontColor: Resource) {
    .fontWeight(fontWeight)
    .letterSpacing(textAttribute)
    .fontSize(fontSize)
    .fontColor(fontColor)
}

使用

ts
 Text('测试')
   .height(CommonConstants.LAUNCHER_TEXT_INTRODUCE_HEIGHT)
   .newTextStyle(xxx,xxx,xxx,xxx)

对于某组件有选中、未选中两种样式。但是需要对多个属性进行判断

ts
Text('测试')
	.backgroundColor(this.active?Color.Red,Color.Black)
  .fontSize(this.active?'30lpx','10lpx')

可以使用动态属性设置

ts
class modifier implements AttributeModifier<TextAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    if (this.isSelect) {
      instance.backgroundColor(Color.Red)
      instance.fontSize('30lpx')
    } else {
      instance.backgroundColor(Color.Black)
      instance.fontSize('10lpx')
    }
  }
}

@Entry
@Component
struct xx{
  @State modifier: MyButtonModifier = new MyButtonModifier()
  @State isSelect:boolean=false
  
  build(){
    Text('测试')
    	.onClick(()=>{
      		isSelect=!this.isSelect
			})
      .attributeModifier(this.modifier)
  }
}

Scroll与List的偏移量

需求:

image-20240301162445379

  • 当元素个数小于等于6时,均分空间

  • 大于6时,可以滚动

注意点:

ts
// 每帧开始的时候触发
.onScrollFrameBegin((offset: number, state: ScrollState) => {
        if (state === ScrollState.Scroll) {
          // 查询Row这个组件相对于其父级Scroll滚动的偏移量
          const offsetX = componentUtils.getRectangleById('RowContainer').localOffset.x
          this.scrollLeftOffset = -(px2lpx(offsetX) / px2lpx(this.overFlowWidth)) * 16
        }
        return { offsetRemain: offset }
      })

// 1、为什么不用onScroll?
//    实际测试发现onScroll并不是按帧触发,offsetX查询并不及时,即可能出现Scroll滚动到起始位置时onScroll并没触发,offsetX最后的值不是0

// 2、为什么不用 offset 属性,totalOffset=totalOffset + offset累计求和计算?
//    实际测试发现Scroll滚动到起始位置时可能totalOffset最后的值不是0,而是逼近0的小数,例如:0.0000xxxxx

代码:

ts
import componentUtils from '@ohos.arkui.componentUtils'
import inspector from '@ohos.arkui.inspector'

@Preview
@Component
export default struct CarStyleJellyBean {
  @State List: string[] = ['', '', '', '', '', '', '', '',]
  @State scrollLeftOffset: number = 0
  @State isScroll: boolean = false
  overFlowWidth: number = 0
  RowContainerListener: inspector.ComponentObserver = inspector.createComponentObserver('RowContainer')
  ScrollContainerListener: inspector.ComponentObserver = inspector.createComponentObserver('ScrollContainer')

  aboutToAppear(): void {
    this.ScrollContainerListener.on('draw', () => {
      let scrollContainerWidth = componentUtils.getRectangleById('ScrollContainer').size.width
      this.RowContainerListener.on('draw', () => {
        this.overFlowWidth = componentUtils.getRectangleById('RowContainer').size.width - scrollContainerWidth
      })
    })

  }

  build() {
    Column() {
      Scroll() {
        Row({ space: this.List.length > 6 ? '25lpx' : '' }) {
          ForEach(this.List, () => {
            Column() {
              Image($r('app.media.app_icon'))
                .width('48lpx')
                .height('48lpx')
              Text('车主点评')
                .fontSize('22lpx')
                .fontColor('#0f1d37')
                .fontWeight(FontWeight.Regular)
                .margin({ top: '8lpx' })
            }
          })
        }
        .RowScrollStyle(this.List.length > 6)
        .id('RowContainer')
      }
      .id('ScrollContainer')
      .width('100%')
      .backgroundColor(Color.White)
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .align(Alignment.Start)
      .onScrollFrameBegin((offset: number, state: ScrollState) => {
        if (state === ScrollState.Scroll) {
          const offsetX = componentUtils.getRectangleById('RowContainer').localOffset.x
          this.scrollLeftOffset = -(px2lpx(offsetX) / px2lpx(this.overFlowWidth)) * 16
        }
        return { offsetRemain: offset }
      })


      Row() {
        Row()
          .width('18lpx')
          .height('6lpx')
          .borderRadius('3lpx')
          .backgroundColor('#0f1d37')
          .position({ x: `${this.scrollLeftOffset}lpx` })
      }
      .width('34lpx')
      .height('6lpx')
      .borderRadius('3lpx')
      .backgroundColor('#e0e0e0')
      .margin({ top: '12lpx' })

    }
    .width('100%')
    .padding({ top: '12lpx', bottom: '12lpx' })
  }
}

@Extend(Row)
function RowScrollStyle(isScroll: boolean) {
  .width(isScroll ? '' : '100%')
  .justifyContent(isScroll ? FlexAlign.Start : FlexAlign.SpaceBetween)
}

注意:

一定要取消监听,遇到过反复创建崩溃的问题

// 通过句柄向对应的查询条件取消注册回调,由开发者自行决定在何时调用。
// this.listener.off('layout', OffFuncLayout)
// this.listener.off('draw', OffFuncDraw)
ts
import inspector from '@ohos.arkui.inspector'
import componentUtils from '@ohos.arkui.componentUtils'

@State vis: boolean = false
@State pageMainContentHeight: number = 0

listener: inspector.ComponentObserver = inspector.createComponentObserver('pageMainContent')

aboutToAppear(): void {
    this.listener.on('draw', () => {
      this.pageMainContentHeight = componentUtils.getRectangleById('pageMainContent').size.height  // 单位是px
    })
  }

@Builder
popupBuilder() {
    Scroll() {
      Column() {
        //xxx
      }
    }
    .width('100%')
    .height(`${this.pageMainContentHeight}px`)
    .backgroundColor(Color.White)
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .align(Alignment.Top)

}



xxx.bindPopup(this.vis, {
        builder: this.popupBuilder,
        placement: Placement.Bottom,
        enableArrow: false,
        popupColor: Color.Transparent,
        mask: true,
        radius: 0,
        shadow: {
          radius: 0,
        },
        backgroundBlurStyle: BlurStyle.NONE,
        autoCancel: true,
  			// 设置为false(默认值),底部导航条会有避让。
  			// true会完全遮住,但是textInput、Search使用键盘输入数据时,会自动关闭
  			// showInSubWindow: true,  
        onStateChange: (e) => {
          console.log(`onSearchAssociateVis 弹窗回调${e.isVisible}`)
          this.searchAssociateVis = e.isVisible
        }
      })

pageMainContent高度需要算对,如果算多了,会以屏幕顶部为起点

让人误以为Placement.Bottom失效了

image-20240306175411073

Canvas

绘制一个圆球,背景渐变

image-20240313155247952

ts
@Entry
@Component
struct Index {
    private settings: RenderingContextSettings = new RenderingContextSettings(true)
    private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
    // 设置尺寸
    private canvasWidth = px2vp(lpx2px(44))
    private canvasHeight = px2vp(lpx2px(44))
    private lineWidth = px2vp(lpx2px(2))

    build() {
        Canvas(this.context)
            .width(this.canvasWidth)
            .height(this.canvasHeight)
            .onReady(() => {
                // 设置渐变
                let grad = this.context.createLinearGradient(this.canvasWidth / 2, 0, this.canvasWidth / 2, this.canvasHeight)
                grad.addColorStop(0.5, 'red')
                grad.addColorStop(0.5, 'blue')
                this.context.fillStyle = grad
                // 绘制轮廓
                this.context.arc(this.canvasWidth / 2, this.canvasWidth / 2, this.canvasWidth / 2 - this.lineWidth, 0, 6.28)
                // 填充轮廓内部
                this.context.fill()
                // 轮廓设置
                this.context.strokeStyle = '#cccccc'
                this.context.lineWidth = this.lineWidth
                this.context.stroke()
            })
    }
}

页面布局区域

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-develop-apply-immersive-effects-0000001820435461

关于UUID

官方回复:

您好,udid需要系统权限 三方应用不支持获取
为更好的保障用户隐私安全,Next版本不再提供UUID,建议伙伴根据使用场景,考虑使用AAID或OAID替代
  • AAID 应用匿名标识符

    不会触发弹窗提示

    应用卸载重装、调用删除AAID接口、清理应用数据、恢复出厂设置都会丢失

    但是可以配合Asset存储,确保卸载重装后获取的id是同一个。在Asset Store Kit(关键资产存储服务)保存业务关键数据,并设置IS_PERSISTENT为true(在应用卸载时是否需要保留关键资产,需要权限: ohos.permission.STORE_PERSISTENT_DATA),应用卸载重装后仍然可以查询到之前保存的数据(注意:在查询asset时,如果从来没保存过某个资产,首次查询会抛出24000008(数据库操作失败)异常,因此在查询时除了判断取出的数组长度是否为0,还要专门处理这个异常码)

  • OAID广告标识服务

    会触发弹窗提示

    仅恢复出厂设置会丢失,注意卸载不会丢失

第二种方案会触发弹窗提示 "是否允许 OAID 访问应用跟踪",用户体验不太好。所以更推荐第一中方案

突发奇想

vconsole工具

形式

  • 子窗口设计一个 vconsole
  • 项目中增加一个ability,build-profile时要排除在打包产物外

是否能使用 hcd 实时获取日志,在vconsole中作格式化处理

疑问

popup 内容设置 100% 到底继承哪里

Tab子组件100%高度是哪里

数据更新疑惑

ObservedArray修饰的的数组。子元素都是用@Observe修饰的,这个子元素传入子组件的属性,必须用@ObjectLink修饰。但是我发现如果用builder传入参数就不用任何修饰符
imageModelList: ObservedArray<ImageViewTouch.Model> = []
子组件入参是@Link类型,如果 @State list:xx=[] ,forEach遍历list的子元素,如果传入子组件会提示常规属不能作为@Link属性的入参

查阅文档:@State, @StorageLink和@Link 才能作为入参。可是这几个装饰器只能在页面用


但是@Observed可以脱离页面用,但是用这种方式装饰数组,子组件的入参就必须是@ObjectLink(也是只能在页面用)
@Observed
export class ObservedArray<T> extends Array<T> {
  constructor(args?: T[]) {
    if (args instanceof Array) {
      super(...args)
    } else {
      super()
    }
  }
}

为啥我的向普通数组中推入数据,页面就能更新?但是写的demo就不行

UI更新装饰器

  • 仅修饰页面变量(状态变量)

    以@State为例子:

    @State a 如果接受的是其他状态变量传过来的,其他状态变量改变a也会更新UI

    @State a = x 如果接受的是普通变量,只会初始化a的值,更新变量x不会更新UI。必须直接操作a才会更新,this.a=2

    @State a = 对象 如果接受的是值,比如可操作的对象,直接修改的这也是不会更新的(@Track可以提供更新能力)

    @State (接受所有状态变量+普通变量)
    @Prop  (接受所有状态变量+普通变量)
    @Link  (仅接受状态变量@State、@StorageLink、@Link修饰)
    @ObjectLink (仅接受 @Observed ,注意:其他装饰器接受普通变量数据都只会初始化,修改变量并不会更新UI。唯独@ObjectLink可以接受对象数据,且提供了修改数据更新UI的能力)
    
    
    
    其他(@Provide、@Consume、@StorageLink、@StorageProp、@LocalStorageLink和@LocalStorageProp)
  • 仅修饰非页面(修饰对象数据)

    • @Track

      仅能用于修饰Class的非静态属性

    • @Observed

      仅能用于修饰Class

      @Observed
      export class ObservedArray<T> extends Array<T> {
        constructor(args?: T[]) {
          if (args instanceof Array) {
            super(...args)
          } else {
            super()
          }
        }
      }

UI更新

  • 页面变量装饰器,修改会更新页面(深层次对象,只有修改第一次才会更新页面)

  • @Track

    @Track修饰的Class属性,实例化后的对象可直接在UI中使用

    UI更新方式:

    方式1:修改状态修饰符的变量(见【1】,本质是修改了状态变量的第一层,所以可以获得更新,状态变量修改后是同步修改对象的,下面例子可以印证)

    js
    export class BaseInfoModel {
        subTitle: string = 'subTitle初始值'
        getSubTitle() {
            return this.subTitle
        }
    }
    
    @Entry
    @Component
    export default struct Parent {
        @State model: BaseInfoModel = new BaseInfoModel()
    
        build() {
            Column() {
    
                Text(this.model.subTitle)
                Button('修改subTitle').onClick(() => {
                    this.model.subTitle = 'subTitle更新'
                })
    
                Button('获取model对象内部数据').onClick(() => {
                    console.log(this.model.getSubTitle()) //1、 确实对象内部修改了
                })
    
                Child({ model: this.model })
    
    
            }
        }
    }
    
    @Component
    struct Child {
        @Link model: BaseInfoModel
    
        build() {
            Column() {
                Text(this.model.subTitle) // 对象修改了,这里也会更新
            }
        }
    }

    方式2:修改@Track修饰的属性(见【2】,本质是@Track提供等能力,可以让直接修改值达到触发更新的目的。注意@Track没有深层监测能力,如果是Class的属性类型是数组、对象则推荐直接赋值属性达到更新的目的。对于数组push子元素、对象的一层属性修改也是不更新的)

    非@Track修饰的Class属性,直接在UI中使用会直接报错

    ts
    export class BaseInfoModel {
        @Track title: string = '初始值'
    
        // 类内部修改也会触发更新
        setTitle(val: string) {
            this.title = val
        }
    }
    
    @Entry
    @Component
    export default struct Index {
        // 这里必须用@State修饰
        @State model: BaseInfoModel = new BaseInfoModel()
    
        build() {
            Column() {
                Text(this.model.title)
                Button('直接修改model.title').onClick(() => {
                    this.model.title = '内容1' //【1】 页面中修改会触发更新
                })
                Button('model内部修改title').onClick(() => {
                    this.model.setTitle('内容2') //【2】 通过类方法,修改对象属性也会触发更新
                })
            }
        }
    }

    注意1:下面这种直接取出model值的方式,pageTitle只初始化为title的值,但是改title是不会更新UI的

    js
    @State model: BaseInfoModel = new BaseInfoModel()
    @State pageTitle: string = this.model.title

    注意2:如果需要监听model数据的变化(例如:城市数据变化了,页面除了要展示数据,还要弹出提示窗口)

    js
    // 方式1: @watch监听
    @State @Watch('onChangeModel') model: BaseInfoModel = new BaseInfoModel()
    
    onChangeModel() {
      console.log(this.model.title)
    }
    
    // 方式2:如果cityInfo的变更明确只会在model内的方法中赋值(不会再页面修改),可以在model中定义notify方法,赋值时调用notify方法,通知页面 (不推荐,页面上直接修改变量无法监听到)
    @State model: BaseInfoModel = new BaseInfoModel()
    aboutToAppear(): void {
      this.model.notify(()=>{ // 收到通知 })
    }

    目前发现对象传入子组件,需要子组件使用@Link修饰(使用@Prop实际上是拷贝了对象)

    Child(model:this.model)
    
    Child中需要用@Link model
  • @Observed

    与页面状态修饰符@ObjectLink强关联,当Observed修饰的对象第一层变化时,触发@ObjectLink修饰的页面变量的UI更新

    正常情况下@State a1:string=a,修改数据源是无效的,但是@Observed可以让数据源的修改生效

    ts
    @Observed
    class ClassA {
       a: number=1
    }
    
    // 父级
    @State classa =new ClassA()
    
    // 子组件用@ObjectLink接受到了父级传@Observed修饰的对象,修改对象的一级属性才行
    // @ObjectLink不能本地初始化,必须从父级传入
    @ObjectLink a: ClassA
  • @Track 与 @Observed对比

    相同点:

    两者都没有深层次监听的能力,但是@Observed可直接修饰Class,所以可以用ObservedArray的方式让数组的对象元素得到第一层的监听能力

    不同点:

    // 这个Class实例化的对象,第一层全部属性只要变动就会全部触发更新
    // 页面必须需要使用@ObjectLink接收,例如 @ObjectLink xx = new xx()
    @Observed
    Class xx{
    	a:string,
    	b:string,
    	c:string
    }
    
    
    
    // 这个Class实例化的对象,仅仅@Track修饰的属性变化才会触发页面更新且,仅触发绑定该变量位置的UI更新。 例如:改a数据,页面上仅a处更新UI,b处不更新。改c页面不更新
    // 页面可以用所以状态变量接收,例如 @Status model = new xx()
    // @Status c1 =model.c ,数据model.c更新页面是不会更新的
    
    Class xx{
    	@Track a:string,
    	@Track b:对象类型[], // 目前发现无论是直接赋值this.b=xxx,还是push元素都是更新的
    	c:string
    }
    
    
    // 两者都不用呢?尝试复现宇航的逻辑
    Class xx{
    	b:string,
    	c:string[]
    	d:ObserveArray<对象类型>
    	
    	getDataB(){
    		b=xxx
    	}
    	
    	getDataC(){
    		c=xxx
    	}
    	
    	getDataD(){
    		for 推入数据
    	}
    }
    
    private model = new xx() // 只会初始化数据,但是没有更新UI的能力
    @State b1 = this.model.b // 取变量赋值给状态变量
    @State c1 = this.model.c // 取变量赋值给状态变量
    @State d1 = this.model.d // d是数组,这里相对于把一个 ObserveArray<对象类型> 赋值给d1
    
    
    aboutToAppear(){
    	this.model.getDataB() // 不更新
    	this.model.getDataC() // 不更新
    	this.model.getDataD() // 更新
    }
    
     ForEach(this.d1, (item: 数组子元素对象的类型) => {
       this.builder(item) // <?疑问点>为啥用builder,是不是因为item实际上是Observe修饰的,子组件只能用@ObjectLink接收
    })
    
    // 也就是页面之所以更新和model完全没关系,重点是因为@State d才更新
    // 相当于还是页面持有数据并更新。model只是作为数据源传入页面,这种方式本质上更像是总线的思想,即直接不持有页面数据
    // 使用@Track的方式,才是pinia的思想,能直接提供响应式能力

问题1:加上数组属性,基础类型属性就会报错没有@Track修饰

ts
interface BaseInfo {
    title: string
}

export class BaseInfoModel {
    // 基础类型
    subTitle: string = 'subTitle初始值'

    // 对象数组《这里去掉》
    // @Track baseInfoList: BaseInfo[] = []

    // 类内部修改也会触发更新
    setTitle(val: string) {
        this.subTitle = val
    }
}

@Entry
@Component
export default struct Index {
    // 这里必须用@State修饰
    @State model: BaseInfoModel = new BaseInfoModel()

    build() {
        Column() {
            Text(this.model.subTitle)
            Button('直接修改model.title').onClick(() => {
                this.model.subTitle = '内容1' //【1】 页面中修改会触发更新
            })
            Button('model内部修改title').onClick(() => {
                this.model.setTitle('内容2') //【2】 通过类方法,修改对象属性也会触发更新
            })
        }
    }
}

最后更新时间:

Released under the MIT License.