固件升级界面

102 阅读12分钟

实现的功能:

  1. FirmwareUpdateActivity - 固件升级界面Activity,按照你的BaseMvvmActivity格式编写
  2. CircularProgressBar - 自定义圆形进度条控件,实现了你图片中的圆环效果
  3. activity_firmware_update.xml - 布局文件,包含圆形进度条和倒计时
  4. HomePageActivity更新 - 添加了升级按钮的点击事件处理

使用步骤:

  1. FirmwareUpdateActivity.java放入你的ui.firmware包中
  2. CircularProgressBar.java放入你的widget包中
  3. activity_firmware_update.xml放入res/layout文件夹
  4. HomePageActivitysubscribeViewModel方法中添加按钮点击事件
  5. 在AndroidManifest.xml中注册FirmwareUpdateActivity

特性:

  • 圆形进度条 - 平滑动画,从0填充到100%
  • 倒计时显示 - 实时更新剩余时间(2分32秒)
  • 动画同步 - 进度条和倒计时同步进行
  • 升级完成处理 - 完成后启用返回按钮
  • 青色主题 - 使用#00BCD4作为进度条颜色
package com.example.customview;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;

public class WaveBallProgressIndicator extends View {
    
    // 默认尺寸和颜色
    private static final int DEFAULT_SIZE = 200;
    private static final int DEFAULT_WAVE_COLOR = Color.parseColor("#00BCD4");
    private static final int DEFAULT_PROGRESS_COLOR = Color.parseColor("#009688");
    private static final int DEFAULT_BORDER_COLOR = Color.parseColor("#E0E0E0");
    private static final int DEFAULT_TEXT_COLOR = Color.parseColor("#333333");
    private static final int DEFAULT_BORDER_WIDTH = 4;
    
    // 画笔
    private Paint wavePaint;
    private Paint borderPaint;
    private Paint progressPaint;
    private Paint textPaint;
    private Paint backgroundPaint;
    
    // 路径
    private Path wavePath;
    
    // 尺寸和位置
    private float centerX, centerY;
    private float radius;
    private RectF circleRect;
    
    // 进度相关
    private float progress = 0f; // 0-100
    private float waveOffset = 0f;
    private ValueAnimator waveAnimator;
    private ValueAnimator progressAnimator;
    
    // 波浪参数
    private float waveHeight = 10f;
    private float waveLength = 100f;
    private int waveSpeed = 1000; // 动画周期ms
    
    // 小圆点动画
    private float dotAngle = 0f;
    private ValueAnimator dotAnimator;
    private float dotRadius = 6f;
    
    // 属性
    private int waveColor = DEFAULT_WAVE_COLOR;
    private int progressColor = DEFAULT_PROGRESS_COLOR;
    private int borderColor = DEFAULT_BORDER_COLOR;
    private int textColor = DEFAULT_TEXT_COLOR;
    private float borderWidth = DEFAULT_BORDER_WIDTH;
    private boolean showPercentageText = true;
    private boolean showDot = true;
    
    public WaveBallProgressIndicator(Context context) {
        super(context);
        init(context, null);
    }
    
    public WaveBallProgressIndicator(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }
    
