CSS 拖曳效果

一、拖拽效果示例

这是移动端很常见的一个效果,可以按住拖来拖去,比如下面的起点中文网触屏版:
纯 CSS 实现拖拽效果 - 图1
这类效果用JS可以很容易实现,无非就是多了一些计算,多考虑了一些临界场景,然后代码量也多了一些。不过CSS也能几乎实现这一效果。

二、CSS实现原理

在传统 web 中,页面滚动是一个很常见交互,操作上就是利用鼠标滚轮或者直接拖动滚动条。但是,移动端可不一样,直接用手指拖动页面就可以滚动了。通常页面是要么垂直方向滚动,要么水平方向滚动,如果两个方向都可以滚动呢?例如

  1. .dragbox{
  2. width: 300px;
  3. height: 300px;
  4. overflow: auto
  5. }
  6. .dragcon{
  7. width: 500px;
  8. height: 500px;
  9. }

只需要内部元素宽高都大于容器就实现两个方向的滚动了(记得设置overflow:auto),示意如下纯 CSS 实现拖拽效果 - 图2
一般情况下,鼠标滚轮只能同时滚动一个方向(按住Shift可以滚动另一方向),但是移动端可以直接拖着内容任意滚动,如下所示
先在内容中间添加一个元素,跟随内容区域一起滚动
纯 CSS 实现拖拽效果 - 图3
接下来,把后面的文本隐藏起来
纯 CSS 实现拖拽效果 - 图4
是不是有点拖拽的味道了?原理就是这么简单!

三、CSS实现细节

首先确定拖拽目标与拖拽容器的尺寸关系,假设拖拽目标的尺寸是w * h,那么很容易得出的尺寸关系为:内部尺寸是容器的2倍减去拖拽目标的尺寸

  1. .dragbox{
  2. width: 100%;
  3. height: 100%;
  4. }
  5. .dragcon{
  6. width: calc(200% - w);
  7. height: calc(200% - h);
  8. }

用一张动图描述如下(中间的橙色块块表示拖拽目标)

纯 CSS 实现拖拽效果 - 图5

四、CSS实现布局

接下来需要把这个特性加入到页面当中,这里列举了两种布局

1.fixed 定位

现在直接把刚才的布局添加到页面上,并添加fixed定位

  1. <body>
  2. ...页面上的其他元素
  3. <div class=“dragbox”>
  4. <div class=“dragcon”></div>
  5. <div class=“ball”></div> <!--拖拽元素-->
  6. </div>
  7. </body>

关键样式如下

  1. .dragbox{
  2. position: fixed;
  3. left: 0;
  4. top: 0;
  5. right: 0;
  6. bottom: 0;
  7. overflow: auto
  8. }

层级示意关系如下
纯 CSS 实现拖拽效果 - 图6
这样一来,dragbox肯定把页面原有的部分遮挡了,所以还需要添加pointer-events: none;同时在拖拽时添加pointer-events: all

  1. .dragbox{
  2. /*...*/
  3. pointer-events: none;
  4. }
  5. .ball{
  6. /*...*/
  7. pointer-events: all;
  8. }
  9. .dragbox.active{
  10. /*...*/
  11. pointer-events: all;
  12. }

借助 JS 可以在按下时触发外层容器滚动

  1. ball.addEventListener('touchstart',(ev)=>{
  2. dragbox.classList.add('active');
  3. })
  4. document.addEventListener('touchend',()=>{
  5. dragbox.classList.remove('active');
  6. })

实际效果如下

纯 CSS 实现拖拽效果 - 图7
完整代码可访问https://codepen.io/xboxyan/pen/PobwxBK(PC访问请打开移动端模式)

2.absolute 定位 + 层级

前面一种布局由于fixed定位层级的影响,不得不借助 JS 来动态改变容器的状态,有没有什么办法可以实现既可以拖拽,又不影响原有页面呢?下面来看这一种布局,用到了absolute定位
这里需要对原有页面包裹一层div容器,如下

  1. <body>
  2. <div class=“dragbox”>
  3. <div class=“dragcon”></div>
  4. <div class=“ball”></div>
  5. </div>
  6. <div class=“body”> <!--单独用一层实现页面滚动-->
  7. ...页面上的其他元素
  8. </div>
  9. </body>

