Vue彈性標(biāo)題欄(收縮擴張標(biāo)題欄背景)

前言

仿酷安個人主頁

酷安效果

Video_20210926_015832_562.gif

我的效果

Video_20210926_015909_456.gif

我的思路是把頁面分成兩塊:
1.頂部標(biāo)題欄區(qū)
2.下方主內(nèi)容區(qū)。

頂部標(biāo)題區(qū)分成普通狀態(tài),收縮狀態(tài),擴張狀態(tài)
觸摸屏幕的時候先判斷頂部當(dāng)前處于哪種狀態(tài)。只有處于收縮狀態(tài)時才開啟主內(nèi)容區(qū)域滾動。其他狀態(tài)下主內(nèi)容是無法滾動的,通過改變頭部區(qū)域高度擠壓下方內(nèi)容實現(xiàn)下方偽滾動效果。

<template>
  <div id="root">
    <div id="header" :style="'height:'+navNormalHeight+'px'">
      <div>你好</div>
    </div>
    <div id="content" :style="'height:calc(100% - '+navShrinkHeight+'px)'">
      <div v-for="i in 100">{{i}}</div>
    </div>
  </div>
</template>

先關(guān)閉頁面整體滾動

document.documentElement.style.overflowY = 'hidden'

監(jiān)聽觸摸事件,判斷滑動方向。

這里提一句,判斷滑動方向網(wǎng)上不少帖子是通過在touchstart手指剛接觸屏幕時設(shè)個變量,然后滑動時判斷手指當(dāng)前位置是否大于或小于初始觸摸位置實現(xiàn)的,這顯然是偷懶的做法。剛開始我也這樣做,后面被坑了一把:比如初始觸摸在y=600的位置,此時向上劃到y(tǒng)=400,判斷上滑沒錯,但是不松手下滑到y(tǒng)=500的位置方向判斷就出問題了,因為還是在600上方,所以還是判斷是上滑動作

設(shè)置頂部三個狀態(tài)的高度

      navNormalHeight: 300,//默認300
      navShrinkHeight: 65,//縮小狀態(tài)65
      navExpandHeight: 500//擴張狀態(tài)500

獲取頂部和主內(nèi)容節(jié)點

    let header = document.getElementById('header')
    let content = document.getElementById('content')

設(shè)一個變量記錄上次手指所在位置
let oldY
設(shè)一個變量標(biāo)識方向
let up
監(jiān)聽touchmove事件,獲取第一根手指的觸摸對象(這里就不考慮多指觸摸了)
let touches = ev.touches[0]
手指移動一像素點時給oldY賦值,直接return不進行其他操作。
在移動監(jiān)聽的最后位置再給oldY賦值為當(dāng)前坐標(biāo)。

window.addEventListener('touchmove', ev => {
      let touches = ev.touches[0]
      //第一次移動僅獲取坐標(biāo),不進行其他操作
      if (oldY===undefined){
        oldY=touches.clientY
        return
      }
      //比較新舊坐標(biāo)獲得準(zhǔn)確方向
      up = touches.clientY <= oldY
     /*
      主要邏輯
      */  
      //最后記錄舊坐標(biāo)
      oldY = touches.clientY
    })

處理上劃邏輯

1.頂部未完成收縮
先關(guān)掉主內(nèi)容滾動

if (content.style.overflowY === 'scroll') {
    content.style.overflowY = 'hidden'
  }

計算滑動偏移量
監(jiān)聽touchstart事件,記錄剛開始觸摸的位置。

 window.addEventListener('touchstart', ev => {
      let touches = ev.touches[0];
      //記錄初始位置
      startY = touches.clientY
    })

上滑偏移量=觸摸初始位置-手指當(dāng)前位置

let offY = startY - touches.clientY

頂部區(qū)域高度=頂部默認高度-偏移量