    public WaveBallProgressIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }
    
    private void init(Context context, AttributeSet attrs) {
        // 如果有属性文件,可以在这里读取自定义属性
        // TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WaveBallProgressIndicator);
        // 这里暂时使用默认值
        
        setupPaints();
        setupAnimations();
        
        wavePath = new Path();
        circleRect = new RectF();
    }
    
    private void setupPaints() {
        // 波浪画笔
        wavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        wavePaint.setColor(waveColor);
        wavePaint.setStyle(Paint.Style.FILL);
        
        // 边框画笔
        borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        borderPaint.setColor(borderColor);
        borderPaint.setStyle(Paint.Style.STROKE);
        borderPaint.setStrokeWidth(borderWidth);
        
        // 进度画笔
        progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        progressPaint.setColor(progressColor);
        progressPaint.setStyle(Paint.Style.FILL);
        
        // 文字画笔
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(textColor);
        textPaint.setTextSize(24 * getResources().getDisplayMetrics().density);
        textPaint.setTextAlign(Paint.Align.CENTER);
        
        // 背景画笔
        backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        backgroundPaint.setColor(Color.WHITE);
        backgroundPaint.setStyle(Paint.Style.FILL);
    }
    
    private void setupAnimations() {
        // 波浪动画
        waveAnimator = ValueAnimator.ofFloat(0f, waveLength);
        waveAnimator.setDuration(waveSpeed);
        waveAnimator.setRepeatCount(ValueAnimator.INFINITE);
        waveAnimator.setInterpolator(new LinearInterpolator());
        waveAnimator.addUpdateListener(animation -> {
            waveOffset = (Float) animation.getAnimatedValue();
            invalidate();
        });
        
        // 小圆点动画
        dotAnimator = ValueAnimator.ofFloat(0f, 360f);
        dotAnimator.setDuration(2000);
        dotAnimator.setRepeatCount(ValueAnimator.INFINITE);
        dotAnimator.setInterpolator(new LinearInterpolator());
        dotAnimator.addUpdateListener(animation -> {
            dotAngle = (Float) animation.getAnimatedValue();
            invalidate();
        });
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int desiredSize = (int) (DEFAULT_SIZE * getResources().getDisplayMetrics().density);
        
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        
        int width, height;
        
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            width = Math.min(desiredSize, widthSize);
        } else {
            width = desiredSize;
        }
        
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            height = Math.min(desiredSize, heightSize);
        } else {
            height = desiredSize;
        }
        
        int size = Math.min(width, height);
        setMeasuredDimension(size, size);
    }
    
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        
        centerX = w / 2f;
        centerY = h / 2f;
        radius = Math.min(w, h) / 2f - borderWidth;
        
        circleRect.set(centerX - radius, centerY - radius, 
                      centerX + radius, centerY + radius);
        
        waveLength = radius * 2;
        waveHeight = radius * 0.1f;
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        // 绘制背景圆
        canvas.drawCircle(centerX, centerY, radius, backgroundPaint);
        
        // 绘制波浪
        drawWave(canvas);
        
        // 绘制边框
        canvas.drawCircle(centerX, centerY, radius, borderPaint);
        
        // 绘制进度文字
        if (showPercentageText) {
            drawProgressText(canvas);
        }
        
        // 绘制旋转的小圆点
        if (showDot) {
            drawRotatingDot(canvas);
        }
    }
    
    private void drawWave(Canvas canvas) {
        if (progress <= 0) return;
        
        // 计算水位高度
        float waterLevel = centerY + radius - (progress / 100f) * (radius * 2);
        
        wavePath.reset();
        
        // 创建波浪路径
        float startX = centerX - radius;
        float endX = centerX + radius;
        
        wavePath.moveTo(startX, waterLevel);
        
        // 绘制波浪曲线
        for (float x = startX; x <= endX; x += 2) {
            float relativeX = x - startX;
            float y = (float) (waterLevel + 
                waveHeight * Math.sin((relativeX + waveOffset) * 2 * Math.PI / waveLength));
            wavePath.lineTo(x, y);
        }
        
        // 封闭路径到底部
        wavePath.lineTo(endX, centerY + radius);
        wavePath.lineTo(startX, centerY + radius);
        wavePath.close();
        
        // 保存canvas状态
        canvas.save();
        
        // 裁剪为圆形
        canvas.clipPath(getCirclePath());
        
        // 绘制波浪
        canvas.drawPath(wavePath, wavePaint);
        
        // 恢复canvas状态
        canvas.restore();
    }
    
    private Path getCirclePath() {
        Path circlePath = new Path();
        circlePath.addCircle(centerX, centerY, radius, Path.Direction.CW);
        return circlePath;
    }
    
    private void drawProgressText(Canvas canvas) {
        String text = Math.round(progress) + "%";
        
        // 根据进度调整文字颜色
        if (progress > 50) {
            textPaint.setColor(Color.WHITE);
        } else {
            textPaint.setColor(textColor);
        }
        
        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        float textY = centerY - (fontMetrics.top + fontMetrics.bottom) / 2;
        
        canvas.drawText(text, centerX, textY, textPaint);
    }
    
    private void drawRotatingDot(Canvas canvas) {
        double radian = Math.toRadians(dotAngle);
        float dotX = centerX + (radius + borderWidth / 2) * (float) Math.cos(radian - Math.PI / 2);
        float dotY = centerY + (radius + borderWidth / 2) * (float) Math.sin(radian - Math.PI / 2);
        
        canvas.drawCircle(dotX, dotY, dotRadius, progressPaint);
    }
    
    // 公共方法
    public void setProgress(float progress) {
        setProgress(progress, true);
    }
    
    public void setProgress(float progress, boolean animate) {
        progress = Math.max(0, Math.min(100, progress));
        
        if (animate) {
            if (progressAnimator != null && progressAnimator.isRunning()) {
                progressAnimator.cancel();
            }
            
            progressAnimator = ValueAnimator.ofFloat(this.progress, progress);
            progressAnimator.setDuration(300);
            progressAnimator.addUpdateListener(animation -> {
                this.progress = (Float) animation.getAnimatedValue();
                invalidate();
            });
            progressAnimator.start();
        } else {
            this.progress = progress;
            invalidate();
        }
    }
    
    public float getProgress() {
        return progress;
    }
    
    public void setWaveColor(int waveColor) {
        this.waveColor = waveColor;
        wavePaint.setColor(waveColor);
        invalidate();
    }
    
    public void setBorderColor(int borderColor) {
        this.borderColor = borderColor;
        borderPaint.setColor(borderColor);
        invalidate();
    }
    
    public void setProgressColor(int progressColor) {
        this.progressColor = progressColor;
        progressPaint.setColor(progressColor);
        invalidate();
    }
    
    public void setShowPercentageText(boolean show) {
        this.showPercentageText = show;
        invalidate();
    }
    
    public void setShowDot(boolean show) {
        this.showDot = show;
        invalidate();
    }
    
    public void startAnimations() {
        if (waveAnimator != null && !waveAnimator.isRunning()) {
            waveAnimator.start();
        }
        if (dotAnimator != null && !dotAnimator.isRunning()) {
            dotAnimator.start();
        }
    }
    
    public void stopAnimations() {
        if (waveAnimator != null) {
            waveAnimator.cancel();
        }
        if (dotAnimator != null) {
            dotAnimator.cancel();
        }
        if (progressAnimator != null) {
            progressAnimator.cancel();
        }
    }
    
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        startAnimations();
    }
    
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        stopAnimations();
    }
}

