在 Vue 上创建类似 Tinder 的滑动用户界面
你有没有想过那种类似 Tinder 的左右滑动式用户体验是如何实现的?几天前,我很好奇。我之前主要做后端开发,所以对于我这个外行来说,这种东西真的太神奇了。
我很好奇,像我这样水平一般的开发者,要做出那样酷炫的东西到底有多难?
侦察
在着手新项目时,收集信息始终是我的第一步。我不会贸然尝试任何代码,而是先上网搜索。毕竟,肯定有比我聪明的人早就想到过这一点了。
果不其然,在搜索“vue 刷卡”之后,谷歌给我的第一个结果就是这个(我真幸运)。
这是Mateusz Rybczonek在css-tricks 上发表的一篇关于使用.interact.js
这篇文章对可滑动组件的构建方式的解释比我好得多。更重要的是,他提取了相关功能并将其作为vue2-interact发布到了 npm 上(开源万岁!)。
虽然文章解释了所有功能的工作原理,但对我们来说,它本质上只是样板代码。我们需要的是实际使用提取出来的功能。这就是为什么这篇文章Vue2InteractDraggable如此宝贵,所有繁重的工作都已经完成,我们只需要弄清楚如何在自己的项目中使用它。
实验
现在,我只需要实际操作一下。文档写得很清楚。我们先从最简单的代码开始:
<template>
<section class="container">
<div class="fixed-center">
<Vue2InteractDraggable
:interact-out-of-sight-x-coordinate="500"
:interact-max-rotation="15"
:interact-x-threshold="200"
:interact-y-threshold="200"
class="rounded-borders shadow-10 card">
<div class="card__main">
</div>
</Vue2InteractDraggable>
</div>
</section>
</template>
<script>
import { Vue2InteractDraggable } from 'vue2-interact'
export default {
name: 'SwipeableCards',
components: { Vue2InteractDraggable }
}
</script>
没什么特别的,它只是在屏幕中央显示一个粉色方框,我可以拖动它。
好,好,好,好。一切正常。既然已经确认了这一点,现在是时候考虑一下我接下来想要完成的事情了。
为了展示我所期望的用户交互方式,我将其简化为以下要求。
- 检测卡片是否被拖出视野,如果是,则将其隐藏。
- 将可拖动的卡片堆叠在一起。
- 能够控制滑动操作(通过按钮以编程方式触发)。
问题一:检测和隐藏
问题 1 很简单,Vue2InteractDraggable组件在超出限制drag*时会发出事件interact-out-of-sight-*-coordinate,并且还会自动隐藏组件。
问题二:堆叠卡片
问题二相当棘手。Vue2InteractDraggable从技术上讲,它只是一个可拖动的组件。从用户界面角度来看,堆叠这些组件可以很简单,只需使用 CSS 结合 `<div>` z-index、width`<span>` 和box-shadow`<span>` 来模拟深度即可。但是滑动组件还能正常工作吗?嗯,我可以停留pointer-events在最底部的卡片上,以避免任何副作用。
我们来试一试。我将使用一个数组,每次向右滑动时弹出数组的第一个元素。这听起来合理,对吧?
以下是目前为止的代码:
<template>
<section class="container">
<div>
<Vue2InteractDraggable
v-for="(card, index) in cards"
:key="index"
:interact-out-of-sight-x-coordinate="500"
:interact-max-rotation="15"
:interact-x-threshold="200"
:interact-y-threshold="200"
@draggedRight="right"
class="rounded-borders card fixed fixed--center"
:class="{
'card--top': index === 0
}">
<div class="flex flex--center" style="height: 100%">
<h1>{{card.text}}</h1>
</div>
</Vue2InteractDraggable>
</div>
</section>
</template>
<script>
import { Vue2InteractDraggable } from 'vue2-interact'
export default {
name: 'SwipeableCards',
components: { Vue2InteractDraggable },
data() {
return {
cards: [
{ text: 'one' },
{ text: 'two' },
{ text: 'three' },
]
}
},
methods: {
right() {
setTimeout(() => this.cards.shift(), 300);
}
}
}
</script>
<style lang="scss" scoped>
.container {
background: #eceff1;
width: 100%;
height: 100vh;
}
.flex {
display: flex;
&--center {
align-items: center;
justify-content: center;
}
}
.fixed {
position: fixed;
&--center {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
.rounded-borders {
border-radius: 2rem
}
.card {
pointer-events: none;
width: 300px;
height: 400px;
&:nth-child(1) {
background: pink;
z-index: 3;
}
&:nth-child(2) {
z-index: 2;
background: red;
top: 52%;
}
&:nth-child(3) {
z-index: 1;
background: green;
top: 54%;
}
&--top {
pointer-events: auto !important;
}
}
</style>
现在我的情况是这样的: 嗯,完全失败了。不知何故,当第一张卡片的事件触发时,第二张卡片的事件也触发了。您可以看到,在我第一次滑动之后,DOM 中只剩下两张卡片,但我们看不到第二张卡片,因为它旋转到屏幕外了。在开发者工具中,我们可以看到,在滑动第一张卡片之后,第二张卡片的 transform 动画样式被应用了(您可以通过开发者工具禁用该样式后,可以看到它又出现了)。
即使我尝试简单地将卡片排列成行,问题仍然存在。我不确定这是为什么。我一定是遗漏了什么,或者这是组件Vue2InteractDraggable本身的问题。
目前我有两个选择:我可以继续调试,深入研究实际实现,或许可以追溯原作者是如何提取功能的,找出不同之处,查看 GitHub 仓库中是否有类似问题,并尝试从中寻找答案;或者思考另一种方法来实现同样的功能,以后再回头处理。
我选择后者。另一种方法或许也能达到同样的效果。现在没必要好高骛远。我也可以以后再来处理这件事。
我们继续吧。
之前的结果让我思考……如果每次使用多个Vue2InteractDraggable组件都会出错,为什么不干脆避免使用多个组件,只用一个呢?毕竟,我一次只拖拽一张卡片。为什么不直接使用同一张卡片,然后根据需要替换内容呢?结合其他一些 CSS 技巧,我觉得这或许可行。
让我们编写一段最简单的代码来验证我的假设:
<template>
<section class="container">
<div class="fixed fixed--center" style="z-index: 3">
<Vue2InteractDraggable
v-if="isVisible"
:interact-out-of-sight-x-coordinate="500"
:interact-max-rotation="15"
:interact-x-threshold="200"
:interact-y-threshold="200"
@draggedRight="right"
class="rounded-borders card card--one">
<div class="flex flex--center" style="height: 100%">
<h1>{{current.text}}</h1>
</div>
</Vue2InteractDraggable>
</div>
<div
class="rounded-borders card card--two fixed fixed--center"
style="z-index: 2">
<div class="flex flex--center" style="height: 100%">
<h1>test</h1>
</div>
</div>
<div
class="rounded-borders card card--three fixed fixed--center"
style="z-index: 1">
<div class="flex flex--center" style="height: 100%">
<h1>test</h1>
</div>
</div>
</section>
</template>
<script>
import { Vue2InteractDraggable } from 'vue2-interact'
export default {
name: 'SwipeableCards',
components: { Vue2InteractDraggable },
data() {
return {
isVisible: true,
index: 0,
cards: [
{ text: 'one' },
{ text: 'two' },
{ text: 'three' },
]
}
},
computed: {
current() {
return this.cards[this.index]
}
},
methods: {
right() {
setTimeout(() => this.isVisible = false, 200)
setTimeout(() => {
this.index++
this.isVisible = true
}, 300)
}
}
}
</script>
<style lang="scss" scoped>
.container {
background: #eceff1;
width: 100%;
height: 100vh;
}
.flex {
display: flex;
&--center {
align-items: center;
justify-items: center;
justify-content: center;
}
}
.fixed {
position: fixed;
&--center {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
.rounded-borders {
border-radius: 12px;
}
.card {
width: 300px;
height: 400px;
color: white;
&--one {
background-color: pink;
}
&--two {
background-color: red;
width: 280px;
top: 51%;
}
&--three {
background-color: orange;
width: 260px;
top: 51.8%;
}
}
</style>
这似乎比我最初的方法更简单。我重复使用同一个Vue2InteractDraggable实例,而不是为数组中的每个元素都实例化一个。反正我们也不需要把所有牌都叠起来,只需要维持这种错觉就行了。
也就是说,为了进一步增强视觉效果,我应该把下一个元素的内容显示在第一个元素后面的卡片上,像这样:
<template>
<section class="container">
<div class="fixed fixed--center" style="z-index: 3">
<Vue2InteractDraggable
v-if="isVisible"
:interact-out-of-sight-x-coordinate="500"
:interact-max-rotation="15"
:interact-x-threshold="200"
:interact-y-threshold="200"
@draggedRight="right"
class="rounded-borders card card--one">
<div class="flex flex--center" style="height: 100%">
<h1>{{current.text}}</h1>
</div>
</Vue2InteractDraggable>
</div>
<div
v-if="next"
class="rounded-borders card card--two fixed fixed--center"
style="z-index: 2">
<div class="flex flex--center" style="height: 100%">
<h1>{{next.text}}</h1>
</div>
</div>
<div
v-if="index + 2 < cards.length"
class="rounded-borders card card--three fixed fixed--center"
style="z-index: 1">
<div class="flex flex--center" style="height: 100%">
<h1>test</h1>
</div>
</div>
</section>
</template>
<script>
import { Vue2InteractDraggable } from 'vue2-interact'
export default {
name: 'SwipeableCards',
components: { Vue2InteractDraggable },
data() {
return {
isVisible: true,
index: 0,
cards: [
{ text: 'one' },
{ text: 'two' },
{ text: 'three' },
]
}
},
computed: {
current() {
return this.cards[this.index]
},
next() {
return this.cards[this.index + 1]
}
},
methods: {
right() {
setTimeout(() => this.isVisible = false, 200)
setTimeout(() => {
this.index++
this.isVisible = true
}, 300)
}
}
}
</script>
当我更改最上面的卡片时,我也会相应地隐藏最下面的虚拟卡片,这是个经典的卡片替换技巧。
效果相当不错。向上移动卡片时隐藏虚拟卡片index也十分有效。如果我们用图片代替text彩色div卡片,效果可能会更好。我们还可以添加一些微妙的过渡动画,让最底部的卡片变成最顶部的卡片,从而进一步增强视觉效果。不过这些以后再说,我们先来完成最后一块拼图。
问题 3:通过按钮点击触发滑动操作
幸运的是,这也很简单。vue2-interact它暴露了一个EventBus可以用来触发拖拽/滑动操作的接口。根据文档,只需向该interact-event-bus-events属性提供一个包含所需事件的对象,然后使用该对象InteractEventBus来触发所需的操作即可。
<template>
<Vue2InteractDraggable
@draggedLeft="draggedLeft"
:interact-event-bus-events="interactEventBusEvents"
v-if="isShowing"
class="card">
<div>
<h3 class="cardTitle">Drag me!</h3>
</div>
</Vue2InteractDraggable>
<BaseButton @click="dragLeft" label="⇦" />
</template>
<script>
import { Vue2InteractDraggable, InteractEventBus } from 'vue2-interact'
const INTERACT_DRAGGED_LEFT = 'INTERACT_DRAGGED_LEFT';
export default {
components: { Vue2InteractDraggable },
data() {
return {
isShowing: true,
interactEventBusEvents: {
draggedLeft: INTERACT_DRAGGED_LEFT,
},
};
},
methods: {
dragLeft() {
InteractEventBus.$emit(INTERACT_DRAGGED_LEFT);
},
}
};
</script>
本质上,我们只是告诉组件draggedLeft每次我们在某个地方时$emit触发该事件。INTERACT_DRAGGED_LEFTInteractEventBus
这样一来,我认为我们已经具备了开始将所有东西整合起来所需的一切条件。
把所有东西整合起来
我从 Unsplash 下载了一些图片,并根据我的需求缩小了尺寸。我将这些图片作为数组的值,这样就可以替换文本并移除背景颜色。我还发现,如果改变卡片堆叠的方向,就能更轻松地增强视觉效果。我没有将它们向上堆叠,而是斜向堆叠。这样一来,我的过渡动画就可以非常简单,只需在切换时将第二张卡片的 x 和 y 坐标平移到第一张卡片上即可。我就不赘述所有步骤了,我想你已经明白我的意思了,剩下的就留给你自己想象吧。
加了一些 CSS 魔法,比如渐变、阴影之类的效果,再配上谷歌字体和 Material 图标,最后就成了这样: 瞧瞧,Kittynder!猫咪版的 Tinder。这名字听起来合理吗?我也不知道。不过这倒是个玩梗的好机会。如果这真的是个 app,我家猫估计会直接挠Katrina 的爪子,它们年纪差不多,我觉得它们肯定很合得来。
你可以在这个 GitHub 仓库kittynder上查看完整代码。我在 Netlify 上发布了一个演示:kittynder.netlify.com。我强烈建议你在移动设备上查看。
结语
在这个简单的活动中,我意识到如今构建类似 Tinder 的滑动式用户界面是多么容易。我只用了不到两个小时就完成了。现在,互联网上的工具和资源比以往任何时候都多,足以让你构建很多东西,那些以前看起来遥不可及的东西。这就是开源社区的力量。这也是我开始写这类教程的原因之一。这是我回馈社区的方式。我可能只是一个平庸的开发者,但我的思考过程和解决问题的方法仍然可能对初学者(以及未来的我,因为一年后我可能会完全忘记)有所帮助。
下一步?
当然,这离正式发布还差得远。我的 CSS 水平很差,你最好考虑使用像tailwind.css这样的工具,预缓存图片,检查浏览器兼容性等等。不过,这倒是个不错的练习。一步一步来,你最终会成功的。只要搜索、阅读、实践就好。
实际上,我正在使用Quasar 框架在一个稍大一些的个人项目中实现类似的功能,但这又是另一个故事了。
实用链接
本文最初发表于我的个人网站。
文章来源:https://dev.to/vycoder/creating-a-tinder-like-swipe-ui-on-vue-4aa4



