实现一个吸附在键盘上的输入框

7540次阅读 130人点赞 作者: WuBin 发布时间: 2024-03-12 09:56:12
扫码到手机查看

实现原理

话不多说,先上效果和 demo 地址:

demo 地址:https://codesandbox.io/p/devbox/keyboard-7fsqr8?file=%2Fsrc%2Fkeyboard.ts%3A54%2C24,体验地址:https://7fsqr8-5173.csb.app/

要实现一个吸附在键盘上的 input,可以分为以下步骤:

  1. 监听键盘高度的变化
  2. 获取「键盘顶部距离视口顶部的高度」
  3. 设置 input 的位置

第一步:监听监听键盘键盘高度的变化

要监听键盘高度的变化,我们得先看看在键盘展开或收起的时候,分别会触发哪些浏览器事件:

iOS 和部分 Android 浏览器

展开:键盘展示时会依次触发 visualViewport resize -> focusin -> visualViewport scroll,部分情况下手动调用 input.focus 不触发 focusin

收起:键盘收起时会依次触发 visualViewport resize -> focusout -> visualViewport scroll

其他 Android 浏览器

  • 展开:键盘展示的时候会触发一段连续的 window resize,约过 200 毫秒稳定

    收起:键盘收起的时候会触发一段连续的 window resize,约过 200 毫秒稳定,但是部分手机上有些异常的 case:键盘收起时 viewport 会先变小,然后变大,最后再变小

总结两者来看,我们要监听键盘高度的变化,可以添加以下监听事件:

if (window.visualViewport) {
  window.visualViewport?.addEventListener("resize", listener);
  window.visualViewport?.addEventListener("scroll", listener);
} else {
  window.addEventListener("resize", listener);
}

window.addEventListener("focusin", listener);
window.addEventListener("focusout", listener);

如何获取键盘展开和收起状态

在实际业务中,获取键盘展开和收起的状态,同样很常见,要完成状态的判断,我们可以设定以下规则:

判断键盘展开:当 visualViewport resize/window.reszie、visualViewport scroll、focusin 任意一个事件触发时,如果高度减少,并且屏幕减少的高度(键盘高度)大于 200px 时,判断键盘为展开状态(由于 focusin 部分情况下不触发,所以还需要监听其他事件辅助判断键盘是否为展开状态)

判断键盘收起:当 visualViewport resize/window.reszie、visualViewport scroll、focusout 任意一个事件触发时,如果高度增加,并且屏幕减少的高度(键盘高度)小于 200px,判断键盘为收起状态

// 获取当前视口高度
const height = window.visualViewport
  ? window.visualViewport.height
  : window.innerHeight;
  
// 获取视口增量:视口高度 - 上次获取的视口高度
const diffHeight = height - lastWinHeight;

// 获取键盘高度:默认屏幕高度 - 当前视口高度
const keyboardHeight = DEFAULT_HEIGHT - height;

// 如果高度减少,且键盘高度大于 200,则视为键盘弹起
if (diffHeight < 0 && keyboardHeight > 200) {
    onKeyboardShow();
} else if (diff > 0) {
    onKeyboardHide();
}

同时,为了避免“收起时 viewport 会先变小,然后变大,最后再变小”这种情况,我们需要在展开收起状态发生变化的时候加一个200毫秒的防抖,避免键盘状态频繁改变执行“收起 -> 展开 -> 收起”的逻辑

let canChangeStatus = true;

function onKeyboardShow({ height, top }) {
    if (canChangeStatus) {
      canChangeStatus = false;
      setTimeout(() => {
          callback();
          canChangeStatus = true;
      }, 200);
    }
}

第二步:获取键盘顶部距离视口顶部的高度

在 safari 浏览器或者部分安卓手机的浏览器中,在点击输入框的时候,可以看到页面会滚动到输入框所在位置(这是想让被软键盘遮挡的部分展示出来),这个时候,其实是触发了虚拟视口 visualViewport 的 scroll 事件,让页面整体往上顶,即使是 fixed 定位也不例外,因此要获取「键盘顶部距离视口顶部的高度」,我们需要进行如下计算:

