HarmonyOS学习第二周(实践篇)

Demo1:任务列表案例

实现图:

代码:

// 任务类
@Observed
class Task{
  static id: number = 1
  // 任务名称
  name: string = `任务${Task.id++}`
  // 任务状态:是否完成
  finished: boolean = false
}

// 统一的卡片样式
@Styles function card(){
  .width('95%')
  .padding(20)
  .backgroundColor(Color.White)
  .borderRadius(15)
  .shadow({radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4})
}

// 任务完成样式
@Extend(Text) function finishedTask(){
  .decoration({type:TextDecorationType.LineThrough})
  .fontColor('#B1B2B1')
}

class  StateInfo{
  totalTask:number = 0
  finishTask:number = 0
}

@Entry
@Component
struct PropPage {
  @State stat:StateInfo = new StateInfo()

  build(){
    Column({space:10}){
      TaskStatistics({finishTask:this.stat.finishTask,totalTask:this.stat.totalTask})
      TaskList({stat:$stat})
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F2F3')
  }

}

@Component
struct TaskStatistics{

  @Prop finishTask:number
  @Prop totalTask:number

  build(){
    Row(){
      Text('任务进度:')
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
      Stack(){//堆叠容器
        Progress({
          value:this.finishTask,
          total:this.totalTask,
          type:ProgressType.Ring
        })
          .width(100)
        Row(){
          Text(this.finishTask.toString())
            .fontSize(24)
            .fontColor('#36D')
          Text('/'+this.totalTask.toString())
            .fontSize(24)
        }
      }
    }
    .card()
    .margin({top:20,bottom:10})
    .justifyContent(FlexAlign.SpaceEvenly)
  }
}

@Component
struct  TaskList{
  @Link stat:StateInfo
  @State tasks:Task[] = []

  handleTaskChange(){
    this.stat.totalTask = this.tasks.length
    this.stat.finishTask = this.tasks.filter(item=>item.finished).length
  }
  build() {
    Column(){
      Button('新增任务')
        .width(200)
        .margin({bottom:10})
        .onClick(()=>{
          this.tasks.push(new Task())
          this.handleTaskChange()
        })

      List({space:10}){
        ForEach(
          this.tasks,
          (item:Task,index)=>{
            ListItem(){
              TaskItem({ item:item ,onTaskChange:this.handleTaskChange.bind(this)})
            }
            .swipeAction({ end: this.DeleteButton(index) })
          }
        )
      }
      .width('100%')
      .layoutWeight(1)
      .alignListItem(ListItemAlign.Center)
    }
  }
  @Builder DeleteButton(index:number){
    Button(){
      Image($r('app.media.delete'))
        .fillColor(Color.Black)
        .width(20)
    }
    .width(40)
    .height(40)
    .type(ButtonType.Circle)
    .backgroundColor(Color.White)
    .margin({left:5})
    .onClick(()=>{
      this.tasks.splice(index,1)
      this.handleTaskChange()
    })
  }
}

@Component
struct TaskItem{
  @ObjectLink item:Task
  onTaskChange:()=>void

