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

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

来源: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)`;  
});
#前端##我的实习求职记录##实习,投递多份简历没人回复怎么办##我的求职思考##如何判断面试是否凉了#
全部评论

相关推荐

线下面试,体验超好,和面试官唠嗑。1.&nbsp;问项目,哪个哪个地方具体怎么实现的,问了很多,人员构成,人员分工,APP具体是做什么用的等等。2.&nbsp;(接上)你提到了安卓和h5,在安卓里有webview可以承载网页,你知道用webview怎么具体实现和网页通信的吗(x)。3.&nbsp;简历里写了封装网络请求,具体讲讲。4.&nbsp;没有对网络请求过程进行优化吗?比如超时重连。5.&nbsp;简历写了熟悉封装、继承、多态,讲讲多态。6.&nbsp;讲讲Android&nbsp;framework指的是什么(简历写了)。这里很搞笑,面试官说一般社招才会写要求framework,校招不会,我说我已经看到好多公司实习都要求这个了,现在卷生卷死,面试官0.07.&nbsp;Android&nbsp;framework了解到什么程度?8.&nbsp;APP启动过程。9.&nbsp;线程和进程的概念、区别。10.&nbsp;handler原理。11.&nbsp;你提到looper从消息队列里取消息执行任务,那如果我想某个任务延迟执行怎么做?(x)12.&nbsp;算法:给一个有序序列,找出里面所有的负数个数,时间复杂度尽可能低(二分查找修改版,就是注意一下边界条件,比如已经全是负数或者全是正数这种)。13.&nbsp;为什么要做Android?这个问题我已经内心排练百八十遍了,开始吟唱。14.&nbsp;反问,我问五一前结果能出来吗,面试官说有点难,因为HR可能明天放假了,我(*꒦ິ⌓꒦ີ)。我又问了下对于我简历的建议,我觉得面试官说的很有道理,也给大家分享下。他说我写的技能点太散了(确实,会很多,但都不算精)要全部围绕岗位要求中的点写,我写的虽然都能粘上点边儿,但40%关系都不大,比如说Git、cmake之类的工具,面试官说是个程序员不会Git那已经不能叫程序员了。还有就是项目,项目分技能点写,不要分功能写,比如说封装了网络请求模块,封装了缓存模块,做了什么优化等等。上层功能去调用这些模块,面试官实际上是不关心你具体做了什么功能的,他会直接看简历里提现出来的亮点,然后根据这些亮点问,不然到时候看简历都不知道问啥,直接反问有什么优势,那不就懵了。最后问了下我现在是不是没课,学校离得远不远。希望过过过。------------------两小时之后通知oc,太迅速了。。。虽然是日常,但我终于不是0&nbsp;offer了呜呜呜。
点赞 评论 收藏
转发
简历为C++相关美团金融服务后端&nbsp;一面&nbsp;70&nbsp;min&nbsp;1.&nbsp;面试官首先介绍自己的工作,具体我忘了,没让我自我介绍2.&nbsp;TCP&nbsp;四次挥手为什么比三次握手多一次3.&nbsp;进程与线程区别4.&nbsp;进程间通信5.&nbsp;中断6.&nbsp;CPU的L1,L2,L3缓存7.&nbsp;Redis&nbsp;有哪些数据结构,Redis&nbsp;锁怎么实现的8.&nbsp;介绍一下&nbsp;HyperLogLog9.&nbsp;手撕三数之和,自己写出个bug没调出来,通过不了,给我唐完了10.&nbsp;为什么不考研11.&nbsp;最早什么时候能来实习这次是二战美团,五天后回到人才库淘天&nbsp;终端开发&nbsp;一面&nbsp;30min1.&nbsp;自我介绍2.&nbsp;说一个最能体现你技术的项目3.&nbsp;关系型数据库和非关系型数据库有哪些区别4.&nbsp;渐进式rehash介绍,和不使用渐进式有什么区别,有没有测试过,性能如何5.&nbsp;事件驱动框架是什么,在数据库服务器中具体是哪些东西,为什么使用Reactor模型,和其他的相比有什么优势6.&nbsp;多线程编程需要注意哪些,数据竞争和线程同步7.&nbsp;死锁怎么产生,描述一个能产生死锁的伪代码,如何解决避免死锁8.&nbsp;自己的职业生涯规划有没有考虑过,比如未来几年内做到什么样子9.&nbsp;反问:面试官工作,终端开发学习建议10.&nbsp;面试官教我怎么面试,听到这个我就感觉不妙,他自己一个人讲了五分钟。纯KPI面,两天后流程终止携程&nbsp;移动端开发&nbsp;一面&nbsp;55min&nbsp;1.&nbsp;自我介绍2.&nbsp;为什么投递移动端开发工程师(因为简历是C++后端相关),我说感兴趣他说对哪些感兴趣,有没有了解过Android和IOS开发的技术栈3.&nbsp;TCP和UDP的区别4.&nbsp;HTTP和HTTPS的区别,证书是什么有什么用,非对称加密底层原理,双方怎么加密解密5.&nbsp;了解哪些HTTP状态码(我说反了4XX应该是客户端,5XX应该是服务端,重定向304)6.&nbsp;你的unordered_map怎么设计的,然后讨论了如何设计一个高性能的哈希表(哈希表的长度,如何避免冲突也就是均匀分布,重哈希的策略,哈希函数的设计等)7.&nbsp;有没有用过Python,用的什么版本,有没有编程干过其他的事情...8.&nbsp;算法题,三数之和。自己写了个demo没有测试,面试官觉得可以优化给了另一个思路9.&nbsp;没有反问环节,面试官说了一句感谢你就润了。技术栈不合适,进入人才库#C++##0offer#
点赞 评论 收藏
转发
1 2 评论
分享
牛客网
牛客企业服务