仿知乎叠加卡片动画切换效果
感谢npro提供的getTouchPoint、getElSize函数方法!
感谢林老板提供的技术指导!
感谢Andy提供的教程贴!https://learnku.com/articles/50593
1、模板文件,这里将其封装为flipcard组件,同时,比较重要的还有css
XML/HTML Code复制内容到剪贴板
- <template>
- <div class="n-full-height n-bg-primary article-answer">
- <!-- >>主页面 -->
- <div class="article-answer-stack">
- <flipcard ref="stack" :pages="stackList" :stackInit="stackInit" @click="handleStackClicked"></flipcard>
- </div>
- <div class="article-answer-btns n-flex-row n-wrap-nowrap n-align-center n-justify-center">
- <div class="n-flex-column n-align-center" @click="handleStackPrev">
- <div class="article-answer-btns-box n-flex-row n-justify-center n-align-center">
- <image class="article-answer-btns-img" src="/static/img/icon-buganxingqu.png"></image>
- </div>
- <text class="article-answer-btns-text">不感兴趣</text>
- </div>
- <div class="n-flex-column n-align-center" @click="handleStackNext">
- <div class="article-answer-btns-box n-flex-row n-justify-center n-align-center">
- <image class="article-answer-btns-img" src="/static/img/icon-shaohou.png"></image>
- </div>
- <text class="article-answer-btns-text">稍后答</text>
- </div>
- </div>
- </div>
- </template>
重要的css,卡片的高度和宽度,在这里定义,在组件里面,都是100%:
XML/HTML Code复制内容到剪贴板
- <script>
- import flipcard from './components/flipcard.vue'
- export default {
- components:{
- flipcard
- },
- data() {
- return {
- stackList:[
- {
- title:"2021高考作文难吗11111?",
- tag: "最近83人提问过这个问题11111",
- problem: "关于陈红军壮烈牺牲的报道,陈红军的“发小”王煦辉这几天看了一遍又一遍,从小玩到大的伙伴如今已经天人相隔,让他至今也无法接受。在同学眼中,陈红军打小就是一位品学兼优的好子,就是这个家境贫寒的农村娃,考上了城里的高?",
- author: {
- id: 1,
- name: "一颗棒棒糖",
- avatar: "/static/avatar/1.jpg",
- },
- comment_num: 5160, // 评论数
- dig_num: 5160, // 顶数
- forward_num: 5160, // 转发数
- attention_num: 30, // 关注数
- },
- {
- title:"2021高考作文难吗?22222",
- tag: "最近83人提问过这个问题22222",
- problem: "关于陈红军壮烈牺牲的报道,陈红军的“发小”王煦辉这几天看了一遍又一遍,从小玩到大的伙伴如今已经天人相隔,让他至今也无法接受。在同学眼中,陈红军打小就是一位品学兼优的好子,就是这个家境贫寒的农村娃,考上了城里的高?",
- author: {
- id: 1,
- name: "两颗棒棒糖",
- avatar: "/static/avatar/2.jpg",
- },
- comment_num: 5160, // 评论数
- dig_num: 5160, // 顶数
- forward_num: 5160, // 转发数
- attention_num: 30, // 关注数
- },
- {
- title:"2021高考作文难吗?33333",
- tag: "最近83人提问过这个问题33333",
- problem: "关于陈红军壮烈牺牲的报道,陈红军的“发小”王煦辉这几天看了一遍又一遍,从小玩到大的伙伴如今已经天人相隔,让他至今也无法接受。在同学眼中,陈红军打小就是一位品学兼优的好子,就是这个家境贫寒的农村娃,考上了城里的高?",
- author: {
- id: 1,
- name: "三颗棒棒糖",
- avatar: "/static/avatar/4.jpg",
- },
- comment_num: 5160, // 评论数
- dig_num: 5160, // 顶数
- forward_num: 5160, // 转发数
- attention_num: 30, // 关注数
- },
- {
- title:"2021高考作文难吗?44444",
- tag: "最近83人提问过这个问题44444",
- problem: "关于陈红军壮烈牺牲的报道,陈红军的“发小”王煦辉这几天看了一遍又一遍,从小玩到大的伙伴如今已经天人相隔,让他至今也无法接受。在同学眼中,陈红军打小就是一位品学兼优的好子,就是这个家境贫寒的农村娃,考上了城里的高?",
- author: {
- id: 1,
- name: "四颗棒棒糖",
- avatar: "/static/avatar/5.jpg",
- },
- comment_num: 5160, // 评论数
- dig_num: 5160, // 顶数
- forward_num: 5160, // 转发数
- attention_num: 30, // 关注数
- }
- ],
- stackInit: {
- currentPage:0,
- visible: 3
- }
- }
- },
- methods: {
- handleStackPrev(){
- this.$refs["stack"].prev()
- },
- handleStackNext(){
- this.$refs["stack"].next()
- }
- }
- }
- </script>
- <style lang="scss">
- page {
- background-color: #477FE7;
- }
- .article-answer {
- &-stack{
- width: 690rpx;
- height: 795rpx;
- position: relative;
- // z-index: 9;
- list-style: none;
- pointer-events: none;
- padding: 30rpx;
- }
- }
- </style>
2、组件部分,这里我试了一下,兼容H5和APP,我在写当前交互的时候是在APP项目上,H5调试,测试都OK,直接复制就好
XML/HTML Code复制内容到剪贴板
- <template>
- <div class="flipcard" id="flipcard" ref="flipcard">
- <div class="flipcard-item" v-for="(item, index) in pages" :key="index"
- :style="[transformIndex(index),transform(index)]" @touchstart="touchstart" @touchmove="touchmove"
- @touchend="touchend" @touchcancel="touchend" @transitionend="onTransitionEnd(index,'transitionend')">
- <div class="flipcard-item-stack-info">
- <div class="t3"><text class="t3-text">{{item.tag}}</text></div>
- <div class="problem">
- <text class="problem-text">{{item.problem}}</text>
- </div>
- <div class="desc n-flex-row n-wrap-nowrap n-align-center">
- <text class="desc-text">{{item.attention_num}}人关注</text>
- <text class="desc-text">·</text>
- <text class="desc-text">{{item.comment_num}}人回答</text>
- </div>
- <div class="n-flex-row n-wrap-nowrap n-align-center n-position-absolute bottom-box">
- <image class="avatar" :src="item.author.avatar" mode="aspectFill"></image>
- <text class="name">{{item.author.name}}</text>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script>
- /**
- * @Desc Vue仿探探/知乎|Tinder卡片滑动FlipCard
- * @Time by 2021-08-24
- * @About Q:398927951
- */
- import {
- getTouchPoint,
- getElSize
- } from '@/nPro/utils/element.js'
- export default {
- props: {
- pages: {
- type: Array,
- default: []
- },
- stackInit: {
- type: Object,
- default: {}
- },
- },
- data() {
- return {
- el: {
- },
- basicdata: {
- start: {},
- end: {}
- },
- temporaryData: {
- offsetY: '',
- poswidth: 0,
- posheight: 0,
- lastPosWidth: '',
- lastPosHeight: '',
- lastZindex: '',
- rotate: 0,
- lastRotate: 0,
- visible: this.stackInit.visible || 3, // 展示几个
- tracking: false,
- animation: false,
- currentPage: this.stackInit.currentPage || 0, // 当前第几个
- opacity: 1,
- lastOpacity: 0,
- swipe: false,
- zIndex: 10
- }
- }
- },
- computed: {
- // 划出面积比例
- offsetRatio(e) {
- let width = this.el.width
- let height = this.el.height
- let offsetWidth = width - Math.abs(this.temporaryData.poswidth)
- let offsetHeight = height - Math.abs(this.temporaryData.posheight)
- let ratio = 1 - (offsetWidth * offsetHeight) / (width * height) || 0
- return ratio > 1 ? 1 : ratio
- },
- // 划出宽度比例
- offsetWidthRatio() {
- let width = this.el.offsetWidth
- let offsetWidth = width - Math.abs(this.temporaryData.poswidth)
- let ratio = 1 - offsetWidth / width || 0
- return ratio
- }
- },
- created() {
- this.$nextTick(async e => {
- this.el = await getElSize('flipcard', this)
- })
- },
- methods: {
- touchstart(e) {
- if (this.temporaryData.tracking) {
- return
- }
- // 是否为touch
- if (e.type === 'touchstart') {
- if (e.touches.length > 1) {
- this.temporaryData.tracking = false
- return
- } else {
- // 记录起始位置
- const point = getTouchPoint(e)
- // console.log("记录起始位置",point);
- this.basicdata.start.t = new Date().getTime()
- this.basicdata.start.x = point.x
- this.basicdata.start.y = point.y
- this.basicdata.end.x = point.x
- this.basicdata.end.y = point.y
- // offsetY在touch事件中没有,只能自己计算
- this.temporaryData.offsetY = point.y - this.el.top
- // console.log(this.temporaryData.offsetY)
- }
- // pc操作
- } else {
- console.log("pc记录起始位置");
- uni.showToast({
- title: '无效操作',
- icon: 'none'
- });
- // this.basicdata.start.t = new Date().getTime()
- // this.basicdata.start.x = e.clientX
- // this.basicdata.start.y = e.clientY
- // this.basicdata.end.x = e.clientX
- // this.basicdata.end.y = e.clientY
- // this.temporaryData.offsetY = e.offsetY
- }
- this.temporaryData.tracking = true
- this.temporaryData.animation = false
- },
- touchmove(e) {
- // 记录滑动位置
- if (this.temporaryData.tracking && !this.temporaryData.animation) {
- const point = getTouchPoint(e)
- if (e.type === 'touchmove') {
- e.preventDefault()
- this.basicdata.end.x = point.x
- this.basicdata.end.y = point.y
- } else {
- e.preventDefault()
- this.basicdata.end.x = point.x
- this.basicdata.end.y = point.y
- }
- // 计算滑动值
- thisthis.temporaryData.poswidth = this.basicdata.end.x - this.basicdata.start.x
- thisthis.temporaryData.posheight = this.basicdata.end.y - this.basicdata.start.y
- let rotateDirection = this.rotateDirection()
- let angleRatio = this.angleRatio()
- this.temporaryData.rotate = rotateDirection * this.offsetWidthRatio * 15 * angleRatio
- }
- },
- touchend(e, index) {
- this.temporaryData.tracking = false
- this.temporaryData.animation = true
- // 滑动结束,触发判断
- // 判断划出面积是否大于0.4
- if (this.offsetRatio >= 0.4) {
- // 计算划出后最终位置
- let ratio = Math.abs(this.temporaryData.posheight / this.temporaryData.poswidth)
- thisthis.temporaryData.poswidth = this.temporaryData.poswidth >= 0 ? this.temporaryData.poswidth + 200 :
- this.temporaryData.poswidth - 200
- thisthis.temporaryData.posheight = this.temporaryData.posheight >= 0 ? Math.abs(this.temporaryData
- .poswidth * ratio) : -Math.abs(this.temporaryData.poswidth * ratio)
- this.temporaryData.opacity = 0
- this.temporaryData.swipe = true
- this.nextTick()
- // 不满足条件则滑入
- } else {
- this.temporaryData.poswidth = 0
- this.temporaryData.posheight = 0
- this.temporaryData.swipe = false
- this.temporaryData.rotate = 0
- }
- },
- nextTick() {
- // 记录最终滑动距离
- thisthis.temporaryData.lastPosWidth = this.temporaryData.poswidth
- thisthis.temporaryData.lastPosHeight = this.temporaryData.posheight
- thisthis.temporaryData.lastRotate = this.temporaryData.rotate
- this.temporaryData.lastZindex = 20
- // 循环currentPage
- thisthis.temporaryData.currentPage = this.temporaryData.currentPage === this.pages.length - 1 ? 0 : this
- .temporaryData.currentPage + 1
- // currentPage切换,整体dom进行变化,把第一层滑动置最低
- this.$nextTick(() => {
- this.temporaryData.poswidth = 0
- this.temporaryData.posheight = 0
- this.temporaryData.opacity = 1
- this.temporaryData.rotate = 0
- })
- },
- onTransitionEnd(index, log) {
- let lastPage = this.temporaryData.currentPage === 0 ? this.pages.length - 1 : this.temporaryData
- .currentPage - 1
- // dom发生变化正在执行的动画滑动序列已经变为上一层
- if (this.temporaryData.swipe && index === lastPage) {
- this.temporaryData.animation = true
- this.temporaryData.lastPosWidth = 0
- this.temporaryData.lastPosHeight = 0
- this.temporaryData.lastOpacity = 0
- this.temporaryData.lastRotate = 0
- this.temporaryData.swipe = false
- this.temporaryData.lastZindex = -1
- }
- },
- prev() {
- this.temporaryData.tracking = false
- this.temporaryData.animation = true
- // 计算划出后最终位置
- let width = this.el.width
- this.temporaryData.poswidth = -width
- this.temporaryData.posheight = 0
- this.temporaryData.opacity = 0
- this.temporaryData.rotate = '-3'
- this.temporaryData.swipe = true
- this.nextTick()
- },
- next() {
- this.temporaryData.tracking = false
- this.temporaryData.animation = true
- // 计算划出后最终位置
- let width = this.el.width
- this.temporaryData.poswidth = width
- this.temporaryData.posheight = 0
- this.temporaryData.opacity = 0
- this.temporaryData.rotate = '3'
- this.temporaryData.swipe = true
- this.nextTick()
- },
- rotateDirection() {
- if (this.temporaryData.poswidth <= 0) {
- return -1
- } else {
- return 1
- }
- },
- angleRatio() {
- let height = this.el.height
- let offsetY = this.temporaryData.offsetY
- let ratio = -1 * (2 * offsetY / height - 1)
- return ratio || 0
- },
- inStack(index, currentPage) {
- let stack = []
- let visible = this.temporaryData.visible
- let length = this.pages.length
- for (let i = 0; i < visible; i++) {
- if (currentPage + i < length) {
- stack.push(currentPage + i)
- } else {
- stack.push(currentPage + i - length)
- }
- }
- return stack.indexOf(index) >= 0
- },
- // 非首页样式切换
- transform(index) {
- let currentPage = this.temporaryData.currentPage
- let length = this.pages.length
- let lastPage = currentPage === 0 ? this.pages.length - 1 : currentPage - 1
- let style = {}
- let visible = this.temporaryData.visible
- if (index === this.temporaryData.currentPage) {
- return
- }
- if (this.inStack(index, currentPage)) {
- let perIndex = index - currentPage > 0 ? index - currentPage : index - currentPage + length
- style['opacity'] = '1'
- style['transform'] = 'translate3D(0,0,' + -1 * 60 * (perIndex - this.offsetRatio) + 'px' + ')'
- style['zIndex'] = visible - perIndex
- if (!this.temporaryData.tracking) {
- style['transitionTimingFunction'] = 'ease'
- style['transitionDuration'] = 300 + 'ms'
- }
- } else if (index === lastPage) {
- style['transform'] = 'translate3D(' + this.temporaryData.lastPosWidth + 'px' + ',' + this.temporaryData
- .lastPosHeight + 'px' + ',0px) ' + 'rotate(' + this.temporaryData.lastRotate + 'deg)'
- style['opacity'] = this.temporaryData.lastOpacity
- style['zIndex'] = this.temporaryData.lastZindex
- style['transitionTimingFunction'] = 'ease'
- style['transitionDuration'] = 300 + 'ms'
- } else {
- style['zIndex'] = '-1'
- style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')'
- }
- return style
- },
- // 首页样式切换
- transformIndex(index) {
- if (index === this.temporaryData.currentPage) {
- let style = {}
- style['transform'] = 'translate3D(' + this.temporaryData.poswidth + 'px' + ',' + this.temporaryData
- .posheight + 'px' + ',0px) ' + 'rotate(' + this.temporaryData.rotate + 'deg)'
- style['opacity'] = this.temporaryData.opacity
- style['zIndex'] = 10
- if (this.temporaryData.animation) {
- style['transitionTimingFunction'] = 'ease'
- style['transitionDuration'] = (this.temporaryData.animation ? 300 : 0) + 'ms'
- }
- return style
- }
- },
- }
- }
- </script>
- <style lang="less" scoped>
- .flipcard {
- width: 100%;
- height: 100%;
- position: relative;
- perspective: 1000px;
- perspective-origin: 50% 150%;
- -webkit-perspective: 1000px;
- -webkit-perspective-origin: 50% 150%;
- margin: 0;
- padding: 0;
- &-item {
- background: #fff;
- height: 100%;
- width: 100%;
- border-radius: 30rpx;
- overflow: hidden;
- position: absolute;
- opacity: 0;
- display: -webkit-flex;
- display: flex;
- -webkit-flex-direction: column;
- flex-direction: column;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- pointer-events: auto;
- padding: 30rpx;
- /* #ifndef APP-NVUE */
- box-sizing: border-box;
- /* #endif */
- box-shadow: 0px 2rpx 30rpx 0px rgba(94, 114, 153, 0.3);
- &-stack-info {
- .t3 {
- padding: 0 0 25rpx 0;
- .t3-text {
- font-size: 26rpx;
- background-color: #ECECEC;
- padding: 8rpx 18rpx;
- color: #999999;
- border-radius: 10rpx;
- }
- }
- .problem {
- height: 180rpx;
- overflow: hidden;
- text-overflow: ellipsis;
- .problem-text {
- font-size: 32rpx;
- color: #333333;
- line-height: 46rpx;
- }
- }
- .desc {
- margin-top: 36rpx;
- .desc-text {
- font-size: 26rpx;
- color: #999999;
- }
- }
- .bottom-box {
- bottom: 30rpx;
- left: 30rpx;
- .avatar {
- width: 80rpx;
- height: 80rpx;
- border-radius: 50%;
- margin-right: 20rpx;
- }
- .name {
- font-size: 28rpx;
- color: #AAAAAA;
- }
- }
- }
- }
- }
- </style>
以下是getTouchPoint,getElSize用到的JS部分
JavaScript Code复制内容到剪贴板
- // #ifdef APP-NVUE
- const dom = uni.requireNativePlugin("dom")
- // #endif
- export function getElSize(name, ins) {
- return new Promise((res, rej) => {
- // #ifndef APP-NVUE
- const el = uni.createSelectorQuery().in(ins).select('#' + name);
- el.fields({
- size: true,
- rect: true
- }, (data) => {
- if (data) {
- res(data);
- } else {
- rej({})
- }
- }).exec();
- // #endif
- // #ifdef APP-NVUE
- let _el = ins.$refs[name][0]
- if (!_el) {
- _el = ins.$refs[name]
- }
- dom.getComponentRect(_el, (data) => {
- if (data.result) {
- res(data.size)
- } else {
- rej({})
- }
- })
- // #endif
- })
- }
- export function getTouchPoint(e) {
- if (!e) {
- return {
- x: 0,
- y: 0,
- sX: 0,
- sY: 0
- }
- }
- if (e.touches && e.touches[0]) {
- return {
- x: e.touches[0].pageX,
- y: e.touches[0].pageY,
- sX: e.touches[0].screenX,
- sY: e.touches[0].screenY
- }
- } else if (e.changedTouches && e.changedTouches[0]) {
- return {
- x: e.changedTouches[0].pageX,
- y: e.changedTouches[0].pageY,
- sX: e.changedTouches[0].screenX,
- sY: e.changedTouches[0].screenY
- }
- } else {
- return {
- x: e.clientX,
- y: e.clientY
- }
- }
- }
3、最后一步,页面随着手指滑动,页面不固定,在pages.json给它固定一下:
XML/HTML Code复制内容到剪贴板
- {
- "path": "pages/article/answer",
- "style": {
- "navigationBarTitleText": "写回答",
- "navigationStyle":"custom",
- "disableScroll":true // 禁止当前页面滚动/固定页面不滚动
- }
- }
2021.08.25,动画效果:在跟随手指移动的时候,带倾斜效果
3D卡片切换的效果,取决于传参的pages数据,是否超过三个!!正好三个就是以下效果
组件直接复制
XML/HTML Code复制内容到剪贴板
- <template>
- <div class="flipcard" id="flipcard" ref="flipcard">
- <div class="flipcard-item" v-for="(item, index) in pages" :key="index"
- :style="[transformIndex(index),transform(index)]" @touchstart="touchstart" @touchmove="touchmove"
- @touchend="touchend" @touchcancel="touchend" @transitionend="onTransitionEnd(index,'transitionend')">
- <!-- <img :src="item.author.avatar" /> -->
- <div class="flipcard-item-stack-info">
- <div class="t3"><text class="t3-text">{{item.tag}}</text></div>
- <div class="problem">
- <text class="problem-text">{{item.problem}}</text>
- </div>
- <div class="desc n-flex-row n-wrap-nowrap n-align-center">
- <text class="desc-text">{{item.attention_num}}人关注</text>
- <text class="desc-text">·</text>
- <text class="desc-text">{{item.comment_num}}人回答</text>
- </div>
- <div class="n-flex-row n-wrap-nowrap n-align-center n-position-absolute bottom-box">
- <image class="avatar" :src="item.author.avatar" mode="aspectFill"></image>
- <text class="name">{{item.author.name}}</text>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script>
- /**
- * @Desc Vue仿探探|Tinder卡片滑动FlipCard
- * @Time by 2021-08-24
- * @About Q:398927951
- */
- import {
- getTouchPoint,
- getElSize
- } from '@/nPro/utils/element.js'
- export default {
- props: {
- pages: {
- type: Array,
- default: []
- },
- stackInit: {
- type: Object,
- default: {}
- },
- },
- data() {
- return {
- el: {
- },
- basicdata: {
- start: {},
- end: {}
- },
- temporaryData: {
- isStackClick: true,
- offsetY: '',
- poswidth: 0,
- posheight: 0,
- lastPosWidth: '',
- lastPosHeight: '',
- lastZindex: '',
- rotate: 0,
- lastRotate: 0,
- visible: this.stackInit.visible || 3, // 展示几个
- tracking: false,
- animation: false,
- currentPage: this.stackInit.currentPage || 0, // 当前第几个
- opacity: 1,
- lastOpacity: 0,
- swipe: false,
- zIndex: 10
- }
- }
- },
- computed: {
- // 划出面积比例
- offsetRatio() {
- let width = this.el.width
- let height = this.el.height
- let offsetWidth = width - Math.abs(this.temporaryData.poswidth)
- let offsetHeight = height - Math.abs(this.temporaryData.posheight)
- let ratio = 1 - (offsetWidth * offsetHeight) / (width * height) || 0
- return ratio > 1 ? 1 : ratio
- },
- // 划出宽度比例
- offsetWidthRatio() {
- let width = this.el.width
- let offsetWidth = width - Math.abs(this.temporaryData.poswidth)
- let ratio = 1 - offsetWidth / width || 0
- return ratio
- }
- },
- created() {
- this.$nextTick(async e => {
- this.el = await getElSize('flipcard', this)
- })
- },
- methods: {
- touchstart(e) {
- if (this.temporaryData.tracking) {
- return
- }
- // 是否为touch
- const point = getTouchPoint(e)
- // console.log(point)
- if (e.type === 'touchstart') {
- if (e.touches.length > 1) {
- this.temporaryData.tracking = false
- return
- } else {
- // 记录起始位置
- this.basicdata.start.t = new Date().getTime()
- this.basicdata.start.x = point.x
- this.basicdata.start.y = point.y
- this.basicdata.end.x = point.x
- this.basicdata.end.y = point.y
- // offsetY在touch事件中没有,只能自己计算
- this.temporaryData.offsetY = point.y - this.el.top
- }
- }
- this.temporaryData.isStackClick = true
- this.temporaryData.tracking = true
- this.temporaryData.animation = false
- },
- touchmove(e) {
- this.temporaryData.isStackClick = false
- // 记录滑动位置
- if (this.temporaryData.tracking && !this.temporaryData.animation) {
- const point = getTouchPoint(e)
- e.preventDefault()
- this.basicdata.end.x = point.x
- this.basicdata.end.y = point.y
- // 计算滑动值
- thisthis.temporaryData.poswidth = this.basicdata.end.x - this.basicdata.start.x
- thisthis.temporaryData.posheight = this.basicdata.end.y - this.basicdata.start.y
- let rotateDirection = this.rotateDirection()
- let angleRatio = this.angleRatio()
- this.temporaryData.rotate = rotateDirection * this.offsetWidthRatio * 15 * angleRatio
- }
- },
- touchend(e, index) {
- if (this.temporaryData.isStackClick) {
- this.$emit('click', index)
- this.temporaryData.isStackClick = false
- }
- this.temporaryData.isStackClick = true
- this.temporaryData.tracking = false
- this.temporaryData.animation = true
- // 滑动结束,触发判断
- // 判断划出面积是否大于0.4
- if (this.offsetRatio >= 0.4) {
- // 计算划出后最终位置
- let ratio = Math.abs(this.temporaryData.posheight / this.temporaryData.poswidth)
- thisthis.temporaryData.poswidth = this.temporaryData.poswidth >= 0 ? this.temporaryData.poswidth + 200 :
- this.temporaryData.poswidth - 200
- thisthis.temporaryData.posheight = this.temporaryData.posheight >= 0 ? Math.abs(this.temporaryData
- .poswidth * ratio) : -Math.abs(this.temporaryData.poswidth * ratio)
- this.temporaryData.opacity = 0
- this.temporaryData.swipe = true
- this.nextTick()
- // 不满足条件则滑入
- } else {
- this.temporaryData.poswidth = 0
- this.temporaryData.posheight = 0
- this.temporaryData.swipe = false
- this.temporaryData.rotate = 0
- }
- },
- nextTick() {
- // 记录最终滑动距离
- thisthis.temporaryData.lastPosWidth = this.temporaryData.poswidth
- thisthis.temporaryData.lastPosHeight = this.temporaryData.posheight
- thisthis.temporaryData.lastRotate = this.temporaryData.rotate
- this.temporaryData.lastZindex = 20
- // 循环currentPage
- thisthis.temporaryData.currentPage = this.temporaryData.currentPage === this.pages.length - 1 ? 0 : this
- .temporaryData.currentPage + 1
- // currentPage切换,整体dom进行变化,把第一层滑动置最低
- this.$nextTick(() => {
- this.temporaryData.poswidth = 0
- this.temporaryData.posheight = 0
- this.temporaryData.opacity = 1
- this.temporaryData.rotate = 0
- })
- },
- onTransitionEnd(index) {
- let lastPage = this.temporaryData.currentPage === 0 ? this.pages.length - 1 : this.temporaryData
- .currentPage - 1
- // dom发生变化正在执行的动画滑动序列已经变为上一层
- if (this.temporaryData.swipe && index === lastPage) {
- this.temporaryData.animation = true
- this.temporaryData.lastPosWidth = 0
- this.temporaryData.lastPosHeight = 0
- this.temporaryData.lastOpacity = 0
- this.temporaryData.lastRotate = 0
- this.temporaryData.swipe = false
- this.temporaryData.lastZindex = -1
- }
- },
- prev() {
- this.temporaryData.tracking = false
- this.temporaryData.animation = true
- // 计算划出后最终位置
- let width = this.el.width
- this.temporaryData.poswidth = -width
- this.temporaryData.posheight = 0
- this.temporaryData.opacity = 0
- this.temporaryData.rotate = '-3'
- this.temporaryData.swipe = true
- this.nextTick()
- },
- next() {
- this.temporaryData.tracking = false
- this.temporaryData.animation = true
- // 计算划出后最终位置
- let width = this.el.width
- this.temporaryData.poswidth = width
- this.temporaryData.posheight = 0
- this.temporaryData.opacity = 0
- this.temporaryData.rotate = '3'
- this.temporaryData.swipe = true
- this.nextTick()
- },
- rotateDirection() {
- if (this.temporaryData.poswidth <= 0) {
- return -1
- } else {
- return 1
- }
- },
- angleRatio() {
- let height = this.el.height
- let offsetY = this.temporaryData.offsetY
- let ratio = -1 * (2 * offsetY / height - 1)
- return ratio || 0
- },
- inStack(index, currentPage) {
- let stack = []
- let visible = this.temporaryData.visible
- let length = this.pages.length
- for (let i = 0; i < visible; i++) {
- if (currentPage + i < length) {
- stack.push(currentPage + i)
- } else {
- stack.push(currentPage + i - length)
- }
- }
- return stack.indexOf(index) >= 0
- },
- // 非首页样式切换
- transform(index) {
- let currentPage = this.temporaryData.currentPage
- let length = this.pages.length
- let lastPage = currentPage === 0 ? this.pages.length - 1 : currentPage - 1
- let style = {}
- let visible = this.temporaryData.visible
- if (index === this.temporaryData.currentPage) {
- return
- }
- if (this.inStack(index, currentPage)) {
- let perIndex = index - currentPage > 0 ? index - currentPage : index - currentPage + length
- style['opacity'] = '1'
- style['transform'] = 'translate3D(0,0,' + -1 * 60 * (perIndex - this.offsetRatio) + 'px' + ')'
- style['zIndex'] = visible - perIndex
- if (!this.temporaryData.tracking) {
- style['transitionTimingFunction'] = 'ease'
- style['transitionDuration'] = 300 + 'ms'
- }
- } else if (index === lastPage) {
- style['transform'] = 'translate3D(' + this.temporaryData.lastPosWidth + 'px' + ',' + this.temporaryData
- .lastPosHeight + 'px' + ',0px) ' + 'rotate(' + this.temporaryData.lastRotate + 'deg)'
- style['opacity'] = this.temporaryData.lastOpacity
- style['zIndex'] = this.temporaryData.lastZindex
- style['transitionTimingFunction'] = 'ease'
- style['transitionDuration'] = 300 + 'ms'
- } else {
- style['zIndex'] = '-1'
- style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')'
- }
- return style
- },
- // 首页样式切换
- transformIndex(index) {
- if (index === this.temporaryData.currentPage) {
- let style = {}
- style['transform'] = 'translate3D(' + this.temporaryData.poswidth + 'px' + ',' + this.temporaryData
- .posheight + 'px' + ',0px) ' + 'rotate(' + this.temporaryData.rotate + 'deg)'
- style['opacity'] = this.temporaryData.opacity
- style['zIndex'] = 10
- if (this.temporaryData.animation) {
- style['transitionTimingFunction'] = 'ease'
- style['transitionDuration'] = (this.temporaryData.animation ? 300 : 0) + 'ms'
- }
- return style
- }
- },
- }
- }
- </script>
- <style lang="less" scoped>
- .flipcard {
- width: 100%;
- height: 100%;
- position: relative;
- perspective: 1000px;
- perspective-origin: 50% 150%;
- -webkit-perspective: 1000px;
- -webkit-perspective-origin: 50% 150%;
- margin: 0;
- padding: 0;
- &-item {
- background: #fff;
- height: 100%;
- width: 100%;
- border-radius: 30rpx;
- overflow: hidden;
- position: absolute;
- opacity: 0;
- display: -webkit-flex;
- display: flex;
- -webkit-flex-direction: column;
- flex-direction: column;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- pointer-events: auto;
- padding: 30rpx;
- /* #ifndef APP-NVUE */
- box-sizing: border-box;
- /* #endif */
- box-shadow: 0px 2rpx 30rpx 0px rgba(94, 114, 153, 0.3);
- &-stack-info {
- .t3 {
- padding: 0 0 25rpx 0;
- .t3-text {
- font-size: 26rpx;
- background-color: #ECECEC;
- padding: 8rpx 18rpx;
- color: #999999;
- border-radius: 10rpx;
- }
- }
- .problem {
- height: 180rpx;
- overflow: hidden;
- text-overflow: ellipsis;
- .problem-text {
- font-size: 32rpx;
- color: #333333;
- line-height: 46rpx;
- }
- }
- .desc {
- margin-top: 36rpx;
- .desc-text {
- font-size: 26rpx;
- color: #999999;
- }
- }
- .bottom-box {
- bottom: 30rpx;
- left: 30rpx;
- .avatar {
- width: 80rpx;
- height: 80rpx;
- border-radius: 50%;
- margin-right: 20rpx;
- }
- .name {
- font-size: 28rpx;
- color: #AAAAAA;
- }
- }
- }
- }
- }
- </style>
动画效果:仿知乎只想要左右划出,上下不动
覆盖其中一段即可:
JavaScript Code复制内容到剪贴板
- // 首页样式切换
- transformIndex(index) {
- if (index === this.temporaryData.currentPage) {
- let style = {}
- style['transform'] = 'translate3D(' + this.temporaryData.poswidth + 'px' + ',0px' + ',0px) ' + 'rotate(' + -this.temporaryData.rotate + 'deg)'
- style['opacity'] = this.temporaryData.opacity
- style['zIndex'] = 10
- if (this.temporaryData.animation) {
- style['transitionTimingFunction'] = 'ease'
- style['transitionDuration'] = (this.temporaryData.animation ? 300 : 0) + 'ms'
- }
- return style
- }
- },
2021.08.25 移动和点击事件相冲突时,需要将handleStackClicked点击事件暴露:
组件直接复制
XML/HTML Code复制内容到剪贴板
- <template>
- <div class="flipcard" id="flipcard" ref="flipcard">
- <div class="flipcard-item" v-for="(item, index) in pages" :key="index"
- :style="[transformIndex(index),transform(index)]" @touchstart="touchstart" @touchmove="touchmove"
- @touchend="touchend" @touchcancel="touchend" @transitionend="onTransitionEnd(index,'transitionend')">
- <div class="flipcard-item-stack-info">
- <div class="t3"><text class="t3-text">{{item.tag}}</text></div>
- <div class="problem">
- <text class="problem-text">{{item.problem}}</text>
- </div>
- <div class="desc n-flex-row n-wrap-nowrap n-align-center">
- <text class="desc-text">{{item.attention_num}}人关注</text>
- <text class="desc-text">·</text>
- <text class="desc-text">{{item.comment_num}}人回答</text>
- </div>
- <div class="n-flex-row n-wrap-nowrap n-align-center n-position-absolute bottom-box">
- <image class="avatar" :src="item.author.avatar" mode="aspectFill"></image>
- <text class="name">{{item.author.name}}</text>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script>
- /**
- * @Desc Vue仿探探/知乎|Tinder卡片滑动FlipCard
- * @Time by 2021-08-24
- * @About Q:398927951
- */
- import {
- getTouchPoint,
- getElSize
- } from '@/nPro/utils/element.js'
- export default {
- props: {
- pages: {
- type: Array,
- default: []
- },
- stackInit: {
- type: Object,
- default: {}
- },
- },
- data() {
- return {
- el: {
- },
- basicdata: {
- start: {},
- end: {}
- },
- temporaryData: {
- isStackClick: true, // 多了一个参数
- offsetY: '',
- poswidth: 0,
- posheight: 0,
- lastPosWidth: '',
- lastPosHeight: '',
- lastZindex: '',
- rotate: 0,
- lastRotate: 0,
- visible: this.stackInit.visible || 3, // 展示几个
- tracking: false,
- animation: false,
- currentPage: this.stackInit.currentPage || 0, // 当前第几个
- opacity: 1,
- lastOpacity: 0,
- swipe: false,
- zIndex: 10
- }
- }
- },
- computed: {
- // 划出面积比例
- offsetRatio(e) {
- let width = this.el.width
- let height = this.el.height
- let offsetWidth = width - Math.abs(this.temporaryData.poswidth)
- let offsetHeight = height - Math.abs(this.temporaryData.posheight)
- let ratio = 1 - (offsetWidth * offsetHeight) / (width * height) || 0
- return ratio > 1 ? 1 : ratio
- },
- // 划出宽度比例
- offsetWidthRatio() {
- let width = this.el.offsetWidth
- let offsetWidth = width - Math.abs(this.temporaryData.poswidth)
- let ratio = 1 - offsetWidth / width || 0
- return ratio
- }
- },
- created() {
- this.$nextTick(async e => {
- this.el = await getElSize('flipcard', this)
- })
- },
- methods: {
- touchstart(e) {
- if (this.temporaryData.tracking) {
- return
- }
- // 是否为touch
- if (e.type === 'touchstart') {
- if (e.touches.length > 1) {
- this.temporaryData.tracking = false
- return
- } else {
- // 记录起始位置
- const point = getTouchPoint(e)
- // console.log("记录起始位置",point);
- this.basicdata.start.t = new Date().getTime()
- this.basicdata.start.x = point.x
- this.basicdata.start.y = point.y
- this.basicdata.end.x = point.x
- this.basicdata.end.y = point.y
- // offsetY在touch事件中没有,只能自己计算
- this.temporaryData.offsetY = point.y - this.el.top
- // console.log(this.temporaryData.offsetY)
- }
- // pc操作
- } else {
- console.log("pc记录起始位置");
- uni.showToast({
- title: '无效操作',
- icon: 'none'
- });
- // this.basicdata.start.t = new Date().getTime()
- // this.basicdata.start.x = e.clientX
- // this.basicdata.start.y = e.clientY
- // this.basicdata.end.x = e.clientX
- // this.basicdata.end.y = e.clientY
- // this.temporaryData.offsetY = e.offsetY
- }
- this.temporaryData.isStackClick = true // 多了一个参数
- this.temporaryData.tracking = true
- this.temporaryData.animation = false
- },
- touchmove(e) {
- this.temporaryData.isStackClick = false // 多了一个参数
- // 记录滑动位置
- if (this.temporaryData.tracking && !this.temporaryData.animation) {
- const point = getTouchPoint(e)
- if (e.type === 'touchmove') {
- e.preventDefault()
- this.basicdata.end.x = point.x
- this.basicdata.end.y = point.y
- } else {
- e.preventDefault()
- this.basicdata.end.x = point.x
- this.basicdata.end.y = point.y
- }
- // 计算滑动值
- thisthis.temporaryData.poswidth = this.basicdata.end.x - this.basicdata.start.x
- thisthis.temporaryData.posheight = this.basicdata.end.y - this.basicdata.start.y
- let rotateDirection = this.rotateDirection()
- let angleRatio = this.angleRatio()
- this.temporaryData.rotate = rotateDirection * this.offsetWidthRatio * 15 * angleRatio
- }
- },
- touchend(e, index) {
- console.log(this.temporaryData.isStackClick)
- if(this.temporaryData.isStackClick) {
- this.$emit('click', index) // 多了一个参数,触发上层传递事件
- this.temporaryData.isStackClick = false
- }
- this.temporaryData.isStackClick = true // 多了一个参数
- this.temporaryData.tracking = false
- this.temporaryData.animation = true
- // 滑动结束,触发判断
- // 判断划出面积是否大于0.4
- if (this.offsetRatio >= 0.4) {
- // 计算划出后最终位置
- let ratio = Math.abs(this.temporaryData.posheight / this.temporaryData.poswidth)
- thisthis.temporaryData.poswidth = this.temporaryData.poswidth >= 0 ? this.temporaryData.poswidth + 200 :
- this.temporaryData.poswidth - 200
- thisthis.temporaryData.posheight = this.temporaryData.posheight >= 0 ? Math.abs(this.temporaryData
- .poswidth * ratio) : -Math.abs(this.temporaryData.poswidth * ratio)
- this.temporaryData.opacity = 0
- this.temporaryData.swipe = true
- this.nextTick()
- // 不满足条件则滑入
- } else {
- this.temporaryData.poswidth = 0
- this.temporaryData.posheight = 0
- this.temporaryData.swipe = false
- this.temporaryData.rotate = 0
- }
- },
- nextTick() {
- // 记录最终滑动距离
- thisthis.temporaryData.lastPosWidth = this.temporaryData.poswidth
- thisthis.temporaryData.lastPosHeight = this.temporaryData.posheight
- thisthis.temporaryData.lastRotate = this.temporaryData.rotate
- this.temporaryData.lastZindex = 20
- // 循环currentPage
- thisthis.temporaryData.currentPage = this.temporaryData.currentPage === this.pages.length - 1 ? 0 : this
- .temporaryData.currentPage + 1
- // currentPage切换,整体dom进行变化,把第一层滑动置最低
- this.$nextTick(() => {
- this.temporaryData.poswidth = 0
- this.temporaryData.posheight = 0
- this.temporaryData.opacity = 1
- this.temporaryData.rotate = 0
- })
- },
- onTransitionEnd(index, log) {
- let lastPage = this.temporaryData.currentPage === 0 ? this.pages.length - 1 : this.temporaryData
- .currentPage - 1
- // dom发生变化正在执行的动画滑动序列已经变为上一层
- if (this.temporaryData.swipe && index === lastPage) {
- this.temporaryData.animation = true
- this.temporaryData.lastPosWidth = 0
- this.temporaryData.lastPosHeight = 0
- this.temporaryData.lastOpacity = 0
- this.temporaryData.lastRotate = 0
- this.temporaryData.swipe = false
- this.temporaryData.lastZindex = -1
- }
- },
- prev() {
- this.temporaryData.tracking = false
- this.temporaryData.animation = true
- // 计算划出后最终位置
- let width = this.el.width
- this.temporaryData.poswidth = -width
- this.temporaryData.posheight = 0
- this.temporaryData.opacity = 0
- this.temporaryData.rotate = '-3'
- this.temporaryData.swipe = true
- this.nextTick()
- },
- next() {
- this.temporaryData.tracking = false
- this.temporaryData.animation = true
- // 计算划出后最终位置
- let width = this.el.width
- this.temporaryData.poswidth = width
- this.temporaryData.posheight = 0
- this.temporaryData.opacity = 0
- this.temporaryData.rotate = '3'
- this.temporaryData.swipe = true
- this.nextTick()
- },
- rotateDirection() {
- if (this.temporaryData.poswidth <= 0) {
- return -1
- } else {
- return 1
- }
- },
- angleRatio() {
- let height = this.el.height
- let offsetY = this.temporaryData.offsetY
- let ratio = -1 * (2 * offsetY / height - 1)
- return ratio || 0
- },
- inStack(index, currentPage) {
- let stack = []
- let visible = this.temporaryData.visible
- let length = this.pages.length
- for (let i = 0; i < visible; i++) {
- if (currentPage + i < length) {
- stack.push(currentPage + i)
- } else {
- stack.push(currentPage + i - length)
- }
- }
- return stack.indexOf(index) >= 0
- },
- // 非首页样式切换
- transform(index) {
- let currentPage = this.temporaryData.currentPage
- let length = this.pages.length
- let lastPage = currentPage === 0 ? this.pages.length - 1 : currentPage - 1
- let style = {}
- let visible = this.temporaryData.visible
- if (index === this.temporaryData.currentPage) {
- return
- }
- if (this.inStack(index, currentPage)) {
- let perIndex = index - currentPage > 0 ? index - currentPage : index - currentPage + length
- style['opacity'] = '1'
- style['transform'] = 'translate3D(0,0,' + -1 * 60 * (perIndex - this.offsetRatio) + 'px' + ')'
- style['zIndex'] = visible - perIndex
- if (!this.temporaryData.tracking) {
- style['transitionTimingFunction'] = 'ease'
- style['transitionDuration'] = 300 + 'ms'
- }
- } else if (index === lastPage) {
- style['transform'] = 'translate3D(' + this.temporaryData.lastPosWidth + 'px' + ',' + this.temporaryData
- .lastPosHeight + 'px' + ',0px) ' + 'rotate(' + this.temporaryData.lastRotate + 'deg)'
- style['opacity'] = this.temporaryData.lastOpacity
- style['zIndex'] = this.temporaryData.lastZindex
- style['transitionTimingFunction'] = 'ease'
- style['transitionDuration'] = 300 + 'ms'
- } else {
- style['zIndex'] = '-1'
- style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')'
- }
- return style
- },
- // 首页样式切换
- transformIndex(index) {
- if (index === this.temporaryData.currentPage) {
- let style = {}
- style['transform'] = 'translate3D(' + this.temporaryData.poswidth + 'px' + ',' + this.temporaryData
- .posheight + 'px' + ',0px) ' + 'rotate(' + this.temporaryData.rotate + 'deg)'
- style['opacity'] = this.temporaryData.opacity
- style['zIndex'] = 10
- if (this.temporaryData.animation) {
- style['transitionTimingFunction'] = 'ease'
- style['transitionDuration'] = (this.temporaryData.animation ? 300 : 0) + 'ms'
- }
- return style
- }
- },
- }
- }
- </script>
- <style lang="less" scoped>
- .flipcard {
- width: 100%;
- height: 100%;
- position: relative;
- perspective: 1000px;
- perspective-origin: 50% 150%;
- -webkit-perspective: 1000px;
- -webkit-perspective-origin: 50% 150%;
- margin: 0;
- padding: 0;
- &-item {
- background: #fff;
- height: 100%;
- width: 100%;
- border-radius: 30rpx;
- overflow: hidden;
- position: absolute;
- opacity: 0;
- display: -webkit-flex;
- display: flex;
- -webkit-flex-direction: column;
- flex-direction: column;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- pointer-events: auto;
- padding: 30rpx;
- /* #ifndef APP-NVUE */
- box-sizing: border-box;
- /* #endif */
- box-shadow: 0px 2rpx 30rpx 0px rgba(94, 114, 153, 0.3);
- &-stack-info {
- .t3 {
- padding: 0 0 25rpx 0;
- .t3-text {
- font-size: 26rpx;
- background-color: #ECECEC;
- padding: 8rpx 18rpx;
- color: #999999;
- border-radius: 10rpx;
- }
- }
- .problem {
- height: 180rpx;
- overflow: hidden;
- text-overflow: ellipsis;
- .problem-text {
- font-size: 32rpx;
- color: #333333;
- line-height: 46rpx;
- }
- }
- .desc {
- margin-top: 36rpx;
- .desc-text {
- font-size: 26rpx;
- color: #999999;
- }
- }
- .bottom-box {
- bottom: 30rpx;
- left: 30rpx;
- .avatar {
- width: 80rpx;
- height: 80rpx;
- border-radius: 50%;
- margin-right: 20rpx;
- }
- .name {
- font-size: 28rpx;
- color: #AAAAAA;
- }
- }
- }
- }
- }
- </style>
上一篇 app左滑旋转后消失
下一篇 uniapp小程序对接腾讯云直播