【HarmonyOS Next】搜索框推荐词轮播

780 阅读3分钟

介绍

在各大 APP 的搜索框中,经常能看到推荐词轮播展示的效果。在 Android 开发中,可以使用 TextSwitcher 组件轻松实现该效果,也可以写两个 TextView 来做轮播动效。下面我们来写一个鸿蒙版本的。

效果预览

未命名.gif

分析

功能比较简单,主要就是两个 Text 循环做动效,通过一个 boolean 类型的 flag 来标记当前轮次。

flag = trueflag = flaseflag = true
Text1 向上位移 + 渐隐Text2 向上位移 + 渐隐Text1 向上位移 + 渐隐
Text2 设置成新的词 + 向上位移 + 渐现Text1 设置成新的词 + 向上位移 + 渐现Text2 设置成新的词 + 向上位移 + 渐现

代码实现

首先,创建一个 TextSwitcher.ets,根容器可以用 Stack ,里面放入 2 个 Text 组件。

// TextSwitcher.ets
@Component
export struct TextSwitcher {
  build() {
    Stack() {
      Text("text1")
        .fontColor(Color.Gray)
        .fontWeight(FontWeight.Bold)
        .height('100%')
      Text("text2")
        .fontColor(Color.Gray)
        .fontWeight(FontWeight.Bold)
        .height('100%')
    }
    .alignContent(Alignment.Start)
  }
}

添加后效果如图

image.png

下面我们来添加动画,动画可以使用 animateTo 来实现。在组件的 aboutToAppear 生命周期中通过setInterval设置每间隔 interval秒触发一次动画。代码如下:

// TextSwitcher.ets
@Entry
@Component
export struct TextSwitcher {

  // 移动距离,每次从 transDistance 移动到 0 再移动到 -transDistance
  @State transDistance: number = 26
  // 动画持续时间
  @State animDuration: number = 500
  // 动画间隔
  @State interval: number = 2000
  
  // Text1 的 位移距离
  @State private trans1: number = 0
  // Text2 的 位移距离
  @State private trans2: number = this.transDistance
  // Text1 的 透明度
  @State private alpha1: number = 1
  // Text2 的 透明度
  @State private alpha2: number = 0
  // 标志位,用于判断当前是 Text1 还是 Text2 移动到中间
  @State private flag: boolean = true

  aboutToAppear(): void {
    setInterval(() => {
      animateTo({
        duration: this.animDuration,
        onFinish: () => {
          // 动画结束后,要把移动到容器上方的 Text 再移动至容器下方
          if (this.flag) {
            this.trans1 = this.transDistance
          } else {
            this.trans2 = this.transDistance
          }
          this.flag = !this.flag
        }
      }, () => {
        if (this.flag) {
          this.trans1 = -this.transDistance
          this.trans2 = 0
          this.alpha1 = 0
          this.alpha2 = 1
        } else {
          this.trans1 = 0
          this.trans2 = -this.transDistance
          this.alpha1 = 1
          this.alpha2 = 0
        }
      })
    }, this.interval)
  }


  build() {
    Stack() {
      Text("text1")
        .fontColor(Color.Gray)
        .fontWeight(FontWeight.Bold)
        .height('100%')
        .translate({ y: this.trans1 })
        .opacity(this.alpha1)
      Text("text2")
        .fontColor(Color.Gray)
        .fontWeight(FontWeight.Bold)
        .height('100%')
        .translate({ y: this.trans2 })
        .opacity(this.alpha2)
    }
    .alignContent(Alignment.Start)
  }
}

效果如下

未命名2.gif

下面增加文字切换

// TextSwitcher.ets 
...
private index: number = 0
@Prop list: string[] = []
@State private text1: string = this.list[this.index % this.list.length]
@State private text2: string = ""


aboutToAppear(): void {
  setInterval(() => {
    animateTo({
      ...
    }, () => {
      if (this.flag) {
        ...
        this.text2 = this.list[(this.index + 1) % this.list.length]
      } else {
        ...
        this.text1 = this.list[(this.index + 1) % this.list.length]
      }
    })
  }, this.interval)
}


