From 3d55eebad57f577c888e6c1bf88f43c020e5d4a3 Mon Sep 17 00:00:00 2001 From: duenyang <377153400@qq.com> Date: Wed, 9 Mar 2022 11:42:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(swiper):=20=E8=B7=9F=E6=8D=AE=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E8=AE=BE=E8=AE=A1=E9=87=8D=E6=9E=84swiper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/swiper/Swiper.tsx | 264 ++++++++++++++++++++++++------- src/swiper/SwiperItem.tsx | 121 +++++++++++++- src/swiper/_example/base.jsx | 36 +++++ src/swiper/_example/swiper.jsx | 39 ----- src/swiper/_example/vertical.jsx | 23 --- src/swiper/swiper.md | 12 +- src/swiper/type.ts | 69 +++++++- 7 files changed, 434 insertions(+), 130 deletions(-) create mode 100644 src/swiper/_example/base.jsx delete mode 100644 src/swiper/_example/swiper.jsx delete mode 100644 src/swiper/_example/vertical.jsx diff --git a/src/swiper/Swiper.tsx b/src/swiper/Swiper.tsx index e9c12f767..92fdfd89a 100644 --- a/src/swiper/Swiper.tsx +++ b/src/swiper/Swiper.tsx @@ -1,8 +1,9 @@ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, useRef, isValidElement } from 'react'; import classnames from 'classnames'; +import { IconFont } from 'tdesign-icons-react'; import useConfig from '../_util/useConfig'; import noop from '../_util/noop'; -import { TdSwiperProps, SwiperChangeSource } from './type'; +import { TdSwiperProps, SwiperChangeSource, SwiperNavigation } from './type'; import { StyledProps } from '../common'; import SwiperItem from './SwiperItem'; @@ -11,26 +12,51 @@ export interface SwiperProps extends TdSwiperProps, StyledProps { children?: React.ReactNode; } +const defaultNavigation = { + placement: 'inside', + showSlideBtn: 'always', + size: 'medium', + type: 'bars', +}; + const Swiper = (props: SwiperProps) => { const { - // animation = 'slide', // 轮播切换动画效果类型(暂时没用) + // theme + animation = 'slide', // 轮播切换动画效果类型 autoplay = true, // 是否自动播放 - current, // 当前轮播在哪一项(下标) + current = 0, // 当前轮播在哪一项(下标) defaultCurrent = 0, // 当前轮播在哪一项(下标),非受控属性 direction = 'horizontal', // 轮播滑动方向,包括横向滑动和纵向滑动两个方向 duration = 300, // 滑动动画时长 interval = 5000, // 轮播间隔时间 + trigger = 'hover', + height, + loop = true, + stopOnHover = true, onChange = noop, // 轮播切换时触发 className, children, + navigation, + type = 'default', } = props; const { classPrefix } = useConfig(); + let navigationConfig = defaultNavigation; + let navigationNode = null; + if (isValidElement(navigation)) { + navigationNode = navigation; + } else { + navigationConfig = { ...defaultNavigation, ...(navigation as SwiperNavigation) }; + } + const [currentIndex, setCurrentIndex] = useState(defaultCurrent); - const [animation, setAnimation] = useState(true); + const [needAnimation, setNeedAnimation] = useState(true); + const [arrowShow, setArrowShow] = useState(navigationConfig.showSlideBtn === 'always'); const swiperTimer = useRef(null); // 计时器指针 const isHovering = useRef(false); - const wrapperRef = useRef(null); + const swiperWrap = useRef(null); + + const getWrapAttribute = (attr) => swiperWrap.current?.parentNode?.[attr]; // 进行子组件筛选,创建子节点列表 const childrenList = useMemo( @@ -44,12 +70,23 @@ const Swiper = (props: SwiperProps) => { // 创建渲染用的节点列表 const swiperItemList = childrenList.map((child: JSX.Element, index: number) => - React.cloneElement(child, { value: index, ...child.props }), + React.cloneElement(child, { + key: index, + index, + currentIndex, + needAnimation, + childrenLength, + getWrapAttribute, + ...props, + ...child.props, + }), ); // 子节点不为空时,复制第一个子节点到列表最后 - if (childrenLength > 0) { + if (childrenLength > 0 && type === 'default') { const firstEle = swiperItemList[0]; - swiperItemList.push(React.cloneElement(firstEle, { ...firstEle.props, key: `${firstEle.key}-cloned` })); + swiperItemList.push( + React.cloneElement(firstEle, { ...firstEle.props, key: childrenLength, index: childrenLength }), + ); } const swiperItemLength = swiperItemList.length; @@ -59,7 +96,7 @@ const Swiper = (props: SwiperProps) => { // 事件通知 onChange(index % childrenLength, context); // 设置内部 index - setAnimation(true); + setNeedAnimation(true); setCurrentIndex(index); }, [childrenLength, onChange], @@ -76,6 +113,7 @@ const Swiper = (props: SwiperProps) => { ); } }, [autoplay, currentIndex, duration, interval, swiperTo]); + const clearTimer = useCallback(() => { if (swiperTimer.current) { clearTimeout(swiperTimer.current); @@ -83,81 +121,199 @@ const Swiper = (props: SwiperProps) => { } }, []); + const isEnd = useCallback(() => { + if (type === 'card') { + return !loop && currentIndex + 1 >= swiperItemLength; + } + return !loop && currentIndex + 2 >= swiperItemLength; + }, [loop, currentIndex, swiperItemLength, type]); + // 监听 current 参数变化 useEffect(() => { if (current !== undefined) { - swiperTo(current % childrenLength, { source: '' }); + swiperTo(current % childrenLength, { source: 'autoplay' }); } }, [current, childrenLength, swiperTo]); - // 在非鼠标 hover 状态时,添加切换下一个组件的定时器 - useEffect(() => { - // 设置自动播放的定时器 - if (!isHovering.current) { - clearTimer(); - setTimer(); - } - }, [clearTimer, setTimer]); - // 动画完成后取消 css 属性 useEffect(() => { setTimeout(() => { - setAnimation(false); - if (currentIndex + 1 >= swiperItemLength) { + setNeedAnimation(false); + if (isEnd()) { + clearTimer(); + } + if (currentIndex + 1 >= swiperItemLength && type !== 'card') { setCurrentIndex(0); } }, duration + 50); // 多 50ms 的间隔时间参考了 react-slick 的动画间隔取值 - }, [currentIndex, swiperItemLength, duration, direction]); + }, [currentIndex, swiperItemLength, duration, direction, animation, type, clearTimer, isEnd]); + + useEffect(() => { + if (!isHovering.current || !stopOnHover) { + clearTimer(); + setTimer(); + } + }, [setTimer, clearTimer, stopOnHover]); // 鼠标移入移出事件 const onMouseEnter = () => { isHovering.current = true; - clearTimer(); + if (stopOnHover) { + clearTimer(); + } + if (navigationConfig.showSlideBtn === 'hover') { + setArrowShow(true); + } }; const onMouseLeave = () => { isHovering.current = false; - setTimer(); + if (!isEnd()) { + setTimer(); + } + if (navigationConfig.showSlideBtn === 'hover') { + setArrowShow(false); + } + }; + + const navMouseAction = (action: 'enter' | 'leave' | 'click', index: number) => { + if (action === 'enter' && trigger === 'hover') { + swiperTo(index, { source: 'hover' }); + } + if (action === 'click' && trigger === 'click') { + swiperTo(index, { source: 'click' }); + } + }; + + const arrowClick = (direction: 'left' | 'right') => { + if (direction === 'right') { + if (type === 'card') { + return swiperTo(currentIndex + 1 >= swiperItemLength ? 0 : currentIndex + 1, { source: 'click' }); + } + return swiperTo(currentIndex + 1, { source: 'click' }); + } + if (direction === 'left') { + if (currentIndex - 1 < 0) { + return swiperTo(childrenLength - 1, { source: 'click' }); + } + return swiperTo(currentIndex - 1, { source: 'click' }); + } + }; + + const createArrow = (type: 'default' | 'fraction') => { + if (!arrowShow) { + return ''; + } + if (navigationConfig.type === 'fraction' && type === 'default') { + return ''; + } + const fractionIndex = currentIndex + 1 > childrenLength ? 1 : currentIndex + 1; + return ( +
+
arrowClick('left')}> + +
+ {type === 'fraction' ? ( +
+ {fractionIndex}/{childrenLength} +
+ ) : ( + '' + )} +
arrowClick('right')}> + +
+
+ ); + }; + + const createNavigation = () => { + if (navigationConfig.type === 'fraction') { + return ( +
+ {createArrow('fraction')} +
+ ); + } + return navigationNode ? ( + <>{navigationNode} + ) : ( + + ); }; // 构造 css 对象 - // 加入了 translateZ 属性是为了使移动的 div 单独列为一个 layer 以提高滑动性能,参考:https://segmentfault.com/a/1190000010364647 - let wrapperStyle = {}; - if (direction === 'vertical') { - wrapperStyle = { - height: `${swiperItemLength * 100}%`, - top: `-${currentIndex * 100}%`, - transition: animation ? `top ${duration / 1000}s` : '', - }; - } else { - wrapperStyle = { - width: `${swiperItemLength * 100}%`, - left: `-${currentIndex * 100}%`, - transition: animation ? `left ${duration / 1000}s` : '', - }; - } + const getWrapperStyle = () => { + const offsetHeight = height ? `${height}px` : `${getWrapAttribute('offsetHeight')}px`; + if (type === 'card' || animation === 'fade') { + return { + height: offsetHeight, + }; + } + if (animation === 'slide') { + if (direction === 'vertical') { + return { + height: offsetHeight, + transform: `translate3d(0, -${currentIndex * 100}%, 0px)`, + transition: needAnimation ? `transform ${duration / 1000}s` : '', + }; + } + return { + transform: `translate3d(-${currentIndex * 100}%, 0px, 0px)`, + transition: needAnimation ? `transform ${duration / 1000}s` : '', + }; + } + }; return (
- {/* 渲染子节点 */} -
-
- {swiperItemList} +
+
+
+ {swiperItemList} +
+ {createNavigation()} + {createArrow('default')}
- {/* 渲染右侧切换小点 */} -
    - {childrenList.map((_: JSX.Element, i: number) => ( -
  • swiperTo(i, { source: 'touch' })} - /> - ))} -
); }; diff --git a/src/swiper/SwiperItem.tsx b/src/swiper/SwiperItem.tsx index 6fdff6249..a5e10a6db 100644 --- a/src/swiper/SwiperItem.tsx +++ b/src/swiper/SwiperItem.tsx @@ -1,13 +1,124 @@ import React from 'react'; -import { StyledProps } from '../common'; +import classnames from 'classnames'; +import useConfig from '../_util/useConfig'; +import { SwiperProps } from './Swiper'; -export interface SwiperItemProps extends StyledProps { - children?: React.ReactNode; +export interface SwiperItemProps extends SwiperProps { + currentIndex?: number; + index?: number; + needAnimation?: boolean; + childrenLength?: number; + getWrapAttribute?: (attr: string) => number; } +const CARD_SCALE = 0.63; + const SwiperItem = (props: SwiperItemProps) => { - const { children, className } = props; - return
{children}
; + const { + children, + currentIndex, + index, + animation, + duration = 300, + needAnimation, + type = 'default', + childrenLength, + getWrapAttribute, + } = props; + const { classPrefix } = useConfig(); + const wrapWidth = getWrapAttribute('offsetWidth'); + + const disposeIndex = (index, currentIndex, childrenLength) => { + if (currentIndex === 0 && index === childrenLength - 1) { + return -1; + } + if (currentIndex === childrenLength - 1 && index === 0) { + return childrenLength; + } + if (index < currentIndex - 1 && currentIndex - index >= childrenLength / 2) { + return childrenLength + 1; + } + if (index > currentIndex + 1 && index - currentIndex >= childrenLength / 2) { + return -2; + } + + return index; + }; + + const calculateTranslate = (index: number, currentIndex: number, parentWidth: number, inStage: boolean) => { + const denominator = 3.4; + if (inStage) { + // if (index === -1) { + // console.log('====', (2 - CARD_SCALE) * (index - currentIndex) + 1); + // } + // // console.log((parentWidth * ((2 - CARD_SCALE) * (index - currentIndex) + 1)) / denominator, '======='); + // return (parentWidth * ((2 - CARD_SCALE) * (index - currentIndex) + 1)) / denominator; + if (index < currentIndex) return -parentWidth * 0.07625; + if (index === currentIndex) return parentWidth * 0.2925; + if (index > currentIndex) { + return parentWidth * 0.66125; + } + // return parentWidth - parentWidth * 0.415 * CARD_SCALE - parentWidth * 0.415 * (1 - CARD_SCALE) * 0.5; + } + if (index < currentIndex) { + // console.log('===index', index); + return (-(1 + CARD_SCALE) * parentWidth) / denominator; + } + return ((denominator - 1 + CARD_SCALE) * parentWidth) / denominator; + }; + + const getZindex = (isActivity, inStage) => { + if (isActivity) { + return 2; + } + if (inStage) { + return 1; + } + return 0; + }; + + const getSwiperItemStyle = () => { + if (animation === 'fade') { + return { + opacity: currentIndex === index ? 1 : 0, + transition: needAnimation ? `opacity ${duration / 1000}s` : '', + }; + } + if (type === 'card') { + const translateIndex = + index !== currentIndex && childrenLength > 2 ? disposeIndex(index, currentIndex, childrenLength) : index; + const inStage = Math.round(Math.abs(translateIndex - currentIndex)) <= 1; + console.log('translateIndex', translateIndex, inStage); + const translate = calculateTranslate(translateIndex, currentIndex, wrapWidth, inStage).toFixed(2); + const isActivity = translateIndex === currentIndex; + return { + msTransform: `translateX(${translate}px) scale(${isActivity ? 1 : CARD_SCALE})`, + WebkitTransform: `translateX(${translate}px) scale(${isActivity ? 1 : CARD_SCALE})`, + transform: `translateX(${translate}px) scale(${isActivity ? 1 : CARD_SCALE})`, + transition: `transform ${duration / 1000}s`, + zIndex: getZindex(isActivity, inStage), + translateIndex, + }; + } + }; + + return ( +
+ {children} + {/* { +
+ {getSwiperItemStyle().translateIndex} +
+ } */} +
+ ); }; SwiperItem.displayName = 'SwiperItem'; diff --git a/src/swiper/_example/base.jsx b/src/swiper/_example/base.jsx new file mode 100644 index 000000000..06d8bbac6 --- /dev/null +++ b/src/swiper/_example/base.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Swiper } from 'tdesign-react'; + +const { SwiperItem } = Swiper; + +export default function BasicSwiper() { + return ( +
+ + +
1
+
+ +
2
+
+ +
3
+
+ +
4
+
+
+
+ ); +} diff --git a/src/swiper/_example/swiper.jsx b/src/swiper/_example/swiper.jsx deleted file mode 100644 index c08461023..000000000 --- a/src/swiper/_example/swiper.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { Swiper, Alert } from 'tdesign-react'; - -const { SwiperItem } = Swiper; - -export default function BasicSwiper() { - return ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ); -} diff --git a/src/swiper/_example/vertical.jsx b/src/swiper/_example/vertical.jsx deleted file mode 100644 index aaefe1fcf..000000000 --- a/src/swiper/_example/vertical.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { Swiper, Alert } from 'tdesign-react'; - -const { SwiperItem } = Swiper; - -export default function BasicSwiper() { - return ( - - - - - - - - - - - - - - - ); -} diff --git a/src/swiper/swiper.md b/src/swiper/swiper.md index bc9fed4ec..a497c4c07 100644 --- a/src/swiper/swiper.md +++ b/src/swiper/swiper.md @@ -5,21 +5,23 @@ 名称 | 类型 | 默认值 | 说明 | 必传 -- | -- | -- | -- | -- +className | String | - | 类名 | N +style | Object | - | 样式,TS 类型:`React.CSSProperties` | N animation | String | slide | 轮播切换动画效果类型:滑动、淡入淡出等。可选项:slide/fade | N autoplay | Boolean | true | 是否自动播放 | N -current | Number | - | 当前轮播在哪一项(下标) | N -defaultCurrent | Number | - | 当前轮播在哪一项(下标)。非受控属性 | N +current | Number | 0 | 当前轮播在哪一项(下标) | N +defaultCurrent | Number | 0 | 当前轮播在哪一项(下标)。非受控属性 | N direction | String | horizontal | 轮播滑动方向,包括横向滑动和纵向滑动两个方向。可选项:horizontal/vertical | N duration | Number | 300 | 滑动动画时长 | N height | Number | - | 当使用垂直方向滚动时的高度 | N interval | Number | 5000 | 轮播间隔时间 | N loop | Boolean | true | 是否循环播放 | N -navigation | Object | - | 导航器全部配置。TS 类型:`SwiperNavigation` | N +navigation | TNode | - | 导航器全部配置。TS 类型:`SwiperNavigation | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N stopOnHover | Boolean | true | 是否悬浮时停止轮播 | N theme | String | light | 深色模式和浅色模式。可选项:light/dark | N trigger | String | hover | 触发切换的方式:悬浮、点击等。可选项:hover/click | N type | String | default | 样式类型:默认样式、卡片样式。可选项:default/card | N -onChange | Function | | TS 类型:`(current: number, context: { source: SwiperChangeSource }) => void`
轮播切换时触发。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/swiper/type.ts)。
`type SwiperChangeSource = 'autoplay' | 'click'`
| N +onChange | Function | | TS 类型:`(current: number, context: { source: SwiperChangeSource }) => void`
轮播切换时触发。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/swiper/type.ts)。
`type SwiperChangeSource = 'autoplay' | 'click' | 'hover'`
| N ### SwiperNavigation @@ -28,4 +30,4 @@ onChange | Function | | TS 类型:`(current: number, context: { source: Swipe placement | String | inside | 导航器位置,位于主体的内侧或是外侧。可选项:inside/outside | N showSlideBtn | String | always | 何时显示导航器的翻页按钮:始终显示、悬浮显示、永不显示。可选项:always/hover/never | N size | String | medium | 导航器尺寸。可选项:small/medium/large | N -type | String | - | 导航器类型,点状(dots)、点条状(dots-bar)、条状(bars)、分式(fraction)等。TS 类型:`SwiperNavigationType`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/swiper/type.ts) | N +type | String | - | 导航器类型,点状(dots)、点条状(dots-bar)、条状(bars)、分式(fraction)等。TS 类型:`SwiperNavigationType` `type SwiperNavigationType = 'dots' | 'dots-bar' | 'bars' | 'fraction'`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/swiper/type.ts) | N diff --git a/src/swiper/type.ts b/src/swiper/type.ts index 1bdc98a5f..0b7309245 100644 --- a/src/swiper/type.ts +++ b/src/swiper/type.ts @@ -2,15 +2,16 @@ /** * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC - * updated at 2021-12-12 18:01:23 * */ +import { TNode } from '../common'; + export interface TdSwiperProps { /** - * 轮播切换动画效果类型 + * 轮播切换动画效果类型:滑动、淡入淡出等 * @default slide */ - animation?: 'slide'; + animation?: 'slide' | 'fade'; /** * 是否自动播放 * @default true @@ -18,10 +19,12 @@ export interface TdSwiperProps { autoplay?: boolean; /** * 当前轮播在哪一项(下标) + * @default 0 */ current?: number; /** * 当前轮播在哪一项(下标),非受控属性 + * @default 0 */ defaultCurrent?: number; /** @@ -34,15 +37,73 @@ export interface TdSwiperProps { * @default 300 */ duration?: number; + /** + * 当使用垂直方向滚动时的高度 + */ + height?: number; /** * 轮播间隔时间 * @default 5000 */ interval?: number; + /** + * 是否循环播放 + * @default true + */ + loop?: boolean; + /** + * 导航器全部配置 + */ + navigation?: SwiperNavigation | TNode; + /** + * 是否悬浮时停止轮播 + * @default true + */ + stopOnHover?: boolean; + /** + * 深色模式和浅色模式 + * @default light + */ + theme?: 'light' | 'dark'; + /** + * 触发切换的方式:悬浮、点击等 + * @default hover + */ + trigger?: 'hover' | 'click'; + /** + * 样式类型:默认样式、卡片样式 + * @default default + */ + type?: 'default' | 'card'; /** * 轮播切换时触发 */ onChange?: (current: number, context: { source: SwiperChangeSource }) => void; } -export type SwiperChangeSource = 'autoplay' | 'touch' | ''; +export interface SwiperNavigation { + /** + * 导航器位置,位于主体的内侧或是外侧 + * @default inside + */ + placement?: 'inside' | 'outside'; + /** + * 何时显示导航器的翻页按钮:始终显示、悬浮显示、永不显示 + * @default always + */ + showSlideBtn?: 'always' | 'hover' | 'never'; + /** + * 导航器尺寸 + * @default medium + */ + size?: 'small' | 'medium' | 'large'; + /** + * 导航器类型,点状(dots)、点条状(dots-bar)、条状(bars)、分式(fraction)等 + * @default '' + */ + type?: SwiperNavigationType; +} + +export type SwiperChangeSource = 'autoplay' | 'click' | 'hover'; + +export type SwiperNavigationType = 'dots' | 'dots-bar' | 'bars' | 'fraction';