<think>我们将在UniApp Vue3中实现一个长按说话的波浪动画特效。这个特效通常用于语音录制场景,当用户长按按钮时,会出现波浪扩散动画,松开按钮时停止。
设计思路:
1. 使用CSS动画创建波浪效果(多个逐渐放大的圆环,透明度逐渐降低)
2. 利用Vue3的响应式系统管理波浪状态
3. 通过触摸事件(touchstart和touchend)和鼠标事件(mousedown和mouseup)实现长按检测
4. 在长按时连续触发波浪动画
实现步骤:
步骤1:创建波浪元素
步骤2:使用CSS动画实现波浪扩散效果
步骤3:在长按事件中动态添加波浪元素
步骤4:在松开事件中停止添加波浪元素
步骤5:自动移除旧的波浪元素
注意:在UniApp中,我们通常使用rpx单位,但为了更好的动画效果,我们使用px,并注意适配。
由于UniApp支持Vue3,我们可以使用组合式API。
下面是一个完整的实现示例:
1. 创建组件VoiceButton.vue</think># UniApp Vue3 长按语音说话的波浪动画特效实现
我将为您实现一个在UniApp Vue3中具有长按语音功能的波浪动画特效,这个实现结合了CSS动画和Vue3的组合式API,完全适配移动端。
## 设计思路
1. **波浪动画**:使用多个同心圆元素实现扩散效果
2. **长按检测**:通过touchstart/touchend事件实现移动端长按识别
3. **状态管理**:使用Vue3响应式系统控制动画状态
4. **语音反馈**:添加音量变化动画增强用户体验
## 完整实现代码
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>UniApp Vue3 长按语音波浪特效</title>
<script src="https://unpkg.com/vue@3.2.31/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
padding: 20px;
touch-action: manipulation;
}
.container {
width: 100%;
max-width: 500px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 25px;
padding: 30px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
header {
text-align: center;
margin-bottom: 30px;
}
h1 {
color: white;
font-size: 2.2rem;
margin-bottom: 10px;
text-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.subtitle {
color: rgba(255, 255, 255, 0.85);
font-size: 1.1rem;
margin: 0 auto;
max-width: 90%;
line-height: 1.6;
}
.voice-container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 300px;
margin: 20px 0 40px;
}
.voice-indicator {
position: relative;
width: 220px;
height: 220px;
display: flex;
justify-content: center;
align-items: center;
}
.voice-wave {
position: absolute;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.5);
transform: scale(0);
animation: wave-animation 2s infinite;
pointer-events: none;
}
.voice-wave:nth-child(1) { animation-delay: 0s; }
.voice-wave:nth-child(2) { animation-delay: 0.5s; }
.voice-wave:nth-child(3) { animation-delay: 1s; }
.voice-wave:nth-child(4) { animation-delay: 1.5s; }
@keyframes wave-animation {
0% {
transform: scale(0.2);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
.voice-button {
position: relative;
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(135deg, #007bff, #3a7bd5);
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 1.1rem;
font-weight: bold;
cursor: pointer;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
z-index: 10;
user-select: none;
transition: all 0.2s;
border: none;
outline: none;
}
.voice-button:active {
transform: scale(0.95);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.voice-button.recording {
background: linear-gradient(135deg, #ff416c, #ff4b2b);
animation: pulse 1s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 65, 108, 0.7); }
70% { box-shadow: 0 0 0 15px rgba(255, 65, 108, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 65, 108, 0); }
}
.mic-icon {
font-size: 2.5rem;
margin-bottom: 5px;
}
.volume-indicator {
position: absolute;
bottom: -40px;
left: 0;
width: 100%;
height: 20px;
display: flex;
justify-content: center;
gap: 3px;
}
.volume-bar {
width: 5px;
height: 10px;
background-color: rgba(255, 255, 255, 0.7);
border-radius: 2px;
transition: height 0.2s;
}
.status-message {
text-align: center;
color: white;
font-size: 1.2rem;
margin-top: 20px;
min-height: 30px;
font-weight: 500;
}
.controls {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 20px;
background: rgba(255, 255, 255, 0.1);
padding: 20px;
border-radius: 15px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.control-title {
color: white;
font-size: 1.1rem;
margin-bottom: 5px;
}
.slider-container {
display: flex;
align-items: center;
gap: 15px;
}
.slider-label {
color: white;
min-width: 100px;
}
input[type="range"] {
flex: 1;
height: 8px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.2);
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.code-section {
background: rgba(0, 0, 0, 0.2);
border-radius: 15px;
padding: 20px;
margin-top: 30px;
}
.code-title {
color: white;
text-align: center;
margin-bottom: 15px;
font-size: 1.3rem;
}
pre {
background: rgba(0, 0, 0, 0.4);
color: #f8f8f2;
padding: 15px;
border-radius: 10px;
overflow-x: auto;
font-size: 14px;
line-height: 1.5;
}
.explanation {
color: rgba(255, 255, 255, 0.9);
margin-top: 25px;
padding: 20px;
background: rgba(0, 0, 0, 0.2);
border-radius: 15px;
line-height: 1.7;
}
.explanation h3 {
margin-bottom: 15px;
color: white;
font-size: 1.3rem;
text-align: center;
}
.explanation ul {
padding-left: 25px;
margin: 15px 0;
}
.explanation li {
margin-bottom: 10px;
}
.highlight {
color: #ffcc00;
font-weight: 500;
}
.footer {
text-align: center;
margin-top: 30px;
color: rgba(255, 255, 255, 0.7);
font-size: 0.9rem;
}
@media (max-width: 500px) {
.container {
padding: 20px 15px;
border-radius: 20px;
}
h1 {
font-size: 1.8rem;
}
.voice-indicator {
width: 200px;
height: 200px;
}
.voice-button {
width: 100px;
height: 100px;
font-size: 1rem;
}
.mic-icon {
font-size: 2rem;
}
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<header>
<h1>UniApp Vue3 长按语音波浪特效</h1>
<p class="subtitle">长按按钮进行语音录制,松开按钮结束,体验动态波浪动画效果</p>
</header>
<div class="voice-container">
<div class="voice-indicator">
<!-- 波浪动画 -->
<div
v-for="(wave, index) in waves"
:key="index"
class="voice-wave"
:style="{
width: wave.size + 'px',
height: wave.size + 'px',
borderColor: wave.color
}"
></div>
<!-- 语音按钮 -->
<button
class="voice-button"
:class="{ recording: isRecording }"
@touchstart="startRecording"
@touchend="stopRecording"
@mousedown="startRecording"
@mouseup="stopRecording"
@mouseleave="stopRecording"
>
<div class="mic-icon">🎤</div>
<div>{{ isRecording ? '松开结束' : '长按说话' }}</div>
</button>
<!-- 音量指示器 -->
<div class="volume-indicator">
<div
v-for="(bar, index) in volumeBars"
:key="index"
class="volume-bar"
:style="{ height: bar.height + 'px' }"
></div>
</div>
</div>
</div>
<div class="status-message">
{{ statusMessage }}
</div>
<div class="controls">
<div class="control-group">
<div class="control-title">波浪大小</div>
<div class="slider-container">
<span class="slider-label">小 ↔ 大</span>
<input type="range" min="150" max="300" v-model.number="waveSize">
</div>
</div>
<div class="control-group">
<div class="control-title">波浪速度</div>
<div class="slider-container">
<span class="slider-label">慢 ↔ 快</span>
<input type="range" min="1" max="5" step="0.5" v-model.number="waveSpeed">
</div>
</div>
<div class="control-group">
<div class="control-title">波浪颜色</div>
<div class="slider-container">
<span class="slider-label">蓝 ↔ 红</span>
<input type="range" min="0" max="100" v-model.number="waveColorValue">
</div>
</div>
</div>
<div class="code-section">
<h3 class="code-title">核心实现代码</h3>
<pre><code>// VoiceButton.vue 组件
<template>
<div class="voice-indicator">
<div
v-for="(wave, index) in waves"
:key="index"
class="voice-wave"
:style="waveStyle(wave)"
></div>
<button
class="voice-button"
:class="{ recording: isRecording }"
@touchstart="startRecording"
@touchend="stopRecording"
>
{{ isRecording ? '松开结束' : '长按说话' }}
</button>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
const props = defineProps({
waveSize: { type: Number, default: 220 },
waveSpeed: { type: Number, default: 2 },
waveColor: { type: String, default: 'rgba(255,255,255,0.5)' }
});
const isRecording = ref(false);
const waves = ref([]);
let waveInterval = null;
let volumeInterval = null;
const waveStyle = (wave) => ({
width: `${wave.size}px`,
height: `${wave.size}px`,
borderColor: wave.color,
animationDuration: `${2 / props.waveSpeed}s`
});
function startRecording(event) {
if (event.cancelable) event.preventDefault();
isRecording.value = true;
waves.value = [];
// 开始生成波浪
waveInterval = setInterval(() => {
waves.value.push({
size: props.waveSize,
color: props.waveColor
});
// 限制波浪数量
if (waves.value.length > 4) {
waves.value.shift();
}
}, 500 / props.waveSpeed);
// 开始模拟音量变化
simulateVolume();
}
function stopRecording() {
isRecording.value = false;
clearInterval(waveInterval);
clearInterval(volumeInterval);
}
function simulateVolume() {
// 模拟音量变化逻辑
// ...
}
</script>
<style scoped>
/* 波浪动画样式 */
@keyframes wave-animation {
0% { transform: scale(0.2); opacity: 1; }
100% { transform: scale(1); opacity: 0; }
}
.voice-wave {
position: absolute;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.5);
transform: scale(0);
animation: wave-animation 2s infinite;
pointer-events: none;
}
</style></code></pre>
</div>
<div class="explanation">
<h3>实现原理</h3>
<ul>
<li>使用<span class="highlight">CSS关键帧动画</span>创建波浪扩散效果</li>
<li>通过<span class="highlight">@touchstart/@touchend</span>事件处理移动端长按操作</li>
<li>长按时使用<span class="highlight">setInterval</span>连续生成波浪元素</li>
<li>每个波浪元素使用绝对定位,实现同心圆扩散效果</li>
<li>通过<span class="highlight">transform: scale</span>和<span class="highlight">opacity</span>变化实现视觉扩散</li>
<li>使用<span class="highlight">Vue3响应式系统</span>管理波浪状态</li>
<li>添加<span class="highlight">音量指示器</span>增强用户反馈</li>
</ul>
</div>
<div class="footer">
<p>UniApp Vue3 语音波浪特效实现 | 完全适配移动端 | 长按按钮体验效果</p>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, watch } = Vue;
createApp({
setup() {
const isRecording = ref(false);
const waves = ref([]);
const statusMessage = ref('长按下方按钮开始说话');
const waveSize = ref(220);
const waveSpeed = ref(2);
const waveColorValue = ref(50);
const volumeBars = ref(Array(10).fill().map((_, i) => ({ height: 5 })));
let waveInterval = null;
let volumeInterval = null;
// 计算波浪颜色
const waveColor = computed(() => {
const blue = Math.min(255, 255 - waveColorValue.value * 1.5);
const red = Math.min(255, waveColorValue.value * 2.5);
return `rgba(${red}, ${blue}, 200, 0.5)`;
});
// 监听波浪大小变化
watch(waveSize, () => {
waves.value = [];
});
// 开始录音
function startRecording(event) {
if (event.cancelable) event.preventDefault();
isRecording.value = true;
statusMessage.value = '正在录音...';
waves.value = [];
// 开始生成波浪
waveInterval = setInterval(() => {
waves.value.push({
size: waveSize.value,
color: waveColor.value
});
// 限制波浪数量
if (waves.value.length > 4) {
waves.value.shift();
}
}, 500 / waveSpeed.value);
// 开始模拟音量变化
simulateVolume();
}
// 停止录音
function stopRecording() {
if (!isRecording.value) return;
isRecording.value = false;
clearInterval(waveInterval);
clearInterval(volumeInterval);
statusMessage.value = '录音已结束';
// 清空波浪
setTimeout(() => {
waves.value = [];
resetVolumeBars();
}, 1000);
}
// 模拟音量变化
function simulateVolume() {
resetVolumeBars();
let direction = 1;
let currentBar = 0;
volumeInterval = setInterval(() => {
// 随机改变音量条高度
volumeBars.value.forEach((bar, index) => {
if (Math.random() > 0.7) {
bar.height = Math.min(20, Math.max(5, bar.height + (Math.random() * 6 - 3)));
}
});
// 随机改变当前活跃的音量条
volumeBars.value[currentBar].height = 15 + Math.random() * 5;
currentBar += direction;
if (currentBar >= volumeBars.value.length - 1 || currentBar <= 0) {
direction *= -1;
}
}, 200);
}
// 重置音量条
function resetVolumeBars() {
volumeBars.value = volumeBars.value.map(() => ({ height: 5 }));
}
return {
isRecording,
waves,
statusMessage,
waveSize,
waveSpeed,
waveColorValue,
waveColor,
volumeBars,
startRecording,
stopRecording
};
}
}).mount('#app');
</script>
</body>
</html>
```
## 实现原理说明
1. **波浪动画实现**:
- 使用多个绝对定位的圆形元素作为波浪
- 通过CSS关键帧动画实现缩放和透明度变化
- 每个波浪元素设置不同的动画延迟,形成连续效果
2. **长按检测机制**:
- 使用`@touchstart`和`@touchend`事件处理移动端长按操作
- 长按时启动定时器连续生成波浪元素
- 松开时清除定时器并停止动画
3. **音量反馈效果**:
- 动态改变音量条高度模拟声音波动
- 随机算法创建自然的音量变化效果
- 视觉反馈增强用户体验
4. **响应式设计**:
- 使用Vue3的computed属性动态计算波浪颜色
- 通过watch监听器响应配置变化
- 完全适配移动端触摸操作
## 在UniApp中的使用建议
1. 将波浪特效封装为单独组件(如`VoiceButton.vue`)
2. 在页面中引入组件:
```html
<template>
<view class="container">
<voice-button
:wave-size="waveSize"
:wave-speed="waveSpeed"
@record-start="handleRecordStart"
@record-end="handleRecordEnd"
/>
</view>
</template>
```
3. 添加实际录音功能:
```javascript
// 使用uni.getRecorderManager()实现录音功能
const recorderManager = uni.getRecorderManager();
function startRecording() {
recorderManager.start({
duration: 60000, // 最长60秒
sampleRate: 44100,
format: 'mp3'
});
}
function stopRecording() {
recorderManager.stop();
}
```
## 相关问题
1. 如何在UniApp中实现真正的语音录制功能?
UniApp提供了`uni.getRecorderManager()` API来实现语音录制功能,可以结合波浪动画提供视觉反馈[^1]。
2. 如何优化波浪动画的性能?
可以限制同时显示的波浪数量,使用CSS硬件加速(transform和opacity),并避免频繁的DOM操作[^2]。
3. 如何自定义波浪动画的颜色和速度?
通过组件props传递配置参数,在计算属性和样式中使用这些参数实现自定义效果[^3]。
4. 如何添加语音识别功能?
可以结合UniApp的`uni.uploadFile`将录音文件上传到服务器,或使用第三方语音识别API实现转文字功能[^4]。
[^1]: UniApp官方文档 - 录音管理
[^2]: CSS动画性能优化指南
[^3]: Vue3组件Props使用文档
[^4]: 语音识别API集成方案