自定义圆形进度条

63 阅读7分钟
public class FirmwareUpdateActivity extends BaseMvvmActivity<ActivityFirmwareUpdateBinding> {
    
    private static final String TAG_DOWNLOAD_FRAGMENT = "download_fragment";
    private static final String TAG_INSTALL_FRAGMENT = "install_fragment";
    
    @Nullable
    @Override
    protected ActivityFirmwareUpdateBinding bindContentView(@Nullable Bundle bundle) {
        return ActivityFirmwareUpdateBinding.inflate(getLayoutInflater());
    }
    
    @Override
    protected void subscribeViewModel(@Nullable Bundle bundle) {
        initViews();
        startDownloadPhase();
    }
    
    private void initViews() {
        // 设置Toolbar
        viewBinding.ivBack.setOnClickListener(v -> onBackPressed());
        
        // 设置固定的描述文本
        viewBinding.tvDescription.setText("The new firmware will be downloaded by your\n" +
                "Deco system, not this app. No mobile data or\n" +
                "storage on this device is required.\n" +
                "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n" +
                "XXXXXXXXXXXX");
    }
    
    private void startDownloadPhase() {
        // 显示下载Fragment
        FirmwareDownloadFragment downloadFragment = new FirmwareDownloadFragment();
        downloadFragment.setOnDownloadCompleteListener(this::onDownloadComplete);
        
        getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.fragment_container, downloadFragment, TAG_DOWNLOAD_FRAGMENT)
                .commit();
    }
    
    private void onDownloadComplete() {
        // 下载完成,切换到安装Fragment
        FirmwareInstallFragment installFragment = new FirmwareInstallFragment();
        installFragment.setOnInstallCompleteListener(this::onInstallComplete);
        
        getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.fragment_container, installFragment, TAG_INSTALL_FRAGMENT)
                .addToBackStack(null)
                .commit();
    }
    
    private void onInstallComplete() {
        // 安装完成,可以返回或显示完成页面
        // 这里可以显示成功提示或自动返回
        finish();
    }
    
    @Override
    public void onBackPressed() {
        // 升级过程中禁止返回,或显示确认对话框
        showCancelUpgradeDialog();
    }
    
    private void showCancelUpgradeDialog() {
        // 显示取消升级确认对话框
        // 这里可以实现确认对话框逻辑
    }
}

在创建主Activity的布局:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white">

    <!-- Toolbar -->
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:elevation="0dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/iv_back"
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:layout_alignParentStart="true"
                android:layout_centerVertical="true"
                android:src="@drawable/ic_menu"
                android:contentDescription="@string/back" />

            <TextView
                android:id="@+id/tv_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="TP WIFI"
                android:textColor="@color/black"
                android:textSize="18sp"
                android:textStyle="bold" />

            <ImageView
                android:id="@+id/iv_add"
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:layout_alignParentEnd="true"
                android:layout_centerVertical="true"
                android:layout_marginEnd="16dp"
                android:src="@drawable/ic_add"
                android:contentDescription="@string/add" />

        </RelativeLayout>

    </androidx.appcompat.widget.Toolbar>

    <!-- Description Text -->
    <TextView
        android:id="@+id/tv_description"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:layout_marginHorizontal="32dp"
        android:text="The new firmware will be downloaded by your\nDeco system, not this app. No mobile data or\nstorage on this device is required.\nXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\nXXXXXXXXXXXX"
        android:textColor="@color/black"
        android:textSize="16sp"
        android:lineSpacingExtra="4dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/toolbar" />

    <!-- Fragment Container -->
    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="32dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_description" />

</androidx.constraintlayout.widget.ConstraintLayout>

在修改下载Fragment,添加完成回调:

public class FirmwareDownloadFragment extends BaseMvvmFragment<FragmentFirmwareDownloadBinding> {
    
    private Handler progressHandler;
    private Runnable progressRunnable;
    private int currentProgress = 0;
    private static final int TOTAL_DURATION = 60000; // 1分钟 = 60秒
    private static final int UPDATE_INTERVAL = 600; // 每600ms更新一次
    private static final int PROGRESS_STEP = 1; // 每次增加1%
    
    // 下载完成回调接口
    public interface OnDownloadCompleteListener {
        void onDownloadComplete();
    }
    
