鸿蒙新特性:Rating 组件 — 星级评分与用户评价系统深度解析
本文介绍了ArkUI中的Rating评分组件及其在电影推荐页面中的应用。该组件通过简洁的API(rating、indicator、stars、stepSize等)实现星级评分功能,支持展示模式和交互模式,可自定义星星数量、精度和样式。文章通过一个电影推荐Demo展示了如何用Rating展示豆瓣评分(indicator模式),并实现点击弹窗邀请用户打分的功能。页面包含电影卡片布局、类型标签色彩映射和
评分是用户反馈体系中最基础也最重要的交互之一——从电商的商品评价到影评网站的星级打分,从外卖的满意度调研到应用商店的五星好评。ArkUI 中的
Rating组件把这个常见需求变成了几行配置:设置星星数量、步长、交互模式,然后在onChange回调中拿到用户选择的分值。本文用它构建一个"电影推荐"页面,展示评分展示和用户打分的完整实现模式。
一、为什么需要 Rating 组件?
在没有 Rating 之前,实现星级评分需要:
- 用 Row + Image 或 Text 手动铺 5 颗星星
- 给每颗星星绑定 onClick,判断点击的是第几颗
- 实现半星逻辑——判断点击位置在星星的左半边还是右半边
- 处理拖动评分——监听 onTouch,根据手指位置计算当前分值
- 维护高亮状态——哪些星星是亮的,哪些是暗的
最简版本至少需要:
- 一个
@State rating: number保存当前分值 - 5 个 Image 组件(或 Text 展示 ★/☆)
- 5 个 onClick 回调(每个星星一个)
- 点击位置计算(区分左半/右半实现半星)
- 一套颜色映射(高分红、中分橙、低分灰)
Rating 组件用一个标签解决了所有这些问题:
Rating({ rating: this.currentScore, indicator: false })
.stars(5)
.stepSize(1)
.onChange((value: number) => {
this.currentScore = value;
})
一行组件 + 三个属性 + 一个回调 = 完整的交互式评分系统。拖动手势、半星精度、视觉反馈全部由框架处理。

二、Rating 核心 API
2.1 基本语法
Rating({ rating: number, indicator?: boolean })
.stars(value: number)
.stepSize(value: number)
.onChange(callback: (value: number) => void)
三个关键配置:
rating:当前评分数值,支持小数(如 4.5)。这是唯一必填的构造参数。indicator:是否为纯展示模式。true表示只读,星星不可点击;false(默认)表示可交互评分。stars:星星总数,默认 5。可以设为 10 实现"十分制"评分。stepSize:评分步长。1表示只能选整数(1、2、3、4、5),0.5表示支持半星(3.5、4.5),0.1表示精确到小数点。onChange:用户评分变化时的回调,参数为新的评分值。
2.2 indicator 模式 vs 交互模式
Rating 有两种工作模式,通过 indicator 参数切换:
展示模式(indicator: true):
Rating({ rating: 4.5, indicator: true })
.stars(5)
.stepSize(0.5)
用于展示平均评分——如商品详情页的"4.8 分"、电影列表的豆瓣评分。星星只读,不可点击。
交互模式(indicator: false,默认):
Rating({ rating: this.userScore, indicator: false })
.stars(5)
.stepSize(1)
.onChange((value: number) => {
this.userScore = value;
})
用于用户亲自打分——如在弹窗中给电影评分、收货后给商品打分。星星可点击/可拖动。
2.3 stepSize 与评分精度
stepSize 控制评分的颗粒度:
- stepSize: 1 — 只能打 1、2、3、4、5 星(整数)
- stepSize: 0.5 — 支持半星:3.5、4.5 等
- stepSize: 0.1 — 精确到 0.1 分(豆瓣式的"4.7分")
// 整数评分:适合简单的满意度调研
Rating({ rating: this.score }).stars(5).stepSize(1)
// 半星评分:适合电影/图书评分(豆瓣风格)
Rating({ rating: this.score }).stars(5).stepSize(0.5)
// 精确评分:适合专业评测
Rating({ rating: this.score }).stars(5).stepSize(0.1)
2.4 自定义星星数量
虽然 5 星是最常见的配置,但 stars 可以设为任意值:
// 十分制评分(IMDb 风格)
Rating({ rating: 7.8 }).stars(10).stepSize(0.1)
// 三星制简化评分(好/中/差)
Rating({ rating: 2 }).stars(3).stepSize(1)
2.5 尺寸控制
Rating 的尺寸通过 .width() 和 .height() 链式调用控制:
// 大尺寸:适合详情页顶部展示
Rating({ rating: 4.5, indicator: true })
.stars(5).stepSize(0.5)
.width(200).height(36)
// 小尺寸:适合列表中缩略展示
Rating({ rating: 4.5, indicator: true })
.stars(5).stepSize(0.5)
.width(100).height(18)
星星图标会按设定的宽高等比例缩放。默认颜色是金色,但可以通过 .starStyle() 自定义。
2.6 自定义星星样式
Rating({ rating: 4.2, indicator: true })
.stars(5)
.stepSize(0.5)
.starStyle({
backgroundUri: '/path/to/star_bg.png', // 未选中星星图片
foregroundUri: '/path/to/star_fg.png', // 选中星星图片
secondaryUri: '/path/to/star_half.png' // 半选星星图片(可选)
})
可以替换默认的 ★ 图标为自定义图片——例如用爱心图标做"收藏"组件,用火焰图标做"热度"指标。

三、Demo:电影推荐
本 Demo 构建一个电影推荐页面——8 部热门电影,展示豆瓣评分(indicator 模式),点击弹窗邀请用户打分。
页面结构
MovieRatingPage (~170行)
├── Header(🎬 电影推荐 + N部计数)
├── Scroll
│ └── Column
│ └── 电影卡片 × 8
│ ├── 左侧:海报占位(70×100彩色背景 + Emoji)
│ ├── 右侧:
│ │ ├── 电影标题(粗体,单行截断)
│ │ ├── 年份 · 导演
│ │ ├── 类型标签(彩色文字 + 浅色背景)
│ │ ├── 豆瓣评分(数字 + 彩色)
│ │ └── Rating 组件(indicator 模式,5星 + 0.5步长,100×18)
│ └── onClick → 弹窗显示详情 + 邀请打分
评分展示(indicator 模式)
每张电影卡片下方都嵌入了 Rating 组件用于展示豆瓣评分:
Rating({ rating: movie.avgRating, indicator: true })
.stars(5)
.stepSize(0.5)
.width(100)
.height(18)
indicator: true 确保星星只用于展示,用户无法在列表中误触修改评分。
评分色彩映射
根据分值高低使用不同颜色,让用户一眼看出"好片"和"烂片":
getRatingColor(rating: number): string {
if (rating >= 4.5) return '#E74C3C'; // 红色 — 神作
if (rating >= 4.0) return '#E67E22'; // 橙色 — 佳作
if (rating >= 3.0) return '#F39C12'; // 金色 — 还行
return '#999'; // 灰色 — 一般
}
豆瓣评分数字和 Rating 组件的星星一起配合,分值数字也用对应的颜色显示,视觉上形成统一。
电影类型标签
每种电影类型使用不同颜色的标签:
getGenreColor(genre: string): string {
switch (genre) {
case '科幻': return '#1677FF';
case '文艺': return '#52C41A';
case '音乐': return '#EB2F96';
case '动画': return '#722ED1';
case '喜剧': return '#FAAD14';
case '悬疑': return '#13C2C2';
case '战争': return '#FF4D4F';
default: return AppColors.PRIMARY;
}
}
标签样式:10 号字体 + 彩色文字 + 同色系 8% 透明度背景 + 4px 圆角,类似豆瓣的标签设计。
海报占位
用 Stack 包裹一个彩色背景的 Column + Emoji 文字,模拟电影海报。实际项目中可以替换为 Image 组件加载真实海报图。
Stack() {
Column() {
Text(movie.posterEmoji).fontSize(36)
}
.width('100%').height('100%')
.justifyContent(FlexAlign.Center)
}
.width(70).height(100)
.borderRadius(BorderRadius.SM)
.backgroundColor(movie.posterColor)
每部电影有不同的 posterColor(深色背景)和 posterEmoji(🚀🏔️🥁🕸️🎭🌊🐼🎖️),形成视觉区分。
三个交互点
- 浏览评分 — 每张卡片展示电影的平均评分(Rating indicator 模式 + 评分数字 + 颜色映射),用户滚动浏览所有电影
- 卡片点击 — 弹窗展示电影详细信息(年份、导演、类型、评分、评价人数),底部有"打分"按钮
- (扩展)自定义打分 — 在弹窗中嵌入交互式 Rating,让用户提交自己的评分
四、完整代码
import { AppColors, BorderRadius, FontSize, Spacing } from '../common/Constants';
import { promptAction } from '@kit.ArkUI';
class Movie {
id: number;
title: string;
year: number;
director: string;
genre: string;
posterColor: string;
posterEmoji: string;
avgRating: number;
ratingCount: number;
constructor(id: number, title: string, year: number, director: string,
genre: string, posterColor: string, posterEmoji: string, avgRating: number,
ratingCount: number) {
this.id = id;
this.title = title;
this.year = year;
this.director = director;
this.genre = genre;
this.posterColor = posterColor;
this.posterEmoji = posterEmoji;
this.avgRating = avgRating;
this.ratingCount = ratingCount;
}
}
const MOVIES: Movie[] = [
new Movie(1, '星际征途', 2026, '郭帆', '科幻', '#1a1a2e', '🚀', 4.5, 128000),
new Movie(2, '山河故人', 2025, '贾樟柯', '文艺', '#2D4059', '🏔️', 4.2, 89000),
new Movie(3, '爆裂鼓手2', 2026, '达米恩·查泽雷', '音乐', '#C0392B', '🥁', 4.7, 156000),
new Movie(4, '夏洛特的网', 2026, '宫崎骏', '动画', '#27AE60', '🕸️', 4.8, 210000),
new Movie(5, '无名之辈2', 2025, '饶晓志', '喜剧', '#E67E22', '🎭', 4.0, 67000),
new Movie(6, '深海追凶', 2026, '陈思诚', '悬疑', '#1B4F72', '🌊', 4.3, 95000),
new Movie(7, '功夫熊猫5', 2026, '梦工厂', '动画', '#8E44AD', '🐼', 4.1, 185000),
new Movie(8, '长津湖之水门桥2', 2025, '陈凯歌', '战争', '#7B241C', '🎖️', 4.6, 320000),
];
@Entry
@Component
struct MovieRatingPage {
@State movies: Movie[] = [...MOVIES];
getRatingColor(rating: number): string {
if (rating >= 4.5) return '#E74C3C';
if (rating >= 4.0) return '#E67E22';
if (rating >= 3.0) return '#F39C12';
return '#999';
}
getGenreColor(genre: string): string {
switch (genre) {
case '科幻': return '#1677FF';
case '文艺': return '#52C41A';
case '音乐': return '#EB2F96';
case '动画': return '#722ED1';
case '喜剧': return '#FAAD14';
case '悬疑': return '#13C2C2';
case '战争': return '#FF4D4F';
default: return AppColors.PRIMARY;
}
}
build() {
Column() {
Row() {
Text('🎬 电影推荐')
.fontSize(FontSize.HEADLINE)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.layoutWeight(1)
Text(`${this.movies.length}部`)
.fontSize(FontSize.CAPTION)
.fontColor('#FFFFFFAA')
}
.width('100%')
.height(52)
.backgroundColor('#1a1a2e')
.padding({ left: Spacing.XXL, right: Spacing.XXL })
Scroll() {
Column() {
ForEach(this.movies, (movie: Movie) => {
Row() {
Stack() {
Column() {
Text(movie.posterEmoji)
.fontSize(36)
}
.width('100%').height('100%')
.justifyContent(FlexAlign.Center)
}
.width(70).height(100)
.borderRadius(BorderRadius.SM)
.backgroundColor(movie.posterColor)
.margin({ right: Spacing.LG })
Column() {
Text(movie.title)
.fontSize(FontSize.MEDIUM)
.fontColor(AppColors.TEXT_PRIMARY)
.fontWeight(FontWeight.Bold)
.width('100%')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(`${movie.year}`)
.fontSize(FontSize.CAPTION)
.fontColor(AppColors.TEXT_TERTIARY)
Text(' · ')
.fontSize(FontSize.CAPTION)
.fontColor(AppColors.TEXT_DISABLED)
Text(movie.director)
.fontSize(FontSize.CAPTION)
.fontColor(AppColors.TEXT_TERTIARY)
}
.width('100%')
.margin({ top: 2 })
Row() {
Text(movie.genre)
.fontSize(10)
.fontColor(this.getGenreColor(movie.genre))
.padding({ left: 8, right: 8, top: 2, bottom: 3 })
.backgroundColor(this.getGenreColor(movie.genre) + '15')
.borderRadius(4)
.margin({ right: Spacing.SM })
Text(`豆瓣 ${movie.avgRating}`)
.fontSize(FontSize.CAPTION)
.fontColor(this.getRatingColor(movie.avgRating))
.fontWeight(FontWeight.Bold)
}
.width('100%')
.margin({ top: Spacing.SM })
Row() {
Rating({ rating: movie.avgRating, indicator: true })
.stars(5)
.stepSize(0.5)
.width(100)
.height(18)
Text(` ${movie.ratingCount / 10000}万人评`)
.fontSize(10)
.fontColor(AppColors.TEXT_DISABLED)
}
.width('100%')
.margin({ top: 4 })
}
.layoutWeight(1)
}
.width('100%')
.padding(Spacing.LG)
.backgroundColor(Color.White)
.borderRadius(BorderRadius.MD)
.margin({ left: Spacing.LG, right: Spacing.LG, bottom: Spacing.SM })
.onClick(() => {
promptAction.showDialog({
title: `🎬 ${movie.title}`,
message: `${movie.year}年 · ${movie.director}导演\n` +
`类型:${movie.genre}\n豆瓣评分:${movie.avgRating} / 5.0\n` +
`${movie.ratingCount / 10000}万人参与评分\n\n` +
`请为你心中的这部电影打分:`,
buttons: [
{ text: '⭐ 打分', color: AppColors.PRIMARY },
{ text: '关闭', color: AppColors.TEXT_TERTIARY }
]
});
})
})
}
.width('100%')
.padding({ top: Spacing.LG, bottom: Spacing.XXL })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.backgroundColor('#F5F6FA')
}
.width('100%')
.height('100%')
}
}
五、扩展:交互式评分弹窗
在上面的 Demo 中,弹窗只是"邀请用户打分"——真正的打分交互还没有实现。下面展示如何在弹窗中嵌入可交互的 Rating 组件,让用户完成真实打分。
5.1 使用 @CustomDialog 嵌入 Rating
@CustomDialog
struct RatingDialog {
controller: CustomDialogController;
movieTitle: string = '';
@State userRating: number = 0;
build() {
Column() {
Text(`为《${this.movieTitle}》打分`)
.fontSize(18).fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
// 交互式 Rating — 用户可以点击/拖动评分
Rating({ rating: this.userRating, indicator: false })
.stars(5)
.stepSize(0.5)
.width(200)
.height(40)
.onChange((value: number) => {
this.userRating = value;
})
Text(this.userRating > 0 ?
`你打了 ${this.userRating} 分` : '请点击星星评分')
.fontSize(14)
.fontColor(this.userRating > 0 ? '#E74C3C' : '#999')
.margin({ top: 12, bottom: 20 })
Row() {
Button('取消')
.backgroundColor('#F0F0F0').fontColor('#666')
.width(100).margin({ right: 12 })
.onClick(() => { this.controller.close(); })
Button('提交评分')
.backgroundColor('#E74C3C').fontColor(Color.White)
.width(120)
.onClick(() => {
// 提交评分逻辑
console.log(`用户给《${this.movieTitle}》打了 ${this.userRating} 分`);
this.controller.close();
})
}
}
.padding(24)
}
}
5.2 在卡片点击时弹出评分弹窗
// 在组件中声明 CustomDialogController
dialogController: CustomDialogController = new CustomDialogController({
builder: RatingDialog({
movieTitle: '',
userRating: 0
}),
autoCancel: true,
alignment: DialogAlignment.Center,
customStyle: true
});
// 在卡片 onClick 中打开评分弹窗
.onClick(() => {
// 更新 dialog 参数
this.dialogController = new CustomDialogController({
builder: RatingDialog({
movieTitle: movie.title,
userRating: movie.avgRating // 初始值设为当前平均分
}),
autoCancel: true,
alignment: DialogAlignment.Center,
customStyle: true
});
this.dialogController.open();
})
这样用户点击电影卡片后,不再是一个简单的信息展示弹窗,而是一个可以直接打分的交互界面——从"浏览评分"到"参与评分"形成完整闭环。
六、Rating 的视觉细节
6.1 默认样式
ArkUI 默认的 Rating 星星是金色填充 + 灰色边框的五角星图案。选中部分填充金色,未选中部分显示灰色轮廓。中间态(如半星 4.5)左侧金色、右侧灰色。
6.2 星星的"填充"逻辑
当 stepSize = 0.5,rating = 4.3 时:
- 前 4 颗星:完全填充(金色)
- 第 5 颗星:部分填充(30% 金色,因为 4.3 - 4 = 0.3,不到 0.5)
- 实际的视觉效果会四舍五入到最近的 stepSize 步进值
系统内部计算逻辑:filledStars = Math.floor(rating),partialStar = rating - filledStars,然后将 partialStar 量化到最近的 stepSize 倍数。
6.3 尺寸建议
| 使用场景 | 推荐尺寸 | 说明 |
|---|---|---|
| 列表中的缩略评分 | width: 80~100, height: 14~18 | 紧凑,不抢眼 |
| 卡片中的评分展示 | width: 100~140, height: 18~24 | 适中 |
| 详情页顶部评分 | width: 160~200, height: 28~36 | 醒目,作为主要信息 |
| 交互式打分弹窗 | width: 200~260, height: 40~48 | 足够大,便于手指点击 |
| 搜索结果/标签 | width: 60~80, height: 12~14 | 极简,辅助信息 |
6.4 与文字配合
Rating 组件本身不显示数字——通常需要搭配 Text 来展示具体分值:
Row() {
Rating({ rating: 4.5, indicator: true })
.stars(5).stepSize(0.5)
.width(100).height(18)
Text('4.5')
.fontSize(14).fontWeight(FontWeight.Bold)
.fontColor('#E74C3C')
.margin({ left: 8 })
Text('(12.8万人评)')
.fontSize(12).fontColor('#999')
.margin({ left: 4 })
}
星星 + 数字 + 评价人数的三层结构,也是豆瓣、IMDb、大众点评等产品的标准展示模式。
七、常见面试题 / 踩坑点
7.1 indicator 模式中 onChange 会触发吗?
不会。当 indicator: true 时,Rating 进入纯展示模式——星星不可点击,onChange 永远不会触发。这是两者的核心区别:
// 展示模式:onChange 永远不会被调用
Rating({ rating: 4.5, indicator: true })
.onChange((v) => { console.log('这行永远不会打印'); })
// 交互模式:onChange 在用户评分时被调用
Rating({ rating: 0, indicator: false })
.onChange((v) => { console.log(`用户打了 ${v} 分`); })
7.2 如何实现"评分后不可修改"?
因为 Rating 是受控组件(通过 rating 参数控制显示值),设置过一次后把 rating 固定住即可:
@State score: number = 0;
@State rated: boolean = false;
// 在 onChange 中记录已评分
Rating({ rating: this.score, indicator: this.rated })
.onChange((value: number) => {
if (!this.rated) {
this.score = value;
this.rated = true; // 评分后将 indicator 设为 true,变为只读
}
})
或者简单地用条件判断:
if (this.hasRated) {
Rating({ rating: this.score, indicator: true }) // 已评分:只读展示
} else {
Rating({ rating: 0, indicator: false }) // 未评分:可交互
.onChange((v) => { this.score = v; this.hasRated = true; })
}
7.3 rating 初始值为 0 时,星星都显示为空吗?
是的。rating: 0 时所有星星都是灰色未填充状态。这适用于"请先评分"的初始状态。但如果你希望默认显示 3 星(中性评价),设置 rating: 3 即可。
7.4 stepSize 和 rating 精度不匹配会怎样?
框架会自动将 rating 量化到最接近的 stepSize 步进值:
Rating({ rating: 4.3, indicator: true })
.stepSize(0.5) // rating 4.3 → 显示为 4.5 星(最近的 0.5 步进值)
Rating({ rating: 4.3, indicator: true })
.stepSize(1) // rating 4.3 → 显示为 4 星(最近的整数步进值)
对于展示用途,通常设置 stepSize: 0.1 以保留评分精度。
7.5 Rating 可以放在 List 中吗?
可以。Rating 是一个轻量组件,可以嵌入到 List/ListItem、Scroll、Grid 等任何容器中。在本文 Demo 中,每张电影卡片都包含一个 Rating,性能表现正常。
如果列表项非常多(超过 50 条),建议配合 LazyForEach 实现按需渲染。
7.6 如何实现"零分不展示星星"?
条件渲染:
if (movie.avgRating > 0) {
Rating({ rating: movie.avgRating, indicator: true })
.stars(5).stepSize(0.5)
.width(100).height(18)
} else {
Text('暂无评分').fontSize(12).fontColor('#999')
}
八、总结
Rating 是 ArkUI 中"小而美"组件的典范——功能聚焦、API 简洁、与声明式 UI 范式完美融合。从展示评分到收集评分,从整数档位到半星精度,一个组件覆盖了评分系统的全部需求。
1. 两种模式,两种场景。 indicator: true 用于展示,indicator: false 用于交互。前者的使用量远远大于后者——每个电商列表页、电影推荐页、应用商店卡片都在用展示模式。但交互模式才是 Rating 区别于纯 Text 或 Image 的核心价值。
2. stepSize 决定精度体验。 stepSize: 1 适合简单场景(满意度调研),stepSize: 0.5 是电影/图书评分的行业标准,stepSize: 0.1 适合对精度有要求的专业评测场景。选择合适的步长,本身就是产品设计的一部分。
3. 星星 + 数字 + 人数,三层信息结构。 Rating 组件只管"视觉呈现",具体的数字和评价人数需要 Text 配合。星星(直觉)、数字(精确)、人数(可信度)三者缺一不可。
4. 与 @CustomDialog 天然配合。 列表中展示评分(indicator 模式),弹窗中收集评分(交互模式)——这个模式适用于所有"浏览→评价"的场景。Rating 与 Dialog、promptAction 等 API 的组合使用,是一套成熟的交互范式。
Rating 组件最合适的应用场景包括:
- 电影/图书/音乐推荐页(展示平均评分)— 本文 Demo
- 商品评价列表(展示用户评分分布)
- 应用商店(展示 App 星级 + 评价数量)
- 外卖/服务满意度调研(收集用户评分)
- 课程/教程评价(展示综合评分 + 各维度分数)
- 任何需要"用星星表达好恶"的 UI
从 0 行手动实现到 3 行声明式配置,Rating 是那种"一旦用过,就不会再想手动实现"的组件。它让"星星"不再是一个需要设计的 UI 元素,而是一个可以随时使用的标准化工具。
更多推荐




所有评论(0)