背景
做了一周大屏开发,也是在近期全部完成。借此记录一下大屏的整体适配处理以及列表自动循环滚动播放动画的处理实现。其余的业务相关的地图以及内容这边不作展示
布局
在进行适配方案选择时,考虑到保留设计稿设计比例的效果,这边是采用transform: scale()的方式进行实现。根据容器宽度来动态调整缩放比例。既然是scale来进行缩放的话,页面布局可以采用px加绝对定位的方式来实现,所有元素按照设计稿的样式属性,也算是没有心智负担。但是缩放也是有一定弊端的,就是在宽高比不是设计稿(1920×1080)的比例时,会出现滚动条或者底部留白。
以下是布局代码
<div class="bs-page" ref="pageRef">
<div class="bs-page-content">
<div class="bs-page-box">
</div>
</div>
</div>
const pageRef = ref<HTMLElement>();
/** 缩放比例 */
const scale = ref(1);
/** 更新缩放比例 */
const updateScale = () => {
const dom = pageRef.value;
if (!dom) {
scale.value = 1;
return;
}
const width = dom.offsetWidth;
const height = dom.offsetHeight;
scale.value = width / 1920;
/** 是否出现滚动条 */
const isSroll = width / height > 1920 / 1080;
if (isSroll) {
nextTick(() => {
const cWidth = dom.clientWidth;
scale.value = cWidth / 1920;
});
}
};
/** 内容宽度 */
const contentWidth = computed(() => `${scale.value * 1920}px`);
/** 内容高度 */
const contentHeight = computed(() => `${scale.value * 1080}px`);
onMounted(() => {
updateScale();
window.addEventListener('resize', updateScale, false);
});
onUnmounted(() => {
window.removeEventListener('resize', updateScale, false);
});
.bs-page {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #020b40;
overflow: auto;
}
.bs-page-content {
width: v-bind(contentWidth);
height: v-bind(contentHeight);
overflow: hidden;
}
.bs-page-box {
width: 1920px;
height: 1080px;
transform-origin: 0 0;
transform: scale(v-bind(scale));
background: url(./img/bg.png);
background-size: 1920px 1080px;
}
滚动特效
循环滚动效果算是这里面一个比较有趣的一个地方。首尾相连进行循环滚动。功能没有多复杂,也有很多实现方式。以下就是我的几种实现方式和我对这几种实现方式的一个分析评价。
setInterval或者setTimeout
在开发时首先想到的就是利用定时器来实现滚动效果,在特定时间间隔,配合 绝对定位+transform:translateY()每次变化1px来达到滚动效果。滚动是实现了,但是如何做到首尾相连呢?
要做到首尾相连,最好最简单的就是额外复制一份数据,要是封装了组件就额外增加一个组件,我是额外增加一个,第二个只需增加一个marginTop保持间距即可,如下:
<RankList ref="RankListRef" :data="dataList" class="rank-one"></RankList>
<!-- 用于滚动,不滚动时不展示 -->
<RankList v-if="isNeedScroll" :data="dataList" class="rank-two"></RankList>
数据不超过容器肯定就不需要滚动,数据超过容器才需要滚动。这时候我的想法就是在上一个div快滚完时,将下一个div放置在上一个之后,增加判断,依次循环。
以下是我的实现方式:
/** 列表是否需要自动滚动,超过十条数据开始滚动 */
const isNeedScroll = ref(false);
/** 首尾相连滚动-上一个滚动距离 */
const scrollTopOne = ref(0);
const scrollTopOneTrans = computed(() => `translateY(${scrollTopOne.value}px)`);
/** 下一个滚动距离 */
const scrollTopTwo = ref(0);
/** 下一个是否开始滚动 */
const twoStart = ref(false);
const scrollTopTwoTrans = computed(() => `translateY(${scrollTopTwo.value}px)`);
const RankListRef = ref();
const timer = ref<number | null>(null);
/** 设置滚动 */
const setScroll = () => {
scrollTopOne.value = 0;
scrollTopTwo.value = 0;
twoStart.value = false;
if (timer.value !== null) {
clearInterval(timer.value);
timer.value = null;
}
timer.value = setInterval(() => {
// 在上方滚到底部时
if (-scrollTopOne.value + 385 === (RankListRef.value?.$el.clientHeight)) {
twoStart.value = true;
// 重置下方滚动位置,下方开始滚动
scrollTopTwo.value = 0;
}
// 在下方滚到底部时,使上方在下方的位置位置开始滚动
if (-scrollTopTwo.value === RankListRef.value?.$el.clientHeight) {
scrollTopOne.value = 428;
}
scrollTopOne.value -= 1;
if (twoStart.value) {
// 此时下方开始滚动
scrollTopTwo.value -= 1;
}
}, 50);
};
.rank-one {
position: absolute;
top: 0;
right: 0;
width: 100%;
transform: v-bind(scrollTopOneTrans);
}
.rank-two {
margin-top: 23px;
position: absolute;
right: 0;
top: 385px;
width: 100%;
transform: v-bind(scrollTopTwoTrans);
}
此时的写法还是比较复杂的,但是效果基本都实现了,可能是开发时用的屏幕比较大,暂时没发现。不过后续同事在笔记本打开时,发现滚动是波浪效果,如下:
看着比较魔幻,我试着将浏览器拉窄一点,果真是这样。
回头看看实现方式,大概也明白为啥会出现这种情况了。setInterval与setTimeout其实都是宏任务,在压入队列时,理想状态是每50ms取出执行。但是现实在执行时,并不能保证间隔都是50ms。此时的效果就会如图,呈现波浪型。后续我尝试setTimeout,也是一样的效果。但是这种效果肯定是不行的。
requestAnimationFrame
既然是动画,当然也会想到这个函数。此函数就是
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数
更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
const fn = () => {
// 在上方滚到底部时
if (-scrollTopOne.value + 385 === (RankListRef.value?.$el.clientHeight)) {
twoStart.value = true;
// 重置下方滚动位置,下方开始滚动
scrollTopTwo.value = 0;
}
// 在下方滚到底部时,使上方在下方的位置位置开始滚动
if (-scrollTopTwo.value === RankListRef.value?.$el.clientHeight) {
scrollTopOne.value = 428;
}
scrollTopOne.value -= 1;
if (twoStart.value) {
// 此时下方开始滚动
scrollTopTwo.value -= 1;
}
requestAnimationFrame(fn);
};
requestAnimationFrame(fn);
函数执行频次大致与浏览器刷新次数相同,谷歌浏览器是每秒刷新60次,所以函数执行一次的时间是1000/60,且这时间是固定的,无法去改变,但是这个滚动时间对于当下这个场景又过于快了,最终效果还是会带点抖动的。所以这个方案也被pass了
css3 animation
兜兜转转还是回到了用css来处理,其实按理处理动画最好的办法还是用css来处理,不管是性能还是动画。具体animation用法这边不做描述,需要可以看这里animation
要使用animation需要解决两个问题:
- 动画
- 持续时间
最开始我沿用之前的做法,打算控制两个div进行滚动,但是发现控制起来很难,你需要等待上一个滚动到指定位置后,再控制下一个开始滚动,而这样做的话又相当于通过js来进行控制,失去了animation的优势。
换一个角度,我们不控制两个,在外面包裹一个div,只需要控制外层div进行滚动。这个div中包裹两个相同的内容,上下排列,我循环transform:translateY(-50%)是不是就可以实现循环滚动了。另一个需要解决的就是持续时间,我们之前处理是50ms移动1px,现在只需要获取其中一个div的高度再乘以50ms不就计算出了持续时间了嘛!!!
开干,代码如下:
<div :class="{scroll_box: isNeedScroll}">
<RankList ref="RankListRef" :data="dataList"></RankList>
<!-- 用于滚动,不滚动时不展示 -->
<RankList v-if="isNeedScroll" :data="dataList" class="rank-two"></RankList>
</div>
const scrollTime = ref(10);
const animationValue = computed(() => `${scrollTime.value}ms`);
const RankListRef = ref();
/** 设置滚动 */
const setScroll = () => {
// 计算滚动时长
const height = RankListRef.value.$el.clientHeight;
scrollTime.value = (height * 50);
};
.scroll_box {
position: absolute;
top: 0;
right: 0;
width: 100%;
animation: scrolltop v-bind(animationValue) linear infinite;
}
@keyframes scrolltop {
0% { transform: translateY(0) }
100% { transform: translateY(-50%) }
}
可以看见,采用这种方式不仅代码量少了很多而且逻辑更加简单,只需要控制animation的持续时间而不去考虑移动多少距离,不错。以下是一个整体的效果
到这,这个效果的优化到这算是告一段落。
结语
其实内容不算复杂,本文更多的是对自己做项目的一个记录吧,将一些有意思的点分享出来。若是大家有其它的处理方式或者是想沟通交流的内容,欢迎大家留言。