保持滚动位置仅在不在消息 div 底部附近时才有效

IT技术 javascript android html css reactjs
2021-05-01 04:55:38

我正在尝试模仿其他移动聊天应用程序,当您选择send-message文本框并打开虚拟键盘时,最底部的消息仍在视图中。似乎没有办法用 CSS 惊人地做到这一点,所以 JavaScript resize(唯一的方法是找出键盘何时明显打开和关闭)事件和手动滚动来救援。

有人提供了这个解决方案,我发现了这个解决方案,它们似乎都有效。

除了一种情况。出于某种原因,如果您MOBILE_KEYBOARD_HEIGHT在消息 div 底部的(在我的情况下为 250 像素)像素内,当您关闭移动键盘时,会发生一些奇怪的事情。使用前一种解决方案,它会滚动到底部。使用后一种解决方案,它会MOBILE_KEYBOARD_HEIGHT从底部向上滚动像素。

如果你滚动到这个高度以上,上面提供的两种解决方案都可以完美地工作。只有当你接近底部时,他们才会有这个小问题。

我想可能只是我的程序导致了一些奇怪的杂散代码,但不,我什至复制了一个小提琴,它有这个确切的问题。我很抱歉让调试变得如此困难,但是如果您在手机上访问https://jsfiddle.net/t596hy8d/6/show(show后缀提供全屏模式),您应该能够看到相同的行为。

这种行为是,如果您向上滚动足够多,则打开和关闭键盘会保持该位置。但是,如果您在距离MOBILE_KEYBOARD_HEIGHT底部的像素关闭键盘,您会发现它会滚动到底部。

什么原因造成的?

代码复制在这里:

window.onload = function(e){ 
  document.querySelector(".messages").scrollTop = 10000;
  
  bottomScroller(document.querySelector(".messages"));
}
  

function bottomScroller(scroller) {
  let scrollBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;

  scroller.addEventListener('scroll', () => { 
  scrollBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
  });   

  window.addEventListener('resize', () => { 
  scroller.scrollTop = scroller.scrollHeight - scrollBottom - scroller.clientHeight;

  scrollBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
  });
}
.container {
  width: 400px;
  height: 87vh;
  border: 1px solid #333;
  display: flex;
  flex-direction: column;
}

.messages {
  overflow-y: auto;
  height: 100%;
}

.send-message {
  width: 100%;
  display: flex;
  flex-direction: column;
}
<div class="container">
  <div class="messages">
  <div class="message">hello 1</div>
  <div class="message">hello 2</div>
  <div class="message">hello 3</div>
  <div class="message">hello 4</div>
  <div class="message">hello 5</div>
  <div class="message">hello 6 </div>
  <div class="message">hello 7</div>
  <div class="message">hello 8</div>
  <div class="message">hello 9</div>
  <div class="message">hello 10</div>
  <div class="message">hello 11</div>
  <div class="message">hello 12</div>
  <div class="message">hello 13</div>
  <div class="message">hello 14</div>
  <div class="message">hello 15</div>
  <div class="message">hello 16</div>
  <div class="message">hello 17</div>
  <div class="message">hello 18</div>
  <div class="message">hello 19</div>
  <div class="message">hello 20</div>
  <div class="message">hello 21</div>
  <div class="message">hello 22</div>
  <div class="message">hello 23</div>
  <div class="message">hello 24</div>
  <div class="message">hello 25</div>
  <div class="message">hello 26</div>
  <div class="message">hello 27</div>
  <div class="message">hello 28</div>
  <div class="message">hello 29</div>
  <div class="message">hello 30</div>
  <div class="message">hello 31</div>
  <div class="message">hello 32</div>
  <div class="message">hello 33</div>
  <div class="message">hello 34</div>
  <div class="message">hello 35</div>
  <div class="message">hello 36</div>
  <div class="message">hello 37</div>
  <div class="message">hello 38</div>
  <div class="message">hello 39</div>
  </div>
  <div class="send-message">
	<input />
  </div>
