圆形进度条:基于HarmonyOS Canvas的自定义组件实现与跨设备同步

一、项目概述

本文实现一个基于HarmonyOS的自定义圆形进度条组件,该组件通过Canvas绘制动态进度环并支持百分比显示。同时借鉴《鸿蒙跨端U同步》中的状态同步机制,使进度条状态能够在多设备间实时同步,适用于健身应用、文件传输进度显示等多设备协同场景。

二、架构设计

+---------------------+       +---------------------+
|  圆形进度条组件     |<----->|  状态同步服务       |
| (CircleProgress)    |       | (StateSyncService)  |
+----------+----------+       +----------+----------+
           |                             |
+----------v----------+       +----------v----------+
|  Canvas绘制引擎     |       |  分布式数据管理     |
| (CanvasRenderer)    |       | (DistributedData)   |
+----------+----------+       +----------+----------+
           |                             |
+----------v-----------------------------v----------+
|                 HarmonyOS图形系统                  |
+---------------------------------------------------+

三、核心代码实现

1. 圆形进度条组件实现

public class CircleProgress extends Component implements Component.DrawTask {
    private static final String TAG = "CircleProgress";
    
    // 默认样式参数
    private static final int DEFAULT_WIDTH = 200;
    private static final int DEFAULT_HEIGHT = 200;
    private static final int DEFAULT_STROKE_WIDTH = 20;
    private static final Color DEFAULT_BACKGROUND_COLOR = Color.GRAY;
    private static final Color DEFAULT_PROGRESS_COLOR = Color.BLUE;
    private static final Color DEFAULT_TEXT_COLOR = Color.BLACK;
    
    // 组件状态
    private int progress = 0;
    private int maxProgress = 100;
    private int strokeWidth = DEFAULT_STROKE_WIDTH;
    private Color backgroundColor = DEFAULT_BACKGROUND_COLOR;
    private Color progressColor = DEFAULT_PROGRESS_COLOR;
    private Color textColor = DEFAULT_TEXT_COLOR;
    private boolean showText = true;
    private String syncId; // 用于跨设备同步的标识
    
    // 同步服务引用
    private StateSyncService stateSync;
    
    public CircleProgress(Context context) {
        super(context);
        init();
    }
    
    public CircleProgress(Context context, AttrSet attrSet) {
        super(context, attrSet);
        initAttributes(attrSet);
        init();
    }
    
    private void init() {
        // 设置绘制委托
        addDrawTask(this);
        
        // 初始化同步服务
        stateSync = StateSyncService.getInstance(getContext());
        
        // 设置默认尺寸
        setWidth(DEFAULT_WIDTH);
        setHeight(DEFAULT_HEIGHT);
    }
    
    private void initAttributes(AttrSet attrSet) {
        // 从XML属性初始化样式
        strokeWidth = attrSet.getAttr("stroke_width").isPresent() ? 
            attrSet.getAttr("stroke_width").get().getIntegerValue() : DEFAULT_STROKE_WIDTH;
            
        backgroundColor = attrSet.getAttr("bg_color").isPresent() ? 
            attrSet.getAttr("bg_color").get().getColorValue() : DEFAULT_BACKGROUND_COLOR;
            
        progressColor = attrSet.getAttr("progress_color").isPresent() ? 
            attrSet.getAttr("progress_color").get().getColorValue() : DEFAULT_PROGRESS_COLOR;
            
        textColor = attrSet.getAttr("text_color").isPresent() ? 
            attrSet.getAttr("text_color").get().getColorValue() : DEFAULT_TEXT_COLOR;
            
        showText = attrSet.getAttr("show_text").isPresent() ? 
            attrSet.getAttr("show_text").get().getBoolValue() : true;
            
        syncId = attrSet.getAttr("sync_id").isPresent() ? 
            attrSet.getAttr("sync_id").get().getStringValue() : null;
    }
    
    @Override
    public void onDraw(Component component, Canvas canvas) {
        // 获取组件尺寸
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width, height) / 2 - strokeWidth / 2;
        int centerX = width / 2;
        int centerY = height / 2;
        
        // 绘制背景圆
        Paint bgPaint = new Paint();
        bgPaint.setStyle(Paint.Style.STROKE);
        bgPaint.setStrokeWidth(strokeWidth);
        bgPaint.setColor(backgroundColor);
        canvas.drawCircle(centerX, centerY, radius, bgPaint);
        
        // 绘制进度弧
        Paint progressPaint = new Paint();
        progressPaint.setStyle(Paint.Style.STROKE);
        progressPaint.setStrokeWidth(strokeWidth);
        progressPaint.setColor(progressColor);
        progressPaint.setStrokeCap(Paint.StrokeCap.ROUND);
        
        RectFloat oval = new RectFloat(
            centerX - radius, 
            centerY - radius, 
            centerX + radius, 
            centerY + radius
        );
        
        float sweepAngle = 360 * progress / (float) maxProgress;
        canvas.drawArc(oval, -90, sweepAngle, false, progressPaint);
        
