注册
web

面试官:实现一个吸附在键盘上的输入框

实现效果


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



demo 地址:codesandbox.io/p/devbox/ke…

体验地址:7fsqr8-5173.csb.app



666.gif


实现原理


要实现一个吸附在键盘上的 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)`;
});

作者:DAHUIAAAAAA
来源:juejin.cn/post/7338335869709385780

0 个评论

要回复文章请先登录注册