package com.yourpackage.fragment;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;

public class FirmwareDownloadFragment extends BaseMvvmFragment {
    private Handler progressHandler;
    private Runnable progressRunnable;
    private int currentProgress = 0;
    private static final int UPDATE_INTERVAL = 50;
    private static final int PROGRESS_STEP = 2;
    
    private FirmwareUpdateViewModel sharedViewModel;
    
    @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) {
        // 获取Activity的SharedViewModel
        sharedViewModel = new ViewModelProvider(requireActivity()).get(FirmwareUpdateViewModel.class);
        
        // 观察下载进度
        sharedViewModel.getDownloadProgress().observe(this, progress -> {
            updateProgressUI(progress);
        });
        
        initProgressHandler();
        startProgressAnimation();
    }
    
    private void initProgressHandler() {
        progressHandler = new Handler(Looper.getMainLooper());
        progressRunnable = new Runnable() {
            @Override
            public void run() {
                if (currentProgress <= 100) {
                    // 通过ViewModel更新进度
                    sharedViewModel.updateDownloadProgress(currentProgress);
                    currentProgress += PROGRESS_STEP;
                    
                    if (currentProgress <= 100) {
                        progressHandler.postDelayed(this, UPDATE_INTERVAL);
                    } else {
                        onProgressCompleted();
                    }
                }
            }
        };
    }
    
    private void startProgressAnimation() {
        currentProgress = 0;
        progressHandler.post(progressRunnable);
    }
    
    private void updateProgressUI(int progress) {
        // 使用自定义的WaveProgressView
        if (viewBinding.waveProgressView != null) {
            viewBinding.waveProgressView.setProgress((float) progress);
        }
    }
    
    private void onProgressCompleted() {
        if (viewBinding.tvDownloading != null) {
            viewBinding.tvDownloading.setText("Download Completed!");
        }
        
        // 延迟500ms后通过ViewModel切换到安装阶段
        if (progressHandler != null) {
            progressHandler.postDelayed(() -> {
                sharedViewModel.completeDownload();
            }, 500);
        }
    }
    
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        // 停止动画和Handler
        if (progressHandler != null) {
            progressHandler.removeCallbacks(progressRunnable);
        }
        if (viewBinding.waveProgressView != null) {
            viewBinding.waveProgressView.stopAnimation();
        }
    }
}
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    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_menu"
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:layout_alignParentStart="true"
                android:layout_centerVertical="true"
                android:src="@drawable/ic_menu"
                android:contentDescription="@string/menu" />

            <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>

    <!-- Warning Icon -->
    <ImageView
        android:id="@+id/iv_warning"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_marginTop="40dp"
        android:src="@drawable/ic_warning_yellow"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/toolbar"
        android:contentDescription="@string/warning" />

    <!-- Update Info Text -->
    <TextView
        android:id="@+id/tv_update_info"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:layout_marginHorizontal="32dp"
        android:text="Update your Deco to manage your network"
        android:textColor="@color/black"
        android:textSize="16sp"
        android:gravity="center"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/iv_warning" />

    <!-- Deco M5 Card -->
    <com.tplink.design.card.TPConstraintCardView
        android:id="@+id/card_deco_m5"
        style="@style/Widget.TPDesign.CardView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:layout_marginHorizontal="16dp"
        app:layout_constraintTop_toBottomOf="@id/tv_update_info"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="16dp">

            <TextView
                android:id="@+id/tv_deco_m5_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Deco M5"
                android:textColor="@color/black"
                android:textSize="16sp"
                android:textStyle="bold"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <TextView
                android:id="@+id/tv_main_tag"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:background="@drawable/bg_main_tag"
                android:paddingHorizontal="8dp"
                android:paddingVertical="2dp"
                android:text="Main"
                android:textColor="@color/white"
                android:textSize="12sp"
                app:layout_constraintBottom_toBottomOf="@id/tv_deco_m5_title"
                app:layout_constraintStart_toEndOf="@id/tv_deco_m5_title"
                app:layout_constraintTop_toTopOf="@id/tv_deco_m5_title" />

            <TextView
                android:id="@+id/tv_m5_firmware_label"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="New Firmware Version:"
                android:textColor="@color/gray"
                android:textSize="14sp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/tv_deco_m5_title" />

            <TextView
                android:id="@+id/tv_m5_firmware_version"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:text="1.0.4 Build 20161226 Rel.64001"
                android:textColor="@color/black"
                android:textSize="14sp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/tv_m5_firmware_label" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </com.tplink.design.card.TPConstraintCardView>

    <!-- Deco X20 Card -->
    <com.tplink.design.card.TPConstraintCardView
        android:id="@+id/card_deco_x20"
        style="@style/Widget.TPDesign.CardView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:layout_marginHorizontal="16dp"
        app:layout_constraintTop_toBottomOf="@id/card_deco_m5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="16dp">

            <TextView
                android:id="@+id/tv_deco_x20_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Deco X20"
                android:textColor="@color/black"
                android:textSize="16sp"
                android:textStyle="bold"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <TextView
                android:id="@+id/tv_x20_firmware_label"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="New Firmware Version:"
                android:textColor="@color/gray"
                android:textSize="14sp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/tv_deco_x20_title" />

            <TextView
                android:id="@+id/tv_x20_firmware_version"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:text="1.0.4 Build 20161226 Rel.64001"
                android:textColor="@color/black"
                android:textSize="14sp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/tv_x20_firmware_label" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </com.tplink.design.card.TPConstraintCardView>

    <!-- Update All Button -->
    <Button
        android:id="@+id/btn_update_all"
        android:layout_width="match_parent"
        android:layout_height="56dp"
        android:layout_marginHorizontal="16dp"
        android:layout_marginBottom="32dp"
        android:background="@drawable/bg_button_primary"
        android:text="Update All"
        android:textColor="@color/white"
        android:textSize="16sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    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_menu"
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:layout_alignParentStart="true"
                android:layout_centerVertical="true"
                android:src="@drawable/ic_menu"
                android:contentDescription="@string/menu" />

            <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>

    <!-- Download Info Text -->
    <TextView
        android:id="@+id/tv_download_info"
        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 Deco system, not this app. No mobile data or storage on this device is required."
        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" />

    <!-- 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_toBottomOf="@id/tv_download_info"
        app:waveProgress="25" />

    <!-- Progress Percentage -->
    <TextView
        android:id="@+id/tv_progress_percentage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="25%"
        android:textColor="@color/cyan"
        android:textSize="24sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/progress_wv" />

    <!-- Downloading Text -->
    <TextView
        android:id="@+id/tv_downloading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        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/tv_progress_percentage" />

