当前位置: 首页 > news >正文

仿手机底部导航栏制作

起初动手写这个仿手机底部导航栏,是因为某天刷手机 App 时,注意到很多应用虽然风格各异,但底部 Tab 栏却有着惊人的一致性:图标尺寸统一、动效自然、响应迅速,而且 UI 极为精致。那一瞬间我意识到,这小小一栏,其实是一个极具锻炼价值的练习场。不仅能挑战前端基础功(比如 Flex 布局和 SVG 运用),还涵盖了 CSS 动效、DOM 操作、甚至一些轻交互逻辑,比如徽章通知、拖拽排序和长按触发菜单等。如果能完整地模拟一套这样的交互体系,简直堪比做一个小型组件库。

所以,我开始了这个“仿手机底部导航栏”的项目。目标清晰:四五个图标组成的底部导航,点击切换内容,高亮状态跟随,添加简单的点击音效、流畅的缩放/平移动画;进阶一点,还要支持拖拽重新排序,长按弹出菜单,以及带有小红点或数字徽章的 Tab。

在正式敲下第一行代码前,我先做了一张功能流转图,理一理这个项目的骨架。整个流程图如下所示:

在这里插入图片描述

这个流程图定下之后,开发起来就心中有谱了。我决定采用原生 HTML/CSS/JS 实现一版,最大限度地控制细节,也更适合讲解。

接下来,进入设计阶段。

我从 UI 开始,设定了整体样式风格:扁平化、微圆角、低饱和色系、浅阴影,参考 iOS 和 Material Design 的部分规范。为了保证响应式和灵活布局,我全程采用 Flexbox 来处理导航栏的分布逻辑。底部导航栏需要固定在底部、等宽分布、图标垂直排列(图标上,文字下)。这是基本布局。

我在 HTML 中这样定义了结构:

<div class="app"><div class="content" id="content"></div><div class="tabbar"><div class="tab-item active" data-tab="home"><span class="icon icon-home"></span><span class="label">首页</span></div><div class="tab-item" data-tab="search"><span class="icon icon-search"></span><span class="label">发现</span></div><div class="tab-item" data-tab="add"><span class="icon icon-add"></span><span class="label">添加</span></div><div class="tab-item" data-tab="message"><span class="icon icon-message"><span class="badge">3</span></span><span class="label">消息</span></div><div class="tab-item" data-tab="profile"><span class="icon icon-user"></span><span class="label">我的</span></div></div>
</div>

可以看到,每个 tab-item 都包含图标和文本,并且某些 tab 上有 badge 提醒(比如消息)。这部分我用的是 Iconfont 图标,也可以替换成 SVG Sprite,更精致一些。CSS 中我为 .tabbar 设置了底部固定,并通过 display: flexflex: 1 让各项平均分布:

.tabbar {position: fixed;bottom: 0;width: 100%;height: 60px;display: flex;background: #fff;border-top: 1px solid #eee;z-index: 10;
}.tab-item {flex: 1;text-align: center;padding: 8px 0;font-size: 12px;color: #888;position: relative;transition: all 0.2s ease;
}.tab-item .icon {font-size: 22px;display: block;transition: transform 0.2s ease;
}.tab-item.active {color: #007aff;
}.tab-item.active .icon {transform: scale(1.2);
}

最初,我在点击 tab 时,只是加个 active 类,然后用 JS 控制 content 区域切换。但这个切换显得太生硬,我想要加入更细腻的体验:点击时图标轻微放大,文字颜色渐变,甚至有“滴”一声的反馈音。于是我在 JS 中加入了这些小交互:

const tabs = document.querySelectorAll('.tab-item');
const content = document.getElementById('content');
const sound = new Audio('./tap.mp3'); // 一段点击音效tabs.forEach(tab => {tab.addEventListener('click', () => {document.querySelector('.tab-item.active').classList.remove('active');tab.classList.add('active');// 播放音效sound.currentTime = 0;sound.play();// 内容切换const tabName = tab.getAttribute('data-tab');content.innerHTML = `<div class="page">${tabName} 页面内容</div>`;});
});

每次点击 tab,不但切换了内容,也播放了点击音,同时通过 CSS 动效让图标轻微缩放。这个细节,虽然微小,却大大提升了整体体验的“真实感”。

我还尝试加入一些更复杂的交互:比如拖拽排序。这个功能听起来简单,实际实现中挑战挺大。如何精准判断拖拽目标?如何在拖拽过程中实时计算位置变化?怎样实现动画化的过渡效果?我决定用原生拖拽 API(dragstart, dragover, drop),但很快发现这个 API 和移动端不太兼容,尤其在手机浏览器中效果很差,最后还是切回了手势模拟。

为了模拟拖拽,我在 touchstart 时记录初始位置,在 touchmove 时改变 translate 值,并在 touchend 判断位置变化、重新排列 DOM。中间遇到不少坑,尤其是事件冒泡和 transition 的兼容问题,我一度打算放弃,后来决定引入一个轻量级库 Sortable.js 来处理这个逻辑。

