As a front-end, people who often use netease music to listen to songs, see the effect of netease sliding around to switch pages really a little curious. So I went to Git to find some materials to learn.

Declaration: this article is I found in git above the scroll tab-bar component to write after their own thinking.

Requirement: realize left and right slide switch page

Final rendering

Functional analysis:

  1. Click the top menu to jump to the corresponding page.
  2. Press the screen and swipe left or right to switch pages.
  3. For the first time, only the content on the home page will load.
  4. Other pages, only after entering, will load.

Thinking and analysis:

  1. You need a menu component, click and modifytabIndex(Which page to display)
  2. The need for aScrollTabComponent that controls finger pressing events and controls which to displaytabIndex
  3. The need for aScrollTabColComponent, wrap realPageComponent to control whether or not to load.

Results:

  1. ScrollTab
    1. You need to accept an argument (tabIndex) to control which page is displayed.
    2. After swiping left or right to switch pages, there needs to be an event to synchronizetabIndex
    3. Implement the logic of finger pressing events
  2. ScrollTabColWrap each page and accept a parameterloadTo control whether the current page is loaded.

Implementation process:

Home page is not the focus, directly paste code.

<script setup>

import { ref } from 'vue'
import ScrollTab from './components/ScrollTab.vue'
import ScrollTabCol from './components/ScrollTabCol.vue'
import Page from './components/Page.vue'
let tabIndex = ref(0) // Todo controls which page to display
let loadIndex = ref(0) // Todo controls which page is loaded
// Todo notification page toggle
const selectChange = (value) = > {
  tabIndex.value = value
  loadIndex.value = value
}
</script>

<template>
  <! // Todo top menu -->
  <ul class="label-list">
    <li
      class="label-list-item"
      :class="{ 'select-label-list-item': tabIndex === 0 }"
      @click="tabIndex = 0"
    >1</li>
    <li
      class="label-list-item"
      :class="{ 'select-label-list-item': tabIndex === 1 }"
      @click="tabIndex = 1"
    >2</li>
    <li
      class="label-list-item"
      :class="{ 'select-label-list-item': tabIndex === 2 }"
      @click="tabIndex = 2"
    >3</li>
    <li
      class="label-list-item"
      :class="{ 'select-label-list-item': tabIndex === 3 }"
      @click="tabIndex = 3"
    >4</li>
    <li
      class="label-list-item"
      :class="{ 'select-label-list-item': tabIndex === 4 }"
      @click="tabIndex = 4"
    >5</li>
  </ul>
  <!-- // todo 页面 -->
  <ScrollTab :tabIndex="tabIndex" @selectChange="selectChange">
    <ScrollTabCol class="item" :loading="loadIndex === 0">
      <Page msg="1"></Page>
    </ScrollTabCol>
    <ScrollTabCol class="item" :loading="loadIndex === 1">
      <Page msg="2"></Page>
    </ScrollTabCol>
    <ScrollTabCol class="item" :loading="loadIndex === 2">
      <Page msg="3"></Page>
    </ScrollTabCol>
    <ScrollTabCol class="item" :loading="loadIndex === 3">
      <Page msg="4"></Page>
    </ScrollTabCol>
    <ScrollTabCol class="item" :loading="loadIndex === 4">
      <Page msg="5"></Page>
    </ScrollTabCol>
  </ScrollTab>
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
html.body.div.body.ul.li {
  margin: 0;
  padding: 0;
  list-style: none;
  text-decoration: none;
}
.label-list {
  position: fixed;
  width: 100%;
  z-index: 100;
  display: flex;
  height: 50px;
  line-height: 50px;
}

.label-list-item {
  flex-grow: 1;
  background: # 909399;
  color: #fff;
  text-align: center;
}

.select-label-list-item {
  font-weight: 600;
  background: #67c23a;
}

.item {
  text-align: center;
  background: #409eff;
  line-height: 400px;
  color: #fff;
  font-size: 100px;
  font-weight: 600;
}
</style>

Copy the code

ScrollTabComponent implementation

Thinking and analysis:
  1. Receive a parameter and control event, obviously props and emit.

  2. To achieve the effect of sliding left and right, it is easy to think of a rote map.

  3. Finger presses three events: TouchStart, TouchMove, and TouchEnd.

The general logic is this

  1. According to theChild componentsThe number and width of the screen, first to achieve a horizontal “wheel map”
  2. To realize the finger press time, according to the mouse position when the finger press down and the mouse position when the finger lift, calculate is: left move, right move, no move.
  3. Switch the correspondingtabIndexIn the event
The Template and CSS

That’s not the point. I’ll just paste it

<template>
  <div class="scroll-tab">
    <div
      class="scroll-tab-item"
      :style="{ width: scrollWidth + 'px', transform: `translate(${contentBoxLeft}px, 0)`, transitionDuration: delay + 's' }"
      ref="scrollTabItem"
      @touchstart="handleTouchStart($event)"
      @touchmove="handleTouchMove($event)"
      @touchend="handleTouchEnd($event)"
    >
      <slot></slot>
    </div>
  </div>
</template>
<style>
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
.scroll-tab {
  width: 100%;
  overflow: hidden;
}
.scroll-tab-item {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
}
</style>
Copy the code
logic

Step 1: Click the menu. When switching, slide to the corresponding page.

const props = defineProps({
  tabIndex: Number,})const emit = defineEmits(['selectChange'])
watch(props, () = > {
  contentBoxLeft.value = calcluateBoxLeft(props.tabIndex)
  tabIndex.value = props.tabIndex
})
Copy the code

