跨设备五星评分组件:基于HarmonyOS的自定义交互控件与同步实现 原创

进修的泡芙
发布于 2025-6-18 21:10
浏览
0收藏

跨设备五星评分组件:基于HarmonyOS的自定义交互控件与同步实现

一、项目概述

本文实现一个基于HarmonyOS的自定义五星评分组件,该组件支持触摸交互、动画效果,并借鉴《鸿蒙跨端U同步》中的状态同步机制,实现评分结果在多设备间的实时同步。该组件适用于商品评价、应用评分、满意度调查等多设备协同场景。

二、架构设计

±--------------------+ ±--------------------+
五星评分组件 <-----> 评分同步服务
(StarRatingView) (RatingSyncService)
±---------±---------+ ±---------±---------+

±---------v----------+ ±---------v----------+
Canvas绘制引擎 分布式数据管理
(StarRenderer) (DistributedData)
±---------±---------+ ±---------±---------+

±---------v-----------------------------v----------+
HarmonyOS图形系统 |

±--------------------------------------------------+

三、核心代码实现
五星评分组件实现

public class StarRatingView extends Component implements Component.DrawTask, Component.TouchEventListener {
private static final String TAG = “StarRatingView”;

// 默认样式参数
private static final int DEFAULT_STAR_COUNT = 5;
private static final int DEFAULT_RATING = 0;
private static final Color DEFAULT_STAR_COLOR = Color.GRAY;
private static final Color DEFAULT_FILLED_COLOR = Color.YELLOW;
private static final int DEFAULT_STAR_SIZE = 48;
private static final int DEFAULT_STAR_PADDING = 8;

// 组件状态
private int starCount = DEFAULT_STAR_COUNT;
private float rating = DEFAULT_RATING;
private Color starColor = DEFAULT_STAR_COLOR;
private Color filledColor = DEFAULT_FILLED_COLOR;
private int starSize = DEFAULT_STAR_SIZE;
private int starPadding = DEFAULT_STAR_PADDING;
private boolean interactive = true;
private String syncId;

// 动画相关
private AnimatorValue animator;
private float animatedRating = DEFAULT_RATING;

// 同步服务
private RatingSyncService syncService;

public StarRatingView(Context context) {
    super(context);
    init();

public StarRatingView(Context context, AttrSet attrSet) {

    super(context, attrSet);
    initAttributes(attrSet);
    init();

private void init() {

    addDrawTask(this);
    setTouchEventListener(this);
    
    // 初始化动画
    animator = new AnimatorValue();
    animator.setDuration(300);
    animator.setCurveType(IAnimator.CurveType.LINEAR);
    animator.setValueUpdateListener((animator, v) -> {
        animatedRating = v;
        invalidate();
    });
    
    // 初始化同步服务
    syncService = RatingSyncService.getInstance(getContext());

private void initAttributes(AttrSet attrSet) {

    starCount = attrSet.getAttr("star_count").isPresent() ? 
        attrSet.getAttr("star_count").get().getIntegerValue() : DEFAULT_STAR_COUNT;
        
    rating = attrSet.getAttr("rating").isPresent() ? 
        attrSet.getAttr("rating").get().getFloatValue() : DEFAULT_RATING;
        
    starColor = attrSet.getAttr("star_color").isPresent() ? 
        attrSet.getAttr("star_color").get().getColorValue() : DEFAULT_STAR_COLOR;
        
    filledColor = attrSet.getAttr("filled_color").isPresent() ? 
        attrSet.getAttr("filled_color").get().getColorValue() : DEFAULT_FILLED_COLOR;
        
    starSize = attrSet.getAttr("star_size").isPresent() ? 
        attrSet.getAttr("star_size").get().getIntegerValue() : DEFAULT_STAR_SIZE;
        
    starPadding = attrSet.getAttr("star_padding").isPresent() ? 
        attrSet.getAttr("star_padding").get().getIntegerValue() : DEFAULT_STAR_PADDING;
        
    interactive = attrSet.getAttr("interactive").isPresent() ? 
        attrSet.getAttr("interactive").get().getBoolValue() : true;
        
    syncId = attrSet.getAttr("sync_id").isPresent() ? 
        attrSet.getAttr("sync_id").get().getStringValue() : null;
        
    animatedRating = rating;

@Override

public void onDraw(Component component, Canvas canvas) {
    int width = getWidth();
    int height = getHeight();
    int totalWidth = starCount  starSize + (starCount - 1)  starPadding;
    int startX = (width - totalWidth) / 2;
    int startY = (height - starSize) / 2;
    
    // 绘制星星
    for (int i = 0; i < starCount; i++) {
        float starLeft = startX + i * (starSize + starPadding);
        
        // 绘制背景星星
        drawStar(canvas, starLeft, startY, starColor);
        
        // 绘制填充星星
        if (i < animatedRating) {
            float fillWidth = starSize;
            if (i < animatedRating && i + 1 > animatedRating) {
                fillWidth = (animatedRating - i) * starSize;

// 裁剪部分填充

            canvas.save();
            canvas.clipRect(new RectFloat(starLeft, startY, starLeft + fillWidth, startY + starSize));
            drawStar(canvas, starLeft, startY, filledColor);
            canvas.restore();

}

// 绘制单个星星

private void drawStar(Canvas canvas, float left, float top, Color color) {
    Paint paint = new Paint();
    paint.setColor(color);
    paint.setStyle(Paint.Style.FILL);
    
    // 五角星路径
    Path path = new Path();
    float centerX = left + starSize / 2f;
    float centerY = top + starSize / 2f;
    float radius = starSize / 2f;
    
    for (int i = 0; i < 5; i++) {
        float angle = (float) (2  Math.PI  i / 5 - Math.PI / 2);
        float x = centerX + radius * (float) Math.cos(angle);
        float y = centerY + radius * (float) Math.sin(angle);
        
        if (i == 0) {
            path.moveTo(x, y);

else {

            path.lineTo(x, y);

// 内角点

        angle += Math.PI / 5;

= centerX + radius 0.4f (float) Math.cos(angle);

= centerY + radius 0.4f (float) Math.sin(angle);

        path.lineTo(x, y);

path.close();

    canvas.drawPath(path, paint);

@Override

public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
    if (!interactive) {
        return false;

int action = touchEvent.getAction();

    if (action  TouchEvent.PRIMARY_POINT_DOWN || action  TouchEvent.POINT_MOVE) {
        updateRating(touchEvent.getPointerPosition(0).getX());
        return true;

return false;

private void updateRating(float touchX) {

    int width = getWidth();
    int totalWidth = starCount  starSize + (starCount - 1)  starPadding;
    int startX = (width - totalWidth) / 2;
    
    // 计算触摸位置对应的评分
    float relativeX = touchX - startX;
    if (relativeX < 0) {
        setRating(0);
        return;

float newRating = relativeX / (starSize + starPadding);

    if (newRating > starCount) {
        newRating = starCount;

// 增加0.5步长的吸附效果

    newRating = Math.round(newRating * 2) / 2f;
    setRating(newRating);

// 设置评分

public void setRating(float rating) {
    this.rating = Math.min(Math.max(rating, 0), starCount);
    
    // 启动动画
    animator.cancel();
    animator.setFloatValues(animatedRating, this.rating);
    animator.start();
    
    // 同步状态到其他设备
    if (syncId != null) {
        syncService.syncRating(syncId, this.rating);

}

// 获取当前评分
public float getRating() {
    return rating;

// 注册同步监听器

public void registerSyncListener() {
    if (syncId != null) {
        syncService.registerListener(syncId, new RatingSyncService.RatingListener() {
            @Override
            public void onRatingChanged(String id, float newRating, String fromDevice) {
                getUITaskDispatcher().asyncDispatch(() -> {
                    setRating(newRating);
                });

});

}

评分同步服务实现

public class RatingSyncService {
private static final String TAG = “RatingSyncService”;
private static final String SYNC_CHANNEL = “rating_sync”;
private static RatingSyncService instance;

private DistributedDataManager dataManager;
private Map<String, RatingListener> listeners = new HashMap<>();

private RatingSyncService(Context context) {
    this.dataManager = DistributedDataManagerFactory.getInstance()
        .createDistributedDataManager(context);
    initDataListener();

public static synchronized RatingSyncService getInstance(Context context) {

    if (instance == null) {
        instance = new RatingSyncService(context);

return instance;

private void initDataListener() {

    dataManager.registerDataChangeListener(SYNC_CHANNEL, new DataChangeListener() {
        @Override
        public void onDataChanged(String deviceId, String key, String value) {
            try {
                JSONObject ratingJson = new JSONObject(value);
                String syncId = ratingJson.getString("sync_id");
                float rating = (float) ratingJson.getDouble("rating");
                long timestamp = ratingJson.getLong("timestamp");
                String sourceDevice = ratingJson.getString("device_id");
                int version = ratingJson.optInt("version", 0);
                
                // 忽略本地设备发送的更新
                if (sourceDevice.equals(DistributedDeviceInfo.getLocalDeviceId())) {
                    return;

RatingListener listener = listeners.get(syncId);

                if (listener != null) {
                    listener.onRatingChanged(syncId, rating, sourceDevice);

} catch (JSONException e) {

                HiLog.error(TAG, "Failed to parse rating data");

}

    });

// 同步评分

public void syncRating(String syncId, float rating) {
    JSONObject ratingJson = new JSONObject();
    try {
        ratingJson.put("sync_id", syncId);
        ratingJson.put("rating", rating);
        ratingJson.put("timestamp", System.currentTimeMillis());
        ratingJson.put("device_id", DistributedDeviceInfo.getLocalDeviceId());
        ratingJson.put("version", getNextVersion());
        
        DistributedOptions options = new DistributedOptions();
        options.setPriority(DistributedOptions.Priority.HIGH);
        
        dataManager.putString(SYNC_CHANNEL, 
            ratingJson.toString(), 
            DistributedDataManager.PUT_MODE_RELIABLE, 
            options);

catch (JSONException e) {

        HiLog.error(TAG, "Failed to serialize rating data");

}

// 注册评分监听器
public void registerListener(String syncId, RatingListener listener) {
    listeners.put(syncId, listener);

// 取消注册监听器

public void unregisterListener(String syncId) {
    listeners.remove(syncId);

public interface RatingListener {

    void onRatingChanged(String syncId, float rating, String fromDevice);

private int versionCounter = 0;

private synchronized int getNextVersion() {
    return ++versionCounter;

}

组件XML属性定义 (resources/attrs.xml)

<?xml version=“1.0” encoding=“UTF-8”?>
<resources>
<attr name=“star_count” format=“integer”/>
<attr name=“rating” format=“float”/>
<attr name=“star_color” format=“color”/>
<attr name=“filled_color” format=“color”/>
<attr name=“star_size” format=“integer”/>
<attr name=“star_padding” format=“integer”/>
<attr name=“interactive” format=“boolean”/>
<attr name=“sync_id” format=“string”/>

<declare-styleable name="StarRatingView">
    <attr name="star_count"/>
    <attr name="rating"/>
    <attr name="star_color"/>
    <attr name="filled_color"/>
    <attr name="star_size"/>
    <attr name="star_padding"/>
    <attr name="interactive"/>
    <attr name="sync_id"/>
</declare-styleable>

</resources>

使用示例 (XML布局)

<DirectionalLayout
xmlns:ohos=“http://schemas.huawei.com/res/ohos
xmlns:app=“http://schemas.huawei.com/res/ohos-auto
ohos:width=“match_parent”
ohos:height=“match_parent”
ohos:orientation=“vertical”
ohos:padding=“24vp”>

<Text
    ohos:width="match_parent"
    ohos:height="wrap_content"
    ohos:text="请为本次服务评分"
    ohos:text_size="20fp"
    ohos:margin_bottom="16vp"/>
    
<com.example.rating.StardRatingView
    ohos:id="$+id:rating_view"
    ohos:width="match_parent"
    ohos:height="80vp"
    app:star_count="5"
    app:rating="0"
    app:star_color="#DDDDDD"
    app:filled_color="#FFCC00"
    app:star_size="48"
    app:star_padding="12"
    app:interactive="true"
    app:sync_id="service_rating"/>
    
<Text
    ohos:id="$+id:rating_text"
    ohos:width="match_parent"
    ohos:height="wrap_content"
    ohos:text="当前评分: 0"
    ohos:text_size="16fp"
    ohos:margin_top="16vp"
    ohos:text_alignment="center"/>
    
<Text
    ohos:id="$+id:sync_status"
    ohos:width="match_parent"
    ohos:height="wrap_content"
    ohos:text="同步状态: 等待输入"
    ohos:text_size="14fp"
    ohos:margin_top="8vp"
    ohos:text_alignment="center"/>

</DirectionalLayout>

使用示例 (Java代码)

public class RatingDemoSlice extends AbilitySlice {
private StarRatingView ratingView;
private Text ratingText;
private Text syncStatus;

@Override
public void onStart(Intent intent) {
    super.onStart(intent);
    setUIContent(ResourceTable.Layout_rating_demo_layout);
    
    ratingView = (StarRatingView) findComponentById(ResourceTable.Id_rating_view);
    ratingText = (Text) findComponentById(ResourceTable.Id_rating_text);
    syncStatus = (Text) findComponentById(ResourceTable.Id_sync_status);
    
    // 注册同步监听器
    ratingView.registerSyncListener();
    
    // 设置评分变化监听
    ratingView.setRatingChangeListener(new StarRatingView.OnRatingChangeListener() {
        @Override
        public void onRatingChanged(float rating) {
            ratingText.setText("当前评分: " + rating);

});

    // 监听同步状态
    RatingSyncService.getInstance(this)
        .registerListener("service_rating", new RatingSyncService.RatingListener() {
            @Override
            public void onRatingChanged(String id, float rating, String fromDevice) {
                getUITaskDispatcher().asyncDispatch(() -> {
                    syncStatus.setText("同步状态: 来自" + fromDevice + "的评分: " + rating);
                });

});

@Override

protected void onStop() {
    super.onStop();
    // 取消注册监听器
    RatingSyncService.getInstance(this).unregisterListener("service_rating");

}

四、与《鸿蒙跨端U同步》的技术关联

本项目借鉴了游戏多设备同步的以下关键技术:
状态同步模型:类似游戏中玩家状态的实时同步,评分状态通过JSON格式广播

设备标识:使用设备ID区分不同来源的评分

版本控制:引入版本号解决潜在的冲突问题

可靠传输:使用高优先级的数据传输确保同步成功率

增强的同步逻辑(借鉴游戏同步机制):

// 增强的评分同步方法
public void syncRating(String syncId, float rating) {
JSONObject ratingJson = new JSONObject();
try {
ratingJson.put(“sync_id”, syncId);
ratingJson.put(“rating”, rating);
ratingJson.put(“timestamp”, System.currentTimeMillis());
ratingJson.put(“device_id”, DistributedDeviceInfo.getLocalDeviceId());
ratingJson.put(“version”, getNextVersion());

    // 增加校验码
    ratingJson.put("checksum", calculateChecksum(rating));
    
    // 设置传输选项
    DistributedOptions options = new DistributedOptions();
    options.setPriority(DistributedOptions.Priority.HIGH);
    options.setTimeToLive(15000); // 15秒有效期
    options.setRetryCount(3); // 重试3次
    
    // 使用可靠传输
    int result = dataManager.putString(SYNC_CHANNEL, 
        ratingJson.toString(), 
        DistributedDataManager.PUT_MODE_RELIABLE, 
        options);
        
    if (result != 0) {
        HiLog.warn(TAG, "Rating sync failed with code: " + result);
        // 可以在这里实现重试逻辑

} catch (JSONException e) {

    HiLog.error(TAG, "Failed to serialize rating data");

}

// 校验评分数据完整性
private boolean validateRatingData(JSONObject ratingJson) {
try {
float rating = (float) ratingJson.getDouble(“rating”);
int checksum = ratingJson.getInt(“checksum”);
return checksum == calculateChecksum(rating);
catch (JSONException e) {

    return false;

}

// 简单的校验码计算
private int calculateChecksum(float rating) {
return (int)(rating * 100) ^ 0x55AA;

五、项目特色与创新点
自定义绘制:通过Canvas实现高性能的五角星绘制

交互体验:支持触摸滑动评分和点击评分

动画效果:平滑的评分变化动画

跨设备同步:评分结果可在多设备间实时同步

灵活配置:支持自定义星星数量、颜色、大小等

六、应用场景
电商评价系统:多设备同步的商品评分

应用商店:应用评分的多端展示

满意度调查:跨设备的调查问卷评分

教学评价系统:师生互评的同步展示

七、总结

本五星评分组件实现了以下功能:
自定义Canvas绘制五角星评分控件

支持触摸交互和动画效果

多设备间评分结果实时同步

丰富的样式定制选项

可靠的同步传输机制

通过借鉴游戏中的状态同步技术,我们构建了一个可靠的跨设备评分系统。未来可扩展功能包括:
半星评分支持

更多评分样式(心形、拇指等)

评分结果统计分析

基于区块链的评分存证

这个组件展示了HarmonyOS自定义UI组件开发的能力,以及如何将游戏中的同步技术应用于交互控件的开发中。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