diff --git a/.vscode/settings.json b/.vscode/settings.json index 2c1e710..fbf0295 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,7 +6,7 @@ "typescriptreact" ], "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, - "source.fixAll.stylelint": true + "source.fixAll.eslint": "explicit", + "source.fixAll.stylelint": "explicit" } } diff --git a/example/src/config.json b/example/src/config.json index 0428314..0a4a1ba 100644 --- a/example/src/config.json +++ b/example/src/config.json @@ -13,6 +13,10 @@ { "title": "Touchable 触摸操作", "key": "Touchable" + }, + { + "title": "AnimationView 动画", + "key": "AnimationView" } ] }, diff --git a/example/tsconfig.json b/example/tsconfig.json index 8719589..9112ab8 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -4,7 +4,8 @@ "baseUrl": ".", "paths": { "@src/*": ["../src/*"], - "tdesign-react-native/*": ["../src/*"] + "tdesign-react-native/*": ["../src/*"], + "tdesign-icons-react-native/*": ["../../tdesign-icons/packages/react-native"] } }, "exclude": [ diff --git a/package.json b/package.json index 69f1d33..e324ff1 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.71.3", + "react-native-gesture-handler": "^2.14.0", "react-native-reanimated": "^3.0.2" }, "devDependencies": { diff --git a/site/package.json b/site/package.json index 010a78b..afe4ea4 100644 --- a/site/package.json +++ b/site/package.json @@ -15,6 +15,7 @@ }, "devDependencies": { "@babel/plugin-proposal-decorators": "^7.20.13", + "@esbuild/darwin-arm64": "^0.19.7", "@originjs/vite-plugin-commonjs": "^1.0.3", "@rollup/plugin-replace": "^5.0.2", "@vitejs/plugin-react": "^3.1.0", diff --git a/src/components/AnimationView/AnimationView.md b/src/components/AnimationView/AnimationView.md new file mode 100644 index 0000000..9f10779 --- /dev/null +++ b/src/components/AnimationView/AnimationView.md @@ -0,0 +1,7 @@ +:: BASE_DOC :: + +## API +### Base Props + +临时md,后期会自动生成 + diff --git a/src/components/AnimationView/AnimationView.tsx b/src/components/AnimationView/AnimationView.tsx new file mode 100644 index 0000000..9c92c5a --- /dev/null +++ b/src/components/AnimationView/AnimationView.tsx @@ -0,0 +1,178 @@ +import React, { useEffect, useMemo } from 'react'; +import { GestureDetector, Gesture, Directions } from 'react-native-gesture-handler'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withDelay, + withTiming, + runOnJS, + Easing, +} from 'react-native-reanimated'; +import { wrapStyleTransforms, flattenStyle, TRANSFORM_STYLE_PROPERTIES } from './utils'; +import { AnimationProps, DIRECTION, AnimationDirections } from './types'; + +export const AnimationView = React.forwardRef((props) => { + const { + style, + duration = 300, + delay = 0, + easing = Easing.bezierFn(0.42, 0, 0.58, 1), + animation, + placement = 'center', + gesture, + gestureCallback, + gestureDirection, + ...rest + } = props; + const offset = useSharedValue({ x: 0, y: 0 }); + const timestamp = useSharedValue(0); + + const direction: AnimationDirections = gestureDirection ?? DIRECTION[placement] ?? 0; + const timeConfig = useMemo(() => ({ duration, easing }), [duration, easing]); + + const fromStyle = wrapStyleTransforms(animation?.from); + const fromFlatten = flattenStyle(fromStyle); + const toStyle = wrapStyleTransforms(animation?.to); + const toFlatten = flattenStyle(toStyle); + + const animationSharedValue = useSharedValue(fromFlatten); + + const animatedStyles = useAnimatedStyle(() => { + const result: any = {}; + Object.keys(animationSharedValue.value).forEach((key) => { + if (TRANSFORM_STYLE_PROPERTIES.indexOf(key) !== -1) { + if (!result?.transform) { + result.transform = []; + } + result.transform.push({ [key]: animationSharedValue.value[key] }); + } else { + result[key] = animationSharedValue.value[key]; + } + }); + return result; + }); + + useEffect(() => { + const result: any = {}; + Object.keys(toFlatten).forEach((key) => { + result[key] = withDelay(delay, withTiming(toFlatten[key], timeConfig)); + }); + animationSharedValue.value = result; + }, [animation, animationSharedValue, delay, timeConfig, toFlatten]); + + const gesturePan = Gesture.Pan() + .onBegin(() => { + 'worklet'; + + offset.value = { + x: toFlatten?.translateX ?? 0, + y: toFlatten?.translateY ?? 0, + }; + }) + .onChange((e) => { + 'worklet'; + + console.log(e.changeY, offset.value.y, toFlatten?.translateY); + switch (direction) { + case Directions.DOWN: + if (e.changeY + offset.value.y < toFlatten?.translateY ?? 0) { + return; + } + break; + case Directions.UP: + if (e.changeY + offset.value.y > toFlatten?.translateY ?? 0) { + return; + } + break; + case Directions.LEFT: + if (e.changeX + offset.value.x > toFlatten?.translateX ?? 0) { + return; + } + break; + case Directions.RIGHT: + if (e.changeX + offset.value.x < toFlatten?.translateX ?? 0) { + return; + } + break; + } + + offset.value = { + x: e.changeX + offset.value.x, + y: e.changeY + offset.value.y, + }; + // 更新最新触摸时间 + timestamp.value = new Date().valueOf(); + + switch (direction) { + case Directions.UP: + case Directions.DOWN: + // case 'none': + animationSharedValue.value = { + ...animationSharedValue.value, + translateY: offset.value.y, + }; + break; + case Directions.LEFT: + case Directions.RIGHT: + animationSharedValue.value = { + ...animationSharedValue.value, + translateX: offset.value.x, + }; + break; + } + }) + .onFinalize((e) => { + 'worklet'; + + const mainAxis = ([Directions.UP, Directions.DOWN] as number[]).includes(direction) ? 'Y' : 'X'; + + // 如果滑动方向和定义不一致 返回原位 + if ( + (([Directions.UP, Directions.LEFT] as number[]).includes(direction) && e[`translation${mainAxis}`] > 0) || + (([Directions.DOWN, Directions.RIGHT] as number[]).includes(direction) && e[`translation${mainAxis}`] < 0) + ) { + animationSharedValue.value = { + ...animationSharedValue.value, + [`translate${mainAxis}`]: toFlatten[`translate${mainAxis}`], + }; + return; + } + + // 计算滑动时间,如果很快 速度很大 判断为投掷 + if (Math.abs(e[`translation${mainAxis}`] / (new Date().valueOf() - timestamp.value)) > 5) { + if (gestureCallback) { + runOnJS(gestureCallback)(); + } + return; + } + + // 不超过三分之一 + const distance = Math.abs( + (toFlatten?.[`translate${mainAxis}`] ?? 0) - (fromFlatten?.[`translate${mainAxis}`] ?? 0), + ); + if (distance > 0 && Math.abs(e[`translation${mainAxis}`]) < distance / 3) { + // 返回原位 + animationSharedValue.value = { + ...animationSharedValue.value, + [`translate${mainAxis}`]: toFlatten[`translate${mainAxis}`], + }; + } else { + // 关闭 + if (gestureCallback) { + runOnJS(gestureCallback)(); + } + } + }); + + return gesture ? ( + + + {props.children} + + + ) : ( + + {props.children} + + ); +}); diff --git a/src/components/AnimationView/_example/index.tsx b/src/components/AnimationView/_example/index.tsx new file mode 100644 index 0000000..bc10956 --- /dev/null +++ b/src/components/AnimationView/_example/index.tsx @@ -0,0 +1,125 @@ +/** + * title: AnimationView 动画容器 + * description: 执行特定的动画 + * spline: base + * isComponent: true + * toc: false + */ +import { View, Button, AnimationView } from 'tdesign-react-native/components'; +import { Section, CodeSpace, H3, H5, P } from '@src/../example/src/components'; +import { useState } from 'react'; +import { Easing } from 'react-native-reanimated'; +import { Directions } from 'react-native-gesture-handler'; + +const NormalDemo = () => { + const [toggle, setToggle] = useState(false); + const fromStyle = { width: 20, height: 20, backgroundColor: 'red', opacity: 1, transform: [{ translateX: 0 }] }; + const toStyle = { width: 50, height: 50, backgroundColor: 'blue', opacity: 0.5, transform: [{ translateX: 100 }] }; + return ( + + +