build() {
  Stack() {
    Text(this.text1)
      ...
    Text(this.text2)
      ...
  }
  .alignContent(Alignment.Start)
}

最后,增加入口页面,上完整代码。

// Index.ets
import { TextSwitcher } from '../components/TextSwitcher'

@Entry
@Component
struct Index {
  @State list: string[] = ["推荐词 1", "推荐词 2", "推荐词 3"]

  build() {
    Column() {
      TextSwitcher({ list: this.list })
        .width('100%')
        .height(40)
        .borderRadius(20)
        .backgroundColor('#22232323')

      Button('切换推荐词').onClick(() => {
        this.list = ["Hello World", "Hello HarmonyOS"]
      }).margin(20).stateStyles({
        pressed: {
          .backgroundColor("#33003cff")
        },
        normal: {
          .backgroundColor("#003cff")
        }
      })
    }
    .margin(20)
  }
}

// TextSwitcher.ets
@Component
export struct TextSwitcher {

  private index: number = 0
  @Prop list: string[] = []
  @State private text1: string = this.list[this.index % this.list.length]
  @State private text2: string = ""

  // 移动距离,每次从 transDistance 移动到 0 再移动到 -transDistance
  @State transDistance: number = 26
  // 动画持续时间
  @State animDuration: number = 500
  // 动画间隔
  @State interval: number = 2000

  // Text1 的 位移距离
  @State private trans1: number = 0
  // Text2 的 位移距离
  @State private trans2: number = this.transDistance
  // Text1 的 透明度
  @State private alpha1: number = 1
  // Text2 的 透明度
  @State private alpha2: number = 0
  // 标志位,用于判断当前是 Text1 还是 Text2 移动到中间
  @State private flag: boolean = true

  aboutToAppear(): void {
    setInterval(() => {
      animateTo({
        duration: this.animDuration,
        onFinish: () => {
          // 动画结束后,要把移动到容器上方的 Text 再移动至容器下方
          if (this.flag) {
            this.trans1 = this.transDistance
          } else {
            this.trans2 = this.transDistance
          }
          this.flag = !this.flag
          this.index++
        }
      }, () => {
        if (this.flag) {
          this.trans1 = -this.transDistance
          this.trans2 = 0
          this.alpha1 = 0
          this.alpha2 = 1
          this.text2 = this.list[(this.index + 1) % this.list.length]
        } else {
          this.trans1 = 0
          this.trans2 = -this.transDistance
          this.alpha1 = 1
          this.alpha2 = 0
          this.text1 = this.list[(this.index + 1) % this.list.length]
        }
      })
    }, this.interval)
  }


  build() {
    Stack() {
      Text(this.text1)
        .fontColor(Color.Gray)
        .fontWeight(FontWeight.Bold)
        .height('100%')
        .translate({ y: this.trans1 })
        .opacity(this.alpha1)
      Text(this.text2)
        .fontColor(Color.Gray)
        .fontWeight(FontWeight.Bold)
        .height('100%')
        .translate({ y: this.trans2 })
        .opacity(this.alpha2)
    }
    .alignContent(Alignment.Start)
    .padding({ left: 20, right: 20 })
    .clip(true)
  }
}

知识点总结

  • 层叠布局 Stack ,有点类似于 Android 中的 FrameLayout
  • 文本组件 Text ,类似于 Android 中的 TextView
  • 显式动画 animateTo,类似于 Android 中的属性动画
  • 组件生命周期 aboutToAppear,在组件即将出现时回调。具体时机为在创建自定义组件的新实例后,在执行其 build() 函数之前执行。
  • 组件的 translate 属性,类似 Android 的 translationXtranslationY
  • 组件的 opacity 属性,类似 Android 的 alpha
  • @State 装饰器,组件内状态同步
  • @Prop 装饰器,父组件向子组件单向同步

本文正在参加华为鸿蒙有奖征文征文活动