preface

Hi, speaking of tantan surely everyone program Wang is not strange (after all, many girls), can on the top of the silky over the brand, tantan stack sliding components played a key role, let’s see how to use Vue to write a tantan stack components 😄

I. Functional analysis

Simple use of the bottom will find that the function of stack sliding is very simple, with a picture is summarized:

  • Stacking of pictures
  • Slide on the first image
  • Slip out after condition success, rebound after condition failure
  • Slide out and stack the next image to the top

Experience optimization

  • According to different touch points, the head image has different Angle offset when sliding
  • Offset area determines whether the slip is successful

Two. Concrete implementation

With well-defined function points, we can think more clearly about how to implement components

1. Stacking effect

There are a large number of examples of stacking image effect on the Internet, and the implementation methods are similar with minor differences. Perspective and Perspective-Origin are mainly set in the parent layer to realize the perspective of the sub-layer. The stacking effect can be simulated by setting the z-axis value of Translate3D in the sub-layer, and the specific codes are as follows

// Image stack dom <! -- Opacity: 0 Hides the stack-item hierarchy we don't want to see --> <! -- Z-index: -1 Adjusts the stack-item hierarchy"-->
<ul class="stack"> stack-item" style="transform: translate3d(0px, 0px, 0px); opacity: 1; z-index: 10;"><img src="1.png" alt="01"> stack-item" style="transform: translate3d(0px, 0px, -60px); opacity: 1; z-index: 1"><img src="2.png" alt="02"> stack-item" style="transform: translate3d(0px, 0px, -120px); opacity: 1; z-index: 1"><img src="3.png" alt="03"> stack-item" style="transform: translate3d(0px, 0px, -180px); opacity: 0; z-index: -1"><img src="4.png" alt="04"> stack-item" style="transform: translate3d(0px, 0px, -180px); opacity: 0; z-index: -1"><img src="5.png" alt="05">  Copy the code

The above is just a set of static code, we want to get vue components, so we need to build a component template stack.vue, in the template we can use V-for, walk through the stack node, use :style to change the style of each item, the code is as follows

