上下拉-抽屉菜单实现

在开发中一般我们都使用左右侧换菜单,而上下抽屉菜单却很少见,今天我们实现一个简单的上下拉抽屉菜单。

首先我们先直观的看下上下拉抽屉菜单的实现效果:

                                       

在学习抽屉菜单前我们先学习标签的touch事件。

一、事件:

1、标签所对应touch事件:

  • touchstart事件:当手指触摸屏幕时候触发,即使已经有一个手指放在屏幕上也会触发。
  • touchmove事件:当手指在屏幕上滑动的时候连续地触发。在这个事件发生期间,调用preventDefault()事件可以阻止滚动。
  • touchend事件:当手指从屏幕上离开的时候触发。
  • touchcancel事件:当系统停止跟踪触摸的时候触发。关于这个事件的确切出发时间,文档中并没有具体说明,咱们只能去猜测了。

2、而每种touch的event事件都包含如下属性:

  • clientX:触摸目标在视口中的x坐标。
  • clientY:触摸目标在视口中的y坐标。
  • identifier:标识触摸的唯一ID。
  • pageX:触摸目标在页面中的x坐标。
  • pageY:触摸目标在页面中的y坐标。
  • screenX:触摸目标在屏幕中的x坐标。
  • screenY:触摸目标在屏幕中的y坐标。
  • target:触目的DOM节点目标。

3、事件监听处理

a) 声明标签对,并设置ref属性

<div ref="drawer"> 
    ...
</div>

b) 通过ref获取当前div标签,对div标签进行监听

    this.$refs.drawer.addEventListener('touchstart', (event) => {
        console.log('====touchstart======', event)
      })
      this.$refs.drawer.addEventListener('touchmove', (event) => {
        console.log('====touchmove======', event)
      })
      this.$refs.drawer.addEventListener('touchend', (event) => {
        console.log('====touchend======', event)
      })
      this.$refs.drawer.addEventListener('touchcancel', (event) => {
        console.log('=====touchcancel=====', event)
      })

学习完事件后,接下来就实现抽屉效果。首先我们将抽屉布局固定到底部,接下来我们只需要改变抽屉的高度即可实现打开抽屉和关闭抽屉效果。

4、实现步骤:

  • touchstart事件触发时记录用户触摸的位置
  • touchmove事件触发时计算用户移动的距离
  • touchend事件触发时根据用户移动的偏移量的正负值判断方向,并设置抽屉到打开回或关闭状态

a) touchstart事件触发处理:

      this.$refs.drawer.addEventListener('touchstart', (event) => {
        //记录点击坐标
        this.startPos.y = event.targetTouches[0].pageY
        this.defaultHeight = this.currentHeight
      })

b) touchmove事件计算处理

      this.$refs.drawer.addEventListener('touchmove', (event) => {
        //计算移动距离
        if (event.targetTouches.length > 1) {
          return
        }
        let touch = event.targetTouches[0]
        let dy = ((this.startPos.y - touch.pageY) * 100) / 667

        if (this.isToped && !this.isScollToTop && dy < 0) {
          this.endPos.y = touch.pageY
          this.currentHeight = Math.max(
            this.window.startHeight,
            this.defaultHeight + dy
          )
          console.log(this.currentHeight)
          event.preventDefault()
        } else if (this.isToped && dy > 0 && this.isScollToTop) {
          //向上
          event.preventDefault()
          this.endPos.y = touch.pageY
          this.currentHeight = Math.min(
            this.window.endHeight,
            this.defaultHeight + dy
          )
          console.log(this.currentHeight)
        }
      })
      

c) touchend事件触发时处理:

    this.$refs.drawer.addEventListener('touchend', (event) => {
        console.log('====touchend======', event)
        // 阈值 = 差值的20%
        const threshold =
          Math.abs(this.defaultHeight - this.currentHeight) * 0.2
        if (!this.isToped || threshold === 0) {
          return
        }
        const scrollDy = Math.abs(this.endPos.y - this.startPos.y)
        const isDirectorTop = this.endPos.y - this.startPos.y < 0
        const isToTop =
          (isDirectorTop && scrollDy > threshold) ||
          (!isDirectorTop && scrollDy < threshold)

        // 60Hz, 16.6ms
        let id = setInterval(() => {
          if (isToTop) {
            this.isScollToTop = false
            this.currentHeight = this.window.endHeight
            clearInterval(id)
          } else {
            this.currentHeight = this.window.startHeight
            this.isScollToTop = true
            clearInterval(id)
          }
        }, 10)
      })
      

d)用户点击头部打开或关闭抽屉事件处理:

  openDrawable() {
      // 60Hz, 16.6ms
      let id = setInterval(() => {
        if (this.isScollToTop) {
          this.isScollToTop = false
          this.currentHeight = this.window.endHeight
          clearInterval(id)
        } else {
          this.currentHeight = this.window.startHeight
          this.isScollToTop = true
          clearInterval(id)
        }
      }, 10)
    },

最后附上完整抽屉组件分装代码:

<template>
  <div class="VerticalDrawable" :style="heightStyle" ref="drawer">
    <div class="title" @click="openDrawable">{{ title }}</div>
    <div class="listview" @scroll.passive="onListScrolled($event)">
      <div v-for="(item, index) in list" :key="index">
        <div class="listItem">{{ item }}</div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  components: {},
  props: {
    title: {
      type: String,
      default: '图书馆借阅记录',
    },
    list: {
      type: Array,
      default: () => {
        return []
      },
    },
  },
  data() {
    return {
      isToped: true, //判断列表的第一个元素是否位于顶部
      isScollToTop: true, //true 向顶部滑动
      defaultHeight: 46, //记录默认高度 %
      currentHeight: 46, //记录当前窗体高度%
      window: {
        startHeight: 46,
        endHeight: 82,
      },
      startPos: {
        y: 0,
      },
      endPos: {
        y: 0,
      },
    }
  },
  computed: {
    heightStyle() {
      return {
        height: this.currentHeight + '%',
      }
    },
  },
  created() {},
  mounted() {
    this.handleTounchListener()
  },
  methods: {
    openDrawable() {
      // 60Hz, 16.6ms
      let id = setInterval(() => {
        if (this.isScollToTop) {
          this.isScollToTop = false
          this.currentHeight = this.window.endHeight
          clearInterval(id)
        } else {
          this.currentHeight = this.window.startHeight
          this.isScollToTop = true
          clearInterval(id)
        }
      }, 10)
    },
    onListScrolled(event) {
      let top = event.target.scrollTop
      this.isToped = top === 0
    },
    handleTounchListener() {
      this.$refs.drawer.addEventListener('touchstart', (event) => {
        //记录点击坐标
        this.startPos.y = event.targetTouches[0].pageY
        this.defaultHeight = this.currentHeight
      })
      this.$refs.drawer.addEventListener('touchmove', (event) => {
        //计算移动距离
        if (event.targetTouches.length > 1) {
          return
        }
        let touch = event.targetTouches[0]
        let dy = ((this.startPos.y - touch.pageY) * 100) / 667

        if (this.isToped && !this.isScollToTop && dy < 0) {
          this.endPos.y = touch.pageY
          this.currentHeight = Math.max(
            this.window.startHeight,
            this.defaultHeight + dy
          )
          console.log(this.currentHeight)
          event.preventDefault()
        } else if (this.isToped && dy > 0 && this.isScollToTop) {
          //向上
          event.preventDefault()
          this.endPos.y = touch.pageY
          this.currentHeight = Math.min(
            this.window.endHeight,
            this.defaultHeight + dy
          )
          console.log(this.currentHeight)
        }
      })
      this.$refs.drawer.addEventListener('touchend', (event) => {
        console.log('====touchend======', event)
        // 阈值 = 差值的20%
        const threshold =
          Math.abs(this.defaultHeight - this.currentHeight) * 0.2
        if (!this.isToped || threshold === 0) {
          return
        }
        const scrollDy = Math.abs(this.endPos.y - this.startPos.y)
        const isDirectorTop = this.endPos.y - this.startPos.y < 0
        const isToTop =
          (isDirectorTop && scrollDy > threshold) ||
          (!isDirectorTop && scrollDy < threshold)

        // 60Hz, 16.6ms
        let id = setInterval(() => {
          if (isToTop) {
            this.isScollToTop = false
            this.currentHeight = this.window.endHeight
            clearInterval(id)
          } else {
            this.currentHeight = this.window.startHeight
            this.isScollToTop = true
            clearInterval(id)
          }
        }, 10)
      })
      this.$refs.drawer.addEventListener('touchcancel', (event) => {
        console.log('=====touchcancel=====', event)
      })
    },
    onClickMore() {
      this.$emit('onClickMore')
    },
    btnClick(item) {
      debugger
      this.$emit('returnBookClick', item)
    },
    cellClick(item) {
      debugger
      this.$emit('bookCellClick', item)
    },
  },
}
</script>

<style lang="scss" scoped>
.VerticalDrawable {
  position: fixed;
  left: 0px;
  right: 0px;
  bottom: 0px;
  overflow-y: hidden;
  background: #ffffff;

  .title {
    line-height: 50px;
    text-align: center;
    background: chocolate;
  }
  .listview {
    display: flex;
    height: 100%;
    flex-direction: column;
    overflow-y: auto;
    .listItem {
      line-height: 50px;
      text-align: center;
      border-bottom: 1px solid #999999;
      margin-bottom: -1px;
    }
  }
}
</style>

在父组件中使用抽屉组件如下:

<template>
  <div class="BookHome">
    <div class="toplayout">
      <img :src="srcUrl" alt="">
      <div>顶部样式布局</div>
    </div>
    <div class="drawer">
      <VerticalDrawer 
      :list='list' />
    </div>
  </div>
</template>

<script>
  import VerticalDrawer from "./VerticalDrawer";
  export default {
    components: {
      VerticalDrawer,
    },
    data() {
      return {
        list: [],
        srcUrl:require('../assets/logo.png'),
      }
    },
    created () {
      for(var i=0;i<100;i++){
        this.list[i] = '第'+i+'条数据';
      }
    },
  }
</script>

<style lang="scss" scoped>
.BookHome{
  display: flex;
  height: 100%;
  flex-direction: column;
  .toplayout{
    height: 50%;
    text-align: center;
    border: 1px solid chocolate;
  }
}

</style>

 

 

 

Logo

前往低代码交流专区

更多推荐