鸿蒙应用开发:媒体展示、轮播与表单交互的全面实践

56 阅读4分钟

鸿蒙应用开发:媒体展示、轮播与表单交互的全面实践

一、写在前面

本文围绕 媒体展示、轮播交互、表单输入与进度反馈 四条主线,系统梳理 ArkUI 关键组件及其工程化落地方法,并覆盖 XComponent 在 NDK 场景下的最小可用方案。你将收获:

  • 各组件的常用属性/事件与易错点;
  • 场景化的代码片段,可直接改造;
  • 圆形屏(穿戴、车机表盘)下的 Arc 系列控件实践;
  • 项目级的性能、体验 checklist。

说明:文中示例采用 ArkTS(声明式 UI)语法,资源引用以 $r('app.media.xxx') 为例;部分 API 名称可能随版本有差异,按你本地 SDK 调整即可。


二、媒体展示:Image 与 Video 的最佳实践

2.1 Image:加载、裁剪与占位

典型场景:商品图、头像、Banner、九宫格等。

@Entry
@Component
struct ImageCardDemo {
  @State urls: ResourceStr[] = [
    $r('app.media.banner1'),
    $r('app.media.banner2'),
    $r('app.media.banner3'),
  ]

  build() {
    Grid() {
      ForEach(this.urls, (src: ResourceStr, idx: number) => {
        GridItem() {
          Column() {
            Image(src)
              .width('100%')
              .aspectRatio(16/9)
              .objectFit(ImageFit.Cover)
              .borderRadius(12)
              .backgroundColor('#F5F6F7')
              .onComplete((info) => console.log('loaded', idx, info))
              .onError(() => console.warn('image error:', idx))
            Text(`第 ${idx + 1} 张`).fontSize(12).opacity(0.6)
          }.padding(8)
        }
      })
    }.columnsTemplate('1fr 1fr').columnsGap(8).rowsGap(8).padding(12)
  }
}

关键要点

  • objectFit:封面图用 Cover,头像/插画用 Contain
  • 占位/错误兜底backgroundColor + onError 保证骨架感;
  • 裁剪
    Image($r('app.media.avatar')).width(96).height(96)
      .clipShape(Circle()) // 圆形头像
      .border({ width: 1, color: '#E5E6EB' })
    
  • 懒加载:在长列表配合 List/LazyForEach,并控制图片尺寸,避免大图撑爆内存;
  • 动效:首帧淡入
    Image($r('app.media.photo')).opacity(0).onComplete(() => {
      // 简易 fade-in;实际项目建议用通用动画封装
      animateTo({ duration: 200 }, () => setOpacity(1))
    })
    

2.2 Video:控制器与自定义控制条

视频播放常见诉求:自动播放/静音、循环、手势快进、缓冲进度、封面图

import { VideoController } from '@ohos.multimedia.video';

@Entry
@Component
struct VideoPlayerDemo {
  private vc: VideoController = new VideoController();
  @State playing: boolean = false;
  @State progress: number = 0; // 0~100

  build() {
    Column() {
      // 封面图层:加载前显示
      Stack() {
        Video({
          src: $r('app.media.demo_mp4'),
          controller: this.vc,
          autoPlay: false,
          muted: false,
          loop: false,
        })
        .width('100%').aspectRatio(16/9).borderRadius(12)
        .onPrepared(() => console.log('prepared'))
        .onPlay(() => this.playing = true)
        .onPause(() => this.playing = false)
        .onTimeUpdate((cur:number, dur:number) => {
          this.progress = Math.floor((cur / Math.max(dur,1)) * 100)
        })

        // 自定义控件(示例:中间大播放键 + 底部进度)
        if (!this.playing) {
          Button('▶ 播放')
            .type(ButtonType.Capsule)
            .onClick(() => this.vc.play())
            .position({ x: '50%', y: '50%' })
            .translate({ x: '-50%', y: '-50%' })
        }
      }

      // 进度与控制
      Row() {
        Progress({ value: this.progress, total: 100 })
          .width('70%').strokeWidth(6)
        Button(this.playing ? '暂停' : '播放')
          .onClick(() => this.playing ? this.vc.pause() : this.vc.play())
        Button('重播').onClick(() => { this.vc.seek(0); this.vc.play(); })
        Button('静音').onClick(() => this.vc.setMuted(true))
      }.spaceBetween(true).padding({ top: 8 })
    }.padding(12)
  }
}

