Bootstrap 简单认识之Carousel组件

周麒
2023-12-01

Carousel(幻灯片)组件

一、简介

  1. 往往用于首页轮流展示不同的图片,电商网站常见;
  2. 在IE8、9下没有图片滑动动画效果;

二、样式

下面给出的样式类并不完整

  1. 从下面可以看出图片切换动画消耗的时间:

    /*from _carousel.scss*/
    .carousel-item {
         /*...省略*/
         @include transition($carousel-transition);
    }
    /*from _variables.scss*/
    $carousel-transition: transform .6s ease-in-out !default;
  2. 跳转按钮(indicators)使用了flex布局,且通过 ::after::before 伪类增加了可点击空间

    .carousel-indicators {
    /*省略*/
    display: flex;
    justify-content: center;
    li {
        position: relative;
        /*剩余空间由多个元素平分*/
        flex: 1 0 auto;
        /*每个元素最长30px*/
        max-width: 30px;
    
        /*使用before和after伪类将indicators的可点击空间放大*/
        &::before {
            position: absolute;
            top: -10px;
            left: 0;
            display: inline-block;
            width: 100%;
            height: 10px;
            content: "";
        }
    
        &::after {
            /*同上*/
        }
    }
    }

    想要使用的话,只需要在与 carusel-inner 同级的区域使用 ol 或是 ul 元素指定与图片数量一样多的 li 标签就可以,如下,我有2个图片:

    <ul class="carousel-indicators">
     <li data-target="#carousel-example" data-slide-to="0" class="active"></li>
     <li data-target="#carousel-example" data-slide-to="1"></li>
    </ul>
  3. 图片切换动画实现,这里要注意的是:

    • display:none; 的元素即使有css3的动画属性(transition),在从 display:none 到 其他非 none 值的过程中,是不会有任何动画效果的。虽然可以用 visibility 属性来取代,但 visibility:hidden 属性元素仍然占据它原来所在的位置
    • js操作css类的过程如下(例子):
      1. 下一个元素 添加 .carousel-item-next
      2. 当前元素 添加 .carousel-item-left
      3. 下一个元素添加 .carousel-item-left
      4. 动画开始—–动画结束——————–
      5. 下一个元素移除 .carousel-item-next .carousel-item-left,之后添加类 .active
      6. 当前元素移除 .carousel-item-next .carousel-item-left .active
    /*每个图片的初始css属性*/
    .carousel-item {
     /*...省略*/
     display: none;
     transition: transform .6s ease-in-out;
    }
    
    /*单独出现可以让元素出现在当前元素的右侧*/
    .carousel-item-next {
     display: flex;
     position: absolute;
     top: 0;
     transform: translate3d(100%, 0, 0);
    }
    
    /*组合出现下面类时,即当前元素开始向左移动动画*/
    .active.carousel-item-left {
     transform: translate3d(-100%, 0, 0);
    }
    
    /*下一个元素开始回归初始位置,也向左滑动*/
    .carousel-item-next.carousel-item-left{
     transform: translate3d(0, 0, 0);
    }
    
    /*动画结束后,下一个元素被添加active类*/
    .carousel-item.active{
     display: flex;
    }
    
    /*而之前的当前元素只剩下初始的 carousel-item 类*/
    .carousel-item {}
  4. 关于此组件的前后切换按钮,也提供了默认的样式。

    .carousel-control-prev,
    .carousel-control-next {
     /*左右导航栏高度为父元素高度*/
     position: absolute; top: 0; bottom: 0;
     /*让容器内元素横向、纵向居中*/
     display: flex; align-items: center; justify-content: center;
     // Bootstrap注释道: 不能对此元素使用动画,因为在一个动画中启动另一个动画Webkit会令该动画失效,也即可能导致Carousel的动画被取消
    }
    
    /*还有分别设置两个按钮的left和right分别为0,这里就省略了*/
    /*关于两个按钮里的内容,bootstrap也已经提供了两个类,类使用在span标签即可*/
    .carousel-control-prev-icon {
    background-image: $carousel-control-prev-icon-bg;
    }
    .carousel-control-next-icon {
    background-image: $carousel-control-next-icon-bg;
    }

    默认样式就是在组件的两侧各有一块区域可以点击,简单的使用方式如下,记得要为按钮的 data-target 属性或是 href 属性指定控制的组件唯一 id(class当然也可,但id更合适):

    <a class="carousel-control-prev" href="#carousel-example" data-slide="prev">
    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
    </a>
    <a class="carousel-control-next" href="#carousel-example" data-slide="next">
    <span class="carousel-control-next-icon" aria-hidden="true"></span>
    </a>

三、脚本

