Skip to content

Commit

Permalink
feat: 添加动画容器组件
Browse files Browse the repository at this point in the history
  • Loading branch information
yatessss committed Dec 22, 2023
1 parent 05476a3 commit 0bfc0c5
Show file tree
Hide file tree
Showing 12 changed files with 444 additions and 3 deletions.
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
}
}
4 changes: 4 additions & 0 deletions example/src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
{
"title": "Touchable 触摸操作",
"key": "Touchable"
},
{
"title": "AnimationView 动画",
"key": "AnimationView"
}
]
},
Expand Down
3 changes: 2 additions & 1 deletion example/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions src/components/AnimationView/AnimationView.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
:: BASE_DOC ::

## API
### Base Props

临时md,后期会自动生成

178 changes: 178 additions & 0 deletions src/components/AnimationView/AnimationView.tsx
Original file line number Diff line number Diff line change
@@ -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<unknown, AnimationProps>((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 ? (
<GestureDetector gesture={gesturePan}>
<Animated.View style={[style, animatedStyles]} {...rest}>
{props.children}
</Animated.View>
</GestureDetector>
) : (
<Animated.View style={[style, animatedStyles]} {...rest}>
{props.children}
</Animated.View>
);
});
125 changes: 125 additions & 0 deletions src/components/AnimationView/_example/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View style={{ height: 50 }} className="flexRow flexBetween gapX40 px16">
<AnimationView animation={!toggle ? { from: fromStyle, to: toStyle } : { from: toStyle, to: fromStyle }} />
<Button onPress={() => setToggle(!toggle)} size="small" theme="primary" content={'重复动画'} />
</View>
);
};

const DurationDemo = () => {
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 (
<View style={{ height: 50 }} className="flexRow flexBetween gapX40 px16">
<AnimationView
duration={2000}
animation={!toggle ? { from: fromStyle, to: toStyle } : { from: toStyle, to: fromStyle }}
/>
<Button onPress={() => setToggle(!toggle)} size="small" theme="primary" content={'重复动画'} />
</View>
);
};

const DelayDemo = () => {
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 (
<View style={{ height: 50 }} className="flexRow flexBetween gapX40 px16">
<AnimationView
delay={2000}
animation={!toggle ? { from: fromStyle, to: toStyle } : { from: toStyle, to: fromStyle }}
/>
<Button onPress={() => setToggle(!toggle)} size="small" theme="primary" content={'重复动画'} />
</View>
);
};

const EasingDemo = () => {
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 (
<View style={{ height: 50 }} className="flexRow flexBetween gapX40 px16">
<AnimationView
easing={Easing.bounce}
animation={!toggle ? { from: fromStyle, to: toStyle } : { from: toStyle, to: fromStyle }}
/>
<Button onPress={() => setToggle(!toggle)} size="small" theme="primary" content={'重复动画'} />
</View>
);
};

const GestureDemo = () => {
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 (
<View>
<View style={{ height: 50 }} className="flexRow flexBetween gapX40 px16">
<AnimationView
gestureDirection={!toggle ? Directions.LEFT : Directions.RIGHT}
gesture
gestureCallback={() => setToggle(!toggle)}
animation={!toggle ? { from: fromStyle, to: toStyle } : { from: toStyle, to: fromStyle }}
/>
<Button onPress={() => setToggle(!toggle)} size="small" theme="primary" content={'重复动画'} />
</View>
<P>
动画左右滑动试试,当滑动距离小于三分之一时会重置动画,大于三分之一时会触发gestureCallback定义的事件,用于某些动画组件的交互场景
</P>
</View>
);
};

const Demo = () => {
return (
<>
<Section>
<H3>1.基础</H3>
<H5>普通动画</H5>
<CodeSpace>
<NormalDemo />
</CodeSpace>
<H5>动画时间</H5>
<CodeSpace>
<DurationDemo />
</CodeSpace>
<H5>延迟动画</H5>
<CodeSpace>
<DelayDemo />
</CodeSpace>
<H5>动画函数</H5>
<CodeSpace>
<EasingDemo />
</CodeSpace>
</Section>
<Section>
<H3>2.手势</H3>
<H5>动画配合手势</H5>
<CodeSpace>
<GestureDemo />
</CodeSpace>
</Section>
</>
);
};
export default Demo;
2 changes: 2 additions & 0 deletions src/components/AnimationView/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './AnimationView';
export * from './types';
Loading

0 comments on commit 0bfc0c5

Please sign in to comment.