鸿蒙应用开发:媒体展示、轮播与表单交互的全面实践
一、写在前面
本文围绕 媒体展示、轮播交互、表单输入与进度反馈 四条主线,系统梳理 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 工程),方便直接拷贝进项目。
版权声明:本文为原创,转载请注明出处与作者。