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


10

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

有人提供了这个解决方案,我发现了这个解决方案,两者似乎都可以工作。

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

如果滚动到该高度以上,则上面提供的两个解决方案都可以正常工作。只有当您接近底部时,他们才会遇到这个小问题。

我以为可能只是我的程序导致了一些奇怪的流浪代码,但不,我什至重现了一个小提琴,它确实有这个问题。我很抱歉使调试变得如此困难,但是如果您在手机上访问https://jsfiddle.net/t596hy8d/6/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>


我将用IntersectionObserver和ResizeObserver替换事件处理程序。与事件处理程序相比,它们的CPU开销要低得多。如果您定位的是较旧的浏览器,则两者都应使用polyfill。

您是否在Firefox移动设备上尝试过此操作?它似乎没有这个问题。但是,在Chrome上尝试此操作确实会导致您提到的问题。
理查德

好吧,无论如何,它必须可以在Chrome上运行。很好,但是Firefox没有问题。
Ryan Peschel

我不好好表达我的观点。如果一个浏览器有问题,而另一个没有问题,则IMO 可能意味着您可能需要为不同的浏览器实现略有不同的实现。
理查德

1
@halfer好吧。我知道了。谢谢您的提醒,下次我要求某人重新回答问题时,我会考虑到这一点。
理查德

Answers:


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事件发生之前将其自身设置为0,从而resize使计算混乱。

  2. 所有解决方案在接近消息div底部时都遇到困难的另一个原因是很难理解。例如,在我的调整大小解决方案中,我只需要scrollTop在打开或关闭虚拟键盘时增加或减去250(移动键盘高度)即可。除了靠近底部之外,这非常有效。为什么?因为假设您距底部50像素,然后关闭键盘。它会scrollTop(键盘高度)减去250 ,但只能减去50!因此,在靠近底部关闭键盘时,它将始终重置到错误的固定位置。

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

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

我不喜欢这种解决方案(时间间隔效率低下,并且容易出现竞争状况),但是我找不到能始终有效的更好的方法。


1

我想你想要的是 overflow-anchor

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

从上面的CSS-Tricks文章中:

滚动锚定通过在当前位置上方的DOM中进行更改时锁定用户在页面上的位置来防止“跳跃”体验。这样,即使将新元素加载到DOM,用户也可以保持锚定在页面上的位置。

如果最好允许在加载元素时对内容进行重排,则overflow-anchor属性使我们可以选择退出“滚动锚定”功能。

这是其中一个示例的稍作修改的版本:

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 }总是在这些新消息之后出现的空元素,因为新消息将被插入该元素之前

必须有一个滚动条来注意到锚点的变化,我认为这通常是好的UX。但是无论哪种方式,键盘打开时都应保持当前滚动位置。


0

我的解决方案与您建议的解决方案相同,但附加了条件检查。这是我的解决方案的说明:

  • 记录的最后一个滚动位置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!之前。更奇怪的是,当我们滚动到不撤回when 的最大值以上时隐藏键盘时,才会发生该scroll事件。就我而言,这意味着当我向下滚动(收回之前的最大值)并隐藏键盘时,该事件发生之前很奇怪,然后将您完全滚动到。这显然弄乱了我们上面的解决方案。scrollTop.messages270.334pxscrollTop.messagesscrollresize.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移动版上进行了测试,并且适用于两种浏览器。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.