企鹅FM点歌台总结

- 2016年的老文,搬运存档用 -

企鹅 FM 点歌台,UI 侧大概是踩到了3个坑

  • 轮播
  • 弹幕
  • 键盘呼起

轮播和弹幕

企鹅 fm 点歌台是 2.5 将会新出的功能,其中有两个点觉得值得分享,一个是老生常谈轮子都不知道造了多少个的 banner 轮播,还有一个就是企鹅 FM 移动端对齐的弹幕

轮播

要求

  • 无限轮播
  • JS 没有加载上来的时,保证占位,保证首张 banner 图片正常显示

在实践过程中,我们尝试了2种方式,无论哪个方法,结构都是视口>轮播容器>banner容器+banner容器...

方法一:position 定位,整体移动轮播容器

原理02
注:该原理图以屏幕宽度为 375px 为例

原理图看起来比较抽象,要结合一下 HTML 结构来说,会比较好:

.slider-wrapper
	.slider(style="/*transform: translateX(-375px);*/")
		.banner(style="left:0%;")
		.banner(style="left:100%;")
		.banner(style="left:200%;")
		.banner(style="left:300%;")
		.banner(style="left:-100%;")
		.banner(style="left:-200%;")
		.banner(style="left:-300%;")

轮播器的初始状态如上,slide-wrapper 指的是视口,视口的宽度是固定的:

.slider-wrapper{
  overflow: hidden;
  position: relative;
  z-index: 0;
  width: 100%;
}

滚动的时候,要有动画效果:

.slider{
  will-change: -webkit-transform;
  -webkit-transition: -webkit-transform 0.4s cubic-bezier(0, 0, 0.2, 1);
  -o-transition: -webkit-transform 0.4s cubic-bezier(0, 0, 0.2, 1);
  transition: -webkit-transform 0.4s cubic-bezier(0, 0, 0.2, 1);
}

高度是通过内容来撑的:

.slider .banner{
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 0;
  width: 100%;
  background-position: bottom;
  background-size: cover;
}

.slider .banner:after{
  content: "";
  display: block;
  padding-top: 40%;
}

.slider 每次移动一屏幕宽的距离,因为 .bannerleft 是用百分比来写的,也是一屏幕一屏幕地移动。这里一共有7个 banner 需要轮播,在 .slider 移动之后,还出现在视口(.slider-wrapper)左边的 banner, 每个 left 都加上 700%,就会按照现有的相对顺序跑到最右边。

这个方法的缺点就是,.slidertranslateX 的值会越来越大,.sliderleft 也会越来越大。目前是没有发现什么性能上的问题,但是不知道会不会遇到 stackoverflow 之类的问题。当一直停留在这个页面上,不断进行计算,我怀疑会不会出现数位不够,无法继续往下计算的状况。