</div>

3个回答

我终于找到了一个真正有效的解决方案尽管它可能并不理想,但它实际上适用于所有情况。这是代码:

bottomScroller(document.querySelector(".messages"));

bottomScroller = scroller => {
  let pxFromBottom = 0;

  let calcPxFromBottom = () => pxFromBottom = scroller.scrollHeight - (scroller.scrollTop + scroller.clientHeight);

  setInterval(calcPxFromBottom, 500);

  window.addEventListener('resize', () => { 
    scroller.scrollTop = scroller.scrollHeight - pxFromBottom - scroller.clientHeight;
  });
}

我一路上的一些顿悟:

  1. 关闭虚拟键盘时,scroll事件会在事件发生之前立即resize发生。这似乎只发生在关闭键盘时,而不是打开它。就是你不能使用scroll事件来设置的pxFromBottom原因,因为如果你接近底部,它会在scroll事件之前的resize事件中将自身设置为 0,从而扰乱计算。

  2. 所有解决方案在消息 div 底部附近都有困难的另一个原因有点难以理解。例如,在我的调整大小解决方案中,我只是scrollTop在打开或关闭虚拟键盘时加上或减去 250(移动键盘高度)除了靠近底部之外,这非常有效。为什么?因为假设您距离底部 50 像素并关闭键盘。它将从scrollTop(键盘高度)中减去 250 ,但它应该只减去 50!所以在靠近底部关闭键盘时,它总是会重置到错误的固定位置。

  3. 我也相信您不能为此解决方案使用onFocusonBlur事件,因为只有在最初选择文本框以打开键盘时才会发生这些事件。您完全可以在不激活这些事件的情况下打开和关闭移动键盘,因此,它们不能在这里使用。

我相信以上几点对于开发解决方案很重要,因为它们起初并不明显,但会阻止开发强大的解决方案。

我不喜欢这个解决方案(间隔有点低效并且容易出现竞争条件),但我找不到更好的方法。

我想你想要的是 overflow-anchor

支持正在增加,但不是全部,但https://caniuse.com/#feat=css-overflow-anchor

来自一篇关于它CSS-Tricks 文章:

滚动锚定通过在当前位置上方的 DOM 中发生更改时锁定用户在页面上的位置来防止出现“跳跃”体验。这允许用户即使在新元素加载到 DOM 时也能锚定他们在页面上的位置。

溢出锚属性允许我们在首选允许内容在加载元素时重新流动的情况下选择退出滚动锚定功能。

这是他们示例之一的略微修改版本:

let scroller = document.querySelector('#scroller');
let anchor = document.querySelector('#anchor');

// https://ajaydsouza.com/42-phrases-a-lexophile-would-love/
let messages = [
  'I wondered why the baseball was getting bigger. Then it hit me.',
  'Police were called to a day care, where a three-year-old was resisting a rest.',
  'Did you hear about the guy whose whole left side was cut off? He’s all right now.',
  'The roundest knight at King Arthur’s round table was Sir Cumference.',
  'To write with a broken pencil is pointless.',
  'When fish are in schools they sometimes take debate.',
  'The short fortune teller who escaped from prison was a small medium at large.',
  'A thief who stole a calendar… got twelve months.',
  'A thief fell and broke his leg in wet cement. He became a hardened criminal.',
  'Thieves who steal corn from a garden could be charged with stalking.',
  'When the smog lifts in Los Angeles , U. C. L. A.',
  'The math professor went crazy with the blackboard. He did a number on it.',
  'The professor discovered that his theory of earthquakes was on shaky ground.',
  'The dead batteries were given out free of charge.',
  'If you take a laptop computer for a run you could jog your memory.',
  'A dentist and a manicurist fought tooth and nail.',
  'A bicycle can’t stand alone; it is two tired.',
  'A will is a dead giveaway.',
  'Time flies like an arrow; fruit flies like a banana.',
  'A backward poet writes inverse.',
  'In a democracy it’s your vote that counts; in feudalism, it’s your Count that votes.',
  'A chicken crossing the road: poultry in motion.',
  'If you don’t pay your exorcist you can get repossessed.',
  'With her marriage she got a new name and a dress.',
  'Show me a piano falling down a mine shaft and I’ll show you A-flat miner.',
  'When a clock is hungry it goes back four seconds.',
  'The guy who fell onto an upholstery machine was fully recovered.',
  'A grenade fell onto a kitchen floor in France and resulted in Linoleum Blownapart.',
  'You are stuck with your debt if you can’t budge it.',
  'Local Area Network in Australia : The LAN down under.',
  'He broke into song because he couldn’t find the key.',
  'A calendar’s days are numbered.',
];