实践提示

  • 首帧封面:可用 Image 叠在 Video 上,在 onPlay 后渐隐;
  • 缓冲监听:结合 onBufferingUpdate 做次级进度;
  • 手势:在外围 gesture(PanGesture...) 实现横向拖拽快进;
  • 后台/画中画:按业务合规接入系统能力。

三、轮播组件:Swiper 与 ArcSwiper

3.1 Swiper:常规矩形屏的万能轮播

import { SwiperController } from '@ohos.arkui';

@Entry
@Component
struct BannerSwiperDemo {
  private sc: SwiperController = new SwiperController();
  @State index: number = 0;
  private imgs: ResourceStr[] = [
    $r('app.media.banner1'),
    $r('app.media.banner2'),
    $r('app.media.banner3'),
  ];

  build() {
    Column() {
      Swiper({ controller: this.sc, index: this.index, autoPlay: true, interval: 3000, loop: true, indicator: true }) {
        ForEach(this.imgs, (src: ResourceStr) => {
          SwiperItem() {
            Image(src).width('100%').aspectRatio(16/9).objectFit(ImageFit.Cover)
          }
        })
      }
      .onChange((i:number) => this.index = i)
      .clipShape(RoundedRectangle({ radius: 12 }))

      Row() {
        Button('上一张').onClick(() => this.sc.showPrevious())
        Text(`${this.index+1}/${this.imgs.length}`).fontSize(12).opacity(0.6)
        Button('下一张').onClick(() => this.sc.showNext())
      }.spaceBetween(true).padding({ top: 8 })
    }.padding(12)
  }
}

优化建议

  • 为每个 SwiperItem 指定固定 aspectRatio,减少布局抖动;
  • 大图做 预加载(相邻两侧先加载),远端图建议加 WebP/AVIF 与压缩;
  • 监听 onAnimationEnd 做曝光统计。

3.2 ArcSwiper:为圆形屏设计的弧形轮播

适用于穿戴、圆形表盘等 圆形/弧形 布局。

@Entry
@Component
struct WatchArcSwiperDemo {
  @State idx: number = 0
  private cards = [ '心率', '步数', '睡眠', '压力' ]

  build() {
    // radius/curvature 等参数按设计适配
    ArcSwiper({ index: this.idx, radius: 180, curvature: 220, loop: true }) {
      ForEach(this.cards, (title: string) => {
        ArcSwiperItem() {
          Column() {
            Text(title).fontSize(22).fontWeight(FontWeight.Bold)
            Text('向上/向下滑动以切换').fontSize(12).opacity(0.5)
          }.alignItems(HorizontalAlign.Center)
        }
      })
    }
    .onChange((i:number)=> this.idx = i)
    .padding(12)
  }
}

设计建议

  • 弧形排布下 触达区域字距 要增大;
  • 注意圆心附近的视觉压缩,标题建议居中并增加行高;
  • 指示器尽量靠近圆缘,保持可读但不遮挡。

四、表单与选择组件概述

4.1 Button 与 ArcButton

@Entry
@Component
struct ButtonsDemo {
  @State loading: boolean = false

  build() {
    Column() {
      // 常规按钮
      Button(this.loading ? '提交中…' : '提交')
        .type(ButtonType.Capsule)
        .backgroundColor('#165DFF')
        .fontColor('#FFFFFF')
        .onClick(() => {
          this.loading = true
          setTimeout(()=> this.loading = false, 1200)
        })
        .width('100%').height(44).borderRadius(22)

      // 次级按钮(幽灵/边框)
      Button('取消')
        .ghost(true)
        .onClick(() => console.log('取消'))
        .width('100%').height(44)

      // 弧形按钮(圆形屏推荐)
      ArcButton({
        text: '开始',
        icon: $r('app.media.ic_play'),
        radius: 160, // 贴合屏幕半径
        angle: 80,   // 按钮所占弧度
      }).onClick(()=> console.log('ArcButton Click'))
    }.padding(12)
  }
}

交互原则:主次分明(色彩/尺寸/位置)、点击反馈(按下缩放/波纹)、禁用与加载态区分明确。


4.2 Radio 单选与分组

