Carousel组件设计和具体代码实现
在前端中很多时候我们都需要实现图片自动播放然后图片一张一张从左往右滑动的一个效果。在组件库中我们称之为Carousel组件,我们从思路到代码来一起看看怎么封装一个属于自己的Carousel组件。
- 一个这样的组件首先想到的依然还是dom结构组成。
- 怎么保证无缝连接这个是这个组件的关键。
- 动画效果的实现,转场的过程中动画的衔接。
这个图大致就是我封装的时候的一个大致方向。每次移动的时候呢就是整个容器一起移动每次移动的距离每次就一个dom。
在可视区显示的就是当前能看到的图,超过这个可视区全部隐藏掉。在末尾我有复制了一个视图第一个元素的dom,这个就是实现无缝滚动的关键所在。当我们滑动到了最后一个视图,我们可以把整个容器的位置设置成最开始的位置,这样就可以完美实现最后一个视图和第一个视图的完美衔接了。
具体的代码逻辑就是先设置容器运动的动画正常执行,动画执行完成之后使用transition:none;然后修改transform:translate()的值。
由于没有动画效果所以这个操作对于用户是无感的。这个无缝的关键就是在动画效果取消,动画效果恢复。
Carousel代码实现逻辑
/*
* @Date: 2023-08-23 14:06:45
* @Auth: 463997479@qq.com
* @LastEditors: 463997479@qq.com
* @LastEditTime: 2023-08-29 14:26:30
* @FilePath: \reactui\src\Carousel\index.tsx
*/
import { LeftCircleOutlined, RightCircleOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { debounce } from '../until';
type CarouselType = {
children: any;
autoplay?: boolean;
};
const Carousel: React.FC<CarouselType> = (props) => {
const { children, autoplay = false } = props;
const contentClass = classNames('carousel-content');
const childrenRef = useRef();
const [contentWidth, setContentWidth] = useState<number>();
const [width, setWidth] = useState<number>(0);
const [activeIndex, setActiveIndex] = useState<number>(0);
const contentRef = useRef();
const [contentStyle, setContentStyle] = useState<any>({});
const [timer, setTimer] = useState<any>(null);
const [flag, setFlag] = useState<boolean>(true);
useLayoutEffect(() => {
let index = activeIndex;
//清除当前任务
if (timer) {
clearInterval(timer);
}
setTimer(
setInterval(() => {
if (flag) {
return;
}
if (index <= children.length - 1) {
index += 1;
console.log(index);
setContentStyle({
transform: `translate3d(-${width * Math.abs(index)}px, 0px, 0px)`,
transition: 'all .2s ease',
});
if (index === children.length) {
index = 0;
setTimeout(() => {
setContentStyle({
transform: `translate3d(-${width * index}px, 0px, 0px)`,
transition: 'none',
});
}, 200);
}
setActiveIndex(index);
}
}, 2000),
);
return () => {
clearInterval(timer);
};
}, [flag, width, activeIndex]);
useEffect(() => {
if (childrenRef.current) {
const _width = childrenRef.current.offsetWidth;
setContentWidth(_width * children.length);
setWidth(_width);
}
}, []);
const handlePre = debounce(() => {
let index = activeIndex - 1;
console.log(index);
if (index === -1) {
index = children.length - 1;
setContentStyle({
transform: `translate3d(-${width * (children.length + 1)}px, 0px, 0px)`,
transition: 'none',
});
setTimeout(() => {
setContentStyle({
transform: `translate3d(-${width * index}px, 0px, 0px)`,
transition: 'all 0.2s ease',
});
}, 0);
} else {
setContentStyle({
transform: `translate3d(-${width * Math.abs(index)}px, 0px, 0px)`,
transition: 'all 0.2s ease',
});
}
setActiveIndex(index);
}, 200);
const handleNext = debounce(() => {
let index = activeIndex + 1;
setContentStyle({
transform: `translate3d(-${width * index}px, 0px, 0px)`,
transition: 'all 0.2s ease',
});
if (index === children.length) {
index = 0;
//到达末尾之后取消动画效果
setTimeout(() => {
setContentStyle({
transform: `translate3d(-${width * index}px, 0px, 0px)`,
transition: 'none',
});
}, 200);
}
setActiveIndex(index);
}, 200);
return (
<>
<div className={contentClass}>
<div
onMouseEnter={() => {
if (autoplay) {
setFlag(true);
}
}}
onMouseLeave={() => {
if (autoplay) {
setFlag(false);
}
}}
className="carousel-content-wrapper"
ref={childrenRef}
>
<ul className="carousel-content-dots">
{new Array(children.length).fill('').map((item, index) => (
<li
onClick={() => {
setContentStyle({
transform: `translate3d(-${width * index}px, 0px, 0px)`,
transition: 'all 0.2s ease',
});
setActiveIndex(index);
}}
className={
index === activeIndex ? 'carousel-content-dots-active' : ''
}
key={index}
></li>
))}
</ul>
<div className="carousel-step-content">
<span
onClick={() => handlePre()}
className="carousel-step-content-left"
>
<LeftCircleOutlined />
</span>
<span
onClick={() => handleNext()}
className="carousel-step-content-right"
>
<RightCircleOutlined />
</span>
</div>
<div
style={{ ...contentStyle, width: contentWidth * 2 + 'px' }}
className="carousel-content-list"
ref={contentRef}
>
{React.Children.map([...children], (child) => {
return (
contentWidth &&
React.cloneElement(child, {
className: 'carousel-content-item',
style: {
width: contentWidth / children.length + 'px',
},
})
);
})}
{React.Children.map([children[0]], (child) => {
return (
contentWidth &&
React.cloneElement(child, {
className: 'carousel-content-item-clone',
style: {
width: contentWidth / children.length + 'px',
},
})
);
})}
</div>
</div>
</div>
</>
);
};
export default Carousel;
主要就是判断临界值在每个临界值的时候处理的逻辑有一点区别,其次就是在动画的取消还有动画的效果上面,什么时候应该滑动的时候不能加动画,什么时候有需要把动画效果加上。还有一个容器的大小还有各个子容器的大小都需要自己通过获取dom获取。
CSS
.carousel-content {
width: 100%;
.carousel-content-wrapper {
width: 100%;
height: 100%;
position: relative;
display: block;
margin: 0;
padding: 0;
overflow: hidden;
.carousel-content-dots {
position: absolute;
display: flex;
z-index: 1;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
li {
cursor: pointer;
position: relative;
list-style: none;
width: 16px;
height: 3px;
margin: 0 5px;
box-sizing: border-box;
transition: all 0.3s;
padding: 0;
background-color: #ffffff;
opacity: 0.3;
text-align: center;
&::after {
position: absolute;
content: '';
display: block;
}
}
li.carousel-content-dots-active {
width: 24px;
opacity: 1;
}
li:hover {
opacity: 0.5;
}
}
.carousel-content-list {
position: relative;
top: 0;
left: 0;
display: block;
&::after {
display: block;
clear: both;
content: '';
}
.carousel-content-item {
height: 100%;
float: left;
}
.carousel-content-item-clone {
height: 100%;
float: left;
}
}
.carousel-step-content {
position: absolute;
width: 100%;
z-index: 2;
top: 50%;
transform: translateY(-50%);
box-sizing: border-box;
padding: 0 20px;
font-size: 30px;
opacity: 0;
transition: all 0.3s;
&::after {
content: '';
display: block;
clear: both;
}
.carousel-step-content-left {
color: #ffffff;
float: left;
opacity: 0.5;
transition: all 0.3s;
cursor: pointer;
&:hover {
opacity: 1;
}
}
.carousel-step-content-right {
color: #ffffff;
float: right;
opacity: 0.5;
transition: all 0.3s;
cursor: pointer;
&:hover {
opacity: 1;
}
}
}
&:hover {
.carousel-step-content {
opacity: 1;
}
}
}
}
demo效果: