在HarmonyOS 6购物比价或电商类应用中,商品评价页常用"五星评分"组件,产品希望不只是静态展示——用户手指在五星条上左右滑动时能实时点亮星星、带有微小填充动效(类似原生评分条渐变出现),且支持半星/整星两种模式。直接用五个Image拼手势虽可行但难做平滑过渡。

官方行业实践明确指出:可用 Progress组件 + ProgressConfigurationContentModifier 自定义内容区(绘制星星/条形)+ PanGesture驱动评分值​ 实现可交互动画评分条。本文将完整实现带滑动选值、整星/半星切换、微填充动效的五星评分组件。


一、需求拆解与设计

1. 交互规格

项目

说明

最大值

5 星

当前值

score: number(支持 0~5,半星精度 0.5)

手势

横向拖拽自动算分,点击也可直接设点

视觉

背景灰星 + 前景金色彩条/星(按比例裁剪),带 animateTo过渡

模式

可切 step=1(整星)或 step=0.5(半星)

2. 实现选型

  • Progress+ ProgressConfigurationContentModifier:用 Progress 的 value/max控制填充比例(0~5),ContentModifier 内用 Canvas 或 Column(Row星图)+clip(Shape)模拟前景裁切——最简是用 Stack[背景星Row / 前景星Row.clip(Rect)]包在 Progress 的 contentModifier 中

  • PanGesture+ onClick:算触摸点 X 占宽比 → 映射分值 → animateTo(()=>this.score=newVal)


二、五星评分组件完整实现

// components/StarRating.ets
import { common } from '@kit.AbilityKit';

@Component
export struct StarRating {
  // ===== 对外属性 =====
  maxStar: number = 5;
  @Link score: number;       // 当前评分 0~maxStar
  step: number = 1;           // 1=整星, 0.5=半星
  starSize: number = 28;      // 单颗星 vp
  activeColor: string = '#FF9800';
  inactiveColor: string = '#E0E0E0';
  showScoreText: boolean = true;

  // 组件宽(用于手势映射)
  private compWidth: number = 0;

  // 将 score 量化到 step
  private quantize(v: number): number {
    const s = Math.max(0, Math.min(this.maxStar, v));
    if (this.step >= 1) return Math.round(s);
    return Math.round(s / 0.5) * 0.5;
  }

  build() {
    Row({ space: 6 }) {
      // === Progress 做评分条容器(value驱动前景裁剪)===
      Progress({
        value: this.score,
        total: this.maxStar,
        type: ProgressType.Ring   // Ring仅占位,真正显示靠contentModifier
      })
        .width(this.maxStar * this.starSize)
        .height(this.starSize)
        .progressStyle({ strokeWidth: 0 })  // 隐藏原生环
        .contentModifier({
          // ✅ ContentModifier:自定义 Progress 内容区画星星
          factory: (config: ProgressConfiguration) => {
            this.starContent(config);
          }
        })
        // 手势:横向拖拽算分
        .gesture(
          PanGesture({ direction: PanDirection.Horizontal })
            .onActionUpdate((e: GestureEvent) => {
              const localX = e.fingerList[0].x;
              let ratio = localX / (this.maxStar * this.starSize);
              ratio = Math.max(0, Math.min(1, ratio));
              const raw = ratio * this.maxStar;
              animateTo({ duration: 80 }, () => {
                this.score = this.quantize(raw);
              });
            })
            .onActionEnd(() => {
              // 动画弹回取整(视觉确认)
              animateTo({ duration: 150, curve: Curve.EaseOut }, () => {
                this.score = this.quantize(this.score);
              });
            })
        )
        // 点击直接设分
        .onClick((e: ClickEvent) => {
          const localX = e.x;
          let ratio = localX / (this.maxStar * this.starSize);
          ratio = Math.max(0, Math.min(1, ratio));
          this.score = this.quantize(ratio * this.maxStar);
        })
        .onAreaChange((_, nv) => {
          this.compWidth = nv.width as number;
        })

      // 分数文本
      if (this.showScoreText) {
        Text(`${this.score.toFixed(this.step < 1 ? 1 : 0)} / ${this.maxStar}`)
          .fontSize(13)
          .fontColor('#888')
          .margin({ left: 4 })
      }
    }
  }

  // ===== 自定义星星绘制(背景+前景裁剪)=====
  @Builder
  starContent(_config: ProgressConfiguration) {
    // 背景:灰星行
    Row() {
      ForEach(Array(this.maxStar).fill(0), (_i, idx) => {
        Image($r('sys.media.ohos_ic_public_star_filled'))
          .width(this.starSize)
          .height(this.starSize)
          .fillColor(this.inactiveColor)
      })
    }
    .width('100%')
    .height('100%')

    // 前景:金色彩条裁剪到 Progress.value/maxStar 比例
    Stack() {
      Row() {
        ForEach(Array(this.maxStar).fill(0), (_i, idx) => {
          Image($r('sys.media.ohos_ic_public_star_filled'))
            .width(this.starSize)
            .height(this.starSize)
            .fillColor(this.activeColor)
        })
      }
      .width('100%')
      .height('100%')
    }
    .clipShape(Rect()
      .width((this.score / this.maxStar) * (this.maxStar * this.starSize))
      .height(this.starSize)
      .alignLeft()   // 从左往右裁切
    )
    .alignSelf(HorizontalAlign.Start)
  }
}

关键实现解读

做法

作用

Progress.contentModifier

完全替换 Progress 默认绘制,在里面叠背景星Row + 前景星Row(前景Row用 clipShape(Rect)score/maxStar×总宽裁切)

clipShape(Rect().width(比例宽))

前景只显示左侧已评分部分 → 产生"点亮"效果

PanGesture onActionUpdate

拖拽时实时算 x/总宽*max → quantize → animateTo 赋 score,产生顺滑动效

onClick

点按直接量化设分(便于快速选星)

quantize(raw)

step四舍五入/半星量化,防止浮点误差

若想用 Canvas 自绘五角星(而非系统 ohos_ic_public_star_filled图标),在 starContent中用 Canvas(ctx).onReady(drawStarPath)替代 Image Row 即可,裁剪原理相同。


三、商品评价页调用示例

// pages/GoodsReviewPage.ets
import { StarRating } from '../components/StarRating';

@Entry
@Component
struct GoodsReviewPage {
  @State rating: number = 0;

  build() {
    Column({ space: 20 }) {
      Text('商品评分')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 32, left: 16 })

      StarRating({
        score: this.rating,     // 双向绑定
        step: 0.5,               // 半星模式
        starSize: 32,
        showScoreText: true
      })

      // 整星模式示例(可切 tab 展示)
      StarRating({
        score: this.rating,
        step: 1,
        starSize: 28,
        showScoreText: true
      })

      Text('当前评分值:' + this.rating)
        .fontSize(14)
        .fontColor('#666')
        .margin({ top: 8 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F6F8')
  }
}

四、避坑指南

问题

原因

修复

前景星不随 score 变

clipShape(Rect().width(...))中 width 未绑定响应式 score

确保用 @Link score驱动、Rect width 表达式中含 this.score

拖拽跳值不准

未用组件 onAreaChange取实际渲染宽算比率

onAreaChange(_,nv)=>this.compWidth=nv.width;Pan 中用 fingerList[0].x / compWidth

半星量化偏差

JS 浮点 2.5 * 2/2偶尔 2.49999

Math.round(raw/step)*step标准做法,已内置 quantize()

想支持只读展示

不加 PanGesture/onClick,只传 @Prop score

同组件只读模式——ContentModifier 仍正常工作

星星间距过大

ForEach Row 默认无 spacing

Row 设 .space(2)或调 starSize紧凑


五、总结:滑动评分五星动效 SOP

  1. Progress+ ProgressConfigurationContentModifier​ 自定义内容区

  2. 背景 Row(灰星) / 前景 Row(金星) 叠放,前景 clipShape(Rect)宽度 = score/max × 总星宽

  3. PanGesture.onActionUpdate​ 映射触摸X→分值→animateTo驱动 score;onClick点选

  4. 量化函数 quantize(raw, step)​ 按整星/半星四舍五入,确保最终值合规

  5. 只读展示去掉手势即可复用同组件

核心法则:HarmonyOS 6 中滑动评分 = "Progress ContentModifier 双 Row 星图 + Rect 裁剪比例 + PanGesture 映射 X→分值 animateTo",不依赖多个 Image 独立状态。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