简单分析: h5实现外卖APP的效果

247 阅读4分钟

特点

  • 仅最外部一个滚动条
  • 随着向下滚动,个人感官是,顶部会逐渐减少,底部会逐渐增多,直到一个阈值时,菜单列表开始可以滚动
  • 随着向下滚动,顶部导航会逐渐显示,并最终固定在顶部,中部的tab栏,会逐渐上升,直到顶部导航之下,也会固定在这个位置
  • 如果是向上滚动,个人感官是,首先会让菜单列表滚动到最顶部,直至顶部再无菜单数据,外部才会开始向上滚动,直至滚动到最顶部,这期间tab栏会感觉,从顶部逐渐在下降,直至回归到他的初始位置,然后头部导航可能会逐渐隐藏,直至完全不可见

模拟的效果

外卖APP商家店铺界面效果.gif

逐步分析

主结构

主界面应该分成上中下4部分:最外层黑框是滚动容器,粉色代表顶部,绿色分割头部和主体,蓝色是主体,空白是为了方便区分,实际并不存在

image.png

主结构细化

  • 最外层黑框是滚动容器,粉色代表顶部,绿色分割头部和主体,蓝色是主体,空白是为了方便区分,实际并不存在
  • 顶部的黄色条是顶部导航条
  • 底部的淡紫色部分是商品分类
  • 底部的褐色部分是商品列表
  • 黑色边框之外的部分,让滚动容器出现滚动条

image.png

粉色和黄色外加绿色区域高度的特殊性

  • 黄色区域在粉色区域之内,绿色区域在粉色和蓝色区域之间,他们的位置和高度比较重要。因为黄色区域和绿色区域随着滚动条的滚动,最终的效果是漂浮在页面顶部。那么就需要知道滚动条滚动到什么位置的时候,需要将这两个区域设置为fixed
  • 粉色加绿色区域的高度不会超过1屏的高度

蓝色区域高度特殊性

蓝色区域是没有高度的,他的高度应该是被内容撑出来的

褐色区域高度

褐色区域是没有固定高度的,会随着数据的增多而增加

淡紫色区域高度特殊性

淡紫色区域高度:100vh - 黄色区域高度 - 绿色区域高度,蓝色区域的初始高度也是被淡紫色区域撑出来的

重点:淡紫色区域fixed切换

当滚动的距离大于等于粉色区域高度 - 绿色区域高度,淡紫色区域设置为fixed,否则去掉fixed

黄色区域fixed切换

黄色区域默认就是fixed, 因为他会一直浮动在顶部,某些特殊情况是,可能他初始是完全透明的,但会随着滑动,逐渐显现出来。

绿色区域fixed切换

当滚动距离大于等于粉色区域高度 - 黄色区域高度时,绿色区域设置为fixed, 否则去掉fixed

代码

特别说明: class基本都使用的tailwindcss中的class,仅classify-panel-height为自定义

<!--

@author: pan
@createDate: 2022-12-06 16:10
-->
<script setup lang="ts">
import { ref } from 'vue'

const scrollDomRef = ref<HTMLElement>()
const splitDomRef = ref<HTMLElement>()
const headerNavDomRef = ref<HTMLElement>()
const headerPanelDomRef = ref<HTMLElement>()
const classifyPanelDomRef = ref<HTMLElement>()
const splitDomPositionClass = ref('')
const classifyPanelDomPositionClass = ref('')

function updateSplitDomPosition() {
  const scrollDom = scrollDomRef.value
  if (!scrollDom) return
  const headerPanelDom = headerPanelDomRef.value
  if (!headerPanelDom) return
  const headerNavDom = headerNavDomRef.value
  if (!headerNavDom) return
  const splitDom = splitDomRef.value
  if (!splitDom) return

  const { offsetHeight: headerPanelDomHeight } = headerPanelDom
  const { offsetHeight: headerNavDomHeight } = headerNavDom

  const { scrollTop } = scrollDom
  // 分割面板fixed定位scrollTop阈值 = 头部面板高度 - 头部导航高度
  const fixedHeight = headerPanelDomHeight - headerNavDomHeight
  if (scrollTop >= fixedHeight) {
    // 超过阈值,则使用fixed定位
    splitDomPositionClass.value = 'fixed top-8 left-0 right-0 h-8'
  } else {
    // 反之不使用特殊定位
    splitDomPositionClass.value = ''
  }
}
function updateClassifyPanelDomPosition() {
  const scrollDom = scrollDomRef.value
  if (!scrollDom) return
  const headerPanelDom = headerPanelDomRef.value
  if (!headerPanelDom) return
  const splitDom = splitDomRef.value
  if (!splitDom) return

  const { offsetHeight: headerPanelDomHeight } = headerPanelDom
  const { offsetHeight: splitDomHeight } = splitDom
  const { scrollTop } = scrollDom

  const fixedHeight = headerPanelDomHeight - splitDomHeight
  if (scrollTop >= fixedHeight) {
    // 超过阈值,则使用fixed定位
    classifyPanelDomPositionClass.value = 'fixed top-16 left-0 w-14'
  } else {
    // 反之不使用特殊定位
    classifyPanelDomPositionClass.value = ''
  }
}
function onScroll() {
  updateSplitDomPosition()
  updateClassifyPanelDomPosition()
}
</script>

<template>
  <!-- 最外层的滚动容器 -->
  <div ref="scrollDomRef" class="h-full overflow-auto" @scroll="onScroll">
    <!-- 头部面板包裹容器 -->
    <div ref="headerPanelDomRef" class="bg-pink-100 h-32 relative">
      <div ref="headerNavDomRef" class="h-8">
        <!-- 外面这层div的作用是使界面高度不发生变化 -->
        <div class="h-8 bg-blue-100 fixed top-0 left-0 right-0"></div>
      </div>
    </div>
    <!-- 分割面板包裹容器 -->
    <div class="h-8 bg-green-100">
      <!-- 外面这层div的作用是当splitDomRef为fixed定位时,界面高度不发生变化 -->
      <div
        ref="splitDomRef"
        class="bg-green-100 h-8"
        :class="splitDomPositionClass"
      ></div>
    </div>
    <!-- 主体内容 -->
    <main class="flex flex-row">
      <!-- 侧边导航包裹面板 -->
      <div class="bg-orange-100 w-14 flex-shrink-0">
        <!-- 外面这层div的作用是当classifyPanelDomRef为fixed定位时,界面宽度不发生变化 -->
        <div
          ref="classifyPanelDomRef"
          class="classify-panel-height bg-orange-100"
          :class="classifyPanelDomPositionClass"
        ></div>
      </div>
      <!-- 右侧列表面本 -->
      <div ref="mainListDomRef" class="flex-1 w-0">
        <div v-for="i in 100" :key="i">第{{ i }}</div>
      </div>
    </main>
  </div>
</template>

<style lang="scss" scoped>
// 分类面板高度
.classify-panel-height {
  /*
  为什么是4rem: 是`headerNavDomRef 的 h8` + `splitDomRef 的 h8`
  h8是什么? 是tailwindcss的一个高度值, 实际是2rem, 两个 2rem 就是 4rem
   */
  height: calc(100vh - 4rem);
}
</style>