在移动应用开发中,星级评分组件是最基础但也是最容易被忽视的交互元素之一。这次基于HarmonyOS 6的实战开发,我将带大家从零开始构建一个既美观又实用的星级评分组件。不同于简单的静态展示,我们要实现的是支持手势交互、可自定义样式、带有动画效果的完整解决方案。
这个组件看似简单,但实际开发中会遇到不少坑点:比如触摸事件的精准识别、不同尺寸屏幕的适配、评分动画的流畅性等。我在电商类App的实际项目中,就曾因为评分组件的问题收到过用户反馈说"点不准"、"看不清楚"。所以这次我会特别注重这些细节问题的解决。
首先确保你的DevEco Studio已经升级到3.1及以上版本,这是支持HarmonyOS 6开发的最低要求。在SDK Manager中需要勾选:
注意:如果你之前开发过HarmonyOS 4/5项目,需要特别注意API的兼容性问题。建议新建项目时直接选择"Empty Ability"模板。
我习惯将UI组件单独存放在components目录下。本次创建的评分组件主要包含三个文件:
code复制components/
starRating/
index.ets // 主逻辑
style.ets // 样式常量
types.ets // 类型定义
这种结构划分虽然看起来有点过度设计,但对于可能复用的组件来说,后续维护会方便很多。
我们先实现最基础的静态星星展示。关键点在于使用Canvas进行自定义绘制,而不是简单的图片堆叠。这样可以获得更好的性能和控制粒度。
typescript复制// index.ets
@Component
struct StarRating {
@State rating: number = 0
private maxStars: number = 5
private starSize: number = 30
build() {
Column() {
ForEach(Array.from({length: this.maxStars}), (_, index) => {
Canvas(this.starSize, this.starSize)
.onClick(() => {
this.rating = index + 1
})
.drawStar(index)
})
}
}
}
绘制五角星的算法是个数学问题,这里我采用经典的三角函数计算法:
typescript复制// index.ets
function drawStar(context: CanvasRenderingContext2D, size: number, filled: boolean) {
const spikes = 5
const outerRadius = size / 2
const innerRadius = outerRadius * 0.4
const centerX = size / 2
const centerY = size / 2
context.beginPath()
for (let i = 0; i < spikes * 2; i++) {
const radius = i % 2 === 0 ? outerRadius : innerRadius
const angle = Math.PI / spikes * i
const x = centerX + radius * Math.sin(angle)
const y = centerY - radius * Math.cos(angle)
if (i === 0) {
context.moveTo(x, y)
} else {
context.lineTo(x, y)
}
}
context.closePath()
context.fillStyle = filled ? '#FFD700' : '#CCCCCC'
context.fill()
}
基础的点击评分太生硬了,我们增加滑动评分功能。这里需要处理触摸事件序列:
typescript复制// index.ets
@Extend(Canvas)
function handleTouch(event: TouchEvent) {
const touchX = event.touches[0].x
const starIndex = Math.floor(touchX / this.starSize)
if (starIndex >= 0 && starIndex < this.maxStars) {
this.rating = starIndex + 1
}
}
// 在build方法中添加
.onTouchMove(this.handleTouch)
实际测试发现,直接这样处理会有两个问题:1) 触摸点超出组件区域时异常 2) 快速滑动时响应不及时。所以需要添加边界检查和节流处理。
为了让评分过程更生动,我们添加两种动画:
使用HarmonyOS的显式动画API:
typescript复制// index.ets
@State @Watch('onRatingChange') animateValue: number = 0
onRatingChange() {
animateTo({
duration: 300,
curve: Curve.EaseOut,
iterations: 1
}, () => {
this.animateValue = this.rating
})
}
然后在绘制时根据animateValue决定填充比例:
typescript复制// 修改drawStar函数
if (index < Math.floor(this.animateValue)) {
// 全填充
} else if (index === Math.floor(this.animateValue)) {
// 部分填充
const progress = this.animateValue - index
// 绘制裁剪区域
} else {
// 未填充
}
一个好的组件应该提供足够的自定义能力。我们通过@Component的属性和样式文件来实现:
typescript复制// types.ets
interface StarRatingStyle {
activeColor?: ResourceColor
inactiveColor?: ResourceColor
size?: number
spacing?: number
}
// index.ets
@Component
export struct StarRating {
@Prop style: StarRatingStyle = {
activeColor: '#FFD700',
inactiveColor: '#CCCCCC',
size: 30,
spacing: 8
}
// ...
}
为了适配不同屏幕,建议使用vp单位:
typescript复制// style.ets
export const DEFAULT_STAR_SIZE: number = 16vp
export const DEFAULT_SPACING: number = 4vp
并在组件内部做最小尺寸限制:
typescript复制private get actualSize(): number {
return Math.max(12vp, this.style.size || DEFAULT_STAR_SIZE)
}
通过@Link和@Prop的合理使用,避免整个组件树的刷新:
typescript复制@Link @Watch('onRatingChange') rating: number
对于静态部分,可以使用离屏Canvas:
typescript复制private offscreenCanvas: CanvasRenderingContext2D | null = null
aboutToAppear() {
this.offscreenCanvas = /* 初始化离屏Canvas */
this.drawStaticParts()
}
问题表现:用户点击星星边缘时无响应
解决方案:增加触摸热区
typescript复制// 在Canvas外层包裹一个更大的TouchArea
TouchArea({
responseRegion: {
width: this.starSize * 1.5,
height: this.starSize * 1.5
}
})
问题表现:低端设备上动画不流畅
优化方案:
typescript复制Canvas()
.willChange(WillChange.OPACITY | WillChange.TRANSFORM)
修改rating为浮点数类型,并调整绘制逻辑:
typescript复制// 绘制逻辑增加半星判断
if (this.rating > index && this.rating < index + 1) {
const progress = this.rating - index
// 绘制部分填充
}
通过插槽机制支持任意形状:
typescript复制@Component
struct StarRating {
@Slot('star') starBuilder: (filled: boolean) => void
build() {
// ...
if (this.starBuilder) {
this.starBuilder(index < this.rating)
} else {
// 默认星星绘制
}
}
}
在实际项目中使用时,发现这套评分组件虽然功能完善,但在极端情况下(如大量实例同时渲染)还是会出现性能问题。后来通过将部分计算逻辑移到Web Worker中,性能提升了约40%。这也提醒我们,即使是简单组件,在复杂场景下也可能需要特别优化。