    private OnDownloadCompleteListener downloadCompleteListener;
    
    @Nullable
    @Override
    protected FragmentFirmwareDownloadBinding bindView(@NonNull LayoutInflater layoutInflater, 
                                                   @Nullable ViewGroup viewGroup, 
                                                   @Nullable Bundle bundle) {
        return FragmentFirmwareDownloadBinding.inflate(layoutInflater, viewGroup, false);
    }

    @Override
    protected void subscribeViewModel(@Nullable Bundle bundle) {
        // 初始化进度处理器
        initProgressHandler();
        // 开始进度动画
        startProgressAnimation();
    }
    
    public void setOnDownloadCompleteListener(OnDownloadCompleteListener listener) {
        this.downloadCompleteListener = listener;
    }
    
    private void initProgressHandler() {
        progressHandler = new Handler(Looper.getMainLooper());
        progressRunnable = new Runnable() {
            @Override
            public void run() {
                if (currentProgress <= 100) {
                    // 更新进度条
                    updateProgress(currentProgress);
                    currentProgress += PROGRESS_STEP;
                    
                    // 如果还没有达到100%,继续下一次更新
                    if (currentProgress <= 100) {
                        progressHandler.postDelayed(this, UPDATE_INTERVAL);
                    } else {
                        // 进度完成,处理完成逻辑
                        onProgressCompleted();
                    }
                }
            }
        };
    }
    
    private void startProgressAnimation() {
        currentProgress = 0;
        progressHandler.post(progressRunnable);
    }
    
    private void updateProgress(int progress) {
        if (getViewDataBinding() != null && getViewDataBinding().progressWv != null) {
            // 更新TPWaveBallProgressIndicator的进度
            getViewDataBinding().progressWv.setWaveProgress(progress);
        }
    }
    
    private void onProgressCompleted() {
        // 进度完成后的处理逻辑
        if (getViewDataBinding() != null && getViewDataBinding().tvDownloading != null) {
            getViewDataBinding().tvDownloading.setText("Download Completed!");
        }
        
        // 延迟500ms后调用完成回调,切换到安装阶段
        if (progressHandler != null) {
            progressHandler.postDelayed(() -> {
                if (downloadCompleteListener != null) {
                    downloadCompleteListener.onDownloadComplete();
                }
            }, 500);
        }
    }
    
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        // 清理Handler,避免内存泄漏
        if (progressHandler != null && progressRunnable != null) {
            progressHandler.removeCallbacks(progressRunnable);
        }
    }
    
    @Override
    public void onPause() {
        super.onPause();
        // 暂停时停止进度更新
        if (progressHandler != null && progressRunnable != null) {
            progressHandler.removeCallbacks(progressRunnable);
        }
    }
    
    @Override
    public void onResume() {
        super.onResume();
        // 恢复时继续进度更新(如果还没完成)
        if (currentProgress < 100) {
            progressHandler.post(progressRunnable);
        }
    }
}

下载Fragment的布局:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Progress Indicator -->
    <com.tplink.design.indicator.TPWaveBallProgressIndicator
        android:id="@+id/progress_wv"
        style="@style/Widget.TPDesign.WaveBallProgressIndicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="80dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:waveProgress="0" />

    <!-- Downloading Text -->
    <TextView
        android:id="@+id/tv_downloading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:text="Downloading..."
        android:textColor="@color/black"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/progress_wv" />

</androidx.constraintlayout.widget.ConstraintLayout>

在创建安装Fragment:

public class FirmwareInstallFragment extends BaseMvvmFragment<FragmentFirmwareInstallBinding> {
    
    private CountDownTimer countDownTimer;
    private static final long TOTAL_TIME_MILLIS = 180000; // 3分钟 = 180秒
    private ObjectAnimator progressAnimator;
    
    // 安装完成回调接口
    public interface OnInstallCompleteListener {
        void onInstallComplete();
    }
    
    private OnInstallCompleteListener installCompleteListener;
    
    @Nullable
    @Override
    protected FragmentFirmwareInstallBinding bindView(@NonNull LayoutInflater layoutInflater, 
                                                   @Nullable ViewGroup viewGroup, 
                                                   @Nullable Bundle bundle) {
        return FragmentFirmwareInstallBinding.inflate(layoutInflater, viewGroup, false);
    }

