element 源码学习六 —— Carousel 走马灯学习

简单使用

走马灯功能在展示图片时经常用到,而 element 中提供了 Carousel 组件。出于好奇学习一下它的实现原理。
具体的功能详情请查阅官方文档
关于组件属性,该组件提供了组件高度、索引、指示器、切换时间等一众配置,这个只要动手试一遍都能理解。
关于事件,提供了一个 change 事件。可以通过 v-on:change="changeFun" 事件绑定来监听。该事件传递了两个参数:当前页索引和前一页索引。参考源码中的 $emit 方法:

  watch: {
    activeIndex(val, oldVal) {
      this.resetItemPosition(oldVal);
      this.$emit('change', val, oldVal);
    },
  },

关于方法,组件提供了三个方法用于操作组件页面的切换。使用方法是通过 $ref 子组件引用来访问子组件,执行其方法。

                // 假设为 el-carousel 设置了 ref="car"
                // setActiveItem    手动切换幻灯片 需要切换的幻灯片的索引,从 0 开始;或相应 el-carousel-item 的 name 属性值
                // prev 切换至上一张幻灯片
                // next 切换至下一张幻灯片
                pre() {
                    this.$refs.car.prev()
                },
                next() {
                    this.$refs.car.next()
                },
                first() {
                    this.$refs.car.setActiveItem(0)
                }

源码位置

Carousel 的源码位于 package/carousel/ 目录下。源码目录如下:

源码目录

简单说下目录内容:

  • item.vue carousel-item 组件代码
  • main.vue carousel 组件代码
  • _index.js 导出 carousel-item 和 carousel
  • cooking.conf.js cooking 配置
  • index.js 导出 carousel 组件
  • package.json 组件信息
  • index.js 导出 carousel-item 组件

其实主要就是两个 .vue 文件,其他都是些配置、导出的功能文件,这里可以忽略不看~

源码解析

老规矩,我们通过几个问题切入去看源代码。

1. 基本原理:页面切换 + 指示器 + 切换按钮的实现

页面切换

看了一下 DOM 的 Elements 排列,发现页面切换使用的是 transform 2D 转换和 transition 过渡。
一般页面切换其实是几个页面使用 translateX 进行位移切换。

一般页面结构

而卡片变化切换同时使用了 translateX 位移和 scale 缩放实现中间一张卡最大,左右卡片小的效果的。


卡片化

而卡片的层叠使用 z-index 值得大小来实现。
所以,逻辑计算只需要根据当前显示页面计算下每个页面的位移值。而 carousel-item 中也的确有计算的逻辑:

    // item.vue
    // index 当前 item 索引
    // activeIndex 激活的 item 索引
    // oldIndex 之前 item 索引
    translateItem(index, activeIndex, oldIndex) {
      // 获取父元素宽度 https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth
      const parentWidth = this.$parent.$el.offsetWidth;
      // 获取 item 页面数量
      const length = this.$parent.items.length;
      // 判断是否需要过渡动画 class
      // .el-carousel__item.is-animating {
      //  transition: transform 0.4s ease-in-out;
      // }
      if (this.$parent.type !== "card" && oldIndex !== undefined) {
        this.animating = index === activeIndex || index === oldIndex;
      }
      // 处理 index
      if (index !== activeIndex && length > 2) {
        index = this.processIndex(index, activeIndex, length);
      }
      if (this.$parent.type === "card") {
        // 卡片化
        this.inStage = Math.round(Math.abs(index - activeIndex)) <= 1; // 激活组件及其前后组件定义 cursor: pointer; z-index: 1;
        this.active = index === activeIndex; // 激活 class
        // 计算卡片化偏移量
        this.translate = this.calculateTranslate(
          index,
          activeIndex,
          parentWidth
        );
        // 激活卡片不缩放,其他卡片缩放为 0.83
        this.scale = this.active ? 1 : CARD_SCALE;
      } else {
        // 非卡片化
        this.active = index === activeIndex; // 激活 class
        this.translate = parentWidth * (index - activeIndex); // 计算位移 根据父组件宽度计算
      }
      // 计算完后显示
      this.ready = true;
    },

指示器

指示器就是多个 button 组成的横向列表,根据当前显示页面修改某个指示器背景颜色。

    <ul
      class="el-carousel__indicators"
      v-if="indicatorPosition !== 'none'"
      :class="{ 'el-carousel__indicators--labels': hasLabel, 'el-carousel__indicators--outside': indicatorPosition === 'outside' || type === 'card' }">
      <li
        v-for="(item, index) in items"
        class="el-carousel__indicator"
        :class="{ 'is-active': index === activeIndex }"
        @mouseenter="throttledIndicatorHover(index)"
        @click.stop="handleIndicatorClick(index)">
        <button class="el-carousel__button"><span v-if="hasLabel">{{ item.label }}</span></button>
      </li>

切换按钮

