HarmonyOS鸿蒙Next中想实现一个界面内选择组件实现放大缩小的功能,选择一个组件后进行放大其余组件缩小以导航栏的方式显示 点击一个组件右上角按钮后实现组件内部内容展示,其余组件变成导航栏方式显示

HarmonyOS鸿蒙Next中想实现一个界面内选择组件实现放大缩小的功能,选择一个组件后进行放大其余组件缩小以导航栏的方式显示 点击一个组件右上角按钮后实现组件内部内容展示,其余组件变成导航栏方式显示 想实现一个界面内选择组件实现放大缩小的功能,选择一个组件后进行放大其余组件缩小以导航栏的方式显示 点击一个组件右上角按钮后实现组件内部内容展示,其余组件变成导航栏方式显示

3 回复

可以用鸿蒙 ArkUI 的声明式开发来实现:核心思路是通过 @State 管理当前选中的模块和导航栏位置状态,再结合 Stack / Row / Column 做四宫格与展开态的动态布局切换;利用 animateTo 和组件级 animation 实现模块放大、缩小、位移和透明度过渡动画;通过条件渲染控制不同状态下模块内容的显示差异(首页只显示标题和数字,展开后显示完整操作区,缩小模块仅保留标题);最后配合圆角、阴影、裁剪、层级控制等 ArkUI 样式能力,就能做出这种带卡片伸缩切换效果的交互界面。

相关代码可参考:

@Entry
@Component
struct Index {
  [@State](/user/State) activeCard: number = -1
  [@State](/user/State) navAtTop: boolean = false

  [@State](/user/State) prevActiveCard: number = -1
  [@State](/user/State) isSwitching: boolean = false

  private readonly navW: string = '31%'
  private readonly navH: number = 100
  private readonly pad: number = 14
  private readonly animDuration: number = 700
private toggleCard(index: number): void {
    if (this.activeCard === index) {
      animateTo({
        duration: this.animDuration,
        curve: Curve.EaseInOut
      }, () => {
        this.prevActiveCard = this.activeCard
        this.isSwitching = true
        this.activeCard = -1
      })

      setTimeout(() => {
        this.isSwitching = false
        this.prevActiveCard = -1
      }, this.animDuration)

      return
    }

    if (this.activeCard === -1) {
      animateTo({
        duration: this.animDuration,
        curve: Curve.EaseInOut
      }, () => {
        if (index === 0 || index === 1) {
          this.navAtTop = false
        } else {
          this.navAtTop = true
        }

        this.prevActiveCard = -1
        this.isSwitching = true
        this.activeCard = index
      })

      setTimeout(() => {
        this.isSwitching = false
        this.prevActiveCard = -1
      }, this.animDuration)

      return
    }

    this.prevActiveCard = this.activeCard
    this.isSwitching = true

    animateTo({
      duration: this.animDuration,
      curve: Curve.EaseInOut
    }, () => {
      this.activeCard = index
    })

    setTimeout(() => {
      this.isSwitching = false
      this.prevActiveCard = -1
    }, this.animDuration)
  }

  private isNormalMode(): boolean {
    return this.activeCard === -1
  }

  private isActive(index: number): boolean {
    return this.activeCard === index
  }

  private isNavCard(index: number): boolean {
    return this.activeCard !== -1 && this.activeCard !== index
  }

  private bgColor(index: number): string {
    if (index === 0) {
      return '#1F3B56'
    } else if (index === 1) {
      return '#2C3E50'
    } else if (index === 2) {
      return '#24594D'
    }
    return '#4A2E5E'
  }

  private title(index: number): string {
    if (index === 0) {
      return '模块 A'
    } else if (index === 1) {
      return '模块 B'
    } else if (index === 2) {
      return '模块 C'
    }
    return '模块 D'
  }

  private value(index: number): string {
    if (index === 0) {
      return '84.4'
    } else if (index === 1) {
      return '85.0'
    } else if (index === 2) {
      return '19.8'
    }
    return 'ON'
  }

