├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .ncurc.json ├── .prettierrc.js ├── .watchmanconfig ├── LICENSE ├── README.md ├── babel.config.js ├── metro.config.js ├── package.json ├── react-native-scaled-layout.jpg ├── src └── index.tsx └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.test.* 2 | **/*.snap.* 3 | **/*.d.ts 4 | 5 | metro.config.js 6 | /dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@dooboo/eslint-config', 4 | plugins: ['react-hooks'], 5 | rules: { 6 | 'react-hooks/rules-of-hooks': 'error', 7 | 'react-hooks/exhaustive-deps': 'error', 8 | 'react/display-name': 0, 9 | '@typescript-eslint/ban-ts-ignore': 0, 10 | '@typescript-eslint/no-explicit-any': 0, 11 | 'no-unused-expressions': 'off', 12 | 'no-extra-parens': 0, 13 | '@typescript-eslint/no-empty-function': 0, 14 | 'max-len': [ 15 | 'error', 16 | { 17 | code: 120, 18 | ignoreRegExpLiterals: true, 19 | ignoreComments: true, 20 | ignoreUrls: true, 21 | ignoreStrings: true, 22 | }, 23 | ], 24 | '@typescript-eslint/no-non-null-assertion': 0, 25 | '@typescript-eslint/no-empty-interface': 1, 26 | '@typescript-eslint/camelcase': 0, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node.js 2 | # 3 | dist/ 4 | node_modules/ 5 | npm-debug.log 6 | yarn-error.log 7 | package-lock.json 8 | yarn.lock 9 | .idea/ 10 | -------------------------------------------------------------------------------- /.ncurc.json: -------------------------------------------------------------------------------- 1 | { 2 | "upgrade": true, 3 | "reject": [ 4 | "react", 5 | "react-dom", 6 | "react-test-renderer" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'all', 3 | arrowParens: 'always', 4 | singleQuote: true, 5 | jsxSingleQuote: false, 6 | printWidth: 120, 7 | }; 8 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 dooboolab 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.md: -------------------------------------------------------------------------------- 1 | # react-native-scaled-layout 2 | ![npm](https://img.shields.io/npm/v/react-native-scaled-layout) 3 | 4 | ![ogimage-1260-630](https://github.com/mj-studio-library/react-native-scaled-layout/assets/33388801/b5b99d49-b9da-4778-9cce-b966357c957d) 5 | 6 | ### Sorry to Api changes in 1.1.0 😢 7 | 8 | ## [Detail Article's Link 🔗](https://medium.com/mj-studio/stop-hesitate-to-support-the-ipad-in-react-native-1040edc318cd) 9 | 10 | ### Flexible, Scalable layout dimensions, font sizes for React Native 11 | 12 | 13 | 14 | `@mj-studio/react-native-scaled-layout` is using monkey-patch feature in javascript(typescript) and augmentation syntax in typescript. 15 | 16 | ## Contents 🏆 17 | 18 | * [Install](#install-) 19 | * [Usage](#usage-) 20 | - [0. Configure Environment](#0-configure-your-environment-or-wanted-behavior) 21 | - [1. Use with extension functions](#1-number-type-augmentationextension) 22 | - [2. ScaledText Component](#2-scaledtext-component) 23 | * [Calculation](#calculation-) 24 | * [Trouble Shooting](#trouble-shooting%EF%B8%8F) 25 | * [Todo](#todo-) 26 | * [Change Logs](#change-logs-) 27 | ## Install 💠 28 | 29 | ``` 30 | npm i @mj-studio/react-native-scaled-layout 31 | ``` 32 | 33 | or 34 | 35 | ``` 36 | yarn add @mj-studio/react-native-scaled-layout 37 | ``` 38 | 39 | ## Usage 📌 40 | 41 | ### 0. Configure your environment or wanted behavior 42 | 43 | ❕ If `@mj-studio/react-native-scaled-layout` is not imported for side-effect, then `TypeError` will be invoked. 44 | 45 | _`index.js`_ 46 | ```ts 47 | import { initScaledSettings } from '@mj-studio/react-native-scaled-layout'; 48 | ... 49 | initScaledSettings(375, { min: 0.5, max: 1.5 }, { min: 0.75, max: 1.35 }, 14); 50 | ``` 51 | | Params | Type | Default | Required | 52 | | ------------------- |---------|---------|----------| 53 | | designSpecWidth | number | 375 | false | 54 | | dimenScaleRange | `{ min: number; max: number }` | [0.5, 1.5] | false | 55 | | fontScaleRange | `{ min: number; max: number }` | [0.75, 1.3] | false | 56 | | defaultFontSize | number | 14 | false | 57 | 58 | ### 1. Number type Augmentation(Extension) 59 | 60 | ```ts 61 | // calculated with width length of design spec 62 | // clamped with dimenScaleRange min, max value 63 | (36).scaled() /* or */ (36).d() 64 | 65 | // calculated with width length of design spec 66 | // clamped with fontScaleRange min, max value 67 | (24).fontScaled() /* or */ (24).f() 68 | ``` 69 | 70 | Example in `ViewStyle` 71 | 72 | ```tsx 73 | style={{ 74 | width: (100).d(), 75 | height: (210).d() + safeAreaBottom, 76 | borderRadius: (16).d(), 77 | justifyContent: 'center', 78 | paddingBottom: safeAreaBottom + (24).d(), 79 | }} 80 | ``` 81 | 82 | ### 2. ScaledText Component 83 | 84 | ```tsx 85 | // automatically adjusted with (14).fontScaled() 86 | My Text 87 | 88 | // ignore calculated font scale 89 | // fixed with 28(14 * 2) 90 | My Text 91 | ``` 92 | 93 | `react-native-scaled-layout` is also compatible with [Styled Component](https://styled-components.com/) 94 | 95 | ```tsx 96 | export const BoldText = styled(ScaledText)` 97 | font-family: ${fonts.NotoSansKRBold}; 98 | `; 99 | ... 100 | const TutorialText = styled(BoldText)` 101 | left: ${(20).d()}px; 102 | right: ${(20).d()}px; 103 | position: absolute; 104 | font-size: 24px; // automatically adjust font size with (24).fontScaled() 105 | color: ${({ theme }): string => theme.white}; 106 | `; 107 | ``` 108 | 109 | ## Calculation 📐 110 | 111 | The following is the implementation of `initScaledSettings` 112 | 113 | ```ts 114 | /** 115 | * Set initial configuration for scaled layout behavior. If your height of design guideline spec is less than width, invert 1st, 2nd params. 116 | * @param designSpecWidth your design width viewport width(zeplin, pigma etc...). If your design viewport is 375 x 1000 then 375 is a right value. 117 | * @param dimenScaleRange dimension scale factor minimum & maximum range. default is [0.5, 1.5].. 118 | * @param fontScaleRange font scale factor minimum & maximum range. default is [0.75, 1.3]. 119 | * @param defaultFontsize default `` fontSize. default is 12. 120 | * 121 | * @example 122 | * ```ts 123 | * initScaledSettings(375, 812, {min: 0.5, max: 1.5}, {min: 0.75, max: 1.3}, 12); 124 | * ``` 125 | */ 126 | export function initScaledSettings( 127 | designSpecWidth = 375, 128 | dimenScaleRange: { min: number; max: number } = { min: 0.5, max: 1.5 }, 129 | fontScaleRange: { min: number; max: number } = { min: 0.75, max: 1.3 }, 130 | defaultFontsize = 12, 131 | ): void { 132 | dimenRatio = minLength / designSpecWidth; 133 | 134 | dimenScale = clamp(dimenRatio, dimenScaleRange.min, dimenScaleRange.max); 135 | 136 | fontScale = 137 | dimenScale >= 1 ? Math.min(dimenScale, fontScaleRange.max) : Math.max(dimenScale * dimenScale, fontScaleRange.min); 138 | _FONT_SCALE_ = fontScale; 139 | _defaultFontSize = defaultFontsize; 140 | 141 | /* eslint-disable no-extend-native */ 142 | Number.prototype.scaled = function scaled(): number { 143 | return Math.round((this as number) * dimenScale); 144 | }; 145 | Number.prototype.fontScaled = function fontScaled(): number { 146 | return Math.round((this as number) * fontScale); 147 | }; 148 | Number.prototype.d = function d(): number { 149 | return (this as number).scaled(); 150 | }; 151 | Number.prototype.f = function f(): number { 152 | return (this as number).fontScaled(); 153 | }; 154 | /* eslint-enable no-extend-native */ 155 | } 156 | initScaledSettings(); 157 | ``` 158 | 159 | ## Trouble Shooting❗️ 160 | 161 | ### 1. TypeError: 40.d is not a function 162 | 163 | Please put `import '@mj-studio/react-native-scaled-layout'` to top of `index.js` or top of file which in `setupFiles` list of `jest.config.js` 164 | 165 | 166 | ## Todo ✅ 167 | 168 | * Create `ScaledView`, `ScaledTextInput`, `ScaledTouchableXXX` like `ScaledText` 169 | 170 | ## Change Logs 🔧 171 | * 1.2.0 172 | - Change `customFontScale` logic in `ScaledText` 173 | * 1.1.7 174 | - Fix `ScaledText` font size scaling issue. Previously, the `fontSize` property is multiplied to float not integer. 175 | * 1.1.6 176 | - The values are calculated with `ceil()` instead of `round()`. 177 | * 1.1.4 178 | - `minimumFontSize` prop is added in `ScaledText` 179 | * 1.1.2 180 | - `dimenWidthScaled()`, `dimenHeightScaled()`, `w()`, `h()` are removed 181 | - `designSpecHeight` parameter is removed from `initScaledSettings` 182 | * 1.1.1 183 | - `FontScale` is calculated with design spec width length not design spec diagonal length 184 | * 1.1.0 (Sorry to API changes) 185 | - `dimenScaled()` is renamed to `scaled()` 186 | - New `number` type augmentation `dimenWidthScaled()`, `dimenHeightScaled()` 187 | - Add simple alias for functions `d()`, `f()`, `w()`, `h()` 188 | * 1.0.6 189 | - Apply round for fixing showing weird line because of floating number dimension 190 | 191 | ### feel free your fork or any PR! Thanks 192 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset', '@babel/preset-typescript'], 3 | sourceMaps: 'inline', 4 | plugins: [ 5 | [ 6 | '@babel/plugin-proposal-decorators', 7 | { 8 | legacy: true, 9 | }, 10 | ], 11 | [ 12 | '@babel/plugin-transform-runtime', 13 | { 14 | helpers: true, 15 | regenerator: false, 16 | }, 17 | ], 18 | '@babel/proposal-object-rest-spread', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('metro-config'); 2 | 3 | module.exports = (async () => { 4 | const { 5 | resolver: { sourceExts, assetExts }, 6 | } = await getDefaultConfig(); 7 | return { 8 | transformer: { 9 | babelTransformerPath: require.resolve('react-native-svg-transformer'), 10 | }, 11 | resolver: { 12 | assetExts: assetExts.filter((ext) => ext !== 'svg'), 13 | sourceExts: [...sourceExts, 'svg'], 14 | }, 15 | }; 16 | })(); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mj-studio/react-native-scaled-layout", 3 | "version": "1.2.0", 4 | "description": "Flexible, Scalable layout dimensions, font sizes for React Native", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist/**/*" 9 | ], 10 | "private": false, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mym0404/react-native-scaled-layout" 14 | }, 15 | "keywords": [], 16 | "author": "MJ Studio ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/mym0404/react-native-scaled-layout/issues" 20 | }, 21 | "homepage": "https://github.com/mym0404/react-native-scaled-layout", 22 | "scripts": { 23 | "prepare": "yarn run check", 24 | "check": "yarn lint && tsc", 25 | "lint": "eslint src --ext .ts,.tsx,.js,.jsx" 26 | }, 27 | "peerDependencies": { 28 | "react": "*", 29 | "react-native": ">=0.50" 30 | }, 31 | "dependencies": {}, 32 | "devDependencies": { 33 | "@babel/cli": "^7.8.4", 34 | "@babel/core": "^7.8.7", 35 | "@babel/plugin-proposal-class-properties": "^7.8.3", 36 | "@babel/plugin-proposal-decorators": "7.8.3", 37 | "@babel/plugin-proposal-object-rest-spread": "^7.8.3", 38 | "@babel/plugin-transform-runtime": "7.8.3", 39 | "@babel/preset-typescript": "^7.8.3", 40 | "@babel/runtime": "^7.8.7", 41 | "@dooboo/eslint-config": "^0.3.8", 42 | "@types/react": "^16.9.23", 43 | "@types/react-native": "^0.61.21", 44 | "babel-preset-react-native": "^5.0.2", 45 | "eslint": "^6.8.0", 46 | "eslint-plugin-react-hooks": "^2.5.0", 47 | "import-sort-style-eslint": "^6.0.0", 48 | "jetifier": "^1.6.5", 49 | "metro-react-native-babel-preset": "^0.58.0", 50 | "prettier": "^2.0.5", 51 | "prettier-plugin-import-sort": "^0.0.4", 52 | "typescript": "3.8.3" 53 | }, 54 | "importSort": { 55 | ".js, .jsx": { 56 | "style": "eslint", 57 | "options": {} 58 | }, 59 | ".ts, .tsx": { 60 | "parser": "typescript", 61 | "style": "eslint", 62 | "options": {} 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /react-native-scaled-layout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mj-studio-library/react-native-scaled-layout/626737bee33fafb2bcc5eae86b6378c64ca59eeb/react-native-scaled-layout.jpg -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dimensions, StyleProp, StyleSheet, Text, TextProps, TextStyle } from 'react-native'; 2 | import React, { ReactElement, ReactNode } from 'react'; 3 | 4 | const window = Dimensions.get('window'); 5 | const minLength = Math.min(window.width, window.height); 6 | let dimenRatio: number; 7 | let dimenScale: number; 8 | let fontScale: number; 9 | export let _FONT_SCALE_: number; 10 | let _defaultFontSize: number; 11 | 12 | declare global { 13 | interface Number { 14 | scaled: () => number; 15 | fontScaled: () => number; 16 | d: () => number; 17 | f: () => number; 18 | } 19 | } 20 | 21 | function clamp(value: number, min: number, max: number): number { 22 | if (value < min) return min; 23 | if (value > max) return max; 24 | return value; 25 | } 26 | 27 | /** 28 | * Set initial configuration for scaled layout behavior. If your height of design guideline spec is less than width, invert 1st, 2nd params. 29 | * @param designSpecWidth your design width viewport width(zeplin, pigma etc...). If your design viewport is 375 x 1000 then 375 is a right value. 30 | * @param dimenScaleRange dimension scale factor minimum & maximum range. default is [0.5, 1.5].. 31 | * @param fontScaleRange font scale factor minimum & maximum range. default is [0.75, 1.3]. 32 | * @param defaultFontsize default `` fontSize. default is 12. 33 | * 34 | * @example 35 | * ```ts 36 | * initScaledSettings(375, 812, {min: 0.5, max: 1.5}, {min: 0.75, max: 1.3}, 12); 37 | * ``` 38 | */ 39 | export function initScaledSettings( 40 | designSpecWidth = 375, 41 | dimenScaleRange: { min: number; max: number } = { min: 0.5, max: 1.5 }, 42 | fontScaleRange: { min: number; max: number } = { min: 0.75, max: 1.3 }, 43 | defaultFontsize = 12, 44 | ): void { 45 | dimenRatio = minLength / designSpecWidth; 46 | 47 | dimenScale = clamp(dimenRatio, dimenScaleRange.min, dimenScaleRange.max); 48 | 49 | fontScale = 50 | dimenScale >= 1 ? Math.min(dimenScale, fontScaleRange.max) : Math.max(dimenScale * dimenScale, fontScaleRange.min); 51 | _FONT_SCALE_ = fontScale; 52 | _defaultFontSize = defaultFontsize; 53 | 54 | /* eslint-disable no-extend-native */ 55 | Number.prototype.scaled = function scaled(): number { 56 | return Math.ceil((this as number) * dimenScale); 57 | }; 58 | Number.prototype.fontScaled = function fontScaled(): number { 59 | return Math.ceil((this as number) * fontScale); 60 | }; 61 | Number.prototype.d = function d(): number { 62 | return (this as number).scaled(); 63 | }; 64 | Number.prototype.f = function f(): number { 65 | return (this as number).fontScaled(); 66 | }; 67 | /* eslint-enable no-extend-native */ 68 | } 69 | initScaledSettings(); 70 | 71 | type ScaledTextProps = { 72 | style?: StyleProp; 73 | children?: ReactNode; 74 | allowFontScaling?: boolean; 75 | customFontScale?: number; 76 | minimumFontSize?: number; 77 | } & TextProps; 78 | export const ScaledText = React.forwardRef( 79 | (props: ScaledTextProps, ref: React.Ref): ReactElement => { 80 | const { style, children, allowFontScaling, customFontScale, minimumFontSize, ...rest } = props; 81 | 82 | let fontSize = StyleSheet.flatten(style)?.fontSize ?? _defaultFontSize; 83 | 84 | if (allowFontScaling !== false) { 85 | if (typeof customFontScale === 'number' && customFontScale > 0) { 86 | fontSize = Math.ceil(fontSize * customFontScale); 87 | } 88 | fontSize = Math.ceil(fontSize * _FONT_SCALE_); 89 | } 90 | 91 | if (minimumFontSize) { 92 | fontSize = Math.ceil(Math.max(minimumFontSize, fontSize)); 93 | } 94 | 95 | return ( 96 | 97 | {children} 98 | 99 | ); 100 | }, 101 | ); 102 | 103 | ScaledText.displayName = 'ScaledText'; 104 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2020"], 4 | "jsx": "react", 5 | "noResolve": false, 6 | "noImplicitAny": false, 7 | "removeComments": false, 8 | "sourceMap": true, 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "target": "es5", 12 | "skipLibCheck": true, 13 | "allowSyntheticDefaultImports": true, 14 | "declaration": true, 15 | "esModuleInterop": true, 16 | "outDir": "dist" /* Redirect output structure to the directory. */ 17 | }, 18 | "compileOnSave": true, 19 | // "include": ["src/index.ts"], 20 | "exclude": ["dist", "node_modules"] 21 | } 22 | --------------------------------------------------------------------------------