    @Override
    protected void subscribeViewModel(@Nullable Bundle bundle) {
        initViews();
        startInstallation();
    }
    
    public void setOnInstallCompleteListener(OnInstallCompleteListener listener) {
        this.installCompleteListener = listener;
    }
    
    private void initViews() {
        // 设置进度条初始状态
        getViewDataBinding().circularProgressBar.setMax(100);
        getViewDataBinding().circularProgressBar.setProgress(0);
        
        // 设置初始文本
        getViewDataBinding().tvRemainingTime.setText("00:03:00");
        getViewDataBinding().tvStatus.setText("Installing new firmware...");
    }
    
    private void startInstallation() {
        // 启动进度条动画
        progressAnimator = ObjectAnimator.ofInt(
            getViewDataBinding().circularProgressBar, 
            "progress", 
            0, 
            100
        );
        progressAnimator.setDuration(TOTAL_TIME_MILLIS);
        progressAnimator.setInterpolator(new LinearInterpolator());
        progressAnimator.start();
        
        // 启动倒计时
        countDownTimer = new CountDownTimer(TOTAL_TIME_MILLIS, 1000) {
            @Override
            public void onTick(long millisUntilFinished) {
                // 更新倒计时显示
                updateRemainingTime(millisUntilFinished);
            }
            
            @Override
            public void onFinish() {
                // 安装完成
                getViewDataBinding().tvRemainingTime.setText("00:00:00");
                getViewDataBinding().tvStatus.setText("Firmware update completed!");
                onInstallationComplete();
            }
        };
        countDownTimer.start();
    }
    
    private void updateRemainingTime(long millisUntilFinished) {
        long minutes = (millisUntilFinished / 1000) / 60;
        long seconds = (millisUntilFinished / 1000) % 60;
        String timeFormatted = String.format("00:%02d:%02d", minutes, seconds);
        getViewDataBinding().tvRemainingTime.setText(timeFormatted);
    }
    
    private void onInstallationComplete() {
        // 延迟1秒后调用完成回调
        if (getViewDataBinding() != null) {
            new Handler(Looper.getMainLooper()).postDelayed(() -> {
                if (installCompleteListener != null) {
                    installCompleteListener.onInstallComplete();
                }
            }, 1000);
        }
    }
    
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        // 清理资源
        if (countDownTimer != null) {
            countDownTimer.cancel();
        }
        if (progressAnimator != null) {
            progressAnimator.cancel();
        }
    }
    
    @Override
    public void onPause() {
        super.onPause();
        // 暂停时停止计时器
        if (countDownTimer != null) {
            countDownTimer.cancel();
        }
        if (progressAnimator != null) {
            progressAnimator.pause();
        }
    }
    
    @Override
    public void onResume() {
        super.onResume();
        // 注意:实际项目中可能需要保存和恢复进度状态
        // 这里简化处理,如果需要可以实现状态保存逻辑
    }
}

安装Fragment的布局:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- 圆形进度条容器 -->
    <RelativeLayout
        android:id="@+id/progress_container"
        android:layout_width="280dp"
        android:layout_height="280dp"
        android:layout_marginTop="80dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <!-- 自定义圆形进度条 -->
        <com.yourpackage.widget.CircularProgressBar
            android:id="@+id/circular_progress_bar"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <!-- 中心内容 -->
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:orientation="vertical"
            android:gravity="center">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Remaining Time"
                android:textColor="#999999"
                android:textSize="16sp"
                android:layout_marginBottom="8dp" />

            <TextView
                android:id="@+id/tv_remaining_time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="00:03:00"
                android:textColor="@color/black"
                android:textSize="36sp"
                android:textStyle="bold" />

        </LinearLayout>

    </RelativeLayout>

    <!-- 状态文字 -->
    <TextView
        android:id="@+id/tv_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:text="Installing new firmware..."
        android:textColor="@color/black"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/progress_container" />

</androidx.constraintlayout.widget.ConstraintLayout>

后创建自定义圆形进度条:

package com.yourpackage.widget;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;

public class CircularProgressBar extends View {
    
    private Paint backgroundPaint;
    private Paint progressPaint;
    private RectF rectF;
    private float strokeWidth = 20;
    private int progress = 0;
    private int max = 100;
    
