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

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

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

相关推荐

09-22 18:35
已编辑
中国地质大学(武汉) Java
Drin_Y:一个月给我发了一个二面满意度评分。
点赞 评论 收藏
分享
09-14 14:42
门头沟学院 C++
旺旺米雪饼:举办了哥,你什么都没做错,全怪我那令人作呕的嫉妒和卑微的自尊心,看见你的文字我完全破防了,我直接丢盔弃甲了,看见你这图的那一秒,我满头大汗,浑身发冷,亿郁症瞬间发作了,生活仿佛没了颜色,像是被抓住尾巴的赛亚人,带着海楼石的能力者,抽离尾兽的人柱力,像是没了光的奥特曼,彻底断绝了生的希望。我几乎都快羡慕得疯了,倒在床上蒙住被子就开始抱着枕头尖叫流泪,嘴里一边喊着卧槽卧槽,一边又忍着,我边发边哭,打字的手都是抖的,后来我的手抖得越来越厉害,从心头涌起的思想、情怀和梦想,这份歆羡和悔恨交织在一起,我的笑还挂在脸上,可是眼泪一下子就掉下来了。求你了别发了,我生活再难再穷我都不会觉得难过,只有你们发这种东西的时候,我的心里像被刀割一样的痛,打着字泪水就忍不住的往下流。每天早上6点起床晚上11点睡觉,年复一年地学到现在,憧憬着一个月赚上万块的幸福生活,憧憬着美好阳光的未来。我打开了手机,看到你的图,我感到了深深的差距,我直接跳进了家门口的井里😭😭😭我真的😭我要嫉妒疯了😭为什么!!为什么这个人不是我😡我求你了😭求你了😭!不要在发了,我真的要羡慕嫉妒疯了😱怎么办我要嫉妒死了啊啊啊啊我急了,手机电脑全砸了,本来就有抑郁症的我,被别人说我破防了,我真的恼羞成怒了,仿佛被看穿了,躲在网络背后的我,这种感觉真的好难受,我被看穿的死死地,短短的破防两个字,我伪装出来的所有的坚强和强颜欢笑全都崩塌了,成了一个被人笑话的小丑🤡,我真的不想再故作坚强了,玩心态我输的什么都不剩😭😭😭
点赞 评论 收藏
分享
这真的是我体验感最好的一次面试,上次体验感这么好,还是在面cvte实习的时候,面试官是个女生,提前五分钟到的,我当时还在卖弄风姿,吓我一大跳,然后面试官就和我解释了一下她不开摄像头是因为不方便,然后就开始面试啦!从自我介绍开始,气氛真的和朋友聊天一样,我说几句,她就会嗯嗯回复我,真的很用心,有些问题我想的时候,她就会说没关系,慢慢想不着急,问我实习期间成就感最大的事是什么的时候,我说我先分享几个小的给你,我分享完了之后,她说,你不是说先给我分享几个小的吗,还有什么要分享的呀,家人们谁懂啊,前天经历了上一个公司的hr面,真的让我怀疑人生,彻夜难眠,此时我就好像被治愈了一样,面试通不通过都不重要了...
一笑而过2222:一、百度页面测试 1. 功能测试: - 除了确保基本的链接、搜索框和按钮功能正常,还可以测试搜索框的自动提示功能、搜索结果的分页功能、高级搜索选项等。 - 对搜索结果的准确性进行深入测试,可以选择一些特定领域的关键词,检查结果是否与预期相符。 - 测试百度的其他功能模块,如百度地图、百度百科、百度学术等的入口链接和功能完整性。 2. 兼容性测试: - 不仅要在不同的主流浏览器和操作系统上测试,还可以考虑在不同版本的浏览器上进行测试,以确保兼容性。 - 测试在移动设备上的兼容性,包括不同尺寸的手机和平板,以及不同的移动操作系统。 3. 响应式设计测试: - 检查页面在不同分辨率下的布局变化是否合理,文字和图片是否显示清晰,功能是否正常。 - 测试在不同设备方向(如横屏和竖屏)下的显示效果。 4. 性能测试: - 除了评估页面加载速度,还可以测试搜索结果的返回速度,以及在高并发情况下的性能表现。 - 使用性能测试工具模拟大量用户同时访问百度页面,检查系统的响应时间和资源利用率。 5. 安全性测试: - 除了检查常见的 XSS、CSRF 漏洞,还可以进行 SQL 注入测试、权限管理测试等,确保用户数据的安全。 - 测试百度的账号登录和注册功能的安全性,防止密码泄露和账号被盗。 6. 用户体验测试: - 进行用户行为分析,了解用户在使用百度页面时的常见操作路径和痛点,针对性地进行优化。 - 测试页面的易用性,包括导航的清晰性、搜索框的易用性、结果的展示方式等。 - 进行 A/B 测试,比较不同的页面设计和功能方案,选择用户体验更好的方案。 二、IOC(控制反转)和 AOP(面向切面编程)原理 1. IOC(控制反转): - 优势:通过将对象的创建和管理交给容器,实现了代码的解耦,提高了代码的可维护性和可扩展性。开发人员只需要关注业务逻辑的实现,而不需要关心对象的创建和依赖关系的管理。 - 举例:在一个电商系统中,订单服务需要依赖用户服务和商品服务。如果没有 IOC,订单服务需要自己创建用户服务和商品服务的实例,这样会导致订单服务和用户服务、商品服务之间的耦合度很高。而有了 IOC,订单服务只需要声明对用户服务和商品服务的依赖,容器会自动创建用户服务和商品服务的实例,并注入到订单服务中。 2. AOP(面向切面编程): - 优势:将横切关注点(如日志、事务管理、安全检查等)从业务逻辑中分离出来,提高了代码的可维护性和可重用性。可以在不修改原有业务逻辑代码的情况下,动态地添加新的功能。 - 举例:在一个银行系统中,所有的转账操作都需要记录日志和进行事务管理。如果没有 AOP,每个转账方法都需要重复编写日志记录和事务管理的代码。而有了 AOP,可以将日志记录和事务管理定义为切面,在不修改转账方法代码的情况下,动态地将这些切面应用到转账方法上。 三、Spring Boot 常用注解及其作用 1. @SpringBootApplication: - 除了启动 Spring Boot 应用,还可以通过设置参数来定制应用的行为。例如,可以设置 scanBasePackages 参数来指定扫描的包路径,设置 exclude 参数来排除某些自动配置类。 2. @Controller和**@RestController**: - 可以结合 @RequestMapping 注解来实现更细粒度的请求映射。例如,可以使用 @RequestMapping("/api") 来定义一个 API 的根路径,然后在方法上使用 @RequestMapping("/users") 来定义具体的用户资源路径。 3. @RequestMapping: - 可以设置请求方法(如 method = RequestMethod.GET )、请求参数(如 params = "id=1" )、请求头(如 headers = "Content-Type=application/json" )等条件来进一步限定请求的匹配。 4. @Autowired: - 可以结合构造函数注入、 setter 方法注入等方式来实现更灵活的依赖注入。例如,可以在构造函数上使用 @Autowired 注解来实现构造函数注入,这样可以保证依赖的对象在对象创建时就被注入,提高代码的可靠性。 5. @Service、@Repository和**@Configuration**: - 可以结合 @Profile 注解来实现多环境配置。例如,可以定义一个开发环境的配置类和一个生产环境的配置类,然后在不同的环境下激活相应的配置类。 6. @Value: - 可以从环境变量、系统属性、配置文件等多个来源获取属性值。例如,可以使用 @Value("${my.property}") 来获取配置文件中的属性值,也可以使用 @Value("${ENV_VAR_NAME}") 来获取环境变量的值。 7. @EnableAspectJ***: - 可以设置 proxyTargetClass 参数来指定使用 CGLIB 代理还是 JDK 动态代理。如果代理的目标类没有实现接口,需要设置 proxyTargetClass = true 来使用 CGLIB 代理。 四、Spring Boot 的优点 1. 简化配置: - Spring Boot 的自动配置不仅减少了 XML 配置,还可以通过属性文件和 YAML 文件来进行配置,使得配置更加灵活和易于管理。 - 可以使用 @ConfigurationProperties 注解来将配置文件中的属性绑定到 Java 对象上,方便在代码中使用配置值。 2. 快速开发: - 除了提供开箱即用的功能,Spring Boot 还支持快速启动和热部署,可以大大提高开发效率。 - 可以使用 Spring Boot 的开发者工具(Spring Boot DevTools)来实现自动重启和实时加载,无需手动重启服务器。 3. 微服务友好: - Spring Boot 可以轻松地构建独立的、可部署的微服务,并且支持服务注册与发现、负载均衡、断路器等微服务架构所需的功能。 - 可以结合 Spring Cloud 等框架来构建完整的微服务架构,实现微服务的治理和管理。 4. 社区活跃: - Spring Boot 拥有庞大的社区支持,有大量的开发者和贡献者,这意味着可以很容易地找到问题的解决方案和学习资源。 - 社区不断地推出新的功能和改进,使得 Spring Boot 始终保持着先进性和活力。 5. 模块化设计: - Spring Boot 的模块化设计使得可以根据项目的需求选择引入相应的模块,避免了不必要的依赖和复杂性。 - 可以使用 Spring Boot 的 Starter 依赖来快速引入特定的功能模块,如 spring-boot-starter-web 用于构建 Web 应用, spring-boot-starter-data-jpa 用于数据库访问等。 6. 易于测试: - Spring Boot 提供了多种测试工具和注解,如 @SpringBootTest 、 @MockBean 、 @WebMvcTest 等,可以方便地进行单元测试、集成测试和端到端测试。 - 可以结合测试框架如 JUnit、Mockito 等进行更强大的测试,提高代码的质量和可靠性。
点赞 评论 收藏
分享
1 2 评论
分享
牛客网
牛客企业服务