Tantan의 스택 슬라이딩 컴포넌트가 중요한 역할을 합니다. vue로 Tantan 스택 컴포넌트를 작성하는 방법을 살펴보겠습니다. 관심 있는 친구들은 함께 살펴보세요
렌더링은 다음과 같습니다.
머리말
안녕하세요, 탄탄이라면 프로그램에 익숙하실 겁니다. (결국 여자분들이 많죠.) 탄탄의 겹겹이 쌓인 슬라이딩 부품은 브랜드를 원활하게 뒤집는 데 중요한 역할을 합니다. .함수, vue를 사용하여 Tantan 스태킹 컴포넌트를 작성하는 방법을 살펴보겠습니다
1. 기능 분석
간단히 Tantan을 사용하면 스태킹 및 슬라이딩 기능이 매우 간단하다는 것을 알 수 있습니다. :
포함된 기본 기능에 대한 간략한 요약:
사진 쌓기
첫 번째 사진 슬라이딩
조건 성공 후 슬라이딩하고 리바운드 조건이 실패한 후
슬라이딩 아웃 후 다음 사진이 위에 쌓입니다
경험 최적화
서로 다른 터치 지점에 따라 슬라이딩 시 첫 번째 사진이 다른 각도로 오프셋됩니다
오프셋 영역에 따라 슬라이딩 성공 여부가 결정됩니다. Out
2. 구체적인 구현
요약된 기능 포인트를 통해 구성요소 구현 아이디어가 더욱 명확해집니다
1.
인터넷에 쌓인 그림 효과가 많이 있습니다. 구현 방법은 주로 상위 레이어에 원근감과 원점을 설정하여 하위 레이어의 원근감을 설정하는 방식으로 유사합니다. 하위 레이어의 값은 스태킹 효과를 시뮬레이션할 수 있습니다. 구체적인 코드는 다음과 같습니다
// 图片堆叠dom <!--opacity: 0 隐藏我们不想看到的stack-item层级--> <!--z-index: -1 调整stack-item层级"--> <ul class="stack"> <li class="stack-item" style="transform: translate3d(0px, 0px, 0px);opacity: 1;z-index: 10;"><img src="1.png" alt="01"></li> <li class="stack-item" style="transform: translate3d(0px, 0px, -60px);opacity: 1;z-index: 1"><img src="2.png" alt="02"></li> <li class="stack-item" style="transform: translate3d(0px, 0px, -120px);opacity: 1;z-index: 1"><img src="3.png" alt="03"></li> <li class="stack-item" style="transform: translate3d(0px, 0px, -180px);opacity: 0;z-index: -1"><img src="4.png" alt="04"></li> <li class="stack-item" style="transform: translate3d(0px, 0px, -180px);opacity: 0;z-index: -1"><img src="5.png" alt="05"></li> </ul> <style> .stack { width: 100%; height: 100%; position: relative; perspective: 1000px; //子元素视距 perspective-origin: 50% 150%; //子元素透视位置 -webkit-perspective: 1000px; -webkit-perspective-origin: 50% 150%; margin: 0; padding: 0; } .stack-item{ background: #fff; height: 100%; width: 100%; border-radius: 4px; text-align: center; overflow: hidden; } .stack-item img { width: 100%; display: block; pointer-events: none; } </style>
위는 우리가 원하는 것은 vue 컴포넌트이므로 컴포넌트를 만들어야 합니다. template stack.vue 먼저 템플릿에서 v-for를 사용하여 스택 노드를 순회하고 :style을 사용하여 각 항목의 스타일을 수정할 수 있습니다. 코드는 다음과 같습니다
<template> <ul class="stack"> <li class="stack-item" v-for="(item, index) in pages" :style="[transform(index)]"> <img :src="item.src"> </li> </ul> </template> <script> export default { props: { // pages数据包含基础的图片数据 pages: { type: Array, default: [] } }, data () { return { // basicdata数据包含组件基本数据 basicdata: { currentPage: 0 // 默认首图的序列 }, // temporaryData数据包含组件临时数据 temporaryData: { opacity: 1, // 记录opacity zIndex: 10, // 记录zIndex visible: 3 // 记录默认显示堆叠数visible } } }, methods: { // 遍历样式 transform (index) { if (index >= this.basicdata.currentPage) { let style = {} let visible = this.temporaryData.visible let perIndex = index - this.basicdata.currentPage // visible可见数量前滑块的样式 if (index <= this.basicdata.currentPage + visible - 1) { style['opacity'] = '1' style['transform'] = 'translate3D(0,0,' + -1 * perIndex * 60 + 'px' + ')' style['zIndex'] = visible - index + this.basicdata.currentPage style['transitionTimingFunction'] = 'ease' style['transitionDuration'] = 300 + 'ms' } else { style['zIndex'] = '-1' style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')' } return style } } } } </script>
핵심 사항
: 스타일은 객체뿐만 아니라 배열 및 함수도 바인딩할 수 있어 탐색할 때 매우 유용합니다.
가장 기본적인 돔 구조가 구축되었으며, 다음 단계는 첫 번째 그림을 "이동"시키는 것입니다
2. Sliding
그림 슬라이딩 효과는 터치 이벤트를 모니터링하고 변위를 얻은 다음 Translate3D를 통해 대상 변위를 변경하는 것이 원칙이므로 구현해야 하는 단계는 다음과 같습니다.
터치 이벤트를 스택에 바인딩<template> <ul class="stack"> <li class="stack-item" v-for="(item, index) in pages" :style="[transformIndex(index),transform(index)]" @touchstart.stop.capture="touchstart" @touchmove.stop.capture="touchmove" @touchend.stop.capture="touchend" @mousedown.stop.capture="touchstart" @mouseup.stop.capture="touchend" @mousemove.stop.capture="touchmove"> <img :src="item.src"> </li> </ul> </template> <script> export default { props: { // pages数据包含基础的图片数据 pages: { type: Array, default: [] } }, data () { return { // basicdata数据包含组件基本数据 basicdata: { start: {}, // 记录起始位置 end: {}, // 记录终点位置 currentPage: 0 // 默认首图的序列 }, // temporaryData数据包含组件临时数据 temporaryData: { poswidth: '', // 记录位移 posheight: '', // 记录位移 tracking: false // 是否在滑动,防止多次操作,影响体验 } } }, methods: { touchstart (e) { if (this.temporaryData.tracking) { return } // 是否为touch 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 = e.targetTouches[0].clientX this.basicdata.start.y = e.targetTouches[0].clientY this.basicdata.end.x = e.targetTouches[0].clientX this.basicdata.end.y = e.targetTouches[0].clientY } // pc操作 } else { 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.tracking = true }, touchmove (e) { // 记录滑动位置 if (this.temporaryData.tracking && !this.temporaryData.animation) { if (e.type === 'touchmove') { this.basicdata.end.x = e.targetTouches[0].clientX this.basicdata.end.y = e.targetTouches[0].clientY } else { this.basicdata.end.x = e.clientX this.basicdata.end.y = e.clientY } // 计算滑动值 this.temporaryData.poswidth = this.basicdata.end.x - this.basicdata.start.x this.temporaryData.posheight = this.basicdata.end.y - this.basicdata.start.y } }, touchend (e) { this.temporaryData.tracking = false // 滑动结束,触发判断 }, // 非首页样式切换 transform (index) { if (index > this.basicdata.currentPage) { let style = {} let visible = 3 let perIndex = index - this.basicdata.currentPage // visible可见数量前滑块的样式 if (index <= this.basicdata.currentPage + visible - 1) { style['opacity'] = '1' style['transform'] = 'translate3D(0,0,' + -1 * perIndex * 60 + 'px' + ')' style['zIndex'] = visible - index + this.basicdata.currentPage style['transitionTimingFunction'] = 'ease' style['transitionDuration'] = 300 + 'ms' } else { style['zIndex'] = '-1' style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')' } return style } }, // 首页样式切换 transformIndex (index) { // 处理3D效果 if (index === this.basicdata.currentPage) { let style = {} style['transform'] = 'translate3D(' + this.temporaryData.poswidth + 'px' + ',' + this.temporaryData.posheight + 'px' + ',0px)' style['opacity'] = 1 style['zIndex'] = 10 return style } } } } </script>
3. 조건이 성공하면 슬라이드 아웃되고, 조건이 실패하면 리바운드됩니다. 조건의 트리거 판단은 터치엔드/마우스업 이후에 수행됩니다. 여기서는 먼저 간단한 조건을 사용하여 판단하고 동시에 첫 번째 이미지 팝업 및 리바운드 효과를 제공합니다
<template>
<ul class="stack">
<li class="stack-item" v-for="(item, index) in pages"
:style="[transformIndex(index),transform(index)]"
@touchmove.stop.capture="touchmove"
@touchstart.stop.capture="touchstart"
@touchend.stop.capture="touchend"
@mousedown.stop.capture="touchstart"
@mouseup.stop.capture="touchend"
@mousemove.stop.capture="touchmove">
<img :src="item.src">
</li>
</ul>
</template>
<script>
export default {
props: {
// pages数据包含基础的图片数据
pages: {
type: Array,
default: []
}
},
data () {
return {
// basicdata数据包含组件基本数据
basicdata: {
start: {}, // 记录起始位置
end: {}, // 记录终点位置
currentPage: 0 // 默认首图的序列
},
// temporaryData数据包含组件临时数据
temporaryData: {
poswidth: '', // 记录位移
posheight: '', // 记录位移
tracking: false, // 是否在滑动,防止多次操作,影响体验
animation: false, // 首图是否启用动画效果,默认为否
opacity: 1 // 记录首图透明度
}
}
},
methods: {
touchstart (e) {
if (this.temporaryData.tracking) {
return
}
// 是否为touch
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 = e.targetTouches[0].clientX
this.basicdata.start.y = e.targetTouches[0].clientY
this.basicdata.end.x = e.targetTouches[0].clientX
this.basicdata.end.y = e.targetTouches[0].clientY
}
// pc操作
} else {
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.tracking = true
this.temporaryData.animation = false
},
touchmove (e) {
// 记录滑动位置
if (this.temporaryData.tracking && !this.temporaryData.animation) {
if (e.type === 'touchmove') {
this.basicdata.end.x = e.targetTouches[0].clientX
this.basicdata.end.y = e.targetTouches[0].clientY
} else {
this.basicdata.end.x = e.clientX
this.basicdata.end.y = e.clientY
}
// 计算滑动值
this.temporaryData.poswidth = this.basicdata.end.x - this.basicdata.start.x
this.temporaryData.posheight = this.basicdata.end.y - this.basicdata.start.y
}
},
touchend (e) {
this.temporaryData.tracking = false
this.temporaryData.animation = true
// 滑动结束,触发判断
// 简单判断滑动宽度超出100像素时触发滑出
if (Math.abs(this.temporaryData.poswidth) >= 100) {
// 最终位移简单设定为x轴200像素的偏移
let ratio = Math.abs(this.temporaryData.posheight / this.temporaryData.poswidth)
this.temporaryData.poswidth = this.temporaryData.poswidth >= 0 ? this.temporaryData.poswidth + 200 : this.temporaryData.poswidth - 200
this.temporaryData.posheight = this.temporaryData.posheight >= 0 ? Math.abs(this.temporaryData.poswidth * ratio) : -Math.abs(this.temporaryData.poswidth * ratio)
this.temporaryData.opacity = 0
// 不满足条件则滑入
} else {
this.temporaryData.poswidth = 0
this.temporaryData.posheight = 0
}
},
// 非首页样式切换
transform (index) {
if (index > this.basicdata.currentPage) {
let style = {}
let visible = 3
let perIndex = index - this.basicdata.currentPage
// visible可见数量前滑块的样式
if (index <= this.basicdata.currentPage + visible - 1) {
style['opacity'] = '1'
style['transform'] = 'translate3D(0,0,' + -1 * perIndex * 60 + 'px' + ')'
style['zIndex'] = visible - index + this.basicdata.currentPage
style['transitionTimingFunction'] = 'ease'
style['transitionDuration'] = 300 + 'ms'
} else {
style['zIndex'] = '-1'
style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')'
}
return style
}
},
// 首页样式切换
transformIndex (index) {
// 处理3D效果
if (index === this.basicdata.currentPage) {
let style = {}
style['transform'] = 'translate3D(' + this.temporaryData.poswidth + 'px' + ',' + this.temporaryData.posheight + 'px' + ',0px)'
style['opacity'] = this.temporaryData.opacity
style['zIndex'] = 10
if (this.temporaryData.animation) {
style['transitionTimingFunction'] = 'ease'
style['transitionDuration'] = 300 + 'ms'
}
return style
}
}
}
}
</script>
4. 슬라이드 아웃 다음 그림을 맨 위로 쌓기
리스태킹은 컴포넌트의 마지막 기능이자 가장 중요하고 복잡한 기능이기도 합니다. 우리 코드에서 스택 항목의 정렬은 바인딩 스타일의 변형 함수와 변형에 따라 달라집니다. 함수에서 결정된 조건은 currentPage를 변경하고 재스택을 완료하려면 +1해야 합니까? ?
답은 그리 간단하지 않습니다. 슬라이드 아웃은 300ms 동안 지속되는 애니메이션 효과이고 currentPage 변경으로 인한 재배열이 즉시 변경되어 애니메이션 진행을 방해하기 때문입니다. 따라서 변환 함수의 정렬 조건을 먼저 수정한 후 currentPage를 변경해야 합니다. #### 특정 구현
변환 함수 정렬 조건 수정
<template> <ul class="stack"> <li class="stack-item" v-for="(item, index) in pages" :style="[transformIndex(index),transform(index)]" @touchmove.stop.capture="touchmove" @touchstart.stop.capture="touchstart" @touchend.stop.capture="touchend" @mousedown.stop.capture="touchstart" @mouseup.stop.capture="touchend" @mousemove.stop.capture="touchmove" @webkit-transition-end="onTransitionEnd" @transitionend="onTransitionEnd" > <img :src="item.src"> </li> </ul> </template> <script> export default { props: { // pages数据包含基础的图片数据 pages: { type: Array, default: [] } }, data () { return { // basicdata数据包含组件基本数据 basicdata: { start: {}, // 记录起始位置 end: {}, // 记录终点位置 currentPage: 0 // 默认首图的序列 }, // temporaryData数据包含组件临时数据 temporaryData: { poswidth: '', // 记录位移 posheight: '', // 记录位移 lastPosWidth: '', // 记录上次最终位移 lastPosHeight: '', // 记录上次最终位移 tracking: false, // 是否在滑动,防止多次操作,影响体验 animation: false, // 首图是否启用动画效果,默认为否 opacity: 1, // 记录首图透明度 swipe: false // onTransition判定条件 } } }, methods: { touchstart (e) { if (this.temporaryData.tracking) { return } // 是否为touch 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 = e.targetTouches[0].clientX this.basicdata.start.y = e.targetTouches[0].clientY this.basicdata.end.x = e.targetTouches[0].clientX this.basicdata.end.y = e.targetTouches[0].clientY } // pc操作 } else { 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.tracking = true this.temporaryData.animation = false }, touchmove (e) { // 记录滑动位置 if (this.temporaryData.tracking && !this.temporaryData.animation) { if (e.type === 'touchmove') { this.basicdata.end.x = e.targetTouches[0].clientX this.basicdata.end.y = e.targetTouches[0].clientY } else { this.basicdata.end.x = e.clientX this.basicdata.end.y = e.clientY } // 计算滑动值 this.temporaryData.poswidth = this.basicdata.end.x - this.basicdata.start.x this.temporaryData.posheight = this.basicdata.end.y - this.basicdata.start.y } }, touchend (e) { this.temporaryData.tracking = false this.temporaryData.animation = true // 滑动结束,触发判断 // 简单判断滑动宽度超出100像素时触发滑出 if (Math.abs(this.temporaryData.poswidth) >= 100) { // 最终位移简单设定为x轴200像素的偏移 let ratio = Math.abs(this.temporaryData.posheight / this.temporaryData.poswidth) this.temporaryData.poswidth = this.temporaryData.poswidth >= 0 ? this.temporaryData.poswidth + 200 : this.temporaryData.poswidth - 200 this.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.temporaryData.lastPosWidth = this.temporaryData.poswidth this.temporaryData.lastPosHeight = this.temporaryData.posheight // currentPage+1 引发排序变化 this.basicdata.currentPage += 1 // currentPage切换,整体dom进行变化,把第一层滑动置零 this.$nextTick(() => { this.temporaryData.poswidth = 0 this.temporaryData.posheight = 0 this.temporaryData.opacity = 1 }) // 不满足条件则滑入 } else { this.temporaryData.poswidth = 0 this.temporaryData.posheight = 0 this.temporaryData.swipe = false } }, onTransitionEnd (index) { // dom发生变化后,正在执行的动画滑动序列已经变为上一层 if (this.temporaryData.swipe && index === this.basicdata.currentPage - 1) { this.temporaryData.animation = true this.temporaryData.lastPosWidth = 0 this.temporaryData.lastPosHeight = 0 this.temporaryData.swipe = false } }, // 非首页样式切换 transform (index) { if (index > this.basicdata.currentPage) { let style = {} let visible = 3 let perIndex = index - this.basicdata.currentPage // visible可见数量前滑块的样式 if (index <= this.basicdata.currentPage + visible - 1) { style['opacity'] = '1' style['transform'] = 'translate3D(0,0,' + -1 * perIndex * 60 + 'px' + ')' style['zIndex'] = visible - index + this.basicdata.currentPage style['transitionTimingFunction'] = 'ease' style['transitionDuration'] = 300 + 'ms' } else { style['zIndex'] = '-1' style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')' } return style // 已滑动模块释放后 } else if (index === this.basicdata.currentPage - 1) { let style = {} // 继续执行动画 style['transform'] = 'translate3D(' + this.temporaryData.lastPosWidth + 'px' + ',' + this.temporaryData.lastPosHeight + 'px' + ',0px)' style['opacity'] = '0' style['zIndex'] = '-1' style['transitionTimingFunction'] = 'ease' style['transitionDuration'] = 300 + 'ms' return style } }, // 首页样式切换 transformIndex (index) { // 处理3D效果 if (index === this.basicdata.currentPage) { let style = {} style['transform'] = 'translate3D(' + this.temporaryData.poswidth + 'px' + ',' + this.temporaryData.posheight + 'px' + ',0px)' style['opacity'] = this.temporaryData.opacity style['zIndex'] = 10 if (this.temporaryData.animation) { style['transitionTimingFunction'] = 'ease' style['transitionDuration'] = 300 + 'ms' } return style } } } } </script>
ok~ 위의 4단계를 완료하면 stacking 구성요소의 기본 기능이 구현되었습니다. 와서 효과를 확인하세요
Stacking 슬라이딩 효과가 출시되었습니다. , 하지만 탄탄은 그것을 경험하고 있습니다. 거기에 터치 각도 오프셋과 슬라이드 아웃 면적 비율 결정도 추가됩니다각도 오프셋의 원리는 사용자가 터치할 때마다 사용자의 터치 위치를 기록하여 최대 오프셋 각도를 계산하고, 슬라이딩 변위가 발생하면 각도가 최대 오프셋 각도까지 선형적으로 증가하는 것입니다.
스택을 사용할 때 특별히 해야 할 일은 다음과 같습니다.
touchmove에서 필요한 각도와 방향을 계산합니다
touchend와 onTransitionEnd를 사용하여 각도를 0으로 설정
슬라이드 아웃을 결정 주로 면적 비율을 통해 오프셋을 사용하여 면적 비율을 구하고 판단을 완료합니다. 소스 코드는 여기에 게시되지 않습니다. 이 글이 마음에 드신다면 github에서 보실 수 있습니다. :star:️를 눌러주세요. 그리고 마지막으로 여러분 모두가 Tantan:green_heart:
에서 전 여자친구를 찾으실 수 있기를 바랍니다. , 앞으로도 도움이 되길 바랍니다.
관련 기사:
Vue 프로젝트에서 프로젝트 구조를 초기화하기 위해 vue-cli 스캐폴딩을 사용하는 방법에 대한 자세한 설명vue에서 요청한 데이터의 특정 항목 값을 변경하는 방법
JavaScript Starry Navigation Column 구현 방법
위 내용은 vue를 사용하여 슬라이딩 스택 구성 요소 만들기(자세한 튜토리얼)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!