整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

带你手写一个轮播图之JavaScript逻辑实现

带你手写一个轮播图之JavaScript逻辑实现

了控制篇幅,本打算分3篇文章来讲述轮播图实现的,后来想了想,JS代码量其实很少,完全没必要,所以这篇文章就是收尾。

昨天,我们完成了结构和布局设计,今天我们来完成JavaScript逻辑部分。我这人比较有代码洁癖,不喜欢过多的变量污染全局作用域,习惯使用自执行的匿名函数包裹一个功能块。我们所有的JS代码都在如下函数内部编写:

(function () {
  // ... ... 我们的代码都写到这儿
})();

我们的程序中需要用到多个DOM元素,所以我们先拿到它们的引用存储到变量中。

/** @type {HTMLDivElement} 轮播图容器 */
  const carouselWrapper=document.querySelector('.carousel-wrapper')
  /** @type {HTMLUListElement} banner列表元素 */
  const slides=carouselWrapper.querySelector('.slides')
  /** @type {HTMLDivElement} 指示器 */
  const indicator=carouselWrapper.querySelector('.indicators')
  /** @type {NodeListOf<HTMLLIElement>} 指示器节点列表 */
  const indicatorItems=indicator.querySelectorAll('.indicator-item')
  /** @type {HTMLUListElement} 导航按钮列表元素 */
  const nav=carouselWrapper.querySelector('.nav')

接下来,我们定义一些常量和变量,建议大家定义常量名使用全大写字母下划线分割。

 /** 活动类名 */
  const ACTIVE_CLS_NAME='active'
  /** 左导航按钮类名 */
  const CLS_NAME_NAV_LEFT='nav-left'
  /** 右导航按钮类名 */
  const CLS_NAME_NAV_RIGHT='nav-right'
  /** 取消过渡的类名 */
  const CLS_NAME_NO_TRANSITION='no-transition'
  /** 定时器间隔 */
  const TIMER_DELAY=3000

  /** 定时器id */
  let timerId
  /** 当前指示器索引 */
  let currentIndex=0
  /** 当前banner索引 */
  let currentslideItemIndex=0
  /** 过渡结束标识 */
  let transitionendFlag=true

要做到左右滚动的无缝切换,我们必须至少拷贝banner列表中的第一和最后一个元素,将其追加到banner列表尾部,并更新banner列表的宽度。为了节省代码,我完全拷贝了一份banner列表的子元素。我们用一个函数来做这件事,代码如下:

function updateSlides() {
    slides.innerHTML +=slides.innerHTML
    slides.style.width=`${slides.childElementCount * 100}%`
  }

当用户单击指示器项的时候,我们需要切换当前显示的banner图。首先需要确定被单击指示器项的索引。我们用一个函数来实现,代码如下:

function getTargetIndex(el) {
    for (let i=0, len=indicatorItems.length; i < len; i++) {
      if (indicatorItems[i]===el) {
        return i
      }
    }
    return -1
  }

索引确定了,我们需要根据索引来更新X轴的平移值。当然,要实现左右滚动,我们还有其它实现方案,例如:改变可滚动元素的滚动条位置,改变元素的左右定位等。那么,为什么使用3D平移变换呢?原因之一就是,浏览器会对3D平移变换开启GPU硬件加速渲染,以获得更好的性能,直观感受就是动画非常丝滑,毫无拖泥带水之感。我们用一个函数来更新X轴平移,代码如下:

function updateTranslateX(index, targetIndex=-1) {
    // 如果过渡没有结束,直接返回
    if (!transitionendFlag) return
    // 当前指示器移除活动类
    indicatorItems[currentIndex].classList.remove(ACTIVE_CLS_NAME)
    // 如果没有提供目标索引,就把index赋给它
    if (targetIndex < 0) {
      targetIndex=index
    }
    // 目标指示器添加活动类
    indicatorItems[targetIndex].classList.add(ACTIVE_CLS_NAME)
    // 将当前索引设为目标索引
    currentIndex=targetIndex
    // 将当前banner索引设为index
    currentslideItemIndex=index
    // 改变banner列表元素的X轴平移变换
    slides.style.transform=`translateX(-${index / slides.childElementCount * 100}%)`
  }

以下是指示器单击处理函数的实现。这个代码很简单,首先获取目标指示器项的索引,如果找到了,就根据该索引更新banner列表的X轴平移。

/** @param {MouseEvent} e */
  function indicatorClickHandler(e) {
    const index=getTargetIndex(e.target)
    index >=0 && updateTranslateX(index)
  }

接下来,我们实现左右导航按钮的单击。首先,我们需要一个向左和向右滚动的函数,代码如下:

function toNext() {
    // 指示器项的数量
    const len=indicatorItems.length
    if (currentIndex < len) {
      if (currentIndex===len - 1) {
        // 继续滚动到下一个索引位置,并将目标索引设为0
        updateTranslateX(currentIndex + 1, 0)
      } else {
        updateTranslateX(currentIndex + 1)
      }
    } else {
      updateTranslateX(0)
    }
  }

  function toPrev() {
    const len=indicatorItems.length
    if (currentIndex < 1) {
      // 去掉过渡
      slides.classList.add(CLS_NAME_NO_TRANSITION)
      // 让索引为len的banner图可见
      updateTranslateX(len, len - 1)
      setTimeout(()=> {
        // 添加过渡
        slides.classList.remove(CLS_NAME_NO_TRANSITION)
        // 让索引为len-1的banner图可见,此时有过渡
        updateTranslateX(len - 1)
      }, 0)
    } else {
      updateTranslateX(currentIndex - 1)
    }
  }

以下是左右导航按钮单击事件处理函数的实现。我们根据是否包含指定的类名,来判断单击的哪个按钮,进而执行不同的导航函数。

/** @param {MouseEvent} e */
  function navClickHandler(e) {
    /** @type {{target: HTMLElement}} */
    const { target }=e
    if (target.classList.contains(CLS_NAME_NAV_RIGHT)) {
      toNext()
    } else if (target.classList.contains(CLS_NAME_NAV_LEFT)) {
      toPrev()
    }
  }

我们还需要监听banner列表的过渡开始和结束事件。过渡开始事件处理函数非常简单,只是将过渡结束标志设为了false,代码如下:

function slidesTransitionstartHandler(e) {
    transitionendFlag=false
  }

下面是过渡结束事件处理函数定义,其实也很简单。

  function slidesTransitionendHandler(e) {
    transitionendFlag=true
    if (currentslideItemIndex >=indicatorItems.length) {
      // 取消过渡
      slides.classList.add(CLS_NAME_NO_TRANSITION)
      updateTranslateX(0)
      setTimeout(()=> {
        // 再添加回过渡
        slides.classList.remove(CLS_NAME_NO_TRANSITION)
      }, 0)
    }
  }

我们还需要让轮播图可以间隔一定时间自动切换。所以,我们需要实现开始和结束定时器的函数。这两个函数非常简单,代码如下:

function startTimer() {
    timerId=setInterval(toNext, TIMER_DELAY)
  }

  function stopTimer() {
    clearInterval(timerId)
    timerId=null
  }

现在,是时候让我们的轮播图动起来了。我们首先调用更新banner列表函数,然后开始定时器,最后注册事件监听器。

  updateSlides()
  startTimer()
  // 指针进入容器的时候停止定时器
  carouselWrapper.addEventListener('mouseenter', stopTimer)
  // 指针离开容器的时候开始定时器
  carouselWrapper.addEventListener('mouseleave', startTimer)
  nav.addEventListener('click', navClickHandler)
  indicator.addEventListener("click", indicatorClickHandler)
  slides.addEventListener('transitionstart', slidesTransitionstartHandler)
  slides.addEventListener('transitionend', slidesTransitionendHandler)

最终实现的效果图如下:

童鞋们都学废了吗?是不是很简单?感谢阅读!

先让我们回顾一下系列内容。

从零开始学Bootstrap(1)介绍了BootStrap最简单的模板,逐条解释了每行代码的含义。

从零开始学Bootstrap(2)强调了边学边做,通过实际的例子,讲解了如何一步一步的实现自己想要的效果。

写到这里,这篇从零开始学Bootstrap(3)我想写以下几个内容:

1. 基于我对Bootstrap的理解,做一个小小的总结。

2. 对从零开始学Bootstrap(2)例子进行UI美化和代码优化,主要是说说我是怎么做出自己想要的效果的。

3. 授人以鱼不如授人以渔,以自己的例子(因为自己也是新手,写出来的东西可能更适合初学者),讲讲代码编写过程中遇到的坑和需要注意的点。

废话不多说,下面开始:

一、 Bootstrap的小小总结