        // 绘制进度文本
        if (showText) {
            Paint textPaint = new Paint();
            textPaint.setColor(textColor);
            textPaint.setTextSize(radius * 0.5f);
            textPaint.setTextAlign(Paint.Align.CENTER);
            
            String text = progress + "%";
            canvas.drawText(textPaint, text, centerX, centerY + radius * 0.1f);
        }
    }
    
    // 设置当前进度
    public void setProgress(int progress) {
        this.progress = Math.min(Math.max(progress, 0), maxProgress);
        
        // 同步状态到其他设备
        if (syncId != null) {
            stateSync.syncProgress(syncId, this.progress);
        }
        
        // 请求重绘
        invalidate();
    }
    
    // 设置最大值
    public void setMaxProgress(int max) {
        this.maxProgress = max > 0 ? max : 100;
        invalidate();
    }
    
    // 其他样式设置方法...
    
    // 注册同步监听器
    public void registerSyncListener() {
        if (syncId != null) {
            stateSync.registerProgressListener(syncId, new StateSyncService.ProgressListener() {
                @Override
                public void onProgressChanged(String id, int newProgress) {
                    getUITaskDispatcher().asyncDispatch(() -> {
                        setProgress(newProgress);
                    });
                }
            });
        }
    }
}

2. 状态同步服务实现

public class StateSyncService {
    private static final String TAG = "StateSyncService";
    private static final String PROGRESS_SYNC_CHANNEL = "progress_sync";
    private static StateSyncService instance;
    
    private DistributedDataManager dataManager;
    private Map<String, ProgressListener> progressListeners = new HashMap<>();
    
    private StateSyncService(Context context) {
        this.dataManager = DistributedDataManagerFactory.getInstance()
            .createDistributedDataManager(context);
        initDataListener();
    }
    
    public static synchronized StateSyncService getInstance(Context context) {
        if (instance == null) {
            instance = new StateSyncService(context);
        }
        return instance;
    }
    
    private void initDataListener() {
        // 监听进度变化
        dataManager.registerDataChangeListener(PROGRESS_SYNC_CHANNEL, new DataChangeListener() {
            @Override
            public void onDataChanged(String deviceId, String key, String value) {
                try {
                    JSONObject progressJson = new JSONObject(value);
                    String syncId = progressJson.getString("sync_id");
                    int progress = progressJson.getInt("progress");
                    
                    ProgressListener listener = progressListeners.get(syncId);
                    if (listener != null) {
                        listener.onProgressChanged(syncId, progress);
                    }
                } catch (JSONException e) {
                    HiLog.error(TAG, "Failed to parse progress data");
                }
            }
        });
    }
    
    // 同步进度状态
    public void syncProgress(String syncId, int progress) {
        JSONObject progressJson = new JSONObject();
        try {
            progressJson.put("sync_id", syncId);
            progressJson.put("progress", progress);
            progressJson.put("timestamp", System.currentTimeMillis());
            progressJson.put("device_id", DistributedDeviceInfo.getLocalDeviceId());
            
            dataManager.putString(PROGRESS_SYNC_CHANNEL, progressJson.toString());
        } catch (JSONException e) {
            HiLog.error(TAG, "Failed to serialize progress data");
        }
    }
    
    // 注册进度监听器
    public void registerProgressListener(String syncId, ProgressListener listener) {
        progressListeners.put(syncId, listener);
    }
    
    // 取消注册进度监听器
    public void unregisterProgressListener(String syncId) {
        progressListeners.remove(syncId);
    }
    
    public interface ProgressListener {
        void onProgressChanged(String syncId, int progress);
    }
}

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

<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <attr name="stroke_width" format="integer"/>
    <attr name="bg_color" format="color"/>
    <attr name="progress_color" format="color"/>
    <attr name="text_color" format="color"/>
    <attr name="show_text" format="boolean"/>
    <attr name="sync_id" format="string"/>
    
    <declare-styleable name="CircleProgress">
        <attr name="stroke_width"/>
        <attr name="bg_color"/>
        <attr name="progress_color"/>
        <attr name="text_color"/>
        <attr name="show_text"/>
        <attr name="sync_id"/>
    </declare-styleable>
</resources>