键盘顶部距离视口顶部的高度 = 视口当前的高度 + 视口滚动上去高度

// 获取当前视口高度
const height = window.visualViewport ? window.visualViewport.height : window.innerHeight;
// 获取视口滚动高度
const viewportScrollTop = window.visualViewport?.pageTop || 0;
// 获取键盘顶部距离视口顶部的距离,这里是关键
const keyboardTop = height + viewportScrollTop;

第三步:设置 input 的位置

我们先设置 input 的 css 样式

input {
    position: absolute;
    top: 0;
    left: 0;
    width: 100vw;
    height: 50px;
    transition: all .3s;
}

然后再动态调整 input 的 translateY,让 input 可以配合键盘移动,为了保证 input 能够露出,还需要用上一步计算好的「键盘距离页面顶部高度」再减去「元素高度」,从而获得「当前元素的位移」:

当前元素的位移 = 键盘距离页面顶部高度 - 元素高度

// input 的 position 为 absolute、top 为 0
keyboardObserver.on(KeyboardEvent.PositionChange, ({ top }) => {
  input.style.tranform = `translateY(${top - input.clientHeight}px)`;
});

完整代码

import EventEmitter from "eventemitter3";

// 默认屏幕高度
const DEFAULT_HEIGHT = window.innerHeight;
const MIN_KEYBOARD_HEIGHT = 200;

// 键盘事件
export enum KeyboardEvent {
  Show = "Show",
  Hide = "Hide",
  PositionChange = "PositionChange",
}

interface KeyboardInfo {
  height: number;
  top: number;
}

class KeyboardObserver extends EventEmitter {
  inited = false;
  lastWinHeight = DEFAULT_HEIGHT;
  canChangeStatus = true;

  _unbind = () => {};

  // 键盘初始化
  init() {
    if (this.inited) {
      return;
    }
    
    const listener = () => this.adjustPos();

    if (window.visualViewport) {
      window.visualViewport?.addEventListener("resize", listener);
      window.visualViewport?.addEventListener("scroll", listener);
    } else {
      window.addEventListener("resize", listener);
    }

    window.addEventListener("focusin", listener);
    window.addEventListener("focusout", listener);

    this._unbind = () => {
      if (window.visualViewport) {
        window.visualViewport?.removeEventListener("resize", listener);
        window.visualViewport?.removeEventListener("scroll", listener);
      } else {
        window.removeEventListener("resize", listener);
      }

      window.removeEventListener("focusin", listener);
      window.removeEventListener("focusout", listener);
    };
    
    this.inited = true;
  }

  // 解绑事件
  unbind() {
    this._unbind();
    this.inited = false;
  }

  // 调整键盘位置
  adjustPos() {
    // 获取当前视口高度
    const height = window.visualViewport
      ? window.visualViewport.height
      : window.innerHeight;

    // 获取键盘高度
    const keyboardHeight = DEFAULT_HEIGHT - height;
    
    // 获取键盘顶部距离视口顶部的距离
    const top = height + (window.visualViewport?.pageTop || 0);

    this.emit(KeyboardEvent.PositionChange, { top });

    // 与上一次计算的屏幕高度的差值
    const diffHeight = height - this.lastWinHeight;

    this.lastWinHeight = height;

    // 如果高度减少,且减少高度大于 200,则视为键盘弹起
    if (diffHeight < 0 && keyboardHeight > MIN_KEYBOARD_HEIGHT) {
      this.onKeyboardShow({ height: keyboardHeight, top });
    } else if (diffHeight > 0) {
      this.onKeyboardHide({ height: keyboardHeight, top });
    }
  }

  onKeyboardShow({ height, top }: KeyboardInfo) {
    if (this.canChangeStatus) {
      this.emit(KeyboardEvent.Show, { height, top });
      this.canChangeStatus = false;
      this.setStatus();
    }
  }

