You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android键盘弹出时,动态添加元素导致底部输入框被遮挡的聊天布局问题求助

Android键盘弹出时,动态添加元素导致底部输入框被遮挡的聊天布局问题求助

大家好,我现在遇到一个聊天布局的棘手问题,想请教各位大佬帮忙看看!

问题现象

我做了一个从下往上堆叠消息的聊天界面(最新消息在最底部),用Tailwind实现了布局。但当动态显示“typing...”提示框时,底部的输入框会被Android键盘挤下去,导致部分被遮挡,就像这样:
输入框被遮挡的演示

我需要保证输入框固定在原本的位置,同时“typing...”提示要从上到下出现(也就是新内容在消息列表的视觉上方添加)。我已经试了各种办法:把justify-end换成margin-top:auto、用flex-col-reverse、甚至旋转180度,结果都一样。还试过把typing提示直接放进循环的消息数组里,这种方式只在部分场景有效——比如对方发消息时没问题,但用户自己发消息后再显示typing,还是会出现同样的遮挡问题。

我的疑问

为什么会出现这个情况?为什么用@for循环添加元素时完全正常,用@if条件动态添加就出问题?而且如果消息是从上到下排列(不用justify-end)就完全没这个问题,反过来布局就出bug?

如果有其他实现“消息从下往上堆叠”的布局方案,不会触发这个遮挡问题的,也请告诉我!不管是原生CSS、JS还是Tailwind的方案都可以,万分感谢!

复现步骤

  • 先确保“typing...”提示框处于隐藏状态
  • 打开Android键盘
  • 触发显示“typing...”提示框,此时就能看到输入框被推下去部分遮挡的问题

附带小问题

我把输入框type设为search时,键盘右下角显示的是搜索图标;改成text类型后,输入框顶部又会弹出密码管理栏(就是常见的保存密码提示)。有没有办法让键盘右下角显示“提交箭头”,同时又不弹出密码管理栏?


我的代码实现

app.component.html

<div class="flex flex-col h-dvh bg-yellow-300">
  <header class="fixed top-0 h-10 border w-full border-blue-600">
    Banner
  </header>
  
  <div class="my-10 h-full flex flex-col-reverse overflow-y-auto">
    @if (showElement) {
      <div class="bg-yellow-100 text-xs">...typing</div>
    }
    @for (message of messages(); track $index) {
      <div>{{ message }}</div>  
    }
  </div>

  <div class="flex bottom-0 fixed border-2 h-10 border-red-500 w-full">
    <input type="search" autocomplete="off" class="border-2 border-green-500 grow normal-text">
  </div>
</div>

app.component.ts

import { isPlatformBrowser } from '@angular/common';
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  imports: [],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit, OnDestroy {

  showElement = false;

  messages = signal<string[]>([]);

  intervalId: any;
  intervalId2: any;

  counter = 1;

  constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

  ngOnInit(): void {
    if(isPlatformBrowser(this.platformId)) {
      this.intervalId = setInterval(() => {
        this.messages.set(['test'+this.counter++, ...this.messages()]);
      }, 500);

      setInterval(() => {
        this.intervalId2 = this.showElement = !this.showElement;
      }, 1000); // Toggles every second
    }
  }

  ngOnDestroy() {
    if (this.intervalId) {
      clearInterval(this.intervalId); // Cleanup to prevent memory leaks
    }
    if (this.intervalId2) {
      clearInterval(this.intervalId2); // Cleanup to prevent memory leaks
    }
  }
}

app.component.scss

.normal-text {
    -webkit-appearance: none;
    appearance: none;
    -moz-appearance: none;
}

styles.scss

/* You can add global styles to this file, and also import other style files */
@tailwind base;
@tailwind components;
@tailwind utilities;

更新:尝试滚动修复但未解决

后来按照建议,在显示typing提示时自动滚动消息容器到底部,但输入框还是会被推下去遮挡,演示如下:
滚动修复后的问题演示

更新后的代码:

app.component.ts

import { isPlatformBrowser } from '@angular/common';
import { Component, ElementRef, Inject, OnDestroy, OnInit, PLATFORM_ID, Renderer2, signal, ViewChild, viewChild } from '@angular/core';

@Component({
  selector: 'app-root',
  imports: [],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit, OnDestroy {

  // 获取消息容器的DOM引用
  @ViewChild('messageContainer') messageContainer: ElementRef | undefined = undefined;

  showElement = false;

  messages = signal<string[]>([]);

  intervalId: any;
  intervalId2: any;

  counter = 1;

  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
    private renderer: Renderer2
    ) {}

  ngOnInit(): void {
    if(isPlatformBrowser(this.platformId)) {
      this.intervalId = setInterval(() => {
        this.messages.set(['test'+this.counter++, ...this.messages()]);
      }, 500);

      setInterval(() => {
        this.intervalId2 = this.showElement = !this.showElement;
        if(this.showElement) {
          if(this.messageContainer) {
            // 滚动容器到底部(因为用了flex-col-reverse,所以scrollTop设为0)
            let elem = this.renderer.selectRootElement(this.messageContainer)?.nativeElement;
            elem.scrollTop = 0;
          }
        }
      }, 1000);
    }
  }

  ngOnDestroy() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
    if (this.intervalId2) {
      clearInterval(this.intervalId2);
    }
  }
}

app.component.html

<div class="flex flex-col h-dvh bg-yellow-300">
  <header class="fixed top-0 h-10 border w-full border-blue-600">
    Banner
  </header>

  <!-- 滚动这个容器 -->
  <div class="my-10 h-full flex flex-col-reverse overflow-y-auto" #messageContainer>
    @if (showElement) {
      <div class="bg-yellow-100 text-xs">...typing</div>
    }
    @for (message of messages(); track $index) {
      <div>{{ message }}</div>  
    }
  </div>

  <div class="flex bottom-0 fixed border-2 h-10 border-red-500 w-full">
    <input type="search" autocomplete="off" class="border-2 border-green-500 grow normal-text">
  </div>
</div>

另外,我在Chrome远程调试里发现,可能是容器的尺寸发生了变化,演示如下:
Chrome调试中容器尺寸变化演示


备注:内容来源于stack exchange,提问作者Sidharth Bajpai

火山引擎 最新活动