if (header.clientHeight > _this.navShrinkHeight) {
    if (content.style.overflowY === 'scroll') {
        content.style.overflowY = 'hidden'
      }
      let number = _this.navNormalHeight - offY 
      header.style.height = number + 'px'
  }

現(xiàn)在已經(jīng)可以隨著手指的上滑,實現(xiàn)頂部區(qū)域的收縮了,同時下方區(qū)域也在向上填充。


Video_20210926_032021_293.gif

但是如果手指離開后再次觸摸接著上次的位置滑動,會發(fā)現(xiàn)每次都是從默認的高度開始向上縮小,顯然不合理。所以我們再定義一個變量,記錄手指離開時的頂部區(qū)域高度

監(jiān)聽touchend事件

//初始值為默認高度
let lastHeight=this.navNormalHeight
window.addEventListener('touchend', ev => {
      lastHeight = header.clientHeigh
    })

手指離開后記錄頂部高度,下次動態(tài)設(shè)置高度的時候減去默認高度與上次的高度的偏差即可

 let number = _this.navNormalHeight - offY - (_this.navNormalHeight - lastHeight);
  header.style.height = number + 'px'

此時手指離開后重新上滑的高度問題解決。
又發(fā)現(xiàn)一個問題:
我們修改頂部高度的條件是頂部高度大于縮小狀態(tài)高度,到縮小狀態(tài)也停止縮小了,但是高度不對啊,設(shè)好的65不靈了


image.png

我猜測是滑動延時,執(zhí)行延時導(dǎo)致的偏差(移動端點擊事件延遲300毫秒)。只需要在修改高度的語句下調(diào)用下修正函數(shù)即可。

      function formatY(){
        //避免過度縮小
        if (_this.navHeight <= _this.navShrinkHeight) {
          _this.navHeight = _this.navShrinkHeight
        }
        //避免過度擴張
        if (_this.navHeight >= _this.navExpandHeight) {
          _this.navHeight = _this.navExpandHeight
        }
      }

修改完高度如果有偏差就再調(diào)整到設(shè)計的范圍內(nèi)即可。
至此頂部普通狀態(tài)到收縮狀態(tài)的過程寫完了。
2.頂部完成收縮
此時就不用對頂部高度進行修改了。
打開下方主內(nèi)容滑動

        content.style.overflowY = 'scroll'

此時下方可以滑動,但是從未收縮狀態(tài)到收縮狀態(tài)到主內(nèi)容打開滑動過程中一直不松手的話主內(nèi)容動不了,必須抬手重新向上劃才可以滑動主內(nèi)容。缺少連貫性,所以我們手動給主內(nèi)容設(shè)置scrollTop滾動起來。

let offO = offY - (lastHeight - _this.navShrinkHeight)
content.scrollTop = offO

此時有了連貫性,解決下再次觸摸滾動位置不對的問題:
對主內(nèi)容滾動進行監(jiān)聽,停止后記錄位置,
由于js沒有對滾動結(jié)束的監(jiān)聽,只能通過函數(shù)防抖,延時200毫秒獲取最后一次滾動時主內(nèi)容的scrollTop

let timeOut
    content.addEventListener('scroll', ev => {
      if (timeOut !== undefined) {
        clearTimeout(timeOut)
      }
      timeOut = setTimeout(() => {
        lastContentY = content.scrollTop
      }, 200)
    })

修改

 let offO = offY - (lastHeight - _this.navShrinkHeight) + lastContentY
 content.scrollTop = offO

上滑處理完成,下滑舉一反三:

//主內(nèi)容頂部展現(xiàn)
        if (content.scrollTop === 0) {
          if (content.style.overflowY === 'scroll') {
            content.style.overflowY = 'hidden'
            startY = touches.clientY;
            lastHeight=_this.navShrinkHeight
          }
          let offY = touches.clientY - startY
          let newHeight = (lastHeight + offY)
          header.style.height = newHeight + 'px'
          formatHeight()
        }
        //主內(nèi)容頂部未出現(xiàn)
        else {
          let offY = startY - touches.clientY
          if (content.style.overflowY === 'hidden') {
            content.style.overflowY = 'scroll'
          }
          let offO = offY - (lastHeight - _this.navShrinkHeight) + lastContentY
          content.scrollTop = offO
        }