function randomMessage() {
  return messages[(Math.random() * messages.length) | 0];
}

function appendChild() {
  let msg = document.createElement('div');
  msg.className = 'message';
  msg.innerText = randomMessage();
  scroller.insertBefore(msg, anchor);
}
setInterval(appendChild, 1000);
html {
  height: 100%;
  display: flex;
}

body {
  min-height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  padding: 0;
}

#scroller {
  flex: 2;
}

#scroller * {
  overflow-anchor: none;
}

.new-message {
  position: sticky;
  bottom: 0;
  background-color: blue;
  padding: .2rem;
}

#anchor {
  overflow-anchor: auto;
  height: 1px;
}

body {
  background-color: #7FDBFF;
}

.message {
  padding: 0.5em;
  border-radius: 1em;
  margin: 0.5em;
  background-color: white;
}
<div id="scroller">
  <div id="anchor"></div>
</div>

<div class="new-message">
  <input type="text" placeholder="New Message">
</div>

在手机上打开:https : //cdpn.io/chasebank/debug/PowxdOR

这样做基本上是禁用新消息元素的任何默认锚定, #scroller * { overflow-anchor: none }

而是锚定一个#anchor { overflow-anchor: auto }总是在这些新消息之后的空元素,因为新消息被插入之前

必须有一个滚动才能注意到锚定的变化,我认为这通常是良好的用户体验。但无论哪种方式,当键盘打开时都应保持当前滚动位置。

我的解决方案与您提出的解决方案相同,但增加了条件检查。这是我的解决方案的描述:

  • 记录的最后一个滚动位置scrollTop和最后clientHeight.messagesoldScrollTopoldHeight分别
  • 更新oldScrollTopoldHeight每一个时间resize发生在window和更新oldScrollTop每一个时间scroll发生在.messages
  • window收缩时(当虚拟键盘所示),的高度.messages会自动缩回。预期的行为是使 ' 的最底部内容.messages即使在.messages' 高度缩回时仍然可见这就要求我们必须手动调整滚动位置scrollTop.messages
  • 当虚拟键盘显示,更新scrollTop.messages确保的最底部.messages的高度回缩发生之前仍清晰可见
  • 当虚拟键盘皮,更新scrollTop.messages确保的最底部.messages遗体的最底部.messages高度扩张后(除非扩张不能向上发生;发生这种情况时,你的顶部是几乎.messages

是什么导致了这个问题?

我的(最初可能有缺陷的)逻辑思维是:resize发生,.messages' 高度变化,更新.messages scrollTop发生在我们的resize事件处理程序中。然而,在.messages' 高度扩展时,一个scroll事件奇怪地发生在resize! 更奇怪的是,该scroll事件在我们滚动超过scrollTopwhen的最大值时隐藏键盘时才会发生.messages在我的情况下,这意味着当我向下滚动270.334px缩回scrollTop之前的最大值.messages)并隐藏键盘时,scrollresize事件发生之前那个奇怪的事件发生并将您滚动.messages270.334px. 这显然弄乱了我们上面的解决方案。

