pin_drop当前位置:知识文库 ❯ 图文
HTML5 API:Vibration API - 完整教程与代码示例
一、教程简介
Vibration API 允许 Web 应用在支持的设备上触发振动效果,为用户提供触觉反馈。这是一个简单但实用的 API,特别适合移动端应用场景。通过振动反馈,可以增强用户交互体验,例如在按钮点击、消息通知、游戏事件等场景中提供触觉提示。
Vibration API 的接口非常简洁,仅通过 navigator.vibrate() 方法即可实现振动控制。它支持单次振动、振动模式(交替振动和暂停),以及取消振动等操作。
二、核心概念
1. 基本振动
代码示例
// 单次振动,参数为振动时长(毫秒)
navigator.vibrate(200); // 振动 200 毫秒
// 振动时长为 0 或负数时不会振动
navigator.vibrate(0); // 不振动
navigator.vibrate(-100); // 不振动2. 振动模式
代码示例
// 振动模式:交替的 [振动时长, 暂停时长, 振动时长, ...]
navigator.vibrate([200, 100, 200]); // 振动200ms,暂停100ms,振动200ms
// 复杂的振动模式
navigator.vibrate([100, 50, 100, 50, 300]); // 短振-暂停-短振-暂停-长振
// 模拟摩斯密码 "SOS"(... --- ...)
navigator.vibrate([
100, 50, 100, 50, 100, // S: 三个短振
200, 50, 200, 50, 200, // O: 三个长振
100, 50, 100, 50, 100 // S: 三个短振
]);3. 取消振动
代码示例
// 传入空数组或 0 来取消正在进行的振动
navigator.vibrate(0);
navigator.vibrate([]);4. 返回值
代码示例
// vibrate() 返回 boolean 值
// true: 振动请求成功
// false: 振动请求失败(设备不支持或参数无效)
const result = navigator.vibrate(200);
console.log(result); // true 或 false三、语法与用法
基本语法
代码示例
// 单次振动
navigator.vibrate(duration);
// 振动模式
navigator.vibrate(pattern);
// 取消振动
navigator.vibrate(0);
navigator.vibrate([]);参数说明
兼容性检测
代码示例
// 检测 Vibration API 支持
if ('vibrate' in navigator) {
navigator.vibrate(200);
} else {
console.log('当前设备不支持振动');
}
// 更严格的检测
function isVibrationSupported() {
return 'vibrate' in navigator && typeof navigator.vibrate === 'function';
}四、代码示例
示例1:振动反馈按钮
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vibration API - 振动反馈按钮</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #2d3436, #636e72);
color: #dfe6e9;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
h1 { color: #fd79a8; margin-bottom: 10px; }
.subtitle { color: #b2bec3; margin-bottom: 30px; font-size: 14px; }
.support-badge {
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
margin-bottom: 30px;
}
.support-badge.supported { background: rgba(0,184,148,0.2); color: #00b894; }
.support-badge.unsupported { background: rgba(214,48,49,0.2); color: #d63031; }
.buttons-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
width: 360px;
max-width: 100%;
}
.vibrate-btn {
padding: 20px;
border-radius: 15px;
border: 2px solid rgba(255,255,255,0.15);
background: rgba(255,255,255,0.05);
color: #dfe6e9;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.3s;
text-align: center;
}
.vibrate-btn:hover {
background: rgba(255,255,255,0.1);
transform: scale(1.02);
}
.vibrate-btn:active { transform: scale(0.98); }
.vibrate-btn .icon { font-size: 28px; display: block; margin-bottom: 8px; }
.vibrate-btn .label { font-size: 13px; }
.vibrate-btn .duration { font-size: 11px; color: #636e72; margin-top: 4px; }
.vibrate-btn.short { border-color: rgba(0,184,148,0.4); }
.vibrate-btn.short:hover { background: rgba(0,184,148,0.15); }
.vibrate-btn.medium { border-color: rgba(253,203,110,0.4); }
.vibrate-btn.medium:hover { background: rgba(253,203,110,0.15); }
.vibrate-btn.long { border-color: rgba(225,112,85,0.4); }
.vibrate-btn.long:hover { background: rgba(225,112,85,0.15); }
.vibrate-btn.pattern { border-color: rgba(162,155,254,0.4); }
.vibrate-btn.pattern:hover { background: rgba(162,155,254,0.15); }
.vibrate-btn.sos { border-color: rgba(255,107,107,0.4); }
.vibrate-btn.sos:hover { background: rgba(255,107,107,0.15); }
.vibrate-btn.stop { border-color: rgba(99,110,114,0.4); }
.vibrate-btn.stop:hover { background: rgba(99,110,114,0.15); }
.custom-section { margin-top: 30px; width: 360px; max-width: 100%; }
.custom-section h3 { color: #fd79a8; margin-bottom: 15px; }
.input-group { display: flex; gap: 10px; margin-bottom: 15px; }
.input-group input {
flex: 1; padding: 10px 15px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 10px; color: #dfe6e9; font-size: 14px; outline: none;
}
.input-group input:focus { border-color: #fd79a8; }
.input-group button {
padding: 10px 20px; background: #fd79a8; color: white;
border: none; border-radius: 10px; cursor: pointer;
font-weight: bold; transition: all 0.3s;
}
.input-group button:hover { background: #e84393; }
.pattern-input {
width: 100%; padding: 10px 15px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 10px; color: #dfe6e9;
font-size: 14px; outline: none; margin-bottom: 10px;
}
.pattern-input:focus { border-color: #fd79a8; }
.hint { font-size: 12px; color: #636e72; }
</style>
</head>
<body>
<h1>Vibration API</h1>
<p class="subtitle">触觉反馈体验</p>
<div class="support-badge" id="supportBadge">检测中...</div>
<div class="buttons-grid">
<button class="vibrate-btn short" data-vibrate="50">
<span class="icon">☞</span> 轻触
<div class="duration">50ms</div>
</button>
<button class="vibrate-btn medium" data-vibrate="200">
<span class="icon">✋</span> 中等
<div class="duration">200ms</div>
</button>
<button class="vibrate-btn long" data-vibrate="500">
<span class="icon">✌</span> 长振
<div class="duration">500ms</div>
</button>
<button class="vibrate-btn pattern" data-vibrate="[100,50,100,50,300]">
<span class="icon">⚠</span> 节奏
<div class="duration">100-50-100-50-300</div>
</button>
<button class="vibrate-btn sos" data-vibrate="[100,50,100,50,100,200,50,200,50,200,100,50,100,50,100]">
<span class="icon">⚠</span> SOS
<div class="duration">摩斯密码 SOS</div>
</button>
<button class="vibrate-btn stop" data-vibrate="0">
<span class="icon">■</span> 停止
<div class="duration">取消振动</div>
</button>
</div>
<div class="custom-section">
<h3>自定义振动</h3>
<div class="input-group">
<input type="number" id="customDuration" placeholder="振动时长 (ms)" min="1" max="10000" value="300">
<button id="customBtn">振动</button>
</div>
<input type="text" class="pattern-input" id="customPattern" placeholder="振动模式,如:200,100,200,100,400">
<p class="hint">振动模式格式:用逗号分隔振动和暂停时长</p>
</div>
<script>
const supportBadge = document.getElementById('supportBadge');
if ('vibrate' in navigator) {
supportBadge.textContent = '当前设备支持振动';
supportBadge.className = 'support-badge supported';
} else {
supportBadge.textContent = '当前设备不支持振动';
supportBadge.className = 'support-badge unsupported';
}
document.querySelectorAll('.vibrate-btn').forEach(btn => {
btn.addEventListener('click', () => {
const value = btn.dataset.vibrate;
if (value.startsWith('[')) {
const pattern = JSON.parse(value);
navigator.vibrate(pattern);
} else {
navigator.vibrate(parseInt(value));
}
});
});
document.getElementById('customBtn').addEventListener('click', () => {
const duration = parseInt(document.getElementById('customDuration').value);
if (duration > 0) { navigator.vibrate(duration); }
});
document.getElementById('customPattern').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const patternStr = e.target.value.trim();
if (!patternStr) return;
const pattern = patternStr.split(',').map(Number).filter(n => !isNaN(n));
if (pattern.length > 0) { navigator.vibrate(pattern); }
}
});
</script>
</body>
</html>示例2:通知振动模式
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vibration API - 通知振动模式</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', sans-serif;
background: #1e272e; color: #d2dae2;
min-height: 100vh; padding: 20px;
}
.container { max-width: 500px; margin: 0 auto; }
h1 { color: #0fbcf9; margin-bottom: 25px; text-align: center; }
.notification-list { display: flex; flex-direction: column; gap: 12px; }
.notification-card {
display: flex; align-items: center; gap: 15px;
padding: 16px 20px; background: rgba(255,255,255,0.05);
border-radius: 12px; border: 1px solid rgba(255,255,255,0.08);
cursor: pointer; transition: all 0.3s;
}
.notification-card:hover { background: rgba(255,255,255,0.08); transform: translateX(5px); }
.notification-card:active { transform: scale(0.98); }
.notification-icon {
width: 44px; height: 44px; border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 22px; flex-shrink: 0;
}
.notification-info { flex: 1; }
.notification-info h3 { font-size: 15px; margin-bottom: 4px; }
.notification-info p { font-size: 12px; color: #808e9b; }
.notification-pattern { font-size: 11px; color: #485460; font-family: monospace; }
.card-message .notification-icon { background: rgba(0,184,148,0.2); }
.card-call .notification-icon { background: rgba(9,132,227,0.2); }
.card-alert .notification-icon { background: rgba(253,203,110,0.2); }
.card-error .notification-icon { background: rgba(214,48,49,0.2); }
.card-success .notification-icon { background: rgba(0,206,201,0.2); }
.card-reminder .notification-icon { background: rgba(162,155,254,0.2); }
.test-section { margin-top: 30px; padding: 20px; background: rgba(255,255,255,0.03); border-radius: 15px; }
.test-section h3 { color: #0fbcf9; margin-bottom: 15px; }
.test-controls { display: flex; gap: 10px; margin-bottom: 15px; }
.test-btn {
padding: 8px 16px; border-radius: 20px;
border: 1px solid rgba(255,255,255,0.2);
background: rgba(255,255,255,0.05); color: #d2dae2;
cursor: pointer; font-size: 13px; transition: all 0.3s;
}
.test-btn:hover { background: rgba(255,255,255,0.1); }
.test-btn.active { background: rgba(15,188,249,0.2); border-color: #0fbcf9; color: #0fbcf9; }
.log-area {
background: rgba(0,0,0,0.3); border-radius: 10px; padding: 12px;
max-height: 150px; overflow-y: auto; font-family: monospace; font-size: 12px;
}
.log-entry { padding: 3px 0; color: #808e9b; }
.log-entry .time { color: #485460; }
.log-entry .event { color: #0fbcf9; }
</style>
</head>
<body>
<div class="container">
<h1>Notification Vibration</h1>
<div class="notification-list">
<div class="notification-card card-message" data-pattern="[100]">
<div class="notification-icon">✉</div>
<div class="notification-info">
<h3>新消息</h3><p>短促单次振动</p>
</div>
<div class="notification-pattern">[100]</div>
</div>
<div class="notification-card card-call" data-pattern="[200,100,200,100,200]">
<div class="notification-icon">☎</div>
<div class="notification-info">
<h3>来电提醒</h3><p>重复振动模式</p>
</div>
<div class="notification-pattern">[200,100,200,100,200]</div>
</div>
<div class="notification-card card-alert" data-pattern="[100,50,100,50,100,50,300]">
<div class="notification-icon">⚠</div>
<div class="notification-info">
<h3>警告通知</h3><p>急促振动后长振</p>
</div>
<div class="notification-pattern">[100,50,100,50,100,50,300]</div>
</div>
<div class="notification-card card-error" data-pattern="[500]">
<div class="notification-icon">❌</div>
<div class="notification-info">
<h3>错误提示</h3><p>单次长振动</p>
</div>
<div class="notification-pattern">[500]</div>
</div>
<div class="notification-card card-success" data-pattern="[50,50,50]">
<div class="notification-icon">✅</div>
<div class="notification-info">
<h3>操作成功</h3><p>轻快双振动</p>
</div>
<div class="notification-pattern">[50,50,50]</div>
</div>
<div class="notification-card card-reminder" data-pattern="[200,100,200]">
<div class="notification-icon">🔔</div>
<div class="notification-info">
<h3>日程提醒</h3><p>双振动模式</p>
</div>
<div class="notification-pattern">[200,100,200]</div>
</div>
</div>
<div class="test-section">
<h3>模拟通知测试</h3>
<div class="test-controls">
<button class="test-btn" id="startTest">开始模拟</button>
<button class="test-btn" id="stopTest">停止模拟</button>
</div>
<div class="log-area" id="logArea">
<div class="log-entry"><span class="time">[系统]</span> 准备就绪</div>
</div>
</div>
</div>
<script>
document.querySelectorAll('.notification-card').forEach(card => {
card.addEventListener('click', () => {
const pattern = JSON.parse(card.dataset.pattern);
navigator.vibrate(pattern);
addLog('触发: ' + card.querySelector('h3').textContent);
});
});
let testInterval = null;
const notifications = [
{ name: '新消息', pattern: [100] },
{ name: '来电提醒', pattern: [200, 100, 200, 100, 200] },
{ name: '警告通知', pattern: [100, 50, 100, 50, 100, 50, 300] },
{ name: '操作成功', pattern: [50, 50, 50] }
];
document.getElementById('startTest').addEventListener('click', () => {
if (testInterval) return;
document.getElementById('startTest').classList.add('active');
addLog('开始模拟通知');
let index = 0;
testInterval = setInterval(() => {
const notification = notifications[index % notifications.length];
navigator.vibrate(notification.pattern);
addLog(`收到通知: ${notification.name}`);
index++;
}, 3000);
});
document.getElementById('stopTest').addEventListener('click', () => {
clearInterval(testInterval); testInterval = null;
navigator.vibrate(0);
document.getElementById('startTest').classList.remove('active');
addLog('停止模拟');
});
function addLog(message) {
const logArea = document.getElementById('logArea');
const entry = document.createElement('div');
entry.className = 'log-entry';
const now = new Date();
const time = now.getHours().toString().padStart(2, '0') + ':' +
now.getMinutes().toString().padStart(2, '0') + ':' +
now.getSeconds().toString().padStart(2, '0');
entry.innerHTML = `<span class="time">[${time}]</span> <span class="event">${message}</span>`;
logArea.appendChild(entry);
logArea.scrollTop = logArea.scrollHeight;
}
</script>
</body>
</html>示例3:游戏振动反馈
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vibration API - 游戏振动反馈</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', sans-serif;
background: #0a0a1a; color: #e0e0e0;
min-height: 100vh; display: flex;
flex-direction: column; align-items: center;
padding: 20px; user-select: none;
}
h1 { color: #e74c3c; margin-bottom: 5px; }
.score { color: #f39c12; font-size: 24px; margin-bottom: 20px; }
.game-area {
width: 360px; height: 360px;
background: rgba(255,255,255,0.03);
border-radius: 15px; position: relative;
overflow: hidden; border: 2px solid rgba(255,255,255,0.1);
margin-bottom: 20px;
}
.target {
width: 50px; height: 50px; border-radius: 50%;
position: absolute; cursor: pointer;
transition: transform 0.1s;
display: flex; align-items: center;
justify-content: center; font-size: 24px;
}
.target:active { transform: scale(0.9); }
.target.normal { background: radial-gradient(circle, #e74c3c, #c0392b); box-shadow: 0 0 20px rgba(231,76,60,0.5); }
.target.bonus { background: radial-gradient(circle, #f1c40f, #f39c12); box-shadow: 0 0 20px rgba(241,196,15,0.5); }
.target.bomb { background: radial-gradient(circle, #636e72, #2d3436); box-shadow: 0 0 20px rgba(99,110,114,0.5); }
.hit-effect {
position: absolute; width: 60px; height: 60px;
border-radius: 50%; border: 3px solid rgba(255,255,255,0.5);
animation: hitExpand 0.5s ease-out forwards; pointer-events: none;
}
@keyframes hitExpand { 0% { transform: scale(0.5); opacity: 1; } 100% { transform: scale(2); opacity: 0; } }
.score-popup {
position: absolute; font-size: 20px; font-weight: bold;
animation: scoreFloat 0.8s ease-out forwards; pointer-events: none;
}
@keyframes scoreFloat { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-50px); opacity: 0; } }
.controls { display: flex; gap: 10px; }
.btn { padding: 10px 24px; border-radius: 25px; border: none; cursor: pointer; font-size: 14px; font-weight: bold; transition: all 0.3s; }
.btn-start { background: #e74c3c; color: white; }
.btn-stop { background: #636e72; color: white; }
.btn:hover { transform: scale(1.05); }
.game-info { margin-top: 15px; font-size: 13px; color: #636e72; text-align: center; }
.combo { color: #e74c3c; font-weight: bold; }
</style>
</head>
<body>
<h1>Tap Game</h1>
<div class="score">得分: <span id="score">0</span> | 连击: <span class="combo" id="combo">0</span></div>
<div class="game-area" id="gameArea"></div>
<div class="controls">
<button class="btn btn-start" id="startBtn">开始游戏</button>
<button class="btn btn-stop" id="stopBtn" disabled>结束游戏</button>
</div>
<div class="game-info">
点击红色目标得分 | 金色目标双倍分 | 避开炸弹<br>
振动反馈:命中=短振 | 金色=双振 | 炸弹=长振
</div>
<script>
const gameArea = document.getElementById('gameArea');
const scoreEl = document.getElementById('score');
const comboEl = document.getElementById('combo');
let score = 0, combo = 0, isPlaying = false;
let spawnInterval = null, gameTimeout = null;
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('stopBtn').addEventListener('click', stopGame);
function startGame() {
score = 0; combo = 0; updateScore();
isPlaying = true;
startBtn.disabled = true; stopBtn.disabled = false;
gameArea.innerHTML = '';
spawnTarget();
spawnInterval = setInterval(spawnTarget, 1200);
gameTimeout = setTimeout(stopGame, 30000);
}
function stopGame() {
isPlaying = false; clearInterval(spawnInterval);
clearTimeout(gameTimeout);
startBtn.disabled = false; stopBtn.disabled = true;
gameArea.innerHTML = '';
navigator.vibrate([100, 50, 100, 50, 300]);
}
function spawnTarget() {
if (!isPlaying) return;
const target = document.createElement('div');
target.className = 'target';
const rand = Math.random();
if (rand < 0.15) {
target.classList.add('bomb');
target.innerHTML = '💣';
target.dataset.type = 'bomb';
} else if (rand < 0.3) {
target.classList.add('bonus');
target.innerHTML = '⭐';
target.dataset.type = 'bonus';
} else {
target.classList.add('normal');
target.innerHTML = '🎯';
target.dataset.type = 'normal';
}
const maxX = gameArea.clientWidth - 50;
const maxY = gameArea.clientHeight - 50;
target.style.left = Math.random() * maxX + 'px';
target.style.top = Math.random() * maxY + 'px';
target.addEventListener('click', (e) => {
e.stopPropagation(); handleTargetClick(target);
});
gameArea.appendChild(target);
setTimeout(() => {
if (target.parentNode) {
target.remove();
if (target.dataset.type !== 'bomb') { combo = 0; updateScore(); }
}
}, 2500);
}
function handleTargetClick(target) {
const type = target.dataset.type;
const rect = target.getBoundingClientRect();
const gameRect = gameArea.getBoundingClientRect();
const x = rect.left - gameRect.left + 25;
const y = rect.top - gameRect.top + 25;
if (type === 'bomb') {
score = Math.max(0, score - 20); combo = 0;
navigator.vibrate(500);
} else if (type === 'bonus') {
const points = 20 * (1 + Math.floor(combo / 5));
score += points; combo++;
navigator.vibrate([50, 30, 50]);
} else {
const points = 10 * (1 + Math.floor(combo / 5));
score += points; combo++;
navigator.vibrate(50);
}
target.remove(); updateScore();
}
function updateScore() {
scoreEl.textContent = score;
comboEl.textContent = combo;
}
</script>
</body>
</html>示例4:振动模式编辑器
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vibration API - 振动模式编辑器</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; padding: 20px; }
.container { max-width: 600px; margin: 0 auto; }
h1 { color: #00cec9; text-align: center; margin-bottom: 25px; }
.editor { background: rgba(255,255,255,0.05); border-radius: 15px; padding: 20px; margin-bottom: 20px; }
.timeline { background: rgba(0,0,0,0.3); border-radius: 10px; padding: 15px; min-height: 80px; display: flex; align-items: center; gap: 3px; overflow-x: auto; margin-bottom: 15px; }
.segment { height: 40px; border-radius: 5px; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: bold; cursor: pointer; transition: all 0.2s; flex-shrink: 0; }
.segment.vibrate { background: linear-gradient(135deg, #00cec9, #0984e3); color: white; }
.segment.pause { background: rgba(255,255,255,0.1); color: #636e72; }
.segment:hover { transform: scaleY(1.1); }
.segment.selected { outline: 2px solid #fdcb6e; }
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 15px; }
.tool-btn { padding: 8px 16px; border-radius: 20px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.05); color: #dfe6e9; cursor: pointer; font-size: 13px; transition: all 0.3s; }
.tool-btn:hover { background: rgba(255,255,255,0.1); }
.tool-btn.add-vibrate { border-color: #00cec9; color: #00cec9; }
.tool-btn.add-pause { border-color: #636e72; }
.tool-btn.delete { border-color: #d63031; color: #d63031; }
.tool-btn.play { border-color: #00b894; color: #00b894; background: rgba(0,184,148,0.1); }
.tool-btn.stop { border-color: #fdcb6e; color: #fdcb6e; }
.tool-btn.clear { border-color: #636e72; }
.presets { background: rgba(255,255,255,0.05); border-radius: 15px; padding: 20px; }
.presets h3 { color: #fdcb6e; margin-bottom: 15px; }
.preset-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
.preset-item { padding: 12px; background: rgba(255,255,255,0.05); border-radius: 10px; cursor: pointer; transition: all 0.3s; border: 1px solid rgba(255,255,255,0.08); }
.preset-item:hover { background: rgba(255,255,255,0.1); transform: scale(1.02); }
.preset-item h4 { font-size: 14px; margin-bottom: 4px; }
.preset-item p { font-size: 11px; color: #636e72; }
.code-output { margin-top: 15px; padding: 12px; background: rgba(0,0,0,0.3); border-radius: 10px; font-family: monospace; font-size: 13px; color: #00cec9; word-break: break-all; }
</style>
</head>
<body>
<div class="container">
<h1>Vibration Pattern Editor</h1>
<div class="editor">
<div class="timeline" id="timeline"></div>
<div class="toolbar">
<button class="tool-btn add-vibrate" id="addVibrate">+ 振动</button>
<button class="tool-btn add-pause" id="addPause">+ 暂停</button>
<button class="tool-btn delete" id="deleteSegment">删除选中</button>
<button class="tool-btn play" id="playBtn">播放</button>
<button class="tool-btn stop" id="stopBtn">停止</button>
<button class="tool-btn clear" id="clearBtn">清空</button>
</div>
</div>
<div class="presets">
<h3>预设模式</h3>
<div class="preset-list">
<div class="preset-item" data-pattern="[100]"><h4>轻触</h4><p>[100]</p></div>
<div class="preset-item" data-pattern="[200,100,200]"><h4>双击</h4><p>[200,100,200]</p></div>
<div class="preset-item" data-pattern="[100,50,100,50,300]"><h4>节奏</h4><p>[100,50,100,50,300]</p></div>
<div class="preset-item" data-pattern="[500]"><h4>长振</h4><p>[500]</p></div>
<div class="preset-item" data-pattern="[100,50,100,50,100,200,50,200,50,200,100,50,100,50,100]"><h4>SOS</h4><p>摩斯密码 SOS</p></div>
<div class="preset-item" data-pattern="[50,30,50,30,50]"><h4>心跳</h4><p>[50,30,50,30,50]</p></div>
</div>
<div class="code-output" id="codeOutput">navigator.vibrate([])</div>
</div>
</div>
<script>
let segments = []; let selectedIndex = -1;
const timeline = document.getElementById('timeline');
const codeOutput = document.getElementById('codeOutput');
function renderTimeline() {
timeline.innerHTML = '';
segments.forEach((seg, index) => {
const el = document.createElement('div');
el.className = `segment ${seg.type}`;
el.style.width = Math.max(30, seg.duration / 5) + 'px';
el.textContent = seg.duration + 'ms';
el.dataset.index = index;
if (index === selectedIndex) el.classList.add('selected');
el.addEventListener('click', () => selectSegment(index));
timeline.appendChild(el);
});
updateCodeOutput();
}
function selectSegment(index) {
selectedIndex = index; renderTimeline();
}
document.getElementById('addVibrate').addEventListener('click', () => {
segments.push({ type: 'vibrate', duration: 200 });
selectedIndex = segments.length - 1; renderTimeline();
});
document.getElementById('addPause').addEventListener('click', () => {
segments.push({ type: 'pause', duration: 100 });
selectedIndex = segments.length - 1; renderTimeline();
});
document.getElementById('deleteSegment').addEventListener('click', () => {
if (selectedIndex >= 0) { segments.splice(selectedIndex, 1); selectedIndex = -1; renderTimeline(); }
});
document.getElementById('playBtn').addEventListener('click', () => {
const pattern = segments.map(s => s.duration);
if (pattern.length > 0) navigator.vibrate(pattern);
});
document.getElementById('stopBtn').addEventListener('click', () => { navigator.vibrate(0); });
document.getElementById('clearBtn').addEventListener('click', () => { segments = []; selectedIndex = -1; renderTimeline(); });
document.querySelectorAll('.preset-item').forEach(item => {
item.addEventListener('click', () => {
const pattern = JSON.parse(item.dataset.pattern);
segments = [];
pattern.forEach((duration, index) => {
segments.push({ type: index % 2 === 0 ? 'vibrate' : 'pause', duration });
});
selectedIndex = -1; renderTimeline();
navigator.vibrate(pattern);
});
});
function updateCodeOutput() {
const pattern = segments.map(s => s.duration);
if (pattern.length === 0) codeOutput.textContent = 'navigator.vibrate([])';
else if (pattern.length === 1) codeOutput.textContent = `navigator.vibrate(${pattern[0]})`;
else codeOutput.textContent = `navigator.vibrate([${pattern.join(', ')}])`;
}
renderTimeline();
</script>
</body>
</html>五、浏览器兼容性
兼容性注意事项
代码示例
// 1. 桌面浏览器不支持 Vibration API
// vibrate() 在桌面浏览器中会返回 false
// 2. iOS Safari 不支持 Vibration API
// 即使在 iOS 上也不会报错,只是不振动
// 3. 完整的兼容性检测和使用
function safeVibrate(pattern) {
if (!('vibrate' in navigator)) {
console.log('当前设备不支持振动');
return false;
}
// 检测是否为移动设备
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
if (!isMobile) {
console.log('桌面浏览器不支持振动');
return false;
}
try {
return navigator.vibrate(pattern);
} catch (e) {
console.error('振动失败:', e);
return false;
}
}六、注意事项与最佳实践
1. 适度使用振动
代码示例
// 最佳实践:振动反馈应适度,避免过度使用
// 好的做法:仅在关键交互时振动
function onButtonClick() {
navigator.vibrate(30); // 轻微反馈
// 执行操作...
}
function onError() {
navigator.vibrate([100, 50, 100]); // 明显的错误反馈
}
// 不好的做法:频繁振动
// setInterval(() => navigator.vibrate(50), 500); // 令人烦躁2. 尊重用户偏好
代码示例
// 提供振动开关
class VibrationManager {
constructor() {
this.enabled = true;
// 从 localStorage 读取用户偏好
const saved = localStorage.getItem('vibration_enabled');
if (saved !== null) {
this.enabled = saved === 'true';
}
}
vibrate(pattern) {
if (!this.enabled) return false;
if (!('vibrate' in navigator)) return false;
return navigator.vibrate(pattern);
}
setEnabled(enabled) {
this.enabled = enabled;
localStorage.setItem('vibration_enabled', enabled.toString());
}
toggle() {
this.setEnabled(!this.enabled);
}
}3. 振动时长限制
代码示例
// 不同浏览器对振动时长有不同的限制
// Chrome: 单次最长约 10 秒
// Firefox: 单次最长约 10 秒
// 超过限制的时长会被截断
// 最佳实践:使用合理的时长
const VIBRATION = {
LIGHT: 15, // 轻触反馈
SHORT: 50, // 短反馈
MEDIUM: 100, // 中等反馈
LONG: 200, // 长反馈
VERY_LONG: 500 // 非常长的反馈(谨慎使用)
};
// 预定义的振动模式
const VIBRATION_PATTERNS = {
SUCCESS: [50, 30, 50],
ERROR: [100, 50, 100],
NOTIFICATION: [100],
ALERT: [100, 50, 100, 50, 300],
RINGTONE: [200, 100, 200, 100, 200],
HEARTBEAT: [50, 30, 50]
};4. 页面可见性
代码示例
// 页面不可见时应停止振动
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
navigator.vibrate(0); // 页面隐藏时取消振动
}
});七、代码规范示例
完整的振动管理封装
代码示例
/**
* VibrationManager - 振动管理器
* 统一管理振动反馈,支持用户偏好、预设模式、兼容性检测
*/
class VibrationManager {
// 预定义振动模式
static PATTERNS = {
TAP: 15,
LIGHT: 30,
SHORT: 50,
MEDIUM: 100,
LONG: 200,
SUCCESS: [50, 30, 50],
ERROR: [100, 50, 100],
WARNING: [100, 50, 100, 50, 300],
NOTIFICATION: [100],
RINGTONE: [200, 100, 200, 100, 200],
HEARTBEAT: [50, 30, 50],
SOS: [100, 50, 100, 50, 100, 200, 50, 200, 50, 200, 100, 50, 100, 50, 100]
};
constructor(options = {}) {
this.enabled = options.enabled ?? true;
this.respectVisibility = options.respectVisibility ?? true;
this.maxDuration = options.maxDuration ?? 10000;
this.supported = 'vibrate' in navigator;
this._loadPreference();
if (this.respectVisibility) {
document.addEventListener('visibilitychange', () => {
if (document.hidden) { this.cancel(); }
});
}
}
vibrate(pattern) {
if (!this.supported || !this.enabled) return false;
if (this.respectVisibility && document.hidden) return false;
const normalizedPattern = this._normalizePattern(pattern);
if (!normalizedPattern) return false;
return navigator.vibrate(normalizedPattern);
}
vibratePreset(presetName) {
const pattern = VibrationManager.PATTERNS[presetName];
if (pattern) { return this.vibrate(pattern); }
return false;
}
cancel() { if (this.supported) { navigator.vibrate(0); } }
setEnabled(enabled) {
this.enabled = enabled;
this._savePreference();
if (!enabled) { this.cancel(); }
}
toggle() { this.setEnabled(!this.enabled); return this.enabled; }
_normalizePattern(pattern) {
if (typeof pattern === 'number') { return Math.max(0, pattern); }
if (Array.isArray(pattern)) {
return pattern.map(p => typeof p === 'number' ? Math.max(0, p) : 0).filter(p => p > 0);
}
return null;
}
_loadPreference() {
try {
const saved = localStorage.getItem('vibration_enabled');
if (saved !== null) { this.enabled = saved === 'true'; }
} catch (e) {}
}
_savePreference() {
try { localStorage.setItem('vibration_enabled', this.enabled.toString()); } catch (e) {}
}
}
// 使用示例
const vibration = new VibrationManager({ respectVisibility: true, maxDuration: 10000 });
vibration.vibrate(50);
vibration.vibratePreset('SUCCESS');
vibration.vibratePreset('ERROR');八、常见问题与解决方案
常见问题
桌面浏览器无法振动怎么办?
Vibration API 仅在移动设备上有效。解决方案是在桌面端提供视觉反馈替代,例如使用 CSS 动画实现闪烁效果,或者在按钮点击时添加缩放动画来模拟触觉反馈。
振动在后台页面仍然执行如何处理?
页面切换到后台后振动可能仍在继续。解决方案是监听 Page Visibility API 的 visibilitychange 事件,当 document.hidden 为 true 时调用 navigator.vibrate(0) 取消振动。
不同 Android 设备振动效果不一致怎么解决?
不同 Android 设备的振动马达和驱动不同,导致振动体验不一致。建议使用标准化的时长档位(如 10ms、25ms、50ms、100ms、200ms),避免依赖精确的振动时长。使用振动模式而非单次长振动也能获得更好的体验。
iOS Safari 支持振动 API 吗?
iOS Safari 不支持 Vibration API。调用 navigator.vibrate() 不会报错,但也不会产生振动效果。在 iOS 上需要使用其他反馈方式(如声音、视觉动画)来替代振动反馈。
如何实现跨平台的反馈方案?
可以封装一个跨平台反馈类,在移动端使用 Vibration API 提供触觉反馈,在桌面端使用 CSS 动画提供视觉反馈(如屏幕闪烁、按钮缩放等),从而实现一致的交互体验。
九、总结
Vibration API 是一个简单但实用的移动端 API,为 Web 应用提供了触觉反馈能力。主要特点包括:
-
简洁的 API:仅通过
navigator.vibrate()方法即可实现所有振动功能,支持单次振动和振动模式。 -
丰富的反馈模式:通过数组参数可以定义复杂的振动模式,实现不同类型的触觉反馈。
-
移动端专属:Vibration API 仅在移动设备上有效,桌面浏览器不支持。
-
用户友好:应适度使用振动,提供开关选项,尊重用户偏好。
在使用 Vibration API 时,需要注意:
-
仅在移动端有效,桌面端需要提供替代反馈方式
-
iOS Safari 不支持此 API
-
应尊重用户偏好,提供振动开关
-
页面不可见时应停止振动
-
避免过度使用,振动反馈应服务于用户体验
通过合理的设计和封装,Vibration API 可以为移动端 Web 应用增添有价值的触觉反馈层,提升用户交互体验。
本文涉及AI创作
内容由AI创作,请仔细甄别