上下滑動已經(jīng)完成了,處理一下松手回彈
這里我們用間歇函數(shù)每5毫秒減去頂部5高度。

window.addEventListener('touchend', ev => {
      lastHeight = header.clientHeight
      //高度大于普通高度,觸發(fā)回彈動畫
      if (lastHeight>=_this.navNormalHeight){
        let interval=setInterval(()=>{
          if (header.clientHeight<=_this.navNormalHeight+5){
            clearInterval(interval)
          }
          let height=header.clientHeight-5
          header.style.height=height+'px'
          if (header.clientHeight<=_this.navNormalHeight+5){
            lastHeight=_this.navNormalHeight
          }
        },5)
      }
    })

添加陰影遮罩

 <div class="header" :style="'height:'+navNormalHeight+'px;'" id="header">
      <div id="headerRoot">
        <img :src="coverImg" class="coverImg">
        <slot name="headerInfo"></slot>
        <div id="mask"></div>
      </div>
    </div>
#mask{
  z-index: 8;
  height: 100%;
  width: 100%;
  position: absolute;
  pointer-events: none;
}

動態(tài)設(shè)置透明度

if (header.clientHeight<=_this.navNormalHeight){
        let temp = _this.navNormalHeight - header.clientHeight
        let val = temp / _this.navNormalHeight
        mask.style.background = 'rgba(0, 0, 0, '+val+')'
  }

完整代碼

<template>
  <div class="root">
    <div class="header" :style="'height:'+navNormalHeight+'px;'" id="header">
      <div id="headerRoot">
        <img :src="coverImg" class="coverImg">
        <div id="imgMask"></div>
        <slot name="headerInfo"></slot>
        <div id="mask"></div>
      </div>
    </div>
    <div class="content" id="content">
      <slot name="content"></slot>
    </div>
  </div>
</template>