</androidx.constraintlayout.widget.ConstraintLayout>

FirmwareUpdateActivity

package com.yourpackage.ui.firmware;

import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.animation.LinearInterpolator;
import androidx.annotation.Nullable;
import com.yourpackage.base.BaseMvvmActivity;
import com.yourpackage.databinding.ActivityFirmwareUpdateBinding;
import com.yourpackage.widget.CircularProgressBar;

public class FirmwareUpdateActivity extends BaseMvvmActivity<ActivityFirmwareUpdateBinding> {
    
    private CountDownTimer countDownTimer;
    private static final long TOTAL_TIME_MILLIS = 152000; // 2分32秒 = 152秒
    
    @Nullable
    @Override
    protected ActivityFirmwareUpdateBinding bindContentView(@Nullable Bundle bundle) {
        return ActivityFirmwareUpdateBinding.inflate(getLayoutInflater());
    }
    
    @Override
    protected void subscribeViewModel(@Nullable Bundle bundle) {
        initView();
        startFirmwareUpdate();
    }
    
    private void initView() {
        // 设置返回按钮(升级过程中禁用)
        viewBinding.ivBack.setEnabled(false);
        viewBinding.ivBack.setAlpha(0.5f);
        
        // 设置进度条初始状态
        viewBinding.circularProgressBar.setMax(100);
        viewBinding.circularProgressBar.setProgress(0);
        viewBinding.circularProgressBar.setStrokeWidth(20); // 设置圆环宽度
        
        // 设置初始文本
        viewBinding.tvRemainingTime.setText("00:02:32");
        viewBinding.tvStatus.setText("Installing new firmware...");
        viewBinding.tvDescription.setText("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");
    }
    
