react-native-x-components 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/XActionSheet/index.d.ts +47 -0
- package/dist/XActionSheet/index.js +100 -0
- package/dist/XAnimatedSearchPanel/index.d.ts +33 -0
- package/dist/XAnimatedSearchPanel/index.js +295 -0
- package/dist/XAnimatedView/index.d.ts +77 -0
- package/dist/XAnimatedView/index.js +227 -0
- package/dist/XButton/index.d.ts +44 -0
- package/dist/XButton/index.js +126 -0
- package/dist/XCalendar/XCalendarPopup.d.ts +38 -0
- package/dist/XCalendar/XCalendarPopup.js +76 -0
- package/dist/XCalendar/index.d.ts +50 -0
- package/dist/XCalendar/index.js +241 -0
- package/dist/XCarousel/index.d.ts +72 -0
- package/dist/XCarousel/index.js +183 -0
- package/dist/XCascadeSelect/index.d.ts +39 -0
- package/dist/XCascadeSelect/index.js +147 -0
- package/dist/XChart/index.d.ts +78 -0
- package/dist/XChart/index.js +162 -0
- package/dist/XCheckbox/index.d.ts +61 -0
- package/dist/XCheckbox/index.js +128 -0
- package/dist/XConfirmForm/index.d.ts +69 -0
- package/dist/XConfirmForm/index.js +113 -0
- package/dist/XDivider/index.d.ts +33 -0
- package/dist/XDivider/index.js +61 -0
- package/dist/XDropdownMenu/index.d.ts +66 -0
- package/dist/XDropdownMenu/index.js +258 -0
- package/dist/XElevator/index.d.ts +52 -0
- package/dist/XElevator/index.js +138 -0
- package/dist/XForm/FormStore.d.ts +82 -0
- package/dist/XForm/FormStore.js +279 -0
- package/dist/XForm/index.d.ts +116 -0
- package/dist/XForm/index.js +212 -0
- package/dist/XForm/types.d.ts +82 -0
- package/dist/XForm/types.js +1 -0
- package/dist/XForm/useForm.d.ts +2 -0
- package/dist/XForm/useForm.js +19 -0
- package/dist/XForm/utils.d.ts +42 -0
- package/dist/XForm/utils.js +131 -0
- package/dist/XForm/validate.d.ts +20 -0
- package/dist/XForm/validate.js +118 -0
- package/dist/XFormPro/components/FieldTrigger.d.ts +14 -0
- package/dist/XFormPro/components/FieldTrigger.js +38 -0
- package/dist/XFormPro/components/XDateField.d.ts +22 -0
- package/dist/XFormPro/components/XDateField.js +22 -0
- package/dist/XFormPro/components/XSelectField.d.ts +16 -0
- package/dist/XFormPro/components/XSelectField.js +19 -0
- package/dist/XFormPro/index.d.ts +44 -0
- package/dist/XFormPro/index.js +111 -0
- package/dist/XFormPro/renderers.d.ts +24 -0
- package/dist/XFormPro/renderers.js +53 -0
- package/dist/XFormPro/types.d.ts +142 -0
- package/dist/XFormPro/types.js +1 -0
- package/dist/XImage/index.d.ts +13 -0
- package/dist/XImage/index.js +32 -0
- package/dist/XInput/index.d.ts +43 -0
- package/dist/XInput/index.js +136 -0
- package/dist/XLicensePlate/index.d.ts +40 -0
- package/dist/XLicensePlate/index.js +172 -0
- package/dist/XLocale/index.d.ts +23 -0
- package/dist/XLocale/index.js +174 -0
- package/dist/XModalForm/index.d.ts +32 -0
- package/dist/XModalForm/index.js +85 -0
- package/dist/XMultiSelect/index.d.ts +31 -0
- package/dist/XMultiSelect/index.js +131 -0
- package/dist/XNumberKeyboard/index.d.ts +57 -0
- package/dist/XNumberKeyboard/index.js +142 -0
- package/dist/XPicker/index.d.ts +35 -0
- package/dist/XPicker/index.js +96 -0
- package/dist/XPickerDate/index.d.ts +58 -0
- package/dist/XPickerDate/index.js +234 -0
- package/dist/XPopupProvider/index.d.ts +23 -0
- package/dist/XPopupProvider/index.js +6 -0
- package/dist/XProgress/index.d.ts +49 -0
- package/dist/XProgress/index.js +196 -0
- package/dist/XProviders/XImagePreview/index.d.ts +44 -0
- package/dist/XProviders/XImagePreview/index.js +164 -0
- package/dist/XProviders/XLoadingModal/index.d.ts +46 -0
- package/dist/XProviders/XLoadingModal/index.js +119 -0
- package/dist/XProviders/XToast/index.d.ts +50 -0
- package/dist/XProviders/XToast/index.js +183 -0
- package/dist/XProviders/index.d.ts +31 -0
- package/dist/XProviders/index.js +28 -0
- package/dist/XProviders/usageExample.d.ts +19 -0
- package/dist/XProviders/usageExample.js +83 -0
- package/dist/XPullView/index.d.ts +76 -0
- package/dist/XPullView/index.js +220 -0
- package/dist/XRadio/index.d.ts +83 -0
- package/dist/XRadio/index.js +186 -0
- package/dist/XRecord/index.d.ts +37 -0
- package/dist/XRecord/index.js +160 -0
- package/dist/XSignature/XSignatureSkia.d.ts +39 -0
- package/dist/XSignature/XSignatureSkia.js +184 -0
- package/dist/XSignature/core.d.ts +69 -0
- package/dist/XSignature/core.js +108 -0
- package/dist/XSignature/index.d.ts +33 -0
- package/dist/XSignature/index.js +88 -0
- package/dist/XTabs/index.d.ts +65 -0
- package/dist/XTabs/index.js +210 -0
- package/dist/XTag/index.d.ts +61 -0
- package/dist/XTag/index.js +153 -0
- package/dist/XTopView/index.d.ts +25 -0
- package/dist/XTopView/index.js +8 -0
- package/dist/XTopViewStore/index.d.ts +45 -0
- package/dist/XTopViewStore/index.js +49 -0
- package/dist/XUpload/XUploadImage.d.ts +37 -0
- package/dist/XUpload/XUploadImage.js +142 -0
- package/dist/XUpload/XUploadVideo.d.ts +24 -0
- package/dist/XUpload/XUploadVideo.js +83 -0
- package/dist/XUpload/adapters.d.ts +50 -0
- package/dist/XUpload/adapters.js +110 -0
- package/dist/XUpload/helpers.d.ts +35 -0
- package/dist/XUpload/helpers.js +85 -0
- package/dist/XUpload/index.d.ts +12 -0
- package/dist/XUpload/index.js +5 -0
- package/dist/XUpload/provider.d.ts +26 -0
- package/dist/XUpload/provider.js +43 -0
- package/dist/XUpload/types.d.ts +31 -0
- package/dist/XUpload/types.js +4 -0
- package/dist/XWheel/index.d.ts +49 -0
- package/dist/XWheel/index.js +263 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +127 -0
- package/dist/theme.d.ts +164 -0
- package/dist/theme.js +161 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# react-native-x-components
|
|
2
|
+
|
|
3
|
+
Ant Design 风格的 React Native (Expo) 组件库。对齐 antd v5 设计 token,内置**暗黑模式**与**中英双语**,弹层体系基于自研 TopView(比 RN Modal 更快),覆盖表单、选择器、日历、录音、签名(Skia 逐字签名)、上传(MinIO 预签名适配器)、轻量图表等 40+ 组件。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 🌗 **暗黑模式**:`useXTheme()` 全组件自适应,`setXThemeMode('light' | 'dark' | 'system')` 一行切换
|
|
8
|
+
- 🌐 **国际化**:内置 zh-CN / en-US 字典,`setXLocale('en-US')` 即时生效
|
|
9
|
+
- ⚡ **TopView 弹层**:Reanimated UI 线程驱动,无原生 Modal 开销
|
|
10
|
+
- 📝 **Skia 逐字签名**:一字一格可重写,结构化 value,中点二次贝塞尔平滑
|
|
11
|
+
- 📤 **上传适配器**:UI 与上传彻底解耦,内置 MinIO 预签名直传 / multipart / Mock 三种适配器
|
|
12
|
+
- 📊 **轻量图表**:react-native-svg 自研折线/柱状/环形,零重量级依赖
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install react-native-x-components dayjs zustand
|
|
18
|
+
# 必需 peer 依赖(Expo 项目推荐用 expo install 自动匹配版本)
|
|
19
|
+
npx expo install react-native-reanimated react-native-safe-area-context react-native-svg
|
|
20
|
+
# 可选(按需装):
|
|
21
|
+
# 签名: npx expo install @shopify/react-native-skia
|
|
22
|
+
# 录音: npx expo install expo-audio
|
|
23
|
+
# 上传: npx expo install expo-image-picker expo-image-manipulator expo-file-system
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 快速上手
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import {
|
|
30
|
+
XPopupProvider, // 1. 根部挂弹层宿主(必需一次)
|
|
31
|
+
XButton,
|
|
32
|
+
XCalendarPopup,
|
|
33
|
+
setXThemeMode, // 2. 暗黑模式
|
|
34
|
+
setXLocale, // 3. 中英切换
|
|
35
|
+
} from 'react-native-x-components';
|
|
36
|
+
|
|
37
|
+
export default function App() {
|
|
38
|
+
return (
|
|
39
|
+
<XPopupProvider>
|
|
40
|
+
<XButton type="primary" onPress={() => setXThemeMode('dark')}>
|
|
41
|
+
切换暗黑
|
|
42
|
+
</XButton>
|
|
43
|
+
</XPopupProvider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 组件总览
|
|
49
|
+
|
|
50
|
+
| 分类 | 组件 |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| 基础 | XButton XDivider XInput XImage XTag XProgress XRadio XCheckbox |
|
|
53
|
+
| 表单 | XForm XFormPro XCascadeSelect XMultiSelect XModalForm |
|
|
54
|
+
| 弹层 | XPullView XActionSheet XPicker XPickerDate XConfirmForm XTopView XAnimatedView |
|
|
55
|
+
| 全局 | XToast XLoadingModal XImagePreview |
|
|
56
|
+
| 交互 | XCarousel XTabs XDropdownMenu XElevator XNumberKeyboard XLicensePlate |
|
|
57
|
+
| 媒体 | XRecord XCalendar XCalendarPopup XSignature XSignatureSkia XUploadImage XUploadVideo |
|
|
58
|
+
| 数据 | XChart(XLineChart/XBarChart/XPieChart) |
|
|
59
|
+
| 基建 | useXTheme setXThemeMode useXLocale setXLocale XUploadProvider |
|
|
60
|
+
|
|
61
|
+
## 上传适配器(MinIO 预签名直传)
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { setXUploadAdapter, createMinioPresignedAdapter } from 'react-native-x-components';
|
|
65
|
+
|
|
66
|
+
// App 入口注入一次(对接你的后端预签名接口)
|
|
67
|
+
setXUploadAdapter(
|
|
68
|
+
createMinioPresignedAdapter({
|
|
69
|
+
getUploadUrl: async ({ name, mimeType }) => {
|
|
70
|
+
const res = await fetch(`${API}/file/getUploadUrl?fileName=${name}&contentType=${mimeType}`);
|
|
71
|
+
const { data } = await res.json();
|
|
72
|
+
return { objectKey: data.objectKey, preSignedUrl: data.preSignedUrl };
|
|
73
|
+
},
|
|
74
|
+
getPreviewUrl: async (objectKey) => {
|
|
75
|
+
const res = await fetch(`${API}/file/getPreviewUrl?objectKey=${objectKey}`);
|
|
76
|
+
const { data } = await res.json();
|
|
77
|
+
return data.preSignedUrl;
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
之后 `<XUploadImage />` / `<XUploadVideo />` / `uploadBase64()` 全部走该通道;换后端只换适配器。
|
|
84
|
+
|
|
85
|
+
## 主题定制
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// 修改浅色/暗黑 token(如品牌色)
|
|
89
|
+
import { lightTokens, darkTokens } from 'react-native-x-components';
|
|
90
|
+
lightTokens.colorPrimary = '#7C3AED';
|
|
91
|
+
darkTokens.colorPrimary = '#A78BFA';
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XActionSheet —— 底部弹出菜单(duxui XActionSheet 移植)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 【组合模式】
|
|
7
|
+
* 它本身不写任何动画代码 —— 只是给 XPullView 传 children(菜单内容),
|
|
8
|
+
* XPullView 负责"底部滑入 + 遮罩淡入"的整套动画。
|
|
9
|
+
* 这就是这套组件库的设计思路:XPullView 是动画引擎,业务组件是内容。
|
|
10
|
+
*
|
|
11
|
+
* 【交互】
|
|
12
|
+
* - 点选项 → onClose + onSelect(option)(先关再回调,保证动画和业务分离)
|
|
13
|
+
* - 点取消/遮罩 → onClose
|
|
14
|
+
* - danger 选项显示红色(危险操作)、disabled 选项置灰不可点
|
|
15
|
+
*
|
|
16
|
+
* 【安全区】
|
|
17
|
+
* useSafeAreaInsets() 取 iPhone 底部 Home 条高度,
|
|
18
|
+
* paddingBottom 保证取消按钮不被 Home 条挡住。
|
|
19
|
+
* ============================================================================
|
|
20
|
+
*/
|
|
21
|
+
import React from 'react';
|
|
22
|
+
import { ViewStyle, StyleProp } from 'react-native';
|
|
23
|
+
/** 菜单选项 */
|
|
24
|
+
export interface XActionSheetOption<T = any> {
|
|
25
|
+
/** 显示文本 */
|
|
26
|
+
label: string;
|
|
27
|
+
/** 回传的值 */
|
|
28
|
+
value: T;
|
|
29
|
+
/** 危险操作:文字变红 */
|
|
30
|
+
danger?: boolean;
|
|
31
|
+
/** 禁用:置灰且不可点 */
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
interface XActionSheetProps<T = any> {
|
|
35
|
+
visible: boolean;
|
|
36
|
+
onClose: () => void;
|
|
37
|
+
options: XActionSheetOption<T>[];
|
|
38
|
+
/** 顶部标题(可选) */
|
|
39
|
+
title?: string;
|
|
40
|
+
cancelText?: string;
|
|
41
|
+
duration?: number;
|
|
42
|
+
/** 选中选项回调 */
|
|
43
|
+
onSelect?: (option: XActionSheetOption<T>) => void;
|
|
44
|
+
style?: StyleProp<ViewStyle>;
|
|
45
|
+
}
|
|
46
|
+
export declare function XActionSheet<T = any>({ visible, onClose, options, title, cancelText, duration, onSelect, style, }: XActionSheetProps<T>): React.JSX.Element;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* XActionSheet —— 底部弹出菜单(duxui XActionSheet 移植)
|
|
5
|
+
* ============================================================================
|
|
6
|
+
*
|
|
7
|
+
* 【组合模式】
|
|
8
|
+
* 它本身不写任何动画代码 —— 只是给 XPullView 传 children(菜单内容),
|
|
9
|
+
* XPullView 负责"底部滑入 + 遮罩淡入"的整套动画。
|
|
10
|
+
* 这就是这套组件库的设计思路:XPullView 是动画引擎,业务组件是内容。
|
|
11
|
+
*
|
|
12
|
+
* 【交互】
|
|
13
|
+
* - 点选项 → onClose + onSelect(option)(先关再回调,保证动画和业务分离)
|
|
14
|
+
* - 点取消/遮罩 → onClose
|
|
15
|
+
* - danger 选项显示红色(危险操作)、disabled 选项置灰不可点
|
|
16
|
+
*
|
|
17
|
+
* 【安全区】
|
|
18
|
+
* useSafeAreaInsets() 取 iPhone 底部 Home 条高度,
|
|
19
|
+
* paddingBottom 保证取消按钮不被 Home 条挡住。
|
|
20
|
+
* ============================================================================
|
|
21
|
+
*/
|
|
22
|
+
import { useCallback, useMemo } from 'react';
|
|
23
|
+
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
|
24
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
25
|
+
import { XPullView } from '../XPullView';
|
|
26
|
+
import { useXTheme } from '../theme';
|
|
27
|
+
import { useXLocale } from '../XLocale';
|
|
28
|
+
export function XActionSheet({ visible, onClose, options, title, cancelText, duration = 200, onSelect, style, }) {
|
|
29
|
+
const t = useXTheme();
|
|
30
|
+
const { t: i18n } = useXLocale();
|
|
31
|
+
const resolvedCancel = cancelText ?? i18n('cancel');
|
|
32
|
+
const insets = useSafeAreaInsets();
|
|
33
|
+
/** style sheet 置于组件内,按主题对象 t 构造(color 字段需跟随暗黑模式) */
|
|
34
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
35
|
+
/** 面板整体:浅灰底 + 圆角,顶部裁圆角 */
|
|
36
|
+
panel: {
|
|
37
|
+
backgroundColor: t.colorBgLayout,
|
|
38
|
+
borderTopLeftRadius: 12,
|
|
39
|
+
borderTopRightRadius: 12,
|
|
40
|
+
overflow: 'hidden',
|
|
41
|
+
},
|
|
42
|
+
header: {
|
|
43
|
+
paddingVertical: 14,
|
|
44
|
+
backgroundColor: t.colorBgContainer,
|
|
45
|
+
},
|
|
46
|
+
title: {
|
|
47
|
+
textAlign: 'center',
|
|
48
|
+
fontSize: 13,
|
|
49
|
+
color: t.colorTextTertiary,
|
|
50
|
+
},
|
|
51
|
+
/** 选项区:白底,每个选项之间用 hairline 细线分隔 */
|
|
52
|
+
optionList: {
|
|
53
|
+
backgroundColor: t.colorBgContainer,
|
|
54
|
+
},
|
|
55
|
+
option: {
|
|
56
|
+
height: 52,
|
|
57
|
+
justifyContent: 'center',
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
60
|
+
borderTopColor: t.colorSplit,
|
|
61
|
+
},
|
|
62
|
+
/** 按压反馈底色 */
|
|
63
|
+
pressed: {
|
|
64
|
+
backgroundColor: t.colorBgLayout,
|
|
65
|
+
},
|
|
66
|
+
optionText: {
|
|
67
|
+
fontSize: 16,
|
|
68
|
+
color: t.colorText,
|
|
69
|
+
},
|
|
70
|
+
dangerText: {
|
|
71
|
+
color: t.colorError,
|
|
72
|
+
},
|
|
73
|
+
disabledText: {
|
|
74
|
+
color: t.colorTextQuaternary,
|
|
75
|
+
},
|
|
76
|
+
cancelGap: {
|
|
77
|
+
height: 8,
|
|
78
|
+
},
|
|
79
|
+
cancel: {
|
|
80
|
+
height: 54,
|
|
81
|
+
justifyContent: 'center',
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
backgroundColor: t.colorBgContainer,
|
|
84
|
+
borderTopLeftRadius: 12,
|
|
85
|
+
borderTopRightRadius: 12,
|
|
86
|
+
},
|
|
87
|
+
cancelText: {
|
|
88
|
+
fontSize: 16,
|
|
89
|
+
color: t.colorTextSecondary,
|
|
90
|
+
},
|
|
91
|
+
}), [t]);
|
|
92
|
+
/** 点选项:先关闭弹层,再通知业务方选中了谁 */
|
|
93
|
+
const handleSelect = useCallback((option) => {
|
|
94
|
+
onClose();
|
|
95
|
+
onSelect?.(option);
|
|
96
|
+
}, [onClose, onSelect]);
|
|
97
|
+
return (_jsx(XPullView, { visible: visible, onClose: onClose, side: 'bottom', duration: duration, overlayOpacity: 0.4, children: _jsxs(View, { style: [styles.panel, style, { paddingBottom: Math.max(insets.bottom, 10) }], children: [!!title && (_jsx(View, { style: styles.header, children: _jsx(Text, { style: styles.title, children: title }) })), _jsx(View, { style: styles.optionList, children: options.map(option => (_jsx(Pressable, { disabled: option.disabled, onPress: () => handleSelect(option),
|
|
98
|
+
// style 可以是函数:pressed 状态时换个底色,模拟按压反馈
|
|
99
|
+
style: ({ pressed }) => [styles.option, pressed && !option.disabled && styles.pressed], children: _jsx(Text, { style: [styles.optionText, option.danger && styles.dangerText, option.disabled && styles.disabledText], children: option.label }) }, String(option.value)))) }), _jsx(View, { style: styles.cancelGap }), _jsx(Pressable, { onPress: onClose, style: ({ pressed }) => [styles.cancel, pressed && styles.pressed], children: _jsx(Text, { style: styles.cancelText, children: resolvedCancel }) })] }) }));
|
|
100
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface XAnimatedSearchPanelProps {
|
|
3
|
+
children?: React.ReactNode;
|
|
4
|
+
/**
|
|
5
|
+
* 内容高度兜底值。
|
|
6
|
+
* 首帧 onLayout 还没回调时用它,测到真实高度后立刻被覆盖。
|
|
7
|
+
* 作用是避免「首次点击展开时高度是 0,动画看起来没反应」。
|
|
8
|
+
*/
|
|
9
|
+
contentHeight?: number;
|
|
10
|
+
onSearch: () => void;
|
|
11
|
+
onReset: () => void;
|
|
12
|
+
/**
|
|
13
|
+
* 'spring'(默认):收尾带极轻微回弹,手感更「活」;
|
|
14
|
+
* 'timing':纯缓动,时长可控、绝对不过冲。
|
|
15
|
+
* 面板嵌在长列表头部、或对时序有严格要求时用 'timing'。
|
|
16
|
+
*/
|
|
17
|
+
motion?: 'spring' | 'timing';
|
|
18
|
+
defaultExpanded?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* 注意:如果父组件在这里 setState,会在展开首帧引入一次 React 渲染,
|
|
21
|
+
* 可能造成第一帧轻微卡顿。只在真的需要同步外部状态时才传。
|
|
22
|
+
*/
|
|
23
|
+
onToggle?: (expanded: boolean) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare const XAnimatedSearchPanel: ({ children, contentHeight, onSearch, onReset, motion, defaultExpanded, onToggle, }: XAnimatedSearchPanelProps) => React.JSX.Element;
|
|
26
|
+
/**
|
|
27
|
+
* 样式取值约定(同 X-Components 其他组件):
|
|
28
|
+
* 颜色 / 圆角 / 字号一律从 theme token 取,组件内不写死色值;
|
|
29
|
+
* 仅阴影基色是纯黑中性值,theme 中无对应 token,故保留字面量。
|
|
30
|
+
* 因 color 字段需跟随暗黑模式,style sheet 在组件内按主题对象 t 构造。
|
|
31
|
+
*/
|
|
32
|
+
export type { XAnimatedSearchPanelProps };
|
|
33
|
+
export default XAnimatedSearchPanel;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useRef } from 'react';
|
|
3
|
+
import { View, StyleSheet, TouchableOpacity, Text } from 'react-native';
|
|
4
|
+
import Animated, { Easing, Extrapolation, ReduceMotion, interpolate, useAnimatedStyle, useSharedValue, withSpring, withTiming, } from 'react-native-reanimated';
|
|
5
|
+
import AntDesign from '@react-native-vector-icons/ant-design';
|
|
6
|
+
import { useXTheme } from '../theme';
|
|
7
|
+
import { useXLocale } from '../XLocale';
|
|
8
|
+
/**
|
|
9
|
+
* ── 展开/收起的弹簧参数 ──────────────────────────────────────────────
|
|
10
|
+
* Reanimated 的 withSpring 是一个**阻尼谐振子**的数值积分:
|
|
11
|
+
* 加速度 = (-stiffness * 位移 - damping * 速度) / mass
|
|
12
|
+
*
|
|
13
|
+
* stiffness 刚度:越大越急,动画越快到达目标;
|
|
14
|
+
* damping 阻尼:越大越「粘」,越小越容易来回振荡;
|
|
15
|
+
* mass 质量:越大惯性越强,起步慢、余振长。
|
|
16
|
+
*
|
|
17
|
+
* 阻尼比 ζ = damping / (2 * sqrt(stiffness * mass))
|
|
18
|
+
* ζ < 1 欠阻尼(会过冲、有回弹)
|
|
19
|
+
* ζ = 1 临界阻尼(最快到达且不过冲)
|
|
20
|
+
* ζ > 1 过阻尼(慢慢爬过去,显得拖沓)
|
|
21
|
+
* 本配置 ζ = 22 / (2 * sqrt(240 * 0.75)) ≈ 0.82 —— 略欠阻尼,
|
|
22
|
+
* 收尾有一丝几乎察觉不到的回弹,这是「丝滑」的来源。
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ rest 阈值必须调小:
|
|
25
|
+
* progress 归一化在 0~1,而 Reanimated 默认阈值是按「像素级数值」定的
|
|
26
|
+
* (restDisplacementThreshold 0.01 / restSpeedThreshold 2)。
|
|
27
|
+
* 对 0~1 的量来说 0.01 相当于面板高度的 1%,弹簧会在肉眼可见的位置
|
|
28
|
+
* 提前判定「已静止」而被直接吸附到终点,出现一次微小的跳变。
|
|
29
|
+
*/
|
|
30
|
+
const SPRING_CONFIG = {
|
|
31
|
+
damping: 22,
|
|
32
|
+
stiffness: 240,
|
|
33
|
+
mass: 0.75,
|
|
34
|
+
// 允许轻微过冲(true 会硬性截断,手感变「木」)
|
|
35
|
+
overshootClamping: false,
|
|
36
|
+
restDisplacementThreshold: 0.001,
|
|
37
|
+
restSpeedThreshold: 0.01,
|
|
38
|
+
// 跟随系统「减弱动效」无障碍开关:开启时 Reanimated 自动降级为瞬时到位
|
|
39
|
+
reduceMotion: ReduceMotion.System,
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* ── 纯缓动参数 ──────────────────────────────────────────────────────
|
|
43
|
+
* cubic-bezier(0.22, 1, 0.36, 1) 即 easeOutQuint:
|
|
44
|
+
* 起步极快、尾巴很长的减速曲线。
|
|
45
|
+
* 折叠面板最忌讳「匀速」(linear 会像机械百叶窗)和「先慢后快」,
|
|
46
|
+
* 人眼对「立刻响应 + 缓缓停住」的判断就是「顺滑」。
|
|
47
|
+
* 320ms 是折叠类交互的舒适区间:<200ms 会显得生硬突兀,>400ms 开始觉得慢。
|
|
48
|
+
*/
|
|
49
|
+
const TIMING_CONFIG = {
|
|
50
|
+
duration: 320,
|
|
51
|
+
easing: Easing.bezier(0.22, 1, 0.36, 1),
|
|
52
|
+
reduceMotion: ReduceMotion.System,
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* ── 内容自身高度变化时的补间 ─────────────────────────────────────────
|
|
56
|
+
* 场景:面板已展开,里面的下拉选项异步加载完成 / 校验错误提示出现,
|
|
57
|
+
* 内容高度从 240 变成 310。若直接赋值,用户会看到一次生硬跳变。
|
|
58
|
+
* 这里用一段更短更朴素的缓动(220ms + easeOutQuad)平滑过去——
|
|
59
|
+
* 它是「修正」而不是「主角」,所以不该比主动画更抢眼。
|
|
60
|
+
*/
|
|
61
|
+
const RESIZE_CONFIG = {
|
|
62
|
+
duration: 220,
|
|
63
|
+
easing: Easing.out(Easing.quad),
|
|
64
|
+
reduceMotion: ReduceMotion.System,
|
|
65
|
+
};
|
|
66
|
+
export const XAnimatedSearchPanel = ({ children, contentHeight = 300, onSearch, onReset, motion = 'spring', defaultExpanded = false, onToggle, }) => {
|
|
67
|
+
const t = useXTheme();
|
|
68
|
+
const { t: i18n } = useXLocale();
|
|
69
|
+
/**
|
|
70
|
+
* style sheet 置于组件内,按主题对象 t 构造(color 字段需跟随暗黑模式)。
|
|
71
|
+
* t 在 light/dark 间切换时引用改变,useMemo 仅在此处重算。
|
|
72
|
+
*/
|
|
73
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
74
|
+
card: {
|
|
75
|
+
backgroundColor: t.colorBgContainer,
|
|
76
|
+
borderRadius: t.borderRadiusXL,
|
|
77
|
+
marginHorizontal: 12,
|
|
78
|
+
paddingHorizontal: 12,
|
|
79
|
+
marginVertical: 12,
|
|
80
|
+
shadowColor: '#000',
|
|
81
|
+
shadowOffset: { width: 0, height: 1 },
|
|
82
|
+
shadowOpacity: 0.1,
|
|
83
|
+
shadowRadius: 2,
|
|
84
|
+
elevation: 1,
|
|
85
|
+
},
|
|
86
|
+
searchHeader: {
|
|
87
|
+
flexDirection: 'row',
|
|
88
|
+
justifyContent: 'space-between',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
paddingVertical: 16,
|
|
91
|
+
},
|
|
92
|
+
/** 给交叉淡入的两层文案撑出固定尺寸,见 JSX 注释 */
|
|
93
|
+
titleWrap: {
|
|
94
|
+
width: 100,
|
|
95
|
+
height: 22,
|
|
96
|
+
justifyContent: 'center',
|
|
97
|
+
},
|
|
98
|
+
titleLayer: {
|
|
99
|
+
position: 'absolute',
|
|
100
|
+
left: 0,
|
|
101
|
+
right: 0,
|
|
102
|
+
},
|
|
103
|
+
searchTitle: {
|
|
104
|
+
fontSize: t.fontSizeLG,
|
|
105
|
+
width: 100,
|
|
106
|
+
fontWeight: '500',
|
|
107
|
+
color: t.colorText,
|
|
108
|
+
},
|
|
109
|
+
/** overflow 静态声明,动画只改 height —— 这是折叠效果的实现基础 */
|
|
110
|
+
body: {
|
|
111
|
+
overflow: 'hidden',
|
|
112
|
+
},
|
|
113
|
+
searchRow: {
|
|
114
|
+
flexDirection: 'row',
|
|
115
|
+
alignItems: 'center',
|
|
116
|
+
marginBottom: 12,
|
|
117
|
+
},
|
|
118
|
+
searchLabel: {
|
|
119
|
+
width: 80,
|
|
120
|
+
fontSize: t.fontSize,
|
|
121
|
+
color: t.colorTextSecondary,
|
|
122
|
+
},
|
|
123
|
+
searchInput: {
|
|
124
|
+
flex: 1,
|
|
125
|
+
height: 40,
|
|
126
|
+
borderWidth: 1,
|
|
127
|
+
borderColor: t.colorBorder,
|
|
128
|
+
borderRadius: t.borderRadiusSM,
|
|
129
|
+
paddingHorizontal: 12,
|
|
130
|
+
paddingVertical: 2,
|
|
131
|
+
fontSize: t.fontSize,
|
|
132
|
+
},
|
|
133
|
+
dateText: {
|
|
134
|
+
fontSize: t.fontSize,
|
|
135
|
+
lineHeight: 34,
|
|
136
|
+
color: t.colorText,
|
|
137
|
+
},
|
|
138
|
+
placeholderText: {
|
|
139
|
+
lineHeight: 34,
|
|
140
|
+
fontSize: t.fontSize,
|
|
141
|
+
color: t.colorTextTertiary,
|
|
142
|
+
},
|
|
143
|
+
searchButtons: {
|
|
144
|
+
flexDirection: 'row',
|
|
145
|
+
justifyContent: 'flex-end',
|
|
146
|
+
marginVertical: 16,
|
|
147
|
+
},
|
|
148
|
+
searchButton: {
|
|
149
|
+
paddingHorizontal: 24,
|
|
150
|
+
height: t.controlHeightSM,
|
|
151
|
+
borderRadius: t.borderRadiusSM,
|
|
152
|
+
justifyContent: 'center',
|
|
153
|
+
alignItems: 'center',
|
|
154
|
+
marginLeft: 12,
|
|
155
|
+
},
|
|
156
|
+
resetButton: {
|
|
157
|
+
borderWidth: 1,
|
|
158
|
+
borderColor: t.colorPrimary,
|
|
159
|
+
backgroundColor: t.colorBgContainer,
|
|
160
|
+
},
|
|
161
|
+
resetButtonText: {
|
|
162
|
+
color: t.colorPrimary,
|
|
163
|
+
fontSize: t.fontSize,
|
|
164
|
+
fontWeight: '500',
|
|
165
|
+
},
|
|
166
|
+
submitButton: {
|
|
167
|
+
backgroundColor: t.colorPrimary,
|
|
168
|
+
},
|
|
169
|
+
submitButtonText: {
|
|
170
|
+
color: t.colorTextLightSolid,
|
|
171
|
+
fontSize: t.fontSize,
|
|
172
|
+
fontWeight: '500',
|
|
173
|
+
},
|
|
174
|
+
}), [t]);
|
|
175
|
+
/**
|
|
176
|
+
* progress:唯一的动画时间轴,0 = 完全收起,1 = 完全展开。
|
|
177
|
+
* SharedValue 是一块 JS 与 UI 线程共享的内存,写入它不会触发 React 渲染,
|
|
178
|
+
* 所以动画过程对 React 完全「透明」。
|
|
179
|
+
*/
|
|
180
|
+
const progress = useSharedValue(defaultExpanded ? 1 : 0);
|
|
181
|
+
/**
|
|
182
|
+
* contentH:onLayout 量到的真实内容高度,同样放在 SharedValue 里。
|
|
183
|
+
* 关键点——不要用 useState 存高度:
|
|
184
|
+
* 那会让每次测量都触发一次 React 重渲染,而且 useAnimatedStyle 里
|
|
185
|
+
* 读 state 需要把它作为依赖重建 worklet,得不偿失。
|
|
186
|
+
*/
|
|
187
|
+
const contentH = useSharedValue(0);
|
|
188
|
+
/**
|
|
189
|
+
* 展开状态用 ref 而非 state:
|
|
190
|
+
* 组件的视觉表现完全由 progress 决定,React 不需要为「展开与否」重渲染。
|
|
191
|
+
* ref 只是给 toggle 逻辑做方向判断用的普通变量。
|
|
192
|
+
*/
|
|
193
|
+
const expandedRef = useRef(defaultExpanded);
|
|
194
|
+
/** 上一次量到的高度,用于去重,避免 onLayout 抖动导致反复触发补间 */
|
|
195
|
+
const measuredRef = useRef(0);
|
|
196
|
+
const toggle = useCallback(() => {
|
|
197
|
+
// 先算目标值,再翻转 ref,顺序反了会拿到错误的方向
|
|
198
|
+
const next = expandedRef.current ? 0 : 1;
|
|
199
|
+
expandedRef.current = !expandedRef.current;
|
|
200
|
+
/**
|
|
201
|
+
* 这一行是整个组件的核心:
|
|
202
|
+
* 只要给 progress 赋一个 withSpring/withTiming 包装的值,
|
|
203
|
+
* Reanimated 就会在 UI 线程按帧推进它,所有依赖它的
|
|
204
|
+
* useAnimatedStyle 自动重算。
|
|
205
|
+
*
|
|
206
|
+
* 打断行为天然正确:连点时新动画会从**当前中间值和当前速度**继续,
|
|
207
|
+
* 而不是从 0/1 重新开始——这就是不会出现「跳一下再动」的原因。
|
|
208
|
+
*/
|
|
209
|
+
progress.value = motion === 'timing' ? withTiming(next, TIMING_CONFIG) : withSpring(next, SPRING_CONFIG);
|
|
210
|
+
onToggle?.(expandedRef.current);
|
|
211
|
+
}, [motion, onToggle, progress]);
|
|
212
|
+
const onContentLayout = useCallback((e) => {
|
|
213
|
+
// 取整:亚像素高度会让去重判断永远不相等,从而反复触发补间
|
|
214
|
+
const h = Math.round(e.nativeEvent.layout.height);
|
|
215
|
+
// h <= 0 是布局过程中的中间态(例如父容器高度为 0 时),直接丢弃
|
|
216
|
+
if (h <= 0 || h === measuredRef.current)
|
|
217
|
+
return;
|
|
218
|
+
const isFirstMeasure = measuredRef.current === 0;
|
|
219
|
+
measuredRef.current = h;
|
|
220
|
+
/**
|
|
221
|
+
* 两种赋值策略:
|
|
222
|
+
* - 首次测量 或 当前是收起态:用户看不见这层内容,直接硬赋值,
|
|
223
|
+
* 省掉一次无意义的动画(也避免首帧出现一次「自己长出来」的动作);
|
|
224
|
+
* - 已展开状态下高度变了:走 RESIZE_CONFIG 补间,避免跳变。
|
|
225
|
+
*/
|
|
226
|
+
if (isFirstMeasure || !expandedRef.current) {
|
|
227
|
+
contentH.value = h;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
contentH.value = withTiming(h, RESIZE_CONFIG);
|
|
231
|
+
}
|
|
232
|
+
}, [contentH]);
|
|
233
|
+
/**
|
|
234
|
+
* ── 派生样式 1:容器高度 ──────────────────────────────────────────
|
|
235
|
+
* useAnimatedStyle 的回调是一个 worklet:它被编译后直接在 UI 线程执行,
|
|
236
|
+
* 每帧读取 SharedValue 并返回样式对象,然后由原生侧直接改视图属性,
|
|
237
|
+
* 完全不经过 JS 线程和 React 的 diff。
|
|
238
|
+
*
|
|
239
|
+
* Math.max(0, ...):spring 欠阻尼在收起方向会短暂冲到负值,
|
|
240
|
+
* 负高度在 Android 上会报警告/表现异常,这里 clamp 住,
|
|
241
|
+
* 保证收起只有减速、不会「反向溢出」。
|
|
242
|
+
*
|
|
243
|
+
* contentH.value || contentHeight:真实高度还没量到时退回兜底值。
|
|
244
|
+
*/
|
|
245
|
+
const bodyStyle = useAnimatedStyle(() => {
|
|
246
|
+
const target = contentH.value || contentHeight;
|
|
247
|
+
return { height: Math.max(0, progress.value * target) };
|
|
248
|
+
});
|
|
249
|
+
/**
|
|
250
|
+
* ── 派生样式 2:内容淡入 + 轻微下滑 ───────────────────────────────
|
|
251
|
+
* 只做高度变化会像「拉幕布」,因为内容是被裁切出来的,缺少自身的运动。
|
|
252
|
+
* 叠一层 opacity + translateY 之后,内容像是「跟着容器一起落下来」,
|
|
253
|
+
* 层次感来自这两条曲线的时间差(错峰,staggering):
|
|
254
|
+
*
|
|
255
|
+
* opacity 在 progress 0.12~0.7 之间完成 → 容器先开一点点,内容才开始显现
|
|
256
|
+
* translateY 在 0~1 全程从 -14 走到 0 → 位移贯穿整段,收尾最平缓
|
|
257
|
+
*
|
|
258
|
+
* Extrapolation.CLAMP:超出输入区间时保持端点值,
|
|
259
|
+
* 否则 spring 过冲到 1.03 时 opacity 会被线性外推成 >1(部分平台异常)。
|
|
260
|
+
*/
|
|
261
|
+
const contentStyle = useAnimatedStyle(() => ({
|
|
262
|
+
opacity: interpolate(progress.value, [0.12, 0.7], [0, 1], Extrapolation.CLAMP),
|
|
263
|
+
transform: [{ translateY: interpolate(progress.value, [0, 1], [-14, 0], Extrapolation.CLAMP) }],
|
|
264
|
+
}));
|
|
265
|
+
/**
|
|
266
|
+
* ── 派生样式 3:箭头旋转 ──────────────────────────────────────────
|
|
267
|
+
* 直接线性映射到 0~180deg。它和高度共享同一 progress,
|
|
268
|
+
* 所以弹簧的回弹也会同步体现在箭头上——这种「一起动、一起停」
|
|
269
|
+
* 的一致性是廉价但极有效的高级感来源。
|
|
270
|
+
*/
|
|
271
|
+
const iconStyle = useAnimatedStyle(() => ({
|
|
272
|
+
transform: [{ rotate: `${progress.value * 180}deg` }],
|
|
273
|
+
}));
|
|
274
|
+
/**
|
|
275
|
+
* ── 派生样式 4/5:标题文案交叉淡入(cross-fade)────────────────────
|
|
276
|
+
* 「展开搜索 / 收起搜索」两段文字**同时存在**,用绝对定位叠在一起,
|
|
277
|
+
* 靠透明度和位移交替出没,而不是 progress > 0.5 ? A : B 的条件渲染。
|
|
278
|
+
*
|
|
279
|
+
* 为什么不用条件渲染:那会在中途产生一次 React 重渲染 + 文字节点增删,
|
|
280
|
+
* 视觉上是硬切换,且会让这一帧比其他帧更耗时(掉帧就发生在这一帧)。
|
|
281
|
+
*
|
|
282
|
+
* 两段区间刻意不重叠(0~0.45 与 0.55~1),中间留出 0.1 的空档,
|
|
283
|
+
* 避免两行字同时半透明地重影在一起。
|
|
284
|
+
*/
|
|
285
|
+
const expandTextStyle = useAnimatedStyle(() => ({
|
|
286
|
+
opacity: interpolate(progress.value, [0, 0.45], [1, 0], Extrapolation.CLAMP),
|
|
287
|
+
transform: [{ translateY: interpolate(progress.value, [0, 0.45], [0, -8], Extrapolation.CLAMP) }],
|
|
288
|
+
}));
|
|
289
|
+
const collapseTextStyle = useAnimatedStyle(() => ({
|
|
290
|
+
opacity: interpolate(progress.value, [0.55, 1], [0, 1], Extrapolation.CLAMP),
|
|
291
|
+
transform: [{ translateY: interpolate(progress.value, [0.55, 1], [8, 0], Extrapolation.CLAMP) }],
|
|
292
|
+
}));
|
|
293
|
+
return (_jsxs(View, { style: styles.card, children: [_jsxs(TouchableOpacity, { style: styles.searchHeader, onPress: toggle, activeOpacity: 0.7, children: [_jsxs(View, { style: styles.titleWrap, children: [_jsx(Animated.View, { style: [styles.titleLayer, expandTextStyle], children: _jsx(Text, { style: styles.searchTitle, children: i18n('expandSearch') }) }), _jsx(Animated.View, { style: [styles.titleLayer, collapseTextStyle], children: _jsx(Text, { style: styles.searchTitle, children: i18n('collapseSearch') }) })] }), _jsx(Animated.View, { style: iconStyle, children: _jsx(AntDesign, { name: 'down', size: 20, color: t.colorText }) })] }), _jsx(Animated.View, { style: [styles.body, bodyStyle], collapsable: false, children: _jsx(Animated.View, { style: contentStyle, children: _jsxs(View, { onLayout: onContentLayout, children: [children, _jsxs(View, { style: styles.searchButtons, children: [_jsx(TouchableOpacity, { style: [styles.searchButton, styles.resetButton], onPress: onReset, activeOpacity: 0.7, children: _jsx(Text, { style: styles.resetButtonText, children: i18n('reset') }) }), _jsx(TouchableOpacity, { style: [styles.searchButton, styles.submitButton], onPress: onSearch, activeOpacity: 0.7, children: _jsx(Text, { style: styles.submitButtonText, children: i18n('search') }) })] })] }) }) })] }));
|
|
294
|
+
};
|
|
295
|
+
export default XAnimatedSearchPanel;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XAnimatedView —— 通用动画视图(duxapp Animated.View 的 RN 移植版)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 【它是干什么的?】
|
|
7
|
+
* XPullView 内部直接用了 useSharedValue + withTiming 写动画;而这里提供的
|
|
8
|
+
* XAnimatedView + anim() 是更通用的"动画引擎",可以给任意自定义组件用,
|
|
9
|
+
* 用法和 duxapp 的 Animated.create() 完全一致(链式声明多段动画)。
|
|
10
|
+
*
|
|
11
|
+
* 【动画声明格式】
|
|
12
|
+
* anim(200, 'ease-out') // 默认时长 200ms、缓动 ease-out
|
|
13
|
+
* .translateY(300) // 第 1 步:Y 移到 300(屏外)
|
|
14
|
+
* .opacity(0) // 同时透明
|
|
15
|
+
* .step() // 把上面这两个"动作"打包成一段
|
|
16
|
+
* .translateY(0) // 第 2 步:Y 移回 0
|
|
17
|
+
* .opacity(1) // 同时不透明
|
|
18
|
+
* .step() // 打包成第二段
|
|
19
|
+
* .export() // 输出 XAnimStep[] 交给 XAnimatedView
|
|
20
|
+
*
|
|
21
|
+
* 【多段动画如何播放?】
|
|
22
|
+
* 用一个 0→N 的 progress(withSequence 顺序播放 N 段 withTiming),
|
|
23
|
+
* 每段把 progress 推向 1、2、3……N。useAnimatedStyle 里用 interpolate
|
|
24
|
+
* 把 progress 映射成每个属性的具体值(inputRange: [0,1,2..N])。
|
|
25
|
+
*
|
|
26
|
+
* 【为什么动画在 UI 线程?】
|
|
27
|
+
* useAnimatedStyle 的函数会被 Reanimated 编译成 worklet,运行在 UI 线程。
|
|
28
|
+
* JS 线程只需要发一次"开始动画"的指令,之后每一帧的插值计算
|
|
29
|
+
* 都在 UI 线程完成 → 即使 JS 线程忙(网络请求、列表渲染),动画也不掉帧。
|
|
30
|
+
* ============================================================================
|
|
31
|
+
*/
|
|
32
|
+
import React from 'react';
|
|
33
|
+
import { StyleProp, ViewStyle } from 'react-native';
|
|
34
|
+
/** 支持的缓动函数(与 CSS / duxapp 命名一致) */
|
|
35
|
+
export type TimingFunction = 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
|
|
36
|
+
/** 一段动画:option 是时长/缓动配置,action 是这一段的属性目标值 */
|
|
37
|
+
export interface XAnimStep {
|
|
38
|
+
option?: {
|
|
39
|
+
duration?: number;
|
|
40
|
+
timingFunction?: TimingFunction;
|
|
41
|
+
delay?: number;
|
|
42
|
+
};
|
|
43
|
+
action: Record<string, number>;
|
|
44
|
+
}
|
|
45
|
+
/** 动画构建器的链式 API */
|
|
46
|
+
interface XAnimBuilder {
|
|
47
|
+
/** 把当前累积的"动作"打包成一段动画 */
|
|
48
|
+
step(option?: Partial<XAnimStep['option']>): XAnimBuilder;
|
|
49
|
+
/** 通用:直接设置某个属性目标值 */
|
|
50
|
+
set(name: string, value: number): XAnimBuilder;
|
|
51
|
+
translateY(v: number): XAnimBuilder;
|
|
52
|
+
translateX(v: number): XAnimBuilder;
|
|
53
|
+
scale(v: number): XAnimBuilder;
|
|
54
|
+
opacity(v: number): XAnimBuilder;
|
|
55
|
+
rotate(v: number): XAnimBuilder;
|
|
56
|
+
/** 导出所有动画段 */
|
|
57
|
+
export(): XAnimStep[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 动画构建器(链式调用)
|
|
61
|
+
*
|
|
62
|
+
* 实现思路:
|
|
63
|
+
* - `current` 暂存当前累积的动作({translateY: 300, opacity: 0});
|
|
64
|
+
* - `.step()` 把 current 打包成 {option, action} 推入 steps,然后清空 current;
|
|
65
|
+
* - `.export()` 把最后一段也打包并返回 steps 数组。
|
|
66
|
+
*/
|
|
67
|
+
export declare function anim(duration?: number, timingFunction?: TimingFunction, delay?: number): XAnimBuilder;
|
|
68
|
+
interface XAnimatedViewProps {
|
|
69
|
+
/** 动画段数组(anim().export() 的结果) */
|
|
70
|
+
animation?: XAnimStep[];
|
|
71
|
+
style?: StyleProp<ViewStyle>;
|
|
72
|
+
children?: React.ReactNode;
|
|
73
|
+
/** 透传其余 View 属性(onPress 等) */
|
|
74
|
+
[key: string]: any;
|
|
75
|
+
}
|
|
76
|
+
export declare const XAnimatedView: ({ animation, style, children, ...props }: XAnimatedViewProps) => React.JSX.Element;
|
|
77
|
+
export {};
|