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([]);

参数说明

参数类型 说明 示例
数字 振动时长(毫秒),0 或负数不振动 navigator.vibrate(200)
数组 振动模式,奇数位为振动时长,偶数位为暂停时长 navigator.vibrate([200, 100, 200])
空数组 取消当前振动 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">&#9758;</span> 轻触
            <div class="duration">50ms</div>
        </button>
        <button class="vibrate-btn medium" data-vibrate="200">
            <span class="icon">&#9995;</span> 中等
            <div class="duration">200ms</div>
        </button>
        <button class="vibrate-btn long" data-vibrate="500">
            <span class="icon">&#9996;</span> 长振
            <div class="duration">500ms</div>
        </button>
        <button class="vibrate-btn pattern" data-vibrate="[100,50,100,50,300]">
            <span class="icon">&#9888;</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">&#9888;</span> SOS
            <div class="duration">摩斯密码 SOS</div>
        </button>
        <button class="vibrate-btn stop" data-vibrate="0">
            <span class="icon">&#9632;</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">&#9993;</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">&#9742;</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">&#9888;</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">&#10060;</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">&#9989;</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">&#128276;</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 = '&#128163;';
                target.dataset.type = 'bomb';
            } else if (rand < 0.3) {
                target.classList.add('bonus');
                target.innerHTML = '&#11088;';
                target.dataset.type = 'bonus';
            } else {
                target.classList.add('normal');
                target.innerHTML = '&#127919;';
                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>

五、浏览器兼容性

特性 Chrome (Android) Firefox (Android) Safari (iOS) Edge (Mobile) 桌面浏览器
基本振动 32+ 16+ 不支持 79+ 不支持
振动模式 32+ 16+ 不支持 79+ 不支持
取消振动 32+ 16+ 不支持 79+ 不支持

兼容性注意事项

代码示例

// 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 应用增添有价值的触觉反馈层,提升用户交互体验。

标签: Vibration API 设备振动 触觉反馈 移动端API 振动模式 HTML5 API

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: HTML5 API:Speech API - 完整教程与代码示例 下一篇: HTML5 API:Battery API - 完整教程与代码示例

poll相关推荐