
跨设备五星评分组件:基于HarmonyOS的自定义交互控件与同步实现 原创
跨设备五星评分组件:基于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组件开发的能力,以及如何将游戏中的同步技术应用于交互控件的开发中。