  build(){
    Row(){
      if(this.item.finished){
        Text(this.item.name)
          .finishedTask()
      }else{
        Text(this.item.name)
      }
      Checkbox()
        .select(this.item.finished)
        .onChange((val)=>{
          this.item.finished = val
          //已完成的任务数量
          this.onTaskChange()
        })
    }
    .card()
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

这次实验中的大部分组件已在前一篇文章中HarmonyOS学习第一周(实践篇)-CSDN博客已经有讲解过,在此不加赘述。

@Styles function card(){
  .width('95%')
  .padding(20)
  .backgroundColor(Color.White)
  .borderRadius(15)
  .shadow({radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4})
}

此代码是公共通用样式代码,用于渲染卡片形式,我们可以看到无论是最上方的任务进度还是下方的任务列表,都是由卡片组成的。因此,封装此代码可以大大提高代码复用性。

@Extend(Text) function finishedTask(){
  .decoration({type:TextDecorationType.LineThrough})
  .fontColor('#B1B2B1')
}

此代码也是公共样式的封装,不同的是,这是针对Text组件的样式,因此要用Extend来封装。

装饰器:

@State

@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。也就是所谓的响应式变量。并且在声明时必须指定其类型和本地初始化。

当装饰的数据类型为boolean、string、number类型或当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,可以实现响应式变化,即修改数据时,UI会对应的渲染。

  @State imageWidth: number = 200
class Person{
  name:string
  age:number
}

@State p:Person= new Person('John',20)

需要注意的是,当为嵌套类型时,是无法实现响应式的。什么是嵌套类型呢,举个例子:

class Person{
  name:string
  age:number
  friend:Person

  constructor(name:string,age:number,friend:Person) {
    this.name = name
    this.age = age
    this.friend = friend
  }
}

@State p:Person= new Person('John',20,('jack',20))

如上述代码,就是在Person中又嵌套了一个Person,则如果用@State进行装饰的话,当friend中的数据被修改时,或者对象数组中,当对象中的值发生变化时,我们是无法实现直接依赖@State实现响应式的。换句话说,我们无法监听到嵌套类型中的数值变化。 那如果我们要监听嵌套类型中的数据变化要怎么实现呢,这是我们就要引入一个新的装饰器@Observed了。

@Observed

对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这时就需要用到@Observed/@ObjectLink装饰器。

@Observed
class Person {
    name: string
    age: number
    gf: Person
    constructor(name: string, age: number,gf?: Person){
        this.name = name
        this.age = age
        this.gf = gf
    }
}

嵌套的类和被嵌套的类前都需要加上@Observed,由于这个案例中嵌套与被嵌套对象都是Person,因此只需要加一次@Observed。

@Entry
@Component
struct Parent {
    @state p: Person = new Person( Jack',21,new Person('Rose',18))
    build(){
        Column(){
            Child({p: this.p.gf})
                .onclick(()=> this.p.gf.age++)
            }
        }
    }

入口组件(父组件)如上图 

@Component
struct Child {
    @ObjectLink p: Person
    build(){
        Column(){
            Text(`${this.p.name} : ${this.p.age}`)
        }
    }
}

子组件上图

为了能监控Person中的gf, 我们创建了子组件Chlid,用@ObjectLink修饰p,以达到监听Person内部gf中的数据变化的目的。(这一部分的内容我了解的还不是特别清楚,后续练习之后会进行补充)

@Prop

@State搭配@Prop可以实现父子组件间的传递,但是只是父组件向子组件的单向传递。

限制条件

@Prop装饰器不能在@Entry装饰的自定义组件中使用。

如有以下定义

class  StateInfo{
  totalTask:number = 0
  finishTask:number = 0
}

在父组件中用@State定义

  @State stat:StateInfo = new StateInfo()

在子组件中需要用到父组件定义的StateInfo中的totalTask和finishTask来进行渲染,但是子组件不会对这两个数据进行修改时,我们就可以在子组件中用@Prop来接收

struct TaskStatistics{

  @Prop finishTask:number
  @Prop totalTask:number
}

 那么父组件要怎么知道要传给哪个子组件呢,我们需要用子组件的名字来制定,并选择要传递的数据,具体如下:

TaskStatistics({finishTask:this.stat.finishTask,totalTask:this.stat.totalTask})

这样就实现了使用@State和@Prop来达到父子组件传递的目的。

但是如果我也想子组件能修改父组件的数据,实现双向修改怎么办呢?那么这个时候我们就要引入一个新的装饰器@Link。

@Link

@State搭配@Prop可以实现父子组件间的传递,可以实现父子之间的双向传递。@Link装饰的变量和其所属的自定义组件共享生命周期。

限制条件

@Link装饰器不能在@Entry装饰的自定义组件中使用。

和@Prop的用法非常相似,也是在父组件中使用@State定义数据,在组件中进行接收,不同的是传递的方式。具体如下:

TaskStatistics({finishTask:this.stat.finishTask,totalTask:this.stat.totalTask})
TaskList({stat:$stat})

上面一行为@Prop的传递,而下面一行则为@Link的传递,我们可以很明显地看到二者的区别。在@Link的传递上,我们不再使用this来进行传递,而是用$进行传递 。

@Link变量初始化和更新机制详见官方文档。

容器:

Stack

前面我们讲过的容器有Column和Row,今天学一个新的容器:Stack。

Stack是堆叠容器,子组件按照顺序依次入栈,后一个子组件覆盖前一个子组件。

如这个任务完成度的轮图就是Progress进度条组件嵌套Text组件所组成的。

组件:

Progress

进度条组件,用于显示内容加载或操作处理等进度。

进度条组件应用的范围非常广,而且Progress提供多种样式的进度条,如线性样式、环形无刻度样式、圆形样式、环形有刻度样式、胶囊样式等,欢迎大家进行探索。

Checkbox

提供多选框组件,通常用于某选项的打开或关闭。

本次的两个组件都为装饰组件,都比较容易,不做详解。

Demo2:初识页面路由

import router from '@ohos.router';
class RouterInfo{
  url:string
  title:string

  constructor(url:string,title:string) {
    this.url=url;
    this.title=title
  }
}

@Entry
@Component
struct Index{
  @State message:string = '页面列表'
  private routers:RouterInfo[]=[
    new RouterInfo('pages/ImagePage','图片查看案例'),
    new RouterInfo('pages/ItemPage','商品列表案例'),
    new RouterInfo('pages/StatePage','Jack和他的女朋友案例'),
    new RouterInfo('pages/PropPage','任务列表案例'),
  ]
  build(){
    Column(){
      Text(this.message)
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .height(80)

      List({space:15}){
        ForEach(
          this.routers,
          (router,index)=>{
            ListItem(){
              this.RouterItem(router,index+1)
            }
          }
        )
      }
      .layoutWeight(1)
      .alignListItem(ListItemAlign.Center)
      .width('100%')
    }
    .width('100%')
    .height('100%')
  }
  @Builder
  RouterItem(r:RouterInfo,i:number){
    Row(){
      Text(i+'')
        .fontSize(20)
        .fontColor(Color.White)
      Blank()
      Text(r.title)
        .fontSize(20)
        .fontColor(Color.White)
    }
    .width('98%')
    .padding(12)
    .backgroundColor('#38F')
    .borderRadius(20)
    .shadow({radius:6, color:'#4F000000',offsetX:2,offsetY:4})
    .onClick(()=>{
      router.pushUrl(
        {
          url:r.url,
          params:{id:i}
        },
        router.RouterMode.Single,
        err=>{
          if(err){
            console.log(`路由失败,errCode:${err.code} errMsg:${err.message}`)
          }
        }
      )
    })
  }
}

概念:

页面路由是指在前端应用中管理不同页面之间导航的机制。它可以通过 URL 来确定应该显示哪个页面,并在用户点击链接或执行其他导航操作时加载相应的页面内容。页面路由的主要作用包括:页面导航、状态管理、路由守卫等。

页面路由的原理,是将页面放入页面栈中,通过各个路由方法将页面插入、回溯、删除等操作。

使用:

首先我们要进行引入

import router from '@ohos.router'

 我们主要介绍两个路由跳转方法:pushUrl、replaceUrl

这两种跳转方式的主要区别是:pushUrl是将新页面加入页面栈中,而replaceUrl使用新页面替换掉当前页面。一般页面栈可以存放32个页面,超出32个页面则会报错。那这样看,replaceUrl可以减少内存占用,不是更好吗。但是pushUrl相对于replaceUrl的好处是,他可以使用back函数返回原来的页面,replaceUrl则做不到。因此我们要视实际情况决定用什么进行跳转。

pushUrl

router.pushUrl({
  url: 'pages/routerpage2',
  params: {
    data1: 'message',
    data2: {
      data3: [123, 456, 789]
    }
  }
})
  .then(() => {
    // success
  })
  .catch(err => {
    console.error(`pushUrl failed, code is ${err.code}, message is ${err.message}`);
  })

replaceUrl

router.replaceUrl({
  url: 'pages/detail',
  params: {
    data1: 'message'
  }
})
  .then(() => {
    // success
  })
  .catch(err => {
    console.error(`replaceUrl failed, code is ${err.code}, message is ${err.message}`);
  })

我们可以看到两个函数的使用方式非常相似,可以说几乎是一模一样。

首先,在url中填写你要跳转的页面的url,然后在params中添加你所要传递的数据 ,then()里则用来进行跳转成功后的逻辑处理,catch()里则是接收错误类型,并对错误进行处理。在路由中有不同的错误提示码:

错误码ID

错误信息

100001

if UI execution context not found.

100002

if the uri is not exist.

100003

if the pages are pushed too much.

有一点需要特别注意:

如上述代码中,我将路由信息存放在对象数组中了

 private routers:RouterInfo[]=[
    new RouterInfo('pages/ImagePage','图片查看案例'),
    new RouterInfo('pages/ItemPage','商品列表案例'),
    new RouterInfo('pages/StatePage','Jack和他的女朋友案例'),
    new RouterInfo('pages/PropPage','任务列表案例'),
    new RouterInfo('pages/AnimationPage','小鱼动画')
  ]

 且使用了跳转函数,但是点击后却出现

04-10 10:11:19.003 28752-16124 E C03900/Ace: [manifest_router.cpp(GetPagePath)-(0)] [Engine Log] can't find this page pages
04-10 10:11:19.003 28752-16124 E C03900/Ace: [page_router_manager.cpp(StartPush)-(0)] [Engine Log] this uri not support in route push.
04-10 10:11:19.004 28752-16124 I A0c0d0/JSApp: app Log: 路由失败,errCode:100002 errMsg:Uri error. The uri of router is not exist.

这样的报错,这是为什么呢?

这是因为我们在创建一个路由之后,还需要将路由信息添加到resource>base>profile> main_pages.json中。通过文件名我们也不难看出,这是用来存放页面的一个JSON文件。

我们需要将数组中的url仿照里面已有的样例存放进src中

{
  "src": [
    "pages/Index",
    "pages/ImagePage",
    "pages/ItemPage",
    "pages/PropPage",
    "pages/StatePage",
    "pages/AnimationPage"
  ]
}

在完成这一步骤后,我们再次进行路由跳转,变可以成功实现啦。

我们再讲讲几个常用的页面路由函数:

 back

这个应该很好理解,作用就是返回上一页面或指定的页面。

router.back({url:'pages/detail'});    

clear 

用于清空页面栈中的所有历史页面,仅保留当前页面作为栈顶页面。

router.clear();    

getLength

 用于获取当前在页面栈内的页面数量。

let size = router.getLength();        
console.log('pages stack size = ' + size);    

后续有更深入的使用与理解时,我会进行进一步的更新。感谢各位的观看。 

山不见我,我自见山。

  • 23
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值