js主要控制的定时器或事件:

  • Carousel组件图片自动轮转
  • indicators元素可以指定index跳转至某个图片
  • prev, next按钮可以前后翻页
  • 键盘方向键控制页面左右跳转
  • 鼠标悬停停止轮转

下面依旧贴上此组件的代码梗概:

const Default = {
  // 这个东西如果不指定false的话,且要执行图片切换动画,至少要是600ms,因为图片切换动画的时间是600ms
  // 如果指定小于600ms,页面依旧能正常的一张图一张图切换,但这个版本会不停的报异常,因为有 isSliding 这个标识指示当前是否在图片切换动画过程中
  interval : 5000,
  // 是否允许键盘控制
  keyboard : true,
  // 用于prev,next按钮(按钮如果不用js控制一般指定 data-slide='prev/next' 来控制前后轮换,此处slide属性即从这里获得)
  slide    : false,
  // 是否鼠标悬停即暂停
  pause    : 'hover',
  // 是否循环轮转
  wrap     : true
}

class Carousel {

  constructor(element, config) {
    this._items             = null
    // this._config.interval的值决定了carousel是否自动轮转
    this._interval          = null
    this._activeElement     = null
    // 是否暂停(鼠标悬停事件不影响此属性,想象一个场景,js代码控制Carousel停止后,鼠标悬停接着移开,如果此时导致Carousel继续轮转就不合情理了)
    this._isPaused          = false
    // 是否在一个图片切换到另一个图片的动画过程中(注意是在滑动,非静止状态)
    this._isSliding         = false

    this._config            = this._getConfig(config)
    this._element           = $(element)[0]
    this._indicatorsElement = $(this._element).find(Selector.INDICATORS)[0]
    // 添加鼠标,键盘事件
    this._addEventListeners()
  }

  // public
  // 切换到下个图片
  next() {}
  // 在页面可见的时候才进行轮转(通过document.hidden属性判断)
  nextWhenVisible() {}
  // 切换到上一张图片
  prev() {}
  // 暂停
  // 什么时候会传event(或参数不为false)? 只有在 mouseenter事件被触发时
  pause(event) {}
  // 什么时候会传event(或参数不为false)?   
  // 1. mouseleave事件    
  // 2. 图片切换动画过程中pause函数被触发(具体场景就是mouseenter事件在图片切换过程中被触发)
  // 如果js调用了pause,那么不能在mouseleave事件后重新cycle,所以鼠标事件无法改变 isPaused的值
  cycle(event) {}
  // 跳转到哪一张
  to(index) {}
  // 销毁
  dispose() {}


  // static
  // 参数config可以从两个地方: 1. window.load传入的$('xx').data(),为一个obj   2. js手动调用传入的数据,可以为 string 或是 number
  // 1. objecg: 表示修改默认配置   2. string: 表示执行某个动作   3. number: 表示滑倒第几章图片
  // 手动调用/页面load后加载
  static _jQueryInterface(config) {}
  // 处理prev,next按钮,以及触发to函数的按钮
  static _dataApiClickHandler(event) {}
}

$(document)
  .on(Event.CLICK_DATA_API,                 // click.bs.carousel.data-api
      Selector.DATA_SLIDE,                  // '[data-slide], [data-slide-to]'
      Carousel._dataApiClickHandler)

// 页面完全(包括图像等媒体文件)加载完毕后执行
$(window).on(Event.LOAD_DATA_API, () => {   // load.bs.carousel.data-api
  $(Selector.DATA_RIDE)                     // [data-ride="carousel"]
    .each(function () {
        const $carousel = $(this)
        Carousel._jQueryInterface.call($carousel, $carousel.data())
    })
})

以上public的函数都是可以用js手动调用的,下面来简要分析一下每一个公共函数的代码:
function prev / next
此俩函数用于控制图片前后轮转,代码类似,只取其一:

prev() {
  // 如果当前图片正在滚动中,此函数不作为
  if (this._isSliding) {
    throw new Error('Carousel is sliding')
  }
  // 调用_slide函数达到目的,此函数后面分析
  this._slide(Direction.PREV)
}

function to
这个函数用于直接跳转到某张图片

to(index) {
  // 如果当前正在切换图片,那么等图片切换完成后,再滚动到指定的图片
  if (this._isSliding) {
    $(this._element).one(Event.SLID, () => this.to(index))
    return
  }

  // 如果指定的图片就是当前图片,那么重新从当前图片开始一个计时器
  if (activeIndex === index) {
    this.pause()
    this.cycle()
    return
  }

  // 根据当前元素和目标元素的index大小判断向左还是向右滚动
  const direction = index > activeIndex ?
    Direction.NEXT :
    Direction.PREV

  // 调用_slide函数达到目的
  this._slide(direction, this._items[index])
}

