HarmonyOS鸿蒙Next中想实现一个界面内选择组件实现放大缩小的功能,选择一个组件后进行放大其余组件缩小以导航栏的方式显示 点击一个组件右上角按钮后实现组件内部内容展示,其余组件变成导航栏方式显示
HarmonyOS鸿蒙Next中想实现一个界面内选择组件实现放大缩小的功能,选择一个组件后进行放大其余组件缩小以导航栏的方式显示 点击一个组件右上角按钮后实现组件内部内容展示,其余组件变成导航栏方式显示 想实现一个界面内选择组件实现放大缩小的功能,选择一个组件后进行放大其余组件缩小以导航栏的方式显示 点击一个组件右上角按钮后实现组件内部内容展示,其余组件变成导航栏方式显示
可以用鸿蒙 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 可通过自定义组件结合 translate 和 scale 属性实现。使用 Stack 布局管理组件层级,利用状态变量控制选中组件的缩放比例(如 1.0)与其余组件的缩小比例(如 0.5)。点击组件右上角按钮时,更新状态使该组件全尺寸展示,其余组件切换为导航栏样式(只显示图标或标题)。
在HarmonyOS Next中,要实现“选择一个组件放大、其余缩小为导航栏”的效果,核心思路是动态控制组件的布局权重和显示状态。
实现方案:
- 数据结构:定义一个组件列表数据源,每个组件包含
isExpanded(是否放大)、isCollapsed(是否缩小为导航栏按钮)等状态。 - 布局容器:使用
Stack或Flex容器,利用.layoutWeight()属性来控制组件的比例。放大时.layoutWeight(3),缩小时.layoutWeight(1)。 - 点击选择逻辑:点击某个组件时,更新该组件的
isExpanded为true,其余组件isExpanded为false。同时,将所有未选中组件的isCollapsed设为true,仅显示为缩略导航栏样式(如图标+标题)。 - 右上角按钮触发器:每个组件右上角放置一个按钮(如
Image或Button)。点击该按钮时,触发“内部内容展示”模式:该组件isExpanded保持true,但内部切换显示模式(例如从概览视图切换到详细内容视图)。其余组件则完全压缩为更小的导航栏条,仅显示图标,宽度固定。 - 动画:对
layoutWeight、width或opacity添加.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)控制组件内部的详细内容与概览内容切换。
这种方式无需复杂布局嵌套,仅通过权重分配和状态管理即可实现“一个放大、其余导航栏化”的效果。

