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);
后续有更深入的使用与理解时,我会进行进一步的更新。感谢各位的观看。
山不见我,我自见山。