HarmonyOS学习第六周(实践版)(一)

本周是跟着尚硅谷的教程进行了一个实践项目的编写,相比于黑马的项目,我个人觉得尚硅谷的项目会比较的简单,结构比较清晰。更多使用@Builder和@Component,将组件封装在page页面中,好处是更容易寻找,而且不会有那么多的文件,坏处是代码会显得很冗长,而且代码的复用性会比较差。

本周的项目是一个单词打卡项目,包括了登录、打卡、答题等页面,是一个相对完整的项目,但这个项目更像一个纯前端项目,通过调用后端接口,而不是建表和操作数据库来进行数据的交互与存储。

欢迎页

这个页面非常的简单,就是一个背景图片加上一个转场动画。

背景设置

  .backgroundImage($r('app.media.img_splash_bg'))
  .backgroundImageSize({ width: '100%', height: '100%' })

转场动画

转场动画的详细介绍见第三周。

 if(this.flag){
       Image($r('app.media.ic_logo'))
         .logoStyle()
         .transition({type:TransitionType.Insert,opacity:0,translate:{x:-150}})
       Text('快速单词记忆神器')
         .titleStyle()
         .transition({type:TransitionType.Insert,opacity:0,translate:{x:150}})
     }

关键在于调用transition,并设置各种自定义属性。如type可以设置进场动画或者是出场动画。

跳转和动画启动逻辑 

 onPageShow(){
    animateTo({duration:1000,onFinish:()=>{
      setTimeout(()=>{
        router.replaceUrl({url:'pages/Index'})
      },200)
    }},()=>{
      this.flag=true;
    })
  }

当我们用transition设置好动画样式后,我们要用animateTo设置动画的运行时间 。

onfinish回调函数,用于设置动画完成后的逻辑。动画完成2s后,我们要跳转到到主页,所以用路由router的replaceUrl进行跳转。(用replaceUrl的原因是:欢迎页面一般只有在进入APP时要调用,后续不会再出现)

flag是控制动画开始与否的变量,当flag为true时,才会显示上面的Image和text,动画才会显示。

最外层的onPageShow是生命周期函数,当页面展示时会调用这个函数。

答题页

(细心的友友可能发现这个页面的停止测试的止不见了,还有一个奇怪的答案灌,这个我也不太清楚是为什么,在预览器的时候是正常的,但是用模拟器跑的时候就会这样,其他项目也没有问题。知道是为什么的友友麻烦评论区跟我说一下,谢谢大家!)

在这个页面中,我们可以看到底部又有熟悉的tabbar页面切换,这应该是每个手机软件的标配了。用于切换不同的主要页面。除此之外主要有三个部分,统计部分,单词部分,选项部分。

统计部分

由图可见,这四个部分的结构都是相同的,由一个icon加上text,中间一段空白,最后再有一个不同的组件。所以我们应该将这个结构单独封装起来,简化代码,提高复用性。但是我们最后有一个不同组件,要怎么封装哇,这时候我们就要引入一个新的装饰器@BuilderParam

@BuilderParam装饰的方法只能被自定义构建函数(@Builder装饰的方法)初始化。什么意思呢,就是我们可以用@Builder封装起来的内容,就可以传入到@BuilderParam装饰的方法中。

所以代码如下图所示:

@Component
export struct StatItem {
  icon:Resource
  name:string
  @BuilderParam statComp:()=>void
  fontColor:Color
  build() {
    Row({space:10}){
      Image(this.icon)
        .height(14)
        .width(14)
      Text(this.name)
        .fontWeight(FontWeight.Medium)
        .fontSize(14)
        .fontColor(this.fontColor)
      Blank()
      this.statComp()
    }
    .width('100%')
    .height(30)
  }
}

以“进度”举例:

StatItem({
          icon:$r('app.media.ic_progress'),
          name:'进度',
          fontColor:Color.Black
        }){
          Progress({value:this.answeredCount,total:this.totalCount})
            .width(100)
        }