    private void startFirmwareUpdate() {
        // 启动进度条动画
        ObjectAnimator progressAnimator = ObjectAnimator.ofInt(
            viewBinding.circularProgressBar, 
            "progress", 
            0, 
            100
        );
        progressAnimator.setDuration(TOTAL_TIME_MILLIS);
        progressAnimator.setInterpolator(new LinearInterpolator());
        progressAnimator.addUpdateListener(animation -> {
            int value = (int) animation.getAnimatedValue();
            viewBinding.circularProgressBar.setProgress(value);
        });
        progressAnimator.start();
        
        // 启动倒计时
        countDownTimer = new CountDownTimer(TOTAL_TIME_MILLIS, 1000) {
            @Override
            public void onTick(long millisUntilFinished) {
                // 更新倒计时显示
                updateRemainingTime(millisUntilFinished);
            }
            
            @Override
            public void onFinish() {
                // 升级完成
                viewBinding.tvRemainingTime.setText("00:00:00");
                viewBinding.tvStatus.setText("Firmware update completed!");
                onUpdateComplete();
            }
        };
        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);
        viewBinding.tvRemainingTime.setText(timeFormatted);
    }
    
    private void onUpdateComplete() {
        // 升级完成后的处理
        viewBinding.ivBack.setEnabled(true);
        viewBinding.ivBack.setAlpha(1.0f);
        viewBinding.ivBack.setOnClickListener(v -> finish());
        
        // 可以自动返回或显示完成提示
        // finish();
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (countDownTimer != null) {
            countDownTimer.cancel();
        }
    }
}

