<think>在uni-app中,由于跨平台兼容性考虑,避免使用`requestAnimationFrame`,我们可以使用`setTimeout`或`setInterval`来模拟动画效果。但是更推荐使用uni-app自带的动画API或者CSS动画。不过,对于Canvas绘图,我们通常使用定时器来实现动画。由于我们需要在Canvas上绘制环形进度条,并且希望有动画效果(从0增长到目标进度),我们可以使用`setTimeout`递归或者`setInterval`来实现逐步绘制。考虑到性能,我们可以使用`setTimeout`递归,因为这样我们可以控制每一步的时间间隔,并且在动画结束时清除定时器。以下是一个使用`setTimeout`实现动画的环形进度条组件:组件思路:1.使用Canvas绘制一个背景圆环(灰色)。2.根据当前进度值,绘制一个彩色圆环(覆盖在背景圆环上)。3.使用动画逐步增加当前绘制的进度,直到达到目标进度。注意:在uni-app中,我们需要使用`uni.createCanvasContext`来创建绘图上下文,并且使用`rpx`单位,但Canvas绘制时使用像素(px),因此需要将rpx转换为px。我们将创建一个组件,它接收`progress`(目标进度)、`color`(进度条颜色)、`bgColor`(背景颜色)、`size`(尺寸,单位rpx)、`strokeWidth`(圆环宽度,单位rpx)等属性。具体实现步骤:-在模板中放置一个canvas元素,设置其宽高(通过style设置,单位rpx,同时需要设置canvas-id)。-在mounted时初始化Canvas,绘制背景圆环,并开始动画。-使用一个定时器,每隔一段时间(如16ms,接近60帧)更新当前进度,并重绘进度圆环。-当当前进度达到目标进度时,清除定时器。另外,我们还需要在中心位置显示进度文本。下面给出完整代码:</think># Uni-App 中使用定时器实现的 Canvas 环形进度条
我将为您提供一个使用 `setTimeout` 替代 `requestAnimationFrame` 的 Canvas 环形进度条实现方案,完全兼容 Uni-App 环境。
## 完整组件代码(使用 setTimeout)
```vue
<template>
<view class="canvas-container">
<canvas
:id="canvasId"
:canvas-id="canvasId"
class="progress-canvas"
:style="{ width: size + 'rpx', height: size + 'rpx' }"
></canvas>
<view class="progress-text" :style="textStyle">
{{ displayValue }}<text v-if="showPercentSymbol">%</text>
</view>
</view>
</template>
<script>
export default {
props: {
progress: {
type: Number,
default: 0,
validator: value => value >= 0 && value <= 100
},
color: {
type: [String, Array],
default: '#2979ff'
},
bgColor: {
type: String,
default: '#ebedf0'
},
size: {
type: Number,
default: 200
},
strokeWidth: {
type: Number,
default: 12
},
showPercentSymbol: {
type: Boolean,
default: true
},
animationDuration: {
type: Number,
default: 800
},
formatter: {
type: Function,
default: null
}
},
data() {
return {
canvasId: `canvas_${Date.now()}`,
displayValue: 0,
ctx: null,
currentProgress: 0,
animationTimer: null
};
},
computed: {
textStyle() {
return {
fontSize: `${this.size * 0.2}rpx`,
color: this.textColor || '#333',
fontWeight: this.fontWeight || 'bold'
};
},
pxSize() {
return uni.upx2px(this.size);
},
pxStrokeWidth() {
return uni.upx2px(this.strokeWidth);
},
radius() {
return (this.pxSize - this.pxStrokeWidth) / 2;
},
// 计算动画帧间隔时间
frameInterval() {
// 根据动画时长自动计算帧间隔(目标60fps)
return Math.max(10, Math.floor(this.animationDuration / 60));
}
},
watch: {
progress(newVal) {
this.animateProgressChange(newVal);
}
},
mounted() {
this.initCanvas();
this.animateProgressChange(this.progress);
},
beforeDestroy() {
this.clearAnimation();
},
methods: {
// 初始化 Canvas
initCanvas() {
this.ctx = uni.createCanvasContext(this.canvasId, this);
this.drawBackground();
},
// 绘制背景圆环
drawBackground() {
this.drawArc(0, 360, this.bgColor, this.pxStrokeWidth);
},
// 绘制圆弧
drawArc(startAngle, endAngle, color, lineWidth) {
const center = this.pxSize / 2;
this.ctx.setLineWidth(lineWidth);
this.ctx.setLineCap('round');
// 处理渐变色
if (Array.isArray(color)) {
const gradient = this.ctx.createLinearGradient(0, 0, this.pxSize, this.pxSize);
color.forEach((c, i) => {
gradient.addColorStop(i / (color.length - 1), c);
});
this.ctx.setStrokeStyle(gradient);
} else {
this.ctx.setStrokeStyle(color);
}
// 绘制圆弧
this.ctx.beginPath();
this.ctx.arc(
center,
center,
this.radius,
(startAngle - 90) * Math.PI / 180,
(endAngle - 90) * Math.PI / 180
);
this.ctx.stroke();
// 绘制到画布
this.ctx.draw();
},
// 使用定时器实现动画
animateProgressChange(targetProgress) {
this.clearAnimation();
const startTime = Date.now();
const startProgress = this.currentProgress;
const animateStep = () => {
const elapsed = Date.now() - startTime;
let progress = Math.min(elapsed / this.animationDuration, 1);
// 应用缓动函数
progress = this.easeOutCubic(progress);
this.currentProgress = startProgress + (targetProgress - startProgress) * progress;
this.displayValue = this.formatter
? this.formatter(Math.round(this.currentProgress))
: Math.round(this.currentProgress);
this.redrawCanvas();
if (progress < 1) {
this.animationTimer = setTimeout(animateStep, this.frameInterval);
} else {
this.currentProgress = targetProgress;
this.displayValue = this.formatter
? this.formatter(Math.round(this.currentProgress))
: Math.round(this.currentProgress);
}
};
this.animationTimer = setTimeout(animateStep, this.frameInterval);
},
// 缓动函数
easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
},
// 重绘Canvas
redrawCanvas() {
// 清除Canvas上下文状态
this.ctx.clearRect(0, 0, this.pxSize, this.pxSize);
// 重新绘制背景
this.drawBackground();
// 计算进度圆弧的角度
const progressAngle = (this.currentProgress / 100) * 360;
// 绘制进度圆弧
if (progressAngle > 0) {
this.drawArc(0, progressAngle, this.color, this.pxStrokeWidth);
}
},
// 清除动画
clearAnimation() {
if (this.animationTimer) {
clearTimeout(this.animationTimer);
this.animationTimer = null;
}
}
}
};
</script>
<style lang="scss" scoped>
.canvas-container {
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
.progress-canvas {
// 确保Canvas元素可见
}
.progress-text {
position: absolute;
text-align: center;
pointer-events: none;
z-index: 10;
}
}
</style>
```
## 组件使用示例
```vue
<template>
<view class="container">
<canvas-progress
:progress="progressValue"
:color="['#FF9A9E', '#FAD0C4']"
bg-color="#f5f5f5"
:size="220"
:strokeWidth="16"
:animationDuration="1000"
/>
<view class="controls">
<button class="btn" @click="setProgress(0)">0%</button>
<button class="btn" @click="setProgress(25)">25%</button>
<button class="btn" @click="setProgress(50)">50%</button>
<button class="btn" @click="setProgress(75)">75%</button>
<button class="btn" @click="setProgress(100)">100%</button>
</view>
<slider
:value="progressValue"
min="0"
max="100"
@change="onSliderChange"
class="slider"
/>
</view>
</template>
<script>
import CanvasProgress from '@/components/canvas-progress.vue'
export default {
components: { CanvasProgress },
data() {
return {
progressValue: 30
}
},
methods: {
setProgress(value) {
this.progressValue = value;
},
onSliderChange(e) {
this.progressValue = e.detail.value;
}
}
}
</script>
<style scoped>
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
padding: 30rpx;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.controls {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 40rpx 0;
.btn {
margin: 10rpx;
padding: 15rpx 25rpx;
border-radius: 50rpx;
background-color: #5b86e5;
color: white;
font-size: 28rpx;
box-shadow: 0 4rpx 12rpx rgba(91, 134, 229, 0.3);
}
}
.slider {
width: 80%;
margin-top: 30rpx;
}
</style>
```
## 实现原理详解
### 1. 定时器动画系统
- 使用 `setTimeout` 替代 `requestAnimationFrame`
- 自动计算帧间隔时间 (`frameInterval`)
- 通过 `clearTimeout` 管理动画生命周期
- 在组件销毁时自动清理定时器
### 2. 动画控制逻辑
```javascript
animateProgressChange(targetProgress) {
this.clearAnimation();
const startTime = Date.now();
const startProgress = this.currentProgress;
const animateStep = () => {
// 计算时间进度
const elapsed = Date.now() - startTime;
let progress = Math.min(elapsed / this.animationDuration, 1);
// 应用缓动函数
progress = this.easeOutCubic(progress);
// 更新当前进度值
this.currentProgress = startProgress + (targetProgress - startProgress) * progress;
// 重绘Canvas
this.redrawCanvas();
// 递归调用直到完成
if (progress < 1) {
this.animationTimer = setTimeout(animateStep, this.frameInterval);
}
};
// 启动动画
this.animationTimer = setTimeout(animateStep, this.frameInterval);
}
```
### 3. 性能优化措施
1. **自动帧率调整**:根据动画时长计算最佳帧间隔
```javascript
frameInterval() {
return Math.max(10, Math.floor(this.animationDuration / 60));
}
```
2. **动画清理机制**:
```javascript
clearAnimation() {
if (this.animationTimer) {
clearTimeout(this.animationTimer);
this.animationTimer = null;
}
}
```
3. **高效重绘**:只绘制必要的元素
```javascript
redrawCanvas() {
this.ctx.clearRect(0, 0, this.pxSize, this.pxSize);
this.drawBackground();
const progressAngle = (this.currentProgress / 100) * 360;
if (progressAngle > 0) {
this.drawArc(0, progressAngle, this.color, this.pxStrokeWidth);
}
}
```
### 4. 跨平台兼容性
- 使用 `uni.createCanvasContext` 创建绘图上下文
- 通过 `uni.upx2px` 处理单位转换
- 避免使用平台特定API
- 兼容 H5、小程序和 App 平台
## 关键优势
1. **完全兼容 Uni-App**:不使用 `requestAnimationFrame`,避免兼容性问题
2. **平滑动画效果**:使用缓动函数实现流畅过渡
3. **自动性能优化**:根据动画时长调整帧率
4. **内存管理**:组件销毁时自动清理资源
5. **支持渐变效果**:线性渐变进度条
6. **响应式设计**:自适应不同屏幕尺寸
## 使用场景
- 数据仪表盘
- 文件上传/下载进度
- 任务完成度展示
- 游戏进度指示器
- 健康/健身应用