实现抖音 “视频无限滑动“效果

前言

在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅
不禁感叹道 "垃圾抖音,费我时间,毁我青春😅"


这是我的 模仿抖音 系列文章的第二篇,本文将一步步实现抖音首页 视频无限滑动 的效果,干货满满

第一篇:200行代码实现类似Swiper.js的轮播组件
第三篇:Vue 路由使用介绍以及添加转场动画
第四篇:Vue 有条件路由缓存,就像传统新闻网站一样
第五篇:Github Actions 部署 Pages、同步到 Gitee、翻译 README 、 打包 docker 镜像

如果您对滑动原理不太熟悉,推荐先看我的这篇文章:200行代码实现类似Swiper.js的轮播组件

最终效果

在线预览:dy.ttentau.top/

Github地址:https://github.com/zyronon/douyin

源码:SlideVerticalInfinite.vue

实现原理

无限滑动的原理和虚拟滚动的原理差不多,要保持 SlideList 里面永远只有 NSlideItem,就要在滑动时不断的删除和增加 SlideItem
滑动时调整 SlideList 的偏移量 translateY 的值,以及列表里那几个 SlideItemtop 值,就可以了

为什么要调整 SlideList 的偏移量 translateY 的值同时还要调整 SlideItemtop 值呢?
因为 translateY 只是将整个列表移动,如果我们列表里面的元素是固定的,不会变多和减少,那么没关系,只调整 translateY 值就可以了,上滑了几页就减几页的高度,下滑同理

但是如果整个列表向前移动了一页,同时前面的 SlideItem 也少了一个,,那么最终效果就是移动了两页…因为 塌陷 了一页
这显然不是我们想要的,所以我们还需要同时调整 SlideItemtop 值,加上前面少的 SlideItem 的高度,这样才能显示出正常的内容

步骤

定义


virtualTotal:页面中同时存在多少个 SlideItem,默认为 5

//页面中同时存在多少个SlideItem
virtualTotal: {
  type: Number,
  default: () => 5
},

设置这个值可以让外部组件使用时传入,毕竟每个人的需求不同,有的要求同时存在 10 条,有的要求同时存在 5 条即可。
不过同时存在的数量越大,使用体验就越好,即使用户快速滑动,我们依然有时间处理。
如果只同时存在 5 条,用户只需要快速滑动两次就到底了(因为屏幕中显示第 3 条,刚开始除外),我们可能来不及添加新的视频到最后


render:渲染函数,SlideItem内显示什么由render返回值决定

render: {
  type: Function,
  default: () => {
    return null
  }
},

之所以要设定这个值,是因为抖音首页可不只有视频,还有图集、推荐用户、广告等内容,所以我们不能写死显示视频。
最好是定义一个方法,外部去实现,我们内部去调用,拿到返回值,添加到 SlideList


list:数据列表,外部传入

list: {
  type: Array,
  default: () => {
    return []
  }
},

我们从 list 中取出数据,然后调用并传给 render 函数,将其返回值插入到 SlideList中

初始化


watch(
  () => props.list,
  (newVal, oldVal) => {
    //新数据长度比老数据长度小,说明是刷新
    if (newVal.length < oldVal.length) {
      //从list中取出数据,然后调用并传给render函数,将其返回值插入到SlideList中
      insertContent()
    } else {
      //没数据就直接插入
      if (oldVal.length === 0) {
        insertContent()
      } else {
        // 走到这里,说明是通过接口加载了下一页的数据,
        // 为了在用户快速滑动时,无需频繁等待请求接口加载数据,给用户更好的使用体验
        // 这里额外加载3条数据。所以此刻,html里面有原本的5个加新增的3个,一共8个dom
        // 用户往下滑动时只删除前面多余的dom,等滑动到临界值(virtualTotal/2+1)时,再去执行新增逻辑
      }
    }
  }
)

watch 监听 list 是因为它一开始不一定有值,通过接口请求之后才有值
同时当我们下滑 加载更多 时,也会触发接口请求新的数据,用 watch 可以在有新数据时,多添加几条到 SlideList 的最后面,这样用户快速滑动也不怕了

如何滑动

这里就不再赘述,参考我的这篇文章:200行代码实现类似Swiper.js的轮播组件

滑动结束

判断滑动的方向

当我们向上滑动时,需要删除最前面的 dom ,然后在最后面添加一个 dom
下滑时反之

slideTouchEnd(e, state, canNext, (isNext) => {
  if (props.list.length > props.virtualTotal) {
    //手指往上滑(即列表展示下一条视频)
    if (isNext) {
      //删除最前面的 `dom` ,然后在最后面添加一个 `dom`  
    } else {
      //删除最后面的 `dom` ,然后在最前面添加一个 `dom`  
    }
  }
})

手指往上滑(即列表展示下一条视频)

  • 首先判断是否要加载更多,快到列表末尾时就要加载更多数据了
  • 再判断是否符合 腾挪 的条件,即当前位置要大于 half,且小于列表长度减 half
  • 在最后面添加一个 dom
  • 删除最前面的 dom
  • 将所有 dom 设置为最新的 top 值(原因前面有讲,因为删除了最前面的 dom,导致塌陷一页,所以要加上删除 dom 的高度)
let half = (props.virtualTotal - 1) / 2

//删除最前面的 `dom` ,然后在最后面添加一个 `dom`  
if (state.localIndex > props.list.length - props.virtualTotal && state.localIndex > half) {
  emit('loadMore')
}

//是否符合 `腾挪` 的条件
if (state.localIndex > half && state.localIndex < props.list.length - half) {
  //在最后面添加一个 `dom`  
  let addItemIndex = state.localIndex + half
  let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addItemIndex}']`)
  if (!res) {
    slideListEl.value.appendChild(getInsEl(props.list[addItemIndex], addItemIndex))
  }

  //删除最前面的 `dom` 
  let index = slideListEl.value
    .querySelector(`.${itemClassName}:first-child`)
    .getAttribute('data-index')
  appInsMap.get(Number(index)).unmount()

  slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
    _css(item, 'top', (state.localIndex - half) * state.wrapper.height)
  })
}

手指往下滑(即列表展示上一条视频)

逻辑和上滑都差不多,不过是反着来而已

  • 再判断是否符合 腾挪 的条件,和上面反着
  • 在最前面添加一个 dom
  • 删除最后面的 dom
  • 将所有 dom 设置为最新的 top
//删除最后面的 `dom` ,然后在最前面添加一个 `dom`
if (state.localIndex >= half && state.localIndex < props.list.length - (half + 1)) {
  let addIndex = state.localIndex - half
  if (addIndex >= 0) {
    let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addIndex}']`)
    if (!res) {
      slideListEl.value.prepend(getInsEl(props.list[addIndex], addIndex))
    }
  }
  let index = slideListEl.value
    .querySelector(`.${itemClassName}:last-child`)
    .getAttribute('data-index')
  appInsMap.get(Number(index)).unmount()

  slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
    _css(item, 'top', (state.localIndex - half) * state.wrapper.height)
  })
}

其他问题

为什么不直接用 v-for直接生成 SlideItem 呢?

如果内容不是视频就可以。要删除或者新增时,直接操作 list 数据源,这样省事多了

如果内容是视频,修改 list 时,Vue 会快速的替换 dom,正在播放的视频,突然一下从头开始播放了😅😅😅

如何获取 Vue 组件的最终 dom

有两种方式,各有利弊

  • Vuerender 方法
    • 优点:只是渲染一个 VNode 而已,理论上讲内存消耗更少。
    • 缺点:但我在开发中,用了这个方法,任何修改都会刷新页面,有点难蚌😅
  • VuecreateApp 方法再创建一个 Vue 的实例
    • 和上面相反😅
import { createApp, onMounted, reactive, ref, render as vueRender, watch } from 'vue'

/**
 * 获取Vue组件渲染之后的dom元素
 * @param item
 * @param index
 * @param play
 */
function getInsEl(item, index, play = false) {
  // console.log('index', cloneDeep(item), index, play)
  let slideVNode = props.render(item, index, play, props.uniqueId)
  const parent = document.createElement('div')
  //TODO 打包到线上时用这个,这个在开发时任何修改都会刷新页面
  if (import.meta.env.PROD) {
    parent.classList.add('slide-item')
    parent.setAttribute('data-index', index)
    //将Vue组件渲染到一个div上
    vueRender(slideVNode, parent)
    appInsMap.set(index, {
      unmount: () => {
        vueRender(null, parent)
        parent.remove()
      }
    })
    return parent
  } else {
    //创建一个新的Vue实例,并挂载到一个div上
    const app = createApp({
      render() {
        return <SlideItem data-index={index}>{slideVNode}</SlideItem>
      }
    })
    const ins = app.mount(parent)
    appInsMap.set(index, app)
    return ins.$el
  }
}

总结

原理其实并不难。主要是一开始可能会用 v-for 去弄,折腾半天发现不行。v-for 不行,就只能想想怎么把 Vue 组件搞到 html 里面去,又去研究如何获取 Vue 组件的最终 dom,又查了半天资料,Vue 官方文档也不写,还得去翻 api ,麻了

结束

以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注我的公众号 前端张余让,我会更新更多实用的前端知识与技巧,期待与你共同成长~

  • 28
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Uniapp是一款基于Vue.js开发的跨平台应用框架,可以用于开发Android、iOS及Web等多个平台的应用程序。要实现仿抖音视频滑动效果,可以按照以下步骤进行操作: 1. 创建一个新的Uniapp项目,并引入需要的库和组件。 2. 在主页面中,使用Scroll View组件作为视频列表的滚动容器,设置滑动方向为水平。 3. 在Scroll View组件内添加一个横向滑动的容器(例如swiper组件),用于存放每个视频播放窗口。 4. 定义一个数据源(例如videoList),用于存放所有视频的信息,包括视频的路径、封面图等。 5. 遍历videoList数据,动态生成每个视频播放窗口,并设置其宽度和高度。 6. 实现左右滑动效果,可以使用Touch事件监听滑动动作,根据触摸开始和结束时的坐标差值来判断滑动方向。 7. 根据滑动的距离和方向,计算视频播放窗口的位置偏移量,实现滑动过程中窗口的平移效果。 8. 针对不同的滑动行为(如滑动到下一个视频滑动到上一个视频),可以通过监听滑动结束事件,根据当前播放窗口的位置和滑动方向,判断所需播放视频,并更新页面。 总结起来,要实现uniapp仿抖音视频滑动效果,需要使用Scroll View和swiper组件来构建滑动容器和视频播放窗口,根据滑动事件和滑动方向来控制视频窗口的位置和播放顺序。同时,需要借助数据源(videoList)来存放视频的信息,实现动态生成和更新视频窗口。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值