@Entry
@Component
struct RadioGroupDemo {
  @State gender: string = 'female'

  build() {
    Column() {
      Text('性别').fontSize(14).opacity(0.6)
      Row() {
        Radio({ value: 'male', group: 'g' })
          .checked(this.gender === 'male')
          .onChange(() => this.gender = 'male')
        Text('男')
        Radio({ value: 'female', group: 'g' })
          .checked(this.gender === 'female')
          .onChange(() => this.gender = 'female')
        Text('女')
      }.spaceBetween(false).gap(16)

      Text(`当前选择:${this.gender}`).fontSize(12).opacity(0.6)
    }.padding(12)
  }
}

要点:单选务必绑定同一 group;给文字也设置点击区域,提升可用性。


4.3 Toggle 开关(Switch/Toggle)

@Entry
@Component
struct ToggleDemo {
  @State enabled: boolean = true

  build() {
    Row() {
      Text('消息推送').fontSize(16)
      Toggle({ type: ToggleType.Switch, isOn: this.enabled })
        .onChange((val:boolean) => this.enabled = val)
    }.justifyContent(FlexAlign.SpaceBetween).padding(12)
  }
}

建议

  • 即时生效型设置用 Toggle,异步失败要 回滚状态并 Toast
  • 需要确认的操作使用 Dialog,不要滥用 Toggle 误导用户。

五、自定义渲染:XComponent 打通 NDK

当内置组件无法满足高性能绘制(地图、3D、游戏、专业图表),使用 XComponent 作为原生渲染表面,在 NDK 层进行 OpenGL/Skia/Vulkan 绘制。

@Entry
@Component
struct XCompDemo {
  private surfaceId: string = 'xc_surface_01'
  private nativeXc: any

  build() {
    Column() {
      Text('XComponent 原生渲染示例').fontSize(16).fontWeight(FontWeight.Medium)
      XComponent({ id: this.surfaceId, type: 'surface' })
        .width('100%').height(220)
        .onLoad((xc:any) => {
          this.nativeXc = xc.getNativeXComponent()
          console.log('XComponent loaded')
          // 通过 NAPI 发送信号到 C++ 初始化 EGL/Skia 管线
        })
        .onDestroy(()=> console.log('XComponent destroyed'))
    }.padding(12)
  }
}

C++(NDK)最小骨架

// 假设已拿到 OH_NativeXComponent* comp 与 ANativeWindow* window
void OnSurfaceCreated(OH_NativeXComponent* comp, void* window) {
    // 1) 初始化 EGL/Vulkan/Skia 设备
    // 2) 配置交换链与颜色空间
}

void OnDrawFrame(OH_NativeXComponent* comp) {
    // 3) 清屏 + 绘制基本图形(示例三角形/矩形)
}

void OnSurfaceDestroyed(OH_NativeXComponent* comp) {
    // 4) 释放显存与上下文
}

要点

  • UI 线程与渲染线程分离,避免阻塞;
  • DPI/旋转/安全区变化需重新计算视口;
  • XComponent.onTouch 把手势传递到原生侧(命中测试/相机控制)。

六、进度反馈:Progress 线性/环形/弧形

@Entry
@Component
struct ProgressDemo {
  @State percent: number = 0

  build() {
    Column() {
      // 线性
      Progress({ value: this.percent, total: 100 })
        .width('100%').strokeWidth(8)
      // 环形
      Progress({ value: this.percent, total: 100, type: ProgressType.Ring })
        .diameter(160).strokeWidth(10)
      // 圆形屏(示意):ArcProgress
      ArcProgress({ value: this.percent, total: 100, radius: 160, angle: 240 })

      Row() {
        Button('-10%').onClick(()=> this.percent = Math.max(0, this.percent - 10))
        Button('+10%').onClick(()=> this.percent = Math.min(100, this.percent + 10))
      }.gap(12).padding({ top: 8 })
    }.padding(12)
  }
}

实践

  • 与异步任务结合(下载/上传),展示 确定型进度
  • 不可准确估算时使用 不确定型(loading)
  • 圆形/弧形进度在表盘界面更易读。

七、组合实战:媒体详情页(图片/视频 + 轮播 + 表单 + 进度)

下面给出一个可直接改造的 组合页面,串起本篇组件:

@Entry
@Component
struct MediaDetailPage {
  private vc: VideoController = new VideoController();
  private sc: SwiperController = new SwiperController();
  @State current: number = 0
  @State liked: boolean = false
  @State quality: string = '720p'
  @State loading: boolean = false
  @State progress: number = 0

  private banners: any[] = [
    { type: 'image', src: $r('app.media.photo1') },
    { type: 'video', src: $r('app.media.demo_mp4') },
    { type: 'image', src: $r('app.media.photo2') },
  ]

  build() {
    Column() {
      // 轮播:图片 + 视频混排
      Swiper({ controller: this.sc, index: this.current, indicator: true, autoPlay: false, loop: false }) {
        ForEach(this.banners, (item) => {
          SwiperItem() {
            if (item.type === 'image') {
              Image(item.src).width('100%').aspectRatio(16/9).objectFit(ImageFit.Cover)
            } else {
              Video({ src: item.src, controller: this.vc })
                .width('100%').aspectRatio(16/9)
                .onPlay(()=> console.log('playing'))
            }
          }
        })
      }.onChange((i:number)=> this.current = i)
       .clipShape(RoundedRectangle({ radius:12 })).padding(12)

      // 表单区域:清晰度单选 + 喜欢开关
      Column() {
        Text('清晰度').fontSize(14).opacity(0.6)
        Row() {
          ['480p','720p','1080p'].forEach((q) => {
            Row() {
              Radio({ value: q, group: 'q' }).checked(this.quality===q).onChange(()=> this.quality=q)
              Text(q)
            }.gap(6)
          })
        }.gap(16)

        Row() {
          Text('点赞').fontSize(16)
          Toggle({ type: ToggleType.Switch, isOn: this.liked }).onChange((v)=> this.liked=v)
        }.justifyContent(FlexAlign.SpaceBetween).padding({ top: 8 })
      }.padding({ left:12, right:12 })

      // 提交动作 + 进度
      Row() {
        Button(this.loading ? '保存中…' : '保存设置')
          .type(ButtonType.Capsule)
          .onClick(()=> {
            if (this.loading) return
            this.loading = true
            this.progress = 0
            // 模拟异步进度
            let t = setInterval(()=>{
              this.progress += 10
              if (this.progress >= 100) { clearInterval(t); this.loading = false }
            }, 120)
          })
        Progress({ value: this.progress, total: 100 }).width(160).strokeWidth(6)
      }.gap(12).padding(12)
    }
  }
}

落地参考

  • 轮播页内含视频时,切换后主动 pause() 节省电量;
  • 提交期间禁用主按钮,使用幽灵副按钮保留返回/取消;
  • 后台任务可在通知/系统胶囊展示进度。

八、性能优化与体验细节清单

  • 图片:尺寸按需、开启 CDN 压缩;onError 回退占位;
  • 视频:首屏静音自动播(避免打扰);短视频循环、长视频不循环;
  • 轮播:仅保活前后页;大列表用 LazyForEach
  • 表单:输入即校验;状态与网络请求解耦;
  • XComponent:渲染线程独立;VSync 驱动;避免频繁 JNI 往返;
  • 动效:200–300ms 以内;进入/退出一致;
  • 无障碍:为可点击元素添加 accessibilityDescription,焦点顺序与视觉一致;
  • 圆形屏:边缘安全区、文字不切边;Arc 组件参数与设备半径匹配。

九、圆形屏与无障碍适配建议

  • 信息层级:圆形屏有效面积小,优先展示关键指标;
  • 触达半径:主交互控件靠近拇指自然弧线;
  • Arc 体系ArcSwiper / ArcButton / ArcProgress 与常规版本各保一套样式;
  • 无障碍:提供语义标签与手势等价操作(例如长按替代滑动)。

十、结语与参考实践路径

  • 第 1 周:掌握 Image、Video、Progress 基础与最佳实践;
  • 第 2 周:完成 Swiper/ArcSwiper 轮播与圆形屏适配;
  • 第 3 周:打通表单(Button/ArcButton/Radio/Toggle)与校验;
  • 第 4 周:引入 XComponent(原生渲染),完成一个 2D/3D Demo;

如果你想,我可以把本文代码拆成 可运行的 Demo 页面 结构(多文件 ArkTS 工程),方便直接拷贝进项目。

版权声明:本文为原创,转载请注明出处与作者。