使用 CSS 动画元素可能非常容易或非常困难,具体取决于您要做什么。 当您将鼠标悬停在按钮上时更改按钮的背景颜色? 很容易。 以一种高效的方式动画化元素的位置和大小,同时还会影响其他元素的位置? 棘手! 这正是我们在本文中要讨论的内容。
一个常见的例子是 **从项目堆栈中删除一个项目**。 位于顶部的项目需要向下移动以弥补从堆栈底部删除的项目的空间。 这也是现实生活中事物的运作方式,用户可能希望在网站上看到这种逼真的运动。 当这种情况没有发生时,用户可能会感到困惑或短暂的迷失方向。 您希望某样东西根据生活经验以一种方式表现,却得到了完全不同的结果,用户可能需要额外的时间来处理这种不切实际的运动。
下面演示了一个添加项目(点击按钮)或删除项目(点击项目)的 UI。
您可以通过添加“淡出”动画或其他动画来稍微掩盖糟糕的 UI,但结果不会很好,因为列表会突然折叠并导致相同的认知问题。
将仅 CSS 动画应用于动态 DOM 事件(添加全新的元素和完全删除元素)是一项 **极其困难的工作**。 我们将正面解决这个问题,并介绍三种截然不同的动画类型来处理这个问题,它们都实现了帮助用户理解项目列表变化的目标。 到我们完成时,您将有能力使用这些动画,或根据这些概念构建您自己的动画。
我们还将简要介绍无障碍功能,以及如何借助 ARIA 属性,即使是精心设计的 HTML 布局也能保留与无障碍设备的一些兼容性。
向下滑动透明度动画
一种非常现代的方法(也是我个人最喜欢的方法)是,新添加的元素根据它们最终将要放置的位置垂直淡入淡出并浮动到位置。 这也意味着列表需要“打开”一个位置(也是动画化的)为其腾出空间。 如果一个元素要离开列表,它所占用的位置需要收缩。
由于我们同时进行了很多事情,我们需要更改 DOM 结构,将每个 .list-item
放在一个容器类中,该容器类恰当地命名为 .list-container
。 这是为了使我们的动画正常工作绝对必要的。
<ul class="list">
<li class="list-container">
<div class="list-item">List Item</div>
</li>
<li class="list-container">
<div class="list-item">List Item</div>
</li>
<li class="list-container">
<div class="list-item">List Item</div>
</li>
<li class="list-container">
<div class="list-item">List Item</div>
</li>
</ul>
<button class="add-btn">Add New Item</button>
现在,这种样式非常规,因为为了使我们的动画效果在以后正常工作,我们需要以一种非常特殊的方式对列表进行样式设置,这种方式可以完成任务,但代价是牺牲了一些惯用的 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-container
元素中的 .list-item
元素被设置为 position
为 absolute
,这意味着它们在 DOM 外部定位,并且相对于它们相对定位的 .list-container
元素进行定位。 我们这样做是为了让 .list-item
元素在移除时向上浮动,同时让其他 .list-item
元素移动并填补移除此 .list-item
元素留下的空间。 发生这种情况时,.list-container
元素(没有定位为 absolute
,因此受 DOM 的影响)会折叠其高度,允许其他 .list-container
元素填充其位置,而 .list-item
元素(使用 absolute
定位)向上浮动,但不影响列表的结构,因为它不受 DOM 的影响。
处理高度
不幸的是,我们还没有做足够的努力来获得一个合适的列表,在该列表中,各个列表项一个接一个地堆叠在一起。 相反,我们现在只能看到一个 .list-item
,它代表了所有堆叠在一起的列表项,并且在完全相同的位置。 这是因为,虽然 .list-item
元素可能通过其 padding
属性具有一定的高度,但它们的父元素没有,而是高度为零。 这意味着我们 DOM 中没有任何东西实际上将这些元素彼此分开,因为要做到这一点,我们需要我们的 .list-container
元素具有一定的高度,因为与它们的孩子元素不同,它们会受到 DOM 的影响。
为了使列表容器的高度完全匹配其子元素的高度,我们需要使用 JavaScript。 因此,我们将所有列表项存储在一个变量中。 然后,我们创建一个函数,该函数在脚本加载后立即被调用。
这将成为处理列表容器元素高度的函数。
const listItems = document.querySelectorAll('.list-item');
function calculateHeightOfListContainer(){
};
calculateHeightOfListContainer();
我们首先从列表中提取第一个 .list-item
元素。 我们可以这样做,因为它们的大小都相同,所以我们使用哪个都无所谓。 获得对它的访问权限后,我们将通过元素的 clientHeight
属性存储其以像素为单位的高度。 之后,我们创建一个新的 <style>
元素,该元素在文档的 body
中的开头立即被追加,这样我们就可以直接创建一个包含我们刚刚提取的高度值的 CSS 类。 随着此 <style>
元素安全地存在于 DOM 中,我们编写了一个新的 .list-container
类,其样式会自动优先于外部样式表中声明的样式,因为这些样式来自一个实际的 <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>
和 <li>
标签列表完全不同。

我们现在将做一些可能在当前时刻看似莫名其妙的事情,并修改我们的 .list-container
和 .list-item
类。 我们还将为这两个类创建额外的样式,这些样式只会添加到它们中,前提是将新类 .show
, 与这两个类分别结合使用。
我们这样做是为了为.list-container
和 .list-item
元素创建两种状态。一种状态是在这两个元素上都没有 .show
类,这种状态表示元素从列表中动画退出时的状态。另一种状态包含添加到这两个元素上的 .show
类。它表示指定的 .list-item
已在列表中牢固地实例化并可见。
过了一会儿,我们将通过在特定 .list-item
的父元素和容器中添加/删除 .show
类来在这两种状态之间切换。我们将把它与 CSS transition
结合起来,在这两种状态之间进行过渡。
请注意,将 .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
属性。此外,我们还将 transition
属性应用于我们的标准 .list-container
元素,并且将在 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
和 .list-container
元素中的必需类,以便我们能够查看它们。
让列表恢复的办法很简单。我们将 .show
类添加到我们所有的 .list-container
元素以及它们内部包含的 .list-item
元素中。完成此操作后,我们应该能够看到我们预先创建的列表项恢复到它们通常的位置。
<ul class="list">
<li class="list-container show">
<div class="list-item show">List Item</div>
</li>
<li class="list-container show">
<div class="list-item show">List Item</div>
</li>
<li class="list-container show">
<div class="list-item show">List Item</div>
</li>
<li class="list-container show">
<div class="list-item show">List Item</div>
</li>
</ul>
<button class="add-btn">Add New Item</button>
但是我们还无法与任何东西交互,因为要做到这一点,我们需要在我们的 JavaScript 文件中添加更多内容。
在我们初始函数之后,我们要做的第一件事是声明对我们单击以添加新列表项的按钮以及 .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
元素,这就是为什么我们在创建它们后立即为它们分配这些确切的类。
准备这两个元素后,我们使用 container
上的 append
方法将 listItem
作为子元素插入其中,与列表中已存在的元素的格式相同。成功将 listItem
附加为 container
的子元素后,我们可以使用 insertBefore
方法将 container
元素及其子元素 listItem
元素移动到 DOM。我们这样做是因为我们希望新项目出现在列表的底部,但在 addBtn
之前,它需要一直停留在列表的最底部。因此,通过使用 addBtn
的 parentNode
属性来定位其父元素 list
,我们说我们想将元素插入为 list
的子元素,并且我们要插入的子元素 (container
) 将插入到已经存在于 DOM 中的子元素之前,并且我们已经使用 insertBefore
方法的第二个参数 addBtn
定位了该子元素。
最后,在成功将 .list-item
及其容器添加到 DOM 后,我们可以将容器的 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
多少次,我们都无法看到列表的任何变化。这不是 click
事件处理程序中的错误。事情按预期进行。.list-item
元素(及其容器)已添加到列表中的正确位置,只是它们添加时没有 .show
类。因此,它们没有任何高度,这就是为什么我们看不到它们,以及为什么看起来列表没有任何变化的原因。
为了让每个新添加的 .list-item
在我们单击 addBtn
时动画进入列表,我们需要将 .show
类应用于 .list-item
及其容器,就像我们必须为查看已硬编码到 DOM 中的列表项所做的那样。
问题是我们不能立即将 .show
类添加到这些元素。如果我们这样做,新的 .list-item
将静态地弹出并出现在列表的底部,没有任何动画。我们需要在动画之前注册一些样式,这些样式会覆盖那些初始样式,以便元素知道要执行什么 transition
。这意味着,如果我们只是将 .show
类应用于已经存在的元素,那么就不会发生过渡。
解决方法是在 setTimeout
回调中应用 .show
类,将回调的激活延迟 15 毫秒,或 1.5/100 秒。这种难以察觉的延迟足以从 provisio 状态创建到添加 .show
类后创建的新状态的 transition
。但这种延迟也足够短,以至于我们永远不会知道实际上存在延迟。
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
类,一旦我们也访问了它。
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 中有一些元素,它们除了在后台积累并减慢我们的应用程序速度之外,没有任何用途。
为了解决这个问题,我们在容器元素上使用 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>
和 <li>
标签,所以设备应该能够毫无问题地将我们创建的内容正确地解释为无序列表。
此技术的其他注意事项
然而,我们确实存在一个问题,即设备可能无法处理我们列表的动态特性,例如列表如何改变其大小以及它所持有的项目数量。新的列表项将被完全忽略,已移除的列表项将被读取为仍然存在。
因此,为了让设备在列表大小发生变化时重新解释我们的列表,我们需要使用 ARIA 属性。它们可以帮助非标准 HTML 列表被兼容设备识别为列表。也就是说,它们不是这里保证的解决方案,因为它们在兼容性方面永远不如原生标签好。以 `<ul>
` 标签为例——我们不需要担心这个问题,因为我们能够使用原生无序列表元素。
我们可以将 `aria-live
` 属性应用于 `.list
` 元素。被 `aria-live
` 标记的 DOM 部分内部的所有内容都会变得响应式。换句话说,对具有 `aria-live
` 属性的元素进行的更改会被识别,从而允许它们发出更新的响应。在我们的例子中,我们希望它们高度响应,因此将 `aria live
` 属性设置为 `assertive
`。这样,每当检测到更改时,它都会立即做出反应,打断它当时正在执行的任务,立即对所做的更改进行评论。
<ul class="list" role="list" aria-live="assertive">
折叠动画
这是一个更微妙的动画,与列表项在更改不透明度时上下浮动不同,元素只是在逐渐淡入或淡出时向外收缩或扩展;同时,列表的其余部分重新定位以适应正在发生的过渡。
列表的妙处(也许是为我们创建的冗长 DOM 结构提供了一些弥补)在于,我们可以非常轻松地更改动画,而不会影响主要效果。
因此,要实现这种效果,我们首先隐藏 `.list-container
` 上的 overflow
。我们这样做是为了在 `.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;
}
侧滑动画
最后一种动画技术与其他动画技术截然不同,`container
` 动画和 `.list-item
` 动画实际上是不同步的。当 `.list-item
` 从列表中移除时,它会向右滑动,当它添加到列表中时,它会从右侧滑动。列表中需要有足够的垂直空间来为新的 `.list-item
` 腾出空间,然后它才能开始动画进入列表,反之亦然,删除时也是如此。
至于样式,它非常类似于向下滑动不透明度动画,唯一的区别是 `.list-item
` 的 `transition
` 现在应该在 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
` 的 `container
` 元素开始过渡后 350 毫秒延迟 `listItem
` 动画的开始。
setTimeout(function(){
container.classList.add('show');
setTimeout(function(){
listItem.classList.add('show');
}, 350);
}, 10);
在 `removeListItem
` 函数中,我们首先删除列表项的 `.show
` 类,以便它可以立即开始过渡。然后,父 `container
` 元素会失去其 `.show
` 类,但这仅在初始 `listItem
` 过渡开始后的 350 毫秒后。然后,在 `container
` 元素开始过渡后的 600 毫秒(或 `listItem
` 过渡后的 950 毫秒),我们将 `container
` 元素从 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 中移除的元素而重新定位的情况下工作。
如你所见,有很多移动的部件和需要考虑的事情。我们从对这种现实世界中运动的期望开始,并考虑了当其中一个元素被更新时,元素组会发生什么。在显示和隐藏状态之间进行转换以及哪个元素在特定时间获得这些状态之间需要一些平衡,但我们做到了。我们甚至还确保我们的列表既高效又易访问,这些都是我们在实际项目中需要处理的事情。
无论如何,我祝愿你未来的项目一切顺利。这就是我的全部内容。完毕。
我正在浏览这篇指南,但发现它对简单的添加和删除动画来说太复杂了。我尝试了一下,想出了这个 https://jsbin.com/bopapiriqu/
它只使用 margin。
我知道这与上面的动画完全不同,但它对我有用。
我去年冬天遇到了这个问题,并想出了 这个解决方案。
它只处理删除情况,并使用稍微不同的方法,网格用于布局,而在运动方面,它归结为:当一个块关闭(在 `
transitionend
` 上)时,我获取它的位置以及所有后续块的位置,并使用 CSS 动画将所有后续块动画到前一个块的位置;在那之后(在 `animationend
` 上),我将关闭的块从 DOM 中移除。一个不错的技巧是将行高、填充和/或边距动画设置为零。不够专业,但它会滑动(或多或少),而不是卡住。
我一直试图解决这个问题,并且看到了这篇文章。请原谅我,但读完之后我感到失望,我将指出以下“原因”并提供我的解决方案
1. 它没有解释最重要的是什么,它让移除一个项目时的过渡变得流畅,而剩余的项目则在 DOM 中占据空间。
2. 所使用的方法过于复杂,我难以理解代码,并试图理解每个代码与动画效果的关系。
3. 这项技术过于困难和混乱,在编码中,简洁性是最好的,为什么你需要在 JavaScript 中编码高度控制,而你可以在 CSS 中完成呢?
我查看了 duckydude20 的解决方案,我喜欢代码的简洁性。但我也有自己的解决方案,虽然只对移除项进行了动画效果,因为添加项非常容易。
我会在这里给出解释。 https://codepen.io/nato11111/pen/GRxYZJZ