基于以Bootstrap的官方文档(http://v3.bootcss.com/)的说明,Bootstrap分为三个大块:CSS样式,组件,Javascript插件。

首先要明确一点:Bootstrap是一个框架,意思就是别人造好了轮子,你可以直接拿来用,免去了自己去造轮子。所以咱们需要明确两点:这些轮子是什么样的轮子,怎么样去使用这些轮子。

1. CSS样式:主要介绍了栅格系统和Bootstrap的全局样式。通过设定Class的值实现。

1.1栅格系统:

让我们可以很方便的实现HTML页面的布局(http://v3.bootcss.com/css/#grid)。

我觉得栅格系统很重要。因为HTML页面的布局是很重要也很繁琐的一项任务(你看一下W3School里关于布局的介绍http://www.w3school.com.cn/html/html_layout.asp,看一下例子里的代码,就明白了),并且需要考虑到不同浏览器、不同设备的兼容性。

栅格系统把这一切大大简化了。打开上面关于栅格系统的连接,你会发现只需要根据你想要实现的效果,给HTML元素Class属性赋相应的值,就可以实现,并且还能设置针对不同屏幕大小的设备展现不同的效果。

1.2 Bootstrap全局样式:

也就是Bootstrap对常用HTML元素(eg: DIV、Button、 P、 Table、 Img等等)是怎样美化的。通过给HTML元素的Class属性赋相应的值,就可以得到自己想要的效果。

举一个最简单的例子:

如上图所示,Bootstrap可以让你仅仅改变Button元素的class的值就是改变按钮的大小,而不用很麻烦的去修改css文件,或者给元素内嵌style的值。

2. 组件:我认为组件就是Bootstrap把一些元素(HTML元素和Javascript代码)组合起来,就变成了组件,并且提供了很多很好看很实用的图标。这些组件基本都是HTML开发过程中常用的。通过设定Class的值实现。(http://v3.bootcss.com/components/)

举一个最简单的例子:

如上图所示,当我们需要实现导航功能的时候。找到Boostrap里相应的组件,依照其给的代码模板,根据自己的需求,赋相应的class和ul、li值就可以了

3. Javascript插件:我认为Bootstrap的Javascript插件就是封装了常用网页交互功能的”轮子”。只需要设定class属性和data属性就可以实现常用的网页交互功能,而不用自己写一大堆javascript代码。

首先说一个小插曲,新手可能误以为”javascript”和”java”有着很深的联系,甚至会认为javascript是java的变种。其实并不是这样,javascript是网景(Netscape)公司开发的应用于互联网的脚本语言,其实它最先的名字”是livescript”,后来网景公司与Sun公司(也就是发明Java的公司,后来被Oracle收购)达成合作,当时Java语言如日中天,名气很大,为了搭顺风车,就把livesript改名为javascript。以至于有人开玩笑:”Java”和”Javascrip”的区别就好比”雷锋”和”雷峰塔”的区别一样。

言归正传,我们知道,Javascript是为了赋予网页交互功能而存在的。所以,Bootsript上丰富的Javascript插件能够让你很方便的实现常用的网页交互功能,而不用把精力放在”造轮子”上。

如上图所示,利用Bootstrap的轮播插件(http://www.runoob.com/bootstrap/bootstrap-carousel-plugin.html, Bootstrap的官方文档这里没有翻译成中文,runoob上却有很详细的中文翻译,并且可以在线修改代码提交观察效果,强烈推荐),你可以很方便的实现现在很多网站都采用的图片轮播功能。这里只需要根据上述链接里面的教程,赋相应的class和图片src值就可以了,连data值都不用设。

二、 对从零开始学Bootstrap(2)例子进行UI美化和代码优化

下图是上期教程中实现的效果:

我们可以看到有以下几个缺点需要改正

(1) 点击具体同学,显示其信息之后,没有处于被选中的状态。(你一开始点击,会处于被选中状态,但那只是button的点击效果,你点一下空白处,被选中的状态就消失了)

(2) 色彩单调,不太美观。

(3) 布局方面需要调整,信息框是呈现我们需要的信息的地方,应该尽可能大,最好其显示的时候能把不需要的框给隐藏起来。

(4) 对于代码方面,对从零开始学Bootstrap(2)中的Javascript代码都写在HTML页面里了,并且有重复的代码段,应该把重复的代码段写成函数,这样可以减少代码体积,再一个修改代码的时候,我直接修改相应的函数就可以了,而不用一个地方一个地方的找。Javascript代码可以写到JS文件里,实现HTML页面和Javascript代码的分离。

技术选型(直白的说就是思考怎样利用Bootstrap框架里已有的东西,实现想要的效果):

1、首先调整布局,把信息框和同学框调整在一起,班级框显示在最上面。

显然,我们只需要把信息框所在的DIV和同学框的DIV放在同一个row的DIV里,并把修改跟栅格系统相关的class属性值”col-md”就可以实现。比如我们想让他们2:1的比例显示,那么信息框的class属性应该有col-md-8,而同学框的应该是col-md-4了。

值得注意的是,前端永远不可能是一步到位的开发,往往最开始的代码都不是我们想要的完美结果,需要细细调整。以下面为例,大家要学会自己搜,去尝试,去不断的调整(后面的调整,具体过程就不交代了):

我们修改完代码后,呈现的效果是下面这样的:

很明显的,上面每行只显示一个Class,有点浪费空间。下面的两个部分没有对齐。

把上面DIV组件的class属性里的”btn-group-vertical”删掉,把col-md-6添加到js代码里的tmp_button的class属性中。另外观察到这样设定了之后,第一行与第二行相比有奇怪的缩进:

毫无疑问,这种外观、布局类的变化跟CSS有关。这时候我们可以看一下元素具体的CSS。

以Chrome为例

在这个元素上点鼠标右键,选择Inspect,即审查,你会在右边的框里查到相应的代码。通过比对,我们发现是margin-left的问题,这个属性是bootstrap框架里默认的,从上层元素继承而来,有的为 -1px,有的为0px,我们只需要改成一样就可以了,比如都改成0px:

在js代码里的tmp_button,修改style属性,添加”margin-left:0px;”,有的人觉得字体居中不好看,可以添加text-align:left,设定按钮上的文字从左边起。

改正后的效果:

2、添加折叠按钮,让用户可以通过这个按钮隐藏/打开class框,点击classmate显示详细信息的时候,自动隐藏,以把大量的空间省给信息框来显示。

另外,我们可以给折叠按钮加一个好看的图标,参考http://v3.bootcss.com/components/#glyphicons的教程就可以轻松实现。

实现点击classmate显示详细信息的时候,自动隐藏,需要修改classmate按钮的点击事件,即相应的js代码。

我们查看Bootstrap折叠插件的用法http://www.runoob.com/bootstrap/bootstrap-collapse-plugin.html(这个更好,官方的这部分还没翻译好),查到下面的内容:

原来Class属性里的cllapse和in值控制了隐藏和显示功能,那我们只需要在按钮的click事件js代码里修改相应的要进行”显示/隐藏”操作的HTML元素的class属性就可以了,于是在classmate按钮的click函数里添加以下语句就可以了:

$("#collapseOne").attr("class","panel-collapse collapse");

3、修正”点击具体同学,显示其信息之后,没有处于被选中的状态”的bug。

我们通过查阅文档,http://www.runoob.com/bootstrap/bootstrap-button-plugin.html, 给button的data-toggle属性设置为”button”,可以使其变成点击后自动呈现被选中的状态。

所以我们给classmate的button添加属性data-toggle=”button”。

此时又出现了另一个问题,我想接下去点击其他classmate的时候,原来点的还是处于激活状态,怎么办?

通过查阅http://www.runoob.com/bootstrap/bootstrap-buttons.html, button的class赋值有active时,才会呈现被选中的状态,也就是说,上面的设定,也就是bootstrap做了这么一件事情:当data-toggle=”button”的按钮被点击时,自动把active添加到class里,此时呈现被选中状态,当再次被点击时,自动把active从class里移除,就呈现未被选中的状态。

也就是说,我们只要自己去做这个操作,比如在点击classmate时,我可以把所有的classmate按钮的active都从class属性里移除,这样点击完成后,只有我最新点击的Button处于active状态。

因此,只需要在classmate按钮的click函数里加入这样的语句:

$(".btn-classmate").attr("class","btn btn-default btn-classmate btn-info");

效果如下图:

4、美化按钮外观

找到bootstrap里CSS关于按钮的部分:

按照教程修改,我这里只是简单的修改了一下按钮的颜色,大家可以按照需求自己改。效果如下图:

5、 HTML页面和Javascript代码的分离

其实分为两步:

第一步:把javascript代码放到js文件里,并在HTML文件里链接上。

第二步:把javascript里复用,或者有明确功能的代码块,写进一个函数里,直接调用函数。

由于这两步都比较简单,任何学过编程语言的应该都会。我就不展开写了。

值得注意的是,在链接JS文件时,要注意顺序。

比如Bootstrap的JS文件,里面的代码是基于Jquery写的,用了很多Jquery的函数,所以Jquery的JS文件要在Bootstrap的JS文件之前声明链接。

同理,咱们的JS文件时基于Bootstrap,所以要在Bootstrap之后,不然代码就不起作用了

最后国际惯例,贴一下相关的代码:

getClassmate.html:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>getClassmate</title>

    <!-- Bootstrap -->
    <link href="css/bootstrap.min.css" rel="stylesheet">

    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
      <script src="//cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script>
      <script src="//cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->
    
    <style type="text/css">
        #btn-group-vertical-classes 
        {
        overflow-y:auto; 
        overflow-x:auto; 
        height:100px;
        }
        #btn-group-vertical-classmates 
        {
        overflow-y:auto; 
        overflow-x:auto; 
        height:500px;
        }
        button
        {
        text-overflow: ellipsis;
        overflow: hidden;
        border-radius: 0px;
        }
        #context_div
        {
        overflow-y:auto; 
        overflow-x:auto; 
        height:500px;
        }
    </style>
  </head>
  <body>
  <div class="container">
        <div class="panel panel-default">
 <div class="panel-heading">
 <h4 class="panel-title" style="text-align:right;">
 <a id="collapse_a" data-toggle="collapse" data-parent="#accordion" 
 href="#collapseOne">
 <span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span> Click this to show/hide class
 </a> 
 </h4>
 </div>
 <div id="collapseOne" class="panel-collapse collapse in">
 <div class="panel-body">
 <div class="row" id="div1-classes-classmates">
 <div class="btn-group-lg col-md-12" role="group" aria-label="..." id="btn-group-vertical-classes">
 <!-- where classes show-->
 </div>
 </div>
 </div>
 </div>
        </div>
        
        <br></br>
        <div class="row">
 <div class="btn-group-vertical btn-group-lg col-md-4 " id="btn-group-vertical-classmates" role="group" aria-label="..."> 
 <!-- where classmates show-->
 <button type="button" style="border-radius: 0px;" class="btn btn-default">Click class to show classmate.</button>
 </div>
 <div class="form-group">
 <div class="col-md-8">
 <div class="jumbotron" id="context_div">
 <p>Click classmate to show details.</p>
 </div>
 </div>
 </div>
        </div>
    </div>

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="js/jquery-3.0.0.min.js"></script> 
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script src="js/bootstrap.min.js"></script>
    <script src="js/script_getClassmate.js"></script>
  </body>
</html>

script_getClassmate.js:

$(document).ready(function{
        $.getJSON("resource/classmates.json",function(result){
 $.each(result, function(i, field){
 var tmp_button=$('<button type="button" style="border-radius: 0px; text-align:left; margin-left:0px" class="btn btn-default btn-class col-md-6 btn-success" data-toggle="button" chosen_state=0></button>').text(i);
 tmp_button.attr("title",i);
 $("#btn-group-vertical-classes").append(tmp_button);
 });
 $(".btn.btn-default.btn-class").click(function{
 var tmp_chosen=Number($(this).attr("chosen_state"))^1;
 $(this).attr("chosen_state",String(tmp_chosen));
 showClassmates(result);
 $(".btn.btn-default.btn-classmate").click(function{
 $(".btn-classmate").attr("class","btn btn-default btn-classmate btn-info");
 $("#collapseOne").attr("class","panel-collapse collapse");
 var selected_classmate=$(this).text;
 showClassmateDetail(result,selected_classmate);
 });
 });
        });
})

function showClassmates(result){
    $("#btn-group-vertical-classmates").empty;
    var chosen_list=new Array;
    
    $(".btn.btn-default.btn-class").filter(function{
        judgeflag=false;
        if($(this).attr("chosen_state")=="1"){
 judgeflag=true;
 chosen_list.push($(this).text);
        }
        return judgeflag; 
    });
    $.each(result,function(i,field){
        var chosen_list_x;
        for (chosen_list_x in chosen_list){
 if(chosen_list[chosen_list_x]==i){
 $.each(field,function(j,field2){
 var tmp_button=$('<button type="button" style="border-radius: 0px;" class="btn btn-default btn-classmate btn-info" data-toggle="button" chosen_state=0></button>').text(j);
 tmp_button.attr("title",j);
 $("#btn-group-vertical-classmates").append(tmp_button);
 });
 }
        }
    });
}

function showClassmateDetail(result,selected_classmate){
    var classmate_context_item;
    $("#context_div").empty;
    $.each(result,function(i,field){
        $.each(field,function(j,field2){
 if(j==selected_classmate){
 $.each(field2,function(k,field3){
 //alert(k);
 //alert(field3);
 classmate_context_item=k + ": " + field3;
 var tmp_p=$('<p></p>').text(classmate_context_item);
 $("#context_div").append(tmp_p);
 });
 }
        });
    });
}

classmates.json:

{
    "Class 001": {
        "Xiao Wang": {
 "Gender": "Male",
 "Age": "18",
 "Interest": "Football",
 "Hometown": "Shanghai",
 "Chinese": "78",
 "Math": "90",
 "English": "66",
 "Physics": "81",
 "Chemistry": "88",
 "History": "69",
 "Geography": "92"
        },
        "Xiao Li": {
 "Gender": "Male",
 "Age": "20",
 "Interest": "Basketball",
 "Hometown": "Beijing",
 "Chinese": "98",
 "Math": "77",
 "English": "97",
 "Physics": "72",
 "Chemistry": "73",
 "History": "88",
 "Geography": "81"
        }
    },
    "Class 002": {
        "Xiao Cai": {
 "Gender": "Female",
 "Age": "19",
 "Interest": "Dance",
 "Hometown": "Gaoxiong",
 "Chinese": "93",
 "Math": "80",
 "English": "92",
 "Physics": "82",
 "Chemistry": "77",
 "History": "89",
 "Geography": "83"
        }
    },
    "Class 003": {
        "Xiao Ma": {
 "Gender": "Male",
 "Age": "18",
 "Interest": "Reading",
 "Hometown": "Taibei",
 "Chinese": "91",
 "Math": "93",
 "English": "96",
 "Physics": "97",
 "Chemistry": "100",
 "History": "94",
 "Geography": "92"
        }
    },
    "Class 005": {
        "Xiao Zhang": {
 "Gender": "Male",
 "Age": "20",
 "Interest": "Running",
 "Hometown": "Guangzhou",
 "Chinese": "67",
 "Math": "70",
 "English": "66",
 "Physics": "80",
 "Chemistry": 68,
 "History": "79",
 "Geography": "93"
        }
    }
}

主要参考链接:

[1].Bootstrap中文官方文档

http://v3.bootcss.com/

[2].Runoob.com的Bootstrap教程

http://www.runoob.com/bootstrap/bootstrap-tutorial.html

章来自iCSS前端趣闻,https://mp.weixin.qq.com/s/Ct-QCJ02K7rhsYCy6kPiOQ

在 CSS 规范 Scroll-linked Animations[1] 中,推出了一个划时代的 CSS 功能。也就是 -- The @scroll-timeline[2] at-rule,直译过来就是滚动时间线

本文,就将带大家一探究竟,从入门到学会使用 CSS @scroll-timeline

何为 @scroll-timeline 滚动时间线?

什么是 @scroll-timeline 滚动时间线呢?

@scroll-timeline 能够设定一个动画的开始和结束由滚动容器内的滚动进度决定,而不是由时间决定。

意思是,我们可以定义一个动画效果,该动画的开始和结束可以通过容器的滚动来进行控制

示意 DEMO

再系统性学习语法之前,我们通过一个 DEMO,简单了解一下它的用法:

我们首先实现一个简单的字体 F 旋转动画

<div id="g-box">F</div>
#g-box {
    animation-name: rotate;
    animation-duration: 3s;
    animation-direction: alternate;
    animation-easing-function: linear;
}
@keyframes rotate {
    0% {
        transform: rotate(0);
    }
    100% {
        transform: rotate(360deg);
    }
}

正常而言,它是这样一个简单的动画:

接下来,我们把这个动画和 @scroll-timeline 相结合,需要把它放置到一个可滚动的容器中:

<div id="g-content">
    <div id="g-box">F</div>
</div>
#g-content {
    width: 300px;
    height: 170vh;
    background: #999;
}
#g-box {
    font-size: 150px;
    margin: 70vh auto 0;
    animation-name: rotate;
    animation-duration: 3s;
    animation-direction: alternate;
    animation-easing-function: linear;
    animation-timeline: box-rotate;
}
@keyframes rotate {
    0% {
        transform: rotate(0);
    }
    100% {
        transform: rotate(360deg);
    }
}
@scroll-timeline box-rotate {
    source: selector("#g-content");
}

这里,我们实现了一个可滚动容器 #g-content,它的高度是 170vh,也就是可视界面高度的 1.7 倍,并且把 #g-box 容器放置在一个距离顶部 70vh 高度的地方:

有意思的来了,我们设置的旋转动画不会自动开始,只有当我们向下滚动的时候,动画才会开始进行,实际效果 Gif:

CodePen Demo -- @scroll-timeline Demo[3]

看到这里,大家应该能够理解 @scroll-timeline 的作用及含义了,它赋予了 CSS 能够基于滚动条的滚动去控制动画行进的能力! Amazing!!

@scroll-timeline 语法介绍

接下来,我们先缓一缓,简单看一看 @scroll-timeline 的语法。

使用 @scroll-timeline,最核心的就是需要定义一个 @scroll-timeline 规则:

@scroll-timeline moveTimeline {
  source: selector("#g-content");
  orientation: vertical;
  scroll-offsets: 0px, 500px;
}

其中:

  • source:绑定触发滚动动画的滚动容器
    • source: auto:绑定到 Document,也就是全局 Windows 对象
    • source: selector("id-selector"),通过 selector(),内置一个 #id 选择器,选取一个可滚动容器
    • source: none:不指的滚动容器
  • orientation:设定滚动时间线的方向
    • orientation: auto:默认为 vertical,也就是竖直方向的滚动
    • orientation: vertical:竖直方向的滚动
    • orientation: horizontal:水平方向的滚动
    • orientation: block:不太常用,使用沿块轴的滚动位置,符合书写模式和方向性
    • orientation: inline:不太常用,使用沿内联轴的滚动位置,符合书写模式和方向性
  • scroll-offsets:滚动时间线的核心,设定在滚动的什么阶段,触发动画,可通过三种方式之一进行设置:
    • scroll-offsets: none 这意味着没有 scroll-offset 指定。
    • 由逗号分隔的[4]值列表确定。每个值都映射到animation-duration[5]。例如,如果 ananimation-duration 设置为 2s 且滚动偏移量为 0px, 30px, 100px,则在 1s 时,滚动偏移量将为 30px。
    • 第三种确定滚动偏移量的方法是使用元素偏移量。这意味着可以指定页面内的元素,其位置决定了滚动时间线以及要使用这些元素的哪个边缘。指定元素是使用 selector() 函数完成的,该函数接收元素的 id。边缘由关键字 start 或确定 end。可选的阈值的 0–1 可用于表示元素滚动中预期可见的百分比。

scroll-offsets 的理解会比较困难,我们稍后详述。

在设定了一个 @scroll-timeline 之后,我们只需要将它和动画绑定起来即可,通过 animation-timeline

@scroll-timeline moveTimeline {
  source: selector("#g-content");
  orientation: vertical;
  scroll-offsets: 0px, 500px;
}
div {
    animation-name: move;
    animation-duration: 3s;
    animation-timeline: moveTimeline;
}
@keyframes move{
    0% {
        transform: translate(0, 0);
    }
    100% {
        transform: translate(100%, 0);
    }
}

使用 @scroll-timeline 实现滚动进度指示器

之前在 不可思议的纯 CSS 滚动进度条效果[6] 一文中,我们介绍了一种使用渐变实现的纯 CSS 滚动进度指示器效果:

该方法有些小小的瑕疵。其中一个就是当滚动距离太短的时候,进度条右侧会有明显的斜边效果。

而有了 @scroll-timeline 之后,我们终于可以将滚动和动画这两个元素绑定起来,再实现滚动进度指示器,就已经非常轻松了:

<div id="g-container">
    <p>...文本内容...</p>
</div>
#g-container {
    width: 100vw;
}
#g-container::before {
    content: "";
    position: fixed;
    height: 5px;
    left: 0;
    top: 0;
    right: 0;
    background: #ffc107;
    animation-name: scale;
    animation-duration: 1s;
    animation-fill-mode: forwards;
    animation-timeline: box-rotate;
    transform-origin: 0 50%;
}

