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

公众号:程序员白特,欢迎一起交流学习~

来源:DAHUIAAAAAA,https://juejin.cn/post/7338335869709385780?searchId=20240424162151EE1DFC79521D9C9269CA

实现效果

话不多说,先上效果和 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)`;  
});
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在金属锌表面吸附有机分子的计算方法与上述步骤类似,但是需要注意以下几点: 1.选择合适的交换-相关泛函 对于含有有机分子的金属表面体系,通常需要选择合适的交换-相关泛函来计算体系的能量和电荷密度等性质。例如,可以采用GGA(广义梯度近似)泛函,如PBE泛函或PW91泛函,或者采用更高级别的泛函,如HSE06混合泛函等。 2.考虑溶剂效应 在实际的表面吸附过程中,通常会涉及到溶剂的存在。因此,需要在模拟中考虑溶剂效应。可以采用VASP中的NEB(能量势垒)计算方法,来模拟表面吸附过程中的溶剂效应。 3.选择合适的赝势 对于含有金属元素的体系,需要选择合适的赝势,以考虑核心电子的效应。在VASP中,可以采用多种类型的赝势,如PAW(投影缀加波)赝势等,以考虑金属锌的核心电子效应。 以下是针对金属锌表面吸附有机分子的一些参考代码: 1. 生成金属锌表面的超胞模型 ``` Zn bulk 1.0 4.03000 0.00000 0.00000 0.00000 4.03000 0.00000 0.00000 0.00000 2.62000 Zn 4 direct 0.00000 0.00000 0.00000 0.50000 0.50000 0.00000 0.50000 0.00000 0.50000 0.00000 0.50000 0.50000 ``` 2. 初步结构优化 ``` ISIF = 2 IBRION = 2 NSW = 100 POTIM = 0.2 ``` 3. 生成有机分子的POSCAR文件 ``` Molecule 1.0 4.000000000000000 0.0000000000000000 0.0000000000000000 -0.6430000000000000 0.0000000000000000 0.0000000000000000 -1.4730000000000000 0.0000000000000000 0.0000000000000000 -2.3030000000000000 0.0000000000000000 -0.8710000000000000 -0.6430000000000000 0.0000000000000000 -0.8710000000000000 -1.4730000000000000 0.0000000000000000 -0.8710000000000000 -2.3030000000000000 0.0000000000000000 -1.7420000000000000 -0.6430000000000000 0.0000000000000000 -1.7420000000000000 -1.4730000000000000 0.0000000000000000 -1.7420000000000000 -2.3030000000000000 C N O 3 3 3 Direct 0.1761 0.0000 0.4731 0.1761 0.0000 0.3031 0.1761 0.0000 0.1331 0.0000 0.1761 0.4731 0.0000 0.1761 0.3031 0.0000 0.1761 0.1331 0.8239 0.0000 0.4731 0.8239 0.0000 0.3031 0.8239 0.0000 0.1331 0.0000 0.8239 0.4731 0.0000 0.8239 0.3031 0.0000 0.8239 0.1331 0.6761 0.1761 0.4731 0.6761 0.1761 0.3031 0.6761 0.1761 0.1331 0.3239 0.1761 0.4731 0.3239 0.1761 0.3031 0.3239 0.1761 0.1331 0.6761 0.8239 0.4731 0.6761 0.8239 0.3031 0.6761 0.8239 0.1331 0.3239 0.8239 0.4731 0.3239 0.8239 0.3031 0.3239 0.8239 0.1331 ``` 4. 计算吸附能 使用VASP计算出金属锌表面,有机分子,以及表面-分子复合体的能量,并计算出吸附能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值