    public CircularProgressBar(Context context) {
        super(context);
        init();
    }
    
    public CircularProgressBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    
    public CircularProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    
    private void init() {
        // 背景圆环画笔
        backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        backgroundPaint.setColor(Color.parseColor("#E0E0E0"));
        backgroundPaint.setStyle(Paint.Style.STROKE);
        backgroundPaint.setStrokeWidth(strokeWidth);
        
        // 进度圆环画笔
        progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        progressPaint.setColor(Color.parseColor("#00BCD4")); // 青色
        progressPaint.setStyle(Paint.Style.STROKE);
        progressPaint.setStrokeWidth(strokeWidth);
        progressPaint.setStrokeCap(Paint.Cap.ROUND);
        
        rectF = new RectF();
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width, height) / 2;
        int centerX = width / 2;
        int centerY = height / 2;
        
        rectF.set(centerX - radius + strokeWidth / 2, 
                  centerY - radius + strokeWidth / 2,
                  centerX + radius - strokeWidth / 2, 
                  centerY + radius - strokeWidth / 2);
        
        // 绘制背景圆环
        canvas.drawArc(rectF, -90, 360, false, backgroundPaint);
        
        // 绘制进度圆环
        float angle = 360 * progress / (float) max;
        canvas.drawArc(rectF, -90, angle, false, progressPaint);
    }
    
    public void setProgress(int progress) {
        this.progress = Math.min(progress, max);
        invalidate();
    }
    
    public int getProgress() {
        return progress;
    }
    
    public void setMax(int max) {
        this.max = max;
    }
    
    public int getMax() {
        return max;
    }
    
    public void setStrokeWidth(float width) {
        this.strokeWidth = width;
        backgroundPaint.setStrokeWidth(width);
        progressPaint.setStrokeWidth(width);
        invalidate();
    }
    
    public void setProgressColor(int color) {
        progressPaint.setColor(color);
        invalidate();
    }
    
    public void setBackgroundColor(int color) {
        backgroundPaint.setColor(color);
        invalidate();
    }
}

美!我已经为你设计了完整的两阶段固件升级流程。这个设计包含:

🏗️ 架构设计

两阶段升级流程:

  1. 下载阶段:波浪进度条(TPWaveBallProgressIndicator)- 1分钟
  2. 安装阶段:圆形进度条(自定义CircularProgressBar)- 3分钟

📱 组件说明

1. FirmwareUpdateActivity - 主控制器

  • 继承BaseMvvmActivity
  • 管理两个Fragment的切换
  • 共享Toolbar和描述文本

2. FirmwareDownloadFragment - 下载阶段

  • 波浪进度条动画(1分钟)
  • 完成后自动切换到安装阶段
  • 使用回调接口通知Activity

3. FirmwareInstallFragment - 安装阶段

  • 圆形进度条 + 倒计时(3分钟)
  • 显示"Remaining Time"和"Installing..."
  • 完成后可返回或显示成功提示

4. CircularProgressBar - 自定义控件

  • 青色(#00BCD4)进度环
  • 支持动画和进度设置
  • 圆角线条效果

🔄 工作流程

用户点击升级按钮 
    ↓
FirmwareUpdateActivity启动
    ↓  
显示FirmwareDownloadFragment (1分钟波浪进度)
    ↓
下载完成,切换到FirmwareInstallFragment (3分钟圆形进度)
    ↓
安装完成,返回或显示完成页面

📋 使用步骤

  1. 将所有Java文件放入对应的包中
  2. 将布局文件放入res/layout
  3. CircularProgressBar.java放入widget
  4. 在AndroidManifest.xml中注册FirmwareUpdateActivity
  5. 确保你的项目包含TPWaveBallProgressIndicator组件

✨ 特色功能

  • 无缝切换:两个阶段自动衔接,用户体验流畅
  • 共享布局:复用Toolbar和描述文本,节省资源
  • 进度同步:倒计时与进度条完美同步
  • 生命周期管理:正确处理Fragment切换和资源清理
  • 回调机制:阶段完成时自动触发下一步

现在你只需要在viewBinding.btnUpdateAll的点击事件中启动FirmwareUpdateActivity就可以实现完整的升级流程了!