官网 https://developer.huawei.com/consumer/cn/
一、状态管理
在声明式UI中,是以状态驱动视图更新
1.@State
状态(State):指驱动视图更新的数据,被装饰器标记的变量
视图(View):基于UI描述渲染得到用户界面
说明
@State装饰器标记的变量必须初始化,不能为空值。
@State支持Object,class,string,number,boolean,enum类型以及这些类型的数组。
嵌套类型以及数组中的对象属性无法触发视图更新。
TypeScript
class Person{
name:string
age:number
gf?:Person
constructor(name:string,age:number,gf?:Person) {
this.name=name;
this.age=age;
this.gf=gf;
}
}
@Entry
@Component
struct Index {
@State idx:number=1
@State name:string='Jack'
@State age:number=10
@State p:Person=new Person('Tony',20)
@State p1:Person=new Person('Tom',20,new Person('Lily',18))
@State gfs:Person[]=[
new Person('露西',18),
new Person('安娜',19)
]
build() {
Column(){
Text(`${this.name}${this.age}`)
.fontSize(40)
.onClick(()=>{
this.age++
})
Text(`${this.p.name}${this.p.age}`)
.fontSize(40)
.onClick(()=>{
this.p.age++
})
if (this.p1.gf)Text(`${this.p1.gf.name}${this.p1.gf.age}`)
.fontSize(40)
.onClick(()=>{
//嵌套对象,对象内的属性发生变更无法触发视图变更
if (this.p1.gf) this.p1.gf.age++
})
ForEach(this.gfs,(item:Person,index:number)=>{
Row(){
Text(`${item.name}:${item.age}`)
.fontSize(40)
.onClick(()=>{
//数组中的对象属性无法触发视图更新
item.age++
//添加删除重新赋值才会触发视图更新
this.gfs[index]=new Person(item.name,item.age)
})
Button('删除')
.onClick(()=>{
this.gfs.splice(index,1)
})
}
.justifyContent(FlexAlign.SpaceBetween)
})
Button('添加女友')
.onClick(()=>{
this.gfs.push(new Person('女友'+this.idx++,18))
})
}
.width('100%')
.height('100%')
}
}
2.@Props和@Link
当父子组件之间需要数据同步时,可以使用@Props和@Link装饰器
|-----------|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|
| | @Props | @Link |
| 同步类型 | 单向同步 | 双向同步 |
| 允许装饰的变量类型 | 1.@Props只支持string,number,boolean,enum类型 2.父组件对象类型,子组件是对象属性 3不可以是数组,any | 1.父子类型一致string,number,boolean,enum,object,class,以及它们的数组 2.数组中的元素增删替换会引起更新 3.嵌套类型以及数组中的对象属性无法触发视图更新 |
| 初始化方式 | 不允许子组件初始化 | 父组件传递,禁止子组件初始化 |
TypeScript
@Component
struct TaskStatistics{
@Prop totalTask:number
@Prop finishTask:number
build() {
Row(){
Text('任务进度:')
.fontSize(30)
Stack({ alignContent: Alignment.Center }) {
Progress({ value: this.finishTask, total: this.totalTask, type: ProgressType.Ring })
.width(100)
.height(100)
Text(`${this.finishTask}/${this.totalTask}`)
.fontSize(30)
}
}
.width('100%')
.height(200)
.borderRadius(10)
.backgroundColor('#fff')
.justifyContent(FlexAlign.SpaceAround)
}
}
@Component
struct TaskList{
@Link stat:Station
@State tasks:Task[]=[]
build() {
Column(){
Row(){
Button('新增任务')
.onClick(()=>{
this.tasks.push(new Task())
this.stat.totalTask=this.tasks.length
})
.width(160)
}
.margin({top:10})
List({space:10}) {
ForEach(this.tasks, (item: Task, index: number) => {
ListItem(){
Row() {
Text(`${item.name}`)
.fontSize(20)
// .finishedStyle()
Checkbox()
.select(item.finish)
.onChange((val) => {
item.finish = val;
this.stat.finishTask = this.tasks.filter((item) => {
return item.finish
}).length
})
}
.margin({ top: 10 })
.width('100%')
.height(50)
.borderRadius(10)
.backgroundColor('#fff')
.justifyContent(FlexAlign.SpaceBetween)
}
.swipeAction({end:this.DeleteButton(index)})
})
}
.layoutWeight(1)
}
}
@Builder DeleteButton(index:number) {
Button() {
Image($r('app.media.ic_public_delete_filled'))
.width(40).height(40)
}
.width(40).height(40).type(ButtonType.Circle)
.onClick(()=>{
this.tasks.splice(index,1)
this.stat.totalTask=this.tasks.length
this.stat.finishTask = this.tasks.filter((item) => {
return item.finish
}).length
})
}
}
class Station{
totalTask:number=0
finishTask:number=0
}
class Task{
static id:number=1
name:string='任务'+Task.id++
finish:boolean=false
}
@Extend(Text) function finishedStyle(){
.decoration({type:TextDecorationType.LineThrough})
.fontColor('#b1b2b1')
}
@Entry
@Component
struct Index {
@State stat:Station=new Station()
@State tasks:Task[]=[]
build() {
Column() {
//任务进度卡片
TaskStatistics({ totalTask: this.stat.totalTask, finishTask: this.stat.finishTask })
// 任务列表
TaskList({stat:$stat })
}
.width('100%')
.height('100%')
.backgroundColor('#f6f6f6')
.padding(20)
}
}
3.@Provide和Consume
可以跨组件提供类似于@Props和@Link的双向同步
TypeScript
@Component
struct TaskStatistics{
@Consume stat:Station
build() {
Row(){
Text('任务进度:')
.fontSize(30)
Stack({ alignContent: Alignment.Center }) {
Progress({ value: this.stat.finishTask, total: this.stat.totalTask, type: ProgressType.Ring })
.width(100)
.height(100)
Text(`${this.stat.finishTask}/${this.stat.totalTask}`)
.fontSize(30)
}
}
.width('100%')
.height(200)
.borderRadius(10)
.backgroundColor('#fff')
.justifyContent(FlexAlign.SpaceAround)
}
}
@Component
struct TaskList{
@Consume stat:Station
@State tasks:Task[]=[]
build() {
Column(){
Row(){
Button('新增任务')
.onClick(()=>{
this.tasks.push(new Task())
this.stat.totalTask=this.tasks.length
})
.width(160)
}
.margin({top:10})
List({space:10}) {
ForEach(this.tasks, (item: Task, index: number) => {
ListItem(){
Row() {
Text(`${item.name}`)
.fontSize(20)
// .finishedStyle()
Checkbox()
.select(item.finish)
.onChange((val) => {
item.finish = val;
this.stat.finishTask = this.tasks.filter((item) => {
return item.finish
}).length
})
}
.margin({ top: 10 })
.width('100%')
.height(50)
.borderRadius(10)
.backgroundColor('#fff')
.justifyContent(FlexAlign.SpaceBetween)
}
.swipeAction({end:this.DeleteButton(index)})
})
}
.layoutWeight(1)
}
}
@Builder DeleteButton(index:number) {
Button() {
Image($r('app.media.ic_public_delete_filled'))
.width(40).height(50)
}
.width(40).height(50).type(ButtonType.Circle)
.onClick(()=>{
this.tasks.splice(index,1)
this.stat.totalTask=this.tasks.length
this.stat.finishTask = this.tasks.filter((item) => {
return item.finish
}).length
})
}
}
class Station{
totalTask:number=0
finishTask:number=0
}
class Task{
static id:number=1
name:string='任务'+Task.id++
finish:boolean=false
}
@Extend(Text) function finishedStyle(){
.decoration({type:TextDecorationType.LineThrough})
.fontColor('#b1b2b1')
}
@Entry
@Component
struct Index {
@Provide stat:Station=new Station()
@State tasks:Task[]=[]
build() {
Column() {
//任务进度卡片
TaskStatistics()
// 任务列表
TaskList()
}
.width('100%')
.height('100%')
.backgroundColor('#f6f6f6')
.padding(20)
}
}
4.@Observed和@ObiectLink
@Observed和@ObiectLink装饰器用于在涉及嵌套对象或数组元素为对象的场景中进行双向数据同步
TypeScript
//任务进度卡片组件
@Component
struct TaskStatistics{
@Consume stat:Station
build() {
Row(){
Text('任务进度:')
.fontSize(30)
Stack({ alignContent: Alignment.Center }) {
Progress({ value: this.stat.finishTask, total: this.stat.totalTask, type: ProgressType.Ring })
.width(100)
.height(100)
Text(`${this.stat.finishTask}/${this.stat.totalTask}`)
.fontSize(30)
}
}
.width('100%')
.height(200)
.borderRadius(10)
.backgroundColor('#fff')
.justifyContent(FlexAlign.SpaceAround)
}
}
// 任务列表项组件
@Component
struct TaskItem{
@ObjectLink item:Task
onTaskChange:()=>void=()=>{}
build() {
Row() {
if(this.item.finish) {
Text(`${this.item.name}`)
.fontSize(20)
.finishedStyle()
}else{
Text(`${this.item.name}`)
.fontSize(20)
}
Checkbox()
.select(this.item.finish)
.onChange((val) => {
this.item.finish = val;
this.onTaskChange()
})
}
.margin({ top: 10 })
.width('100%')
.height(50)
.borderRadius(10)
.backgroundColor('#fff')
.justifyContent(FlexAlign.SpaceBetween)
}
}
// 任务列表组件
@Component
struct TaskList{
@Consume stat:Station
@State tasks:Task[]=[]
handleTaskChange(){
this.stat.finishTask = this.tasks.filter((item) => {
return item.finish
}).length
this.stat.totalTask=this.tasks.length
}
build() {
Column(){
Row(){
Button('新增任务')
.onClick(()=>{
this.tasks.push(new Task())
this.handleTaskChange()
})
.width(160)
}
.margin({top:10})
List({space:10}) {
ForEach(this.tasks, (item: Task, index: number) => {
ListItem(){
TaskItem({ item: item, onTaskChange: this.handleTaskChange.bind(this) })
}
.swipeAction({end:this.DeleteButton(index)})
})
}
.layoutWeight(1)
}
}
@Builder DeleteButton(index:number) {
Button() {
Image($r('app.media.ic_public_delete_filled'))
.width(40).height(50)
}
.width(40).height(50).type(ButtonType.Circle)
.onClick(()=>{
this.tasks.splice(index,1)
this.handleTaskChange()
})
}
}
class Station{
totalTask:number=0
finishTask:number=0
}
@Observed
class Task{
static id:number=1
name:string='任务'+Task.id++
finish:boolean=false
}
@Extend(Text) function finishedStyle(){
.decoration({type:TextDecorationType.LineThrough})
.fontColor('#b1b2b1')
}
@Entry
@Component
struct Index {
@Provide stat:Station=new Station()
build() {
Column() {
//任务进度卡片
TaskStatistics()
// 任务列表
TaskList()
}
.width('100%')
.height('100%')
.backgroundColor('#f6f6f6')
.padding(20)
}
}
二、页面路由
页面路由是指在应用程序中实现不同页面之间的跳转和传递函数。
页面栈的最大容量上限为32 个页面,使用,router.clear()方法可以清空页面栈,释放内存。
1.Router有两种页面跳转模式
router.pushUrl()目标页不会替换当前页,而是压入栈,因此可以用router.back()返回当前页
router.replaceUrl目标页替换当前页,当前页面会被销毁并释放资源,无法返回当前页
2.Router有两种页面实例模式
standard标准实例模式,每一次跳转都会新建一个目标页压入栈顶,默认就是这种模式
single单实例模式,如果目标页已经在栈中,则离栈顶最近的url页面会被移动到栈顶并重新加载
3.使用步骤
TypeScript
//1.导入harmonyos提供的Router模块
import router from '@ohos.router';
//利用router实现跳转、返回操作
router.pushUrl(
{
url:'pages/ImagePage',
params:{id:1}
},
router.RouterMode.Single,
err=>{
if(err){
console.log(err)
}
}
}
//获取传递过来的参数
params:any=router.getParams()
//返回上一页
router.back()
//返回到指定页,并携带参数
router.back(
{
url:'pages/Index',
params:{id:10}
}
)
4.示例
TypeScript
import router from '@ohos.router';
class Router{
url:string
title:string
constructor(url:string,title:string) {
this.url=url;
this.title=title;
}
}
@Entry
@Component
struct Index {
@State message:string='页面列表'
private routers:Router[]=[
new Router('pages/ComIndex','图片页面'),
new Router('pages/CustomIndex','自定义组件页面'),
new Router('pages/ForEachIndex','渲染页面'),
new Router('pages/ProvideIndex','任务列表页面')
]
build() {
Column() {
//标题
Text('页面列表')
.fontSize(35)
//列表
List(){
ForEach(this.routers,(router:Router,index)=>{
ListItem(){
this.RouterItem(router,index+1)
}
})
}
}
.width('100%')
.height('100%')
.backgroundColor('#f6f6f6')
.padding(20)
}
@Builder RouterItem(r:Router,i:number){
Row(){
Text(i+'.')
.fontSize(30)
Blank()
Text(r.title)
.fontSize(30)
}
.width('100%')
.height(60)
.padding(10)
.margin({bottom:20})
.backgroundColor('#36d')
.borderRadius(25)
.onClick(()=>{
router.pushUrl(
{
url:r.url,
params:{idx:i}
},
router.RouterMode.Single,
err=>{
if(err){
console.log(err.message)
}
}
)
})
}
}
TypeScript
//Header组件可以返回上一页
import router from '@ohos.router';
interface Params {
idx?: number; // idx 是可选的,可能存在也可能不存在
}
@Component
export struct Header {
private title:ResourceStr|string=''
@State params: Params = router.getParams() as Params; // 强制类型转换为 Params
build(){
Row(){
Image($r('app.media.left'))
.width(30)
.onClick(()=>{
router.showAlertBeforeBackPage({message:'确定要返回吗?'})
router.back()
})
if(this.params &&this.title) {
Text(this.params.idx +'.'+this.title)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
Blank()
Image($r('app.media.reset'))
.width(24)
.onClick(()=>{})
}
.width('100%')
.height(30)
.backgroundColor('#f6f6f6')
}
}
三、动画
属性动画、显示动画、组件转场动画
1.属性动画
属性动画是通过设置组件的animation属性来给组件添加动画,当前组件的width,height,opacity,backgroundColor,scale,rotate,translate属性时,可以实现渐变过渡效果。
TypeScript
import router from '@ohos.router';
@Entry
@Component
struct FishIndex {
@State fishX:number=200
@State fishY:number=100
@State isBegin:boolean=false
@State angle:number=0
@State src:Resource=$r('app.media.fish')
build() {
Row() {
Button('返回')
.position({ x: 0, y: 0 })
.onClick((event: ClickEvent) => {
router.back()
})
if(!this.isBegin){
Button('开始游戏')
.onClick(()=>{
this.isBegin=true;
})
}else{
Image(this.src)
.width(50)
.height(50)
.position({x:this.fishX,y:this.fishY})
.rotate({angle:this.angle,centerX:'50%',centerY:'50%'})
.animation({duration:500})
Row(){
Button('←')
.backgroundColor('#20101010')
.onClick((event: ClickEvent) => {
this.fishX-=20
this.src=$r('app.media.fish')
})
Column(){
Button('↑')
.backgroundColor('#20101010')
.onClick((event: ClickEvent) => {
this.fishY-=20
})
Button("↓")
.backgroundColor('#20101010')
.onClick((event: ClickEvent) => {
this.fishY+=20
})
}
Button('→')
.backgroundColor('#20101010')
.onClick((event: ClickEvent) => {
this.src=$r('app.media.fish_rev')
this.fishX+=20
})
}
}
}
.width('100%')
.height('100%')
.backgroundImage($r('app.media.bg'))
.backgroundImageSize({ width: '100%', height: '100%' })
}
}
2.显示动画
显示动画是通过全局animateTo函数来修改组件属性,实现属性变化时的渐变效果。
TypeScript
import router from '@ohos.router';
@Entry
@Component
struct FishIndex {
@State fishX:number=200
@State fishY:number=100
@State isBegin:boolean=false
@State angle:number=0
@State src:Resource=$r('app.media.fish')
build() {
Row() {
Button('返回')
.position({ x: 0, y: 0 })
.onClick((event: ClickEvent) => {
router.back()
})
if(!this.isBegin){
Button('开始游戏')
.onClick(()=>{
this.isBegin=true;
})
}else{
Image(this.src)
.width(50)
.height(50)
.position({x:this.fishX,y:this.fishY})
.rotate({angle:this.angle,centerX:'50%',centerY:'50%'})
Row(){
Button('←')
.backgroundColor('#20101010')
.onClick((event: ClickEvent) => {
animateTo({duration:500},()=>{
this.fishX-=20
this.src=$r('app.media.fish')
})
})
Column(){
Button('↑')
.backgroundColor('#20101010')
.onClick((event: ClickEvent) => {
animateTo({duration:500},()=>{
this.fishY-=20
})
})
Button("↓")
.backgroundColor('#20101010')
.onClick((event: ClickEvent) => {
animateTo({duration:500},()=>{
this.fishY+=20
})
})
}
Button('→')
.backgroundColor('#20101010')
.onClick((event: ClickEvent) => {
animateTo({duration:500},()=>{
this.src=$r('app.media.fish_rev')
this.fishX+=20
})
})
}
}
}
.width('100%')
.height('100%')
.backgroundImage($r('app.media.bg'))
.backgroundImageSize({ width: '100%', height: '100%' })
}
}
3.组件转场动画
组件的转场动画是在组件插入或移除时的过渡动画,通过组件的transition属性来配置
TypeScript
import router from '@ohos.router';
@Entry
@Component
struct FishIndex {
@State fishX:number=200
@State fishY:number=100
@State isBegin:boolean=false
@State angle:number=0
@State src:Resource=$r('app.media.fish')
build() {
Row() {
Button('返回')
.position({ x: 0, y: 0 })
.onClick((event: ClickEvent) => {
router.back()
})
if(!this.isBegin){
Button('开始游戏')
.onClick(()=>{
animateTo({duration:500},()=>{
this.isBegin=true;
})
})
}else{
Image(this.src)
.width(50)
.height(50)
.position({x:this.fishX,y:this.fishY})
.rotate({angle:this.angle,centerX:'50%',centerY:'50%'})
.transition({
type:TransitionType.Insert,
opacity:0,
translate:{x:-250}
})
}
Row(){
Button('←')
.backgroundColor('#20101010')
.onClick((event: ClickEvent) => {
animateTo({duration:500},()=>{
this.fishX-=20
this.src=$r('app.media.fish')
})
})
Column(){
Button('↑')
.backgroundColor('#20101010')
.onClick((event: ClickEvent) => {
animateTo({duration:500},()=>{
this.fishY-=20
})
})
Button("↓")
.backgroundColor('#20101010')
.onClick((event: ClickEvent) => {
animateTo({duration:500},()=>{
this.fishY+=20
})
})
}
Button('→')
.backgroundColor('#20101010')
.onClick((event: ClickEvent) => {
animateTo({duration:500},()=>{
this.src=$r('app.media.fish_rev')
this.fishX+=20
})
})
}
}
.width('100%')
.height('100%')
.backgroundImage($r('app.media.bg'))
.backgroundImageSize({ width: '100%', height: '100%' })
}
}