4. 使用示例 (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">
    
    <com.example.circleprogress.CircleProgress
        ohos:id="$+id:progress1"
        ohos:width="200vp"
        ohos:height="200vp"
        app:stroke_width="15"
        app:bg_color="#EEEEEE"
        app:progress_color="#4285F4"
        app:text_color="#000000"
        app:show_text="true"
        app:sync_id="workout_progress"/>
        
    <Slider
        ohos:id="$+id:progress_control"
        ohos:width="match_parent"
        ohos:height="wrap_content"
        ohos:margin_top="24vp"
        ohos:min_value="0"
        ohos:max_value="100"
        ohos:progress="0"/>
</DirectionalLayout>

5. 使用示例 (Java代码)

public class ProgressDemoSlice extends AbilitySlice {
    private CircleProgress circleProgress;
    private Slider progressControl;
    
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        setUIContent(ResourceTable.Layout_progress_demo_layout);
        
        // 获取组件
        circleProgress = (CircleProgress) findComponentById(ResourceTable.Id_progress1);
        progressControl = (Slider) findComponentById(ResourceTable.Id_progress_control);
        
        // 注册同步监听器
        circleProgress.registerSyncListener();
        
        // 设置滑动条监听
        progressControl.setValueChangedListener((slider, value, fromUser) -> {
            circleProgress.setProgress(value);
        });
        
        // 模拟进度变化
        getUITaskDispatcher().delayDispatch(() -> {
            animateProgress(0, 75, 2000);
        }, 1000);
    }
    
    private void animateProgress(int from, int to, long duration) {
        ValueAnimator animator = ValueAnimator.ofInt(from, to);
        animator.setDuration(duration);
        animator.setCurveType(IAnimator.CurveType.LINEAR);
        animator.setValueUpdateListener((animator, value) -> {
            circleProgress.setProgress((int) value);
        });
        animator.start();
    }
}

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

本项目的跨设备同步机制借鉴了游戏中的多设备同步方案:

  1. ​状态同步模型​​:类似游戏中玩家状态的同步,进度条状态通过JSON格式广播
  2. ​冲突解决​​:基于时间戳的更新策略,确保最终一致性
  3. ​设备过滤​​:避免本地设备处理自己发送的状态更新
  4. ​轻量级协议​​:使用最小必要数据实现同步,减少网络开销

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

// 增强的进度同步方法
public void syncProgress(String syncId, int progress) {
    JSONObject progressJson = new JSONObject();
    try {
        progressJson.put("sync_id", syncId);
        progressJson.put("progress", progress);
        progressJson.put("timestamp", System.currentTimeMillis());
        progressJson.put("device_id", DistributedDeviceInfo.getLocalDeviceId());
        progressJson.put("version", getNextVersion());
        
        // 增加可靠性选项
        DistributedOptions options = new DistributedOptions();
        options.setPriority(DistributedOptions.Priority.HIGH);
        options.setTimeToLive(5000); // 5秒有效期
        
        // 使用可靠传输
        dataManager.putString(PROGRESS_SYNC_CHANNEL, 
            progressJson.toString(), 
            DistributedDataManager.PUT_MODE_RELIABLE, 
            options);
    } catch (JSONException e) {
        HiLog.error(TAG, "Failed to serialize progress data");
    }
}

// 增强的数据监听器
private void initDataListener() {
    dataManager.registerDataChangeListener(PROGRESS_SYNC_CHANNEL, new DataChangeListener() {
        @Override
        public void onDataChanged(String deviceId, String key, String value) {
            try {
                JSONObject progressJson = new JSONObject(value);
                String syncId = progressJson.getString("sync_id");
                int progress = progressJson.getInt("progress");
                long timestamp = progressJson.getLong("timestamp");
                String sourceDevice = progressJson.getString("device_id");
                int version = progressJson.optInt("version", 0);
                
                // 忽略本地设备发送的更新
                if (sourceDevice.equals(DistributedDeviceInfo.getLocalDeviceId())) {
                    return;
                }
                
                // 获取当前状态
                ProgressState currentState = getCurrentState(syncId);
                
                // 冲突解决策略:选择最新版本或时间戳更大的更新
                if (currentState == null || 
                    version > currentState.version || 
                    (version == currentState.version && timestamp > currentState.timestamp)) {
                    
                    ProgressListener listener = progressListeners.get(syncId);
                    if (listener != null) {
                        listener.onProgressChanged(syncId, progress);
                    }
                    
                    // 更新本地状态
                    saveCurrentState(syncId, new ProgressState(progress, version, timestamp));
                }
            } catch (JSONException e) {
                HiLog.error(TAG, "Failed to parse progress data");
            }
        }
    });
}

五、项目特色与创新点

  1. ​自定义绘制​​:通过Canvas实现高性能的圆形进度绘制
  2. ​灵活样式​​:支持自定义颜色、线宽、文本显示等
  3. ​跨设备同步​​:进度状态可在多设备间实时同步
  4. ​动画支持​​:内置平滑的进度变化动画
  5. ​轻量级实现​​:核心代码精简,性能高效

六、总结

本圆形进度条组件实现了以下功能:

  1. 自定义Canvas绘制圆形进度条
  2. 支持百分比文本显示
  3. 多设备间进度状态同步
  4. 平滑的进度变化动画
  5. 丰富的样式定制选项

通过借鉴游戏中的状态同步机制,我们实现了可靠的跨设备进度同步,适用于以下场景:

  1. 健身应用中训练进度的多设备展示
  2. 文件传输进度的跨设备监控
  3. 多设备协同任务的进度同步
  4. IoT设备状态的多端展示

未来可扩展功能包括:

  1. 多段颜色进度(如红黄绿渐变)
  2. 内环图标显示
  3. 更丰富的动画效果
  4. 同步过程中的冲突解决策略优化

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

Logo

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

更多推荐