ArkUI 相对布局(对照 Android):用 RelativeContainer 把“控件之间的约束关系”讲清楚

到目前为止,我们已经按顺序写完了:

  • Row / Column:沿一个方向排(主轴/交叉轴)。
  • Stack:做覆盖(角标、浮层、背景图)。
  • Flex:做换行与流式分布(chips、标签区)。

但你很快会遇到一种“Stack 和 Row/Column 都不顺手”的布局:同一块区域里塞了好几个控件(角标、按钮、标题区),它们要彼此对齐,而且对齐关系是“依赖另一个控件的位置”。

本文我们来讲一个实际案例:做一张视频封面卡片。它包含右上角 VIP 角标、居中的播放按钮、底部标题区。重点不是把卡片画出来,而是把“谁贴谁、谁居中、谁贴底”这些约束信息写清楚。

约束信息梳理

当你说“我要一个角标在右上角、一个按钮在正中间、标题贴在底部”,这句话背后其实是三类约束:

  1. 对容器的约束:贴容器边、居中、距边多少。
  2. 对同伴的约束:贴着某个控件的右边/下边/对齐其中心线。
  3. 同时满足多条约束:比如输入框左边贴 label,右边贴容器右侧,还要垂直与 label 对齐。

在 Android View 体系里,你很自然会想到 ConstraintLayout。在 ArkUI 里,对应的“主力容器”就是 RelativeContainer

ArkUI:RelativeContainer 写封面卡片(角标 + 播放按钮 + 标题区)

我们用一个“视频封面卡片”例子,把常见的三件套放进同一张卡片:

  • 右上角:VIP 角标(贴容器右上)
  • 正中间:播放按钮(容器居中)
  • 底部:标题与副标题(贴底,左右留边)

示例里假设资源目录中有两个图片资源:

  • cover_movie:一张横向封面图,负责撑起卡片背景。本文配套资源是 assets/cover_movie.jpg
  • ic_play_circle:一个播放按钮图标,放在封面正中间。本文配套资源是 assets/ic_play_circle.svg
@Entry
@Component
struct CoverCardDemo {
  build() {
    RelativeContainer() {
      Image($r('app.media.cover_movie'))
        .width('100%')
        .height(180)
        .id('cover')
        .alignRules({
          top: { anchor: '__container__', align: VerticalAlign.Top },
          left: { anchor: '__container__', align: HorizontalAlign.Start },
          right: { anchor: '__container__', align: HorizontalAlign.End }
        })

      Text('VIP')
        .fontSize(12)
        .fontColor(Color.White)
        .padding({ left: 8, right: 8, top: 4, bottom: 4 })
        .backgroundColor('#EF4444')
        .borderRadius(999)
        .id('badge')
        .alignRules({
          top: { anchor: '__container__', align: VerticalAlign.Top },
          right: { anchor: '__container__', align: HorizontalAlign.End }
        })
        .margin({ top: 10, right: 10 })

      Image($r('app.media.ic_play_circle'))
        .width(44)
        .height(44)
        .id('play')
        .alignRules({
          center: { anchor: 'cover', align: VerticalAlign.Center },
          middle: { anchor: 'cover', align: HorizontalAlign.Center }
        })

      Column() {
        Text('标题:把约束关系写清楚')
          .fontSize(14)
          .fontColor(Color.White)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        Text('副标题:不用再套三层 Stack + Row')
          .fontSize(12)
          .fontColor('#E5E7EB')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .id('meta')
      .padding({ left: 12, right: 12, top: 10, bottom: 10 })
      .backgroundColor('#111827')
      .opacity(0.85)
      .alignRules({
        left: { anchor: '__container__', align: HorizontalAlign.Start },
        right: { anchor: '__container__', align: HorizontalAlign.End },
        bottom: { anchor: '__container__', align: VerticalAlign.Bottom }
      })
    }
    .width('100%')
    .height(220)
    .borderRadius(12)
    .clip(true)
  }
}

最终的实现效果:

Pasted image 20260531113121
读这段代码时,先看三件事:

  1. 每个“要被引用的组件”都要 .id("...")
  2. __container__ 是最常用的锚点:贴边、铺满、对齐边界。
  3. 先写约束,再用 .margin(...) 微调间距,别把间距混进约束里。

这里不要继续用默认的 app_icon 做演示资源。app_icon 是应用入口标识,不是内容图片;封面卡片里应该区分“内容图”和“操作图标”,这样读者才能把资源职责和布局职责一起理解清楚。

Android:同一个封面卡片,ConstraintLayout/Compose 里通常怎么写

View/XML:ConstraintLayout

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="220dp">

    <ImageView
        android:id="@+id/cover"
        android:layout_width="0dp"
        android:layout_height="180dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/badge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="10dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <ImageView
        android:id="@+id/play"
        android:layout_width="44dp"
        android:layout_height="44dp"
        app:layout_constraintTop_toTopOf="@id/cover"
        app:layout_constraintBottom_toBottomOf="@id/cover"
        app:layout_constraintStart_toStartOf="@id/cover"
        app:layout_constraintEnd_toEndOf="@id/cover" />

    <LinearLayout
        android:id="@+id/meta"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Compose:ConstraintLayout

ConstraintLayout(Modifier.fillMaxWidth().height(220.dp)) {
  val (cover, badge, play, meta) = createRefs()

  Image(painterResource(R.drawable.cover), null,
    modifier = Modifier
      .fillMaxWidth()
      .height(180.dp)
      .constrainAs(cover) { top.linkTo(parent.top) })

  Text("VIP",
    modifier = Modifier.constrainAs(badge) {
      top.linkTo(parent.top, 10.dp)
      end.linkTo(parent.end, 10.dp)
    })

  Icon(Icons.Default.PlayArrow, null,
    modifier = Modifier.constrainAs(play) { centerTo(cover) })

  Column(Modifier.constrainAs(meta) {
    start.linkTo(parent.start)
    end.linkTo(parent.end)
    bottom.linkTo(parent.bottom)
  }) { /* title/subtitle */ }
}

迁移到 ArkUI 时,不需要逐行翻译。你只要保留三件事就够了:

  1. 锚点命名(Android 是 view id / ConstrainedLayoutReference,ArkUI 是 .id(...)
  2. 关系列表(谁贴 parent,谁贴 cover,谁居中)
  3. 关系与间距分离(先让关系成立,再用 margin/padding 调距离)

迁移清单:从“画出来”到“好改”

当你在 Android ↔ HarmonyOS 间迁移这类布局时,可以参考的 checklist 是:

  1. 先写“关系清单”(谁贴谁、谁居中、谁铺满),再动手写代码。
  2. 能引用就必须命名锚点(id/标识),不要用“猜出来的层级位置”。
  3. 贴边、铺满优先用容器锚点(parent / __container__)。
  4. 约束先写核心边界,再补居中与间距(别一开始就写得太花)。
  5. 一次只引入一个容器:不要在 RelativeContainer 里再套多层 Stack/Row(除非你明确知道为什么)。
  6. 把“关键卡片布局”做成回归用例:截图对比或 UI 自动化都行,避免改一处牵一片。

对照表:RelativeContainer ↔ ConstraintLayout

你脑子里的问题 Android(View/XML)常用做法 ArkUI(ArkTS)常用做法 迁移要点
“谁是锚点?” app:layout_constraint* 指向另一个 view 的 id alignRules({ ... anchor: "xxx" ... }) 指向另一个组件 .id("xxx") 都是“先给锚点命名”,再写关系
“贴容器边怎么写?” ..._toStartOf="parent" / ..._toEndOf="parent" anchor: "__container__" + HorizontalAlign.Start/End ArkUI 把“父容器”当成一个特殊锚点
“上下左右 + 居中怎么表达?” 多条 constraint + bias / chain top/left/right/bottom/center 等规则组合 先写核心的边界约束,再补居中/分布
“怎么避免嵌套地狱?” 用一层 ConstraintLayout 代替多层嵌套 用一层 RelativeContainer 代替多层 Row/Column/Stack 思路相同:用关系代替层级

参考素材(精选)

// Kai@CodeHubble

// 观测坐标:Android-HarmonyOS/2026-05-26

上一篇