幸运的是,我们可以解决这个问题。我个人scrollresize事件发生推论是因为当高度膨胀时.messages无法保持scrollTop在上面的位置(这就是为什么我提到我最初的逻辑思维是有缺陷的;只是因为没有办法保持它的位置在其最大值以上值)因此,它立即将其设置为它可以给出的最大值(不出所料,即)。270.334px.messagesscrollTopscrollTop270.334px

我们能做什么?

因为我们只oldHeight在调整大小时更新,我们可以检查这个强制滚动(或更准确地说,resize)是否发生,如果发生,不要更新oldScrollTop(因为我们已经在resize!)我们只需要比较oldHeight和当前高度scroll看看是否会发生这种强制滚动。这是有效的,因为oldHeight不等于当前高度的条件scroll仅在resize发生时才成立(巧合的是当强制滚动发生时)。

这是下面的代码(在 JSFiddle 中)

window.onload = function(e) {
  let messages = document.querySelector('.messages')
  messages.scrollTop = messages.scrollHeight - messages.clientHeight
  bottomScroller(messages);
}


function bottomScroller(scroller) {
  let oldScrollTop = scroller.scrollTop
  let oldHeight = scroller.clientHeight

  scroller.addEventListener('scroll', e => {
    console.log(`Scroll detected:
      old scroll top = ${oldScrollTop},
      old height = ${oldHeight},
      new height = ${scroller.clientHeight},
      new scroll top = ${scroller.scrollTop}`)
    if (oldHeight === scroller.clientHeight)
      oldScrollTop = scroller.scrollTop
  });

  window.addEventListener('resize', e => {
    let newScrollTop = oldScrollTop + oldHeight - scroller.clientHeight

    console.log(`Resize detected:
      old scroll top = ${oldScrollTop},
      old height = ${oldHeight},
      new height = ${scroller.clientHeight},
      new scroll top = ${newScrollTop}`)
    scroller.scrollTop = newScrollTop
    oldScrollTop = newScrollTop
    oldHeight = scroller.clientHeight
  });
}
.container {
  width: 400px;
  height: 87vh;
  border: 1px solid #333;
  display: flex;
  flex-direction: column;
}

.messages {
  overflow-y: auto;
  height: 100%;
}

.send-message {
  width: 100%;
  display: flex;
  flex-direction: column;
}
<div class="container">
  <div class="messages">
    <div class="message">hello 1</div>
    <div class="message">hello 2</div>
    <div class="message">hello 3</div>
    <div class="message">hello 4</div>
    <div class="message">hello 5</div>
    <div class="message">hello 6 </div>
    <div class="message">hello 7</div>
    <div class="message">hello 8</div>
    <div class="message">hello 9</div>
    <div class="message">hello 10</div>
    <div class="message">hello 11</div>
    <div class="message">hello 12</div>
    <div class="message">hello 13</div>
    <div class="message">hello 14</div>
    <div class="message">hello 15</div>
    <div class="message">hello 16</div>
    <div class="message">hello 17</div>
    <div class="message">hello 18</div>
    <div class="message">hello 19</div>
    <div class="message">hello 20</div>
    <div class="message">hello 21</div>
    <div class="message">hello 22</div>
    <div class="message">hello 23</div>
    <div class="message">hello 24</div>
    <div class="message">hello 25</div>
    <div class="message">hello 26</div>
    <div class="message">hello 27</div>
    <div class="message">hello 28</div>
    <div class="message">hello 29</div>
    <div class="message">hello 30</div>
    <div class="message">hello 31</div>
    <div class="message">hello 32</div>
    <div class="message">hello 33</div>
    <div class="message">hello 34</div>
    <div class="message">hello 35</div>
    <div class="message">hello 36</div>
    <div class="message">hello 37</div>
    <div class="message">hello 38</div>
    <div class="message">hello 39</div>
  </div>
  <div class="send-message">
    <input />
  </div>
</div>

在 Firefox 和 Chrome 移动版上进行了测试,它适用于两种浏览器。