├── .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 | 11 | 17 | 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 | 11 | 12 | 13 | 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 | 15 | 16 | 24 | 36 | maui 37 | 38 | 39 | 40 | 47 | 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 |
20 | 26 |
27 |
28 | 34 |
35 |
36 | 37 |
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 |
10 | 16 | 22 |
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 |
10 | 11 | 12 | 13 |
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 |
10 | 11 |
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 |
11 | 12 |
13 |
14 | 18 | 23 |
24 |
25 | {}} /> 26 |
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 | 27 | 36 | 37 | ); 38 | } 39 | 40 | return ( 41 | 42 | 51 | 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 | 21 | 22 | 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 | } --------------------------------------------------------------------------------