切换按钮其实就是两个 absolute 的按钮,点击实现页面切换即可。

      <transition name="carousel-arrow-left">
        <button
          type="button"
          v-if="arrow !== 'never'"
          v-show="arrow === 'always' || hover"
          @mouseenter="handleButtonEnter('left')"
          @mouseleave="handleButtonLeave"
          @click.stop="throttledArrowClick(activeIndex - 1)"
          class="el-carousel__arrow el-carousel__arrow--left">
          <i class="el-icon-arrow-left"></i>
        </button>
      </transition>
      <transition name="carousel-arrow-right">
        <button
          type="button"
          v-if="arrow !== 'never'"
          v-show="arrow === 'always' || hover"
          @mouseenter="handleButtonEnter('right')"
          @mouseleave="handleButtonLeave"
          @click.stop="throttledArrowClick(activeIndex + 1)"
          class="el-carousel__arrow el-carousel__arrow--right">
          <i class="el-icon-arrow-right"></i>
        </button>
      </transition>

这里要注意的就是按钮外包裹了 <transition> 标签来实现按钮进入和离开的过渡效果。

2. 页面切换动画如何实现?

其实在基本原理里面都提到了,可以从源码中找到两个样式:

# 内联样式
element.style {
  transform: translateX(888.56px) scale(0.83);
}

# 过渡样式
.is-animating {
  transition: transform 0.4s ease-in-out;
}

静态情况下,使用 transitionX 维持页面位置,显示当前页面。过渡行为时,加入 0.4 秒的 transition 过渡效果。

3. 卡片化如何实现效果?

卡片化切换其实和一般的切换差不多,只是显示卡片从一张变为三张。中间的卡片 z-index 为 2,而左右的卡片 z-index 为 1,从而实现中间卡片在前面的样式。而计算三张卡片位置的方法如下:

    calculateTranslate(index, activeIndex, parentWidth) {
      if (this.inStage) {
        return parentWidth * ((2 - CARD_SCALE) * (index - activeIndex) + 1) / 4;
      } else if (index < activeIndex) {
        return -(1 + CARD_SCALE) * parentWidth / 4;
      } else {
        return (3 + CARD_SCALE) * parentWidth / 4;
      }
    },

而卡片的宽度为容器宽度的一半~左右两边的卡片缩放了 83% 。

.el-carousel__item--card {
  width: 50%;
  transition: transform 0.4s ease-in-out;
}

element.style {
  transform: translateX(-39.44px) scale(0.83)
}

总结下:

  • 显示三张卡片。
  • 三张卡片高度跟随容器,宽度为容器的 50%。
  • 左右两张卡片缩放了 83% 大小。
  • 左右两张卡片 z-index 为 1;中间卡片 z-index 为 2。

4. 按钮出现和消失的效果如何实现?

使用了 vue 的 transition 标签来实现。具体效果为:

.el-carousel__arrow {
  border: none;
  outline: none;
  padding: 0;
  margin: 0;
  height: 36px;
  width: 36px;
  cursor: pointer;
  transition: 0.3s;
  border-radius: 50%;
  background-color: rgba(31, 45, 61, 0.11);
  color: #fff;
  position: absolute;
  top: 50%;
  z-index: 10;
  transform: translateY(-50%);
  text-align: center;
  font-size: 12px;
}

.carousel-arrow-left-enter,
.carousel-arrow-left-leave-active {
  transform: translateY(-50%) translateX(-10px);
  opacity: 0;
}

.carousel-arrow-right-enter,
.carousel-arrow-right-leave-active {
  transform: translateY(-50%) translateX(10px);
  opacity: 0;
}

实现的效果为:左边箭头向从左边 10px 位置进入和离开,而右边箭头从右边 10px 位置进入和离开。而 translateY 是为了垂直居中。

5. 如何实现自动切换功能?

使用 setInterval 方法来实现定时向后切换页面。

    playSlides() {
      if (this.activeIndex < this.items.length - 1) {
        this.activeIndex++;
      } else {
        // 如果是最后一页则跳转到第一页
        this.activeIndex = 0;
      }
    },
    // 取消 timer
    pauseTimer() {
      clearInterval(this.timer);
    },
    // 开始 timer
    startTimer() {
      if (this.interval <= 0 || !this.autoplay) return;
      this.timer = setInterval(this.playSlides, this.interval);
    },

自己实现个走马灯玩玩~

学以致用,这里写个简单的 demo,实现下走马灯功能。
例一:走马灯效果
例二:卡片化走马灯效果
其中,例一和例二实现了走马灯和卡片化的自动切换、手动切换、切换效果。
这里留了一个问题待解决:

  • 走马灯中第一页和最后一页的切换如何做到更好?

这些问题将会尽快解决!

最后

至此,我们了解了走马灯的实现原理,以及一些小功能的实现。最后也用两个例子证明了我们的分析。

Vue 实验室

《Vue 实验室》 至今也写了不少了,作者将在把最后几篇计划中的博客写完后,以当前的知识认知水平重新改进《Vue 实验室》往期的文章,并整理到 gitbook 中便于读者阅读,敬请期待。

打个广告

链家上海研发中心招聘前端、后端、测试。
机会不多,需要内推机会的请将简历发送至 dingxiaojie001@ke.com

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 目录 UI组件 开发框架 实用库 服务端 辅助工具 应用实例 Demo示例 UI组件 element★13489 ...
    余生社会阅读 19,660评论 7 233
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • 符号加载 鉴于符号加载对调试的重要意义,先介绍下windbg符号加载路径的设置格式: .sympath local...
    纵横而乐阅读 802评论 0 1
  • 在你安静的目光里 我学会了沉静 在你自然的生长中 我行走得越来越自然 生长得快活 你安静的让我惊憾 叫人不再贪念一...
    龙青阅读 149评论 0 5