实现的功能:
- FirmwareUpdateActivity - 固件升级界面Activity,按照你的BaseMvvmActivity格式编写
- CircularProgressBar - 自定义圆形进度条控件,实现了你图片中的圆环效果
- activity_firmware_update.xml - 布局文件,包含圆形进度条和倒计时
- HomePageActivity更新 - 添加了升级按钮的点击事件处理
使用步骤:
- 将
FirmwareUpdateActivity.java放入你的ui.firmware包中 - 将
CircularProgressBar.java放入你的widget包中 - 将
activity_firmware_update.xml放入res/layout文件夹 - 在
HomePageActivity的subscribeViewModel方法中添加按钮点击事件 - 在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);
});
// 其他初始化代码...
}
}