  private isTopNavMode(): boolean {
    return this.navAtTop
  }

  private getCardRadius(index: number): number {
    if (this.isNavCard(index)) {
      return 22
    }
    if (this.isActive(index)) {
      return 34
    }
    return 28
  }

  private getTitleSize(index: number): number {
    if (this.isNormalMode()) {
      return 22
    }
    if (this.isActive(index)) {
      return 24
    }
    return 18
  }

  private getValueSize(index: number): number {
    if (this.isNormalMode()) {
      return 46
    }
    return 58
  }

  private getMainButtonHeight(): number {
    return 40
  }

  private getMainButtonFont(): number {
    return 15
  }

  private getMainTextFont(): number {
    return 17
  }

  private getMainPadding(index: number): number {
    if (this.isNormalMode()) {
      return 20
    }
    if (this.isActive(index)) {
      return 24
    }
    return 14
  }

  @Builder
  buildActionButton(label: string) {
    Button() {
      Text(label)
        .fontSize(this.getMainButtonFont())
        .fontColor('#000000')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.None })
    }
    .width('78%')
    .height(this.getMainButtonHeight())
    .borderRadius(22)
    .padding({ left: 8, right: 8 })
  }

  @Builder
  buildNormalCardContent(index: number) {
    Column() {
      Blank()

      Column({ space: 16 }) {
        Text(this.title(index))
          .fontSize(this.getTitleSize(index))
          .fontWeight(FontWeight.Medium)
          .fontColor('#FFFFFF')
          .maxLines(1)

        Text(this.value(index))
          .fontSize(this.getValueSize(index))
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .maxLines(1)
      }
      .alignItems(HorizontalAlign.Start)

      Blank()
    }
    .width('100%')
    .height('100%')
    .padding(this.getMainPadding(index))
    .alignItems(HorizontalAlign.Start)
  }

  @Builder
  buildActiveCardContent(index: number) {
    Column() {
      Column({ space: 18 }) {
        Row() {
          Text(this.title(index))
            .fontSize(this.getTitleSize(index))
            .fontWeight(FontWeight.Medium)
            .fontColor('#FFFFFF')
            .maxLines(1)
          Blank()
        }
        .width('100%')

        Text(this.value(index))
          .fontSize(this.getValueSize(index))
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .maxLines(1)

        Column({ space: 8 }) {
          this.buildActionButton('按钮1')
          this.buildActionButton('按钮2')
        }
        .alignItems(HorizontalAlign.Start)

        Row({ space: 10 }) {
          Text('开关')
            .fontSize(this.getMainTextFont())
            .fontColor('#FFFFFF')

          Toggle({ type: ToggleType.Switch, isOn: true })
            .scale({ x: 1, y: 1 })
        }
      }
      .width('100%')
      .alignItems(HorizontalAlign.Start)

      Blank()
    }
    .width('100%')
    .height('100%')
    .padding(this.getMainPadding(index))
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Start)
    .animation({
      duration: this.animDuration,
      curve: Curve.EaseInOut
    })
  }

  @Builder
  buildNavCardContent(index: number) {
    Column() {
      Blank()

      Text(this.title(index))
        .fontSize(this.getTitleSize(index))
        .fontWeight(FontWeight.Medium)
        .fontColor('#FFFFFF')
        .maxLines(1)

      Blank()
    }
    .width('100%')
    .height('100%')
    .padding(14)
    .alignItems(HorizontalAlign.Start)
    .justifyContent(FlexAlign.Center)
    .opacity(0.96)
    .animation({
      duration: this.animDuration,
      curve: Curve.EaseInOut
    })
  }

  @Builder
  buildCardContent(index: number) {
    if (this.isNormalMode()) {
      this.buildNormalCardContent(index)
    } else if (this.isActive(index)) {
      this.buildActiveCardContent(index)
    } else {
      this.buildNavCardContent(index)
    }
  }

  @Builder
  buildCard(index: number) {
    Stack({ alignContent: Alignment.TopEnd }) {
      this.buildCardContent(index)

      Button() {
        Text(this.isActive(index) ? '−' : '+')
          .fontSize(this.isNavCard(index) ? 18 : (this.isNormalMode() ? 24 : 30))
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .lineHeight(this.isNavCard(index) ? 18 : (this.isNormalMode() ? 24 : 30))
          .textAlign(TextAlign.Center)
      }
      .width(this.isNavCard(index) ? 34 : (this.isNormalMode() ? 42 : 52))
      .height(this.isNavCard(index) ? 34 : (this.isNormalMode() ? 42 : 52))
      .backgroundColor('#FF4D8D')
      .borderRadius(this.isNavCard(index) ? 17 : (this.isNormalMode() ? 21 : 26))
      .margin({
        top: this.isNavCard(index) ? 10 : (this.isNormalMode() ? 14 : 18),
        right: this.isNavCard(index) ? 10 : (this.isNormalMode() ? 14 : 18)
      })
      .padding(0)
      .onClick(() => {
        this.toggleCard(index)
      })
      .animation({
        duration: this.animDuration,
        curve: Curve.EaseInOut
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.bgColor(index))
    .borderRadius(this.getCardRadius(index))
    .clip(true)
    .shadow({
      radius: this.isActive(index) ? 28 : 14,
      color: '#55000000',
      offsetX: 0,
      offsetY: 10
    })
    .animation({
      duration: this.animDuration,
      curve: Curve.EaseInOut
    })
  }

  @Builder
  buildPositionedCard(index: number) {
    Stack() {
      this.buildCard(index)
    }
    .width(this.getCardWidth(index))
    .height(this.getCardHeight(index))
    .position({
      x: this.getCardX(index),
      y: this.getCardY(index)
    })
    .opacity(this.getCardOpacity(index))
    .zIndex(this.getCardZIndex(index))
    .animation({
      duration: this.animDuration,
      curve: Curve.EaseInOut
    })
  }

  private getCardWidth(index: number): string | number {
    if (this.activeCard === -1) {
      return '48%'
    }
    if (this.activeCard === index) {
      return '100%'
    }
    return this.navW
  }

  private getCardHeight(index: number): string | number {
    if (this.activeCard === -1) {
      return '48%'
    }
    if (this.activeCard === index) {
      return '72%'
    }
    return this.navH
  }

  private getCardOpacity(index: number): number {
    if (this.activeCard === -1 && !this.isSwitching) {
      return 1
    }

    if (this.isSwitching) {
      if (index === this.prevActiveCard) {
        return 0.92
      }
      if (index === this.activeCard) {
        return 1
      }
      return 0.95
    }

    if (this.activeCard === index) {
      return 1
    }

    return 0.97
  }

  private getCardZIndex(index: number): number {
    if (this.activeCard === -1 && !this.isSwitching) {
      return 1
    }

    if (this.isSwitching) {
      if (index === this.prevActiveCard) {
        return 30
      }
      if (index === this.activeCard) {
        return 20
      }
      return 1
    }

    if (index === this.activeCard) {
      return 20
    }

    return 1
  }

  private getCardX(index: number): string {
    if (this.activeCard === -1) {
      if (index === 0 || index === 2) {
        return '0%'
      }
      return '52%'
    }

    if (this.activeCard === index) {
      return '0%'
    }

    let slot = this.getNavSlot(index)
    if (slot === 0) {
      return '0%'
    } else if (slot === 1) {
      return '34.5%'
    }
    return '69%'
  }

  private getCardY(index: number): string {
    if (this.activeCard === -1) {
      if (index === 0 || index === 1) {
        return '0%'
      }
      return '52%'
    }

    if (this.activeCard === index) {
      if (this.isTopNavMode()) {
        return '20%'
      }
      return '0%'
    }

    if (this.isTopNavMode()) {
      return '0%'
    }
    return '76%'
  }

  private getNavSlot(index: number): number {
    let slot = 0

    if (this.activeCard !== 0) {
      if (index === 0) {
        return slot
      }
      slot++
    }

    if (this.activeCard !== 1) {
      if (index === 1) {
        return slot
      }
      slot++
    }

    if (this.activeCard !== 2) {
      if (index === 2) {
        return slot
      }
      slot++
    }

    if (index === 3) {
      return slot
    }

    return 0
  }

  build() {
    Column() {
      Stack() {
        this.buildPositionedCard(0)
        this.buildPositionedCard(1)
        this.buildPositionedCard(2)
        this.buildPositionedCard(3)
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .padding(this.pad)
    .backgroundColor('#0E1722')
  }
}

更多关于HarmonyOS鸿蒙Next中想实现一个界面内选择组件实现放大缩小的功能,选择一个组件后进行放大其余组件缩小以导航栏的方式显示 点击一个组件右上角按钮后实现组件内部内容展示,其余组件变成导航栏方式显示的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


HarmonyOS 鸿蒙 Next 自定义组件实现

HarmonyOS 鸿蒙 Next 可通过自定义组件结合 translatescale 属性实现。使用 Stack 布局管理组件层级,利用状态变量控制选中组件的缩放比例(如 1.0)与其余组件的缩小比例(如 0.5)。点击组件右上角按钮时,更新状态使该组件全尺寸展示,其余组件切换为导航栏样式(只显示图标或标题)。

在HarmonyOS Next中,要实现“选择一个组件放大、其余缩小为导航栏”的效果,核心思路是动态控制组件的布局权重和显示状态

实现方案:

  1. 数据结构:定义一个组件列表数据源,每个组件包含 isExpanded(是否放大)、isCollapsed(是否缩小为导航栏按钮)等状态。
  2. 布局容器:使用StackFlex容器,利用.layoutWeight()属性来控制组件的比例。放大时.layoutWeight(3),缩小时.layoutWeight(1)
  3. 点击选择逻辑:点击某个组件时,更新该组件的isExpandedtrue,其余组件isExpandedfalse。同时,将所有未选中组件的isCollapsed设为true,仅显示为缩略导航栏样式(如图标+标题)。
  4. 右上角按钮触发器:每个组件右上角放置一个按钮(如ImageButton)。点击该按钮时,触发“内部内容展示”模式:该组件isExpanded保持true,但内部切换显示模式(例如从概览视图切换到详细内容视图)。其余组件则完全压缩为更小的导航栏条,仅显示图标,宽度固定。
  5. 动画:对layoutWeightwidthopacity添加.animation()属性,使切换过程平滑。

示例伪代码逻辑:

@Component
struct ComponentGrid {
  @State components: MyComponentModel[] = [...]

  build() {
    Row() {
      ForEach(this.components, (item: MyComponentModel, index: number) => {
        ComponentCell({
          data: item,
          onSelect: () => this.selectComponent(index),
          onShowDetail: () => this.showDetail(index)
        })
        .layoutWeight(item.isExpanded ? 3 : 1)
        .animation({ duration: 300 })
      })
    }
    .width('100%')
    .height('100%')
  }

  selectComponent(index: number) {
    this.components.forEach((c, i) => {
      c.isExpanded = (i === index)
      c.isCollapsed = (i !== index)
    })
  }

  showDetail(index: number) {
    // 将该组件的内部视图切换为详细内容,其余组件进一步缩小
    this.components.forEach((c, i) => {
      if (i === index) {
        c.isDetailView = true
      } else {
        c.isCollapsed = true
        c.isExpanded = false
      }
    })
  }
}

关键API.layoutWeight().animation()、条件渲染(if/else)控制组件内部的详细内容与概览内容切换。

这种方式无需复杂布局嵌套,仅通过权重分配和状态管理即可实现“一个放大、其余导航栏化”的效果。

回到顶部