DEV Community

HarmonyOS
HarmonyOS

Posted on

Creating a Custom Camera UI in HarmonyOS Using Stack and XComponent

Read the original article:Creating a Custom Camera UI in HarmonyOS Using Stack and XComponent

Need to scan a face, ID, credit card, or IBAN? Here’s how you can build a beautiful custom camera overlay in HarmonyOS using ArkTS and Huawei’s Camera Kit.

Introduction

In many mobile apps, a basic camera preview just isn’t enough. Whether you’re scanning an identity card, credit card, or performing face verification, users expect a clean and guided interface with visual cues.

Thanks to Huawei’s Camera Kit and ArkUI’s Stack + XComponent structure, we can build a camera screen with a custom frame overlay that feels intuitive and professional.

In this article, we’ll build a custom camera screen using Stack and XComponent, ideal for use cases like IBAN detection or OCR-based scanning.

Background: What Are Stack and XComponent?

  • XComponent is the gateway to native components like the camera. It renders the camera preview on the screen.
  • Stack, on the other hand, allows layered UI composition — meaning we can put overlays, text, buttons, or frames on top of the camera feed.

This combination gives us full visual control over the camera screen.

Use Case: Capture an IBAN Within a Custom Frame

In this example, we’ll show how to implement a screen that:

  • Displays a camera feed
  • Adds a dashed-border frame for scanning
  • Places test instructions and a capture button below the camera view
  • Provides a fully styled and interactive UI using ArkTS and ArkUI components

Implementation: ArkTS Layout with Camera Frame Overlay

Here’s the full layout code using ArkTS:

build() {
  Row() {
    Column() {
      Stack() {
        // CAMERA PREVIEW
        XComponent({
          id: CommonConstants.XCOMPONENT_ID,
          type: XComponentType.SURFACE,
          controller: this.xcomponentController
        })
        .onLoad(async () => {
          await this.camera.releaseCamera();
          this.XComponentinit()
        })
        .width(CommonConstants.FULL_WIDTH)
        .height(this.xcomponentHeight)

        // OVERLAY FRAME
        if (this.showOverlay) {
          Column() {
            Row().height('25%').width('100%') // Top spacer

            Column() {
              Row() {
                Row().width('2%') // Left margin

                Column()
                  .width('96%')
                  .height('100%')
                  .border({
                    color: Color.Black,
                    width: '12px',
                    radius: '8px',
                    style: BorderStyle.Dashed
                  })

                Row().width('2%') // Right margin
              }
              .height('95%') // Frame height
            }
            .width('100%')
            .height('50%')
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)

            Row().height('25%').width('100%') // Bottom spacer
          }
          .width('100%')
          .height('100%')
        }
      }
      .width(CommonConstants.FULL_WIDTH)
      .height(this.xcomponentHeight)
      .margin({ top: $r('app.float.top_height') })

      // BOTTOM BUTTON + TEXT
      Column() {
        Text($r('app.string.Recognize_text'))
          .fontSize($r('app.float.button_tip_size'))
          .fontColor(Color.White)
          .margin({ top: $r('app.float.indicate_upper_margin') })

        // Decorative dot
        Row()
          .backgroundColor($r('app.color.round_color'))
          .width($r('app.float.decorative_point_size'))
          .height($r('app.float.decorative_point_size'))
          .border({
            radius: $r('app.float.decorative_dots_rounded_corners')
          })
          .margin({
            top: $r('app.float.decorative_dots_upper_margin'),
            bottom: $r('app.float.decorative_dots_bottom_margin')
          })

        // CAPTURE BUTTON
        Row() {
          Row()
            .backgroundColor(Color.White)
            .width($r('app.float.button_size'))
            .height($r('app.float.button_size'))
            .border({
              radius: $r('app.float.button_border_radius')
            })
        }
        .onClick(async () => {
          this.showOverlay = false;
          await this.camera.takePicture();
        })
        .backgroundColor(Color.Black)
        .width($r('app.float.button_border_size'))
        .height($r('app.float.button_border_size'))
        .border({
          color: Color.White,
          width: $r('app.float.button_border_width'),
          radius: $r('app.float.button_border_radius')
        })
        .justifyContent(FlexAlign.Center)
        .alignItems(VerticalAlign.Center)
      }
      .width(CommonConstants.FULL_WIDTH)
      .height($r('app.float.camera_lower_height'))
      .backgroundColor(Color.Black)
      .alignItems(HorizontalAlign.Center)
    }
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.FULL_Height)
    .backgroundColor(Color.Black)
  }
  .height(CommonConstants.FULL_Height)
}
Enter fullscreen mode Exit fullscreen mode

Real-World Results

This layout provides a clean and professional camera screen with:

  • A styled overlay frame (dashed border)
  • Centered scan area
  • Clear instructions
  • Decorative elements
  • A circular capture button

Perfect for use cases like:

  • IBAN recognition
  • Face or ID scanning
  • Credit card OCR
  • QR/barcode scanning with UI feedback

Limitations and Considerations

  • Requires a real HarmonyOS device XComponent and Camera Kit wil not work on emulators.
  • Ensure camera permissions are handled Always check runtime permissions before launching the camera view.

Conclusion

Using XComponent + Stack in HarmonyOS is a powerful way to build highly customized, layered camera interfaces that feel modern and intuitive. Whether you’re building a face scanner, IBAN reader, or card recognizer, this layout structure gives you full control over the camera experience.

References

Camera Kit
Stack Component

Written by Mehmet Algul

Top comments (0)