关键样式如下

  1. .dragbox{
  2. position: absolute;
  3. width: 100%;
  4. height: 100%;
  5. overflow: auto;
  6. }
  7. .body{
  8. position: relative;
  9. height: 100%;
  10. overflow: auto;
  11. }
  12. .ball{
  13. position: relative;
  14. z-index: 10; /*拖拽目标的层级设置高一点*/
  15. }

现在层级关系就变成了这样

纯 CSS 实现拖拽效果 - 图8
这里原先页面内容在层级上处于dragbox和拖拽目标之间,所以在拖拽时也不会影响到原有页面的滚动,无需任何 JS 处理
纯 CSS 实现拖拽效果 - 图9
完整代码可访问https://codepen.io/xboxyan/pen/bGBNQxL(PC访问请打开移动端模式)
提示:上面两种布局方式,第一种方式适应性更好,不影响现有项目;第二种体验更好,但是会使用div作为页面滚动容器,会对页面结构做一定的改动,可以根据实际情况自行选择。

五、CSS实现其他功能

1.吸附功能
很多时候,在拖拽结束时需要让它自动吸附在边缘,就如同文章开头的示意图一样。那么,通过吸附,可以联想到什么属性呢?
答案就是CSS Scroll Snap

  1. <body>
  2. ...页面上的其他元素
  3. <div class=“dragbox”>
  4. <div class=“dragcon”>A</div>
  5. <div class=“dragcon”>B</div>
  6. <div class=“ball”></div>
  7. </div>
  8. </body>

下面是关键样式

  1. .dragbox{
  2. ...
  3. scroll-snap-type: x mandatory;
  4. }
  5. .dragcon{
  6. scroll-snap-align: start;
  7. }

实际效果如下

纯 CSS 实现拖拽效果 - 图10
完整代码可访问https://codepen.io/xboxyan/pen/XWNJyPw(PC访问请打开移动端模式)

1.设置初始位置

默认情况下,拖拽目标是唯有右下角的,如何位于左下角呢?很简单,这里拖拽是滚动容器实现的,所以只需要改变scrollLeft 或者 scrollTop 即可

  1. dragbox.scrollLeft = 999;
  2. dragbox.scrollTop = 999;

除此之外,也可以采用纯 HTML 方式实现,利用元素的 autofocus 自动聚焦到可视范围的特性

  1. .dragbox{
  2. left: 10px;
  3. top: 10px;
  4. right: 10px;
  5. bottom: 10px;
  6. /*rect: 10px;*/
  7. }
  8. .dragbox{
  9. padding: 10px;
  10. }

比如希望初始位置在左上角,那么添加一个右下角的自动聚焦元素就可以了(当然还需要设置透明度等隐藏起来)~

2.设置边界

现在拖拽目标的边界是屏幕边缘,有时候可能需要留一些间距,这种需要在 CSS 就很容易了,可以改变left/top/right/bottom、padding、border …很多方式

  1. .dragbox{
  2. left: 10px;
  3. top: 10px;
  4. right: 10px;
  5. bottom: 10px;
  6. /*rect: 10px;*/
  7. }
  8. .dragbox{
  9. padding: 10px;
  10. }

六、说明和小结

关于兼容性本来以为是没什么问题的,实测下来 ios 问题多多,主要是 safari 滚动容器的问题。例如,有些低版本 ios 滚动不顺畅,需要添加
-webkit-overflow-scrolling:touch,才能实现平滑滚动和自动吸附,但是又会导致层级问题,有些文档上描述设置该属性会创建一个原生的滚动容器,层级最高。还有第一种 fixed 布局,如果默认情况下设置pointer-events: none,在touchstart之后设置成auto,这个在 ios 上滚动竟然失效了,但是反转过来就可以了(demo中已兼容ios)。
优点:继承 CSS 的灵活性,几乎零成本,容易复用,同时利用了原生滚动,也不会有卡顿。
不过也有少许局限,如果拖拽目标的尺寸是不固定的,可能需要借助 JS 来获取了。当然相比较而言,这还是一个性价比极高的实现方式。