@keyframes scale {
    0% {
        transform: scaleX(0);
    }
    100% {
        transform: scaleX(1);
    }
}
@scroll-timeline box-rotate {
    source: auto;
    orientation: vertical;
}
  1. 我们在页面最上方,通过一个伪元素,实现一个占满屏幕 100%5px 高的进度条。正常而言是这样:

  1. 通过设定一个 transform: scaleX(0)transform: scaleX(1) 的动画,并且将它与 body 的滚动相绑定,即可得到滚动指示器,效果如下:

完整的代码,你可以戳这里:CodePen Demo - 使用 @scroll-timeline 实现滚动进度条[7]

使用 scroll-offsets 精确控制动画触发时机

大家可以再看看上面的 Gif 图,都有一个问题,就是动画的开始时间都是从滚动一开始就开始了,刚好在滚动结束时结束。那么如果我希望动画在滚动的特定阶段触发,那该怎么办呢?

这里,就需要借助 scroll-offsets,去更加精确的控制我们的动画。

在滚动过程中,我们可以将一个元素,划分为 3 个区域:

  • 滚动过程中,从上方视野盲区,进入视野
  • 滚动过程中,处于视野中
  • 滚动过程中,从视野中,进入下方视野盲区

在这里,我们就可以得到两个边界,上方边界,下方边界:

而对于上下两个边界,又会有两种状态。以上边界为例子,会有:

  • 元素刚刚开始进入可视区
  • 元素完全进入可视区

对于这两种状态,我们用 start 0start 1 表示,同理,下方的边界也可以用 end 0end 1 表示:

这里的 0 和 1 实际表示的是,元素滚动中预期可见的百分比

有了这些状态值,配合 scroll-offsets,我们就可以精确控制滚动动画的触发时间。

我们设定一个从左向右并且伴随透明度变化的动画,的看看下面几种情况:

  1. 滚动动画在元素从下方开始出现时开始,完全出现后截止。

动画运行范围:end 0 --> end 1

@keyframes move {
    0% {
        transform: translate(-100%, 0);
        opacity: 0;
    }
    100% {
        transform: translate(0, 0);
        opacity: 1;
    }
}
@scroll-timeline box-move {
    source: auto;
    orientation: "vertical";
    scroll-offsets: 
        selector(#g-box) end 0, 
        selector(#g-box) end 1;

    /* Legacy Descriptors Below: */
    start: selector(#g-box) end 0;
    end: selector(#g-box) end 1;
    time-range: 1s;
}
#g-box {
    animation-name: move;
    animation-duration: 3s;
    animation-fill-mode: both;
    animation-timeline: box-move;
}

效果如下:

  1. 滚动动画在元素从下方完全出现时开始,在滚动到上方即将离开屏幕后截止:

动画运行范围:end 1 --> start 1

// ...
@scroll-timeline box-move {
    source: auto;
    orientation: "vertical";
    scroll-offsets: 
        selector(#g-box) end 1, 
        selector(#g-box) start 1;

    /* Legacy Descriptors Below: */
    start: selector(#g-box) end 1;
    end: selector(#g-box) start 1;
    time-range: 1s;
}
// ...

效果如下:

  1. 滚动动画在元素滚动到上方即将离开屏幕后开始,完全离开屏幕后截止:

动画运行范围:start 1 --> start 0

// ...
@scroll-timeline box-move {
    source: auto;
    orientation: "vertical";
    scroll-offsets: 
        selector(#g-box) start 1, 
        selector(#g-box) start 0;

    /* Legacy Descriptors Below: */
    start: selector(#g-box) start 1;
    end: selector(#g-box) start 0;
    time-range: 1s;
}
// ...

效果如下:

掌握 scroll-offsets 的用法是灵活运用滚动时间线的关键,当然,在上面你还会看到 start: selector(#g-box) start 1end: selector(#g-box) start 0 这种写法,这是规范历史遗留问题,最新的规范已经使用了 scroll-offsets 去替代 start: end: 的写法。

把上述 3 种情况放在一起,再比较比较:

完整的代码,你可以戳这里:CodePen Demo - @scroll-timeline Demo | element-based offset[8]

使用 @scroll-timeline 实现各类效果

在能够掌握 @scroll-timeline 的各个语法之后,我们就可以开始使用它创造各种动画效果了。

譬如如下的,滚动内容不断划入:

代码较长,可以戳这里,来自 bramus 的 Codepen CodePen Demo -- Fly-in Contact List (CSS @scroll-timeline version)[9]

甚至可以结合 scroll-snap-type 制作一些全屏滚动的大屏特效动画:

要知道,这在以前,是完全不可能利用纯 CSS 实现的。完整的代码你可以戳这里:CodePen Demo -- CSS Scroll-Timeline Split Screen Carousel[10]

简而言之,任何动画效果,如今,都可以和滚动相结合起来,甚至乎是配合 SVG 元素也不例外,这里我还简单改造了一下之前的一个 SVG 线条动画:

完整的代码你可以戳这里:CodePen Demo -- SVG Text Line Effect | Scroll Timeline[11]

@scroll-timeline 的实验室特性与特性检测

@scroll-timeline 虽好,目前仍处于实验室特性时间,Chrome 从 85 版本开始支持,但是默认是关闭的。

开启该特性需要:

  1. 浏览器 URL 框输入 chrome://flags
  2. 开启 #enable-experimental-web-platform-features

特性检测

基于目前的兼容性问题,我们可以通过浏览器的特性检测 @supports 语法,来渐进增强使用该功能。

特性检测的语法也非常简单:

@supports (animation-timeline: works) {
    @scroll-timeline list-item-1 {
 source: selector(#list-view);
 start: selector(#list-item-1) end 0;
 end: selector(#list-item-1) end 1;
        scroll-offsets:
            selector(#list-item-1) end 0,
            selector(#list-item-1) end 1
        ;
 time-range: 1s;
    }
    // ...
}

通过 @supports (animation-timeline: works) {} 可以判断浏览器是否支持 @scroll-timeline

最后

目前关于 @scroll-timeline 的相关介绍还非常少,但是它确是能够改变 CSS 动画的一个非常大的革新。随着兼容性的逐渐普及,未来势必会在 CSS 中占据一席之地。

本文到此结束,希望对你有帮助 :)

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

参考资料

[1]Scroll-linked Animations: https://drafts.csswg.org/scroll-animations-1/

[2]@scroll-timeline: https://drafts.csswg.org/scroll-animations/#at-ruledef-scroll-timeline

[3]CodePen Demo -- @scroll-timeline Demo: https://codepen.io/Chokcoco/pen/JjOZMaQ

[4]: https://developer.mozilla.org/en-US/docs/Web/CSS/length-percentage

[5]animation-duration: https://developer.mozilla.org/en-US/docs/Web/CSS/animation-duration

[6]不可思议的纯 CSS 滚动进度条效果: https://www.cnblogs.com/coco1s/p/10244168.html

[7]CodePen Demo - 使用 @scroll-timeline 实现滚动进度条: https://codepen.io/Chokcoco/pen/eYeKLMj

[8]CodePen Demo - @scroll-timeline Demo | element-based offset: https://codepen.io/Chokcoco/pen/qBVyqob

[9]CodePen Demo -- Fly-in Contact List (CSS @scroll-timeline version): https://codepen.io/bramus/pen/bGwJVzg

[10]CodePen Demo -- CSS Scroll-Timeline Split Screen Carousel: https://codepen.io/Chokcoco/pen/QWOrPdM

[11]CodePen Demo -- SVG Text Line Effect | Scroll Timeline: https://codepen.io/Chokcoco/pen/wvPxbRm