那么需要 JS 做什么呢?
  1. 获取屏幕宽度,.slider-wrapper 还是需要内联样式:width: 屏幕宽度
  2. 这里获取屏幕宽度,要注意使用的方法,安卓可能获取到的是实际像素(就是物理像素*ratio),导致显示不正确
  3. setInterval 或者 setTimeout 或者 RAF,移动 ```.slider
有什么问题呢?
  1. 要注意获取屏幕宽度的方法,用 screen.width() 在安卓上会得到实际像素(比如魅族MX4,就会得到1080px),如果要使用这个方法,获取屏幕宽度是不可以的,可以获取视口宽度
  2. 因为 li.banner 是用绝对定位写的,在移动的过程中 left 的值还在改变,所以在计算 translate 的时候,在部分安卓机上 webview 会有问题,轮播不会通过流畅的动画切换,而是轮播区域黑一下,再闪现下一张

解决上面两个问题,就可以了。

方法二:flexbox,轮播器中的首尾片段

如上提到,使用绝对定位写轮播,会出现因为计算引起的 bug。为了避免这个问题,所以我们回到了最传统的写无限轮播的方法:

.slider-wrapper
	.slider
		.banner(style="background-image:url(img/banner_01.jpg);")
		.banner(style="background-image:url(img/banner_02.jpg);")
		.banner(style="background-image:url(img/banner_03.jpg);")
		.banner(style="background-image:url(img/banner_04.jpg);")
		.banner(style="background-image:url(img/banner_05.jpg);")
		.banner(style="background-image:url(img/banner_06.jpg);")
                .banner(style="background-image:url(img/banner_01.jpg);")

将每一个 .banner 一行排开,是用 flexbox 实现的,.slider-wrapper.slider 的宽度都是屏幕宽度:

.slider-wrapper{
  overflow: hidden;
  position: relative;
  z-index: 0;
  width: 100%;
}

.slider{
  will-change: -webkit-transform;
  -webkit-transition: -webkit-transform 0.4s cubic-bezier(0, 0, 0.2, 1);
  -o-transition: -webkit-transform 0.4s cubic-bezier(0, 0, 0.2, 1);
  transition: -webkit-transform 0.4s cubic-bezier(0, 0, 0.2, 1);

  width: 100%;
  display: -webkit-box; 
  display: -moz-box; 
  display: -ms-flexbox; 
  display: -webkit-flex; 
  display: flex;
}

.slider .banner{
  width: 100%;
  background-position: bottom;
  background-size: cover;
  width: 100%;

  -webkit-flex-shrink: 0;
  -ms-flex: 0 0 auto;
  flex-shrink: 0;
}

.slider .banner:after{
  content: "";
  display: block;
  padding-top: 40%;
}
JS 要做的

当按照顺序播放到结构的最后一张,也就是上面的最后一个 .banner 结构,背景是 banner_01.jpg 时,通过 JS 迅速切换到第一个 .banner ,它们两个的背景都是 baner_01.jpg ,切换成功以后,轮播部分的 translate 都被更新了,可以开始新的一轮播放了。

一个小 tip

要看看 JS 没有加载好之前,你的页面是什么样的?Chrome Dev Tools > 按下 F1 > 勾选 Disable JavaScript。当页面需要 JS 参与进来做一些计算或者一些调整(比如轮播)的时候,开发者可以看到当 JS 来不及加载好之前,用户看到的是什么的,保证了在网络糟糕、JS 堵塞或者 JS 被禁用的情况下,我们做出来的页面是不是还能看。

弹幕

任何事情开始之前都是困难的,一旦开始了,就完成了一半。在做项目的时候,我常常有这样的体会。从弹幕需求到真正实现,这句话又出现在我的脑海中。现在要写总结了,它又冒了出来。如上文说到的,点歌台项目中,将会用 CSS 模拟企鹅 FM 客户端的弹幕效果

原理

从某种程度上说,弹幕的实现和轮播有异曲同工之妙,也是视口+滚动区域的模式。

原谅我这个野生的美工的示意图。

红色区域是视口黑色的矩形长条是评论白色区域是滚动区域即评论容器。HTML 结构如下:

.cmt-wrapper
	.cmt-list
    	.cmt-item
        .cmt-item
        .cmt-item
        .cmt-item
        .cmt-item
        .cmt-item
        ...

.cmt-wrapper 即视口,只有滚动到视口区域中的弹幕,才会被展现出来:

.cmt-wrapper{
  position: absolute;
  bottom: 3.75rem;
  left: 55px;
  z-index: 20;
  overflow-y: hidden;
  padding-left: 1px;

  height: 215px;
  width: 248px;
}

一开始呢,用户是看不到弹幕的,弹幕是从下往上出现的,所以动区域要藏在视口的底下

.cmt-list{
  position: absolute;
  top: 215px; 
  left: 0;
  z-index: 0;

  -webkit-transition: -webkit-transform 500ms;
  -o-transition: -webkit-transform 500ms;
  transition: -webkit-transform 500ms;
}

所以滚动区域的 top 值就是视口区域的高度。滚动区域每一次向上移动多少呢?即将显示的 .cmt-item 高度(弹幕可能是一行也可能是两行,所以移动的高度无法固定)。

弹幕容器 .cmt-item 本身的样式很普通,有一点要注意的,每一条弹幕的初始状态不应该在动画类中写,而应该一开始就写好。每条弹幕的动画是以各自左下角为中心,缩小到0,因为之后每条弹幕的显示是通过 setInterval 来控制的,红米在计算时间和渲染上有某种 bug,会出现某几条弹幕动画来不及执行:

.aod-share .cmt-item{
  margin-bottom: 15px;
  position: relative;

  -webkit-transform-origin: 0 100%;
  -moz-transform-origin: 0 100%;
  -ms-transform-origin: 0 100%;
  -o-transform-origin: 0 100%;
  transform-origin: 0 100%;

  -webkit-transform: scale(0);
  -ms-transform: scale(0);
  -o-transform: scale(0);
  transform: scale(0);
}

但,弹幕的出现和消失是有动效的:

  • 出现:当目标弹幕即将要显示出来的时候,它是有一个显示动画的。参照客户端,每一条弹幕都是从屏幕的左下角(或者说是视口的左下角)出现的: @keyframes showCmt{ to { -webkit-transform: scale(1.0); -ms-transform: scale(1.0); -o-transform: scale(1.0); transform: scale(1.0); } } .cmt-item.anim-show{ -webkit-transform-origin: 0 100%; -moz-transform-origin: 0 100%; -ms-transform-origin: 0 100%; -o-transform-origin: 0 100%; transform-origin: 0 100%; transform: scale(0); -webkit-animation: showCmt 500ms ease-in both; -o-animation: showCmt 500ms ease-in both; animation: showCmt 500ms ease-in both; } 
  • 消失:视口就那么大,每次能显示的弹幕就那么几条,为了不让用户发现我们有”视口“这个东西,需要有一个渐隐动画(也是为了和客户端对齐):@keyframes fadeOut{ to { opacity: 0; } } .cmt-item.anim-hide{ -webkit-transform: scale(1.0); -ms-transform: scale(1.0); -o-transform: scale(1.0); transform: scale(1.0); -webkit-animation: fadeOut 1s forwards; -o-animation: fadeOut 1s forwards; animation: fadeOut 1s forwards; }

不过还有最后一个问题:要在什么时候让哪一个 .cmt-item 消失呢?

因为滚动区域是从下到上滚动,而视口是保持在同一位置,以下是初始状态:

再滚动一下(要注意滚动的幅度哦),滚动区域和视口会出现接壤或者滚动区域会跑到视口的上面了,那么第一个 .cmt-item 就要加上 .anim-hide 了:

最后的效果大概是这样的:

键盘呼起时

安卓和 iOS 键盘呼起时页面的形态不同,iOS 上会将页面上移一点,保证输入区域不会被键盘挡住,此时键盘是盖在页面上的。而安卓上会将整个页面上移,键盘和页面会形成有接壤但不重合的两个区域:

请忽略那个下载条

iOS 处理的很智能,所以一般不用担心它。但是安卓上就不一样了,整个页面都网上顶了,普通文档流还好,不会出现遮挡的状况,但是用 position:absolute 定位的页面就不太好,像点歌台里面的这个页面,DOM 结构可以简化为:

.wrapper
	textarea

 样式则是:

.wrapper{
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}
.wrapper textarea{
  position: absolute;
  bottom: 1rem;
  left: 50%;

  transform: translateX(-50%);
}

这样写,如果 textarea 稍微高一点,输入区域一定会出现被遮挡或部分遮挡的状况。所以现在的解决办法是:不用 bottom,用 top。也就是 textarea 相关区域用 top 来定位。不过万一出现键盘很高,占了屏幕的 2/3,所剩区域本来就不多,偏偏 top 值又定的很大,输入区域直接掉出了页面…那也是没sei了。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区