  onKeyboardHide({ height, top }: KeyboardInfo) {
    if (this.canChangeStatus) {
      this.emit(KeyboardEvent.Hide, { height, top });
      this.canChangeStatus = false;
      this.setStatus();
    }
  }

  setStatus() {
    const timer = setTimeout(() => {
      clearTimeout(timer);
      this.canChangeStatus = true;
    }, 300);
  }
}

const keyboardObserver = new KeyboardObserver();

export default keyboardObserver;

使用:

keyboardObserver.on(KeyboardEvent.PositionChange, ({ top }) => {
  input.style.tranform = `translateY(${top - input.clientHeight}px)`;
});

相关资料

点赞 支持一下 觉得不错?客官您就稍微鼓励一下吧!
关键词:移动端,输入框,输入框固定
推荐阅读
  • uniapp实现被浏览器唤起的功能

    当用户打开h5链接时候,点击打开app若用户在已经安装过app的情况下直接打开app,若未安装过跳到应用市场下载安装这个功能在实现上主要分为两种场景,从普通浏览器唤醒以及从微信唤醒。

    9603次阅读 623人点赞 发布时间: 2022-12-14 16:34:53 立即查看
  • Vue

    盘点Vue2和Vue3的10种组件通信方式

    Vue中组件通信方式有很多,其中Vue2和Vue3实现起来也会有很多差异;本文将通过选项式API组合式API以及setup三种不同实现方式全面介绍Vue2和Vue3的组件通信方式。

    4297次阅读 317人点赞 发布时间: 2022-08-19 09:40:16 立即查看
  • JS

    几个高级前端常用的API

    推荐4个前端开发中常用的高端API,分别是MutationObserver、IntersectionObserver、getComputedstyle、getBoundingClientRect、requ...

    14452次阅读 948人点赞 发布时间: 2021-11-11 09:39:54 立即查看
  • PHP

    【正则】一些常用的正则表达式总结

    在日常开发中,正则表达式是非常有用的,正则表达式在每个语言中都是可以使用的,他就跟JSON一样,是通用的。了解一些常用的正则表达式,能大大提高你的工作效率。

    13497次阅读 491人点赞 发布时间: 2021-10-09 15:58:58 立即查看
  • 【中文】免费可商用字体下载与考证

    65款免费、可商用、无任何限制中文字体打包下载,这些字体都是经过长期验证,经得住市场考验的,让您规避被无良厂商起诉的风险。

    12015次阅读 963人点赞 发布时间: 2021-07-05 15:28:45 立即查看
  • Vue

    Vue3开发一个v-loading的自定义指令

    在vue3中实现一个自定义的指令,有助于我们简化开发,简化复用,通过一个指令的调用即可实现一些可高度复用的交互。

    16376次阅读 1307人点赞 发布时间: 2021-07-02 15:58:35 立即查看
  • JS

    关于手机上滚动穿透问题的解决

    当页面出现浮层的时候,滑动浮层的内容,正常情况下预期应该是浮层下边的内容不会滚动;然而事实并非如此。在PC上使用css即可解决,但是在手机端,情况就变的比较复杂,就需要禁止触摸事件才可以。

    15187次阅读 1234人点赞 发布时间: 2021-05-31 09:25:50 立即查看
  • Vue

    Vue+html2canvas截图空白的问题

    在使用vue做信网单页专题时,有海报生成的功能,这里推荐2个插件:一个是html2canvas,构造好DOM然后转canvas进行截图;另外使用vue-canvas-poster(这个截止到2021年3月...

    29849次阅读 2347人点赞 发布时间: 2021-03-02 09:04:51 立即查看
  • Vue

    vue-router4过度动画无效解决方案

    在初次使用vue3+vue-router4时候,先后遇到了过度动画transition进入和退出分别无效的情况,搜遍百度没没找到合适解决方法,包括vue-route4有一些API都进行了变化,以前的一些操...

    25911次阅读 1994人点赞 发布时间: 2021-02-23 13:37:20 立即查看
交流 收藏 目录