Android 9-patch 到 HarmonyOS:把 .9.png 的拉伸规则拆到 ArkUI

这次看一个很常见的 UI 资源:聊天气泡、按钮背景、标签背景里经常出现的 xxx.9.png

9-patch 在 Android 里由 PNG 和一圈 1px 标记线组成。View 设置成背景后,系统按标记线拉伸图片,并按内容区域安排文字位置。迁到 HarmonyOS 时,不能只把文件复制到 resources/base/media。需要迁的是两个规则:哪里可以被拉伸,内容应该离边缘多远。

示例资源是一张普通气泡 PNG。实际从 Android 迁移时,它应来自去掉 1px 标记边后的 .9.png

bg_message_bubble

Android:.9.png 把拉伸和内容区域写进图片边框

Android 侧通常是这几个文件:

app/src/main/res/drawable/bg_message_bubble.9.png
app/src/main/res/layout/item_message.xml

布局里只引用背景:

<TextView
    android:id="@+id/messageText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_message_bubble"
    android:paddingLeft="16dp"
    android:paddingTop="10dp"
    android:paddingRight="16dp"
    android:paddingBottom="10dp"
    android:text="Hello 9-patch"
    android:textColor="#1F2937"
    android:textSize="16sp" />

如果团队把 padding 也画进了 9-patch 的右边线和下边线,XML 里甚至可能看不到 padding。这正是迁移时容易误判的地方:文件名看起来只是一张图片,实际包含了布局信息。

Android 官方文档对 NinePatch 的描述可以拆成三件事:

  • 文件是带特殊格式的 PNG,通常放在 res/drawable/,文件名以 .9.png 结尾。
  • 左边和上边的 1px 黑线标记可拉伸区域。
  • 右边和下边的 1px 黑线标记内容允许放置的区域,也就是背景图给内容预留的 padding。

所以 Android 这里不是单纯的:

TextView -> background image

更准确的是:

TextView -> NinePatchDrawable
NinePatchDrawable -> bitmap + stretch areas + content padding

HarmonyOS:先把图片还原,再显式写规则

HarmonyOS 里不要保留 bg_message_bubble.9.png 这个资源语义。资源落点可以是:

entry/src/main/resources/base/media/bg_message_bubble.png

处理资源时先去掉 Android 9-patch 外围那一圈 1px 标记边,只保留要显示的气泡图。带黑线的 .9.png 如果被当作普通图片使用,黑线就可能变成可见像素。

页面代码可以这样写:

@Entry
@Component
struct Index {
  build() {
    Column() {
      MessageBubble({ text: 'Hello 9-patch' })
      MessageBubble({ text: '这是一段更长的文本,用来验证气泡背景横向拉伸后的边角是否稳定。' })
    }
    .width('100%')
    .height('100%')
    .padding(24)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Start)
    .backgroundColor('#F4F6FA')
  }
}

@Component
struct MessageBubble {
  text: string = ''

  build() {
    Row() {
      Text(this.text)
        .fontSize(16)
        .fontColor('#1F2937')
        .lineHeight(22)
    }
    .constraintSize({ maxWidth: 280 })
    .padding({ left: 16, right: 16, top: 10, bottom: 10 })
    .backgroundImage($r('app.media.bg_message_bubble'))
    .backgroundImageResizable({
      slice: { left: 22, right: 22, top: 18, bottom: 18 }
    })
    .margin({ bottom: 16 })
  }
}

这里的迁移关系很直接:

  • bg_message_bubble.9.png 的可见部分变成 bg_message_bubble.png
  • Android 左/上黑线表达的拉伸区,在 ArkUI 里变成 slice
  • Android 右/下黑线表达的内容区域,在 ArkUI 里变成 .padding(...)

slice 写的是四边保留区域。上面的配置表示:左右各 22vp、上下各 18vp 不参与中间区域拉伸,气泡边角和边缘质感保留下来,中间区域随内容扩展。

如果只是显示一张可拉伸图片

有些 9-patch 不作为组件背景,而是直接作为图片显示。比如一个可伸缩的面板底图。这个场景可以用 Image().resizable(...)

Image($r('app.media.bg_message_bubble'))
  .width(260)
  .height(88)
  .resizable({
    slice: { left: 22, right: 22, top: 18, bottom: 18 }
  })

Image.resizable(...) 适合“图片本身就是要显示的对象”。如果图片只是承载文字或子组件的背景,优先用 backgroundImageResizable(...),这样内容仍然按 ArkUI 组件树排版。

如果 9-patch 更像边框图

另一类 9-patch 是边框或面板框:四角、四边很重要,中间区域可以填充,也可以留空。这个时候可以看 borderImage(...)

Column() {
  Text('Panel content')
    .fontSize(16)
    .fontColor('#1F2937')
}
.padding(16)
.borderImage({
  source: $r('app.media.bg_message_bubble'),
  slice: { left: 22, right: 22, top: 18, bottom: 18 },
  width: { left: 22, right: 22, top: 18, bottom: 18 },
  repeat: RepeatMode.Stretch,
  fill: true
})

borderImage 的处理方式接近 CSS border-image:图片被切成边框和中心区域,再决定边框如何拉伸、中心是否填充。纯边框类资源可以从它开始;普通聊天气泡、按钮背景用 backgroundImageResizable(...) 更直接。

迁移前先把 .9.png 拆成这张表

拿到一个 Android 9-patch 文件时,可以先不要急着写 ArkTS。先把它拆成下面几项:

Android 9-patch 信息 在 HarmonyOS 里放到哪里
可见图片内容 去掉 1px 标记边后,放到 resources/base/media/*.png
左/上黑线:可拉伸区域 resizable({ slice })backgroundImageResizable({ slice })
右/下黑线:内容区域 .padding(...)
用作 View 背景 backgroundImage(...) + backgroundImageResizable(...)
用作单张图片 Image(...) + .resizable(...)
用作边框图 .borderImage(...)

多数按钮、气泡、标签背景只需要 slice 就够了。如果 Android .9.png 里有多个分离的可拉伸段,或者对切片规则有更复杂的要求,再看 ArkUI ResizableOptions 里的 lattice。初次迁移不要先碰复杂切片,先用一张真实 UI 图验证边角、边线和内容 padding 是否一致。

常见问题

第一个问题是把 .9.png 原样复制到 HarmonyOS。这样做会丢掉 Android 资源编译阶段对 1px 标记边的解析,黑线也可能进入最终显示。迁移时应该输出一张干净 PNG,再手写拉伸规则。

第二个问题是只迁 slice,忘了迁内容 padding。Android 9-patch 的右边线和下边线经常在控制文字安全区。HarmonyOS 里内容是 ArkUI 子组件,padding 要写在承载内容的 RowColumn 或自定义组件上。

第三个问题是把所有 9-patch 都迁成 Image.resizable(...)。如果这个资源原本是 View 背景,迁到 Image 后,文字和内部布局还要再叠一层 Stack,反而绕远。背景类资源优先从 backgroundImageResizable(...) 开始。

参考

// Kai@CodeHubble // 观测坐标:Android-HarmonyOS/2026-07-07

上一篇
下一篇