引入后,拖拽交互变得丝滑自然。配置代码如下:

new Sortable(document.querySelector('.tabbar'), {animation: 150,onEnd: function (evt) {console.log('Tab moved:', evt.oldIndex, '->', evt.newIndex);}
});

这段配置可以让整个 tabbar 支持拖拽重排序,同时自动带有过渡动画。而我只需在 onEnd 中更新本地存储或状态就行。

徽章功能我设计得也很精巧。消息图标上的数字徽章,使用 CSS 的绝对定位实现:

.badge {position: absolute;top: 2px;right: 16px;background: red;color: white;font-size: 10px;padding: 2px 5px;border-radius: 10px;
}

如果消息数量为 0,则隐藏 badge。这个状态可以通过定时 AJAX 轮询或 WebSocket 实时更新,甚至是伪造数据进行演示。为了演示效果,我加入一个简单的定时器模拟新消息到来:

setInterval(() => {const badge = document.querySelector('.icon-message .badge');const newCount = Math.floor(Math.random() * 10);badge.textContent = newCount;badge.style.display = newCount > 0 ? 'block' : 'none';
}, 5000);

接下来是“长按弹出二级菜单”。说实话,长按这个交互一开始我并没有打算加入,因为它不像点击那样天然地由浏览器事件支持。在 PC 上长按是没有语义意义的,只有在触屏设备上,才存在“长按 ≠ 点击”的语义分离。而为了模拟移动端的行为,我不得不实现一个“长按事件模拟器”。

它的基本原理其实很简单:当用户触发 touchstart 或 mousedown 时,记录下开始时间,并启动一个定时器,比如 500ms。如果在这个时间内用户抬起手指(触发 touchend 或 mouseup),说明只是一次普通点击,那就取消长按行为。如果 500ms 过去了,手指还在原地不动,那我们就可以认为用户是“长按了”。

于是我封装了这样一个函数:

function attachLongPress(targetElement, callback, duration = 500) {let timer = null;targetElement.addEventListener('touchstart', start, { passive: true });targetElement.addEventListener('touchend', cancel);targetElement.addEventListener('touchmove', cancel);targetElement.addEventListener('mousedown', start);targetElement.addEventListener('mouseup', cancel);targetElement.addEventListener('mouseleave', cancel);function start(e) {timer = setTimeout(() => {callback(e);}, duration);}function cancel() {if (timer) clearTimeout(timer);timer = null;}
}

用法也很简单:

const addTab = document.querySelector('[data-tab="add"]');
attachLongPress(addTab, () => {showSecondaryMenu();
});

当用户长按“添加”这个按钮时,会弹出一个二级菜单。这个二级菜单我是使用绝对定位和遮罩实现的,效果参考抖音、微信等 App 的交互风格。菜单从底部缓慢浮现,背景加一个半透明黑色遮罩,可以点击取消。

HTML 结构如下:

<div class="mask" id="mask"></div>
<div class="popup-menu" id="popupMenu"><div class="menu-item">拍摄</div><div class="menu-item">从相册选择</div><div class="menu-item cancel">取消</div>
</div>

CSS 动画我用了简单的 transform: translateY(...)opacity 混合动画。整体效果很顺滑:

.mask {position: fixed;top: 0; left: 0;right: 0; bottom: 0;background: rgba(0,0,0,0.4);display: none;z-index: 99;
}.popup-menu {position: fixed;bottom: -200px;left: 0;right: 0;background: #fff;border-top-left-radius: 12px;border-top-right-radius: 12px;padding: 10px 0;text-align: center;z-index: 100;transition: all 0.3s ease;
}.popup-menu.active {bottom: 0;
}

JavaScript 逻辑如下:

function showSecondaryMenu() {const mask = document.getElementById('mask');const menu = document.getElementById('popupMenu');mask.style.display = 'block';setTimeout(() => {menu.classList.add('active');}, 20);mask.addEventListener('click', closeMenu);document.querySelector('.popup-menu .cancel').addEventListener('click', closeMenu);function closeMenu() {menu.classList.remove('active');setTimeout(() => {mask.style.display = 'none';}, 300);}
}

这个小小的长按交互看似简单,但实际上前后花了我一整天时间去调试。尤其是触发条件的精度控制,比如用户手指略微移动该不该取消长按?遮罩点击是否需要防止事件穿透?这些都必须考虑清楚,否则体验会打折。

经过这些努力,这个项目的功能已经趋于完整。但我并不满足,我还想在视觉上再精致一点。

我重新回头打磨了一下 SVG 图标。之前用的是 Iconfont,它虽然方便,但在精度和动画支持上不如原生 SVG。我把每个图标都换成了 inline SVG,并定义了专门的 viewBox、path 动画。例如选中时图标颜色渐变、路径平滑过渡。

