HarmonyOS 6商城开发学习:优惠券页面骨架屏效果——List+渐变占位与animateTo驱动完整实现
在HarmonyOS 6购物比价或电商类应用中,优惠券页加载网络数据时常有300~800ms延迟,直接白屏或突然闪出内容会使用户感觉"卡顿"。骨架屏(Skeleton Screen)是在数据返回前展示页面骨架占位符,通过微光扫过动画暗示内容正在加载,数据就绪后平滑替换为真实列表,显著提升感知流畅度。
本文将基于官方行业实践示例,用 List组件 + linearGradient占位 + animateTo显隐过渡 完整实现一个优惠券页面的骨架屏效果,同样适用于商品列表、消息记录等场景。
一、需求拆解与设计
1. 页面状态机
|
状态 |
表现 |
|---|---|
|
|
显示3~4条灰色骨架条(模仿优惠券卡片高宽比例),带左侧亮色渐变位移动画 |
|
|
隐藏骨架,显示真实 |
2. 骨架卡片视觉规格(模仿真实券)
-
高度 ≈ 真实券卡片高度(88vp)
-
左侧圆形占位(金额区)+ 右侧两行矩形占位(标题/有效期)
-
整体
borderRadius+ 浅灰#E8E8E8背景 -
微光渐变:宽度30%、透明度白→透→白,水平位移动画循环
二、骨架屏子组件(可复用)
// components/SkeletonCouponItem.ets
@Component
export struct SkeletonCouponItem {
@State gradientX: number = -120; // 渐变起始X(驱动动画)
aboutToAppear() {
// 启动微光扫过动画(循环)
this.startShimmer();
}
startShimmer() {
animateTo(
{ duration: 1200, iterations: -1 /* 无限循环 */, curve: Curve.Linear },
() => { this.gradientX = this.calcMaxX(); }
);
}
// 计算渐变最大偏移(略大于组件宽使扫过完整)
calcMaxX(): number {
// 在onAreaChange中取实际宽,此处先用估算
return 360;
}
build() {
Row({ space: 14 }) {
// 左侧圆形成分占位
Column()
.width(56)
.height(56)
.borderRadius(28)
.backgroundColor('#E0E0E0')
// 右侧文本行占位
Column({ space: 8 }) {
Row()
.width(140)
.height(14)
.borderRadius(7)
.backgroundColor('#E0E0E0')
Row()
.width(90)
.height(12)
.borderRadius(6)
.backgroundColor('#E8E8E8')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.padding(14)
.width('100%')
.height(88)
.borderRadius(12)
.clip(true) // 关键:限制渐变在卡片内
.backgroundColor('#F5F5F5')
// 微光覆盖层——线性渐变做扫光效果
.overlay(
Row()
.width('100%')
.height('100%')
.linearGradient({
angle: 90,
colors: [
['rgba(255,255,255,0)', this.gradientX - 60],
['rgba(255,255,255,0.45)', this.gradientX],
['rgba(255,255,255,0)', this.gradientX + 60]
]
}),
{ align: Alignment.TopStart }
)
.onAreaChange((_, newValue) => {
// 精确计算最大偏移(组件渲染后)
const w = newValue.width as number;
if (w > 0) this.calcMaxX = () => w + 120;
})
}
}
原理:
overlay叠一层半透白色线性渐变,通过animateTo不断改变渐变色停止位置(gradientX),产生"光从左到右扫过"的效果。
三、优惠券页面(骨架↔真实切换)
// pages/CouponListPage.ets
import { SkeletonCouponItem } from '../components/SkeletonCouponItem';
import { hilog } from '@kit.PerformanceAnalysisKit';
// 模拟真实优惠券数据
interface Coupon {
id: number;
title: string;
discount: string;
expire: string;
}
@Entry
@Component
struct CouponListPage {
@State loading: boolean = true;
@State coupons: Coupon[] = [];
// 模拟网络请求
fetchCoupons() {
this.loading = true;
setTimeout(() => {
this.coupons = [
{ id: 1, title: '满199减50', discount: '¥50', expire: '2026-12-31' },
{ id: 2, title: '满99减20', discount: '¥20', expire: '2026-10-15' },
{ id: 3, title: '新人专享减30', discount: '¥30', expire: '2026-08-01' }
];
// 关闭骨架(带动画过渡)
animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
this.loading = false;
});
}, 1500); // 模拟1.5s延迟
}
aboutToAppear() {
this.fetchCoupons();
}
build() {
Column() {
// 标题
Text('我的优惠券')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.padding({ top: 16, bottom: 12, left: 16 })
// ===== 内容区 =====
if (this.loading) {
// --- 骨架屏 ---
Column({ space: 10 }) {
ForEach([1, 2, 3], (_) => {
SkeletonCouponItem()
}, (i: number) => i.toString())
}
.padding({ horizontal: 16 })
.layoutWeight(1)
} else {
// --- 真实列表 ---
List() {
ForEach(this.coupons, (item: Coupon) => {
ListItem() {
Row({ space: 14 }) {
// 左侧金额
Column() {
Text(item.discount)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FF5722')
Text('满减券')
.fontSize(10)
.fontColor('#888')
}
.width(56)
.alignItems(HorizontalAlign.Center)
// 右侧信息
Column() {
Text(item.title).fontSize(15).fontColor('#333')
Text(`有效期至 ${item.expire}`).fontSize(11).fontColor('#AAA').margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.padding(14)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: 'rgba(0,0,0,0.05)', offsetX: 0, offsetY: 2 })
}
}, (item: Coupon) => item.id.toString())
}
.padding({ horizontal: 16 })
.layoutWeight(1)
}
// 重新加载按钮(演示用)
if (!this.loading) {
Button('重新模拟加载')
.margin(16)
.onClick(() => this.fetchCoupons())
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F6F8')
}
}
四、避坑指南
|
问题 |
原因 |
修复 |
|---|---|---|
|
微光不移动 |
|
确保 |
|
渐变溢出卡片边缘 |
|
给卡片 |
|
骨架与列表闪切生硬 |
直接切换 |
用 |
|
骨架高度与真实不符 |
占位高度/圆角未对齐真实卡片 |
按真实券卡片尺寸(此处88vp)设骨架高度 |
|
列表首次渲染跳动 |
网络极快(<50ms)时骨架一闪而过 |
可设最小展示时间 |
五、总结:骨架屏实现SOP
-
抽象骨架组件 → 用
Row/Column+ 灰色背景模拟页面布局结构 -
微光动画 →
overlay叠linearGradient,animateTo循环平移渐变色停止位 -
状态切换 → 网络开始时
loading=true显示骨架;数据返回后animateTo(()=>loading=false) -
复用 → 同组件略改高宽可套用到商品列表、消息列表等场景
核心法则:在 HarmonyOS 6 商城页面中,"骨架屏 = 布局占位 + 扫光渐变动画 + 状态驱动显隐"*,List 承载、animateTo 控制过渡,简单可靠。
©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任。
更多推荐


所有评论(0)