<script>
export default {
  name: "Temp",
  props: {
    navNormalHeight: 0,
    navShrinkHeight: 0,
    navExpandHeight: 0,
    coverImg: ''
  },
  mounted() {
    let _this = this;
    let header = document.getElementById('header')
    let content = document.getElementById('content')
    let mask = document.getElementById('mask')

    document.documentElement.style.overflowY = 'hidden'

    let startY, lastContentY = 0
    let oldY
    let up, lastHeight = _this.navNormalHeight
    let beTouch=false

    function start(ev) {
      let touches = ev.touches[0];
      startY = touches.clientY
      beTouch=true
    }


    function move(ev) {
      let touches = ev.touches[0]
      //第一次移動僅獲取坐標(biāo),不進行其他操作
      if (oldY === undefined) {
        oldY = touches.clientY
        return
      }
      up = touches.clientY <= oldY
      // console.log(up);
      //上滑
      if (up) {
        let offY = startY - touches.clientY
        if (header.clientHeight > _this.navShrinkHeight) {
          if (content.style.overflowY === 'scroll') {
            content.style.overflowY = 'hidden'
          }
          let number = _this.navNormalHeight - offY - (_this.navNormalHeight - lastHeight);
          header.style.height = number + 'px'
          formatHeight()
        }
        //頂部收縮完成
        else {
          // console.log(content.style.overflowY)
          //打開主內(nèi)容滑動(下方滑動)
          content.style.overflowY = 'scroll'

          let offO = offY - (lastHeight - _this.navShrinkHeight) + lastContentY
          content.scrollTop = offO
        }
      }
      //下滑
      else {
        //主內(nèi)容頂部展現(xiàn)
        if (content.scrollTop === 0) {
          if (content.style.overflowY === 'scroll') {
            content.style.overflowY = 'hidden'
            startY = touches.clientY;
            lastHeight = _this.navShrinkHeight
          }
          let offY = touches.clientY - startY
          let newHeight = (lastHeight + offY)
          header.style.height = newHeight + 'px'
          formatHeight()
        }
        //主內(nèi)容頂部未出現(xiàn)
        else {
          let offY = startY - touches.clientY
          if (content.style.overflowY === 'hidden') {
            content.style.overflowY = 'scroll'
          }
          let offO = offY - (lastHeight - _this.navShrinkHeight) + lastContentY
          content.scrollTop = offO
        }
      }
      if (header.clientHeight <= _this.navNormalHeight) {
        let temp = _this.navNormalHeight - header.clientHeight
        let val = (temp / _this.navNormalHeight) - 0.2
        mask.style.background = 'rgba(0, 0, 0, ' + val + ')'
      }
      if (header.clientHeight <= _this.navExpandHeight) {
        _this.$emit('navHide', header.clientHeight)
      }
      //電腦端模擬手機滑動到屏幕外修正
      if (touches.clientY < document.documentElement.clientHeight
          && touches.clientX < 0 && touches.clientX > document.documentElement.clientWidth) {
        oldY = touches.clientY
      }
      oldY = touches.clientY
    }


    function end(ev) {
      beTouch=false
      lastHeight = header.clientHeight
      //高度大于普通高度,觸發(fā)回彈動畫
      if (lastHeight >= _this.navNormalHeight) {
        let interval = setInterval(() => {
          if (header.clientHeight <= _this.navNormalHeight + 5) {
            clearInterval(interval)
          }
          let height = header.clientHeight - 5
          header.style.height = height + 'px'
          if (header.clientHeight <= _this.navNormalHeight + 5) {
            lastHeight = _this.navNormalHeight
          }
        }, 5)
      }
    }


    //函數(shù)防抖
    let timeOut
    function beScroll(ev) {
      if (timeOut !== undefined) {
        clearTimeout(timeOut)
      }
      if (beTouch){
        return
      }
      timeOut = setTimeout(() => {
        lastContentY = content.scrollTop
      }, 1)
    }


    function formatHeight() {
      if (header.clientHeight < _this.navShrinkHeight) {
        header.style.height = _this.navShrinkHeight + 'px'
      }
      if (header.clientHeight >= _this.navExpandHeight) {
        header.style.height = _this.navExpandHeight + 'px'
      }
    }

    window.addEventListener('touchstart', start)
    window.addEventListener('touchmove', move)
    window.addEventListener('touchend', end)
    content.addEventListener('scroll', beScroll)

    this.$on('hook:beforeDestroy', () => {
      console.log('銷毀事件')
      window.removeEventListener('touchstart',start)
      window.removeEventListener('touchmove',move)
      window.removeEventListener('touchend',end)
      content.removeEventListener('scroll',beScroll)
      document.documentElement.style.overflowY = 'auto'
    })
  }
}
</script>

<style lang="less" scoped>
.coverImg {
  width: 100%;
  height: 100%;
  object-fit: cover;
  position: absolute;
}

.root {
  height: 100%;
  width: 100%;
  position: fixed;
}

.header {
  width: 100%;
  overflow: hidden;
}

.content {
  height: calc(100% - 65px);
  width: 100%;
  background-color: @bg-color;
  overflow-y: hidden;
}

#mask {
  z-index: 8;
  height: 100%;
  width: 100%;
  position: absolute;
  pointer-events: none;
}

#imgMask {
  height: 100%;
  width: 100%;
  position: absolute;
  background-color: rgba(0, 0, 0, 0.45);
  pointer-events: none;
}

#headerRoot {
  position: relative;
  height: 100%;
  width: 100%;
}
</style>

正常使用,效果還不錯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容