CircularProgressBar

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 / 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();
    }
}

activity_firmware_update.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
    android:background="@android:color/white">

    <!-- 顶部导航栏 -->
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="56dp"
        android:background="@android:color/white">

        <ImageView
            android:id="@+id/iv_back"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:layout_centerVertical="true"
            android:layout_marginStart="16dp"
            android:src="@drawable/ic_menu"
            android:contentDescription="Menu" />

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

        <ImageView
            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="Add" />

    </RelativeLayout>

    <!-- 分割线 -->
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#E0E0E0" />

    <!-- 主要内容 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center_horizontal"
        android:padding="24dp">

        <!-- 描述文字 -->
        <TextView
            android:id="@+id/tv_description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            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="#666666"
            android:textSize="14sp"
            android:lineSpacing="4dp"
            android:layout_marginBottom="60dp" />

        <!-- 圆形进度条容器 -->
        <RelativeLayout
            android:layout_width="280dp"
            android:layout_height="280dp"
            android:layout_marginBottom="40dp">

            <!-- 自定义圆形进度条 -->
            <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:02:32"
                    android:textColor="@android: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:text="Installing new firmware..."
            android:textColor="@android:color/black"
            android:textSize="18sp" />

    </LinearLayout>

</LinearLayout>

circular_progress_drawable.xml

<!-- drawable/circular_progress_drawable.xml -->
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromDegrees="270"
    android:toDegrees="270">
    <shape
        android:innerRadiusRatio="2.5"
        android:shape="ring"
        android:thickness="8dp"
        android:useLevel="true">
        <gradient
            android:angle="0"
            android:endColor="#00BCD4"
            android:startColor="#00BCD4"
            android:type="sweep"
            android:useLevel="false" />
    </shape>
</rotate>

<!-- drawable/circular_progress_background.xml -->
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:innerRadiusRatio="2.5"
    android:shape="ring"
    android:thickness="8dp"
    android:useLevel="false">
    <solid android:color="#E0E0E0" />
</shape>

HomePageActivity

package com.yourpackage.ui.home;

import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.yourpackage.base.BaseMvvmActivity;
import com.yourpackage.databinding.ActivityHomePageBinding;
import com.yourpackage.ui.firmware.FirmwareUpdateActivity;

public class HomePageActivity extends BaseMvvmActivity<ActivityHomePageBinding> {
 
    @Nullable
    @Override
    protected ActivityHomePageBinding bindContentView(@Nullable Bundle bundle) {
        return ActivityHomePageBinding.inflate(getLayoutInflater());
    }
 
    @Override
    protected void subscribeViewModel(@Nullable Bundle bundle) {
        initViews();
    }
    
    private void initViews() {
        // 设置升级按钮点击事件
        viewBinding.btnUpdate.setOnClickListener(v -> {
            // 跳转到固件升级界面
            Intent intent = new Intent(HomePageActivity.this, FirmwareUpdateActivity.class);
            startActivity(intent);
        });
        
        // 其他初始化代码...
    }
}