<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>
exportDefault {props: {// myprops: {// myprops: {type: Array,
      default: []
    }
  },
  data () {
    returnBasicdata: {currentPage: 0 // default header sequence}, // temporaryData data contains the component's temporaryData temporaryData: {// basicdata data: {currentPage: 0 // default header sequence} 1, // record opacity zIndex visible: 10, // record opacity zIndex visible: 3, // record default stack number visible}}}, methods: {// traversing style transform (index) {if (index >= this.basicdata.currentPage) {
        let style = {}
        let visible = this.temporaryData.visible
        letPerIndex = index - this. Basicdata. CurrentPage / / visible visible quantity before the style of the sliderif (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>
Copy the code

The key point

  • The :style can bind objects as well as arrays and functions, which is useful when traversing

Now that the basic DOM structure is in place, the next step is to get the first image “moving.

2. Slide pictures

Image sliding effect appears in many scenes. The principle is nothing more than listening to touchs event to get the displacement, and then changing the target displacement through translate3D, so the steps we need to achieve are as follows

  • Bind the stack to the Touchs event
  • Listen for and store the value of gesture position changes
  • Change the x and y values of translate3D in the CSS property of the header image

The specific implementation

In the VUE framework, it is not recommended to operate nodes directly, but to bind elements with the instruction V-ON. Therefore, we write all bindings in the V-for traversal, determine whether it is the first graph by index, and then use :style to modify the style of the home page. The specific code is as follows:

<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>
exportDefault {props: {// myprops: {// myprops: {type: Array,
      default: []
    }
  },
  data () {
    returnBasicdata: {start: {}, // Record the start position end: {}, // record the end position currentPage: TemporaryData contains component temporaryData temporaryData: {poswidth:' '// Record the displacement posheight:' ', // Record tracking:false}}}, methods: {touchstart (e) {if (this.temporaryData.tracking) {
        return} // Whether it is touchif (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.gettouches [0].clientx this.basicdata.start.y = e.targetTouches[0].clientY this.basicdata.end.x = e.targetTouches[0].clientX This.basicData.end. Y = e.argettouches [0].clienty}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) {// Record the slide positionif(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{enclosing basicdata. End. X = e.c. with our fabrication: lientX enclosing basicdata. The end. The y = e.c. with our fabrication: lientY} / / sliding enclosing 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 =falseTransform (index) {if (index > this.basicdata.currentPage) {
        let style = {}
        let visible = 3
        letPerIndex = index - this. Basicdata. CurrentPage / / visible visible quantity before the style of the sliderif (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' + ') '
        }
        returnTransformIndex (index) {// Handle 3D effectif (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>
Copy the code

3. Slip out after condition success, rebound after condition failure

The trigger judgment of the condition is carried out after touchend/mouseup. Here, we first use simple conditions to judge, and give the effect of pop-up and rebound of the header image. The code is as follows

<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>
exportDefault {props: {// myprops: {// myprops: {type: Array,
      default: []
    }
  },
  data () {
    returnBasicdata: {start: {}, // Record the start position end: {}, // record the end position currentPage: TemporaryData contains component temporaryData temporaryData: {poswidth:' '// Record the displacement posheight:' ', // Record tracking:false, // Whether it is sliding to prevent multiple operations from affecting the animation experience:false}}}, methods: {touchstart (e) {if (this.temporaryData.tracking) {
        return} // Whether it is touchif (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.gettouches [0].clientx this.basicdata.start.y = e.targetTouches[0].clientY this.basicdata.end.x = e.targetTouches[0].clientX This.basicData.end. Y = e.argettouches [0].clienty}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) {// Record the slide positionif(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{enclosing basicdata. End. X = e.c. with our fabrication: lientX enclosing basicdata. The end. The y = e.c. with our fabrication: lientY} / / sliding enclosing 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// If the width of the slide exceeds 100 pixels, the slide will be triggeredif(Math. Abs (enclosing temporaryData. Poswidth) > = 100) {/ / a simple setting of ultimate displacement x 200 - pixel offsetletratio = 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 (enclosing temporaryData. Poswidth * thewire) enclosing temporaryData. Opacity = 0 is sliding into} / / does not meet the conditionselse{this. TemporaryData. Poswidth = 0 enclosing temporaryData. Posheight = 0}}, / / the homepage style switch transform (index) {if (index > this.basicdata.currentPage) {
        let style = {}
        let visible = 3
        letPerIndex = index - this. Basicdata. CurrentPage / / visible visible quantity before the style of the sliderif (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' + ') '
        }
        returnTransformIndex (index) {// Handle 3D effectif (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>
Copy the code

4. Slide out and stack the next image to the top

Restack is the last and most important and complex feature of components. In our code, the order of stack-items depends on the binding: transformIndex style and transform function. If currentPage +1 is used, the stack will be restacked.

The answer is not that simple, because our slide is animated and will take 300ms, while the rearrangement caused by the currentPage change will change immediately and interrupt the animation. So we need to change the sorting condition of the Transform function first and then change currentPage.

The specific implementation

  • Modify the sorting condition of the transform function
  • Let currentPage + 1
  • Add the onTransitionEnd event and re-place the stack list after the slide out

The code is as follows:

<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>
exportDefault {props: {// myprops: {// myprops: {type: Array,
      default: []
    }
  },
  data () {
    returnBasicdata: {start: {}, // Record the start position end: {}, // record the end position currentPage: TemporaryData contains component temporaryData temporaryData: {poswidth:' '// Record the displacement posheight:' ', // Record the displacement lastPosWidth:' '// Record the last final displacement lastPosHeight:' '// Record the last final displacement tracking:false, // Whether it is sliding to prevent multiple operations from affecting the animation experience:falseSwipe: // Whether animating is enabled for the top image, default is no opacity: 1, // Record the top image transparency swipe:false}}}, methods: {touchstart (e) {if (this.temporaryData.tracking) {
        return} // Whether it is touchif (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.gettouches [0].clientx this.basicdata.start.y = e.targetTouches[0].clientY this.basicdata.end.x = e.targetTouches[0].clientX This.basicData.end. Y = e.argettouches [0].clienty}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) {// Record the slide positionif(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{enclosing basicdata. End. X = e.c. with our fabrication: lientX enclosing basicdata. The end. The y = e.c. with our fabrication: lientY} / / sliding enclosing 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// If the width of the slide exceeds 100 pixels, the slide will be triggeredif(Math. Abs (enclosing temporaryData. Poswidth) > = 100) {/ / a simple setting of ultimate displacement x 200 - pixel offsetlet 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 record finally sliding distance. TemporaryData. LastPosWidth = this. TemporaryData. Poswidth enclosing temporaryData. LastPosHeight = Enclosing temporaryData. Posheight / / currentPage + 1 induced changes this. Sort basicdata. CurrentPage + = 1 / switch/currentPage, whole to change the dom, Slide the first layer to zero this.$nextTick(() = > {this. TemporaryData. Poswidth = 0 enclosing temporaryData. Posheight = 0 enclosing temporaryData. Opacity = 1}) / / does not meet the conditions of the slide }else {
        this.temporaryData.poswidth = 0
        this.temporaryData.posheight = 0
        this.temporaryData.swipe = false}}, onTransitionEnd (index) {// After dom changes, the animation slide sequence being executed has changed to the previous levelif (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
        letPerIndex = index - this. Basicdata. CurrentPage / / visible visible quantity before the style of the sliderif (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' + ') '
        }
        returnStyle // After the sliding module is released}else if (index === this.basicdata.currentPage - 1) {
        letStyle = {} // Continue to execute animation 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'
        returnTransformIndex (index) {// Handle 3D effectif (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>
Copy the code

Ok ~ After completing the above four steps, the basic functions of the stack components have been implemented

The principle of Angle offset is to record the user’s touch position every time when the user touches, and calculate the maximum offset Angle. When the sliding displacement occurs, the Angle is linearly increased to the maximum offset Angle. To do this in the stack:

  • Touchmove calculates the desired Angle and direction
  • Angle to zero on touchEnd and onTransitionEnd

To determine the slipping-out area ratio, the offset area is calculated mainly by the offset, so as to obtain the area ratio and complete the judgment

Complete code anddemoCan be found ingithubView the source code, here will not be posted

Thank you for reading this article. If you like, you can give it to ⭐️ on Github. Finally, I wish you can find your ex-girlfriend 💚 on Tantan

Share another vue-Slider component I wrote vue-Consise-Slider

Recently, I am looking for a new job in Guangzhou. I have three years of front-end experience and am familiar with VUE. Friends with job introductions can contact me via email [email protected]