我曾经认为执行滑动手势非常困难,但是最近我发现自己处在不得不做的情况下,发现现实远没有我想象的那么令人沮丧。
本文将逐步指导您以最少的代码量实现该实现。 因此,让我们直接进入吧!
HTML结构
我们从一个.container
开始,里面有很多图像:
<div class='container'>
<img src='img1.jpg' alt='image description'/>
...
</div>
基本样式
我们使用display: flex
来确保图像彼此并排,且两者之间没有空格。 align-items: center
居中将它们垂直对齐。 我们使图像和容器都采用容器的父对象(在本例中为body
)的width
。
.container {
display: flex;
align-items: center;
width: 100%;
img {
min-width: 100%; /* needed so Firefox doesn't make img shrink to fit */
width: 100%; /* can't take this out either as it breaks Chrome */
}
}
.container
及其子图像具有相同的width
,这一事实使得这些图像在右侧溢出(如红色轮廓所示),从而创建了水平滚动条,但这正是我们想要的:
![屏幕快照显示了这种非常基本的布局,其中容器以及与主体具有相同宽度的图像以及图像从容器溢出到右侧,从而在主体上创建了水平滚动条。](https://i-blog.csdnimg.cn/blog_migrate/3c6a955e82f44bcc7706f145e94b8fd4.png)
鉴于并非所有图像都具有相同的尺寸和宽高比,因此我们在其中某些图像的上方和下方都有一些空白。 因此,我们将通过给.container
一个显式的height
这对于这些图像的平均纵横比应该起作用)并将overflow-y
设置为hidden
:
.container {
/* same as before */
overflow-y: hidden;
height: 50vw;
max-height: 100vh;
}
结果可以在下面看到,所有图像都被修剪到相同的height
并且不再有空白:
![屏幕快照显示了在限制容器的高度并用溢流y修剪掉所有不垂直的内容之后的结果。这意味着我们现在在容器本身上有了一个水平滚动条。](https://i-blog.csdnimg.cn/blog_migrate/888d8b9731209cacfdf11fd6321471ff.png)
.container
上通过overflow-y
修剪图像后的结果(请参见实时演示 )。
好的,但是现在我们在.container
本身上有了一个水平滚动条。 好吧,对于没有JavaScript的情况,这实际上是一件好事。
否则,我们为图像的数量创建一个CSS变量--n
,并使用它使.container
足够宽,以容纳其所有与父级(在此情况下为body
)宽度相同的子级图像:
.container {
--n: 1;
width: 100%;
width: calc(var(--n)*100%);
img {
min-width: 100%;
width: 100%;
width: calc(100%/var(--n));
}
}
请注意,我们将先前的width
声明保留为后备。 除非我们在获取.container
及其所包含的子图像数量后从JavaScript设置--n
,否则calc()
值不会改变。
const _C = document.querySelector('.container'),
N = _C.children.length;
_C.style.setProperty('--n', N)
现在,我们的.container
已扩展为适合其中的所有图像:
切换影像
接下来,我们通过将overflow-x: hidden
设置overflow-x: hidden
在容器的父级(在本例中为body
中来摆脱水平滚动条,并创建另一个CSS变量来保存当前所选图像的索引( --i
)。 我们使用它通过平移将.container
相对于视口正确.container
(请记住, translate()
函数中的%
值相对于我们设置此transform
基于的元素的尺寸):
body { overflow-x: hidden }
.container {
/* same styles as before */
transform: translate(calc(var(--i, 0)/var(--n)*-100%));
}
将--i
更改为大于或等于零但小于--n
的其他整数值,可以看到另一幅图像,如下面的交互式演示所示(其中--i
的值由范围输入控制) ):
见笔由thebabydino( @thebabydino )上CodePen 。
好的,但是我们不想使用滑块来做到这一点。
基本思想是,我们将检测"touchstart"
(或"mousedown"
)事件与"touchend"
(或"mouseup"
)事件之间的运动方向,然后相应地更新--i
以移动容器这样,沿所需方向的下一幅图像(如果有)将移入视口。
function lock(e) {};
function move(e) {};
_C.addEventListener('mousedown', lock, false);
_C.addEventListener('touchstart', lock, false);
_C.addEventListener('mouseup', move, false);
_C.addEventListener('touchend', move, false);
请注意,这仅在我们设置pointer-events: none
时才适用于鼠标pointer-events: none
在图像上pointer-events: none
设置。
.container {
/* same styles as before */
img {
/* same styles as before */
pointer-events: none;
}
}
另外,Edge需要从about:flags启用触摸事件,因为默认情况下此选项是关闭的:
![屏幕快照显示Edge中about:标志中的“启用触摸事件”选项设置为“仅当检测到触摸屏时”。](https://i-blog.csdnimg.cn/blog_migrate/75ed962edd114636504a2807e2f57e1f.png)
在填充lock()
和move()
函数之前,我们先结合一下touch和click案例:
function unify(e) { return e.changedTouches ? e.changedTouches[0] : e };
锁定"touchstart"
(或"mousedown"
)意味着获取x坐标并将其存储到初始坐标变量x0
:
let x0 = null;
function lock(e) { x0 = unify(e).clientX };
为了查看如何移动我们的.container
(或者是否这样做,因为我们不想在结束时进一步移动),我们检查是否执行过lock()
动作,以及是否执行过,我们读取当前的x坐标,计算它与x0
之间的差,并从其符号和当前索引中解析出要执行的操作:
let i = 0;
function move(e) {
if(x0 || x0 === 0) {
let dx = unify(e).clientX - x0, s = Math.sign(dx);
if((i > 0 || s < 0) && (i < N - 1 || s > 0))
_C.style.setProperty('--i', i -= s);
x0 = null
}
};
左右拖动的结果如下所示:
![动画的gif。显示如果我们想要的方向上有下一幅图像,我们如何通过向左/向右拖动来切换到下一幅图像。尝试在第一个图像上向右移动或在最后一个图像上向左移动均无济于事,因为我们之前或之后均没有其他图像。](https://i-blog.csdnimg.cn/blog_migrate/c9ece7f72daf932aaa9b9d103a886f22.gif)
以上是预期的结果,也是我们在Chrome和Firefox中花了一点力气才得到的结果。 但是,当我们向左或向右拖动时,Edge会前后导航,这也是Chrome在拖动时也会做的事情。
![动画的gif。显示当我们向左或向右滑动时Edge如何前后浏览浏览量。](https://i-blog.csdnimg.cn/blog_migrate/16a9e324b6a10661f12b140b9762cfd2.gif)
为了覆盖此问题,我们需要添加"touchmove"
事件侦听器:
_C.addEventListener('touchmove', e => {e.preventDefault()}, false)
好了,我们现在在所有浏览器中都具有一些功能 ,但是看起来并不像我们真正想要的那样……!
平稳运动
实现我们想要的最简单的方法是添加transition
:
.container {
/* same styles as before */
transition: transform .5s ease-out;
}
这就是大约25行JavaScript和25行CSS的非常基本的滑动效果:
不幸的是,有一个Edge错误 ,使任何transition
到依赖CSS变量的calc()
转换的转换都失败了。 gh,我想我们现在应该忘记Edge。
完善整体
有了所有很酷的滑动效果,到目前为止我们还没有完全削减它,所以让我们看看可以做出哪些改进。
拖动时更好的视觉提示
首先,什么都不会发生,而我们拖,所有的行动遵循"touchend"
(或"mouseup"
)事件。 因此,当我们拖动时,我们没有迹象表明接下来会发生什么。 是否有下一张图像可以切换到所需的方向? 还是我们已经到达终点,什么也不会发生?
为了解决这个问题,我们通过添加一个CSS变量--tx
来调整翻译量,该变量原本是0px
:
transform: translate(calc(var(--i, 0)/var(--n)*-100% + var(--tx, 0px)))
我们再使用两个事件侦听器:一个用于"touchmove"
,另一个用于"mousemove"
。 请注意,我们已经在Chrome中使用"touchmove"
侦听器阻止了向后和向前导航:
function drag(e) { e.preventDefault() };
_C.addEventListener('mousemove', drag, false);
_C.addEventListener('touchmove', drag, false);
现在,让我们填充drag()
函数! 如果执行了lock()
操作,则读取当前的x坐标,计算该坐标与初始一个x0
之间的差dx
,并将--tx
设置--tx
值(像素值)。
function drag(e) {
e.preventDefault();
if(x0 || x0 === 0)
_C.style.setProperty('--tx', `${Math.round(unify(e).clientX - x0)}px`)
};
我们还需要确保在最后将--tx
重置为0px
,并在拖动期间移除transition
。 为了.smooth
此操作,我们将transition
声明.smooth
类上:
.smooth { transition: transform .5s ease-out; }
在lock()
函数中,我们从.container
删除此类(我们将在"touchend"
和"mouseup"
的末尾再次添加"touchend"
),并且还设置了一个locked
布尔变量,因此我们不必保留执行x0 || x0 === 0
x0 || x0 === 0
检查。 然后,我们改为使用locked
变量进行检查:
let locked = false;
function lock(e) {
x0 = unify(e).clientX;
_C.classList.toggle('smooth', !(locked = true))
};
function drag(e) {
e.preventDefault();
if(locked) { /* same as before */ }
};
function move(e) {
if(locked) {
let dx = unify(e).clientX - x0, s = Math.sign(dx);
if((i > 0 || s < 0) && (i < N - 1 || s > 0))
_C.style.setProperty('--i', i -= s);
_C.style.setProperty('--tx', '0px');
_C.classList.toggle('smooth', !(locked = false));
x0 = null
}
};
结果可以在下面看到。 当我们仍在拖动时 ,我们现在可以直观地看到下一步将发生的情况:
固定transition-duration
此时,无论拖动后仍然需要转换多少图像width
,我们都始终使用相同的transition-duration
。 我们可以通过引入因子f
,以非常简单的方式解决此问题,我们也将其设置为CSS变量以帮助我们计算实际的动画持续时间:
.smooth { transition: transform calc(var(--f, 1)*.5s) ease-out; }
在JavaScript中,我们获得图像的width
(在"resize"
上更新),并计算出水平拖动的比例:
let w;
function size() { w = window.innerWidth };
function move(e) {
if(locked) {
let dx = unify(e).clientX - x0, s = Math.sign(dx),
f = +(s*dx/w).toFixed(2);
if((i > 0 || s < 0) && (i < N - 1 || s > 0)) {
_C.style.setProperty('--i', i -= s);
f = 1 - f
}
_C.style.setProperty('--tx', '0px');
_C.style.setProperty('--f', f);
_C.classList.toggle('smooth', !(locked = false));
x0 = null
}
};
size();
addEventListener('resize', size, false);
现在这给我们带来了更好的结果 。
如果阻力不足,请返回
假设如果我们只拖动某个阈值以下的一点,我们就不想继续下一张图像。 因为现在在拖动过程中相差1 1px
,这意味着我们前进到下一张图像,感觉有点不自然。
为了解决这个问题,我们将阈值设置为图像width
20%
:
function move(e) {
if(locked) {
let dx = unify(e).clientX - x0, s = Math.sign(dx),
f = +(s*dx/w).toFixed(2);
if((i > 0 || s < 0) && (i < N - 1 || s > 0) && f > .2) {
/* same as before */
}
/* same as before */
}
};
结果如下所示:
也许添加反弹?
我不确定这是一个好主意,但无论如何我还是很想尝试:更改计时功能,以便引入跳动。 在cubic-bezier.com上拖动了一些手柄之后,我想到了一个看起来很有希望的结果 :
![动画的gif。显示三次贝塞尔曲线的图形表示,起点在(0,0),终点在(1,1),控制点在(1,1.59)和(.61,.74), [0,1]间隔是[0,1]间隔中时间的函数。还说明了与简单缓和相比,三次三次贝塞尔曲线给出的过渡函数在平移上的外观如何。](https://i-blog.csdnimg.cn/blog_migrate/02a5a991633a4a84dffa65ca192cde6f.gif)
ease-out
相比,我们选择的三次Bézier计时函数看起来像什么。
transition: transform calc(var(--f)*.5s) cubic-bezier(1, 1.59, .61, .74);
那么,JavaScript方式又如何呢?
通过采用JavaScript路线进行过渡,我们可以更好地控制更自然的感觉和更复杂的弹跳。 这也将为我们提供Edge支持。
我们首先摆脱transition
以及--tx
和--f
CSS变量。 这将我们的transform
降低为最初的转换:
transform: translate(calc(var(--i, 0)/var(--n)*-100%));
上面的代码还意味着--i
不再是整数。 当我们完全看到单个图像时,它仍然是整数,但是在触发"touchend"
或"mouseup"
事件后拖动或运动期间,情况就不再如此。
![带注释的屏幕截图说明了我们为--i:0(第一张图片),-i:1(第二张图片),-i:.5(第一张图片的一半和第二张图片的一半)和--i:.75看到的图片(四分之一的第一和四分之三的第二)。](https://i-blog.csdnimg.cn/blog_migrate/cdd375c29dcefb3342d0f6164f1ee863.png)
--i
为0
。
虽然我们可以看到第二个, --i
是1
。
当我们位于第一个和第二个之间时,-- --i
是.5
。
当我们看到第一部分的四分之一和第二部分的四分之三时,-- --i
为.75
。
然后,我们更新JavaScript,以替换更新这些CSS变量的代码部分。 首先,我们要注意lock()
函数,在此我们.smooth
切换.smooth
类,而在drag()
函数中,我们使用更新--tx
替换我们放弃的更新--i
变量,前面提到的,不再需要是整数:
function lock(e) {
x0 = unify(e).clientX;
locked = true
};
function drag(e) {
e.preventDefault();
if(locked) {
let dx = unify(e).clientX - x0,
f = +(dx/w).toFixed(2);
_C.style.setProperty('--i', i - f)
}
};
在我们还更新move()
函数之前,我们引入两个新变量ini
和fin
。 这些代表我们在动画开始时将--i
设置为的初始值,以及我们在动画结束时将相同的变量设置为的最终值。 我们还创建了一个动画函数ani()
:
let ini, fin;
function ani() {};
function move(e) {
if(locked) {
let dx = unify(e).clientX - x0,
s = Math.sign(dx),
f = +(s*dx/w).toFixed(2);
ini = i - s*f;
if((i > 0 || s < 0) && (i < N - 1 || s > 0) && f > .2) {
i -= s;
f = 1 - f
}
fin = i;
ani();
x0 = null;
locked = false;
}
};
这与我们之前的代码没有太大不同。 发生的变化是,我们不再在此函数中设置任何CSS变量,而是设置了ini
和fin
JavaScript变量,并调用了动画ani()
函数。
ini
是我们在"touchend"
/ "mouseup"
事件触发的动画开始时将--i
设置为初始值。 这由这两个事件之一触发时的当前位置给出。
fin
是我们在同一动画结尾处将--i
设置为的最终值。 这始终是一个整数值,因为我们总是以一个图像完全看到结尾,因此fin
和--i
是该图像的索引。 如果我们拖动足够多( f > .2
),并且如果存在沿期望方向的下一个图像( (i > 0 || s < 0) && (i < N - 1 || s > 0)
,则这是沿期望方向的下一个图像。 (i > 0 || s < 0) && (i < N - 1 || s > 0)
)。 在这种情况下,我们还更新了JavaScript变量,该变量存储了当前图像索引( i
)及其相对距离( f
)。 否则,它是同一张图片,因此i
和f
不需要更新。
现在,让我们进入ani()
函数。 我们从简化的线性版本开始,它省略了方向变化。
const NF = 30;
let rID = null;
function stopAni() {
cancelAnimationFrame(rID);
rID = null
};
function ani(cf = 0) {
_C.style.setProperty('--i', ini + (fin - ini)*cf/NF);
if(cf === NF) {
stopAni();
return
}
rID = requestAnimationFrame(ani.bind(this, ++cf))
};
这里的主要思想是,初始值ini
和最后一个fin
之间的过渡发生在整个帧NF
。 每次调用ani()
函数时,我们都将进度计算为当前帧索引cf
与帧总数NF
。 这始终是介于0
和1
之间的数字(或者您可以将其作为百分比,从0%
到100%
)。 然后,我们使用此进度值获取--i
的当前值,并将其设置在容器_C
的style属性中。 如果到达最终状态(当前帧索引cf
等于总帧数NF
,则退出动画循环)。 否则,我们只需增加当前帧索引cf
然后再次调用ani()
。
至此,我们有了一个带有线性JavaScript过渡的有效演示:
但是,这存在我们最初在CSS案例中遇到的问题:无论距离"touchend"
,我们都必须在发布时顺利地转换元素( "touchend"
/ "mouseup"
),并且持续时间始终相同,因为我们总是在相同数量的帧NF
设置动画。
让我们解决这个问题!
为了做到这一点,我们引入了另一个变量anf
,在调用动画函数ani()
之前,我们在move()
函数中存储了使用的实际帧数和计算出的帧数:
function move(e) {
if(locked) {
let dx = unify(e).clientX - x0,
s = Math.sign(dx),
f = +(s*dx/w).toFixed(2);
/* same as before */
anf = Math.round(f*NF);
ani();
/* same as before */
}
};
我们还需要在动画函数ani()
中用anf
替换NF
:
function ani(cf = 0) {
_C.style.setProperty('--i', ini + (fin - ini)*cf/anf);
if(cf === anf) { /* same as before */ }
/* same as before */
};
这样,我们解决了计时问题!
linear
JavaScript过渡的版本( 实时演示 )。
好的,但是线性定时功能并不令人兴奋。
我们可以尝试使用CSS计时功能JavaScript等效项,例如ease-in
, ease-out
或ease-in-out
并观察它们之间的比较。 在前面的链接文章中 ,我已经详细解释了如何获取这些内容 ,因此,我不再赘述,只需将包含所有对象的对象放入代码中:
const TFN = {
'linear': function(k) { return k },
'ease-in': function(k, e = 1.675) {
return Math.pow(k, e)
},
'ease-out': function(k, e = 1.675) {
return 1 - Math.pow(1 - k, e)
},
'ease-in-out': function(k) {
return .5*(Math.sin((k - .5)*Math.PI) + 1)
}
};
k
值是进度,它是当前帧索引cf
与过渡在anf
发生的实际帧数之间的比率。 这意味着,例如,如果我们要使用ease-out
选项,则需要稍微修改ani()
函数:
function ani(cf = 0) {
_C.style.setProperty('--i', ini + (fin - ini)*TFN['ease-out'](cf/anf));
/* same as before */
};
ease-out
JavaScript过渡的版本( 实时演示 )。
我们还可以通过使用CSS无法提供给我们的跳动计时功能使事情变得更加有趣。 例如,类似于下面的演示所示的示例(单击以触发转换):
见笔由thebabydino( @thebabydino )上CodePen 。
此图形将与easeOutBounce
计时功能图形相似。
![动画的gif。显示跳动计时功能的图表。此功能从初始值到最终值的增加缓慢,然后加速。一旦达到最终值,它就会快速反弹回最终值和初始值之间的距离的四分之一,然后返回到最终值,再次反弹一点。总共反弹了三下。在右侧,我们有一个动画,说明函数值(图上的纵坐标)随时间的变化(随着我们沿横坐标的变化)。](https://i-blog.csdnimg.cn/blog_migrate/c9f91caee11fa60aa40136e60729c1c2.gif)
获得这种计时功能的过程类似于获得CSS版本JavaScript渐进ease-in-out
(同样, 先前链接的文章中介绍了用JavaScript模拟CSS计时功能)。
我们从[0, 90°]
间隔(或弧度的[0, π/2]
)上的余弦函数开始不跳动,从[0, 270°]
( [0, 3·π/2]
)开始1
弹跳, [0, 450°]
( [0, 5·π/2]
) 2
弹跳,依此类推……通常是[0, (n + ½)·180°]
间隔( [0, (n + ½)·π]
)反弹n
。
见笔由thebabydino( @thebabydino )上CodePen 。
此cos(k)
函数的输入为[0, 450°]
区间,而其输出为[-1, 1]
区间。 但是我们想要的是一个函数的域为[0, 1]
间隔,并且其共域也为[0, 1]
间隔。
我们可以通过仅取绝对值|cos(k)|
来将共域限制为[0, 1]
间隔|cos(k)|
:
见笔由thebabydino( @thebabydino )上CodePen 。
在获得共域所需的间隔的同时,我们希望此函数在0
处的0
0
,在间隔另一端的值为1
。 当前,这是另一种方法,但是如果将函数更改为1 - |cos(k)|
,则可以解决此问题1 - |cos(k)|
:
见笔由thebabydino( @thebabydino )上CodePen 。
现在我们可以继续将域从[0, (n + ½)·180°]
区间限制为[0, 1]
区间。 为此,我们将函数更改为1 - |cos(k·(n + ½)·180°)|
:
见笔由thebabydino( @thebabydino )上CodePen 。
这为我们提供了所需的域和共域,但是仍然存在一些问题。
首先,我们所有的弹跳都具有相同的高度,但是我们希望它们的高度随着k
从0
增加到1
而减小。 在这种情况下,我们的解决方法是将余弦乘以1 - k
(或将1 - k
的幂用于幅度的非线性减小)。 下面的交互式演示演示了该振幅如何随各种指数a
变化以及如何影响到目前为止的功能:
见笔由thebabydino( @thebabydino )上CodePen 。
其次,所有的弹跳都花费相同的时间,即使它们的振幅不断减小。 这里的第一个想法是在余弦函数中使用k
的幂,而不是仅使用k
。 由于余弦不再以相等的间隔达到0
,这使事情变得很奇怪 ,这意味着我们不再总是得到f(1) = 1
,这实际上是我们始终需要的计时函数所需要的要使用。 但是,对于像a = 2.75
, n = 3
和b = 1.5
,我们得到的结果看起来令人满意,因此即使可以对其进行调整以获得更好的控制,我们也将其保留在该位置:
![先前链接的演示的屏幕快照,显示了a = 2.75,n = 3和b = 1.5设置的图形结果:从0(对于f(0))到1的缓慢然后快速的增加,反弹不到一半。到达1之后的方式,先回升,然后在到达1之前又有一个更小的弹跳,在这里我们总是想得到f(1)。](https://i-blog.csdnimg.cn/blog_migrate/53acd751f3b9dfd2dd71c0138a452d96.png)
如果我们希望发生反弹,这是我们在JavaScript中尝试的功能。
const TFN = {
/* the other function we had before */
'bounce-out': function(k, n = 3, a = 2.75, b = 1.5) {
return 1 - Math.pow(1 - k, a)*Math.abs(Math.cos(Math.pow(k, b)*(n + .5)*Math.PI))
}
};
嗯,在实践中似乎有点太极端了:
也许我们可以使n
取决于从发行之日起我们仍然需要执行的翻译量。 我们将其设置为变量,然后在调用动画函数ani()
之前在move()
函数中进行设置:
const TFN = {
/* the other function we had before */
'bounce-out': function(k, a = 2.75, b = 1.5) {
return 1 - Math.pow(1 - k, a)*Math.abs(Math.cos(Math.pow(k, b)*(n + .5)*Math.PI))
}
};
var n;
function move(e) {
if(locked) {
let dx = unify(e).clientX - x0,
s = Math.sign(dx),
f = +(s*dx/w).toFixed(2);
/* same as before */
n = 2 + Math.round(f)
ani();
/* same as before */
}
};
这给了我们最终结果:
肯定还有改进的余地,但是我对制作好的动画没有任何感觉,因此我只保留它。 实际上,这是一个功能跨浏览器(不存在使用CSS过渡的版本所具有的Edge问题)并且非常灵活。
翻译自: https://css-tricks.com/simple-swipe-with-vanilla-javascript/