Step 2: According to the number of sub-components and the width of the screen, calculate the required width scrollWidth

Obviously, this is immediate and needs to be placed in the onMounted life cycle.

ClientWidth and clientHeight will be used in multiple files, so all are removed.

export const clientWidth = document.documentElement.clientWidth ? document.documentElement.clientWidth : document.body.clientWidth
export const clientHeight = document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight
Copy the code
onMounted(() = > {
  calculateScrollWidth()
})
/ /? Calculate scrollWidth
const calculateScrollWidth = () = > {
  children = scrollTabItem.value.children
  length.value = children.length
  scrollWidth.value = children.length * clientWidth 
}
Copy the code

Step 3: Implement the three finger press event

Logical flow:

touchstart:

Record the following states: 1. Whether the finger is pressed down, 2

touchmove:

From the position of the finger when pressed and the current finger position, calculate whether the direction of sliding is up or down or left or right.

If it is left or right, set the latest rotation map position by rate.

touchend:

Calculate the distance of movement based on the last and beginning position of the finger.

Determine whether to switch pages based on the moving distance and tabIndex.

/ * * *@param TabIndex Current page *@param ContentBoxLeft The current rotation position *@param ClientWidth Width of the page *@param Length Number of pages *@param ScrollWidth Indicates the total width of the multicast graph */
const useMoveTouch = (tabIndex, contentBoxLeft, clientWidth, length, scrollWidth) = > {
  let moveOptions = reactive({
    direction: ' './ /? The direction of
    isStart: false./ /? Whether to press
    startPos: null./ /? Record the starting position
    endPos: null / /? Record the end location
  })
  const calcluateBoxLeft = (index) = > {
    window.scrollTo(0.0)
    return -(clientWidth * index)
  }

  const handleTouchStart = (e) = > {
    moveOptions.isStart = true
    moveOptions.startPos = e.touches[0]
    moveOptions.endPos = e.touches[0]}const handleTouchMove = (e) = > {
    / /? Based on the current position of E, determine whether you want to slide up or down or left or right
    let left = e.touches[0].clientX - moveOptions.endPos.clientX
    let top = e.touches[0].clientY - moveOptions.endPos.clientY
    if (moveOptions.isStart) {
      moveOptions.direction = (Math.abs(left) > Math.abs(top)) ? 'X' : 'Y'
    }
    moveOptions.endPos = e.touches[0]
    if (moveOptions.direction === 'X') {
      e.preventDefault()
      // Todo moves left and right
      / /? 1. Define the rate of movement
      let coefficient = 1
      if (contentBoxLeft.value >= 20 || contentBoxLeft.value < -(scrollWidth.value - clientWidth + 20)) {
        coefficient = 0
      } else if (left >= 0 && tabIndex.value === 0) {
        coefficient = 0.3
      } else if (left <= 0 && tabIndex.value === length.value - 1) {
        coefficient = 0.3
      }
      contentBoxLeft.value += coefficient * left
    }
  }
  const handleTouchEnd = (e) = > {
    if(moveOptions.direction ! = ='X') return
    / /? 1. Obtain the distance traveled
    const moveLen = moveOptions.endPos.clientX - moveOptions.startPos.clientX
    if (moveLen > 80&& tabIndex.value ! = =0) {
      tabIndex.value--
    } else if (moveLen < -80&& tabIndex.value ! == length.value -1) {
      tabIndex.value++
    }
    / /? Calculate the offset
    contentBoxLeft.value = calcluateBoxLeft(tabIndex.value)
  }
  return {
    calcluateBoxLeft,
    handleTouchStart,
    handleTouchMove,
    handleTouchEnd
  }
}
Copy the code

Step 4: Notify the parent if a switch is required.

const usetabIndex = (emit) = > {
  let tabIndex = ref(0)
  watch(tabIndex, () = > {
    emit('selectChange', tabIndex.value)
  })
  return tabIndex
}
Copy the code

ScrollTabColComponent implementation

Thinking and analysis:

  1. Receives a parameter that controls whether the page loads

Implementation:

<template>
  <div class="scroll-tab-col" :style="{width:clientWidth + 'px', height: clientHeight + 'px'}">
    <! // todo blank page -->
    <div v-if=! "" show">Enter the blank effect</div>
    <! -- // Todo specific page -->
    <slot v-else></slot> 
  </div>
</template>

<script setup>
import {clientWidth, clientHeight} from './clientParameters'
import {defineProps, watchEffect, ref} from 'vue'
const props = defineProps({
  loading: Boolean
})
// Todo controls whether to display blank pages or mount them
const show = ref(false)
// todo controls mount based on props. Loading
const stop = watchEffect(() = > {
  if (props.loading) {
    show.value = true
    setTimeout(() = > {
      stop()
    })
  }
})

</script>

<style scoped>
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}
</style>
Copy the code

Conclusion thinking

Why do you need control loading?

Speed up the first screen loading, control the loading, the first rendering will only render the content of the first page, not the entire page loading and rendering.

Why use watchEffect and not Watch?

Watch will do, but

Loading =true, and then it doesn’t need to be listened on, and it needs to execute immediately.

One difference between Watch and watchEffect is that watchEffect executes immediately

To use Watch, the logic goes like this

const stop = watch(props, () = > {
  if (props.loading) {
    show.value = true
    setTimeout(() = > {
      stop()
    })
  }
},{
  immediate: true
})
Copy the code

Reference and source code

scroll-tab-bar

The source address