交互友好的导航栏

标签:UX, 人机交互, react-hooks
分类:技术
4分钟阅读
概述:页面滚动行为下,让导航栏具备友好访问特性;并给出基本的代码实现和优化建议

背景

对于页面(不局限在 web 端网页),吾辈总是会关注其导航栏(菜单栏)是否具有友好访问的特性,是吾辈(对于站点整体体验)评价体系下的一大加分点 👍。

如何算得上是具有“友好访问”特性呢,其中很重要的一点表现是:无论在页面(纵向)滚动到任意位置,总能够让用户(或许通过一定的交互条件)便捷的访问到导航栏。

常见的“反面”例子是:当用户滚动一段距离后,当想要再次访问(点击)导航栏时,只能选择再次滚动回页面顶部,(“负面代价”是)丢失当前的滚动进度。

当然 UI 上确保导航栏友好的设计方案多样:

  1. 总是将导航栏固定在页面的特定区域,不随滚动而消失(比如固定在页面顶部、底部);
  2. 为了确保阅读的清爽,只在特定情况下才呼出导航栏;
  3. ...

正题

本博客站点最终选定了上面提及的第二点来保证导航栏的友好访问特性,更进一步确定的表现效果是:

当用户往下滚动页面时,隐藏导航栏。而往上滚动页面时,视作用户可能想操作导航栏的场景,此时即(一定动效效果后)显示导航栏 —— 如此的显隐切换机制。

接下来就是如何转换为代码实现了。

本文以 React 生态为例,通过自定义 hook: useScrollVisible.ts 来支持如上的显隐切换逻辑。

至于导航栏(组件)具体的显隐切换,就是对该 Hook 的调用以及 CSS (或 JS ) 控制样式的细节了,本文不作展开。

⚠️ 简易版本

useScrollVisible-old.ts
export function useScrollVisible() {
  // const [prevScrollPos, setPrevScrollPos] = React.useState<number>(0); // ❌ cause re-render problem!
  const prevScrollPosRef = React.useRef(0);
  const [visible, toggleVisible] = React.useState<boolean>(true);

  React.useEffect(() => {
    function hdlScroll() {
      const curScrollPos = window.scrollY;

      toggleVisible(curScrollPos <= prevScrollPosRef.current);
      prevScrollPosRef.current = curScrollPos;
    }

    window.addEventListener("scroll", hdlScroll);
    return () => {
      window.removeEventListener("scroll", hdlScroll);
    };
  }, []);

  return { visible };
}

需要注意:对滚动数值的缓存应有意识的使用 useRef 来操作,而不应“错误”使用 useState 来存储 prevScrollPosValue。—— 这和 React 的刷新机制有关系,(即:若使用 React useState 声明值,则值更新后,总会有 ui-rerender 情况 —— 这也是 React 的基本优化点),而这里我们只是需要拿取上一次的滚动值作为更新 visible 的判断条件。

优化建议

以及上面还存在两个必要优化:

  1. (业务上的)一般,实际滚动行为中,我们可能是会期望在页面向下滚动特定距离之前,页面的导航栏总是保持在最前显示,此时并不期望隐藏掉导航栏,而之后,才开始动态的显隐切换。(所以我们会在useScrollVisible 的优化版本中额外声明形参:initialScrollLimit 作为触发显隐切换的“边界值”)
  2. (🎈 该点是不易察觉的可优化点!和浏览器本身交互机制有关)PC 端下,实际上面的实现已经能够满足预期,但是在移动端(吾辈测试设备(浏览器)为 iphone#ios#Safari),当页面滚动到最底部时,实际还有一定的“弹性滚动区间”,而立刻触发页面的回滚效果,而向上(小幅度)回滚时,会再次切换 visible (实际这并不在用户的预期) —— 这里的优化点即在于:优化 curScrollPos 的赋值

相关代码段

在列出最终的优化版本之前,先来看两个必要的代码段:

clamp()

lib/math/clamp.ts
export function clamp(val: number, min: number, max: number) {
  const minAssure = Math.min(min, max);
  const maxAssure = Math.max(min, max);
  return Math.min(maxAssure, Math.max(minAssure, val));
}

该“数学”函数的效果时:把一个值限制在一个上限和下限之间,当这个值超过最小值和最大值的范围时,在最小值和最大值之间选择一个值使用。(ref: MDN - clamp()

之后我们会用到这个函数。

预先计算最大可滚动数值

移动端(Safari)上因为具有“页面外的弹性滚动区间”,这会导致 window.scrollY 取值可以超过页面本身的最大可滚动值。

我们有必要计算拿到这个最大可滚动值。

const scrollY_MAX = document.documentElement.scrollHeight - window.innerHeight;

👁️ 优化版本

useScrollVisible.ts
function useScrollVisible(initialScrollLimit = 100) {
  const prevScrollPosRef = React.useRef(0);
  const [visible, toggleVisible] = React.useState<boolean>(true);

  React.useEffect(() => {
    const hdlScroll = () => {
      const curScrollPos = clamp(
        window.scrollY,
        0,
        document.body.scrollHeight - window.innerHeight,
      );
      toggleVisible(
        curScrollPos < prevScrollPosRef.current ||
          prevScrollPosRef.current < initialScrollLimit,
      );
      prevScrollPosRef.current = curScrollPos;
    };

    window.addEventListener("scroll", hdlScroll, { passive: true });
    return () => {
      window.removeEventListener("scroll", hdlScroll);
    };
  }, [initialScrollLimit]);

  return {
    visible,
  };
}

交互友好的导航栏

https://blog.ninoh.cc/loc-blog/18_uiux-details-on-user-scroll[Copy]
本文作者
ninohx96
创建/发布于
Published On
更新/发布于
Updated On
许可协议
CC BY-NC-SA 4.0

转载或引用本文时请遵守“署名-非商业性使用-相同方式共享 4.0 国际”许可协议,注明出处、不得用于商业用途!分发衍生作品时必须采用相同的许可协议。