CSS动画,简单易用,但复杂场景却极具挑战。例如,鼠标悬停时改变按钮背景颜色?简单。但以高性能的方式动画化元素的位置和大小,并影响其他元素的位置?棘手!本文将深入探讨这个问题。
一个常见例子是从一堆项目中移除一个项目。堆叠在顶部的项目需要向下移动以弥补从堆栈底部移除的项目的空隙。现实生活中就是这样运作的,用户可能期望网站上出现这种逼真的运动。如果没有,用户可能会感到困惑或暂时迷失方向。基于生活经验,你期望某些事物以某种方式运作,却得到完全不同的结果,用户可能需要额外的时间来处理这种不切实际的运动。
这里演示了一个添加项目(点击按钮)或移除项目(点击项目)的UI。
你可以通过添加“淡出”动画等方式略微掩盖糟糕的UI,但结果不会很好,因为列表会突然折叠并导致同样的认知问题。
将仅CSS动画应用于动态DOM事件(添加全新的元素和完全移除元素)是极其棘手的工作。我们将直接面对这个问题,并介绍三种截然不同的动画类型来处理这个问题,所有这些都实现了帮助用户理解项目列表变化的目标。完成本文后,你将能够使用这些动画,或者根据这些概念构建你自己的动画。
我们还将简要介绍可访问性以及如何借助ARIA属性使精细的HTML布局仍然保留与辅助设备的一些兼容性。
一种非常现代的方法(也是我个人最喜欢的)是,新添加的元素根据它们最终将要到达的位置垂直淡入淡出。这也意味着列表需要“打开”一个位置(也进行了动画处理)来为其腾出空间。如果一个元素离开了列表,它占据的空间需要收缩。
由于我们同时进行许多不同的操作,我们需要更改DOM结构以将每个.list-item
包装到一个恰当命名的.list-container
容器类中。这对于使我们的动画工作绝对至关重要。
现在,这种样式是不正统的,因为为了使我们的动画效果稍后能够工作,我们需要以非常特定的方式设置列表样式,这以牺牲一些惯用的CSS实践为代价来完成任务。
.list { list-style: none; } .list-container { cursor: pointer; font-size: 3.5rem; height: 0; list-style: none; position: relative; text-align: center; width: 300px; } .list-container:not(:first-child) { margin-top: 10px; } .list-container .list-item { background-color: #D3D3D3; left: 0; padding: 2rem 0; position: absolute; top: 0; transition: all 0.6s ease-out; width: 100%; } .add-btn { background-color: transparent; border: 1px solid black; cursor: pointer; font-size: 2.5rem; margin-top: 10px; padding: 2rem 0; text-align: center; width: 300px; }
首先,我们使用margin-top
在堆栈中的元素之间创建垂直空间。底部没有边距,以便其他列表项可以填充移除的列表项创建的间隙。这样,即使我们将容器高度设置为零,它仍然在底部有边距。额外的空间是在删除的列表项正下方的列表项之间创建的。并且同一个列表项应该向上移动以响应删除的列表项的容器高度为零。并且由于这个额外的空间进一步扩大了列表项之间的垂直间隙,因此我们使用margin-top
来防止这种情况发生。
但是我们只有在所讨论的项目容器不是列表中的第一个项目时才这样做。这就是我们使用:not(:first-child)
的原因——它定位所有除了第一个容器(启用选择器)。我们这样做是因为我们不希望第一个列表项从列表的顶部边缘向下推。我们只想让此操作发生在之后的所有项目上,因为它们位于另一个列表项的正下方,而第一个列表项则不是。
现在,这不太可能完全有意义,因为我们目前没有将任何元素的高度设置为零。但我们稍后会这样做,为了使列表元素之间的垂直间距正确,我们需要像我们那样设置边距。
另一个值得指出的内容是.list-item
元素嵌套在其父.list-container
元素中的事实,它们被设置为具有absolute
位置,这意味着它们相对于其相对定位的.list-container
元素在DOM之外定位。我们这样做是为了使.list-item
元素在移除时向上浮动,同时使其他.list-item
元素移动并填充移除此.list-item
元素留下的间隙。发生这种情况时,.list-container
元素(未绝对定位,因此受DOM影响)会折叠其高度,允许其他.list-container
元素填充其位置,而.list-item
元素(以绝对方式定位)向上浮动,但不影响列表的结构,因为它不受DOM的影响。
不幸的是,我们还没有做足够的努力来获得一个合适的列表,其中各个列表项一个接一个地堆叠在一起。相反,目前我们所能看到的只是一个.list-item
,它代表所有堆叠在同一位置的列表项。这是因为,尽管.list-item
元素可能通过其padding
属性具有一定的高度,但它们的父元素没有,而是高度为零。这意味着我们在DOM中没有任何东西实际上将这些元素彼此分开,因为要做到这一点,我们需要我们的.list-container
元素具有一定的高度,因为与它们的子元素不同,它们受DOM的影响。
为了使我们的列表容器的高度与它们的子元素的高度完全匹配,我们需要使用JavaScript。因此,我们将所有列表项存储在一个变量中。然后,我们创建一个函数,该函数在脚本加载后立即调用。
这成为处理列表容器元素高度的函数:
const listItems = document.querySelectorAll('.list-item'); function calculateHeightOfListContainer(){ }; calculateHeightOfListContainer();
我们首先做的就是从列表中提取第一个.list-item
元素。我们可以这样做,因为它们的大小都相同,因此我们使用哪个元素无关紧要。一旦我们访问了它,我们就通过元素的clientHeight
属性以像素为单位存储其高度。之后,我们创建一个新的<style></style>
元素,该元素在加载后立即追加到文档的主体中,以便我们可以直接创建一个包含我们刚刚提取的高度值的CSS类。并且在这个<style></style>
元素安全地位于DOM中后,我们编写一个新的.list-container
类,其样式会自动优先于外部样式表中声明的样式,因为这些样式来自实际的<style></style>
标签。这使.list-container
类具有与其.list-item
子元素相同的高度。
const listItems = document.querySelectorAll('.list-item'); function calculateHeightOfListContainer() { const firstListItem = listItems[0]; let heightOfListItem = firstListItem.clientHeight; const styleTag = document.createElement('style'); document.body.prepend(styleTag); styleTag.innerHTML = `.list-container{ height: ${heightOfListItem}px; }`; }; calculateHeightOfListContainer();
现在,我们的列表看起来有点单调——与我们在第一个示例中看到的相同,只是没有任何添加或删除逻辑,并且样式与在该开头示例中使用的<ul></ul>
和<li>
标签列表完全不同。我们现在将做一些目前可能看起来无法解释的事情,并修改我们的.list-container
和.list-item
类。我们还为这两个类创建了额外的样式,只有当新的类.show
与这两个类分别一起使用时,才会将这些样式添加到它们中。
我们这样做的目的是为.list-container
和.list-item
元素创建两种状态。一种状态是在这两个元素上都没有.show
类,这种状态表示元素从列表中动画消失。另一种状态包含添加到这两个元素的.show
类。它表示指定的.list-item
已牢固地实例化并显示在列表中。
稍后,我们将通过向这两个元素添加/删除.show
类来在这两种状态之间切换。我们将将其与这两种状态之间的CSS转换结合起来。
请注意,将.list-item
类与.show
类组合会为某些内容引入一些额外的样式。具体来说,我们正在引入我们正在创建的动画,其中列表项在添加到列表时向下淡入并淡入可见性——删除时会发生相反的情况。由于动画元素位置最有效的方法是使用transform
属性,因此我们将在此处使用它,同时应用opacity
来处理可见性部分。因为我们已经在.list-item
和.list-container
元素上应用了transition
属性,所以每当我们向这两个元素添加或删除.show
类时,由于.show
类带来的额外属性,都会自动发生转换,从而在添加或删除这些新属性时都会发生转换。
.list-container { cursor: pointer; font-size: 3.5rem; height: 0; list-style: none; position: relative; text-align: center; width: 300px; } .list-container.show:not(:first-child) { margin-top: 10px; } .list-container .list-item { background-color: #D3D3D3; left: 0; opacity: 0; padding: 2rem 0; position: absolute; top: 0; transform: translateY(-300px); transition: all 0.6s ease-out; width: 100%; } .list-container .list-item.show { opacity: 1; transform: translateY(0); }
响应.show
类,我们返回我们的JavaScript文件并更改我们的唯一函数,以便只有当所讨论的元素也具有.show
类时,.list-container
元素才具有height
属性。此外,我们正在向标准的.list-container
元素应用transition
属性,并且我们将在setTimeout
函数中执行此操作。如果我们不这样做,那么我们的容器会在脚本加载时页面初始加载时进行动画处理,并且高度在第一次应用时就会应用,这不是我们想要发生的事情。
const listItems = document.querySelectorAll('.list-item'); function calculateHeightOfListContainer(){ const firstListItem = listItems[0]; let heightOfListItem = firstListItem.clientHeight; const styleTag = document.createElement('style'); document.body.prepend(styleTag); styleTag.innerHTML = `.list-container.show { height: ${heightOfListItem}px; }`; setTimeout(function() { styleTag.innerHTML = `.list-container { transition: all 0.6s ease-out; }`; }, 0); }; calculateHeightOfListContainer();
现在,如果我们返回并在DevTools中查看标记,那么我们应该能够看到列表已消失,只剩下按钮。列表没有消失,因为这些元素已从DOM中移除;它消失了,因为.show
类现在是一个必须添加到.list-item
及其容器才能查看它们的必需类。
获取列表的方法非常简单。我们将.show
类添加到所有.list-container
元素以及其中包含的.list-item
元素。完成此操作后,我们应该能够在它们通常的位置看到我们预先创建的列表项。
但是,我们还无法与任何内容进行交互,因为要做到这一点——我们需要向我们的JavaScript文件中添加更多内容。
我们在初始函数之后首先要做的是声明对我们单击以添加新列表项的按钮和.list
元素本身的引用,.list
元素是围绕每个.list-item
及其容器的元素。然后,我们选择嵌套在父.list
元素中的每个.list-container
元素,并使用forEach
方法循环遍历它们。我们在回调中分配一个方法removeListItem
,将其分配给每个.list-container
的onclick
事件处理程序。在循环结束时,每次页面加载到DOM上的每个.list-container
都会在单击时调用相同的方法。
完成此操作后,我们将一个方法分配给addBtn
的onclick
事件处理程序,以便我们可以在单击它时激活代码。但是显然,我们现在还不会创建该代码。现在,我们只是将某些内容记录到控制台以进行测试。
const addBtn = document.querySelector('.add-btn'); const list = document.querySelector('.list'); function removeListItem(e){ console.log('Deleted!'); } // DOCUMENT LOAD document.querySelectorAll('.list .list-container').forEach(function(container) { container.onclick = removeListItem; }); addBtn.onclick = function(e){ console.log('Add Btn'); }
开始处理addBtn
的onclick
事件处理程序,我们首先要做的是创建两个新元素:container
和listItem
。这两个元素都代表.list-item
元素及其各自的.list-container
元素,这就是为什么我们在创建它们后立即为它们分配这些确切的类。
一旦这两个元素准备就绪,我们使用append
方法在container
上插入listItem
作为子元素,这与列表中已存在的元素的格式相同。成功将listItem
作为子元素追加到container
后,我们可以使用insertBefore
方法将container
元素及其子listItem
元素移动到DOM。我们这样做是因为我们希望新项目出现在列表的底部,但在addBtn
之前,addBtn
需要保留在列表的最底部。因此,通过使用addBtn
的parentNode
属性来定位其父级list
,我们表示我们希望将元素插入为list
的子元素,并且我们将插入的子元素(container
)将插入在已经位于DOM上并且我们已使用insertBefore
方法的第二个参数addBtn
定位的子元素之前。
最后,成功将.list-item
及其容器添加到DOM后,我们可以将container
的onclick
事件处理程序设置为与DOM上默认情况下所有其他.list-item
相同的方法。
addBtn.onclick = function(e){ const container = document.createElement('li'); container.classList.add('list-container'); const listItem = document.createElement('div'); listItem.classList.add('list-item'); listItem.innerHTML = 'List Item'; container.append(listItem); addBtn.parentNode.insertBefore(container, addBtn); container.onclick = removeListItem; }
如果我们尝试一下,那么无论我们单击addBtn
多少次,我们都将看不到列表的任何更改。这不是单击事件处理程序的错误。事情正在按预期进行。.list-item
元素(及其容器)已添加到列表中的正确位置,只是它们是在没有.show
类的情况下添加的。因此,它们没有任何高度,这就是为什么我们看不到它们,以及为什么看起来列表没有任何变化的原因。
为了在每次单击addBtn
时使每个新添加的.list-item
都动画到列表中,我们需要向.list-item
及其容器应用.show
类,就像我们必须对已硬编码到DOM中的列表项所做的那样。
问题是我们不能立即将.show
类添加到这些元素。如果我们这样做,新的.list-item
会静态地出现在列表底部,没有任何动画。我们需要在动画之前注册一些样式,这些额外的样式会覆盖元素的初始样式,以便元素知道要进行哪些转换。这意味着,如果我们只是将.show
类应用到已经就位——因此没有转换。
解决方案是在setTimeout
回调中应用.show
类,将回调的激活延迟15毫秒,或1.5/100秒。这种难以察觉的延迟足以在附加状态和通过添加.show
类创建的新状态之间创建转换。但是,这种延迟也足够短,以至于我们永远不会知道首先存在延迟。
addBtn.onclick = function(e){ const container = document.createElement('li'); container.classList.add('list-container'); const listItem = document.createElement('div'); listItem.classList.add('list-item'); listItem.innerHTML = 'List Item'; container.append(listItem); addBtn.parentNode.insertBefore(container, addBtn); container.onclick = removeListItem; setTimeout(function(){ container.classList.add('show'); listItem.classList.add('show'); }, 15); }
成功!现在是时候处理我们单击时如何移除列表项了。
现在移除列表项不应该太难了,因为我们已经完成了添加它们的艰巨任务。首先,我们需要确保我们正在处理的元素是.list-container
元素而不是.list-item
元素。由于事件传播,触发此单击事件的目标很可能是.list-item
元素。
由于我们想要处理关联的.list-container
元素而不是实际触发事件的.list-item
元素,因此我们使用while
循环向上循环一个祖先,直到container
中保存的元素成为.list-container
元素。当container
获得.list-container
类时,我们知道它有效,我们可以通过使用container
元素的classList
属性上的contains
方法来发现这一点。
一旦我们访问了container
,我们就会立即从container
及其.list-item
中删除.show
类,一旦我们也访问了.list-item
类。
function removeListItem(e) { let container = e.target; while (!container.classList.contains('list-container')) { container = container.parentElement; } container.classList.remove('show'); const listItem = container.querySelector('.list-item'); listItem.classList.remove('show'); }
这是最终结果:(此处应插入最终结果的截图或动画)
现在你可能很想就此结束项目,因为列表添加和删除现在都应该可以工作了。但是,重要的是要记住,此功能只是表面上的,为了使它成为一个完整的包,肯定需要进行一些润色。
首先,仅仅因为已移除的元素已向上淡出并消失,并且列表已收缩以填充它留下的间隙,并不意味着已移除的元素已从DOM中移除。事实上,它没有。这是一个性能问题,因为它意味着我们在DOM中有一些元素没有任何用途,除了在后台累积并减慢我们的应用程序速度。
为了解决这个问题,我们使用container
元素上的ontransitionend
方法将其从DOM中移除,但只有在由我们移除.show
类引起的转换完成后才移除它,这样它的移除就不可能中断我们的转换。
function removeListItem(e) { let container = e.target; while (!container.classList.contains('list-container')) { container = container.parentElement; } container.classList.remove('show'); const listItem = container.querySelector('.list-item'); listItem.classList.remove('show'); container.ontransitionend = function(){ container.remove(); } }
此时我们不应该看到任何区别,因为我们所做的只是提高了性能——没有样式更新。
另一个区别也是不明显的,但非常重要:兼容性。因为我们使用了正确的<ul></ul>
和<li>
标签,设备应该不会在正确解释我们创建的无序列表方面有任何问题。
然而,我们确实存在一个问题,即设备可能无法处理列表的动态特性,例如列表如何更改其大小以及它保存的项目数量。新的列表项将被完全忽略,并且移除的列表项将被读取为仍然存在。
因此,为了使设备在列表大小发生变化时重新解释我们的列表,我们需要使用ARIA属性。它们有助于使我们的非标准HTML列表被兼容设备识别为无序列表。也就是说,它们在这里并不是一个有保证的解决方案,因为它们在兼容性方面永远不如原生标签好。以<ul></ul>
标签为例——无需担心这一点,因为我们能够使用原生的无序列表元素。我们可以将aria-live
属性用于.list
元素。嵌套在用aria-live
标记的DOM部分内的所有内容都变得响应迅速。换句话说,对具有aria-live
的元素所做的更改会被识别,从而允许它们发出更新的响应。在我们的例子中,我们希望事物具有高度的反应性,我们通过将aria-live
属性设置为assertive
来做到这一点。这样,每当检测到更改时,它都会这样做,中断它当时正在执行的任何任务,以立即评论所做的更改。
这是一种更细微的动画,其中,列表项不会在更改不透明度时向上或向下浮动,而是元素在逐渐淡入或淡出时会向外折叠或展开;同时,其余列表会重新定位自身以进行转换。
列表的妙处(也许是对我们创建的冗长DOM结构的一些宽恕),在于我们可以非常轻松地更改动画而不会干扰主要效果。
因此,为了实现这种效果,我们首先隐藏.list-container
上的溢出。我们这样做是为了当.list-container
自身折叠时,它会这样做,而子.list-item
在它缩小时不会流出列表容器的边界之外。除此之外,我们唯一需要做的就是从具有.show
类的.list-item
中移除transform
属性,因为我们不再希望.list-item
向上浮动。
.list-container { cursor: pointer; font-size: 3.5rem; height: 0; overflow: hidden; list-style: none; position: relative; text-align: center; width: 300px; } .list-container.show:not(:first-child) { margin-top: 10px; } .list-container .list-item { background-color: #D3D3D3; left: 0; opacity: 0; padding: 2rem 0; position: absolute; top: 0; transition: all 0.6s ease-out; width: 100%; } .list-container .list-item.show { opacity: 1; }
最后一种动画技术与其他动画技术截然不同,因为容器动画和.list-item
动画实际上不同步。.list-item
在从列表中移除时会向右滑动,在添加到列表时会从右侧滑动。在它开始动画到列表之前,列表中需要有足够的垂直空间来为新的.list-item
让路,反之亦然。
至于样式,它与滑动淡出动画非常相似,唯一不同的是.list-item
的转换现在应该在x轴上而不是y轴上。
.list-container { cursor: pointer; font-size: 3.5rem; height: 0; list-style: none; position: relative; text-align: center; width: 300px; } .list-container.show:not(:first-child) { margin-top: 10px; } .list-container .list-item { background-color: #D3D3D3; left: 0; opacity: 0; padding: 2rem 0; position: absolute; top: 0; transform: translateX(300px); transition: all 0.6s ease-out; width: 100%; } .list-container .list-item.show { opacity: 1; transform: translateX(0); }
至于我们JavaScript中addBtn
的onclick
事件处理程序,我们使用嵌套的setTimeout
方法将listItem
动画的开始延迟其容器元素已经开始转换后的350毫秒。
setTimeout(function(){ container.classList.add('show'); setTimeout(function(){ listItem.classList.add('show'); }, 350); }, 10);
在removeListItem
函数中,我们首先移除listItem
的.show
类,以便它可以立即开始转换。然后,容器元素失去其.show
类,但这仅在初始listItem
转换开始后的350毫秒之后。然后,在容器元素开始转换后的600毫秒(或listItem
转换后的950毫秒)之后,我们从DOM中移除容器元素,因为此时listItem
和容器转换都应该已经结束。
function removeListItem(e){ let container = e.target; while(!container.classList.contains('list-container')){ container = container.parentElement; } const listItem = container.querySelector('.list-item'); listItem.classList.remove('show'); setTimeout(function(){ container.classList.remove('show'); container.ontransitionend = function(){ container.remove(); } }, 350); }
这是最终结果:(此处应插入最终结果的截图或动画)
就是这样,三种不同的方法可以为添加到堆栈中并从中移除的项目设置动画。我希望通过这些示例,你现在有信心在DOM结构响应已添加到DOM中或从中移除的元素而调整到新位置的情况下工作。
正如你所看到的,有很多活动部件和需要考虑的事情。我们从对现实世界中这种类型的运动的期望开始,并考虑当其中一个元素更新时会发生什么情况。在显示和隐藏状态之间以及哪些元素在特定时间获得它们之间进行转换需要一些平衡,但我们做到了。我们甚至不遗余力地确保我们的列表既高效又易于访问,这些都是我们在实际项目中肯定需要处理的事情。
无论如何,我希望你在未来的项目中一切顺利。这就是我全部的内容。结束语。
以上是用于添加和删除堆栈项目的动画技术的详细内容。更多信息请关注PHP中文网其他相关文章!