在传入其他参数后,我们再传入一个对象,这个对象就和我们编写@builder一样,传入自定组件机器样式即可,如这个代码中就传入了一个进度条组件Progress。 

同时我们每次每次完成一次答题,就会有一个记录我们答题情况的弹窗,这个弹窗的结构也和统计页面的样式相似,也可以复用这个结构。

个数

个数中的自定义组件是一个button组件,点击后会出现一个弹窗,是一个TextPickerDialog,是一个文本滑动弹窗。

用于设置本次答题组中包含多少个单词。

StatItem({
          icon:$r('app.media.ic_count'),
          name:'个数',
          fontColor:Color.Black
        }){
          Button(this.totalCount.toString())
            .width(100)
            .height(25)
            .backgroundColor('#EBEBEB')
            .enabled(this.practiceStatus===PracticeStatus.Stopped)
            .fontColor(Color.Black)
            .onClick(()=>{
              TextPickerDialog.show({
                range:['5','10','15','20'],
                value:this.totalCount.toString(),
                onAccept:(result)=>{
                  this.totalCount=parseInt(result.value)
                  this.questions=getRandomQuestions(this.totalCount)
                }
              })
            })

 这里面有个enabled属性,标识什么时候这个组件可以被点击。practiceStatus是一个用于标识单词测试状态的变量,有停止、暂停、进行三个状态,只有状态为停止才可以进行修改。因为我们不可以测试测一半进行修改,这不符合逻辑。TextPickerDialog中的onAccept就是我们点击确定后的逻辑。getRandomQuestions是自定义的一个函数,用于从题库中抽取用户设置的题目数量的题。

export function getRandomQuestions(count: number) {
    let length = questionData.length;
    let indexes: number[] = [];
    while (indexes.length < count) {
      let index = Math.floor(Math.random() * length);
      if (!indexes.includes(index)) {
        indexes.push(index)
      }
    }
    return indexes.map(index => questionData[index])
}

计时器

计时器中用到了一个新的组件TextTimer,是一个通过文本显示计时信息并控制其计时器状态的组件。

TextTimer({ controller: this.timerController })
              .onTimer((utc,elapsedTime)=>{
                this.timeUsed=elapsedTime
              })

需要设置一个控制组件controller,需要提前进行声明:

  timerController: TextTimerController = new TextTimerController();

 声明后timerController将会有start、pause、stop三个属性,对应我们所定义的开始、暂停、停止。

onTimer事件时间文本发生变化时触发,elapsedTime:计时器经过的时间,单位为毫秒。

单词部分

这个部分就很简单了,就是两个text的组件,代码如下:

Column(){
        Text(this.questions[this.currentIndex].word)
          .wordStyle()
        Text(this.questions[this.currentIndex].sentence)
          .sentenceStyle()
      }

选项部分

因为四个选项的样式逻辑全都相同,且储存在数组中,所以我们可以使用foreach循环来进行渲染

        ForEach(this.questions[this.currentIndex].options,(option)=>{
          OptionButton({
            option: option,
            answerStatus:this.answerStatus,
            answer:this.questions[this.currentIndex].answer,
            selectedOption:this.selectedOption
          })
            .enabled(this.answerStatus==AnswerStatus.Answering)
            .onClick(()=>{
              if(this.practiceStatus!==PracticeStatus.Running){
                promptAction.showToast({message:'请先开始测试'})
                return
              }

              this.selectedOption=option

              this.answeredCount++
              if(option===this.questions[this.currentIndex].answer){
                this.rightCount++
              }

              this.answerStatus=AnswerStatus.Answered
              if(this.currentIndex<this.questions.length-1){
                setTimeout(()=>{
                  this.currentIndex++
                  this.answerStatus=AnswerStatus.Answering
                },500)
              }else{
                this.stopPractice()
              }
            })
        },option => this.questions[this.currentIndex].word + '-' + option)

OptionButton是封装的组件,也就是每个选项的样式和逻辑,如下:

@Component
struct OptionButton{
  option:string
  answer:string
  @State optionStatus:OptionStatus=OptionStatus.Default
  //注意!!声明顺序影响更新顺序,会导致出错
  @Prop selectedOption:string
  @Prop @Watch('onAnswerStatus') answerStatus:AnswerStatus

  onAnswerStatus(){
    if (this.option===this.answer) {
      this.optionStatus=OptionStatus.Right
    }else{
      if(this.option===this.selectedOption){
        this.optionStatus=OptionStatus.Wrong
      }else{
        this.optionStatus=OptionStatus.Default
      }
    }
  }
  
  getBgColor(){
    switch (this.optionStatus) {
      case OptionStatus.Right:
        return '#1DBF7B'
      case OptionStatus.Wrong:
        return '#FA635F'
      default:
        return Color.White
    }
  }
  build(){
    Stack(){
      Button(this.option)
        .optionButtonStyle({
          bg: this.getBgColor(),
          font: this.optionStatus === OptionStatus.Default ? Color.Black : Color.White
        })
      if(this.optionStatus===OptionStatus.Right){
        Image($r('app.media.ic_right'))
          .width(22)
          .height(22)
          .offset({x:10})
      }else if (this.optionStatus===OptionStatus.Wrong){
        Image($r('app.media.ic_wrong'))
          .width(22)
          .height(22)
          .offset({x:10})
      }
    }.alignContent(Alignment.Start)
  }
}

首先我们要明确点击选项后我们需要触发什么逻辑操作:1、我们要判断所选项是否为正确选项;2、如果为正确选项,则该选项要变绿,不为正确选项则要变红,且正确选项要变绿;3、在选择后要跳转到下一题,若已经是最后一题则要跳出弹窗。

onAnswerStatus用于判断按键状态,判断所选是否为正确,getBgColor用于给按键添加背景颜色,正确或错误的背景色。answerStatus是用于监测用户是否已经点击了选项,监听到状态变化则触发onAnswerStatus。

if(this.currentIndex<this.questions.length-1){
                setTimeout(()=>{
                  this.currentIndex++
                  this.answerStatus=AnswerStatus.Answering
                },500)
              }else{
                this.stopPractice()
              }

这段代码则是用于判断是否为本测试组的最后一题,如果是的话,则调用停止逻辑: 

 stopPractice(){
    this.practiceStatus = PracticeStatus.Stopped
    //停止计时器
    this.timerController.pause()
    //弹窗
    this.dialogController.open()
  }

弹窗内容就是前面讲到的完成答题后的弹窗。

 

Tabs页面切换

Tabs({ index: this.currentTabIndex }){
      TabContent(){
        PracticePage()
      }.tabBar(this.barBuilder(0,'答题',$r('app.media.ic_practice'),$r('app.media.ic_practice_selected')))

      TabContent(){
        CirclePage()
      }.tabBar(this.barBuilder(1,'圈子',$r('app.media.ic_circle'),$r('app.media.ic_circle_selected')))

      TabContent(){
        MinePage()
      }.tabBar(this.barBuilder(2,'我的',$r('app.media.ic_mine'),$r('app.media.ic_mine_selected')))
    }

用Tabs搭配TabContent和tabbar使用,TabContent中用于存放页面的UI界面 ,tabbar则是定义页面切换导航的样式,详见第五六周的文章。本代码中,把每个页面详细的样式封装成了单独的组件,然后用import引入后使用。

tabbar样式
 @Builder barBuilder(index:number,title:string,icon:Resource,iconSelected:Resource){
    Column(){
      Image(this.currentTabIndex === index ? iconSelected : icon)
        .width(25)
        .height(25)
      Text(title)
        .tabTitleStyle(this.currentTabIndex === index ? Color.Black:'#959595')
    }
  }

这里自定义了tabbar 的样式,让其为图片加文字的组合。并且当tabbar被选中时会有不同其他tabbar的效果。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值