FLIP动画实现拖拽排序

FLIP动画实现拖拽排序

1.概念

  • First:元素的初始状态,记录当前元素的位置、尺寸、透明度等等的样式信息
  • Last:元素的最终状态,即动画后元素的位置、尺寸、透明度等等的样式信息
  • Invert:将元素恢复至动画前状态,即相反操作,先计算出从初始状态到最终状态元素发生的改变,比如宽度、高度、透明度等,然后在元素上应用 transform 或 opacity 使这些改变反转,给人一种错觉,即它原来就在初始位置。
  • Play:执行动画,前面的准备工作都做好了,最后就是 Play 了,移除元素上的 transform(将transform置为0或none) 并启用 tansition 相关的动画。

2.拖拽排序

①首先写好样式,此部分不做过多解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drag and Drop with FLIP Animation</title>
<style>
.list{
width: 250px;
margin: auto;
}
.list-item {
width: 200px;
height: 60px;
background-color: #fff;
margin-top: 10px;
border:1px solid #1D93AB;
border-radius: 10px;
text-align: center;
line-height: 60px;
transition: transform 0.3s ease;
}

.moving {
background: transparent;
color:transparent;
border: 0;
}
</style>

</head>
<body>
<div class="list">
<div draggable="true" class="list-item">1</div>
<div draggable="true" class="list-item">2</div>
<div draggable="true" class="list-item">3</div>
<div draggable="true" class="list-item">4</div>
<div draggable="true" class="list-item">5</div>
<div draggable="true" class="list-item">6</div>
<div draggable="true" class="list-item">7</div>
<div draggable="true" class="list-item">8</div>
<div draggable="true" class="list-item">9</div>
<div draggable="true" class="list-item">10</div>
</div>
</body>
</html>

②使用拖拽API实现元素拖拽排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const list = document.querySelector('.list');
let sourceNode;

list.ondragstart = (e)=>{
setTimeout(()=>{
e.target.classList.add('moving');
})
//获取拖拽元素
sourceNode = e.target;
}

list.ondragover = (e)=>{
//获取所有的子元素
const children = Array.from(list.children);

//获取拖拽元素的索引
const sourceIndex = children.indexOf(sourceNode);
//获取进入元素的索引
const targetIndex = children.indexOf(e.target);

//如果说拖拽元素的索引小于进入元素的索引
if(sourceIndex<targetIndex){
list.insertBefore(sourceNode,e.target.nextElementSibling);
}else{ //如果说拖拽元素的索引大于进入元素的索引
list.insertBefore(sourceNode,e.target);
}
}

list.ondragend = (e)=>{
//移除拖拽元素的moving类
e.target.classList.remove('moving')
}

3.加上FLIP动画

以上代码已经实现了列表元素的拖拽排序,但是没有动画,生硬丑陋,接下来使用FLIP思想,给列表元素加上丝滑动画效果,对ondragenter作以下修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
list.ondragenter = (e)=>{
e.preventDefault();
if (e.target === list || e.target === sourceNode) {
return;
}
const taregetElement = e.target;
//FLIP动画的First部分
let startPosition = taregetElement.getBoundingClientRect();

//获取所有的子元素
const children = Array.from(list.children);

//获取拖拽元素的索引
const sourceIndex = children.indexOf(sourceNode);
//获取进入元素的索引
const targetIndex = children.indexOf(taregetElement);

//如果说拖拽元素的索引小于进入元素的索引
if(sourceIndex<targetIndex){
list.insertBefore(sourceNode,taregetElement.nextElementSibling);
}else{ //如果说拖拽元素的索引大于进入元素的索引
list.insertBefore(sourceNode,taregetElement);
}

const sourceLastIndex = children.indexOf(sourceNode);
//FLIP动画的Last部分
//获取进入元素的位置
let lastPosition = taregetElement.getBoundingClientRect();
//获取移动距离
const disY = startPosition.top - lastPosition.top

if(Math.abs(targetIndex-sourceIndex)==1){
animateElement(taregetElement,disY)
}else if(targetIndex>sourceIndex){
animateAboveElement(targetIndex,sourceLastIndex,disY)
}else{
animateBelowElement(targetIndex,sourceLastIndex,disY)
}
}
function animateElement(element,disY){
//FLIP动画的Invert部分
element.style.transition = 'none';
element.style.transform = `translateY(${disY}px)`;
//FLIP动画的Play部分
requestAnimationFrame(() => {
element.style.transition = 'transform 0.3s ease';
element.style.transform = 'none';
});
}

function animateAboveElement(targetIndex,sourceLastIndex,disY){
Array.from(list.children).forEach((child,index)=>{
if(index<=targetIndex && index>=sourceLastIndex){
animateElement(child,disY)
}
})
}

function animateBelowElement(targetIndex,sourceLastIndex,disY){
Array.from(list.children).forEach((child,index)=>{
if(index>=targetIndex && index<=sourceLastIndex){
animateElement(child,disY)
}
})
}

4.完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drag and Drop with FLIP Animation</title>
<style>
.list{
width: 250px;
margin: auto;
}
.list-item {
width: 200px;
height: 60px;
background-color: #fff;
margin-top: 10px;
border:1px solid #1D93AB;
border-radius: 10px;
text-align: center;
line-height: 60px;
transition: transform 0.3s ease;
}

.moving {
background: transparent;
color:transparent;
border: 0;
}
</style>

</head>
<body>
<div class="list">
<div draggable="true" class="list-item">1</div>
<div draggable="true" class="list-item">2</div>
<div draggable="true" class="list-item">3</div>
<div draggable="true" class="list-item">4</div>
<div draggable="true" class="list-item">5</div>
<div draggable="true" class="list-item">6</div>
<div draggable="true" class="list-item">7</div>
<div draggable="true" class="list-item">8</div>
<div draggable="true" class="list-item">9</div>
<div draggable="true" class="list-item">10</div>
</div>
</body>

<script src="./index.js"></script>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
//js部分
const list = document.querySelector('.list');
let sourceNode;

list.ondragstart = (e)=>{
setTimeout(()=>{
e.target.classList.add('moving');
},0)
//获取拖拽元素
sourceNode = e.target;
}
list.ondragover = (e) => {
e.preventDefault();
};
list.ondragenter = (e)=>{
e.preventDefault();
if (e.target === list || e.target === sourceNode) {
return;
}
const taregetElement = e.target;
//FLIP动画的First部分
let startPosition = taregetElement.getBoundingClientRect();

//获取所有的子元素
const children = Array.from(list.children);

//获取拖拽元素的索引
const sourceIndex = children.indexOf(sourceNode);
//获取进入元素的索引
const targetIndex = children.indexOf(taregetElement);

//如果说拖拽元素的索引小于进入元素的索引
if(sourceIndex<targetIndex){
list.insertBefore(sourceNode,taregetElement.nextElementSibling);
}else{ //如果说拖拽元素的索引大于进入元素的索引
list.insertBefore(sourceNode,taregetElement);
}

const sourceLastIndex = children.indexOf(sourceNode);
//FLIP动画的Last部分
//获取进入元素的位置
let lastPosition = taregetElement.getBoundingClientRect();
//获取移动距离
const disY = startPosition.top - lastPosition.top

if(Math.abs(targetIndex-sourceIndex)==1){
animateElement(taregetElement,disY)
}else if(targetIndex>sourceIndex){
animateAboveElement(targetIndex,sourceLastIndex,disY)
}else{
animateBelowElement(targetIndex,sourceLastIndex,disY)
}
}

list.ondragend = (e)=>{
//移除拖拽元素的moving类
e.target.classList.remove('moving')
}

function animateElement(element,disY){
//FLIP动画的Invert部分
element.style.transition = 'none';
element.style.transform = `translateY(${disY}px)`;
//FLIP动画的Play部分
requestAnimationFrame(() => {
element.style.transition = 'transform 0.3s ease';
element.style.transform = 'none';
});
}

function animateAboveElement(targetIndex,sourceLastIndex,disY){
Array.from(list.children).forEach((child,index)=>{
if(index<=targetIndex && index>=sourceLastIndex){
animateElement(child,disY)
}
})
}

function animateBelowElement(targetIndex,sourceLastIndex,disY){
Array.from(list.children).forEach((child,index)=>{
if(index>=targetIndex && index<=sourceLastIndex){
animateElement(child,disY)
}
})
}