├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .husky
└── commit-msg
├── .prettierrc
├── LICENSE
├── README.md
├── README.zh.md
├── app.json
├── babel.config.js
├── commitlint.config.js
├── example
├── .detoxrc.json
├── .gitignore
├── App.tsx
├── app.json
├── assets
│ ├── a.jpg
│ ├── adaptive-icon.png
│ ├── b.jpg
│ ├── c.jpg
│ ├── d.jpg
│ ├── favicon.png
│ ├── icon.png
│ └── splash.png
├── babel.config.js
├── e2e
│ ├── config.json
│ ├── environment.js
│ └── main.e2e.js
├── eas.json
├── metro.config.js
├── package-lock.json
├── package.json
├── public
│ └── .nojekyll
├── src
│ ├── app.tsx
│ ├── components
│ │ ├── Card
│ │ │ └── index.tsx
│ │ ├── Icon
│ │ │ ├── arrow.tsx
│ │ │ └── plus.tsx
│ │ ├── Section
│ │ │ └── index.tsx
│ │ └── WaterMark
│ │ │ └── index.tsx
│ ├── navigate.ts
│ ├── pages
│ │ ├── ActionSheetExample.tsx
│ │ ├── AnimatedNumberExample.tsx
│ │ ├── AvatarExample.tsx
│ │ ├── BadgeExample.tsx
│ │ ├── ButtonExample.tsx
│ │ ├── CarouselExample.tsx
│ │ ├── CollapseExample.tsx
│ │ ├── DividerExample.tsx
│ │ ├── Home.tsx
│ │ ├── IconExample.tsx
│ │ ├── ImageViewerExample.tsx
│ │ ├── ListRowExample.tsx
│ │ ├── LoadingExample.tsx
│ │ ├── NestedTabViewExample.tsx
│ │ ├── OverlayExample.tsx
│ │ ├── PageViewExample.tsx
│ │ ├── PaginationExample.tsx
│ │ ├── PickerExample.tsx
│ │ ├── PopoverExample.tsx
│ │ ├── ProgressExample.tsx
│ │ ├── RefreshControlExample.tsx
│ │ ├── RefreshExample.tsx
│ │ ├── SegmentedExample.tsx
│ │ ├── ShadowExample.tsx
│ │ ├── SkeletonExample.tsx
│ │ ├── SwipeActionExample.tsx
│ │ ├── SwitchExample.tsx
│ │ ├── TabBarExample.tsx
│ │ ├── TabViewExample.tsx
│ │ ├── ThemeExample.tsx
│ │ ├── ToastExample.tsx
│ │ ├── WaterfallListExample.tsx
│ │ └── index.ts
│ └── thumbnail.tsx
└── tsconfig.json
├── jest.config.js
├── jest.setup.js
├── package-lock.json
├── package.json
├── publish.sh
├── screenshoot
├── ActionSheet.png
├── AnimatedNumber.gif
├── Avatar.png
├── Badge.png
├── Button.png
├── Collapse.png
├── Divider.png
├── Icon.png
├── ImageViewer.png
├── ListRow.png
├── Loading.gif
├── Overlay.png
├── Pagination.png
├── Picker.png
├── Progress.gif
├── RefreshList.png
├── Segmented.png
├── Shadow.png
├── Skeleton.gif
├── Swiper.png
├── Switch.png
├── Theme.png
├── Toast.png
├── WaterFallList.png
├── android_1.jpg
├── android_2.jpg
├── button.gif
├── carousel.gif
├── ios_1.jpeg
├── ios_2.jpeg
├── number.gif
├── overlay.gif
├── qrcode.png
├── refresh.gif
└── waterFall.gif
├── script
└── npm-publish.sh
├── src
├── components
│ ├── ActionSheet
│ │ ├── ActionSheet.tsx
│ │ ├── ActionSheetFull.tsx
│ │ ├── ActionSheetUtil.tsx
│ │ └── index.tsx
│ ├── AnimatedNumber
│ │ ├── AnimatedNumber.tsx
│ │ ├── ScrollNumber.tsx
│ │ └── index.tsx
│ ├── Avatar
│ │ ├── Avatar.tsx
│ │ ├── __test__
│ │ │ ├── __snapshots__
│ │ │ │ └── avatar.test.tsx.snap
│ │ │ └── avatar.test.tsx
│ │ └── index.tsx
│ ├── Badge
│ │ ├── Badge.tsx
│ │ ├── __test__
│ │ │ ├── __snapshots__
│ │ │ │ └── badge.test.tsx.snap
│ │ │ └── badge.test.tsx
│ │ └── index.tsx
│ ├── Button
│ │ ├── BaseButton.tsx
│ │ ├── Button.tsx
│ │ ├── GradientButton.tsx
│ │ ├── __test__
│ │ │ ├── __snapshots__
│ │ │ │ └── button.test.tsx.snap
│ │ │ └── button.test.tsx
│ │ ├── index.tsx
│ │ ├── type.ts
│ │ └── utils.ts
│ ├── Carousel
│ │ ├── BaseLayout.tsx
│ │ ├── Carousel.tsx
│ │ ├── ItemWrapper.tsx
│ │ ├── RotateLayout.tsx
│ │ ├── ScaleLayout.tsx
│ │ ├── __test__
│ │ │ ├── carousel.test.tsx
│ │ │ └── hook.test.ts
│ │ ├── index.tsx
│ │ ├── type.ts
│ │ └── utils.ts
│ ├── Collapse
│ │ ├── Collapse.tsx
│ │ ├── CollapseGroup.tsx
│ │ ├── index.tsx
│ │ └── type.tsx
│ ├── Divider
│ │ ├── Divider.tsx
│ │ ├── __test__
│ │ │ ├── __snapshots__
│ │ │ │ └── divider.test.tsx.snap
│ │ │ └── divider.test.tsx
│ │ └── index.tsx
│ ├── Error
│ │ └── index.tsx
│ ├── Icon
│ │ ├── Icon.tsx
│ │ ├── index.tsx
│ │ └── library.ts
│ ├── ImageViewer
│ │ ├── Display.tsx
│ │ ├── ImageContainer.tsx
│ │ ├── ImageOverlay.tsx
│ │ ├── ImageViewer.tsx
│ │ └── index.tsx
│ ├── ListRow
│ │ ├── ListRow.tsx
│ │ ├── index.tsx
│ │ └── utils.tsx
│ ├── Loading
│ │ ├── CircleLoading.tsx
│ │ ├── GrowLoading.tsx
│ │ ├── Loading.tsx
│ │ ├── LoadingTitle.tsx
│ │ ├── LoadingUtil.tsx
│ │ ├── ScaleLoading.tsx
│ │ ├── Spinner.tsx
│ │ └── index.tsx
│ ├── NestedTabView
│ │ ├── NestedRefresh.tsx
│ │ ├── NestedScene.tsx
│ │ ├── NestedTabView.tsx
│ │ ├── RefreshController.tsx
│ │ ├── hooks.ts
│ │ ├── index.tsx
│ │ ├── type.ts
│ │ └── util.ts
│ ├── Overlay
│ │ ├── Container
│ │ │ ├── DrawerContainer.tsx
│ │ │ ├── NormalContainer.tsx
│ │ │ ├── OpacityContainer.tsx
│ │ │ ├── ScaleContainer.tsx
│ │ │ ├── TranslateContainer.tsx
│ │ │ └── type.ts
│ │ ├── Overlay.tsx
│ │ ├── OverlayUtil.ts
│ │ ├── RootViewAnimations.ts
│ │ └── index.tsx
│ ├── PageView
│ │ ├── PageView.tsx
│ │ ├── SinglePage.tsx
│ │ ├── hook.ts
│ │ ├── index.tsx
│ │ └── type.ts
│ ├── Pagination
│ │ ├── Dot.tsx
│ │ ├── DotItem.tsx
│ │ ├── Pagination.tsx
│ │ ├── Percent.tsx
│ │ └── index.tsx
│ ├── Picker
│ │ ├── Picker.tsx
│ │ ├── PickerItem.tsx
│ │ ├── __test__
│ │ │ └── hook.test.ts
│ │ ├── index.tsx
│ │ ├── type.ts
│ │ └── utils.ts
│ ├── Popover
│ │ ├── Popover.tsx
│ │ ├── PopoverContainer.tsx
│ │ ├── index.tsx
│ │ ├── type.ts
│ │ └── utils.ts
│ ├── Progress
│ │ ├── CircleProgress.tsx
│ │ ├── Progress.tsx
│ │ └── index.tsx
│ ├── RefreshControl
│ │ ├── BottomContainer.tsx
│ │ ├── NormalControl.tsx
│ │ ├── RefreshContainer.tsx
│ │ ├── RefreshScrollView.tsx
│ │ ├── index.tsx
│ │ └── type.ts
│ ├── RefreshList
│ │ ├── RefreshList.tsx
│ │ └── index.tsx
│ ├── Segmented
│ │ ├── Segmented.tsx
│ │ └── index.tsx
│ ├── Shadow
│ │ ├── Shadow.tsx
│ │ └── index.tsx
│ ├── Skeleton
│ │ ├── Animation
│ │ │ ├── Breath.tsx
│ │ │ ├── Load.tsx
│ │ │ ├── Normal.tsx
│ │ │ ├── Shine.tsx
│ │ │ ├── ShineOver.tsx
│ │ │ └── index.tsx
│ │ ├── SkeletonContainer.tsx
│ │ ├── SkeletonRect.tsx
│ │ ├── index.tsx
│ │ └── type.ts
│ ├── Switch
│ │ ├── Switch.tsx
│ │ ├── __test__
│ │ │ └── switch.test.tsx
│ │ └── index.tsx
│ ├── TabBar
│ │ ├── Separator.tsx
│ │ ├── TabBar.tsx
│ │ ├── TabBarItem.tsx
│ │ ├── TabBarSlider.tsx
│ │ ├── __test__
│ │ │ └── hook.test.ts
│ │ ├── hook.ts
│ │ ├── index.tsx
│ │ └── type.ts
│ ├── TabView
│ │ ├── TabView.tsx
│ │ ├── hook.ts
│ │ ├── index.tsx
│ │ └── type.ts
│ ├── Theme
│ │ ├── Theme.tsx
│ │ ├── dark.ts
│ │ ├── default.ts
│ │ ├── index.tsx
│ │ └── type.ts
│ ├── Toast
│ │ ├── Toast.tsx
│ │ ├── ToastUtil.tsx
│ │ └── index.tsx
│ └── WaterfallList
│ │ ├── AsyncImage.tsx
│ │ ├── WaterfallList.tsx
│ │ ├── index.tsx
│ │ └── utils.ts
├── index.ts
└── utils
│ ├── Freeze.tsx
│ ├── hooks.ts
│ ├── redash.ts
│ └── typeUtil.ts
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/*.test.ts
2 | **/*.test.tsx
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["expo", "prettier"],
3 | "rules": {
4 | "prettier/prettier": [
5 | "error",
6 | {
7 | "quoteProps": "preserve",
8 | "singleQuote": true,
9 | "tabWidth": 2,
10 | "trailingComma": "es5",
11 | "useTabs": false
12 | }
13 | ],
14 | "no-bitwise": 0,
15 | "prefer-const": "warn",
16 | "no-console": 0,
17 | "react-hooks/exhaustive-deps": 0,
18 | "no-array-constructor": 0,
19 | "@typescript-eslint/no-shadow": 0,
20 | "jest/no-identical-title": 0,
21 | "@typescript-eslint/no-unused-vars": "warn",
22 | "react-native/no-inline-styles": 0,
23 | "react/no-unstable-nested-components": 0
24 | },
25 | "plugins": ["prettier"]
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | strategy:
15 | matrix:
16 | node-version: [14.x]
17 | environment: expo
18 | steps:
19 | - uses: actions/checkout@v3
20 |
21 | - name: Use Node.js ${{ matrix.node-version }}
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 |
26 | - name: Install dependencies
27 | run: npm install
28 | - run: npm run test
29 |
30 | - name: publish example
31 | run: cd ./example && npm install
32 |
33 | - name: Expo Init
34 | uses: expo/expo-github-action@7.2.0
35 | with:
36 | expo-version: 5.4.3
37 | token: ${{ secrets.TOKEN }}
38 | packager: npm
39 |
40 | - name: Expo Publish app
41 | run: cd ./example && expo publish
42 |
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | .expo/
4 | dist/
5 | npm-debug.*
6 | *.jks
7 | *.p8
8 | *.p12
9 | *.key
10 | *.mobileprovision
11 | *.orig.*
12 | web-build/
13 |
14 | package-lock.json
15 | # macOS
16 | .DS_Store
17 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 |
2 | npx --no-install commitlint --edit "$1"
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "jsxBracketSameLine": false,
4 | "useTabs": false,
5 | "eslintIntegration": false,
6 | "tslintIntegration": true,
7 | "parser": "typescript",
8 | "requireConfig": false,
9 | "stylelintIntegration": false
10 | }
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 mahao
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.
22 |
--------------------------------------------------------------------------------
/README.zh.md:
--------------------------------------------------------------------------------
1 | # react-native-maui
2 |
3 | 此项目包含了ReactNative中常用的基础组件。所有的组件均由funciton components和hooks的实现,采用typescript编码。
4 | 动画及交互效果由`react-native-reanimated`、`react-native-gesture-handler` 和 `react-native-svg`实现。
5 |
6 | > 此项目由个人开发,不能保证组件的稳定性及适配度,请谨慎使用
7 |
8 | ## Update
9 | * 迁移`Overlay`到[react-native-ma-modal](https://github.com/mahaaoo/react-native-ma-modal)
10 |
11 |
12 | ## Preview
13 |
14 | | Loading | Progress | Button |
15 | | ------------- | ------------- | ------------- |
16 | |
|
|
|
17 |
18 |
19 | | AnimatedNumber | Overlay | RefreshScrollView |
20 | | ------------- | ------------- | ------------- |
21 | |
|
|
|
22 |
23 |
24 | | Carousel | WaterfallList |
25 | | ------------- | ------------- |
26 | |
|
|
27 |
28 |
29 | ## Installation
30 |
31 | 在安装之前react-native-maui之前,首先要确保项目中已经安装好了`react-native-reanimated`、`react-native-gesture-handler` and `react-native-svg`,可以使用如下命令进行安装:
32 |
33 | ```
34 | npm install react-native-reanimated react-native-gesture-handler react-native-svg
35 | npx pod-install
36 | ```
37 |
38 | 如果遇到问题,可以查看对应官网
39 | - [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated)
40 | - [react-native-gesture-handler](https://github.com/software-mansion/react-native-gesture-handler)
41 | - [react-native-svg](https://github.com/react-native-svg/react-native-svg)
42 |
43 | 通过以下命令安装`react-native-maui`
44 | ```
45 | npm install react-native-maui
46 | ```
47 |
48 | 使用:
49 | ```
50 | import { Button } from 'react-native-maui'
51 | ```
52 |
53 | ## Expo Demo
54 | [Expo HomePage](https://expo.dev/@mah22/react-native-maui-example?serviceType=classic&distribution=expo-go)
55 |
56 | 也可以使用Expo Go客户端扫描下方二维码,进行体验
57 |
58 |
59 |
60 | ## License
61 |
62 | Under The MIT License.
63 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {}
3 | }
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: [
5 | 'module:metro-react-native-babel-preset',
6 | [
7 | '@babel/preset-env',
8 | {
9 | targets: {node: 'current'},
10 | loose: true,
11 | }
12 | ],
13 | '@babel/preset-typescript',
14 | ],
15 | plugins: [
16 | 'react-native-reanimated/plugin'
17 | ],
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {extends: ['@commitlint/config-conventional']}
2 |
--------------------------------------------------------------------------------
/example/.detoxrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "testRunner": "jest",
3 | "runnerConfig": "e2e/config.json",
4 | "skipLegacyWorkersInjection": true,
5 | "apps": {
6 | "ios": {
7 | "type": "ios.app",
8 | "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/reactnativemauiexample.app",
9 | "build": "xcodebuild -workspace ios/reactnativemauiexample.xcworkspace -scheme reactnativemauiexample -sdk iphonesimulator -derivedDataPath ios/build"
10 | },
11 | "android": {
12 | "type": "android.apk",
13 | "binaryPath": "SPECIFY_PATH_TO_YOUR_APP_BINARY"
14 | }
15 | },
16 | "devices": {
17 | "simulator": {
18 | "type": "ios.simulator",
19 | "device": {
20 | "type": "iPhone 13"
21 | }
22 | },
23 | "emulator": {
24 | "type": "android.emulator",
25 | "device": {
26 | "avdName": "Pixel_3a_API_30_x86"
27 | }
28 | }
29 | },
30 | "configurations": {
31 | "ios": {
32 | "device": "simulator",
33 | "app": "ios"
34 | },
35 | "android": {
36 | "device": "emulator",
37 | "app": "android"
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
--------------------------------------------------------------------------------
/example/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import { NavigationContainer } from '@react-navigation/native';
4 | import { GestureHandlerRootView } from 'react-native-gesture-handler';
5 |
6 | import { Overlay, overlayRef, Theme } from 'react-native-maui';
7 |
8 | import { navigationRef } from './src/navigate';
9 | import Index from './src/app';
10 |
11 | export default function App() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | const styles = StyleSheet.create({
26 | container: {
27 | flex: 1,
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "example",
4 | "slug": "example",
5 | "version": "1.3.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "newArchEnabled": true,
10 | "splash": {
11 | "image": "./assets/splash.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "assetBundlePatterns": [
16 | "**/*"
17 | ],
18 | "ios": {
19 | "supportsTablet": true
20 | },
21 | "android": {
22 | "adaptiveIcon": {
23 | "foregroundImage": "./assets/adaptive-icon.png",
24 | "backgroundColor": "#ffffff"
25 | }
26 | },
27 | "web": {
28 | "favicon": "./assets/favicon.png"
29 | },
30 | "extra": {
31 | "eas": {
32 | "projectId": "bc991390-e718-43ce-8c10-a24fc7ad7244"
33 | }
34 | },
35 | "runtimeVersion": {
36 | "policy": "appVersion"
37 | },
38 | "updates": {
39 | "url": "https://u.expo.dev/bc991390-e718-43ce-8c10-a24fc7ad7244"
40 | },
41 | "experiments": {
42 | "baseUrl": "/react-native-maui/"
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/example/assets/a.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/example/assets/a.jpg
--------------------------------------------------------------------------------
/example/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/example/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/example/assets/b.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/example/assets/b.jpg
--------------------------------------------------------------------------------
/example/assets/c.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/example/assets/c.jpg
--------------------------------------------------------------------------------
/example/assets/d.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/example/assets/d.jpg
--------------------------------------------------------------------------------
/example/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/example/assets/favicon.png
--------------------------------------------------------------------------------
/example/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/example/assets/icon.png
--------------------------------------------------------------------------------
/example/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/example/assets/splash.png
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | plugins: [
6 | 'react-native-reanimated/plugin',
7 | ],
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/example/e2e/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "maxWorkers": 1,
3 | "testEnvironment": "./environment",
4 | "testRunner": "jest-circus/runner",
5 | "testTimeout": 120000,
6 | "testRegex": "\\.e2e\\.js$",
7 | "reporters": ["detox/runners/jest/streamlineReporter"],
8 | "verbose": true
9 | }
10 |
--------------------------------------------------------------------------------
/example/e2e/environment.js:
--------------------------------------------------------------------------------
1 | const {
2 | DetoxCircusEnvironment,
3 | SpecReporter,
4 | WorkerAssignReporter,
5 | } = require('detox/runners/jest-circus');
6 |
7 | class CustomDetoxEnvironment extends DetoxCircusEnvironment {
8 | constructor(config, context) {
9 | super(config, context);
10 |
11 | // Can be safely removed, if you are content with the default value (=300000ms)
12 | this.initTimeout = 300000;
13 |
14 | // This takes care of generating status logs on a per-spec basis. By default, Jest only reports at file-level.
15 | // This is strictly optional.
16 | this.registerListeners({
17 | SpecReporter,
18 | WorkerAssignReporter,
19 | });
20 | }
21 | }
22 |
23 | module.exports = CustomDetoxEnvironment;
24 |
--------------------------------------------------------------------------------
/example/e2e/main.e2e.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 |
3 | describe('Example', () => {
4 | beforeAll(async () => {
5 | await device.launchApp();
6 | });
7 |
8 | // beforeEach(async () => {
9 | // await device.reloadReactNative();
10 | // });
11 |
12 | it('Icon E2E-TEST', async () => {
13 | await element(by.id('Navigate-Button-Icon')).tap();
14 | await expect(element(by.id('MAUI-ICON-ID')).atIndex(0)).toBeVisible();
15 | await element(by.text('Back')).tap();
16 | });
17 |
18 | it('Button E2E-TEST', async () => {
19 | await element(by.id('Navigate-Button-Button')).tap();
20 |
21 | await expect(
22 | element(by.id('MAUI-BASE-BUTTON-ID')).atIndex(0)
23 | ).toBeVisible();
24 | await element(by.id('MAUI-BASE-BUTTON-ID')).atIndex(0).tap();
25 |
26 | await expect(element(by.id('MAUI-GRADIENT-BUTTON-ID'))).toBeVisible();
27 | await element(by.id('MAUI-GRADIENT-BUTTON-ID')).atIndex(0).tap();
28 |
29 | await element(by.text('Back')).tap();
30 | });
31 |
32 | it('Badge E2E-TEST', async () => {
33 | await element(by.id('Navigate-Button-Badge')).tap();
34 | await expect(element(by.id('MAUI-BADGE-ID')).atIndex(0)).toBeVisible();
35 | await element(by.text('Back')).tap();
36 | });
37 |
38 | it('Divider E2E-TEST', async () => {
39 | await element(by.id('Navigate-Button-Divider')).tap();
40 | await expect(element(by.id('MAUI-DIVIDER-ID')).atIndex(0)).toBeVisible();
41 | await element(by.text('Back')).tap();
42 | });
43 |
44 | it('Swiper E2E-TEST', async () => {
45 | // EXAMPLE-FLAT-LIST
46 | await element(by.id('EXAMPLE-FLAT-LIST')).swipe(
47 | 'up',
48 | 'fast',
49 | NaN,
50 | NaN,
51 | 0.8
52 | ); // set starting point X
53 |
54 | await element(by.id('Navigate-Button-Swiper')).tap();
55 |
56 | await expect(element(by.id('MAUI-SWIPER-ID')).atIndex(0)).toBeVisible();
57 |
58 | await element(by.text('Next')).tap();
59 | await element(by.text('Next')).tap();
60 | await element(by.id('MAUI-SWIPER-ID'))
61 | .atIndex(0)
62 | .swipe('left', 'fast', NaN, 0.8); // set starting point X
63 | await element(by.id('MAUI-SWIPER-ID'))
64 | .atIndex(0)
65 | .swipe('left', 'fast', NaN, 0.8); // set starting point X
66 |
67 | await element(by.text('Pre')).tap();
68 | await element(by.text('Pre')).tap();
69 |
70 | await element(by.text('Back')).tap();
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/example/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 5.5.0"
4 | },
5 | "build": {
6 | "development": {
7 | "developmentClient": true,
8 | "distribution": "internal",
9 | "channel": "development"
10 | },
11 | "preview": {
12 | "distribution": "internal",
13 | "channel": "preview"
14 | },
15 | "production": {
16 | "channel": "production"
17 | }
18 | },
19 | "submit": {
20 | "production": {}
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const { getDefaultConfig } = require('expo/metro-config')
4 |
5 | const projectRoot = __dirname
6 | const workspaceRoot = path.resolve(projectRoot, '..')
7 |
8 | const config = getDefaultConfig(projectRoot)
9 |
10 | // #1 - Watch all files in the monorepo
11 | config.watchFolders = [workspaceRoot]
12 | // #3 - Force resolving nested modules to the folders below
13 | config.resolver.disableHierarchicalLookup = true
14 | // #2 - Try resolving with project modules first, then workspace modules
15 | config.resolver.nodeModulesPaths = [
16 | path.resolve(projectRoot, 'node_modules'),
17 | path.resolve(workspaceRoot, 'node_modules'),
18 | ]
19 |
20 | module.exports = config
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-maui-example",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start --reset-cache",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web",
10 | "eject": "expo eject",
11 | "test": "jest",
12 | "testc": "jest --coverage",
13 | "buildios": "npx expo run:ios",
14 | "e2e-build": "detox build -c ios",
15 | "e2e-test": "detox test -c ios",
16 | "deploy": "gh-pages -t -d dist",
17 | "predeploy": "expo export -p web"
18 | },
19 | "dependencies": {
20 | "@expo/metro-runtime": "~4.0.0",
21 | "@react-navigation/native": "^6.0.10",
22 | "@react-navigation/native-stack": "^6.6.2",
23 | "crypto": "^1.0.1",
24 | "expo": "^52.0.4",
25 | "expo-splash-screen": "~0.29.7",
26 | "expo-status-bar": "~2.0.0",
27 | "expo-updates": "~0.26.5",
28 | "jest-expo": "~52.0.0",
29 | "react": "18.3.1",
30 | "react-dom": "18.3.1",
31 | "react-native": "0.76.1",
32 | "react-native-gesture-handler": "~2.20.2",
33 | "react-native-reanimated": "~3.16.1",
34 | "react-native-redash": "^16.2.4",
35 | "react-native-safe-area-context": "4.12.0",
36 | "react-native-screens": "~4.0.0",
37 | "react-native-svg": "15.8.0",
38 | "react-native-svg-transformer": "^1.3.0",
39 | "react-native-web": "~0.19.13"
40 | },
41 | "devDependencies": {
42 | "@babel/core": "^7.20.0",
43 | "@babel/runtime": "^7.18.3",
44 | "@types/jest": "^29.5.12",
45 | "@types/react": "~18.3.12",
46 | "@types/react-native": "~0.70.6",
47 | "babel-plugin-module-resolver": "^4.1.0",
48 | "detox": "^19.11.0",
49 | "gh-pages": "^6.1.1",
50 | "jest": "^29.3.1",
51 | "typescript": "^5.3.3"
52 | },
53 | "private": true,
54 | "jest": {
55 | "preset": "jest-expo"
56 | },
57 | "setupFilesAfterEnv": [
58 | "@testing-library/jest-native/extend-expect"
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------
/example/public/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/example/public/.nojekyll
--------------------------------------------------------------------------------
/example/src/app.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createNativeStackNavigator } from '@react-navigation/native-stack';
3 | import { useTheme } from 'react-native-maui';
4 |
5 | import Home from './pages/Home';
6 | import ExampleList from './pages';
7 |
8 | export type RootStackParamList = {
9 | ComponentScreen: undefined;
10 | ButtonExample: undefined;
11 | BadgeExample: undefined;
12 | SegmentedExample: undefined;
13 | SwitchExample: undefined;
14 | ThemeExample: undefined;
15 | DividerExample: undefined;
16 | AvatarExample: undefined;
17 | CollapseExample: undefined;
18 | OverlayExample: undefined;
19 | CarouselExample: undefined;
20 | PickerExample: undefined;
21 | SkeletonExample: undefined;
22 | RefreshExample: undefined;
23 | LoadingExample: undefined;
24 | ToastExample: undefined;
25 | ListRowExample: undefined;
26 | ImageViewerExample: undefined;
27 | PaginationExample: undefined;
28 | ActionSheetExample: undefined;
29 | ProgressExample: undefined;
30 | AnimatedNumberExample: undefined;
31 | ShadowExample: undefined;
32 | IconExample: undefined;
33 | WaterFallListExample: undefined;
34 | PopoverExample: undefined;
35 | SwipeActionExample: undefined;
36 | RefreshControlExample: undefined;
37 | TabViewExample: undefined;
38 | NestedTabViewExample: undefined;
39 | PageViewExample: undefined;
40 | TabBarExample: undefined;
41 | };
42 |
43 | const Stack = createNativeStackNavigator();
44 |
45 | const App: React.FC<{}> = () => {
46 | const { theme } = useTheme();
47 |
48 | const headerOptions = React.useMemo(() => {
49 | return {
50 | headerTitleStyle: {
51 | color: theme.navbarTitleColor,
52 | },
53 | headerStyle: {
54 | backgroundColor: theme.navbarBgColor,
55 | },
56 | headerTintColor: theme.clickTextColor,
57 | headerBackTitle: 'Back',
58 | };
59 | }, [theme]);
60 |
61 | return (
62 |
63 |
68 | {Object.keys(ExampleList).map((item: any) => {
69 | return (
70 |
77 | );
78 | })}
79 |
80 | );
81 | };
82 |
83 | export default App;
84 |
--------------------------------------------------------------------------------
/example/src/components/Card/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | View,
4 | StyleSheet,
5 | Text,
6 | TouchableOpacity,
7 | ViewStyle,
8 | Dimensions,
9 | } from 'react-native';
10 | import { useTheme } from 'react-native-maui';
11 |
12 | const { width } = Dimensions.get('window');
13 |
14 | interface CardProps {
15 | content: React.ReactNode;
16 | onPress: () => void;
17 | title: string;
18 |
19 | style?: ViewStyle;
20 | }
21 |
22 | const Card: React.FC = (props) => {
23 | const { content, onPress, style, title } = props;
24 | const { theme } = useTheme();
25 | return (
26 |
33 |
39 |
40 |
41 | {title}
42 |
43 |
44 |
45 | {content}
46 |
47 |
48 |
53 | more
54 |
55 |
56 | );
57 | };
58 |
59 | const styles = StyleSheet.create({
60 | container: {
61 | width: (width - 50) / 2,
62 | height: 150,
63 | borderRadius: 8,
64 | },
65 | backgroundContainer: {
66 | flex: 1,
67 | marginHorizontal: 2,
68 | marginTop: 2,
69 | borderTopLeftRadius: 8,
70 | borderTopRightRadius: 8,
71 | },
72 | titleContainer: {
73 | height: 40,
74 | justifyContent: 'center',
75 | alignItems: 'center',
76 | },
77 | title: {
78 | fontSize: 18,
79 | fontWeight: 'bold',
80 | },
81 | content: {
82 | justifyContent: 'center',
83 | alignItems: 'center',
84 | flex: 1,
85 | padding: 5,
86 | overflow: 'hidden',
87 | },
88 | });
89 |
90 | export default Card;
91 |
--------------------------------------------------------------------------------
/example/src/components/Icon/arrow.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Svg, { Path } from 'react-native-svg';
3 |
4 | interface ArrowProps {}
5 |
6 | const Arrow: React.FC = (props) => {
7 | const {} = props;
8 |
9 | return (
10 |
18 | );
19 | };
20 |
21 | export default Arrow;
22 |
--------------------------------------------------------------------------------
/example/src/components/Icon/plus.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Svg, { Rect } from 'react-native-svg';
3 |
4 | interface PlusProps {}
5 |
6 | const Plus: React.FC = (props) => {
7 | const {} = props;
8 |
9 | return (
10 |
14 | );
15 | };
16 |
17 | export default Plus;
18 |
--------------------------------------------------------------------------------
/example/src/components/Section/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, StyleSheet, Text, ViewStyle } from 'react-native';
3 |
4 | interface SectionProps {
5 | title: string;
6 | style?: ViewStyle;
7 | children: React.ReactNode;
8 | }
9 |
10 | const Section: React.FC = (props) => {
11 | const { title, children, style } = props;
12 |
13 | return (
14 |
15 | {title}
16 | {children}
17 |
18 | );
19 | };
20 |
21 | const styles = StyleSheet.create({
22 | container: {},
23 | title: {
24 | padding: 15,
25 | fontSize: 16,
26 | },
27 | content: {
28 | padding: 15,
29 | backgroundColor: 'white',
30 | flexDirection: 'row',
31 | justifyContent: 'flex-start',
32 | alignItems: 'center',
33 | },
34 | });
35 |
36 | export default Section;
37 |
--------------------------------------------------------------------------------
/example/src/components/WaterMark/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dimensions, View, StyleSheet } from 'react-native';
3 | import Svg, { Defs, Pattern, Rect, Text } from 'react-native-svg';
4 |
5 | const { width, height } = Dimensions.get('window');
6 |
7 | interface WaterMarkExampleProps {}
8 |
9 | const WaterMarkExample: React.FC = (props) => {
10 | const {} = props;
11 |
12 | return (
13 |
14 |
48 |
49 | );
50 | };
51 |
52 | export default WaterMarkExample;
53 |
--------------------------------------------------------------------------------
/example/src/navigate.ts:
--------------------------------------------------------------------------------
1 | import { createNavigationContainerRef } from '@react-navigation/native';
2 | import { StackActions } from '@react-navigation/native';
3 |
4 | export const navigationRef = createNavigationContainerRef();
5 |
6 | export const navigate = (name: string, params?: object | undefined) => {
7 | console.log('navigate to', name);
8 | if (navigationRef.isReady()) {
9 | navigationRef.navigate(name, params);
10 | }
11 | };
12 |
13 | export function push(name: string, params?: object | undefined) {
14 | if (navigationRef.isReady()) {
15 | navigationRef.dispatch(StackActions.push(name, params));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/example/src/pages/ActionSheetExample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, StyleSheet, Text } from 'react-native';
3 | import {
4 | Button,
5 | ActionSheet,
6 | ActionSheetUtil,
7 | ActionSheetFull,
8 | } from 'react-native-maui';
9 |
10 | import Section from '../components/Section';
11 |
12 | interface ActionSheetExampleProps {}
13 |
14 | const options = ['option1', 'option2', 'option3', 'option4', 'option5'];
15 | const ActionSheetExample: React.FC = (props) => {
16 | const {} = props;
17 |
18 | return (
19 |
20 |
21 |
38 |
56 |
57 |
58 | );
59 | };
60 |
61 | const styles = StyleSheet.create({
62 | container: {
63 | flex: 1,
64 | },
65 | marginLeft: {
66 | marginLeft: 15,
67 | },
68 | });
69 |
70 | export default ActionSheetExample;
71 |
--------------------------------------------------------------------------------
/example/src/pages/AnimatedNumberExample.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { View, StyleSheet, Text } from 'react-native';
3 | import {
4 | AnimatedNumber,
5 | ScrollNumber,
6 | Button,
7 | ButtonType,
8 | } from 'react-native-maui';
9 | import Section from '../components/Section';
10 |
11 | interface AnimatedNumberExampleProps {}
12 |
13 | const AnimatedNumberExample: React.FC = (props) => {
14 | const {} = props;
15 | const [value, setValue] = useState(0);
16 |
17 | return (
18 |
19 |
27 |
35 |
38 |
47 |
48 | );
49 | };
50 |
51 | const styles = StyleSheet.create({
52 | container: {
53 | flex: 1,
54 | },
55 | section: {
56 | paddingVertical: 30,
57 | },
58 | animatedNumber1: {
59 | fontSize: 40,
60 | fontWeight: 'bold',
61 | marginHorizontal: 10,
62 | },
63 | animatedNumber2: {
64 | fontSize: 40,
65 | color: 'orange',
66 | fontWeight: 'bold',
67 | marginHorizontal: 10,
68 | },
69 | animatedNumber3: {
70 | fontSize: 40,
71 | fontWeight: 'bold',
72 | marginHorizontal: 10,
73 | color: '#f1441d',
74 | },
75 | });
76 |
77 | export default AnimatedNumberExample;
78 |
--------------------------------------------------------------------------------
/example/src/pages/AvatarExample.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { StyleSheet, View, Text, ActivityIndicator } from 'react-native';
3 | import { Avatar } from 'react-native-maui';
4 | import Section from '../components/Section';
5 |
6 | export default function AvatarExample() {
7 | return (
8 |
9 |
23 |
24 | 加载中}
30 | />
31 | }
37 | />
38 |
39 |
40 | );
41 | }
42 |
43 | const styles = StyleSheet.create({
44 | container: {
45 | flex: 1,
46 | },
47 | avatar1: {
48 | width: 60,
49 | height: 60,
50 | marginHorizontal: 10,
51 | },
52 | avatar2: {
53 | width: 60,
54 | height: 60,
55 | borderRadius: 30,
56 | marginHorizontal: 10,
57 | },
58 | avatar3: {
59 | width: 60,
60 | height: 60,
61 | borderRadius: 30,
62 | marginHorizontal: 10,
63 | },
64 | avatar4: {
65 | width: 60,
66 | height: 60,
67 | marginHorizontal: 10,
68 | },
69 | });
70 |
--------------------------------------------------------------------------------
/example/src/pages/BadgeExample.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { Badge } from 'react-native-maui';
4 | import Section from '../components/Section';
5 |
6 | export default function BadgeExample() {
7 | return (
8 |
9 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | const styles = StyleSheet.create({
26 | container: {
27 | flex: 1,
28 | },
29 | margin: {
30 | marginHorizontal: 5,
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/example/src/pages/CollapseExample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View, Text, ScrollView, Dimensions } from 'react-native';
3 | import { Collapse, CollapseGroup } from 'react-native-maui';
4 | import Section from '../components/Section';
5 |
6 | const { width } = Dimensions.get('window');
7 |
8 | export default function ButtonExample() {
9 | return (
10 |
11 |
12 |
13 |
14 | contentcontentcontentcontent
15 |
16 |
17 |
18 |
19 | contentcontentcontentcontent
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | contentcontentcontentcontent
28 |
29 |
30 |
31 |
32 | contentcontentcontentcontent
33 |
34 |
35 |
36 |
37 | contentcontentcontentcontent
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | const styles = StyleSheet.create({
47 | container: {
48 | flex: 1,
49 | },
50 | section1: {
51 | backgroundColor: '#F8F8F8',
52 | flexDirection: 'column',
53 | alignItems: 'center',
54 | },
55 | content: {
56 | width: width - 30,
57 | height: 100,
58 | backgroundColor: 'white',
59 | },
60 | section2: {
61 | backgroundColor: '#F8F8F8',
62 | },
63 | });
64 |
--------------------------------------------------------------------------------
/example/src/pages/DividerExample.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { StyleSheet, View, Dimensions } from 'react-native';
3 | import { Divider } from 'react-native-maui';
4 | import Section from '../components/Section';
5 |
6 | const Width = Dimensions.get('window').width;
7 |
8 | export default function DividerExample() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
25 |
26 |
27 |
28 |
29 |
30 |
37 |
38 |
39 |
40 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | const styles = StyleSheet.create({
55 | container: {
56 | flex: 1,
57 | },
58 | section1: {
59 | flexDirection: 'column',
60 | },
61 | horizontal: {
62 | height: 20,
63 | },
64 | section2: {
65 | flexDirection: 'row',
66 | },
67 | vertical: {
68 | width: 20,
69 | },
70 | container2: {
71 | height: 100,
72 | justifyContent: 'center',
73 | flexDirection: 'row',
74 | },
75 | });
76 |
--------------------------------------------------------------------------------
/example/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View, Text, FlatList } from 'react-native';
3 | import { navigate } from '../navigate';
4 |
5 | import Card from '../components/Card';
6 | import { exampleList } from '../thumbnail';
7 |
8 | import { useTheme } from 'react-native-maui';
9 | import WaterMark from '../components/WaterMark';
10 |
11 | export default function ComponentScreen() {
12 | const { theme } = useTheme();
13 |
14 | return (
15 |
18 |
19 | `example_${index}`}
25 | renderItem={({ item }) => {
26 | return (
27 |
35 | {item.title}
36 |
37 | )
38 | }
39 | onPress={() => {
40 | navigate(`${item.title}Example`);
41 | }}
42 | />
43 | );
44 | }}
45 | />
46 |
47 | );
48 | }
49 |
50 | const styles = StyleSheet.create({
51 | container: {
52 | flex: 1,
53 | alignItems: 'center',
54 | paddingBottom: 30,
55 | },
56 | flatList: {
57 | flex: 1,
58 | },
59 | card: {
60 | margin: 10,
61 | },
62 | });
63 |
--------------------------------------------------------------------------------
/example/src/pages/IconExample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dimensions, View, StyleSheet } from 'react-native';
3 | import { Icon } from 'react-native-maui';
4 | import Section from '../components/Section';
5 |
6 | const { width } = Dimensions.get('window');
7 | const Width = (width - 30) / 4;
8 |
9 | interface IconExampleProps {}
10 |
11 | const allIcons = [
12 | 'arrow-left',
13 | 'arrow-right',
14 | 'arrow-up',
15 | 'arrow-down',
16 | 'plus',
17 | 'minus',
18 | 'code',
19 | 'check',
20 | 'sorting',
21 | 'favorites',
22 | 'favorites-fill',
23 | 'search',
24 | 'arrow-line-up',
25 | 'arrow-line-down',
26 | ];
27 |
28 | const IconExample: React.FC = (props) => {
29 | const {} = props;
30 | const colors = [
31 | '#f8e0b0',
32 | '#d2d97a',
33 | '#6e8b74',
34 | '#1491a8',
35 | '#d2568c',
36 | '#692a1b',
37 | ];
38 |
39 | return (
40 |
41 |
42 |
43 | {allIcons.map((name: any, index) => {
44 | const color = colors[index % colors.length];
45 | return (
46 |
56 |
57 |
58 | );
59 | })}
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | const styles = StyleSheet.create({
67 | container: {
68 | flexDirection: 'row',
69 | flexWrap: 'wrap',
70 | },
71 | icon: {
72 | justifyContent: 'center',
73 | alignItems: 'center',
74 | },
75 | });
76 |
77 | export default IconExample;
78 |
--------------------------------------------------------------------------------
/example/src/pages/ImageViewerExample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, StyleSheet, Image } from 'react-native';
3 | import { ImageViewer } from 'react-native-maui';
4 |
5 | interface ImageViewerExampleProps {}
6 |
7 | const card = [
8 | {
9 | source: require('../../assets/a.jpg'),
10 | },
11 | {
12 | source: require('../../assets/b.jpg'),
13 | },
14 | {
15 | source: require('../../assets/c.jpg'),
16 | },
17 | {
18 | source: require('../../assets/d.jpg'),
19 | },
20 | ];
21 |
22 | const ImageViewerExample: React.FC = (props) => {
23 | const {} = props;
24 |
25 | return (
26 |
27 | {
30 | return (
31 |
36 | );
37 | }}
38 | />
39 |
40 | );
41 | };
42 |
43 | const styles = StyleSheet.create({
44 | container: {
45 | flex: 1,
46 | },
47 | image: {
48 | width: 150,
49 | height: 150,
50 | margin: 10,
51 | },
52 | });
53 |
54 | export default ImageViewerExample;
55 |
--------------------------------------------------------------------------------
/example/src/pages/ListRowExample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, StyleSheet, Text, Dimensions } from 'react-native';
3 | import { Button, ListRow, Switch, Icon } from 'react-native-maui';
4 | import Section from '../components/Section';
5 |
6 | const { width } = Dimensions.get('window');
7 | interface ListRowExampleProps {}
8 |
9 | const ListRowExample: React.FC = (props) => {
10 | const {} = props;
11 |
12 | return (
13 |
14 |
15 |
16 |
17 | ListRow}
19 | mid={Content}
20 | right={}
21 | />
22 |
23 |
24 | Test Front}
26 | mid={Test Mid}
27 | right={
28 |
31 | }
32 | />
33 | }
37 | />
38 |
46 | } />
47 |
48 |
49 | );
50 | };
51 |
52 | const styles = StyleSheet.create({
53 | container: {
54 | flex: 1,
55 | },
56 | section: {
57 | backgroundColor: '#F8F8F8',
58 | flexDirection: 'column',
59 | },
60 | marginLeft: {
61 | marginLeft: 10,
62 | },
63 | margin: {
64 | marginTop: 10,
65 | },
66 | });
67 |
68 | export default ListRowExample;
69 |
--------------------------------------------------------------------------------
/example/src/pages/LoadingExample.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { StyleSheet, View, ScrollView, Dimensions } from 'react-native';
3 | import {
4 | Loading,
5 | LoadingTitle,
6 | OpacityContainerRef,
7 | Spinner,
8 | CircleLoading,
9 | GrowLoading,
10 | ScaleLoading,
11 | } from 'react-native-maui';
12 | import Section from '../components/Section';
13 |
14 | const { width } = Dimensions.get('window');
15 | const Width = (width - 30) / 3;
16 |
17 | export default function LoadingExample() {
18 | const ref = React.createRef();
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | const styles = StyleSheet.create({
55 | container: {
56 | flex: 1,
57 | },
58 | content: {
59 | width: Width,
60 | height: Width,
61 | justifyContent: 'center',
62 | alignItems: 'center',
63 | },
64 | colum: {
65 | flexDirection: 'column',
66 | },
67 | row: {
68 | flexDirection: 'row',
69 | },
70 | loadingTitle: {
71 | color: 'white',
72 | },
73 | circleContainer: {
74 | flexDirection: 'row',
75 | justifyContent: 'flex-start',
76 | },
77 | containerStyle: {
78 | justifyContent: 'center',
79 | alignItems: 'center',
80 | },
81 | });
82 |
--------------------------------------------------------------------------------
/example/src/pages/OverlayExample.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { StyleSheet, View, Text } from 'react-native';
3 |
4 | export default function OverlayExample() {
5 | return (
6 |
7 | 已迁移至react-native-ma-modal单独维护
8 |
9 | );
10 | }
11 |
12 | const styles = StyleSheet.create({
13 | main: {
14 | flex: 1,
15 | justifyContent: 'center',
16 | alignItems: 'center',
17 | },
18 | text: {
19 | fontSize: 16,
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/example/src/pages/PopoverExample.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { View, StyleSheet, Text, Dimensions } from 'react-native';
3 | import { Popover, Button } from 'react-native-maui';
4 |
5 | const { width } = Dimensions.get('window');
6 |
7 | interface PopoverExampleProps {}
8 |
9 | const PopoverExample: React.FC = (props) => {
10 | const {} = props;
11 | const [modal, setModal] = useState(false);
12 | return (
13 |
14 |
22 | 复制
23 | 粘贴
24 | {
26 | setModal(false);
27 | }}
28 | style={styles.options}
29 | >
30 | 取消
31 |
32 |
33 | }
34 | onPressMask={() => {
35 | setModal(false);
36 | }}
37 | >
38 |
46 |
47 |
48 | );
49 | };
50 |
51 | const styles = StyleSheet.create({
52 | container: {
53 | flex: 1,
54 | alignItems: 'flex-start',
55 | },
56 | popover: {
57 | marginLeft: (width - 150) / 2,
58 | },
59 | button: {
60 | width: 150,
61 | height: 200,
62 | },
63 | content: {
64 | backgroundColor: 'white',
65 | justifyContent: 'center',
66 | alignItems: 'center',
67 | },
68 | popContainer: {
69 | flexDirection: 'row',
70 | backgroundColor: 'black',
71 | borderRadius: 5,
72 | paddingHorizontal: 5,
73 | },
74 | options: {
75 | fontSize: 16,
76 | color: 'white',
77 | padding: 10,
78 | },
79 | });
80 |
81 | export default PopoverExample;
82 |
--------------------------------------------------------------------------------
/example/src/pages/RefreshControlExample.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { View, StyleSheet, Dimensions, Text, FlatList } from 'react-native';
3 | import { RefreshScrollView, NormalControl } from 'react-native-maui';
4 | // import { useSafeAreaInsets } from 'react-native-safe-area-context';
5 | // import { useHeaderHeight } from '@react-navigation/elements';
6 |
7 | const { width, height } = Dimensions.get('window');
8 |
9 | interface RefreshControlExampleProps {}
10 |
11 | let random = 0;
12 |
13 | const RefreshControlExample: React.FC = (props) => {
14 | const {} = props;
15 | const [dataSource, setDataSource] = useState(new Array(3).fill(0));
16 | const [refresh, setFresh] = useState(false);
17 |
18 | // const insets = useSafeAreaInsets();
19 | // const headerHeight = useHeaderHeight();
20 |
21 | return (
22 | }
25 | loadComponent={() => }
26 | onRefresh={() => {
27 | setFresh(true);
28 | console.log('下拉刷新');
29 | setTimeout(() => {
30 | random = Math.floor(Math.random() * 100);
31 | setFresh(false);
32 | }, 2000);
33 | }}
34 | handleOnLoadMore={() => {
35 | setFresh(true);
36 | console.log('上拉加载');
37 | setTimeout(() => {
38 | const newLength = dataSource.length + 1;
39 | setDataSource(new Array(newLength).fill(0));
40 | setFresh(false);
41 | }, 2000);
42 | }}
43 | >
44 | {dataSource.map((item, index) => {
45 | const backgroundColor = (index & 1) === 0 ? 'pink' : 'orange';
46 | return (
47 |
48 | {index + random}
49 |
50 | );
51 | })}
52 |
53 | );
54 | };
55 |
56 | const styles = StyleSheet.create({
57 | container: {
58 | flex: 1,
59 | },
60 | item1: {
61 | width,
62 | height: 500,
63 | backgroundColor: 'pink',
64 | justifyContent: 'center',
65 | alignItems: 'center',
66 | },
67 | title: {
68 | fontSize: 30,
69 | },
70 | });
71 |
72 | export default RefreshControlExample;
73 |
--------------------------------------------------------------------------------
/example/src/pages/RefreshExample.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { View, StyleSheet, Text, Dimensions } from 'react-native';
3 | import { RefreshList, RefreshState } from 'react-native-maui';
4 |
5 | const { width } = Dimensions.get('window');
6 |
7 | interface RefreshProps {}
8 |
9 | const mockArray = (count: number): number[] => {
10 | const data = new Array(count).fill(0);
11 | const randomIndex = Math.floor(Math.random() * 100);
12 | for (let index = 0; index < data.length; index++) {
13 | data[index] = randomIndex + index;
14 | }
15 | return data;
16 | };
17 |
18 | const Refresh: React.FC = () => {
19 | const [data, setData] = useState([]);
20 | const [status, setStatus] = useState(RefreshState.Idle);
21 |
22 | useEffect(() => {
23 | const list = mockArray(20);
24 | setData(list);
25 | }, []);
26 |
27 | return (
28 |
29 | {
33 | setStatus(RefreshState.HeaderRefreshing);
34 | setTimeout(() => {
35 | const list = mockArray(20);
36 | setData(list);
37 | setStatus(RefreshState.Idle);
38 | }, 2000);
39 | }}
40 | onFooterRefresh={() => {
41 | setStatus(RefreshState.FooterRefreshing);
42 | setTimeout(() => {
43 | const list = mockArray(2);
44 | setData((data) => data.concat(list));
45 | setStatus(RefreshState.Idle);
46 | }, 2000);
47 | }}
48 | renderItem={({ item }) => {
49 | return (
50 |
51 | {item}
52 |
53 | );
54 | }}
55 | />
56 |
57 | );
58 | };
59 |
60 | const styles = StyleSheet.create({
61 | container: {
62 | flex: 1,
63 | },
64 | item: {
65 | width,
66 | height: 100,
67 | justifyContent: 'center',
68 | alignItems: 'center',
69 | borderWidth: 1,
70 | borderColor: 'red',
71 | },
72 | });
73 |
74 | export default Refresh;
75 |
--------------------------------------------------------------------------------
/example/src/pages/SegmentedExample.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { Segmented } from 'react-native-maui';
4 | import Section from '../components/Section';
5 |
6 | export default function SegmentedExample() {
7 | return (
8 |
9 |
12 |
13 | {
20 | console.log([item, index]);
21 | }}
22 | />
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | const styles = StyleSheet.create({
30 | container: {
31 | flex: 1,
32 | },
33 | section: {
34 | flexDirection: 'column',
35 | alignItems: 'flex-start',
36 | },
37 | segment1: {
38 | marginVertical: 10,
39 | height: 50,
40 | borderRadius: 12,
41 | backgroundColor: 'pink',
42 | },
43 | segment2: {
44 | marginVertical: 10,
45 | height: 50,
46 | width: 200,
47 | borderRadius: 25,
48 | },
49 | item: {
50 | fontWeight: 'bold',
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/example/src/pages/ShadowExample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, StyleSheet, Dimensions } from 'react-native';
3 | import { Shadow } from 'react-native-maui';
4 | import Section from '../components/Section';
5 |
6 | const { width } = Dimensions.get('window');
7 |
8 | const SIZE = (width - 120) / 3;
9 |
10 | interface ShadowExampleProps {}
11 |
12 | const ShadowExample: React.FC = () => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | const styles = StyleSheet.create({
39 | container: {
40 | flex: 1,
41 | },
42 | shadow1: {
43 | width: SIZE,
44 | height: SIZE,
45 | backgroundColor: 'white',
46 | },
47 | shadow2: {
48 | width: SIZE,
49 | height: SIZE,
50 | backgroundColor: 'white',
51 | borderRadius: 30,
52 | },
53 | shadow3: {
54 | width: SIZE,
55 | height: SIZE,
56 | backgroundColor: 'white',
57 | borderRadius: 50,
58 | },
59 | shadow4: {
60 | width: 100,
61 | height: 100,
62 | backgroundColor: '#f9d27d',
63 | borderRadius: 30,
64 | },
65 | shadow5: {
66 | width: 60,
67 | height: 60,
68 | backgroundColor: '#e9ccd3',
69 | borderRadius: 50,
70 | },
71 | });
72 |
73 | export default ShadowExample;
74 |
--------------------------------------------------------------------------------
/example/src/pages/SwitchExample.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { Switch } from 'react-native-maui';
4 | import Section from '../components/Section';
5 |
6 | export default function SwitchExample() {
7 | useEffect(() => {});
8 | return (
9 |
10 |
13 |
24 |
27 |
28 | );
29 | }
30 |
31 | const styles = StyleSheet.create({
32 | container: {
33 | flex: 1,
34 | },
35 | switch1: {
36 | width: 80,
37 | height: 50,
38 | marginHorizontal: 20,
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/example/src/pages/TabBarExample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 | import { TabBar } from 'react-native-maui';
4 |
5 | interface TabBarExampleProps {}
6 |
7 | const tabs = ['tab1', 'tab2', 'this is tab3', 'tab5', '11', 'tab8', 'ta11'];
8 | const tabs2 = ['tab1', 'tab2'];
9 | const tabs3 = ['tab1', 'tab2', 'this is tab3'];
10 | const tabs4 = ['tab1', 'tab2', 'this is tab3', 'tab5', '11', 'tab8', 'ta11'];
11 |
12 | const TabBarExample: React.FC = (props) => {
13 | const {} = props;
14 |
15 | return (
16 | <>
17 | (
22 |
23 | )}
24 | tabBarItemStyle={{
25 | height: 50,
26 | borderRadius: 25,
27 | paddingHorizontal: 30,
28 | paddingVertical: 10,
29 | backgroundColor: 'orange',
30 | }}
31 | />
32 | (
39 |
40 | )}
41 | sliderComponent={() => (
42 |
50 | )}
51 | style={{ height: 80 }}
52 | />
53 |
68 |
74 | >
75 | );
76 | };
77 |
78 | export default TabBarExample;
79 |
--------------------------------------------------------------------------------
/example/src/pages/TabViewExample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, StyleSheet, Text } from 'react-native';
3 | import { TabView } from 'react-native-maui';
4 |
5 | interface TabViewExampleProps {}
6 |
7 | const tabs = ['tab1', 'tab2', 'this is tab3', 'tab5', '11', 'tab8', 'ta11'];
8 |
9 | const TabViewExample: React.FC = (props) => {
10 | const {} = props;
11 |
12 | return (
13 |
14 |
15 | {tabs.map((tab, index) => {
16 | return (
17 |
18 | {tab}
19 | {tab}
20 |
21 | );
22 | })}
23 |
24 |
25 | );
26 | };
27 |
28 | const styles = StyleSheet.create({
29 | container: {
30 | flex: 1,
31 | },
32 | });
33 |
34 | export default TabViewExample;
35 |
--------------------------------------------------------------------------------
/example/src/pages/ThemeExample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View, TouchableOpacity, Text } from 'react-native';
3 | import { useTheme, ThemeType, Button } from 'react-native-maui';
4 | import Section from '../components/Section';
5 |
6 | export default function ThemeExample() {
7 | const { theme, changeTheme } = useTheme();
8 | return (
9 |
10 |
11 |
18 |
26 |
27 |
28 | );
29 | }
30 |
31 | const styles = StyleSheet.create({
32 | container: {
33 | flex: 1,
34 | },
35 | rect: {
36 | width: 50,
37 | height: 50,
38 | },
39 | themeTitle: {
40 | fontSize: 16,
41 | },
42 | marginLeft: {
43 | marginLeft: 15,
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/example/src/pages/ToastExample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Text, StyleSheet } from 'react-native';
3 | import {
4 | Toast,
5 | ToastUtil,
6 | TranslateContainer,
7 | OpacityContainer,
8 | Button,
9 | } from 'react-native-maui';
10 | import Section from '../components/Section';
11 |
12 | interface ToastExampleProps {}
13 |
14 | const ToastExample: React.FC = (props) => {
15 | const {} = props;
16 |
17 | return (
18 |
19 |
20 |
36 |
37 |
38 |
54 |
55 |
56 | );
57 | };
58 |
59 | const styles = StyleSheet.create({
60 | container: {
61 | justifyContent: 'center',
62 | alignItems: 'center',
63 | paddingVertical: 50,
64 | },
65 | containerStyle: {
66 | justifyContent: 'center',
67 | alignItems: 'center',
68 | },
69 | container2: {
70 | justifyContent: 'center',
71 | alignItems: 'center',
72 | paddingVertical: 20,
73 | },
74 | margin: {
75 | marginBottom: 50,
76 | },
77 | });
78 |
79 | export default ToastExample;
80 |
--------------------------------------------------------------------------------
/example/src/pages/index.ts:
--------------------------------------------------------------------------------
1 | import ButtonExample from './ButtonExample';
2 | import BadgeExample from './BadgeExample';
3 | import SegmentedExample from './SegmentedExample';
4 | import SwitchExample from './SwitchExample';
5 | import ThemeExample from './ThemeExample';
6 | import DividerExample from './DividerExample';
7 | import AvatarExample from './AvatarExample';
8 | import CollapseExample from './CollapseExample';
9 | import OverlayExample from './OverlayExample';
10 | import CarouselExample from './CarouselExample';
11 | import PickerExample from './PickerExample';
12 | import SkeletonExample from './SkeletonExample';
13 | import RefreshExample from './RefreshExample';
14 | import LoadingExample from './LoadingExample';
15 | import ToastExample from './ToastExample';
16 | import ListRowExample from './ListRowExample';
17 | import ImageViewerExample from './ImageViewerExample';
18 | import PaginationExample from './PaginationExample';
19 | import ActionSheetExample from './ActionSheetExample';
20 | import ProgressExample from './ProgressExample';
21 | import AnimatedNumberExample from './AnimatedNumberExample';
22 | import ShadowExample from './ShadowExample';
23 | import IconExample from './IconExample';
24 | import WaterFallListExample from './WaterfallListExample';
25 | import PopoverExample from './PopoverExample';
26 | import SwipeActionExample from './SwipeActionExample';
27 | import RefreshControlExample from './RefreshControlExample';
28 | import TabViewExample from './TabViewExample';
29 | import NestedTabViewExample from './NestedTabViewExample';
30 | import PageViewExample from './PageViewExample';
31 | import TabBarExample from './TabBarExample';
32 |
33 | export default {
34 | ButtonExample,
35 | BadgeExample,
36 | SegmentedExample,
37 | SwitchExample,
38 | ThemeExample,
39 | DividerExample,
40 | AvatarExample,
41 | CollapseExample,
42 | OverlayExample,
43 | CarouselExample,
44 | PickerExample,
45 | SkeletonExample,
46 | RefreshExample,
47 | LoadingExample,
48 | ToastExample,
49 | ListRowExample,
50 | ImageViewerExample,
51 | PaginationExample,
52 | ActionSheetExample,
53 | ProgressExample,
54 | AnimatedNumberExample,
55 | ShadowExample,
56 | IconExample,
57 | WaterFallListExample,
58 | PopoverExample,
59 | SwipeActionExample,
60 | RefreshControlExample,
61 | TabViewExample,
62 | NestedTabViewExample,
63 | PageViewExample,
64 | TabBarExample,
65 | };
66 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "react-native-maui": [
6 | "../src/index"
7 | ]
8 | },
9 | "allowSyntheticDefaultImports": true,
10 | "jsx": "react-native",
11 | "lib": [
12 | "dom",
13 | "esnext"
14 | ],
15 | "moduleResolution": "node",
16 | "noEmit": true,
17 | "skipLibCheck": true,
18 | "resolveJsonModule": true,
19 | "strict": true
20 | },
21 | "extends": "expo/tsconfig.base"
22 | }
23 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'react-native',
3 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
4 | transformIgnorePatterns: [
5 | 'node_modules/(?!@react-native|react-native)'
6 | ],
7 | };
8 |
9 |
10 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | // So
--------------------------------------------------------------------------------
/publish.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 检查是否以root用户运行
4 | if [ "$(id -u)" -eq 0 ]; then
5 | echo "This script should not be run as root"
6 | exit 1
7 | fi
8 |
9 | # 切换到npm官方源
10 | echo "Switching npm registry to https://registry.npmjs.org/"
11 | npm config set registry https://registry.npmjs.org/
12 |
13 | # 验证是否切换成功
14 | current_registry=$(npm config get registry)
15 | if [ "$current_registry" != "https://registry.npmjs.org/" ]; then
16 | echo "Failed to switch npm registry to https://registry.npmjs.org/"
17 | exit 1
18 | fi
19 |
20 | echo "npm registry has been switched to https://registry.npmjs.org/"
21 |
22 | # 执行npm publish命令
23 | echo "Executing npm publish..."
24 | npm publish
25 | publish_status=$?
26 |
27 | # 检查publish命令是否成功
28 | if [ $publish_status -ne 0 ]; then
29 | echo "npm publish failed"
30 | # 切换回npmmirror源之前先退出脚本,避免进一步操作
31 | exit $publish_status
32 | fi
33 |
34 | echo "npm publish succeeded"
35 |
36 | # 切换到npmmirror源
37 | echo "Switching npm registry back to https://registry.npmmirror.com/"
38 | npm config set registry https://registry.npmmirror.com/
39 |
40 | # 验证是否切换成功
41 | current_registry=$(npm config get registry)
42 | if [ "$current_registry" != "https://registry.npmmirror.com/" ]; then
43 | echo "Failed to switch npm registry to https://registry.npmmirror.com/"
44 | exit 1
45 | fi
46 |
47 | echo "npm registry has been switched back to https://registry.npmmirror.com/"
48 |
49 | echo "Script execution completed successfully"
50 | exit 0
--------------------------------------------------------------------------------
/screenshoot/ActionSheet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/ActionSheet.png
--------------------------------------------------------------------------------
/screenshoot/AnimatedNumber.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/AnimatedNumber.gif
--------------------------------------------------------------------------------
/screenshoot/Avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Avatar.png
--------------------------------------------------------------------------------
/screenshoot/Badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Badge.png
--------------------------------------------------------------------------------
/screenshoot/Button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Button.png
--------------------------------------------------------------------------------
/screenshoot/Collapse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Collapse.png
--------------------------------------------------------------------------------
/screenshoot/Divider.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Divider.png
--------------------------------------------------------------------------------
/screenshoot/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Icon.png
--------------------------------------------------------------------------------
/screenshoot/ImageViewer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/ImageViewer.png
--------------------------------------------------------------------------------
/screenshoot/ListRow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/ListRow.png
--------------------------------------------------------------------------------
/screenshoot/Loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Loading.gif
--------------------------------------------------------------------------------
/screenshoot/Overlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Overlay.png
--------------------------------------------------------------------------------
/screenshoot/Pagination.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Pagination.png
--------------------------------------------------------------------------------
/screenshoot/Picker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Picker.png
--------------------------------------------------------------------------------
/screenshoot/Progress.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Progress.gif
--------------------------------------------------------------------------------
/screenshoot/RefreshList.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/RefreshList.png
--------------------------------------------------------------------------------
/screenshoot/Segmented.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Segmented.png
--------------------------------------------------------------------------------
/screenshoot/Shadow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Shadow.png
--------------------------------------------------------------------------------
/screenshoot/Skeleton.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Skeleton.gif
--------------------------------------------------------------------------------
/screenshoot/Swiper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Swiper.png
--------------------------------------------------------------------------------
/screenshoot/Switch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Switch.png
--------------------------------------------------------------------------------
/screenshoot/Theme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Theme.png
--------------------------------------------------------------------------------
/screenshoot/Toast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/Toast.png
--------------------------------------------------------------------------------
/screenshoot/WaterFallList.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/WaterFallList.png
--------------------------------------------------------------------------------
/screenshoot/android_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/android_1.jpg
--------------------------------------------------------------------------------
/screenshoot/android_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/android_2.jpg
--------------------------------------------------------------------------------
/screenshoot/button.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/button.gif
--------------------------------------------------------------------------------
/screenshoot/carousel.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/carousel.gif
--------------------------------------------------------------------------------
/screenshoot/ios_1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/ios_1.jpeg
--------------------------------------------------------------------------------
/screenshoot/ios_2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/ios_2.jpeg
--------------------------------------------------------------------------------
/screenshoot/number.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/number.gif
--------------------------------------------------------------------------------
/screenshoot/overlay.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/overlay.gif
--------------------------------------------------------------------------------
/screenshoot/qrcode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/qrcode.png
--------------------------------------------------------------------------------
/screenshoot/refresh.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/refresh.gif
--------------------------------------------------------------------------------
/screenshoot/waterFall.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mahaaoo/react-native-maui/60e77f0a08779dd1624c19fa2d96344a011da066/screenshoot/waterFall.gif
--------------------------------------------------------------------------------
/script/npm-publish.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | npm config set registry=https://registry.npmjs.org
3 | echo '请进行登录相关操作:'
4 | npm login # 登陆
5 | echo "-------publishing-------"
6 | npm publish # 发布
7 | echo "发布结束,请注意控制台的实际输出情况"
8 | exit
--------------------------------------------------------------------------------
/src/components/ActionSheet/ActionSheet.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | View,
4 | Text,
5 | TouchableOpacity,
6 | ViewStyle,
7 | StyleSheet,
8 | } from 'react-native';
9 | import { ActionSheetUtil } from './ActionSheetUtil';
10 |
11 | export interface ActionSheetProps {
12 | options: string[];
13 | optionStyle?: ViewStyle;
14 | closeStyle?: ViewStyle;
15 | marginBottom?: number;
16 |
17 | onSelect?: (item: string, index: number) => void;
18 | onDisappear?: () => void;
19 | }
20 |
21 | const ActionSheet: React.FC = (props) => {
22 | const {
23 | options,
24 | optionStyle,
25 | closeStyle,
26 | onSelect,
27 | marginBottom = 50,
28 | } = props;
29 |
30 | return (
31 |
32 |
33 | {options.map((item, index) => {
34 | return (
35 | {
39 | ActionSheetUtil.hide();
40 | onSelect && onSelect(item, index);
41 | }}
42 | >
43 | {item}
44 |
45 | );
46 | })}
47 |
48 |
49 |
53 | {`close`}
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | const styles = StyleSheet.create({
61 | item: {
62 | fontSize: 20,
63 | color: '#1e90ff',
64 | },
65 | close: {
66 | fontSize: 20,
67 | color: 'red',
68 | },
69 | container: {
70 | borderRadius: 8,
71 | marginHorizontal: 15,
72 | overflow: 'hidden',
73 | },
74 | itemContainer: {
75 | justifyContent: 'center',
76 | backgroundColor: '#F8F8F8',
77 | alignItems: 'center',
78 | paddingVertical: 15,
79 | marginTop: StyleSheet.hairlineWidth,
80 | },
81 | errorContainer: {
82 | borderRadius: 8,
83 | marginHorizontal: 15,
84 | marginTop: 10,
85 | overflow: 'hidden',
86 | },
87 | });
88 |
89 | export default ActionSheet;
90 |
--------------------------------------------------------------------------------
/src/components/ActionSheet/ActionSheetFull.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | View,
4 | Text,
5 | TouchableOpacity,
6 | ViewStyle,
7 | StyleSheet,
8 | } from 'react-native';
9 | import { ActionSheetUtil } from './ActionSheetUtil';
10 |
11 | export interface ActionSheetFullProps {
12 | options: string[];
13 | optionStyle?: ViewStyle;
14 | closeStyle?: ViewStyle;
15 |
16 | onSelect?: (item: string, index: number) => void;
17 | onDisappear?: () => void;
18 | }
19 |
20 | const ActionSheetFull: React.FC = (props) => {
21 | const { options, optionStyle, closeStyle, onSelect } = props;
22 |
23 | return (
24 | <>
25 |
26 | {options.map((item, index) => {
27 | return (
28 | {
32 | ActionSheetUtil.hide();
33 | onSelect && onSelect(item, index);
34 | }}
35 | >
36 | {item}
37 |
38 | );
39 | })}
40 |
41 |
42 |
46 | {`close`}
47 |
48 |
49 | >
50 | );
51 | };
52 |
53 | const styles = StyleSheet.create({
54 | item: {
55 | fontSize: 20,
56 | color: '#1e90ff',
57 | },
58 | close: {
59 | fontSize: 20,
60 | color: 'red',
61 | },
62 | itemContainer: {
63 | justifyContent: 'center',
64 | backgroundColor: '#F8F8F8',
65 | alignItems: 'center',
66 | paddingVertical: 15,
67 | marginTop: StyleSheet.hairlineWidth,
68 | },
69 | cancelContainer: {
70 | justifyContent: 'center',
71 | backgroundColor: '#F8F8F8',
72 | alignItems: 'center',
73 | paddingTop: 15,
74 | paddingBottom: 50,
75 | marginTop: StyleSheet.hairlineWidth,
76 | },
77 | hidden: {
78 | overflow: 'hidden',
79 | },
80 | });
81 |
82 | export default ActionSheetFull;
83 |
--------------------------------------------------------------------------------
/src/components/ActionSheet/ActionSheetUtil.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { overlayRef } from '../Overlay';
3 | import { TranslateContainer } from '../Overlay';
4 | import { ActionSheetProps } from './ActionSheet';
5 |
6 | export const ActionSheetUtil = {
7 | key: 'global-action-sheet',
8 | show: (actionSheet: React.ReactElement) => {
9 | const onDisappear = actionSheet?.props?.onDisappear;
10 | const component = (
11 |
12 | {actionSheet}
13 |
14 | );
15 |
16 | overlayRef.current?.add(component, ActionSheetUtil.key);
17 | },
18 | hide: () => overlayRef.current?.remove(ActionSheetUtil.key),
19 | isExist: () => overlayRef.current?.isExist(ActionSheetUtil.key),
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/ActionSheet/index.tsx:
--------------------------------------------------------------------------------
1 | import ActionSheet from './ActionSheet';
2 | import ActionSheetFull from './ActionSheetFull';
3 |
4 | import { ActionSheetUtil } from './ActionSheetUtil';
5 |
6 | export { ActionSheet, ActionSheetUtil, ActionSheetFull };
7 |
--------------------------------------------------------------------------------
/src/components/AnimatedNumber/AnimatedNumber.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useState } from 'react';
2 | import { EasingFunction, Text, TextStyle } from 'react-native';
3 | import {
4 | Easing,
5 | EasingFunctionFactory,
6 | runOnJS,
7 | useAnimatedReaction,
8 | useSharedValue,
9 | withDelay,
10 | withTiming,
11 | } from 'react-native-reanimated';
12 |
13 | interface AnimatedNumberProps {
14 | value: number;
15 |
16 | style?: TextStyle;
17 | toFixed?: number;
18 | duration?: number;
19 | delay?: number;
20 |
21 | easing?: 'linear' | 'ease' | EasingFunction | EasingFunctionFactory;
22 | }
23 |
24 | const AnimatedNumber: React.FC = (props) => {
25 | const {
26 | value,
27 | style,
28 | toFixed = 0,
29 | duration = 1000,
30 | delay = 500,
31 | easing = 'linear',
32 | } = props;
33 | const animation = useSharedValue(value);
34 | const [show, setShow] = useState(value);
35 |
36 | const initialEasing = useMemo(() => {
37 | if (typeof easing === 'string') {
38 | if (easing === 'linear') return Easing.linear;
39 | if (easing === 'ease') return Easing.ease;
40 | }
41 |
42 | return easing;
43 | }, [easing]);
44 |
45 | useEffect(() => {
46 | animation.value = withDelay(
47 | delay,
48 | withTiming(value, { duration, easing: initialEasing })
49 | );
50 | }, [value]);
51 |
52 | useAnimatedReaction(
53 | () => Number(animation.value.toFixed(toFixed)),
54 | (value) => {
55 | runOnJS(setShow)(value);
56 | }
57 | );
58 |
59 | return {show};
60 | };
61 |
62 | export default AnimatedNumber;
63 |
--------------------------------------------------------------------------------
/src/components/AnimatedNumber/ScrollNumber.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { View, Text, StyleSheet } from 'react-native';
3 | import Animated, {
4 | useAnimatedStyle,
5 | useSharedValue,
6 | withDelay,
7 | withSpring,
8 | } from 'react-native-reanimated';
9 |
10 | const useNumber = (number: number) => {
11 | const numberList = String(number).split('');
12 | return numberList.map((item: string) => Number(item));
13 | };
14 |
15 | interface ScrollNumberItemProps {
16 | value: number;
17 | delay: number;
18 | }
19 |
20 | const sigleList = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
21 |
22 | const ScrollNumberItem: React.FC = (props) => {
23 | const { value, delay } = props;
24 | const translateY = useSharedValue(0);
25 |
26 | useEffect(() => {
27 | const dest = -value * 40;
28 | translateY.value = withDelay(
29 | delay * 50,
30 | withSpring(dest, { velocity: 100 })
31 | );
32 | }, [value]);
33 |
34 | const animatedStyle = useAnimatedStyle(() => {
35 | return {
36 | transform: [
37 | {
38 | translateY: translateY.value,
39 | },
40 | ],
41 | };
42 | });
43 |
44 | return (
45 |
46 |
47 | {sigleList.map((item) => {
48 | return {item};
49 | })}
50 |
51 |
52 | );
53 | };
54 |
55 | const styles = StyleSheet.create({
56 | container: {
57 | flexDirection: 'row',
58 | },
59 | numberContainer: {
60 | height: 40,
61 | alignItems: 'center',
62 | overflow: 'hidden',
63 | },
64 | number: {
65 | fontSize: 40,
66 | fontWeight: 'bold',
67 | lineHeight: 40,
68 | },
69 | });
70 |
71 | interface ScrollNumberProps {
72 | value: number;
73 | }
74 |
75 | const ScrollNumber: React.FC = (props) => {
76 | const { value } = props;
77 | const dataSource = useNumber(value);
78 |
79 | return (
80 |
81 | {dataSource.map((item, index) => {
82 | return ;
83 | })}
84 |
85 | );
86 | };
87 |
88 | export default ScrollNumber;
89 |
--------------------------------------------------------------------------------
/src/components/AnimatedNumber/index.tsx:
--------------------------------------------------------------------------------
1 | import AnimatedNumber from './AnimatedNumber';
2 | import ScrollNumber from './ScrollNumber';
3 |
4 | export { AnimatedNumber, ScrollNumber };
5 |
--------------------------------------------------------------------------------
/src/components/Avatar/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import {
3 | View,
4 | StyleSheet,
5 | Image,
6 | ImageStyle,
7 | ImageResizeMode,
8 | } from 'react-native';
9 | import Animated, {
10 | runOnJS,
11 | useAnimatedStyle,
12 | useSharedValue,
13 | withTiming,
14 | } from 'react-native-reanimated';
15 |
16 | interface AvatarProps {
17 | url: string;
18 | style: ImageStyle;
19 |
20 | delay?: number;
21 | placeholder?: React.ReactNode;
22 | resizeMode?: ImageResizeMode;
23 | }
24 |
25 | const Avatar: React.FC = (props) => {
26 | const { style, placeholder, url, resizeMode = 'cover', delay = 500 } = props;
27 | const [remove, setRemove] = useState(false);
28 | const finished = useSharedValue(false);
29 |
30 | const loadFinish = useCallback(() => {
31 | finished.value = true;
32 | }, []);
33 |
34 | const animationStyle = useAnimatedStyle(() => {
35 | return {
36 | opacity: finished.value ? withTiming(1, { duration: delay }) : 0,
37 | };
38 | });
39 |
40 | const placeholderStyle = useAnimatedStyle(() => {
41 | return {
42 | opacity: finished.value
43 | ? withTiming(0, { duration: delay }, () => {
44 | runOnJS(setRemove)(true);
45 | })
46 | : 1,
47 | };
48 | });
49 |
50 | return (
51 |
52 |
53 |
59 |
60 | {!remove && (
61 |
62 | {placeholder}
63 |
64 | )}
65 |
66 | );
67 | };
68 |
69 | const styles = StyleSheet.create({
70 | placeholder: {
71 | ...StyleSheet.absoluteFillObject,
72 | justifyContent: 'center',
73 | alignItems: 'center',
74 | backgroundColor: '#eee',
75 | },
76 | });
77 |
78 | export default Avatar;
79 |
--------------------------------------------------------------------------------
/src/components/Avatar/__test__/__snapshots__/avatar.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Test:Avatar render correctly 1`] = `
4 |
5 |
20 |
36 |
37 |
63 |
64 | 加载中
65 |
66 |
67 |
68 | `;
69 |
--------------------------------------------------------------------------------
/src/components/Avatar/__test__/avatar.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '@testing-library/react-native';
3 | import { withReanimatedTimer } from 'react-native-reanimated/src/reanimated2/jestUtils';
4 | import { Text } from 'react-native';
5 | import { Avatar } from '../index';
6 |
7 | describe('Test:Avatar', () => {
8 | it('render correctly', () => {
9 | withReanimatedTimer(() => {
10 | const tree = render(
11 | 加载中}
17 | />
18 | ).toJSON();
19 | expect(tree).toMatchSnapshot();
20 | });
21 | });
22 |
23 | it('base', () => {
24 | withReanimatedTimer(() => {
25 | const { queryByText } = render(
26 | 加载中}
32 | />
33 | );
34 | const element = queryByText('加载中');
35 | expect(element).not.toBeNull();
36 | });
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/components/Avatar/index.tsx:
--------------------------------------------------------------------------------
1 | import Avatar from './Avatar';
2 |
3 | export { Avatar };
4 |
--------------------------------------------------------------------------------
/src/components/Badge/Badge.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { View, StyleSheet, Text, ViewStyle } from 'react-native';
3 |
4 | interface BadgeProps {
5 | number: number;
6 |
7 | size?: number;
8 | fontSize?: number;
9 | color?: string;
10 | style?: ViewStyle;
11 | }
12 |
13 | const Badge: React.FC = (props) => {
14 | const {
15 | number,
16 | size = 15,
17 | fontSize = 0.8 * size,
18 | color = '#fc4840',
19 | style,
20 | } = props;
21 |
22 | const content = useMemo(() => {
23 | if (number > 99) {
24 | return {
25 | title: '99+',
26 | width: 2 * size,
27 | height: 2 * size * 0.61,
28 | borderRadius: (2 * size * 0.61) / 2,
29 | };
30 | }
31 | if (number > 9) {
32 | return {
33 | title: number,
34 | width: 1.5 * size,
35 | height: 1.5 * size * 0.61,
36 | borderRadius: (1.5 * size * 0.61) / 2,
37 | };
38 | }
39 | return {
40 | title: number,
41 | width: size,
42 | height: size,
43 | borderRadius: size / 2,
44 | };
45 | }, [number, size]);
46 |
47 | if (!number || number === 0) {
48 | return ;
49 | }
50 |
51 | return (
52 |
65 |
66 | {content.title}
67 |
68 |
69 | );
70 | };
71 |
72 | const styles = StyleSheet.create({
73 | container: {
74 | justifyContent: 'center',
75 | alignItems: 'center',
76 | },
77 | textColor: {
78 | color: '#fff',
79 | },
80 | });
81 |
82 | export default Badge;
83 |
--------------------------------------------------------------------------------
/src/components/Badge/__test__/__snapshots__/badge.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Test:Avatar render correctly 1`] = `
4 |
24 |
36 | 10
37 |
38 |
39 | `;
40 |
--------------------------------------------------------------------------------
/src/components/Badge/__test__/badge.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '@testing-library/react-native';
3 | import { Badge } from '../index';
4 |
5 | describe('Test:Avatar', () => {
6 | it('render correctly', () => {
7 | const tree = render().toJSON();
8 | expect(tree).toMatchSnapshot();
9 | });
10 |
11 | it('base', () => {
12 | const { queryByText: queryByText1 } = render(
13 |
14 | );
15 | const element1 = queryByText1('10');
16 | expect(element1).not.toBeNull();
17 | });
18 | it('over 99', () => {
19 | const { queryByText: queryByText2 } = render(
20 |
21 | );
22 | const element2 = queryByText2('99+');
23 | expect(element2).not.toBeNull();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/components/Badge/index.tsx:
--------------------------------------------------------------------------------
1 | import Badge from './Badge';
2 |
3 | export { Badge };
4 |
--------------------------------------------------------------------------------
/src/components/Button/BaseButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo } from 'react';
2 | import { TouchableOpacity, StyleProp, ViewStyle } from 'react-native';
3 |
4 | export interface BaseButtonProps {
5 | onPress: () => void;
6 | style?: StyleProp;
7 | disabled?: boolean;
8 | withoutFeedback?: boolean;
9 | }
10 |
11 | const BaseButton: React.FC = (props) => {
12 | const {
13 | onPress,
14 | style,
15 | children,
16 | disabled = false,
17 | withoutFeedback = false,
18 | } = props;
19 |
20 | const activeOpacity = useMemo(() => {
21 | return disabled ? 1 : 0.2;
22 | }, [disabled]);
23 |
24 | const handlePress = useCallback(() => {
25 | if (disabled) return;
26 | onPress && onPress();
27 | }, [onPress]);
28 |
29 | return (
30 |
36 | {children}
37 |
38 | );
39 | };
40 |
41 | export default BaseButton;
42 |
--------------------------------------------------------------------------------
/src/components/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleProp, ViewStyle, Text, TextStyle } from 'react-native';
3 | import BaseButton, { BaseButtonProps } from './BaseButton';
4 | import { useType } from './utils';
5 | import { ButtonType } from './type';
6 |
7 | interface ButtonProps extends BaseButtonProps {
8 | style?: StyleProp;
9 | type?: ButtonType;
10 | textStyle?: TextStyle;
11 | children: React.ReactNode;
12 | }
13 |
14 | const Button: React.FC = (props) => {
15 | const {
16 | onPress,
17 | style,
18 | children,
19 | type = ButtonType.Default,
20 | disabled,
21 | textStyle,
22 | ...options
23 | } = props;
24 | const typeStyle = useType(type);
25 |
26 | return (
27 |
31 | {typeof children === 'string' ? (
32 | {children}
33 | ) : (
34 | children
35 | )}
36 |
37 | );
38 | };
39 |
40 | export default Button;
41 |
--------------------------------------------------------------------------------
/src/components/Button/__test__/__snapshots__/button.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Test:Button render correctly 1`] = `
4 |
29 |
30 | 默认样式
31 |
32 |
33 | `;
34 |
--------------------------------------------------------------------------------
/src/components/Button/__test__/button.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, fireEvent } from '@testing-library/react-native';
3 | import { Text } from 'react-native';
4 | import { Button } from '../index';
5 |
6 | describe('Test:Button', () => {
7 | it('render correctly', () => {
8 | const tree = render(
9 |
12 | ).toJSON();
13 | expect(tree).toMatchSnapshot();
14 | });
15 |
16 | it('base button', async () => {
17 | const onPressMock = jest.fn();
18 | const { queryByText: queryByText1 } = render(
19 |
22 | );
23 | const element1 = queryByText1('默认样式');
24 | expect(element1).not.toBeNull();
25 |
26 | // onPressMock被调用了
27 | fireEvent(element1, 'onPress'); // 触发Button里的组件方法
28 | expect(onPressMock).toHaveBeenCalled();
29 | });
30 |
31 | it('button disabled', async () => {
32 | const onPressMock2 = jest.fn();
33 | const { queryByText: queryByText2 } = render(
34 |
37 | );
38 |
39 | const element2 = queryByText2('禁止点击');
40 | expect(element2).not.toBeNull();
41 |
42 | // 不可点击
43 | fireEvent(element2, 'onPress');
44 | expect(onPressMock2).not.toHaveBeenCalled();
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/src/components/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import Button from './Button';
2 | import { ButtonType } from './type';
3 | import GradientButton from './GradientButton';
4 |
5 | export { Button, ButtonType, GradientButton };
6 |
--------------------------------------------------------------------------------
/src/components/Button/type.ts:
--------------------------------------------------------------------------------
1 | export enum ButtonType {
2 | Default = 1 << 2,
3 | Primary = 1 << 1,
4 | Link = 1 << 3,
5 | Disabled = 1 << 4,
6 | None = 1 << 5,
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/Button/utils.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { ViewStyle } from 'react-native';
3 | import { ButtonType } from './type';
4 |
5 | export const useType = (type: ButtonType): ViewStyle => {
6 | const style: ViewStyle = useMemo(() => {
7 | switch (true) {
8 | case type === ButtonType.Primary:
9 | return {
10 | padding: 15,
11 | borderRadius: 3,
12 | justifyContent: 'center',
13 | alignItems: 'center',
14 | backgroundColor: '#2593FC',
15 | };
16 | case type === ButtonType.Default:
17 | return {
18 | padding: 15,
19 | borderRadius: 3,
20 | justifyContent: 'center',
21 | alignItems: 'center',
22 | backgroundColor: '#fff',
23 | borderColor: '#D9D9D9',
24 | borderWidth: 1,
25 | };
26 | case type === ButtonType.Link:
27 | return {
28 | padding: 15,
29 | justifyContent: 'center',
30 | alignItems: 'center',
31 | };
32 | case type === ButtonType.Disabled:
33 | return {
34 | padding: 15,
35 | borderRadius: 3,
36 | justifyContent: 'center',
37 | alignItems: 'center',
38 | backgroundColor: '#F5F5F5',
39 | borderColor: '#D9D9D9',
40 | borderWidth: 1,
41 | };
42 | default:
43 | return {};
44 | }
45 | }, [type]);
46 |
47 | return style;
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/Carousel/BaseLayout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import Animated, { useAnimatedStyle } from 'react-native-reanimated';
4 | import { getItemOffset } from './utils';
5 | import { BaseLayoutProps, useCarousel } from './type';
6 |
7 | const BaseLayout: React.FC = (props) => {
8 | const { index, children } = props;
9 |
10 | const {
11 | currentIndex,
12 | translate,
13 | size,
14 | options,
15 | horizontal,
16 | stepDistance,
17 | itemSize = 0,
18 | } = useCarousel();
19 |
20 | const style = useAnimatedStyle(() => {
21 | const itemOffset = getItemOffset(
22 | -currentIndex.value,
23 | index,
24 | size,
25 | currentIndex.value,
26 | options
27 | );
28 |
29 | if (horizontal) {
30 | return {
31 | transform: [
32 | {
33 | translateX:
34 | translate.value +
35 | itemOffset * size * stepDistance +
36 | index * itemSize,
37 | },
38 | {
39 | translateY: 0,
40 | },
41 | ],
42 | };
43 | } else {
44 | return {
45 | transform: [
46 | {
47 | translateY: translate.value + itemOffset * size * stepDistance,
48 | },
49 | {
50 | translateX: 0,
51 | },
52 | ],
53 | };
54 | }
55 | }, [currentIndex, horizontal]);
56 |
57 | return (
58 | {children}
59 | );
60 | };
61 |
62 | const styles = StyleSheet.create({
63 | container: {
64 | position: 'absolute',
65 | },
66 | });
67 |
68 | export default BaseLayout;
69 |
--------------------------------------------------------------------------------
/src/components/Carousel/ItemWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
3 | import { judgeRange } from './utils';
4 | import { ItemWrapperProps, useCarousel } from './type';
5 |
6 | const ItemWrapper: React.FC = (props) => {
7 | const { index, children } = props;
8 | const { currentIndex, size, options } = useCarousel();
9 | const [hidden, setHidden] = useState(true);
10 |
11 | useAnimatedReaction(
12 | () => currentIndex.value,
13 | () => {
14 | const isRange = judgeRange(index, size, currentIndex.value, options);
15 | runOnJS(setHidden)(!isRange);
16 | }
17 | );
18 |
19 | if (hidden) {
20 | return null;
21 | }
22 |
23 | return <>{children}>;
24 | };
25 |
26 | export default ItemWrapper;
27 |
--------------------------------------------------------------------------------
/src/components/Carousel/RotateLayout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dimensions } from 'react-native';
3 | import Animated, {
4 | Extrapolation,
5 | interpolate,
6 | useAnimatedStyle,
7 | } from 'react-native-reanimated';
8 | import { getItemOffset, getLayoutValue } from './utils';
9 | import { RotateLayoutProps, useCarousel } from './type';
10 |
11 | const { width } = Dimensions.get('window');
12 |
13 | const RotateLayout: React.FC = (props) => {
14 | const { children, index } = props;
15 | const {
16 | currentIndex,
17 | translate,
18 | size,
19 | options,
20 | stepDistance,
21 | translateIndex,
22 | layoutOption,
23 | indexAtData,
24 | } = useCarousel();
25 |
26 | const style = useAnimatedStyle(() => {
27 | const itemOffset = getItemOffset(
28 | -currentIndex.value,
29 | index,
30 | size,
31 | currentIndex.value,
32 | options
33 | );
34 | const value = getLayoutValue(
35 | index,
36 | translateIndex,
37 | currentIndex,
38 | indexAtData,
39 | translate,
40 | stepDistance,
41 | size,
42 | true
43 | );
44 |
45 | // if (index === 0) {
46 | // console.log({
47 | // value,
48 | // })
49 | // }
50 |
51 | // console.log(translateIndex.value);
52 | // 3 -> 4
53 | // -1 -> 0
54 | const rotateY = interpolate(
55 | value,
56 | [index - 1, index, index + 1],
57 | [-45, 0, 45],
58 | Extrapolation.CLAMP
59 | );
60 |
61 | return {
62 | position: 'absolute',
63 | transform: [
64 | {
65 | translateX:
66 | translate.value +
67 | itemOffset * size * stepDistance +
68 | (width - layoutOption?.options.mainAxisSize) / 2 +
69 | 250 * index,
70 | },
71 | { translateY: 30 },
72 | {
73 | perspective: 800,
74 | },
75 | {
76 | rotateY: `${rotateY}deg`,
77 | },
78 | ],
79 | };
80 | });
81 |
82 | return {children};
83 | };
84 |
85 | export default RotateLayout;
86 |
--------------------------------------------------------------------------------
/src/components/Carousel/index.tsx:
--------------------------------------------------------------------------------
1 | import Carousel from './Carousel';
2 | import { CarouselRef } from './type';
3 | import BaseLayout from './BaseLayout';
4 | import ScaleLayout from './ScaleLayout';
5 | import RotateLayout from './RotateLayout';
6 |
7 | export { Carousel, BaseLayout, ScaleLayout, RotateLayout, CarouselRef };
8 |
--------------------------------------------------------------------------------
/src/components/Collapse/CollapseGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import { View } from 'react-native';
3 | import { CollapseContext } from './type';
4 |
5 | interface CollapseGroupProps {
6 | defaultActive?: string;
7 | accordion?: boolean;
8 | }
9 |
10 | const CollapseGroup: React.FC = (props) => {
11 | const { children, accordion = false, defaultActive } = props;
12 | const [currentActive, setActive] = useState(defaultActive);
13 |
14 | const handleOnChange = useCallback((tag: string) => {
15 | setActive(tag);
16 | }, []);
17 |
18 | return (
19 |
26 | {children}
27 |
28 | );
29 | };
30 |
31 | export default CollapseGroup;
32 |
--------------------------------------------------------------------------------
/src/components/Collapse/index.tsx:
--------------------------------------------------------------------------------
1 | import Collapse from './Collapse';
2 | import CollapseGroup from './CollapseGroup';
3 |
4 | export { Collapse, CollapseGroup };
5 |
--------------------------------------------------------------------------------
/src/components/Collapse/type.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 |
3 | export const CollapseContext = React.createContext(
4 | {} as CollapseContextProps
5 | );
6 | export const useCollapse = () => useContext(CollapseContext);
7 |
8 | export interface CollapseContextProps {
9 | accordion: boolean;
10 | currentActive?: string;
11 | handleOnChange: (tag: string) => void;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Divider/Divider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Svg, { Line } from 'react-native-svg';
3 |
4 | export interface DividerProps {
5 | start: number;
6 | end: number;
7 |
8 | vertical?: boolean;
9 | color?: string;
10 | strokeDasharray?: string;
11 | width?: number;
12 | }
13 |
14 | const Divider: React.FC = (props) => {
15 | const {
16 | vertical,
17 | strokeDasharray,
18 | color = '#e4e4e4',
19 | start,
20 | end,
21 | width = 1,
22 | } = props;
23 |
24 | if (vertical) {
25 | return (
26 |
37 | );
38 | }
39 |
40 | return (
41 |
52 | );
53 | };
54 |
55 | export default Divider;
56 |
--------------------------------------------------------------------------------
/src/components/Divider/__test__/__snapshots__/divider.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Test:Divider render correctly 1`] = `
4 |
25 |
26 |
40 |
41 |
42 | `;
43 |
--------------------------------------------------------------------------------
/src/components/Divider/__test__/divider.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Divider } from '../index';
3 | import { render } from '@testing-library/react-native';
4 |
5 | describe('Test:Divider', () => {
6 | it('render correctly', () => {
7 | const tree = render().toJSON();
8 | expect(tree).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/src/components/Divider/index.tsx:
--------------------------------------------------------------------------------
1 | import Divider, { DividerProps } from './Divider';
2 |
3 | export { Divider, DividerProps };
4 |
--------------------------------------------------------------------------------
/src/components/Error/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | class ErrorBoundary extends React.Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = { hasError: false };
8 |
9 | global.ErrorUtils.setGlobalHandler((e) => {
10 | /*你的异常处理逻辑*/
11 | console.log('%c 处理异常 .....', 'font-size:12px;color:#869');
12 | console.log(e.message);
13 | this.setState({
14 | hasError: true,
15 | });
16 | });
17 | }
18 |
19 | static getDerivedStateFromError(error) {
20 | // 更新 state 使下一次渲染能够显示降级后的 UI
21 | return { hasError: true };
22 | }
23 |
24 | componentDidCatch(error, errorInfo) {
25 | // 你同样可以将错误日志上报给服务器
26 | console.log(error, errorInfo);
27 | }
28 |
29 | render() {
30 | if (this.state.hasError) {
31 | // 你可以自定义降级后的 UI 并渲染
32 | return this.props.errorPage ? (
33 | this.props.errorPage
34 | ) : (
35 |
38 | Something went wrong.
39 |
40 | );
41 | }
42 |
43 | return this.props.children;
44 | }
45 | }
46 |
47 | export default ErrorBoundary;
48 |
--------------------------------------------------------------------------------
/src/components/Icon/Icon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Svg, { Path } from 'react-native-svg';
3 | import { Library } from './library';
4 |
5 | interface IconProps {
6 | name: keyof typeof Library;
7 | size?: number;
8 | color?: string;
9 | }
10 |
11 | const Icon: React.FC = (props) => {
12 | const { name, size = 20, color = 'black' } = props;
13 |
14 | return (
15 |
23 | );
24 | };
25 |
26 | export default Icon;
27 |
--------------------------------------------------------------------------------
/src/components/Icon/index.tsx:
--------------------------------------------------------------------------------
1 | import Icon from './Icon';
2 |
3 | export { Icon };
4 |
--------------------------------------------------------------------------------
/src/components/ImageViewer/ImageContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useRef } from 'react';
2 | import { TouchableOpacity, View } from 'react-native';
3 | import Animated, {
4 | useAnimatedStyle,
5 | SharedValue,
6 | } from 'react-native-reanimated';
7 |
8 | interface ImageContainerProps {
9 | children: React.ReactNode;
10 | currentIndex: SharedValue;
11 | index: number;
12 |
13 | onPress: () => void;
14 | onLayout: (position: Position) => void;
15 | }
16 |
17 | export type Position = {
18 | x: number | undefined;
19 | y: number | undefined;
20 | width: number | undefined;
21 | height: number | undefined;
22 | pageX: number | undefined;
23 | pageY: number | undefined;
24 | };
25 |
26 | const ImageContainer: React.FC = (props) => {
27 | const { children, onPress, onLayout, currentIndex, index } = props;
28 | const aref = useRef(null);
29 |
30 | const handleLayout = useCallback(() => {
31 | try {
32 | aref?.current?.measure((x, y, width, height, pageX, pageY) => {
33 | onLayout &&
34 | onLayout({
35 | x,
36 | y,
37 | width,
38 | height,
39 | pageX,
40 | pageY,
41 | });
42 | });
43 | } catch {
44 | onLayout &&
45 | onLayout({
46 | height: undefined,
47 | width: undefined,
48 | x: undefined,
49 | y: undefined,
50 | pageX: undefined,
51 | pageY: undefined,
52 | });
53 | }
54 | }, []);
55 |
56 | const handlePress = useCallback(() => {
57 | onPress && onPress();
58 | }, [onPress]);
59 |
60 | const animationStyle = useAnimatedStyle(() => {
61 | return {
62 | opacity: currentIndex.value === index ? 0 : 1,
63 | };
64 | });
65 |
66 | return (
67 |
68 |
69 | (aref.current = ref)} onLayout={handleLayout}>
70 | {children}
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default ImageContainer;
78 |
--------------------------------------------------------------------------------
/src/components/ImageViewer/ImageViewer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useRef } from 'react';
2 | import { View, StyleSheet } from 'react-native';
3 | import { useSharedValue } from 'react-native-reanimated';
4 | import { useOverlay } from '../Overlay';
5 | import ImageContainer, { Position } from './ImageContainer';
6 | import ImageOverlay from './ImageOverlay';
7 |
8 | interface ImageViewerProps {
9 | data: any[];
10 | renderItem: (item: any, index: number) => React.ReactNode;
11 | }
12 |
13 | const ImageViewer: React.FC = (props) => {
14 | const { data, renderItem } = props;
15 | const { add } = useOverlay();
16 | const positionList = useRef(new Array(data.length).fill(0));
17 | const currentIndex = useSharedValue(-1);
18 |
19 | const handlePress = useCallback((index: number) => {
20 | currentIndex.value = index;
21 | add(
22 | {
28 | // when disappeared, all image show
29 | currentIndex.value = -1;
30 | }}
31 | />,
32 | 'global-image-viewer'
33 | );
34 | }, []);
35 |
36 | return (
37 |
38 | {data.map((item, index) => {
39 | return (
40 | {
43 | handlePress(index);
44 | }}
45 | index={index}
46 | currentIndex={currentIndex}
47 | onLayout={(position) => {
48 | positionList.current[index] = position;
49 | }}
50 | >
51 | {renderItem && renderItem(item, index)}
52 |
53 | );
54 | })}
55 |
56 | );
57 | };
58 |
59 | const styles = StyleSheet.create({
60 | container: {
61 | flexDirection: 'row',
62 | flexWrap: 'wrap',
63 | },
64 | });
65 |
66 | export default ImageViewer;
67 |
--------------------------------------------------------------------------------
/src/components/ImageViewer/index.tsx:
--------------------------------------------------------------------------------
1 | import ImageViewer from './ImageViewer';
2 |
3 | export { ImageViewer };
4 |
--------------------------------------------------------------------------------
/src/components/ListRow/ListRow.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import {
3 | View,
4 | Dimensions,
5 | StyleSheet,
6 | ViewStyle,
7 | TouchableOpacity,
8 | } from 'react-native';
9 | import { DividerProps, Divider } from '../Divider';
10 | import { useProps } from './utils';
11 |
12 | const { width } = Dimensions.get('window');
13 |
14 | export type ContentType = string | React.ReactNode | null;
15 |
16 | export interface ListRowProps {
17 | left: ContentType;
18 | mid?: ContentType;
19 | right?: ContentType;
20 |
21 | disabled?: boolean;
22 | onPress?: () => void;
23 | style?: ViewStyle;
24 |
25 | divider?: boolean;
26 | dividerProps?: DividerProps;
27 | }
28 |
29 | const ListRow: React.FC = (props) => {
30 | const {
31 | style,
32 | left,
33 | right,
34 | mid,
35 | onPress,
36 | divider = true,
37 | dividerProps,
38 | disabled = false,
39 | } = useProps(props);
40 |
41 | const handlePress = useCallback(() => {
42 | if (!disabled) {
43 | onPress && onPress();
44 | }
45 | }, []);
46 |
47 | return (
48 | <>
49 |
54 |
55 | {left}
56 | {mid}
57 |
58 | {right}
59 |
60 | {divider && }
61 | >
62 | );
63 | };
64 |
65 | const styles = StyleSheet.create({
66 | listRow: {
67 | width: '100%',
68 | backgroundColor: 'white',
69 | flexDirection: 'row',
70 | paddingHorizontal: 15,
71 | paddingVertical: 15,
72 | alignItems: 'center',
73 | justifyContent: 'space-between',
74 | },
75 | leftContent: {
76 | flexDirection: 'row',
77 | },
78 | });
79 |
80 | export default ListRow;
81 |
--------------------------------------------------------------------------------
/src/components/ListRow/index.tsx:
--------------------------------------------------------------------------------
1 | import ListRow from './ListRow';
2 |
3 | export { ListRow };
4 |
--------------------------------------------------------------------------------
/src/components/ListRow/utils.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text } from 'react-native';
3 | import { ContentType, ListRowProps } from './ListRow';
4 |
5 | const componentByType = (content: ContentType): React.ReactNode | null => {
6 | if (content === null) return null;
7 | if (typeof content === 'string') {
8 | return {content};
9 | }
10 | return content;
11 | };
12 |
13 | export const useProps = (props: ListRowProps) => {
14 | const { left, mid, right } = props;
15 | return {
16 | ...props,
17 | left: componentByType(left),
18 | mid: componentByType(mid),
19 | right: componentByType(right),
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/Loading/CircleLoading.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import Animated, {
3 | interpolate,
4 | useAnimatedStyle,
5 | useSharedValue,
6 | withRepeat,
7 | withTiming,
8 | } from 'react-native-reanimated';
9 | import Svg, { Circle } from 'react-native-svg';
10 |
11 | const AnimationSvg = Animated.createAnimatedComponent(Svg);
12 |
13 | interface CircleLoadingProps {
14 | size?: number;
15 | circle?: number;
16 | color?: string;
17 | duration?: number;
18 | }
19 |
20 | const CircleLoading: React.FC = (props) => {
21 | const { size = 30, circle = 120, color = '#1e90ff', duration = 1000 } = props;
22 | const progress = useSharedValue(0);
23 |
24 | const all = size * 2 * Math.PI * 0.8;
25 | const dashPath = ((Math.PI * 2) / 360) * circle * size * 0.8;
26 |
27 | useEffect(() => {
28 | progress.value = withRepeat(
29 | withTiming(1, {
30 | duration,
31 | }),
32 | -1,
33 | false
34 | );
35 | }, []);
36 |
37 | const animationStyle = useAnimatedStyle(() => {
38 | const degree = interpolate(progress.value, [0, 1], [0, 360]);
39 | return {
40 | transform: [
41 | {
42 | rotateZ: `${degree}deg`,
43 | },
44 | ],
45 | };
46 | });
47 |
48 | return (
49 |
50 |
59 |
60 | );
61 | };
62 |
63 | export default CircleLoading;
64 |
--------------------------------------------------------------------------------
/src/components/Loading/GrowLoading.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import Animated, {
3 | useSharedValue,
4 | withRepeat,
5 | withTiming,
6 | useAnimatedStyle,
7 | interpolate,
8 | useAnimatedProps,
9 | } from 'react-native-reanimated';
10 | import Svg, { Circle } from 'react-native-svg';
11 |
12 | const AnimationSvg = Animated.createAnimatedComponent(Svg);
13 | const AnimationCircle = Animated.createAnimatedComponent(Circle);
14 |
15 | interface GrowLoadingProps {
16 | size?: number;
17 | color?: string;
18 | duration?: number;
19 | }
20 |
21 | const GrowLoading: React.FC = (props) => {
22 | const { size = 30, color = '#1e90ff', duration = 2000 } = props;
23 | const progress = useSharedValue(0);
24 | const all = size * 2 * Math.PI * 0.8;
25 |
26 | useEffect(() => {
27 | progress.value = withRepeat(
28 | withTiming(1, {
29 | duration,
30 | }),
31 | -1,
32 | false
33 | );
34 | }, []);
35 |
36 | const animationStyle = useAnimatedStyle(() => {
37 | const degree = interpolate(progress.value, [0, 1], [0, 360]);
38 | return {
39 | transform: [
40 | {
41 | rotateZ: `${degree}deg`,
42 | },
43 | ],
44 | };
45 | });
46 |
47 | const animatedProps = useAnimatedProps(() => {
48 | const offset = interpolate(progress.value, [0, 1], [all, -all]);
49 | return {
50 | strokeDashoffset: offset,
51 | };
52 | });
53 |
54 | return (
55 |
56 |
66 |
67 | );
68 | };
69 |
70 | export default GrowLoading;
71 |
--------------------------------------------------------------------------------
/src/components/Loading/Loading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ActivityIndicator } from 'react-native';
3 |
4 | export interface LoadingProps {
5 | color?: string;
6 | size?: 'small' | 'large';
7 | animating?: boolean;
8 | }
9 |
10 | const Loading: React.FC = (props) => {
11 | const { color, size, animating = true } = props;
12 | return ;
13 | };
14 |
15 | export default Loading;
16 |
--------------------------------------------------------------------------------
/src/components/Loading/LoadingTitle.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ActivityIndicator,
4 | View,
5 | StyleSheet,
6 | Text,
7 | TextStyle,
8 | } from 'react-native';
9 | import { LoadingProps } from './Loading';
10 |
11 | interface LoadingTitleProps extends LoadingProps {
12 | title?: string;
13 | titleStyle?: TextStyle;
14 | }
15 |
16 | const LoadingTitle: React.FC = (props) => {
17 | const { title = '', titleStyle, ...options } = props;
18 |
19 | return (
20 |
21 |
22 | {title.length > 0 && (
23 |
24 | {title}
25 |
26 | )}
27 |
28 | );
29 | };
30 |
31 | const styles = StyleSheet.create({
32 | loading: {
33 | width: 100,
34 | height: 100,
35 | borderRadius: 10,
36 | justifyContent: 'center',
37 | alignItems: 'center',
38 | backgroundColor: 'rgba(0,0,0,0.3)',
39 | },
40 | titleContainer: {
41 | position: 'absolute',
42 | bottom: 10,
43 | left: 0,
44 | right: 0,
45 | justifyContent: 'center',
46 | alignItems: 'center',
47 | },
48 | });
49 |
50 | export default LoadingTitle;
51 |
--------------------------------------------------------------------------------
/src/components/Loading/LoadingUtil.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { overlayRef } from '../Overlay';
3 | import Loading from './Loading';
4 | import { OpacityContainer } from '../Overlay';
5 | import { StyleSheet } from 'react-native';
6 |
7 | export const LoadingUtil = {
8 | key: 'global-loading',
9 | template: () => {
10 | return (
11 |
12 |
13 |
14 | );
15 | },
16 | show: () => {
17 | overlayRef.current?.add(LoadingUtil.template(), LoadingUtil.key);
18 | },
19 | hide: () => overlayRef.current?.remove(LoadingUtil.key),
20 | isExist: () => overlayRef.current?.isExist(LoadingUtil.key),
21 | };
22 |
23 | const styles = StyleSheet.create({
24 | container: {
25 | justifyContent: 'center',
26 | alignItems: 'center',
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/src/components/Loading/ScaleLoading.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo } from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import Animated, {
4 | useAnimatedStyle,
5 | useSharedValue,
6 | withDelay,
7 | withRepeat,
8 | withTiming,
9 | } from 'react-native-reanimated';
10 |
11 | interface ScaleLoadingProps {
12 | color?: string;
13 | duration?: number;
14 | size?: number;
15 | number?: number;
16 | }
17 |
18 | const ScaleLoading: React.FC = (props) => {
19 | const { color = '#1e90ff', size = 8, number = 3, duration = 1000 } = props;
20 |
21 | return (
22 |
23 | {new Array(number).fill(0).map((_, index) => {
24 | return (
25 |
29 | );
30 | })}
31 |
32 | );
33 | };
34 |
35 | interface ScaleLoadingItemProps {
36 | index: number;
37 | color: string;
38 | size: number;
39 | duration: number;
40 | }
41 |
42 | const ScaleLoadingItem: React.FC = (props) => {
43 | const { index, color, size, duration } = props;
44 |
45 | const scale = useSharedValue(1);
46 |
47 | useEffect(() => {
48 | scale.value = withDelay(
49 | 300 * index,
50 | withRepeat(withTiming(0, { duration }), -1, true)
51 | );
52 | });
53 |
54 | const dot = useMemo(() => {
55 | return {
56 | width: size,
57 | height: size,
58 | borderRadius: size / 2,
59 | marginHorizontal: 4,
60 | backgroundColor: color,
61 | };
62 | }, [size]);
63 |
64 | const animationStyle = useAnimatedStyle(() => {
65 | return {
66 | transform: [
67 | {
68 | scale: scale.value,
69 | },
70 | ],
71 | };
72 | });
73 |
74 | return ;
75 | };
76 |
77 | const styles = StyleSheet.create({
78 | container: {
79 | flexDirection: 'row',
80 | },
81 | });
82 |
83 | export default ScaleLoading;
84 |
--------------------------------------------------------------------------------
/src/components/Loading/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo } from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import Animated, {
4 | interpolateColor,
5 | useAnimatedStyle,
6 | useSharedValue,
7 | withRepeat,
8 | withTiming,
9 | SharedValue,
10 | } from 'react-native-reanimated';
11 |
12 | interface SpinnerProps {
13 | activeColor?: string;
14 | inactiveColor?: string;
15 | duration?: number;
16 | size?: number;
17 | number?: number;
18 | }
19 |
20 | const Spinner: React.FC = (props) => {
21 | const {
22 | activeColor = '#1e90ff',
23 | inactiveColor = 'white',
24 | duration = 1000,
25 | size = 8,
26 | number = 3,
27 | } = props;
28 | const currentIndex = useSharedValue(0);
29 |
30 | useEffect(() => {
31 | currentIndex.value = withRepeat(
32 | withTiming(number - 1, { duration }),
33 | -1,
34 | false
35 | );
36 | }, []);
37 |
38 | return (
39 |
40 | {new Array(number).fill(0).map((_, index) => {
41 | return (
42 |
46 | );
47 | })}
48 |
49 | );
50 | };
51 |
52 | interface SpinnerItemProps {
53 | currentIndex: SharedValue;
54 | index: number;
55 | inactiveColor: string;
56 | activeColor: string;
57 | size: number;
58 | }
59 |
60 | const SpinnerItem: React.FC = (props) => {
61 | const { currentIndex, index, inactiveColor, activeColor, size } = props;
62 |
63 | const dot = useMemo(() => {
64 | return {
65 | width: size,
66 | height: size,
67 | borderRadius: size / 2,
68 | marginHorizontal: 4,
69 | };
70 | }, [size]);
71 |
72 | const animationStyle = useAnimatedStyle(() => {
73 | return {
74 | backgroundColor: interpolateColor(
75 | currentIndex.value,
76 | [index - 1, index, index + 1],
77 | [inactiveColor, activeColor, inactiveColor]
78 | ),
79 | };
80 | });
81 |
82 | return ;
83 | };
84 |
85 | const styles = StyleSheet.create({
86 | container: {
87 | flexDirection: 'row',
88 | },
89 | });
90 |
91 | export default Spinner;
92 |
--------------------------------------------------------------------------------
/src/components/Loading/index.tsx:
--------------------------------------------------------------------------------
1 | import Loading from './Loading';
2 | import LoadingTitle from './LoadingTitle';
3 | import Spinner from './Spinner';
4 | import CircleLoading from './CircleLoading';
5 | import GrowLoading from './GrowLoading';
6 | import ScaleLoading from './ScaleLoading';
7 |
8 | import { LoadingUtil } from './LoadingUtil';
9 |
10 | export {
11 | Loading,
12 | LoadingUtil,
13 | LoadingTitle,
14 | Spinner,
15 | CircleLoading,
16 | GrowLoading,
17 | ScaleLoading,
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/NestedTabView/RefreshController.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useWindowDimensions } from 'react-native';
3 | import Animated, {
4 | interpolate,
5 | useAnimatedStyle,
6 | } from 'react-native-reanimated';
7 | import { RefreshControllerProps } from './type';
8 | import { RefreshControllerContext } from './hooks';
9 |
10 | const RefreshController: React.FC = (props) => {
11 | const { scrollOffset, refreshStatus, triggerHeight, children } = props;
12 | const { height: HEIGHT } = useWindowDimensions();
13 |
14 | const refreshBar = useAnimatedStyle(() => {
15 | return {
16 | height: interpolate(scrollOffset.value, [0, HEIGHT], [0, HEIGHT / 2]),
17 | opacity: interpolate(
18 | scrollOffset.value,
19 | [0, triggerHeight / 3, triggerHeight],
20 | [0, 0, 1]
21 | ),
22 | };
23 | }, []);
24 |
25 | return (
26 |
33 |
49 | {children}
50 |
51 |
52 | );
53 | };
54 |
55 | export default RefreshController;
56 |
--------------------------------------------------------------------------------
/src/components/NestedTabView/index.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollViewProps, FlatListProps } from 'react-native';
2 | import Animated from 'react-native-reanimated';
3 |
4 | import NestedTabView from './NestedTabView';
5 | import NestedScene from './NestedScene';
6 | import NestedRefresh from './NestedRefresh';
7 |
8 | const NestedScrollView: React.FC = (props: any) => {
9 | const AnimateScrollView = Animated.ScrollView;
10 |
11 | return ;
12 | };
13 |
14 | const NestedFlatList: React.FC> = (props: any) => {
15 | const AnimateFlatList = Animated.FlatList;
16 |
17 | return ;
18 | };
19 |
20 | // const createNestedComponent = (ScrollableComponent: any) => {
21 | // const AnimateList = Animated.createAnimatedComponent(ScrollableComponent);
22 | // return forwardRef((props: any, ref) => {
23 | // return ;
24 | // });
25 | // };
26 |
27 | const Nested = {
28 | ScrollView: NestedScrollView,
29 | FlatList: NestedFlatList,
30 | };
31 |
32 | export { NestedTabView, Nested, NestedRefresh };
33 |
--------------------------------------------------------------------------------
/src/components/NestedTabView/util.ts:
--------------------------------------------------------------------------------
1 | import { scrollTo, AnimatedRef } from 'react-native-reanimated';
2 |
3 | export const mscrollTo = (
4 | animatedRef: AnimatedRef,
5 | x: number,
6 | y: number,
7 | animated: boolean
8 | ) => {
9 | 'worklet';
10 | if (!animatedRef) return;
11 | scrollTo(animatedRef, x, y, animated);
12 | };
13 |
14 | export const mergeProps = (
15 | restProps: any,
16 | headerHeight: number,
17 | childMinHeight: number
18 | ) => {
19 | restProps.style = {
20 | ...restProps.style,
21 | };
22 | restProps.contentContainerStyle = {
23 | ...restProps.contentContainerStyle,
24 | paddingTop: headerHeight,
25 | minHeight: childMinHeight,
26 | };
27 | restProps.scrollIndicatorInsets = {
28 | ...restProps.scrollIndicatorInsets,
29 | top: headerHeight,
30 | };
31 | return restProps;
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/Overlay/Container/NormalContainer.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * if use this componet wrapper overlay componet
3 | * onAppear will be called when it mount,
4 | * onDisappear will be called when it unMount
5 | */
6 | import React, { forwardRef, useEffect } from 'react';
7 | import { View, StyleSheet } from 'react-native';
8 | import { BaseContainerProps } from './type';
9 |
10 | interface NormalContainerProps extends BaseContainerProps {}
11 |
12 | interface NormalContainerRef {}
13 |
14 | const NormalContainer = forwardRef(
15 | (props, ref) => {
16 | const { children, containerStyle, onAppear, onDisappear } = props;
17 |
18 | useEffect(() => {
19 | onAppear && onAppear();
20 | return () => {
21 | onDisappear && onDisappear();
22 | };
23 | }, [onAppear, onDisappear]);
24 |
25 | return (
26 |
27 | {children}
28 |
29 | );
30 | }
31 | );
32 |
33 | const styles = StyleSheet.create({
34 | overlay: {
35 | ...StyleSheet.absoluteFillObject,
36 | },
37 | container: {
38 | flex: 1,
39 | },
40 | });
41 |
42 | NormalContainer.displayName = 'NormalContainer';
43 |
44 | export default NormalContainer;
45 |
--------------------------------------------------------------------------------
/src/components/Overlay/Container/type.ts:
--------------------------------------------------------------------------------
1 | import { ViewStyle } from 'react-native';
2 | import { RootAnimationType } from '../RootViewAnimations';
3 |
4 | /**
5 | * All Overlay Container Must exntent this interface
6 | */
7 | export interface BaseContainerProps {
8 | /**
9 | * overlay show componet
10 | */
11 | children: React.ReactNode;
12 | /**
13 | * this innerKey equals overlay.tsx's key, it ensure the overlay can remove itself
14 | */
15 | readonly innerKey?: string;
16 | /**
17 | * containerStyle is the closest to children, you can set flex to control children's position
18 | */
19 | containerStyle?: ViewStyle;
20 |
21 | /**
22 | * will be called after the overlay mount
23 | */
24 | onAppear?: () => void;
25 | /**
26 | * will be called after the overlay unmount
27 | */
28 | onDisappear?: () => void;
29 | /**
30 | * 'none': overlay can't response event, can click view under overlay
31 | * 'auto': overlay response evet
32 | */
33 | pointerEvents?: 'none' | 'auto';
34 | /**
35 | * config root view pointerEvents
36 | * 'none': overlay can't response event, can click view under overlay
37 | * 'auto': overlay response evet
38 | */
39 | rootPointerEvents?: 'none' | 'auto';
40 | /**
41 | * config root view animation
42 | */
43 | rootAnimation?: RootAnimationType;
44 | }
45 |
46 | /**
47 | * Animation Overlay Container All has below props
48 | */
49 | export interface AnimationContainerProps extends BaseContainerProps {
50 | /**
51 | * need mask to cover rest of window
52 | */
53 | mask?: boolean;
54 | /**
55 | * animation duration time
56 | * ms
57 | */
58 | duration?: number;
59 | /**
60 | * If modal equal true, the overlay must be remove by call remove function
61 | * If modal equal false, the overlay can be close by click mask
62 | */
63 | modal?: boolean;
64 | /**
65 | * will be called after click mask
66 | */
67 | onClickMask?: () => void;
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/Overlay/OverlayUtil.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { OverlayRef } from './Overlay';
3 |
4 | /**
5 | * Must set at top
6 | * OverlayUtil is just another way to invoke useOverlay
7 | * Can be used at out of FunctionComponent
8 | */
9 | export const overlayRef = React.createRef();
10 |
11 | export const OverlayUtil = {
12 | add: (children: React.ReactNode, key?: string) =>
13 | overlayRef.current?.add(children, key),
14 | remove: () => overlayRef.current?.remove(),
15 | removeAll: () => overlayRef.current?.removeAll(),
16 | isExist: (key: string) => overlayRef.current?.isExist(key),
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/Overlay/index.tsx:
--------------------------------------------------------------------------------
1 | import Overlay, { useOverlay } from './Overlay';
2 |
3 | import NormalContainer from './Container/NormalContainer';
4 | import OpacityContainer, {
5 | OpacityContainerRef,
6 | } from './Container/OpacityContainer';
7 | import TranslateContainer, {
8 | TranslateContainerRef,
9 | } from './Container/TranslateContainer';
10 | import DrawerContainer, {
11 | DrawerContainerRef,
12 | } from './Container/DrawerContainer';
13 | import ScaleContainer from './Container/ScaleContainer';
14 |
15 | import { BaseContainerProps, AnimationContainerProps } from './Container/type';
16 |
17 | import { overlayRef, OverlayUtil } from './OverlayUtil';
18 |
19 | export {
20 | Overlay,
21 | useOverlay,
22 | overlayRef,
23 | OverlayUtil,
24 | NormalContainer,
25 | OpacityContainer,
26 | TranslateContainer,
27 | DrawerContainer,
28 | ScaleContainer,
29 | TranslateContainerRef,
30 | OpacityContainerRef,
31 | BaseContainerProps,
32 | AnimationContainerProps,
33 | DrawerContainerRef,
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/PageView/SinglePage.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo, useState } from 'react';
2 | import { View } from 'react-native';
3 | import {
4 | SharedValue,
5 | runOnJS,
6 | useAnimatedReaction,
7 | } from 'react-native-reanimated';
8 |
9 | interface SinglePageProps {
10 | children: React.ReactNode;
11 | contentSize: number;
12 | currentIndex: SharedValue;
13 | index: number;
14 | lazy: boolean;
15 | lazyPreloadNumber: number;
16 | pageMargin: number;
17 | isHorizontal: boolean;
18 | }
19 |
20 | const SinglePage: React.FC = (props) => {
21 | const {
22 | children,
23 | contentSize,
24 | currentIndex,
25 | index,
26 | lazy,
27 | lazyPreloadNumber,
28 | pageMargin,
29 | isHorizontal,
30 | } = props;
31 | const [load, setLoad] = useState(() => {
32 | if (!lazy) return true;
33 | return (
34 | index >= currentIndex.value - lazyPreloadNumber &&
35 | index <= currentIndex.value + lazyPreloadNumber
36 | );
37 | });
38 |
39 | useAnimatedReaction(
40 | () => currentIndex.value,
41 | (value) => {
42 | if (!lazy) return;
43 | if (!!load) return;
44 | const canLoad =
45 | index >= value - lazyPreloadNumber &&
46 | index <= value + lazyPreloadNumber;
47 | if (!canLoad) return;
48 | runOnJS(setLoad)(canLoad);
49 | }
50 | );
51 |
52 | const margin = index === 0 ? 0 : pageMargin;
53 |
54 | const directionStyle = useMemo(() => {
55 | return isHorizontal
56 | ? {
57 | width: contentSize,
58 | marginLeft: margin,
59 | }
60 | : {
61 | height: contentSize,
62 | marginTop: margin,
63 | };
64 | }, [isHorizontal]);
65 |
66 | if (!children) return ;
67 |
68 | return {!!load ? children : null};
69 | };
70 |
71 | export default SinglePage;
72 |
--------------------------------------------------------------------------------
/src/components/PageView/hook.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dimensions } from 'react-native';
3 | import { PageViewProps, PageViewVerifyProps } from './type';
4 |
5 | const { width, height } = Dimensions.get('window');
6 |
7 | export const useVerifyProps = (props: PageViewProps): PageViewVerifyProps => {
8 | const { children, style, pageMargin = 0, orientation = 'horizontal' } = props;
9 | const pageSize = React.Children.count(children);
10 | if (pageSize === 0) {
11 | throw new Error('PageView must be contains at least one chid');
12 | }
13 | if (orientation !== 'horizontal' && orientation !== 'vertical') {
14 | throw new Error('orientation only support horizontal or vertical');
15 | }
16 |
17 | let contentSize: number = orientation === 'horizontal' ? width : height;
18 | if (orientation === 'horizontal') {
19 | if (style && style.width) {
20 | if (typeof style.width === 'number') {
21 | contentSize = style.width;
22 | } else {
23 | throw new Error('PageView width only support number');
24 | }
25 | }
26 | } else {
27 | if (style && style.height) {
28 | if (typeof style.height === 'number') {
29 | contentSize = style.height;
30 | } else {
31 | throw new Error('PageView height only support number');
32 | }
33 | }
34 | }
35 |
36 | const snapPoints = new Array(pageSize)
37 | .fill(0)
38 | .map((_, index) => index * contentSize + index * pageMargin);
39 |
40 | return {
41 | ...props,
42 | orientation,
43 | pageSize,
44 | contentSize,
45 | snapPoints,
46 | };
47 | };
48 |
--------------------------------------------------------------------------------
/src/components/PageView/index.tsx:
--------------------------------------------------------------------------------
1 | import PageView from './PageView';
2 | export { PageViewRef, PageViewProps, PageStateType } from './type';
3 |
4 | export { PageView };
5 |
--------------------------------------------------------------------------------
/src/components/PageView/type.ts:
--------------------------------------------------------------------------------
1 | import { ViewStyle } from 'react-native';
2 |
3 | export interface PageViewProps {
4 | children: React.ReactNode;
5 |
6 | style?: ViewStyle;
7 | initialPage?: number;
8 | scrollEnabled?: boolean;
9 | bounces?: boolean;
10 | gestureBack?: boolean;
11 | pageMargin?: number;
12 | orientation?: 'horizontal' | 'vertical';
13 |
14 | lazy?: boolean;
15 | lazyPreloadNumber?: number;
16 |
17 | onPageScroll?: (translate: number) => void;
18 | onPageSelected?: (currentPage: number) => void;
19 | onPageScrollStateChanged?: (state: PageStateType) => void;
20 | }
21 |
22 | export interface PageViewRef {
23 | setPage: (index: number) => void;
24 | setPageWithoutAnimation: (index: number) => void;
25 | setScrollEnabled: (scrollEnabled: boolean) => void;
26 | getCurrentPage: () => number;
27 | }
28 |
29 | export const DURATION = 350;
30 |
31 | export type PageStateType = 'dragging' | 'settling' | 'idle';
32 |
33 | export interface PageViewVerifyProps extends PageViewProps {
34 | pageSize: number;
35 | contentSize: number;
36 | snapPoints: number[];
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/Pagination/Dot.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, ViewStyle } from 'react-native';
3 | import DotItem from './DotItem';
4 | import { usePagination } from './Pagination';
5 |
6 | interface DotProps {
7 | size?: number;
8 | activeColor?: string;
9 | inActiveColor?: string;
10 | shape?: 'circle' | 'cube';
11 | style?: ViewStyle;
12 | direction?: 'row' | 'column';
13 | }
14 |
15 | const Dot: React.FC = (props) => {
16 | const {
17 | size,
18 | activeColor,
19 | inActiveColor,
20 | shape,
21 | style,
22 | direction = 'row',
23 | } = props;
24 | const { total } = usePagination();
25 |
26 | return (
27 |
28 | {new Array(total).fill(0).map((_, index: number) => {
29 | return (
30 |
34 | );
35 | })}
36 |
37 | );
38 | };
39 |
40 | export default Dot;
41 |
--------------------------------------------------------------------------------
/src/components/Pagination/DotItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { ViewStyle } from 'react-native';
3 | import Animated, {
4 | Extrapolation,
5 | interpolate,
6 | interpolateColor,
7 | useAnimatedStyle,
8 | } from 'react-native-reanimated';
9 | import { usePagination } from './Pagination';
10 |
11 | interface DotItemProps {
12 | index: number;
13 | size?: number;
14 | activeColor?: string;
15 | inActiveColor?: string;
16 | shape?: 'circle' | 'cube';
17 | style?: ViewStyle;
18 | }
19 |
20 | const DotItem: React.FC = (props) => {
21 | const {
22 | index,
23 | shape = 'circle',
24 | size = 8,
25 | activeColor = 'white',
26 | style,
27 | inActiveColor = 'white',
28 | } = props;
29 | const { currentIndex } = usePagination();
30 |
31 | const animationStyle = useAnimatedStyle(() => {
32 | let value = 0;
33 | if (typeof currentIndex === 'number') {
34 | value = currentIndex;
35 | } else {
36 | value = currentIndex.value;
37 | }
38 |
39 | const inputRange = [index - 1, index, index + 1];
40 | const opacity = interpolate(
41 | value,
42 | inputRange,
43 | [0.5, 1, 0.5],
44 | Extrapolation.CLAMP
45 | );
46 | const scale = interpolate(
47 | value,
48 | inputRange,
49 | [1, 1.25, 1],
50 | Extrapolation.CLAMP
51 | );
52 |
53 | return {
54 | opacity,
55 | transform: [{ scale }],
56 | backgroundColor: interpolateColor(value, inputRange, [
57 | inActiveColor,
58 | activeColor,
59 | inActiveColor,
60 | ]),
61 | };
62 | });
63 |
64 | const borderRadius = useMemo(() => {
65 | if (shape === 'cube') {
66 | return 0;
67 | }
68 | if (shape === 'circle') {
69 | return size / 2;
70 | }
71 | return 0;
72 | }, [shape, size]);
73 |
74 | const propStyle = useMemo(() => {
75 | return {
76 | width: size,
77 | height: size,
78 | borderRadius: borderRadius,
79 | margin: size / 2,
80 | };
81 | }, [size, borderRadius]);
82 |
83 | return ;
84 | };
85 |
86 | export default DotItem;
87 |
--------------------------------------------------------------------------------
/src/components/Pagination/Pagination.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useMemo } from 'react';
2 | import { View } from 'react-native';
3 | import { SharedValue } from 'react-native-reanimated';
4 |
5 | interface PaginationRef {
6 | currentIndex: number | SharedValue;
7 | total: number;
8 | }
9 |
10 | export const PaginationContext = createContext({} as PaginationRef);
11 | export const usePagination = () => useContext(PaginationContext);
12 |
13 | interface PaginationProps {
14 | currentIndex: number | SharedValue;
15 | total: number;
16 | position?: 'left' | 'center' | 'right';
17 | children: React.ReactNode;
18 | }
19 |
20 | const Pagination: React.FC = (props) => {
21 | const { currentIndex, total, position = 'center', children } = props;
22 |
23 | const alignItemsType = useMemo(() => {
24 | if (position === 'left') return 'flex-start';
25 | if (position === 'center') return 'center';
26 | if (position === 'right') return 'flex-end';
27 | return 'center';
28 | }, [position]);
29 |
30 | return (
31 |
32 |
38 | {children}
39 |
40 |
41 | );
42 | };
43 |
44 | export default Pagination;
45 |
--------------------------------------------------------------------------------
/src/components/Pagination/Percent.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { View, StyleSheet, Text, TextStyle } from 'react-native';
3 | import { useAnimatedReaction, runOnJS } from 'react-native-reanimated';
4 | import { usePagination } from './Pagination';
5 |
6 | interface PercentProps {
7 | style?: TextStyle;
8 | }
9 |
10 | const Percent: React.FC = (props) => {
11 | const { style } = props;
12 | const { currentIndex, total } = usePagination();
13 | const [index, setIndex] = useState(() => {
14 | if (typeof currentIndex === 'number') {
15 | return currentIndex;
16 | }
17 |
18 | return currentIndex.value;
19 | });
20 |
21 | useEffect(() => {
22 | if (typeof currentIndex === 'number') {
23 | setIndex(currentIndex);
24 | }
25 | }, [currentIndex]);
26 |
27 | useAnimatedReaction(
28 | () => currentIndex,
29 | (currentIndex) => {
30 | if (typeof currentIndex !== 'number') {
31 | runOnJS(setIndex)(currentIndex.value);
32 | }
33 | }
34 | );
35 |
36 | return (
37 |
38 | {index}
39 | /
40 | {total}
41 |
42 | );
43 | };
44 |
45 | const styles = StyleSheet.create({
46 | container: {
47 | flexDirection: 'row',
48 | },
49 | });
50 |
51 | export default Percent;
52 |
--------------------------------------------------------------------------------
/src/components/Pagination/index.tsx:
--------------------------------------------------------------------------------
1 | import Pagination from './Pagination';
2 | import Dot from './Dot';
3 | import Percent from './Percent';
4 |
5 | export { Pagination, Dot, Percent };
6 |
--------------------------------------------------------------------------------
/src/components/Picker/PickerItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import Animated, {
4 | Extrapolation,
5 | interpolate,
6 | useAnimatedStyle,
7 | } from 'react-native-reanimated';
8 | import { PickerItemProps } from './type';
9 |
10 | const PickerItem: React.FC = (props) => {
11 | const { translateY, children, options, paningIndex, item } = props;
12 |
13 | const style = useAnimatedStyle(() => {
14 | const visibleRotateX = [50, 30, 20, 0, -20, -30, -50];
15 | const visibleOffsetY = [-15, -5, 0, 0, 0, 5, 15];
16 | const visibleIndex = [
17 | item - 3,
18 | item - 2,
19 | item - 1,
20 | item,
21 | item + 1,
22 | item + 2,
23 | item + 3,
24 | ];
25 | const rotateX = interpolate(
26 | paningIndex.value,
27 | visibleIndex,
28 | visibleRotateX,
29 | Extrapolation.CLAMP
30 | );
31 | const offsetY = interpolate(
32 | paningIndex.value,
33 | visibleIndex,
34 | visibleOffsetY
35 | );
36 |
37 | return {
38 | opacity: interpolate(
39 | paningIndex.value,
40 | visibleIndex,
41 | [0.2, 0.4, 0.6, 1, 0.6, 0.4, 0.2]
42 | ),
43 | transform: [
44 | { translateY: translateY.value + item * options.itemHeight + offsetY },
45 | { perspective: 1500 },
46 | { rotateX: `${rotateX}deg` },
47 | {
48 | scaleX: interpolate(
49 | paningIndex.value,
50 | visibleIndex,
51 | [0.9, 0.92, 0.95, 1, 0.95, 0.92, 0.9],
52 | Extrapolation.CLAMP
53 | ),
54 | },
55 | {
56 | scaleY: interpolate(
57 | paningIndex.value,
58 | visibleIndex,
59 | [0.9, 0.92, 0.95, 1, 0.95, 0.92, 0.9],
60 | Extrapolation.CLAMP
61 | ),
62 | },
63 | ],
64 | };
65 | });
66 |
67 | return (
68 |
77 | {children}
78 |
79 | );
80 | };
81 |
82 | const styles = StyleSheet.create({
83 | container: {
84 | width: '100%',
85 | justifyContent: 'center',
86 | alignItems: 'center',
87 | position: 'absolute',
88 | },
89 | });
90 |
91 | export default PickerItem;
92 |
--------------------------------------------------------------------------------
/src/components/Picker/__test__/hook.test.ts:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks';
2 | import { Easing } from 'react-native-reanimated';
3 | import { useProps, useInitialValue } from '../utils';
4 |
5 | describe('Test:Picker->hook/useProps', () => {
6 | it('dataSource empty', () => {
7 | const renderItem = () => null;
8 |
9 | const test1 = {
10 | renderItem,
11 | dataSource: [],
12 | };
13 | expect(() => useProps(test1)).toThrow("dataSource can't be empty");
14 | });
15 |
16 | it('default options', () => {
17 | const renderItem = () => null;
18 |
19 | const { options } = useProps({
20 | renderItem,
21 | dataSource: [1],
22 | });
23 |
24 | expect(options.itemHeight).toEqual(30);
25 | expect(options.maxRender).toEqual(2);
26 | });
27 |
28 | it('mix options', () => {
29 | const renderItem = () => null;
30 |
31 | const { options } = useProps({
32 | renderItem,
33 | dataSource: [1],
34 | options: {
35 | itemHeight: 50,
36 | maxRender: 3,
37 | },
38 | });
39 |
40 | expect(options.itemHeight).toEqual(50);
41 | expect(options.maxRender).toEqual(3);
42 | });
43 | });
44 |
45 | describe('Test:Picker->hook/useInitialValue', () => {
46 | it('base', () => {
47 | renderHook(() => {
48 | const timingOptionsMock = {
49 | duration: 1000,
50 | easing: Easing.bezier(0.22, 1, 0.36, 1),
51 | };
52 |
53 | const { translateY, offset, currentIndex, snapPoints, timingOptions } =
54 | useInitialValue(
55 | {
56 | itemHeight: 30,
57 | maxRender: 2,
58 | },
59 | [1, 2, 3]
60 | );
61 |
62 | expect(translateY.value).toEqual(2 * 30);
63 | expect(offset.value).toEqual(0);
64 | expect(currentIndex.value).toEqual(0);
65 | expect(snapPoints).toEqual([60, 30, 0]);
66 | expect(timingOptions).toEqual(timingOptionsMock);
67 | });
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/src/components/Picker/index.tsx:
--------------------------------------------------------------------------------
1 | import Picker from './Picker';
2 |
3 | export { Picker };
4 |
--------------------------------------------------------------------------------
/src/components/Picker/type.ts:
--------------------------------------------------------------------------------
1 | import { ViewStyle } from 'react-native';
2 | import { SharedValue, DerivedValue } from 'react-native-reanimated';
3 |
4 | export interface PickOptions {
5 | itemHeight?: number;
6 | maxRender?: number;
7 | }
8 |
9 | export interface PickDefaultOptions {
10 | itemHeight: number;
11 | maxRender: number;
12 | }
13 |
14 | export interface PickerProps {
15 | dataSource: any[];
16 | style?: ViewStyle;
17 | renderItem: (item: any, index: number) => React.ReactNode;
18 | onChange?: (item: any) => void;
19 | options?: PickOptions;
20 | }
21 |
22 | export interface PickerDefaultProps {
23 | dataSource: any[];
24 | style?: ViewStyle;
25 | renderItem: (item: any, index: number) => React.ReactNode;
26 | onChange?: (item: any) => void;
27 | options: PickDefaultOptions;
28 | }
29 |
30 | export interface PickerItemProps {
31 | index: number;
32 | currentIndex: SharedValue;
33 | translateY: SharedValue;
34 | options: PickDefaultOptions;
35 | paningIndex: DerivedValue;
36 | item: number;
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/Picker/utils.ts:
--------------------------------------------------------------------------------
1 | import { Easing, useSharedValue } from 'react-native-reanimated';
2 | import { PickerProps, PickerDefaultProps, PickDefaultOptions } from './type';
3 |
4 | /**
5 | * 处理参数以及默认值
6 | * @param props
7 | * @returns
8 | */
9 | const useProps = (props: PickerProps): PickerDefaultProps => {
10 | const { options, dataSource } = props;
11 |
12 | if (dataSource.length === 0) {
13 | throw new Error("dataSource can't be empty");
14 | }
15 |
16 | const defaultOptions = {
17 | itemHeight: 0,
18 | maxRender: 0,
19 | };
20 |
21 | defaultOptions.itemHeight = options?.itemHeight || 30;
22 | defaultOptions.maxRender = options?.maxRender || 2;
23 |
24 | return { ...props, options: defaultOptions };
25 | };
26 |
27 | /**
28 | * 初始化必须参数
29 | * @param options
30 | * @returns
31 | */
32 | const useInitialValue = (options: PickDefaultOptions) => {
33 | const defaultY = options.itemHeight * options.maxRender;
34 | const translateY = useSharedValue(defaultY);
35 | const offset = useSharedValue(0);
36 | const currentIndex = useSharedValue(0);
37 |
38 | const timingOptions = {
39 | duration: 1000,
40 | easing: Easing.bezier(0.22, 1, 0.36, 1),
41 | };
42 |
43 | return {
44 | translateY,
45 | offset,
46 | currentIndex,
47 | timingOptions,
48 | };
49 | };
50 |
51 | export { useProps, useInitialValue };
52 |
--------------------------------------------------------------------------------
/src/components/Popover/Popover.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
2 | import { View, ViewStyle } from 'react-native';
3 | import { useOverlay } from '../Overlay';
4 | import PopoverContainer from './PopoverContainer';
5 | import { Placement, ArrowPlacement } from './type';
6 |
7 | interface PopoverProps {
8 | children: ReactNode;
9 |
10 | modal: boolean;
11 | content: React.ReactNode;
12 |
13 | style?: ViewStyle;
14 | arrowPosition?: ArrowPlacement;
15 | placement?: Placement;
16 | arrowSize?: number;
17 | arrowColor?: string;
18 |
19 | onPressMask?: () => void;
20 | }
21 |
22 | const Popover: React.FC = (props) => {
23 | const {
24 | children,
25 | modal,
26 | content,
27 | style,
28 | onPressMask,
29 | arrowPosition = 'center',
30 | placement = 'top',
31 | arrowSize = 10,
32 | arrowColor = 'white',
33 | } = props;
34 | const aref = useRef(null);
35 | const { add, remove } = useOverlay();
36 |
37 | useEffect(() => {
38 | if (modal) {
39 | show();
40 | } else {
41 | remove('popover');
42 | }
43 | }, [modal]);
44 |
45 | const show = useCallback(() => {
46 | aref?.current?.measure((x, y, width, height, pageX, pageY) => {
47 | console.log({
48 | x,
49 | y,
50 | width,
51 | height,
52 | pageX,
53 | pageY,
54 | });
55 |
56 | add(
57 |
69 | {content}
70 | ,
71 | 'popover'
72 | );
73 | });
74 | }, [arrowPosition, placement]);
75 |
76 | return (
77 |
78 | {children}
79 |
80 | );
81 | };
82 |
83 | export default Popover;
84 |
--------------------------------------------------------------------------------
/src/components/Popover/index.tsx:
--------------------------------------------------------------------------------
1 | import Popover from './Popover';
2 |
3 | export { Popover };
4 |
--------------------------------------------------------------------------------
/src/components/Popover/type.ts:
--------------------------------------------------------------------------------
1 | export interface Position {
2 | x: number;
3 | y: number;
4 | width: number;
5 | height: number;
6 | pageX: number;
7 | pageY: number;
8 | }
9 |
10 | export interface Layout {
11 | x: number;
12 | y: number;
13 | width: number;
14 | height: number;
15 | }
16 |
17 | export type Placement =
18 | | 'top'
19 | | 'top-start'
20 | | 'top-end'
21 | | 'right'
22 | | 'right-start'
23 | | 'right-end'
24 | | 'bottom'
25 | | 'bottom-start'
26 | | 'bottom-end'
27 | | 'left'
28 | | 'left-start'
29 | | 'left-end';
30 | export type ArrowPlacement = 'start' | 'end' | 'center';
31 |
--------------------------------------------------------------------------------
/src/components/Progress/index.tsx:
--------------------------------------------------------------------------------
1 | import Progress from './Progress';
2 | import CircleProgress from './CircleProgress';
3 |
4 | export { Progress, CircleProgress };
5 |
--------------------------------------------------------------------------------
/src/components/RefreshControl/BottomContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import { StyleSheet, Dimensions } from 'react-native';
3 | import Animated, {
4 | interpolate,
5 | useAnimatedStyle,
6 | } from 'react-native-reanimated';
7 | import { RefreshStatus, useRefreshScroll } from './type';
8 |
9 | const { width } = Dimensions.get('window');
10 |
11 | interface BottomContainerProps {
12 | children: ReactNode;
13 | }
14 |
15 | const BottomContainer: React.FC = (props) => {
16 | const { children } = props;
17 | const {
18 | scrollBounse,
19 | transitionY,
20 | triggleHeight,
21 | direction,
22 | refreshStatus,
23 | canRefresh,
24 | } = useRefreshScroll();
25 |
26 | const animatedStyle = useAnimatedStyle(() => {
27 | // console.log({
28 | // transitionY: transitionY.value,
29 | // refreshStatus: refreshStatus.value,
30 | // });
31 | if (
32 | scrollBounse.value ||
33 | direction.value === 1 ||
34 | refreshStatus.value === RefreshStatus.Idle ||
35 | refreshStatus.value === RefreshStatus.Done ||
36 | (!canRefresh.value && refreshStatus.value === RefreshStatus.AutoLoad)
37 | ) {
38 | return {
39 | height: 0,
40 | opacity: 0,
41 | };
42 | }
43 | return {
44 | bottom: 0,
45 | height: transitionY.value * direction.value,
46 | opacity: interpolate(
47 | transitionY.value * direction.value,
48 | [0, triggleHeight / 3, triggleHeight],
49 | [0, 0, 1]
50 | ),
51 | };
52 | });
53 |
54 | return (
55 |
56 | {children}
57 |
58 | );
59 | };
60 |
61 | const styles = StyleSheet.create({
62 | container: {
63 | width,
64 | alignItems: 'center',
65 | justifyContent: 'center',
66 | position: 'absolute',
67 | overflow: 'hidden',
68 | },
69 | });
70 | export default BottomContainer;
71 |
--------------------------------------------------------------------------------
/src/components/RefreshControl/RefreshContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext } from 'react';
2 | import { StyleSheet, Dimensions } from 'react-native';
3 | import Animated, {
4 | interpolate,
5 | useAnimatedStyle,
6 | SharedValue,
7 | } from 'react-native-reanimated';
8 | import { RefreshStatus } from './type';
9 |
10 | const { width } = Dimensions.get('window');
11 |
12 | interface RefreshContainerContextProps extends RefreshContainerProps {}
13 |
14 | export const RefreshContainerContext =
15 | createContext(
16 | {} as RefreshContainerContextProps
17 | );
18 | export const useRefresh = () => useContext(RefreshContainerContext);
19 |
20 | interface RefreshContainerProps {
21 | transitionY: SharedValue;
22 | triggleHeight: number; // 当下拉距离超过该值,触发下拉刷新方法
23 | refreshStatus: SharedValue;
24 | children: React.ReactNode;
25 | }
26 |
27 | const RefreshContainer: React.FC = (props) => {
28 | const { children, transitionY, triggleHeight, refreshStatus } = props;
29 |
30 | const animatedStyle = useAnimatedStyle(() => {
31 | if (transitionY.value < 0) {
32 | return {};
33 | }
34 | return {
35 | height: transitionY.value,
36 | opacity: interpolate(
37 | transitionY.value,
38 | [0, triggleHeight / 3, triggleHeight],
39 | [0, 0, 1]
40 | ),
41 | };
42 | });
43 |
44 | return (
45 |
52 |
53 | {children}
54 |
55 |
56 | );
57 | };
58 |
59 | const styles = StyleSheet.create({
60 | container: {
61 | width,
62 | alignItems: 'center',
63 | justifyContent: 'center',
64 | position: 'absolute',
65 | },
66 | });
67 |
68 | export default RefreshContainer;
69 |
--------------------------------------------------------------------------------
/src/components/RefreshControl/index.tsx:
--------------------------------------------------------------------------------
1 | import NormalControl from './NormalControl';
2 | import RefreshScrollView from './RefreshScrollView';
3 |
4 | export { RefreshScrollView, NormalControl };
5 |
--------------------------------------------------------------------------------
/src/components/RefreshControl/type.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 | import { SharedValue, DerivedValue } from 'react-native-reanimated';
3 |
4 | interface SkeletonContextProps {
5 | /**
6 | * Refresh Container transitionY
7 | */
8 | transitionY: SharedValue;
9 | /**
10 | * ScrollView scroll to the top with velocity. It can continue scroll a little, scrollBounse is a marker to mark the progress
11 | * If True, scrollView is bounceing, and refresh animation will not triggle
12 | */
13 | scrollBounse: SharedValue;
14 | /**
15 | * Only transitionY bigger than triggleHeight, refresh animation will triggle
16 | */
17 | triggleHeight: number;
18 | /**
19 | * current RefreshContainer is refreshing
20 | */
21 | refreshing: boolean;
22 | /**
23 | * RefreshStatus by transitionY
24 | * When transitionY.value reach some point, it will change
25 | */
26 | refreshStatus: SharedValue;
27 | /**
28 | * ScrollView pulling direction
29 | * 1: down
30 | * -1: up
31 | * */
32 | direction: DerivedValue<1 | -1>;
33 | /**
34 | * inside scrollView wheather reach boundary
35 | */
36 | canRefresh: DerivedValue;
37 | }
38 |
39 | export const RefreshContainerContext = createContext(
40 | {} as SkeletonContextProps
41 | );
42 | export const useRefreshScroll = () => useContext(RefreshContainerContext);
43 |
44 | /**
45 | * Once Refresh LifeCycle:
46 | * Idle -> Pulling -> Idle: Not reach triggleHeight, fail to refresh
47 | * Idle -> Pulling -> Reached -> Holding -> Done -> Idle: A compelete refresh
48 | */
49 | export enum RefreshStatus {
50 | /**
51 | * Refresh normal status
52 | */
53 | Idle,
54 | /**
55 | * Refresh is pulling down, and not reach triggleHeight
56 | */
57 | Pulling,
58 | /**
59 | * Refresh is pulling down continue, and reached triggleHeight
60 | */
61 | Reached,
62 | /**
63 | * Refresh is Refreshing
64 | */
65 | Holding,
66 | /**
67 | * Refresh is done
68 | */
69 | Done,
70 | /**
71 | * ScrollView Auto Load More
72 | */
73 | AutoLoad,
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/RefreshList/index.tsx:
--------------------------------------------------------------------------------
1 | import RefreshList, { RefreshState, RefreshListProps } from './RefreshList';
2 |
3 | export { RefreshList, RefreshState, RefreshListProps };
4 |
--------------------------------------------------------------------------------
/src/components/Segmented/index.tsx:
--------------------------------------------------------------------------------
1 | import Segmented from './Segmented';
2 |
3 | export { Segmented };
4 |
--------------------------------------------------------------------------------
/src/components/Shadow/index.tsx:
--------------------------------------------------------------------------------
1 | import Shadow from './Shadow';
2 |
3 | export { Shadow };
4 |
--------------------------------------------------------------------------------
/src/components/Skeleton/Animation/Breath.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Animation effect child component
3 | */
4 | import React from 'react';
5 | import { StyleSheet } from 'react-native';
6 | import Animated, {
7 | interpolate,
8 | useAnimatedStyle,
9 | } from 'react-native-reanimated';
10 | import { useSkeletonStyle, BaseChildAnimationProps } from '../type';
11 |
12 | interface BreathProps extends BaseChildAnimationProps {}
13 |
14 | const Breath: React.FC = (props) => {
15 | const { style } = props;
16 | const { animationProgress, color } = useSkeletonStyle();
17 |
18 | const animationStyle = useAnimatedStyle(() => {
19 | return {
20 | opacity: interpolate(animationProgress.value, [0, 1], [0.6, 1]),
21 | };
22 | });
23 |
24 | return (
25 |
28 | );
29 | };
30 |
31 | const styles = StyleSheet.create({
32 | mask: {
33 | ...StyleSheet.absoluteFillObject,
34 | },
35 | });
36 |
37 | export default Breath;
38 |
--------------------------------------------------------------------------------
/src/components/Skeleton/Animation/Load.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, StyleSheet, ActivityIndicator } from 'react-native';
3 |
4 | interface LoadProps {}
5 |
6 | const Load: React.FC = (props) => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | const styles = StyleSheet.create({
15 | mask: {
16 | ...StyleSheet.absoluteFillObject,
17 | justifyContent: 'center',
18 | alignItems: 'center',
19 | },
20 | });
21 |
22 | export default Load;
23 |
--------------------------------------------------------------------------------
/src/components/Skeleton/Animation/Normal.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Animation effect child component
3 | */
4 | import React from 'react';
5 | import { View, StyleSheet } from 'react-native';
6 | import { BaseChildAnimationProps, useSkeletonStyle } from '../type';
7 |
8 | interface NormalProps extends BaseChildAnimationProps {}
9 |
10 | const Normal: React.FC = (props) => {
11 | const { style } = props;
12 | const { color } = useSkeletonStyle();
13 |
14 | return ;
15 | };
16 |
17 | const styles = StyleSheet.create({
18 | mask: {
19 | ...StyleSheet.absoluteFillObject,
20 | },
21 | });
22 |
23 | export default Normal;
24 |
--------------------------------------------------------------------------------
/src/components/Skeleton/Animation/Shine.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Animation effect child component
3 | */
4 | import React from 'react';
5 | import { View, StyleSheet, Dimensions } from 'react-native';
6 | import Animated, {
7 | interpolate,
8 | useAnimatedStyle,
9 | useSharedValue,
10 | } from 'react-native-reanimated';
11 | import { useSkeletonStyle, BaseChildAnimationProps } from '../type';
12 |
13 | const { width: Wwidth } = Dimensions.get('window');
14 |
15 | interface ShineProps extends BaseChildAnimationProps {}
16 |
17 | const Shine: React.FC = (props) => {
18 | const { style } = props;
19 | const { animationProgress, color } = useSkeletonStyle();
20 | const width = useSharedValue(Wwidth);
21 |
22 | const animationStyle = useAnimatedStyle(() => {
23 | return {
24 | transform: [
25 | {
26 | translateX: interpolate(
27 | animationProgress.value,
28 | [0, 1],
29 | [-50, width.value]
30 | ),
31 | },
32 | ],
33 | };
34 | });
35 |
36 | return (
37 | (width.value = w)}
44 | >
45 |
46 |
47 | );
48 | };
49 |
50 | const styles = StyleSheet.create({
51 | mask: {
52 | ...StyleSheet.absoluteFillObject,
53 | flex: 1,
54 | overflow: 'hidden',
55 | },
56 | shineSlider: {
57 | height: '100%',
58 | backgroundColor: 'white',
59 | width: 50,
60 | opacity: 0.7,
61 | },
62 | });
63 |
64 | export default Shine;
65 |
--------------------------------------------------------------------------------
/src/components/Skeleton/Animation/ShineOver.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, StyleSheet, Dimensions } from 'react-native';
3 | import Animated, {
4 | interpolate,
5 | useAnimatedStyle,
6 | useSharedValue,
7 | } from 'react-native-reanimated';
8 | import { useSkeletonStyle } from '../type';
9 |
10 | const { width: Wwidth } = Dimensions.get('window');
11 |
12 | interface ShineOverProps {}
13 |
14 | const ShineOver: React.FC = (props) => {
15 | const { animationProgress } = useSkeletonStyle();
16 | const width = useSharedValue(Wwidth);
17 |
18 | const animationStyle = useAnimatedStyle(() => {
19 | return {
20 | transform: [
21 | {
22 | translateX: interpolate(
23 | animationProgress.value,
24 | [0, 1],
25 | [-50, width.value]
26 | ),
27 | },
28 | ],
29 | };
30 | });
31 |
32 | return (
33 | (width.value = w)}
40 | >
41 |
42 |
43 | );
44 | };
45 |
46 | const styles = StyleSheet.create({
47 | mask: {
48 | ...StyleSheet.absoluteFillObject,
49 | flex: 1,
50 | },
51 | shineSlider: {
52 | height: '100%',
53 | backgroundColor: 'white',
54 | width: 20,
55 | opacity: 0.7,
56 | },
57 | });
58 |
59 | export default ShineOver;
60 |
--------------------------------------------------------------------------------
/src/components/Skeleton/Animation/index.tsx:
--------------------------------------------------------------------------------
1 | import Breath from './Breath';
2 | import Shine from './Shine';
3 | import Normal from './Normal';
4 | import Load from './Load';
5 | import ShineOver from './ShineOver';
6 |
7 | export { Breath, Shine, Normal, Load, ShineOver };
8 |
--------------------------------------------------------------------------------
/src/components/Skeleton/SkeletonContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { View } from 'react-native';
3 | import {
4 | Easing,
5 | useSharedValue,
6 | withRepeat,
7 | withTiming,
8 | } from 'react-native-reanimated';
9 | import { Normal } from './Animation';
10 | import { SkeletonContext, SkeletonContainerProps } from './type';
11 |
12 | const SkeletonContainer: React.FC = (props) => {
13 | const {
14 | children,
15 | finished = false,
16 | reverse = true,
17 | childAnimation = Normal,
18 | containerAnimation,
19 | color = '#D8D8D8',
20 | } = props;
21 | const initialValue = 0;
22 | const toValue = 1;
23 | const animationProgress = useSharedValue(initialValue);
24 |
25 | useEffect(() => {
26 | animationProgress.value = withRepeat(
27 | withTiming(toValue, {
28 | duration: 1000,
29 | easing: Easing.bezier(0.65, 0, 0.35, 1),
30 | }),
31 | -1,
32 | reverse
33 | );
34 | }, [reverse]);
35 |
36 | const Animation = containerAnimation || View;
37 |
38 | return (
39 |
47 | <>
48 | {children}
49 | {!finished && }
50 | >
51 |
52 | );
53 | };
54 |
55 | export default SkeletonContainer;
56 |
--------------------------------------------------------------------------------
/src/components/Skeleton/SkeletonRect.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, ViewStyle } from 'react-native';
3 | import Animated, {
4 | useAnimatedStyle,
5 | withTiming,
6 | } from 'react-native-reanimated';
7 | import { BaseChildAnimationProps, useSkeletonStyle } from './type';
8 | import { Normal } from './Animation';
9 |
10 | interface SkeletonRectProps {
11 | style?: ViewStyle;
12 | delay?: number;
13 | }
14 |
15 | const SkeletonRect: React.FC = (props) => {
16 | const { children, style, delay = 1000 } = props;
17 | const { finished, childAnimation } = useSkeletonStyle();
18 |
19 | const fadeStyle = useAnimatedStyle(() => {
20 | return {
21 | opacity: finished ? withTiming(1, { duration: delay }) : 0,
22 | };
23 | });
24 |
25 | const Animation: React.FC = childAnimation || Normal;
26 |
27 | return (
28 |
29 | {children}
30 | {!finished && }
31 |
32 | );
33 | };
34 |
35 | export default SkeletonRect;
36 |
--------------------------------------------------------------------------------
/src/components/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import SkeletonContainer from './SkeletonContainer';
2 | import SkeletonRect from './SkeletonRect';
3 | import { Breath, Shine, Normal, Load, ShineOver } from './Animation';
4 |
5 | export {
6 | SkeletonContainer,
7 | SkeletonRect,
8 | Breath,
9 | Shine,
10 | Normal,
11 | Load,
12 | ShineOver,
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/Skeleton/type.ts:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { ViewStyle } from 'react-native';
3 | import { SharedValue } from 'react-native-reanimated';
4 | import { Breath, Shine, Normal, Load, ShineOver } from './Animation';
5 |
6 | export const SkeletonContext = React.createContext(
7 | {} as SkeletonContextProps
8 | );
9 | export const useSkeletonStyle = () => useContext(SkeletonContext);
10 |
11 | export type ChildAnimationType = typeof Breath | typeof Shine | typeof Normal;
12 | export type ContainerAnimationType = typeof Load | typeof ShineOver;
13 |
14 | /**
15 | * Any component wrapper by Skeleton can use below props
16 | */
17 | export interface SkeletonContextProps {
18 | /**
19 | * animationProgress will repeat animation from 0 to 1
20 | */
21 | animationProgress: SharedValue;
22 | /**
23 | * Skeleton finished
24 | */
25 | finished: boolean;
26 | /**
27 | * type of childAnimation
28 | */
29 | childAnimation: ContainerAnimationType;
30 | color?: string;
31 | }
32 |
33 | export interface SkeletonContainerProps {
34 | childAnimation?: ChildAnimationType;
35 | containerAnimation?: ContainerAnimationType;
36 | finished?: boolean;
37 | reverse?: boolean;
38 | color?: string;
39 | }
40 |
41 | export interface BaseChildAnimationProps {
42 | style?: ViewStyle;
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/Switch/__test__/switch.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Switch } from '../index';
3 | import { render } from '@testing-library/react-native';
4 | import { withReanimatedTimer } from 'react-native-reanimated/src/reanimated2/jestUtils';
5 |
6 | describe('Test:Switch', () => {
7 | it('base', () => {
8 | withReanimatedTimer(() => {
9 | const onChangeMock = jest.fn();
10 | const { getByTestId } = render(
11 |
12 | );
13 |
14 | const switch1 = getByTestId('test-switch');
15 | expect(switch1).not.toBeNull();
16 |
17 | // fireEvent(switch1, 'onPress');
18 | // jest.setTimeout(1000);
19 | // expect(onChangeMock).toHaveBeenCalledTimes(1);
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/components/Switch/index.tsx:
--------------------------------------------------------------------------------
1 | import Switch from './Switch';
2 |
3 | export { Switch };
4 |
--------------------------------------------------------------------------------
/src/components/TabBar/Separator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 |
4 | interface SeparatorProps {
5 | spacing: number;
6 | children?: React.ReactNode;
7 | }
8 |
9 | const Separator: React.FC = (props) => {
10 | const { spacing, children = <>> } = props;
11 |
12 | return (
13 |
21 | {children}
22 |
23 | );
24 | };
25 |
26 | export default Separator;
27 |
--------------------------------------------------------------------------------
/src/components/TabBar/TabBarItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { TouchableOpacity, StyleSheet } from 'react-native';
3 | import { TabBarItemProps } from './type';
4 | import Animated, {
5 | Extrapolation,
6 | interpolate,
7 | interpolateColor,
8 | useAnimatedStyle,
9 | } from 'react-native-reanimated';
10 |
11 | const TabBarItem: React.FC = (props) => {
12 | const {
13 | index,
14 | title,
15 | width = 'auto',
16 | style,
17 | titleStyle,
18 | currentIndex,
19 | activeTextColor = 'black',
20 | inactiveTextColor = 'black',
21 | activeScale = 1,
22 | onLayout,
23 | onPress,
24 | } = props;
25 |
26 | const input = useMemo(() => [index - 1, index, index + 1], [index]);
27 |
28 | const animatedText = useAnimatedStyle(() => {
29 | return {
30 | opacity: interpolate(
31 | currentIndex.value,
32 | input,
33 | [0.8, 1, 0.8],
34 | Extrapolation.CLAMP
35 | ),
36 | color: interpolateColor(
37 | currentIndex.value,
38 | input,
39 | [inactiveTextColor, activeTextColor, inactiveTextColor],
40 | 'RGB'
41 | ),
42 | transform: [
43 | {
44 | scale: interpolate(
45 | currentIndex.value,
46 | input,
47 | [1, activeScale, 1],
48 | Extrapolation.CLAMP
49 | ),
50 | },
51 | ],
52 | };
53 | });
54 |
55 | return (
56 | onPress(index)}
58 | onLayout={(event) => onLayout(index, event.nativeEvent.layout)}
59 | activeOpacity={1}
60 | style={[styles.container, { width: width }, style]}
61 | >
62 | {title}
63 |
64 | );
65 | };
66 |
67 | const styles = StyleSheet.create({
68 | container: {
69 | backgroundColor: '#fff',
70 | alignItems: 'center',
71 | justifyContent: 'center',
72 | paddingHorizontal: 10,
73 | height: '100%',
74 | },
75 | });
76 |
77 | export default TabBarItem;
78 |
--------------------------------------------------------------------------------
/src/components/TabBar/TabBarSlider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, ViewStyle, StyleSheet, StyleProp } from 'react-native';
3 |
4 | interface TabBarSliderProps {
5 | style?: StyleProp;
6 | }
7 |
8 | const TabBarSlider: React.FC = (props) => {
9 | const { style } = props;
10 |
11 | return ;
12 | };
13 |
14 | const styles = StyleSheet.create({
15 | slider: {
16 | width: 20,
17 | height: 5,
18 | backgroundColor: 'red',
19 | },
20 | });
21 |
22 | export default TabBarSlider;
23 |
--------------------------------------------------------------------------------
/src/components/TabBar/__test__/hook.test.ts:
--------------------------------------------------------------------------------
1 | import { useVerifyProps } from '../hook';
2 |
3 | describe('Test:TabBar->hook/useVerifyProps', () => {
4 | it('tabs is not array', () => {
5 | const errorProps = {
6 | tabs: 'string',
7 | };
8 | expect(() => useVerifyProps(errorProps)).toThrow(
9 | 'TabBar tabs must be array'
10 | );
11 | });
12 | it("tabs can't be empty", () => {
13 | const errorProps = {
14 | tabs: [],
15 | };
16 | expect(() => useVerifyProps(errorProps)).toThrow(
17 | "TabBar tabs can't be empty"
18 | );
19 | });
20 | it('contentSize must be number', () => {
21 | const errorProps = {
22 | tabs: ['1'],
23 | style: {
24 | width: 'auto',
25 | },
26 | };
27 | expect(() => useVerifyProps(errorProps)).toThrow(
28 | 'TabBar width only support number'
29 | );
30 | });
31 | it('contentSize default and normal', () => {
32 | // const { contentSize: contentSize1 } = useVerifyProps({
33 | // tabs: ['1'],
34 | // });
35 | // expect(contentSize1).toEqual(width);
36 |
37 | const { contentSize } = useVerifyProps({
38 | tabs: ['1'],
39 | style: {
40 | width: 200,
41 | },
42 | });
43 | expect(contentSize).toEqual(200);
44 | });
45 | it('defalutSliderWidth must be number', () => {
46 | const errorProps = {
47 | tabs: ['1'],
48 | defaultSliderStyle: {
49 | width: 'auto',
50 | },
51 | };
52 | expect(() => useVerifyProps(errorProps)).toThrow(
53 | 'TabBar defaultSliderStyle width only support number'
54 | );
55 | });
56 | it('defalutSliderWidth default and normal', () => {
57 | const { defalutSliderWidth } = useVerifyProps({
58 | tabs: ['1'],
59 | });
60 | expect(defalutSliderWidth).toEqual(20);
61 |
62 | const { defalutSliderWidth: defalutSliderWidth2 } = useVerifyProps({
63 | tabs: ['1'],
64 | defaultSliderStyle: {
65 | width: 50,
66 | },
67 | });
68 | expect(defalutSliderWidth2).toEqual(50);
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/src/components/TabBar/hook.ts:
--------------------------------------------------------------------------------
1 | import { Dimensions } from 'react-native';
2 | import { TabBarProps, TabBarVerifyProps } from './type';
3 | const { width } = Dimensions.get('window');
4 |
5 | export const useVerifyProps = (props: TabBarProps): TabBarVerifyProps => {
6 | const { tabs, defaultSliderStyle, style } = props;
7 |
8 | if (!Array.isArray(tabs)) {
9 | throw new Error('TabBar tabs must be array');
10 | }
11 | if (tabs.length <= 0) {
12 | throw new Error("TabBar tabs can't be empty");
13 | }
14 |
15 | let contentSize: number = width;
16 | if (style && style.width) {
17 | if (typeof style.width === 'number') {
18 | contentSize = style.width;
19 | } else {
20 | throw new Error('TabBar width only support number');
21 | }
22 | }
23 |
24 | let defalutSliderWidth: number = 20;
25 | if (!!defaultSliderStyle && !!defaultSliderStyle?.width) {
26 | if (typeof defaultSliderStyle.width === 'number') {
27 | defalutSliderWidth = defaultSliderStyle.width;
28 | } else {
29 | throw new Error('TabBar defaultSliderStyle width only support number');
30 | }
31 | }
32 |
33 | return {
34 | ...props,
35 | defalutSliderWidth,
36 | contentSize,
37 | };
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/TabBar/index.tsx:
--------------------------------------------------------------------------------
1 | import TabBar from './TabBar';
2 | export { TabBarRef, TabBarProps } from './type';
3 | export { TabBar };
4 |
--------------------------------------------------------------------------------
/src/components/TabBar/type.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DimensionValue,
3 | LayoutChangeEvent,
4 | TextStyle,
5 | ViewStyle,
6 | } from 'react-native';
7 | import { SharedValue } from 'react-native-reanimated';
8 |
9 | export interface TabBarProps {
10 | tabs: string[];
11 |
12 | tabBarflex?: 'auto' | 'equal-width';
13 | tabScrollEnabled?: boolean;
14 | spacing?: number;
15 | showSeparator?: boolean;
16 | separatorComponent?: (index: number) => React.ReactNode;
17 | hideSlider?: boolean;
18 | sliderComponent?: () => React.ReactNode;
19 | defaultSliderStyle?: ViewStyle;
20 | style?: ViewStyle;
21 | tabBarItemStyle?: ViewStyle;
22 | tabBarItemTitleStyle?: TextStyle;
23 | initialTab?: number;
24 | activeTextColor?: string;
25 | inactiveTextColor?: string;
26 | activeScale?: number;
27 | bounces?: boolean;
28 |
29 | onTabPress?: (index: number) => void;
30 | onLayout?: (e: LayoutChangeEvent) => void;
31 | }
32 |
33 | export interface TabBarRef {
34 | setTab: (index: number) => void;
35 | getCurrent: () => number;
36 | syncCurrentIndex: (offset: number) => void;
37 | keepScrollViewMiddle: (index: number) => void;
38 | }
39 |
40 | export interface TabBarItemProps {
41 | index: number;
42 | currentIndex: SharedValue;
43 | title: string;
44 | activeTextColor?: string;
45 | inactiveTextColor?: string;
46 | style?: ViewStyle;
47 | titleStyle?: TextStyle;
48 | width?: DimensionValue;
49 | activeScale?: number;
50 | onLayout: (index: number, layout: TabBarItemLayout) => void;
51 | onPress: (index: number) => void;
52 | }
53 |
54 | export interface TabBarItemLayout {
55 | x: number;
56 | y: number;
57 | width: number;
58 | height: number;
59 | }
60 |
61 | export interface TabBarVerifyProps extends TabBarProps {
62 | defalutSliderWidth: number;
63 | contentSize: number;
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/TabView/TabView.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useImperativeHandle, useRef } from 'react';
2 | import { View } from 'react-native';
3 | import { TabBar, TabBarRef } from '../TabBar';
4 | import { PageView, PageViewRef } from '../PageView';
5 |
6 | import { TabViewProps, TabViewRef } from './type';
7 | import { useVerifyProps } from './hook';
8 | import { isInteger } from '../../utils/typeUtil';
9 |
10 | const TabView = forwardRef((props, ref) => {
11 | const {
12 | style,
13 | children,
14 | pageProps,
15 | tabProps,
16 |
17 | onTabPress,
18 | onPageScroll,
19 | onPageScrollStateChanged,
20 | onPageSelected,
21 | } = useVerifyProps(props);
22 | const pageRef = useRef(null);
23 | const tabRef = useRef(null);
24 |
25 | const setPage = (index: number) => {
26 | if (!isInteger(index)) {
27 | throw new Error('index type must be Integer');
28 | }
29 | if (index < 0 || index >= tabProps.tabs.length) {
30 | throw new Error('setPage can only handle index [0, pageSize - 1]');
31 | }
32 | pageRef.current && pageRef.current?.setPage(index);
33 | };
34 |
35 | const getCurrentPage = () => {
36 | return (pageRef.current && pageRef.current?.getCurrentPage()) || 0;
37 | };
38 |
39 | useImperativeHandle(ref, () => ({
40 | setPage,
41 | getCurrentPage,
42 | }));
43 |
44 | return (
45 |
46 | {
50 | pageRef.current && pageRef.current?.setPage(index);
51 | onTabPress && onTabPress(index);
52 | }}
53 | />
54 | {
58 | tabRef.current && tabRef.current?.keepScrollViewMiddle(currentPage);
59 | onPageSelected && onPageSelected(currentPage);
60 | }}
61 | onPageScroll={(translate) => {
62 | tabRef.current && tabRef.current?.syncCurrentIndex(translate);
63 | onPageScroll && onPageScroll(translate);
64 | }}
65 | onPageScrollStateChanged={onPageScrollStateChanged}
66 | >
67 | {children}
68 |
69 |
70 | );
71 | });
72 |
73 | TabView.displayName = 'TabView';
74 |
75 | export default TabView;
76 |
--------------------------------------------------------------------------------
/src/components/TabView/index.tsx:
--------------------------------------------------------------------------------
1 | import TabView from './TabView';
2 |
3 | export { TabView };
4 |
--------------------------------------------------------------------------------
/src/components/TabView/type.ts:
--------------------------------------------------------------------------------
1 | import { TabBarProps } from '../TabBar';
2 | import { PageViewProps, PageStateType } from '../PageView';
3 | import { ViewStyle } from 'react-native';
4 |
5 | export interface TabViewProps
6 | extends Omit,
7 | Omit {
8 | initialIndex?: number;
9 | style?: ViewStyle;
10 | tabBarBounces?: boolean;
11 | pageBounces?: boolean;
12 | tabStyle?: ViewStyle;
13 | pageStyle?: ViewStyle;
14 | }
15 |
16 | export interface TabViewVerifyProps {
17 | pageProps: Omit<
18 | PageViewProps,
19 | 'children' | 'onPageScroll' | 'onPageSelected' | 'onPageScrollStateChanged'
20 | >;
21 | tabProps: Omit;
22 | style?: ViewStyle;
23 | children: React.ReactNode;
24 |
25 | onTabPress?: (index: number) => void;
26 | onPageScroll?: (translate: number) => void;
27 | onPageScrollStateChanged?: (state: PageStateType) => void;
28 | onPageSelected?: (currentPage: number) => void;
29 | }
30 |
31 | export interface TabViewRef {
32 | setPage: (index: number) => void;
33 | getCurrentPage: () => number;
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/Theme/Theme.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import defaultTheme from './default';
3 | import darkTheme from './dark';
4 | import { ThemeColorType } from './type';
5 |
6 | const themes: any = {
7 | default: defaultTheme,
8 | dark: darkTheme,
9 | };
10 |
11 | export enum ThemeType {
12 | Default = 'default',
13 | Dark = 'dark',
14 | }
15 |
16 | export interface ThemeContextProps {
17 | theme: ThemeColorType;
18 | themeName: ThemeType;
19 | changeTheme: (themeName: ThemeType) => void;
20 | }
21 |
22 | export const ThemeContext = React.createContext({} as ThemeContextProps);
23 |
24 | export const useTheme = () => React.useContext(ThemeContext);
25 |
26 | interface ThemeProps {}
27 |
28 | const Theme: React.FC = (props) => {
29 | const { children } = props;
30 | const [theme, changeTheme] = useState(ThemeType.Default);
31 |
32 | const handleChangetheme = (theme: ThemeType) => {
33 | changeTheme(theme);
34 | };
35 |
36 | return (
37 |
44 | {children}
45 |
46 | );
47 | };
48 |
49 | export default Theme;
50 |
--------------------------------------------------------------------------------
/src/components/Theme/dark.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | themeColor: '#000',
3 | backgroundColor: '#0a0a0a',
4 |
5 | statusBarColor: 'light-content',
6 | tabbarColor: '#1e90ff', // 底部tabbar颜色
7 | tabbarBgColor: '#000', // 底部tabbar背景颜色
8 | navbarBgColor: '#000', // 头部导航栏颜色
9 | navbarTitleColor: '#fff', // 头部导航栏标题颜色
10 |
11 | clickTextColor: '#1e90ff', // 可点击文字的颜色
12 | /**
13 | * Card
14 | */
15 | cardBackgroundColor: '#1e1e1e',
16 | cardContentColor: '#010101',
17 | cardTitleColor: '#fff',
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/Theme/default.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | themeColor: '#fff',
3 | backgroundColor: '#f5f5f5',
4 |
5 | statusBarColor: 'dark-content',
6 | tabbarColor: '#1e90ff', // 底部tabbar颜色
7 | tabbarBgColor: '#fff', // 底部tabbar背景颜色
8 | navbarBgColor: '#fff', // 头部导航栏颜色
9 | navbarTitleColor: '#000', // 头部导航栏标题颜色
10 |
11 | clickTextColor: '#1e90ff', // 可点击文字的颜色
12 | /**
13 | * Card
14 | */
15 | cardBackgroundColor: '#fff',
16 | cardContentColor: '#eee',
17 | cardTitleColor: '#000',
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/Theme/index.tsx:
--------------------------------------------------------------------------------
1 | import Theme, { useTheme, ThemeType } from './Theme';
2 |
3 | export { Theme, useTheme, ThemeType };
4 |
--------------------------------------------------------------------------------
/src/components/Theme/type.ts:
--------------------------------------------------------------------------------
1 | export interface ThemeColorType {
2 | [key: string]: string;
3 | statusBarColor: 'light-content' | 'dark-content';
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Toast/Toast.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, StyleSheet, Text, ViewStyle } from 'react-native';
3 |
4 | interface ToastProps {
5 | title: string;
6 | style?: ViewStyle;
7 | }
8 |
9 | const Toast: React.FC = (props) => {
10 | const { title, style } = props;
11 |
12 | return (
13 |
14 |
15 | {title}
16 |
17 | );
18 | };
19 |
20 | const styles = StyleSheet.create({
21 | container: {
22 | borderRadius: 5,
23 | overflow: 'hidden',
24 | },
25 | mask: {
26 | ...StyleSheet.absoluteFillObject,
27 | backgroundColor: '#000',
28 | opacity: 0.3,
29 | },
30 | title: {
31 | paddingHorizontal: 20,
32 | paddingVertical: 10,
33 | color: 'white',
34 | fontSize: 16,
35 | },
36 | });
37 |
38 | export default Toast;
39 |
--------------------------------------------------------------------------------
/src/components/Toast/ToastUtil.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet } from 'react-native';
3 |
4 | import { overlayRef } from '../Overlay';
5 | import Toast from './Toast';
6 | import { TranslateContainer } from '../Overlay';
7 |
8 | export interface ToastOptions {
9 | duration?: number;
10 | animation?: 'translate' | 'opacity';
11 | direction?: 'top' | 'bottom' | 'left' | 'right';
12 | }
13 |
14 | export const ToastUtil = {
15 | key: 'global-toast',
16 | template: (title: string, options?: ToastOptions) => {
17 | return (
18 |
24 |
25 |
26 | );
27 | },
28 | show: (title: string, options?: ToastOptions) => {
29 | const duration = options?.duration || 2000;
30 | if (!ToastUtil.isExist()) {
31 | const time = setTimeout(() => {
32 | ToastUtil.hide();
33 | clearTimeout(time);
34 | }, duration);
35 | }
36 | overlayRef.current?.add(ToastUtil.template(title, options), ToastUtil.key);
37 | },
38 | hide: () => overlayRef.current?.remove(ToastUtil.key),
39 | isExist: () => overlayRef.current?.isExist(ToastUtil.key),
40 | };
41 |
42 | const styles = StyleSheet.create({
43 | container: {
44 | justifyContent: 'center',
45 | alignItems: 'center',
46 | },
47 | toast: {
48 | marginBottom: 50,
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/src/components/Toast/index.tsx:
--------------------------------------------------------------------------------
1 | import Toast from './Toast';
2 | import { ToastUtil } from './ToastUtil';
3 |
4 | export { Toast, ToastUtil };
5 |
--------------------------------------------------------------------------------
/src/components/WaterfallList/AsyncImage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import { Image, ImageResizeMode, ViewStyle } from 'react-native';
3 | import { SkeletonContainer, SkeletonRect, Breath } from '../Skeleton';
4 |
5 | interface AsyncImageProps {
6 | url: string;
7 | style?: ViewStyle;
8 |
9 | resizeMode?: ImageResizeMode;
10 | }
11 |
12 | const AsyncImage: React.FC = (props) => {
13 | const { style, url, resizeMode = 'cover' } = props;
14 | const [finished, setFinished] = useState(false);
15 | const [imageSize, setImageSize] = useState<{
16 | width: number | string;
17 | height: number | string;
18 | }>(() => {
19 | return {
20 | width: style?.width || 0,
21 | height: style?.height || 0,
22 | };
23 | });
24 |
25 | useState(() => {
26 | if (imageSize.height === 0 || imageSize.width === 0) {
27 | Image.getSize(url, (width: number, height: number) => {
28 | const getWidth = imageSize.width === 0 ? width : imageSize.width;
29 | const getHeight = imageSize.height === 0 ? height : imageSize.height;
30 |
31 | setImageSize({ width: getWidth, height: getHeight });
32 | });
33 | }
34 | });
35 |
36 | const loadFinish = useCallback(() => {
37 | setTimeout(() => {
38 | setFinished(true);
39 | }, 2000);
40 | }, []);
41 |
42 | return (
43 |
48 |
49 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default AsyncImage;
63 |
--------------------------------------------------------------------------------
/src/components/WaterfallList/index.tsx:
--------------------------------------------------------------------------------
1 | import WaterfallList from './WaterfallList';
2 | import AsyncImage from './AsyncImage';
3 | import { getImageSize } from './utils';
4 |
5 | export { WaterfallList, getImageSize, AsyncImage };
6 |
--------------------------------------------------------------------------------
/src/components/WaterfallList/utils.ts:
--------------------------------------------------------------------------------
1 | import { Image } from 'react-native';
2 |
3 | /**
4 | * 获取heightList最小高度的索引
5 | * @param heightList 瀑布流存储子视图最高高度
6 | * @returns number
7 | */
8 | export const getMinHeight = (heightList: number[]): number => {
9 | let minIndex = 0;
10 | let minHeight = heightList[0];
11 |
12 | for (let i = 0; i < heightList.length; i++) {
13 | const height = heightList[i];
14 | if (height < minHeight) {
15 | minHeight = height;
16 | minIndex = i;
17 | }
18 | }
19 | return minIndex;
20 | };
21 |
22 | /**
23 | * 获取当前数据长度
24 | * @param array dataSource
25 | * @returns number
26 | */
27 | export const getArrayTotalLength = (array: any[][]): number => {
28 | return array.reduce((a, b) => a + b.length, 0);
29 | };
30 |
31 | /**
32 | * 获取网络图片的尺寸
33 | * @param url 图片地址
34 | * @returns
35 | */
36 | export const getImageSize = async (url: string) => {
37 | let height = 0;
38 | let width = 0;
39 | try {
40 | const size = await new Promise<{ width: number; height: number }>(
41 | (resolve, reject) => {
42 | Image.getSize(
43 | url,
44 | (imageWidth, imageHeight) => {
45 | resolve({
46 | width: imageWidth,
47 | height: imageHeight,
48 | });
49 | },
50 | reject
51 | );
52 | }
53 | );
54 | width = size.width;
55 | height = size.height;
56 | } catch (err) {
57 | console.log({ err });
58 | }
59 |
60 | return { width, height };
61 | };
62 |
--------------------------------------------------------------------------------
/src/utils/Freeze.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, Suspense, Fragment } from 'react';
2 |
3 | interface StorageRef {
4 | promise?: Promise;
5 | resolve?: (value: void | PromiseLike) => void;
6 | }
7 |
8 | function Suspender({
9 | freeze,
10 | children,
11 | }: {
12 | freeze: boolean;
13 | children: React.ReactNode;
14 | }) {
15 | const promiseCache = useRef({}).current;
16 |
17 | if (freeze && !promiseCache.promise) {
18 | promiseCache.promise = new Promise((resolve) => {
19 | promiseCache.resolve = resolve;
20 | });
21 | throw promiseCache.promise;
22 | } else if (freeze) {
23 | throw promiseCache.promise;
24 | } else if (promiseCache.promise) {
25 | promiseCache.resolve!();
26 | promiseCache.promise = undefined;
27 | }
28 |
29 | return {children};
30 | }
31 |
32 | interface Props {
33 | freeze: boolean;
34 | children: React.ReactNode;
35 | placeholder?: React.ReactNode;
36 | }
37 |
38 | export function Freeze({ freeze, children, placeholder = null }: Props) {
39 | return (
40 |
41 | {children}
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/utils/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | /**
4 | * make UI refresh immediately
5 | * @returns () => void
6 | * usage:
7 | * const { forceUpdate } = useForceUpdate();
8 | * forceUpdate();
9 | */
10 | const useForceUpdate = () => {
11 | const [force, update] = useState(0);
12 |
13 | return {
14 | force,
15 | forceUpdate: () => {
16 | update((up) => up + 1);
17 | },
18 | };
19 | };
20 |
21 | export { useForceUpdate };
22 |
--------------------------------------------------------------------------------
/src/utils/redash.ts:
--------------------------------------------------------------------------------
1 | // from [react-native-redash](https://github.com/wcandillon/react-native-redash)
2 | export const snapPoint = (
3 | value: number,
4 | velocity: number,
5 | points: readonly number[]
6 | ): number => {
7 | 'worklet';
8 | const point = value + 0.2 * velocity;
9 | const deltas = points.map((p) => Math.abs(point - p));
10 | const minDelta = Math.min.apply(null, deltas);
11 | return points.filter((p) => Math.abs(point - p) === minDelta)[0];
12 | };
13 |
14 | export const clamp = (
15 | value: number,
16 | lowerBound: number,
17 | upperBound: number
18 | ) => {
19 | 'worklet';
20 | return Math.min(Math.max(lowerBound, value), upperBound);
21 | };
22 |
--------------------------------------------------------------------------------
/src/utils/typeUtil.ts:
--------------------------------------------------------------------------------
1 | export function isInteger(number: any) {
2 | return typeof number === 'number' && number % 1 === 0;
3 | }
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["./src"],
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "paths": {
6 | "react-native-maui": ["./src/index"],
7 | },
8 | "allowJs": true,
9 | "allowSyntheticDefaultImports": true,
10 | "esModuleInterop": true,
11 | "isolatedModules": false,
12 | "jsx": "react-native",
13 | "lib": ["es2017"],
14 | "moduleResolution": "node",
15 | "noEmit": true,
16 | "strict": true,
17 | "target": "esnext"
18 | },
19 | "exclude": [
20 | "node_modules",
21 | "babel.config.js",
22 | "metro.config.js",
23 | "jest.config.js"
24 | ]
25 | }
--------------------------------------------------------------------------------