#每日一记# 滚动时动态内阴影效果的实现方式

307 阅读2分钟

关键词:scroll absolute shadow ResizeObserver

最终效果

c.gif

需求

设计师有这样一个需求,当有一个滚动列表时,用一个内阴影来提示用户某个方向能滚动。

下面的列表没有任何提示,用户不清楚是否有更多内容:

image.png

所以如果下面还有更多内容时,出现一个内阴影来提示用户:

image 2.png

如果列表向下滚动,则上方也出现一个内阴影:

image 3.png

问题一:在滚动容器内设置绝对定位不符合预期

首先先绘制 UI,使用 ::before ::after 来绘制内阴影:

<div class='container'>
  <div
    v-for='item in list'
    :key='item.id'
    class='item'
  >
    {{ item.title }}
  </div>
</div>
.container {
  position: relative;
  overflow: auto;
  width: 200px;
  height: 200px;
  border-radius: 4px;

  &::before,
  &::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    height: 10px;
    width: 100%;
    pointer-events: none;
  }

  &::before {
    top: 0;
    background-image: linear-gradient(180deg, rgba(0, 0, 0, 0.2), transparent);
  }

  &::after {
    bottom: 0;
    background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.2), transparent);
  }
}

.item {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 32px;
}

a.gif

运行下来发现,阴影居然跟着子元素一起滚动了。

为此得再添加一个容器来单独存放伪类元素。

<div class='container'>
  <div class='scroll'>  /* 额外添加一个容器 */
    <div
      v-for='item in list'
      :key='item.id'
      class='item'
    >
      {{ item.title }}
    </div>
  </div>
</div>
.scroll {
  overflow: auto; // 把 .container 的滚动设置移到这里
  width: 100%;
  height: 100%;
}

b.gif

问题二:如何监听滚动内容尺寸的变化

监听元素尺寸变化的方法通常使用 ResizeObserver 对象,但是现在缺少一个能体现滚动内容高度的容器。因为 div.scroll 虽然是滚动容器,但是它自身的高度是固定的。

所以我们还得加一个容器,这个容器在 div.scroll 内,它的高度由子元素撑开。

添加一个 div.list

<div class='container'>
  <div class='scroll'>
    <div class='list'>
      <div
        v-for='item in list'
        :key='item.id'
        class='item'
      >
        {{ item.title }}
      </div>
    </div>
  </div>
</div>

逻辑部分

接下来我们编写逻辑,我们需要在三个时机更新阴影显示状态:

  • 列表首次渲染
  • 尺寸发生变化
  • 滚动时
// 列表首次渲染 和 尺寸发生变化
onMounted(() => {
  const observer = new ResizeObserver(() => {
    checkShadowShowStatus()
  })

  observer.observe(listRef.value)
})
/* 滚动时 */
<div class='container'>
  <div 
    class='scroll' 
    @scroll='checkShadowShowStatus'
  >
    <div class='list'>
      <div
        v-for='item in list'
        :key='item.id'
        class='item'
      >
        {{ item.title }}
      </div>
    </div>
  </div>
</div>

总结

image 4.png

c.gif

代码 最后附上所有代码:

<template>
  <div
    :class='["container", {
      "top-shadow": showTopShadow,
      "bottom-shadow": showBottomShadow
    }]'
  >
    <div
      ref='scrollRef'
      class='scroll'
      @scroll='checkShadowShowStatus'
    >
      <div
        ref='listRef'
        class='list'
      >
        <div
          v-for='item in list'
          :key='item.id'
          class='item'
        >
          {{ item.title }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { onMounted, ref } from 'vue'

const list = ref(
  Array(3).fill(null).map((item, index) => ({
    id: index,
    title: `Item ${index + 1}`,
  })),
)

setTimeout(() => {
  list.value = Array(20).fill(null).map((item, index) => ({
    id: index,
    title: `Item ${index + 1}`,
  }))
}, 2000)

const scrollRef = ref(null)
const listRef = ref(null)
const showTopShadow = ref(false)
const showBottomShadow = ref(false)

onMounted(() => {
  const observer = new ResizeObserver(() => {
    checkShadowShowStatus()
  })

  observer.observe(listRef.value)
})

function checkShadowShowStatus () {
  const {
    scrollTop,
    scrollHeight,
    clientHeight,
  } = scrollRef.value

  showTopShadow.value = scrollTop > 0
  showBottomShadow.value = scrollTop + clientHeight < scrollHeight
}

</script>
<style
  lang='scss'
  scoped
>
.container {
  position: relative;
  width: 200px;
  height: 200px;
  overflow: hidden;
  border-radius: 4px;

  &.top-shadow::before,
  &.bottom-shadow::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    height: 10px;
    width: 100%;
    pointer-events: none;
  }

  &.top-shadow::before {
    top: 0;
    background-image: linear-gradient(180deg, rgba(0, 0, 0, 0.2), transparent);
  }

  &.bottom-shadow::after {
    bottom: 0;
    background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.2), transparent);
  }
}

.scroll {
  overflow: auto;
  width: 100%;
  height: 100%;
}

.item {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 32px;
}
</style>