前言
仿酷安個人主頁
酷安效果

我的效果

我的思路是把頁面分成兩塊:
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ū)域也在向上填充。

但是如果手指離開后再次觸摸接著上次的位置滑動,會發(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不靈了

我猜測是滑動延時,執(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>
正常使用,效果還不錯。