示意 SVG 结构如下:

<svg class="icon" viewBox="0 0 24 24"><path d="M..." fill="currentColor"></path>
</svg>

通过控制 .tab-item.active .icon path 的颜色和 transform,我们可以实现更流畅的动画效果。

我还为每个 tab 添加了 hover 态、选中高亮的渐变背景,并对响应式进行了适配处理,比如在横屏模式下底栏缩小字体、缩短间距,在 iOS 安全区内添加 padding-bottom 等。

我甚至添加了暗色模式支持。只需用 media query:

@media (prefers-color-scheme: dark) {.tabbar {background: #1e1e1e;border-top: 1px solid #444;}.tab-item {color: #aaa;}.tab-item.active {color: #2da1ff;}
}

这一点点细节,让整个项目的质感提升了一个维度。

到了这个阶段,我突然意识到:这个小项目已经远远超出了“仿一个 UI”的范畴。它其实成了一个完整的交互系统练习。不仅考察布局功底、动画技巧、状态管理,还涉及可访问性、用户行为模式、设备兼容性等等。它逼迫我重新思考很多“理所当然”的前端习惯。

我决定将这个项目组件化,以便今后可以快速在其它项目中复用。我把 tabbar 写成了一个独立类,用纯 JavaScript 模拟组件的挂载和更新逻辑:

class TabBar {constructor(rootElement, options) {this.root = rootElement;this.tabs = options.tabs;this.onChange = options.onChange;this.render();this.bindEvents();}render() {this.root.innerHTML = this.tabs.map(tab => `<div class="tab-item" data-tab="${tab.key}"><span class="icon">${tab.icon}</span><span class="label">${tab.label}</span></div>`).join('');}bindEvents() {this.root.addEventListener('click', e => {const item = e.target.closest('.tab-item');if (item) {this.selectTab(item.dataset.tab);if (this.onChange) this.onChange(item.dataset.tab);}});}selectTab(key) {this.root.querySelectorAll('.tab-item').forEach(tab => {tab.classList.toggle('active', tab.dataset.tab === key);});}
}

使用方式也非常清晰:

new TabBar(document.querySelector('.tabbar'), {tabs: [{ key: 'home', label: '首页', icon: '🏠' },{ key: 'search', label: '发现', icon: '🔍' },{ key: 'add', label: '添加', icon: '➕' },{ key: 'message', label: '消息', icon: '💬' },{ key: 'profile', label: '我的', icon: '👤' },],onChange: tab => {console.log('切换到:', tab);}
});

当然,这还只是第一版组件化。后续我还会把它封装成 Web Component 或 Vue/React 组件,以适应不同项目架构。

http://www.dtcms.com/a/457869.html

相关文章:

  • 二维码生成的技术原理与全场景实践
  • 做网站 嵌入支付wordpress优化攻略
  • Chromium Embedded Framework (CEF)的构建及运行
  • 批量替换yaml文件url字段
  • “软件维护” 分 4 类?用 “奶茶店售后” 讲透更正 / 适应性 / 完善性维护
  • 恋爱ppt模板免费下载网站网站建设项目风险管理的主要内容
  • 网站主机选择98建筑人才网
  • Windows中在QTCreator中调试,提示缺少debug information files问题的解决
  • 做宠物店网站的素材seo一级域名和二级域名
  • 施工工地云监管平台,工程建设现场管理,智慧工地云平台源码,以AI、物联网、BIM技术为手段,对施工现场进行立体化、全方位、全时段管理
  • 用单调栈高效解决 “首尾均为最大值” 的子数组计数问题(Leetcode 3113)
  • 企业网站自己可以做吗wordpress 登陆 插件
  • 初学c#-c#和.NET Framework - onecopper
  • 大沥南庄网站建设网站开发建设流程
  • nvMolKit:一套基于GPU加速的RDKit核心函数集
  • LOBE-GS:分块致密化效率提升
  • 福州建设招聘信息网站pt网站怎么下载与做
  • dede免费手机网站模板象山seo的优化
  • 央视支持新消费模式:积分助力商家锁客,复购率翻倍
  • 专业政务软件开发北京移动端网站优化
  • 怎样提高网站访问速度一起做网站欧洲站
  • 公司网站建设费属于宣传费吗重庆专业的网站建设公司
  • 广西网站建设哪里好wordpress消息通知
  • 海南网站建设网站开发网站建设的流程步骤
  • 吴恩达机器学习课程(PyTorch适配)学习笔记:1.5 决策树与集成学习
  • 网站推广营销方法成都网站建设 致尚
  • 常州网站建设网站做网站必须要加v吗
  • 理想星环 OS 深度解析:开源浪潮下的智能汽车操作系统革命
  • Qt开发的应用程序编译链接Fast DDS库
  • 网站开发人员属于旅行社手机网站建设成