最近需求 DIY 了一个移动端首屏的轮播图,从原生设计轮播图到实现细节,有了一些不成熟的想法,在这里和大家分享下。
DEMO地址在文章尾部,需要的自取。
产品需求
实现一个首页轮播 Banner,支持12个产品的自动滚动播放,在 App 内 Hybrid H5 的活动首页位置放置。
技术关键细节
1、在动画执行过程中,产品展示组建进行整体缩小和平移。
2、支持自动匀速滚动
3、支持手动滑动滚动
4、支持手机App的顺利展示
5、滚动过程中,产品展示组件间隔不变,商品图底部始终保持对齐
6、能够停到指定位置,即第几个产品在最中间
7、一个屏幕可以容纳一个100%、2个 172 / 212、2个 144 / 212(展示一般)比例产品卡
我先放上我实现 Demo,一些细节并没有按照UI的精细程度来做,避免一些保密风险。
设计思路
首先将对于传统设计来说,还是依赖于 "translate" 的动画结合数据驱动的思路来进行设计,首先给大家介绍下我找到的最接近产品设计的原型【横向循环焦点图片展示】。
很明显上述的功能设计并不能直接使用,无法满足部分需求细节,例如间距不变,自定义底边对齐等,因此想DIY开发一个组件。
经过一次 Demo 验证开发,大体思路如下:
1、首先分为展示层和滚动层,展示层主要是主要负责UI展示,滚动层是负责数据 Scheme 的处理借鉴数据可视化的设计思路,有多少个产品数据,则在滚动层放置同样数量的空白元素(空白元素的高宽均是产品组件的最大时尺寸),用于匀速滚动模拟。这样做的好处是将逻辑控制和展示进行分离。
2、根据滚动层的滚动距离,计算出最靠近 view port 中线(手机屏幕中线)位置的产品卡索引和相对 中线 的相对位置,然后以此为开始向左右两侧进行排布。
3、每个产品组件的大小都是以距离 中线 的水平距离来决定的,距离越远则相对越小,其关系图如下:
4、产品卡部分,为了保持产品图片底部对齐,将底部产品 logo 部分通过 "position: absolute" 进行定位,整个产品卡底部对齐即可
以上则是设计思路,细节会在接下来的部分进行详细介绍。
实现细节
进入正题,整体功能代码比较多,这里就依据代码将具体细节和大家论述下。
滚动层实现
滚动层主要是承载逻辑计算和匀速滚动的模拟,放置了等同数量的空白站位元素,这里大小取 212px (极限大小),方便计算。通过在外层监听滚动事件、进行滚动模拟。当组件的滚动距离 scrollX 变化时,计算出所有产品卡的大小和位置信息。
<div className="brand-showcase-wrapper-content"
id="scrollX" data-scroll={scrollX}>
{/* 滚动层 */}
{data.map(index =>
<div key={index} className="item-place-holder" />
)}
{/* 展示层 从占位的宽度,逐渐按比例缩小 */}
{data.map((item, index) =>
(<div className="item-wapper-layout"
key={index}
style={{
width: layouts[index].width + 'px',
height: layouts[index].width + 'px',
left: layouts[index].position.left + 'px',
right: layouts[index].position.right + 'px'
}}
>
<Item data={item} scalStyle={layouts[index].ratio} />
</div>)
)}
</div>
展示层实现
留意上面的代码你会发现,展示层所需要的数据如下图所展示。
{
width: 212,
ratio: 1,
position: {
left: 375 - 212 / 2,
right: 375 - 212 / 2,
},
};
width 是产品组件的大小;
ratio 是产品组件内部元素的缩放比例;
position 是产品组件的 css position 的属性值。
如何计算得这些数据?
核心在 “getLayoutByScrollX” 的方法中,其思路是首先定位到最靠近屏幕中线的产品组件,然后确定其大小和位置信息,然后依次向左右重复相同的操作,这其中存在很多临界判定和位置计算,有点枯燥,这里就不详细介绍了,有兴趣的可以在代码中检索这个方法。
动画控制实现
动画本质就是一帧帧画面以一定顺序进行播放,下面则是一组《启动时滚动到底部动画》的实现,生产环境中由于是长时间运行请务必注意内存泄漏、屏幕切换、后台运行、预加载等考量,保证其动画的流畅和安全,不要直接使用下面的方式。
useEffect(() => {
timer = setInterval(() => {
const currScrollX = parseInt(document.getElementById('scrollX').dataset.scroll, 10);
if (currScrollX >= maxScrollX || currScrollX <= minScrollX) {
clearInterval(timer);
}
for (let index = 0; index < 10; index ++) {
setTimeout(() => {
const nextScrollX = currScrollX + (index + 1) * 0.1 * UnitWidth;
setScrollX(nextScrollX < minScrollX ? minScrollX : nextScrollX > maxScrollX ? maxScrollX : nextScrollX);
}, index * 25);
}
}, 3000);
return () => {
clearInterval(timer);
}
})
疑问点
1、元素的排布为何不是从头开始排列,而是先定位最靠近中线的元素,然后以此为基础左右排布呢?
回答:如果从头排布的话,本身大小的会影响它距离屏幕中线的具体,相当于它变成一个二元一次方程求解计算,这样的话计算会更加复杂,性能消耗也更大。
2、为何不采用 css translate + scale 的方式来实现?现在通过绝对布局来模拟实现动画,性能方面是不是有缺陷?
回答:首先回答第一个问题,起始我更倾向于css translate + scale方案,但是遇到了一些知识盲区。
其一: 不太了解的CSS动画的协同工作机制,无法做到在滚动同时保持商品卡间距不变;
其二: 诸多组件动画同时启动和停止协同设计遇到了障碍。
从方案设计角度来说,上述的方案肯定是最优解,但是实现需要更加深入知识学习和设计模式的调整,本人后续也会持续优化的。
从成本角度来看,使用 position 来模拟确实更快,当然使用position定位来模拟动画确实是性能消耗很大,这也是js动画的通病,但是可以通过细心的微调可以提升一部分的性能,满足产品需求。
3、在手机端使用,那么它的适配是如何做的?
回答:这里为了一些保密需要就没有直接迁移完整代码,而是在很久之前的工程里在调研期间简单实现了下。
实际这里还是使用 rem 那一套方式来进行做的,其中在实现模拟滚动的时候,其速度的计算需要获取屏幕宽度和像素点密度来进行调整。
4、关于代码质量方面的一些情况介绍
这次Demo并没有引入完善的ESLINT,TYPESCRIPT,代码也粗糙,仅做一个验证思考的demo。无论在个人项目中还是工作项目中,尽可能保持良好的代码规范,受益你我他。
总结
首先这并不是一个完美无缺的解决方案,其性能存在一定的缺陷,需要客户端为 webview 提供足够的计算性能和资源缓存工作,尽可能提高其性能和流畅度。我相信在前端领域权威总是被挑战的,总是有更加优秀的方案产生,我也会一直探索下去,如果你有好的想法也请在留言里讨论。
DEMO github地址 首页轮播图-React,运行路径:/personal/branchShowCase。