可见,如果在图片切换期间点击跳转某个图片,那么会在跳转动画完成后跳转到目的图片;如果跳转的图片为当前图片,那么重置计时器;其他则根据index大小判断是向前还是后滑动。

function pause
此函数用于停止轮转,除了手动js调用,在鼠标触发mouseenter事件时也会调用此函数

pause(event) {
  // 鼠标事件不改变此属性
  if (!event){ this._isPaused = true }
  // 如果carousel组件有'.carousel-item-next, .carousel-item-prev'类的元素,表示正在图片切换过程中,此时触发pause函数,会立即触发 slid.bs.carousel事件()
  if ($(this._element).find(Selector.NEXT_PREV)[0] &&
    Util.supportsTransitionEnd()) {
    Util.triggerTransitionEnd(this._element)
    // 此处未能理解。。。
    this.cycle(true)
  }

  // 移除定时器
  clearInterval(this._interval)
  this._interval = null
}

总的来说,此函数用于移除定时器,如果是手动js调用,那么还会设立标志位表示此时组件已经被暂停。

function cycle
此函数用于控制组件开始轮转,除了手动js调用,在鼠标触发mouseleave事件时也会调用

cycle(event) {
  if (!event){ this._isPaused = false }
  // 移除定时器,所以在每次调用
  if (this._interval) {
    clearInterval(this._interval)
    this._interval = null
  }
  // 如果此时没有被暂停,且时间间隔不为空,那么设置定时器继续轮转
  if (this._config.interval && !this._isPaused) {
    this._interval = setInterval(
      (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),
      this._config.interval
    )
  }
}

根据上面 pausecycle 的分析,可以看出来 _isPaused 是为了解决鼠标悬停事件和手动调用 pause 事件冲突的标志位; pause 也会在执行图片切换动画的情况下保证 slid.bs.carousel 事件的触发; cycle 也会尽可能在页面可见时进行页面的重绘操作,避免浪费性能。

function _slide
此函数是轮转的核心函数,用于处理图片的切换工作以及相应函数的触发

_slide(direction, element) {
  // ...

  // 当前是否处于自动轮转状态
  const isCycling = Boolean(this._interval)

  // 根据滑动方向获取动画需要的 class(Carousel是纯css动画)
  if (direction === Direction.NEXT) {
    directionalClassName = ClassName.LEFT
    orderClassName = ClassName.NEXT
    eventDirectionName = Direction.LEFT
  } else { 
    // ... 
  }
  // 如果下一个元素是当前元素(wrap为false,不循环),那么直接返回
  if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {
    this._isSliding = false
    return
  }
  // 在真正开始滑动前触发 slide.bs.carousel函数
  const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)
  // 该回调函数中如果调用preventdefault函数将不再继续下面的工作
  if (slideEvent.isDefaultPrevented()) {
    return
  }
  // 标识开始滑动
  this._isSliding = true

  // 未知作用,重置?
  if (isCycling) { this.pause() }

  // 在Carousel元素拥有 slide 类的时候,幻灯片轮转会有动画
  if (Util.supportsTransitionEnd() && $(this._element).hasClass(ClassName.SLIDE))
    // 更新元素 class
    // 根据 transitionEnd事件,注册slid.bs.carousel函数
  } else {
    // 更新元素 class
    // 触发slid.bs.carousel函数
  }

  if (isCycling) { this.cycle() }
}

回顾以上提及的所有函数,其实并不复杂,不过有几处 pause 函数的调用让我有些不知所措,感觉没有调用 pause
差别也不大。。。对这些函数的分析,其实没有多大的作用,但可以对组件的一些特殊情况有所了解,不至于遇见时手忙脚乱

使用细节

  1. 组件最外层必须使用到 carousel
  2. 如果想要组件图片轮转时有动画效果,必须在组件最外层使用 slide
  3. 要使用 indicators,可以使用js为元素触发事件 $('#xxx').carousel(n),也可以在元素属性中添加 data-slide-to="n" 其中n为整型
  4. 要使用 prev/next 按钮,可以使用js为元素触发 prev / next 事件,也可以在元素中添加 data-slide="prev" 这样
  5. 要注意,上面两个使用的元素要么有 data-target 属性指向控制的 carousel 组件,要么使用 href 属性指向元素。
  6. 一个普通情况完整带动画的timeline如下:

    Created with Raphaël 2.1.0 Start cycle设置定时器 pause暂停 End 定时器timeout 异步开始过渡动画 yes no

    可以看出,由于动画是异步的,不阻塞线程,第二个图片的定时器在第一张图片切换到第二张图片动画开始的时候开始计时,也即,第一张图片停留n秒,图片切换动画持续m秒,那么第二张图片停留n-m秒

 类似资料: