├── .circleci └── config.yml ├── .gitignore ├── LICENSE.txt ├── PULL_REQUEST_TEMPLATE.txt ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── dev │ │ └── jorgecastillo │ │ └── androidcolorx │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── dev │ │ │ └── jorgecastillo │ │ │ └── androidcolorx │ │ │ ├── GeneratedColorsAdapter.kt │ │ │ ├── MainActivity.kt │ │ │ ├── RoundedCornersColor.kt │ │ │ └── copyToClipboard.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_content_copy_black_24dp.xml │ │ ├── ic_content_copy_white_24dp.xml │ │ ├── ic_favorite_black_24dp.xml │ │ ├── ic_favorite_border_black_24dp.xml │ │ ├── ic_favorite_border_white_24dp.xml │ │ ├── ic_favorite_white_24dp.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── item_generated_color_list.xml │ │ ├── menu │ │ └── menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── strings_actions.xml │ │ └── styles.xml │ └── test │ └── java │ └── dev │ └── jorgecastillo │ └── androidcolorx │ └── ExampleUnitTest.kt ├── assets ├── sample_app.gif └── shades_and_tints.gif ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ktlint └── ktlint.gradle ├── library ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── gradle-mvn-push.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── dev │ │ │ └── jorgecastillo │ │ │ └── androidcolorx │ │ │ └── library │ │ │ ├── ARGBColor.kt │ │ │ ├── CMYKColor.kt │ │ │ ├── ColorIntExtensions.kt │ │ │ ├── HEXColor.kt │ │ │ ├── HSLAColor.kt │ │ │ ├── HSLColor.kt │ │ │ ├── HSVColor.kt │ │ │ ├── RGBColor.kt │ │ │ └── Utils.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── dev │ └── jorgecastillo │ └── androidcolorx │ └── library │ ├── ARGBColorTests.kt │ ├── AssertExtensions.kt │ ├── CMYKColorTests.kt │ ├── ColorIntTests.kt │ ├── HEXColorTests.kt │ ├── HSLAColorTests.kt │ ├── HSLColorTests.kt │ ├── HSVColorTests.kt │ └── RGBColorTests.kt └── settings.gradle /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/android:api-29 6 | 7 | working_directory: ~/repo 8 | 9 | environment: 10 | GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -XX:MaxPermSize=512m" 11 | 12 | steps: 13 | - checkout 14 | - restore_cache: 15 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "library/build.gradle" }} 16 | - run: 17 | name: Update Android SDK 18 | command: yes | sdkmanager --update || true 19 | - run: 20 | name: Install Android SDK Packages 21 | command: yes | sdkmanager --install "build-tools;29.0.2" "tools" "platform-tools" "platforms;android-29" || true 22 | - run: 23 | name: Download Dependencies 24 | command: ./gradlew androidDependencies 25 | - save_cache: 26 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "library/build.gradle" }} 27 | paths: 28 | - ~/.gradle 29 | - run: 30 | name: Run Klint on app module 31 | command: ./gradlew :app:ktlint 32 | when: always 33 | - run: 34 | name: Run Klint on library module 35 | command: ./gradlew :library:ktlint 36 | when: always 37 | - run: 38 | name: Run Unit Tests 39 | command: ./gradlew test 40 | when: always 41 | - store_artifacts: 42 | path: app/build/reports 43 | destination: reports 44 | - store_test_results: 45 | path: app/build/test-results 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | misc.xml 2 | workspace.xml 3 | thumbdrawable.svg 4 | Thumbs.db 5 | 6 | .DS_Store 7 | 8 | # Built application files 9 | *.apk 10 | *.ap_ 11 | *.iml 12 | 13 | # Files for the Dalvik VM 14 | *.dex 15 | 16 | # Java class files 17 | *.class 18 | 19 | # Generated files 20 | bin/ 21 | gen/ 22 | 23 | # Gradle files 24 | .gradle/ 25 | build/ 26 | reports/ 27 | 28 | # Local configuration file (sdk path, etc) 29 | local.properties 30 | 31 | # Proguard folder generated by Eclipse 32 | proguard/ 33 | 34 | # Log Files 35 | *.log 36 | 37 | # Intellij IDEA files 38 | /.idea 39 | 40 | # Captures from IDE (Layout Inspector, ...) 41 | /captures 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.txt: -------------------------------------------------------------------------------- 1 | ### ⚠️ TEMPLATE ⚠️ 2 | **Template for GitHub PR descriptions. Use it as a guide on how to describe your work.** 3 | 4 | ### :pushpin: References 5 | **Issues:** 6 | * Fixes #1 7 | 8 | ### :tophat: What is the goal? 9 | 10 | * Here you would describe the PR goals. Just the stuff we need to implement for the fix / feature and a simple rationale. 11 | * It could contain many check points if needed. We have no restrictions at all on the content of each section itself. 12 | 13 | ### 💻 How is it being implemented? 14 | 15 | Here we use to include all the relevant things implemented and also rationale, aclarations / disclaimers etc related to the approach you used. 16 | 17 | * Stuff implemented 1 18 | * Stuff implemented 2 19 | * Stuff implemented 3 20 | 21 | ### 🖼 Screenshots or Gifs 22 | 23 | We include all the media files to give some context about what's being implemented or fixed. It's not mandatory to upload screenshots or gifts, but for most of the cases it becomes really handy to get into the scope of the feature / bug being fixed. It's also REALLY useful for UI related PRs. 24 | 25 | ![ezgif com-video-to-gif 1](https://user-images.githubusercontent.com/6547526/28528893-5269d13e-708f-11e7-9b2e-384f4e66c8ae.gif) 26 | 27 | ### 📱 How to Test 28 | 29 | Here we use to add the steps to manually test the PR. Here you have some sample checks: 30 | 31 | * Install the branch and click on "+" on toolbar under rides tab. 32 | * Click on one ride. 33 | * New blank screen with "Duplicate Ride title" should appear on screen. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AndroidColorX [![CircleCI](https://circleci.com/gh/JorgeCastilloPrz/AndroidColorX/tree/master.svg?style=svg&circle-token=5cd5f9a1d941936290fde62a8321f9bf9d60f2c5)](https://circleci.com/gh/JorgeCastilloPrz/AndroidColorX/tree/master) 2 | ====== 3 | 4 | AndroidColorX (i.e: Android Color Extensions) is an Android library written in Kotlin that provides color utilities as [Kotlin extension functions](https://kotlinlang.org/docs/tutorials/kotlin-for-py/extension-functionsproperties.html). The library relies on AndroidX [`ColorUtils`](https://developer.android.com/reference/kotlin/androidx/core/graphics/ColorUtils) for some of its calculations. 5 | 6 | ### How to use 7 | 8 | Add the following dependency to your `build.gradle`. It's available in **Maven Central**. 9 | 10 | ```groovy 11 | dependencies { 12 | implementation 'me.jorgecastillo:androidcolorx:0.2.0' 13 | } 14 | ``` 15 | 16 | ### Color conversion 17 | 18 | This library provides seamless conversion between the following color types: 19 | 20 | * `android.graphics.Color (ColorInt)` 21 | * `RGBColor` 22 | * `ARGBColor` 23 | * `HEXColor` 24 | * `HSLColor` 25 | * `HSLAColor` 26 | * `HSVColor` 27 | * `CMYKColor` 28 | 29 | To convert a color type to any of the other types, you can use the extensions provided for it. 30 | 31 | ```kotlin 32 | val color = Color.parseColor("#e91e63") 33 | 34 | val rgb = color.asRgb() 35 | val argb = color.asArgb() 36 | val hex = color.asHex() 37 | val hsl = color.asHsl() 38 | val hsla = color.asHsla() 39 | val hsv = color.asHsv() 40 | val cmyk = color.asCmyk() 41 | 42 | val colorHsl = HSLColor(hue = 210f, saturation = 0.5f, lightness = 0.5f) 43 | 44 | val colorInt = colorHsl.asColorInt() 45 | val rgb = colorHsl.asRgb() 46 | val argb = colorHsl.asArgb() 47 | val hex = colorHsl.asHex() 48 | val cmyk = colorHsl.asCmyk() 49 | val hsla = colorHsl.asHsla() 50 | val hsv = colorHsl.asHsv() 51 | ``` 52 | 53 | The same extensions **are available for all the mentioned color types**. Note that when converting from a color with an alpha component (i.e: `ARGB` or `HSLA`) to one without it (i.e: `RGB` or `HSL`) it will be a lossy conversion. If it's the other way around, conversions will assume full alpha (255 or 1f). 54 | 55 | Also note that conversions from colors that store their components as `Float` (i.e: cmyk, hsl, hsla) to colors that store them as `Int` (i.e: RGBColor, ColorInt) and back will imply precision loss. 56 | 57 | ### Shades and tints 58 | 59 | If you're aware of webs like [Color-Hex](https://www.color-hex.com/color/6dc066) you've probably seen those shades and tints palettes. AndroidColorX provides extensions for calculating those: 60 | 61 | 62 | 63 | Here's the code for all the color types available: 64 | 65 | ```kotlin 66 | 67 | val color = Color.parseColor("#e91e63") 68 | 69 | // shades 70 | val shades: List = color.shades() 71 | val shadesHsl: List = color.asHsl().shades() 72 | val shadesHsla: List = color.asHsla().shades() 73 | val shadesHsv: List = color.asHsv().shades() 74 | val shadesCmyk: List = color.asCmyk().shades() 75 | val shadesHex: List = color.asHex().shades() 76 | val shadesRgb: List = color.asRgb().shades() 77 | val shadesArgb: List = color.asArgb().shades() 78 | 79 | // tints 80 | val tints: List = color.tints() 81 | val tintsHsl: List = color.asHsl().tints() 82 | val tintsHsla: List = color.asHsla().tints() 83 | val tintsHsv: List = color.asHsv().tints() 84 | val tintsCmyk: List = color.asCmyk().tints() 85 | val tintsHex: List = color.asHex().tints() 86 | val tintsRgb: List = color.asRgb().tints() 87 | val tintsArgb: List = color.asArgb().tints() 88 | ``` 89 | 90 | These functions calculate `10` shades or tints by default that will be returned **along with the original color**, but you can also generate an arbitrary amount if you need to: 91 | 92 | ```kotlin 93 | val color = Color.parseColor("#e91e63") 94 | 95 | // shades 96 | val shades: List = color.shades(count = 3) 97 | val shadesHsl: List = color.asHsl().shades(count = 3) 98 | ... 99 | 100 | // tints 101 | val tints: List = color.tints(count = 15) 102 | val tintsHsl: List = color.asHsl().tints(count = 15) 103 | ... 104 | ``` 105 | 106 | Note that the colors will always range from 0 to 1 luminance for tinting or shading regardless of the amount of colors to generate, so the more you generate the more gradual the contrast will be between each consecutive pair of colors. 107 | 108 | These extensions **are available for all the mentioned color types**. 109 | 110 | ### Complimentary, triadic, tetradic and analogous colors 111 | 112 | The library provides extensions to calculate all those. There is a brief but very useful [post to understand all those here](https://www.tigercolor.com/color-lab/color-theory/color-harmonies.htm). 113 | 114 | 115 | 116 | ```kotlin 117 | val color = Color.parseColor("#e91e63") 118 | 119 | // Returns the complimentary color to the given one. 120 | val complimentary: Int = color.complimentary() 121 | 122 | // Returns the other 2 colors to complete the triadic color scheme. 123 | val triadic: Pair = color.triadic() 124 | 125 | // Returns the other 3 colors to complete the tetradic color scheme. 126 | val tetradic: Triple = color.tetradic() 127 | 128 | // Returns the other 2 colors to complete the analogous color scheme. 129 | val analogous: Pair = color.analogous() 130 | ``` 131 | 132 | Once again, you've got those extensions available for all the color types in the library. 133 | 134 | ### Darken / Lighten a color 135 | 136 | You can darken or lighten a color by an amount: 137 | 138 | ```kotlin 139 | val color = Color.parseColor("#e91e63") 140 | 141 | // Int in the 0...100 range. Anything else is clamped to 0...100 142 | val darkerColor = color.darken(50) 143 | val lighterColor = color.lighten(50) 144 | 145 | // Float in the 0...1 range. Anything else is clamped to 0...1 146 | val darkerColor = color.darken(0.5) 147 | val lighterColor = color.lighten(0.5) 148 | ``` 149 | 150 | You can use this extensions over any color type available. 151 | 152 | ### Other interesting extensions 153 | 154 | #### isDark 155 | 156 | You can use this one to detect whether a color is considered "dark" or not. It delegates in the following AndroidX `ColorUtils` code `ColorUtils.calculateLuminance(this) < 0.5`. This is frequently used for inferring which color you should tint an icon or a text with, so it can have a good contrast on top of the underlying background. 157 | 158 | ```kotlin 159 | icon.setImageDrawable( 160 | ContextCompat.getDrawable( 161 | context, 162 | if (bgColor.isDark()) { 163 | R.drawable.ic_fav_light 164 | } else { 165 | R.drawable.ic_fav_dark 166 | }) 167 | ) 168 | ``` 169 | 170 | #### contrasting 171 | 172 | The library also provides a handy extension to get a nicely contrasting color for a given color. You can use it for the same purpose and to remove a bit of boilerplate. 173 | 174 | ```kotlin 175 | val color = HEXColor("#e91e63") 176 | 177 | // Defaults to black or white, the one with better contrast 178 | color.contrasting() 179 | 180 | // You can pass custom light and dark colors 181 | color.contrasting(lightColor = HEXColor("#ffffff"), darkColor = HEXColor("#000000")) 182 | ``` 183 | 184 | These extensions can be used over any color types provided by the library. 185 | 186 | License 187 | ------- 188 | 189 | Copyright 2019 Jorge Castillo Pérez 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | 204 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 29 9 | buildToolsVersion "29.0.2" 10 | defaultConfig { 11 | applicationId "dev.jorgecastillo.androidcolorx" 12 | minSdkVersion 17 13 | targetSdkVersion 29 14 | versionCode 1 15 | versionName "1.0" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation project(":library") 33 | 34 | ktlint "com.github.shyiko:ktlint:0.31.0" 35 | 36 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 37 | implementation 'androidx.appcompat:appcompat:1.1.0' 38 | implementation "com.google.android.material:material:1.1.0-alpha08" 39 | implementation 'androidx.core:core-ktx:1.1.0' 40 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 41 | testImplementation 'junit:junit:4.12' 42 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 43 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 44 | } 45 | 46 | apply from: '../ktlint/ktlint.gradle' 47 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/dev/jorgecastillo/androidcolorx/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import junit.framework.TestCase.assertEquals 6 | 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | 10 | /** 11 | * Instrumented test, which will execute on an Android device. 12 | * 13 | * See [testing documentation](http://d.android.com/tools/testing). 14 | */ 15 | @RunWith(AndroidJUnit4::class) 16 | class ExampleInstrumentedTest { 17 | @Test 18 | fun useAppContext() { 19 | // Context of the app under test. 20 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 21 | assertEquals("dev.jorgecastillo.androidcolorx", appContext.packageName) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/dev/jorgecastillo/androidcolorx/GeneratedColorsAdapter.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.TextView 7 | import androidx.recyclerview.widget.RecyclerView 8 | import dev.jorgecastillo.androidcolorx.library.asHex 9 | 10 | class GeneratedColorsAdapter : RecyclerView.Adapter() { 11 | 12 | var colors: List = listOf() 13 | set(value) { 14 | field = value 15 | notifyDataSetChanged() 16 | } 17 | 18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 19 | val view = LayoutInflater.from(parent.context) 20 | .inflate(R.layout.item_generated_color_list, parent, false) 21 | return ViewHolder(view) 22 | } 23 | 24 | override fun getItemCount(): Int = colors.size 25 | 26 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 27 | holder.bind(colors[position]) 28 | } 29 | 30 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 31 | fun bind(color: Int) { 32 | val colorSquare = itemView.findViewById(R.id.colorSquare) 33 | colorSquare.setColor(color) 34 | 35 | val colorText = itemView.findViewById(R.id.colorText) 36 | colorText.text = color.asHex().toString() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/dev/jorgecastillo/androidcolorx/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx 2 | 3 | import android.graphics.Color 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.view.Menu 7 | import android.view.MenuItem 8 | import android.view.View 9 | import androidx.annotation.ColorInt 10 | import androidx.annotation.DrawableRes 11 | import androidx.appcompat.app.AppCompatActivity 12 | import androidx.appcompat.widget.PopupMenu 13 | import androidx.core.content.ContextCompat 14 | import androidx.recyclerview.widget.LinearLayoutManager 15 | import dev.jorgecastillo.androidcolorx.library.analogous 16 | import dev.jorgecastillo.androidcolorx.library.complimentary 17 | import dev.jorgecastillo.androidcolorx.library.shades 18 | import dev.jorgecastillo.androidcolorx.library.tints 19 | import dev.jorgecastillo.androidcolorx.library.isDark 20 | import dev.jorgecastillo.androidcolorx.library.tetradic 21 | import dev.jorgecastillo.androidcolorx.library.asCmyk 22 | import dev.jorgecastillo.androidcolorx.library.asHsl 23 | import dev.jorgecastillo.androidcolorx.library.asHex 24 | import dev.jorgecastillo.androidcolorx.library.asRgb 25 | import dev.jorgecastillo.androidcolorx.library.triadic 26 | import kotlinx.android.synthetic.main.activity_main.analogousColor1 27 | import kotlinx.android.synthetic.main.activity_main.analogousColor1Hex 28 | import kotlinx.android.synthetic.main.activity_main.analogousColor2 29 | import kotlinx.android.synthetic.main.activity_main.analogousColor2Hex 30 | import kotlinx.android.synthetic.main.activity_main.analogousColorBase 31 | import kotlinx.android.synthetic.main.activity_main.analogousColorBaseHex 32 | import kotlinx.android.synthetic.main.activity_main.analogousColorsTitle 33 | import kotlinx.android.synthetic.main.activity_main.appBarLayout 34 | import kotlinx.android.synthetic.main.activity_main.colorShadesList 35 | import kotlinx.android.synthetic.main.activity_main.complimentaryColor 36 | import kotlinx.android.synthetic.main.activity_main.complimentaryColorBase 37 | import kotlinx.android.synthetic.main.activity_main.complimentaryColorBaseHex 38 | import kotlinx.android.synthetic.main.activity_main.complimentaryColorHex 39 | import kotlinx.android.synthetic.main.activity_main.complimentaryColorTitle 40 | import kotlinx.android.synthetic.main.activity_main.selectedColorHex 41 | import kotlinx.android.synthetic.main.activity_main.shadesTitle 42 | import kotlinx.android.synthetic.main.activity_main.tetradicColor1 43 | import kotlinx.android.synthetic.main.activity_main.tetradicColor1Hex 44 | import kotlinx.android.synthetic.main.activity_main.tetradicColor2 45 | import kotlinx.android.synthetic.main.activity_main.tetradicColor2Hex 46 | import kotlinx.android.synthetic.main.activity_main.tetradicColor3 47 | import kotlinx.android.synthetic.main.activity_main.tetradicColor3Hex 48 | import kotlinx.android.synthetic.main.activity_main.tetradicColorBase 49 | import kotlinx.android.synthetic.main.activity_main.tetradicColorBaseHex 50 | import kotlinx.android.synthetic.main.activity_main.tetradicColorsTitle 51 | import kotlinx.android.synthetic.main.activity_main.tintsList 52 | import kotlinx.android.synthetic.main.activity_main.tintsTitle 53 | import kotlinx.android.synthetic.main.activity_main.toolbar 54 | import kotlinx.android.synthetic.main.activity_main.triadicColor1 55 | import kotlinx.android.synthetic.main.activity_main.triadicColor1Hex 56 | import kotlinx.android.synthetic.main.activity_main.triadicColor2 57 | import kotlinx.android.synthetic.main.activity_main.triadicColor2Hex 58 | import kotlinx.android.synthetic.main.activity_main.triadicColorBase 59 | import kotlinx.android.synthetic.main.activity_main.triadicColorBaseHex 60 | import kotlinx.android.synthetic.main.activity_main.triadicColorsTitle 61 | 62 | class MainActivity : AppCompatActivity() { 63 | 64 | private lateinit var menu: Menu 65 | 66 | @ColorInt 67 | private fun selectedColor(): Int = Color.parseColor("#e91e63") 68 | 69 | override fun onCreate(savedInstanceState: Bundle?) { 70 | super.onCreate(savedInstanceState) 71 | setContentView(R.layout.activity_main) 72 | setupStatusBar() 73 | 74 | val selectedColor = selectedColor() 75 | appBarLayout.setBackgroundColor(selectedColor) 76 | val headerTextColor = ContextCompat.getColor( 77 | this, 78 | if (selectedColor.isDark()) R.color.white else R.color.black 79 | ) 80 | selectedColorHex.setTextColor(headerTextColor) 81 | selectedColorHex.text = selectedColor.asHex().toString() 82 | 83 | generateColors(selectedColor) 84 | } 85 | 86 | private fun setupStatusBar() { 87 | title = "" 88 | setSupportActionBar(toolbar) 89 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 90 | window.statusBarColor = selectedColor() 91 | } 92 | } 93 | 94 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 95 | menuInflater.inflate(R.menu.menu, menu) 96 | val favIcon = menu.findItem(R.id.favColor) 97 | val copyToClipboardItem = menu.findItem(R.id.copyToClipBoard) 98 | renderMenuIcons(favIcon, copyToClipboardItem) 99 | 100 | this.menu = menu 101 | return true 102 | } 103 | 104 | private fun renderMenuIcons(favIcon: MenuItem, copyToClipboardItem: MenuItem) { 105 | if (selectedColor().isDark()) { 106 | renderClipboardIcon(copyToClipboardItem, R.drawable.ic_content_copy_white_24dp) 107 | renderFavIcon( 108 | favIcon, 109 | R.drawable.ic_favorite_white_24dp 110 | ) 111 | } else { 112 | renderClipboardIcon(copyToClipboardItem, R.drawable.ic_content_copy_black_24dp) 113 | renderFavIcon( 114 | favIcon, 115 | R.drawable.ic_favorite_black_24dp 116 | ) 117 | } 118 | } 119 | 120 | private fun renderClipboardIcon(clipboardItem: MenuItem, @DrawableRes iconRes: Int) { 121 | clipboardItem.icon = ContextCompat.getDrawable(this, iconRes) 122 | } 123 | 124 | @Suppress("SENSELESS_COMPARISON") 125 | private fun renderFavIcon(favIcon: MenuItem, @DrawableRes iconRes: Int) { 126 | favIcon.actionView = null 127 | favIcon.icon = ContextCompat.getDrawable(this, iconRes) 128 | } 129 | 130 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 131 | return when (item.itemId) { 132 | R.id.favColor -> { 133 | true 134 | } 135 | R.id.copyToClipBoard -> { 136 | val anchorView = findViewById(R.id.copyToClipBoard) 137 | val popup = PopupMenu(this, anchorView) 138 | val options = resources.getStringArray(R.array.copy_modes) 139 | options.forEachIndexed { index, action -> 140 | popup.menu.add(0, index, index, action) 141 | } 142 | popup.setOnMenuItemClickListener { 143 | when (it.itemId) { 144 | 0 -> copyToClipboard(selectedColor().asRgb().toString()) 145 | 1 -> copyToClipboard(selectedColor().asHex().toString()) 146 | 2 -> copyToClipboard(selectedColor().asCmyk().toString()) 147 | 3 -> copyToClipboard(selectedColor().asHsl().toString()) 148 | } 149 | true 150 | } 151 | popup.show() 152 | true 153 | } 154 | else -> super.onOptionsItemSelected(item) 155 | } 156 | } 157 | 158 | private fun generateColors(selectedColor: Int) { 159 | generateComplimentary(selectedColor) 160 | generateShades(selectedColor) 161 | generateTints(selectedColor) 162 | generateAnalogous(selectedColor) 163 | generateTriadic(selectedColor) 164 | generateTetradic(selectedColor) 165 | } 166 | 167 | private fun generateShades(selectedColor: Int) { 168 | val adapter = GeneratedColorsAdapter() 169 | colorShadesList.adapter = adapter 170 | colorShadesList.layoutManager = 171 | LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) 172 | colorShadesList.setHasFixedSize(true) 173 | adapter.colors = selectedColor.shades() 174 | shadesTitle.text = resources.getString(R.string.shades) 175 | } 176 | 177 | private fun generateTints(selectedColor: Int) { 178 | val adapter = GeneratedColorsAdapter() 179 | tintsList.adapter = adapter 180 | tintsList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) 181 | tintsList.setHasFixedSize(true) 182 | adapter.colors = selectedColor.tints() 183 | tintsTitle.text = resources.getString(R.string.tints) 184 | } 185 | 186 | private fun generateComplimentary(selectedColor: Int) { 187 | val hexColor = selectedColor.asHex() 188 | complimentaryColorTitle.text = resources.getString(R.string.complimentary) 189 | complimentaryColorBase.setColor(selectedColor) 190 | complimentaryColor.setColor(selectedColor.complimentary()) 191 | 192 | val hexColorComplimentary = selectedColor.complimentary().asHex() 193 | complimentaryColorBaseHex.text = hexColor.toString() 194 | complimentaryColorHex.text = hexColorComplimentary.toString() 195 | } 196 | 197 | private fun generateAnalogous(selectedColor: Int) { 198 | val hexColor = selectedColor.asHex().toString() 199 | analogousColorsTitle.text = resources.getString(R.string.analogous) 200 | analogousColorBase.setColor(selectedColor) 201 | val analogousColors = selectedColor.analogous() 202 | analogousColor1.setColor(analogousColors.first) 203 | analogousColor2.setColor(analogousColors.second) 204 | 205 | analogousColorBaseHex.text = hexColor 206 | analogousColor1Hex.text = analogousColors.first.asHex().toString() 207 | analogousColor2Hex.text = analogousColors.second.asHex().toString() 208 | } 209 | 210 | private fun generateTriadic(selectedColor: Int) { 211 | val hexColor = selectedColor.asHex() 212 | triadicColorsTitle.text = resources.getString(R.string.triadic) 213 | triadicColorBase.setColor(selectedColor) 214 | val triadicColors = selectedColor.triadic() 215 | triadicColor1.setColor(triadicColors.first) 216 | triadicColor2.setColor(triadicColors.second) 217 | 218 | triadicColorBaseHex.text = hexColor.toString() 219 | triadicColor1Hex.text = triadicColors.first.asHex().toString() 220 | triadicColor2Hex.text = triadicColors.second.asHex().toString() 221 | } 222 | 223 | private fun generateTetradic(selectedColor: Int) { 224 | val hexColor = selectedColor.asHex() 225 | tetradicColorsTitle.text = resources.getString(R.string.tetradic) 226 | tetradicColorBase.setColor(selectedColor) 227 | val tetradicColors = selectedColor.tetradic() 228 | tetradicColor1.setColor(tetradicColors.first) 229 | tetradicColor2.setColor(tetradicColors.second) 230 | tetradicColor3.setColor(tetradicColors.third) 231 | 232 | tetradicColorBaseHex.text = hexColor.toString() 233 | tetradicColor1Hex.text = tetradicColors.first.asHex().toString() 234 | tetradicColor2Hex.text = tetradicColors.second.asHex().toString() 235 | tetradicColor3Hex.text = tetradicColors.third.asHex().toString() 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /app/src/main/java/dev/jorgecastillo/androidcolorx/RoundedCornersColor.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx 2 | 3 | import android.content.Context 4 | import android.content.res.ColorStateList 5 | import android.graphics.Color 6 | import android.util.AttributeSet 7 | import android.view.View 8 | import androidx.annotation.ColorInt 9 | import com.google.android.material.shape.MaterialShapeDrawable 10 | 11 | class RoundedCornersColor @JvmOverloads constructor( 12 | context: Context, 13 | attrs: AttributeSet? = null, 14 | defStyleAttr: Int = 0 15 | ) : View(context, attrs, defStyleAttr) { 16 | 17 | companion object { 18 | private val DEFAULT_COLOR = Color.parseColor("#FFE0B2") 19 | } 20 | 21 | init { 22 | background = MaterialShapeDrawable().apply { 23 | setCornerRadius(resources.getDimensionPixelSize(R.dimen.card_corner_radius).toFloat()) 24 | val attributes = context.obtainStyledAttributes(attrs, R.styleable.RoundedCornersColor) 25 | val color = attributes.getColor(R.styleable.RoundedCornersColor_color, DEFAULT_COLOR) 26 | attributes.recycle() 27 | fillColor = ColorStateList.valueOf(color) 28 | } 29 | } 30 | 31 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 32 | super.onMeasure(widthMeasureSpec, widthMeasureSpec) 33 | } 34 | 35 | fun setColor(@ColorInt color: Int) { 36 | (background as MaterialShapeDrawable).fillColor = ColorStateList.valueOf(color) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/dev/jorgecastillo/androidcolorx/copyToClipboard.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.widget.Toast 7 | 8 | fun Context.copyToClipboard(text: String) { 9 | val clipboardService = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 10 | val clip = ClipData.newPlainText("Color generated by AndroidColorX app", text) 11 | clipboardService.setPrimaryClip(clip) 12 | Toast.makeText(this, R.string.feedback_copy_to_clipboard, Toast.LENGTH_SHORT).show() 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_content_copy_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_content_copy_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_border_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_border_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_generated_color_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 19 | 20 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #262833 4 | #262833 5 | #262833 6 | #4caf50 7 | #357a38 8 | #ffffff 9 | #000000 10 | #101010 11 | #1E1E1E 12 | #2D2D2D 13 | #EDEDED 14 | #55ffffff 15 | #CE3535 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2dp 4 | 4dp 5 | 8dp 6 | 16dp 7 | 20dp 8 | 24dp 9 | 32dp 10 | 48dp 11 | 12 | 4dp 13 | 32dp 14 | 72dp 15 | 92dp 16 | 8dp 17 | 32dp 18 | 20sp 19 | 180dp 20 | 52dp 21 | 300dp 22 | 4dp 23 | 6dp 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidColorX 3 | RGB: %1$s 4 | CMYK: %1$s 5 | \"%1$s\" 6 | Shades 7 | Tints 8 | Complimentary color 9 | Triadic colors 10 | Tetradic colors 11 | Analogous colors 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings_actions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Settings 5 | Logout 6 | 7 | 8 | Copy to clipboard 9 | 10 | Copy as RGB 11 | Copy as Hex 12 | Copy as CMYK 13 | Copy as HSL 14 | 15 | 16 | Select from gallery 17 | Color copied to clipboard. 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/test/java/dev/jorgecastillo/androidcolorx/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx 2 | 3 | import junit.framework.TestCase.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /assets/sample_app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/assets/sample_app.gif -------------------------------------------------------------------------------- /assets/shades_and_tints.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/assets/shades_and_tints.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | 4 | repositories { 5 | google() 6 | jcenter() 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.5.2' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | } 14 | } 15 | 16 | allprojects { 17 | version = VERSION_NAME 18 | group = GROUP 19 | 20 | repositories { 21 | google() 22 | jcenter() 23 | mavenCentral() 24 | } 25 | 26 | configurations { 27 | ktlint 28 | } 29 | 30 | allprojects { 31 | tasks.withType(Javadoc) { 32 | options.addStringOption('Xdoclint:none', '-quiet') 33 | options.addStringOption('encoding', 'UTF-8') 34 | } 35 | } 36 | } 37 | 38 | task clean(type: Delete) { 39 | delete rootProject.buildDir 40 | } 41 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536m 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | kotlin.code.style=official 5 | POM_NAME=AndroidColorX 6 | POM_PACKAGING=aar 7 | POM_ARTIFACT_ID=androidcolorx 8 | VERSION_NAME=0.2.0 9 | VERSION_CODE=1 10 | GROUP=me.jorgecastillo 11 | POM_DESCRIPTION=AndroidColorX (i.e: Android Color Extensions) is an Android library written in Kotlin that provides color utilities as extension functions. 12 | POM_URL=https://github.com/JorgeCastilloPrz/androidcolorx.git 13 | POM_SCM_URL=https://github.com/JorgeCastilloPrz/androidcolorx.git 14 | POM_SCM_CONNECTION=scm:git@github.com:JorgeCastilloPrz/androidcolorx.git 15 | POM_SCM_DEV_CONNECTION=scm:git@github.com:JorgeCastilloPrz/androidcolorx.git 16 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 17 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 18 | POM_LICENCE_DIST=repo 19 | POM_DEVELOPER_ID=jorgecastilloprz 20 | POM_DEVELOPER_NAME=JorgeCastillo 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Nov 08 21:37:33 CET 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /ktlint/ktlint.gradle: -------------------------------------------------------------------------------- 1 | task ktlint(type: JavaExec, group: "verification") { 2 | description = "Check Kotlin code style." 3 | classpath = configurations.ktlint 4 | main = "com.github.shyiko.ktlint.Main" 5 | args "src/**/*.kt" 6 | // to generate report in checkstyle format prepend following args: 7 | // "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/ktlint.xml" 8 | // see https://github.com/shyiko/ktlint#usage for more 9 | } 10 | check.dependsOn ktlint 11 | 12 | task ktlintFormat(type: JavaExec, group: "formatting") { 13 | description = "Fix Kotlin code style deviations." 14 | classpath = configurations.ktlint 15 | main = "com.github.shyiko.ktlint.Main" 16 | args "-F", "src/**/*.kt" 17 | } 18 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.2" 8 | 9 | defaultConfig { 10 | minSdkVersion 14 11 | targetSdkVersion 29 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles 'consumer-rules.pro' 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | testOptions { 25 | unitTests.returnDefaultValues = true 26 | unitTests.includeAndroidResources = true 27 | unitTests.all { 28 | testLogging { 29 | events "passed", "skipped", "failed", "standardOut", "standardError" 30 | outputs.upToDateWhen { false } 31 | showStandardStreams = true 32 | } 33 | } 34 | } 35 | 36 | sourceSets { 37 | main { 38 | java { 39 | include '**/*.java' 40 | include '**/*.kt' 41 | } 42 | } 43 | } 44 | } 45 | 46 | dependencies { 47 | // linters 48 | ktlint "com.github.shyiko:ktlint:0.31.0" 49 | 50 | // kotlin 51 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 52 | 53 | // android 54 | implementation 'androidx.core:core-ktx:1.1.0' 55 | 56 | // test 57 | testImplementation 'junit:junit:4.12' 58 | testImplementation("androidx.test:runner:1.2.0") 59 | testImplementation 'androidx.test.ext:junit:1.1.1' 60 | testImplementation 'androidx.test.espresso:espresso-core:3.2.0' 61 | testImplementation "org.robolectric:robolectric:4.3.1" 62 | testImplementation 'com.willowtreeapps.assertk:assertk-jvm:0.20' 63 | } 64 | 65 | apply from: '../ktlint/ktlint.gradle' 66 | apply from: './gradle-mvn-push.gradle' 67 | -------------------------------------------------------------------------------- /library/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorgeCastilloPrz/AndroidColorX/3cc37aa242b35b1bd152bbf62219e9e443f3a9c8/library/consumer-rules.pro -------------------------------------------------------------------------------- /library/gradle-mvn-push.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven' 2 | apply plugin: 'signing' 3 | 4 | def isReleaseBuild() { 5 | return VERSION_NAME.contains("SNAPSHOT") == false 6 | } 7 | 8 | def getReleaseRepositoryUrl() { 9 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 10 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 11 | } 12 | 13 | def getSnapshotRepositoryUrl() { 14 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 15 | : "https://oss.sonatype.org/content/repositories/snapshots/" 16 | } 17 | 18 | def getRepositoryUsername() { 19 | return hasProperty('nexusUsername') ? nexusUsername : "" 20 | } 21 | 22 | def getRepositoryPassword() { 23 | return hasProperty('nexusPassword') ? nexusPassword : "" 24 | } 25 | 26 | afterEvaluate { project -> 27 | uploadArchives { 28 | repositories { 29 | mavenDeployer { 30 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 31 | 32 | pom.groupId = GROUP 33 | pom.artifactId = POM_ARTIFACT_ID 34 | pom.version = VERSION_NAME 35 | 36 | repository(url: getReleaseRepositoryUrl()) { 37 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 38 | } 39 | snapshotRepository(url: getSnapshotRepositoryUrl()) { 40 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 41 | } 42 | 43 | pom.project { 44 | name POM_NAME 45 | packaging POM_PACKAGING 46 | description POM_DESCRIPTION 47 | url POM_URL 48 | 49 | scm { 50 | url POM_SCM_URL 51 | connection POM_SCM_CONNECTION 52 | developerConnection POM_SCM_DEV_CONNECTION 53 | } 54 | 55 | licenses { 56 | license { 57 | name POM_LICENCE_NAME 58 | url POM_LICENCE_URL 59 | distribution POM_LICENCE_DIST 60 | } 61 | } 62 | 63 | developers { 64 | developer { 65 | id POM_DEVELOPER_ID 66 | name POM_DEVELOPER_NAME 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | signing { 75 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 76 | sign configurations.archives 77 | } 78 | 79 | task androidJavadocs(type: Javadoc) { 80 | excludes = ['**/*.kt'] 81 | source = android.sourceSets.main.java.srcDirs 82 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 83 | } 84 | 85 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 86 | classifier = 'javadoc' 87 | from androidJavadocs.destinationDir 88 | } 89 | 90 | task androidSourcesJar(type: Jar) { 91 | classifier = 'sources' 92 | from android.sourceSets.main.java.sourceFiles 93 | } 94 | 95 | artifacts { 96 | archives androidSourcesJar 97 | archives androidJavadocsJar 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=AndroidColorX Library 2 | POM_ARTIFACT_ID=androidcolorx 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /library/src/main/java/dev/jorgecastillo/androidcolorx/library/ARGBColor.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.graphics.Color 4 | import androidx.annotation.ColorInt 5 | import androidx.core.graphics.ColorUtils 6 | 7 | data class ARGBColor( 8 | val alpha: Int, 9 | val red: Int, 10 | val green: Int, 11 | val blue: Int 12 | ) { 13 | override fun toString(): String { 14 | return "$alpha / $red / $green / $blue" 15 | } 16 | } 17 | 18 | fun @receiver:ColorInt Int.asArgb(): ARGBColor = 19 | ARGBColor(Color.alpha(this), Color.red(this), Color.green(this), Color.blue(this)) 20 | 21 | @ColorInt 22 | fun ARGBColor.asColorInt(): Int = Color.argb(alpha, red, green, blue) 23 | 24 | fun ARGBColor.asRgb(): RGBColor = asColorInt().asRgb() 25 | 26 | fun ARGBColor.asCmyk(): CMYKColor = asColorInt().asCmyk() 27 | 28 | fun ARGBColor.asHex(): HEXColor = asColorInt().asHex() 29 | 30 | fun ARGBColor.asHsl(): HSLColor = asColorInt().asHsl() 31 | 32 | fun ARGBColor.asHsla(): HSLAColor = asColorInt().asHsla() 33 | 34 | fun ARGBColor.asHsv(): HSVColor = asColorInt().asHsv() 35 | 36 | /** 37 | * @param value amount to lighten in the range 0...1 38 | */ 39 | fun ARGBColor.lighten(value: Float): ARGBColor = this.asColorInt().lighten(value).asArgb() 40 | 41 | /** 42 | * @param value amount to lighten in the range 0...100 43 | */ 44 | fun ARGBColor.lighten(value: Int): ARGBColor = this.asColorInt().lighten(value).asArgb() 45 | 46 | /** 47 | * @param value amount to darken in the range 0...1 48 | */ 49 | fun ARGBColor.darken(value: Float): ARGBColor = this.asColorInt().darken(value).asArgb() 50 | 51 | /** 52 | * @param value amount to darken in the range 0...100 53 | */ 54 | fun ARGBColor.darken(value: Int): ARGBColor = this.asColorInt().darken(value).asArgb() 55 | 56 | /** 57 | * @return a list of shades for the given color like the ones in https://www.color-hex.com/color/e91e63. 58 | * Each one of the colors is a ARGBColor. 59 | * 60 | * @param count of shades to generate over the source color. It generates 10 by default. 61 | */ 62 | fun ARGBColor.shades(count: Int = 10): List = asColorInt().shades(count).map { it.asArgb() } 63 | 64 | /** 65 | * @return a list of tints for the given color like the ones in https://www.color-hex.com/color/e91e63. 66 | * Each one of the colors is a ARGBColor. 67 | * 68 | * @param count of tints to generate over the source color. It generates 10 by default. 69 | */ 70 | fun ARGBColor.tints(count: Int = 10): List = asColorInt().tints(count).map { it.asArgb() } 71 | 72 | /** 73 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 74 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The complimentary color stands for the 75 | * color in the opposite side of the circle, so it's (hue + 180) % 360. 76 | */ 77 | fun ARGBColor.complimentary(): ARGBColor = asColorInt().complimentary().asArgb() 78 | 79 | /** 80 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 81 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The triadic colors stand for 3 colors that 82 | * compose a perfect triangle (equal sides) over the circle, so they are equally far from each other. 83 | * 84 | * Triadic colors for h0 would be (hue + 120) % 360 and (hue + 240) % 360. 85 | */ 86 | fun ARGBColor.triadic(): Pair = 87 | asColorInt().triadic().let { Pair(it.first.asArgb(), it.second.asArgb()) } 88 | 89 | /** 90 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 91 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The tetradic colors stand for 4 colors 92 | * that compose a perfect square (equal sides) over the circle, so they are equally far from each other. 93 | * 94 | * Tetradic colors for h0 would be (hue + 90) % 360, (hue + 180) % 360 and (hue + 270) % 360. 95 | */ 96 | fun ARGBColor.tetradic(): Triple = 97 | asColorInt().tetradic().let { Triple(it.first.asArgb(), it.second.asArgb(), it.third.asArgb()) } 98 | 99 | /** 100 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 101 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The analogous colors stand for 3 colors 102 | * that are together in the circle, separated by 30 degrees each. 103 | * 104 | * Analogous colors for h0 would be (hue + 30) % 360 & (hue - 30) % 360. 105 | */ 106 | fun ARGBColor.analogous(): Pair = 107 | asColorInt().analogous().let { Pair(it.first.asArgb(), it.second.asArgb()) } 108 | 109 | /** 110 | * Check if a color is dark (convert to XYZ & check Y component) 111 | */ 112 | fun ARGBColor.isDark(): Boolean = ColorUtils.calculateLuminance(this.asColorInt()) < 0.5 113 | 114 | /** 115 | * Returns a color that contrasts nicely with the one given (receiver). Fallbacks to white and 116 | * black, but optional light and dark colors can be passed. 117 | */ 118 | fun ARGBColor.contrasting( 119 | lightColor: ARGBColor = ARGBColor(255, 255, 255, 255), 120 | darkColor: ARGBColor = ARGBColor(255, 0, 0, 0) 121 | ) = if (isDark()) { 122 | lightColor 123 | } else { 124 | darkColor 125 | } 126 | -------------------------------------------------------------------------------- /library/src/main/java/dev/jorgecastillo/androidcolorx/library/CMYKColor.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.graphics.Color 4 | import androidx.annotation.ColorInt 5 | import androidx.annotation.FloatRange 6 | import androidx.core.graphics.ColorUtils 7 | import kotlin.math.max 8 | 9 | data class CMYKColor( 10 | val cyan: Float, 11 | val magenta: Float, 12 | val yellow: Float, 13 | val key: Float 14 | ) { 15 | override fun toString(): String { 16 | return "$cyan / $magenta / $yellow / $key" 17 | } 18 | } 19 | 20 | /** 21 | * Formula extracted from {@see https://www.rapidtables.com/convert/color/rgb-to-cmyk.html}. 22 | */ 23 | fun @receiver:ColorInt Int.asCmyk(): CMYKColor { 24 | val r = Color.red(this) 25 | val g = Color.green(this) 26 | val b = Color.blue(this) 27 | 28 | val r1 = r / 255f 29 | val g1 = g / 255f 30 | val b1 = b / 255f 31 | 32 | val k = 1.0f - max(r1, max(g1, b1)) 33 | 34 | val cyan = if (k == 1f) 0f else (1.0f - r1 - k) / (1.0f - k) 35 | val magenta = if (k == 1f) 0f else (1.0f - g1 - k) / (1.0f - k) 36 | val yellow = if (k == 1f) 0f else (1.0f - b1 - k) / (1.0f - k) 37 | 38 | return CMYKColor(cyan, magenta, yellow, k) 39 | } 40 | 41 | @ColorInt 42 | fun CMYKColor.asColorInt(): Int { 43 | val red = (1 - cyan) * (1 - key) 44 | val green = (1 - magenta) * (1 - key) 45 | val blue = (1 - yellow) * (1 - key) 46 | 47 | return rgbColorInt(red, green, blue) 48 | } 49 | 50 | fun CMYKColor.asRgb(): RGBColor = asColorInt().asRgb() 51 | 52 | fun CMYKColor.asArgb(): ARGBColor = asColorInt().asArgb() 53 | 54 | fun CMYKColor.asHex(): HEXColor = asColorInt().asHex() 55 | 56 | fun CMYKColor.asHsl(): HSLColor { 57 | val red = (1 - cyan) * (1 - key) 58 | val green = (1 - magenta) * (1 - key) 59 | val blue = (1 - yellow) * (1 - key) 60 | 61 | return FloatArray(3).apply { 62 | rgbFToHsl(red, green, blue, this) 63 | }.let { 64 | HSLColor(it[0], it[1], it[2]) 65 | } 66 | } 67 | 68 | /** 69 | * Convert RGB components defined by floats [0..1] to HSL (hue-saturation-lightness). 70 | * 71 | * * outHsl[0] is Hue [0 .. 360) 72 | * * outHsl[1] is Saturation [0...1] 73 | * * outHsl[2] is Lightness [0...1] 74 | * 75 | * 76 | * @param r red component value [0..1] 77 | * @param g green component value [0..1] 78 | * @param b blue component value [0..1] 79 | * @param outHsl 3-element array which holds the resulting HSL components 80 | */ 81 | fun rgbFToHsl( 82 | @FloatRange(from = 0.0, to = 1.0) rf: Float, 83 | @FloatRange(from = 0.0, to = 1.0) gf: Float, 84 | @FloatRange(from = 0.0, to = 1.0) bf: Float, 85 | outHsl: FloatArray 86 | ) { 87 | val max = Math.max(rf, Math.max(gf, bf)) 88 | val min = Math.min(rf, Math.min(gf, bf)) 89 | val deltaMaxMin = max - min 90 | 91 | var h: Float 92 | val s: Float 93 | val l = (max + min) / 2f 94 | 95 | if (max == min) { 96 | // Monochromatic 97 | s = 0f 98 | h = s 99 | } else { 100 | if (max == rf) { 101 | h = (gf - bf) / deltaMaxMin % 6f 102 | } else if (max == gf) { 103 | h = (bf - rf) / deltaMaxMin + 2f 104 | } else { 105 | h = (rf - gf) / deltaMaxMin + 4f 106 | } 107 | 108 | s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)) 109 | } 110 | 111 | h = h * 60f % 360f 112 | if (h < 0) { 113 | h += 360f 114 | } 115 | 116 | outHsl[0] = constrain(h, 0f, 360f) 117 | outHsl[1] = constrain(s, 0f, 1f) 118 | outHsl[2] = constrain(l, 0f, 1f) 119 | } 120 | 121 | fun CMYKColor.asHsla(): HSLAColor = 122 | asHsl().let { HSLAColor(it.hue, it.saturation, it.lightness, 1f) } 123 | 124 | fun CMYKColor.asHsv(): HSVColor = asColorInt().asHsv() 125 | 126 | /** 127 | * @param value amount to lighten in the range 0...1 128 | */ 129 | fun CMYKColor.lighten(value: Float): CMYKColor = this.asColorInt().lighten(value).asCmyk() 130 | 131 | /** 132 | * @param value amount to lighten in the range 0...100 133 | */ 134 | fun CMYKColor.lighten(value: Int): CMYKColor = this.asColorInt().lighten(value).asCmyk() 135 | 136 | /** 137 | * @param value amount to darken in the range 0...1 138 | */ 139 | fun CMYKColor.darken(value: Float): CMYKColor = this.asColorInt().darken(value).asCmyk() 140 | 141 | /** 142 | * @param value amount to darken in the range 0...100 143 | */ 144 | fun CMYKColor.darken(value: Int): CMYKColor = this.asColorInt().darken(value).asCmyk() 145 | 146 | /** 147 | * @return a list of shades for the given color like the ones in https://www.color-hex.com/color/e91e63. 148 | * Each one of the colors is a CMYKColor. 149 | * 150 | * @param count of shades to generate over the source color. It generates 10 by default. 151 | */ 152 | fun CMYKColor.shades(count: Int = 10): List = 153 | asColorInt().shades(count).map { it.asCmyk() } 154 | 155 | /** 156 | * @return a list of tints for the given color like the ones in https://www.color-hex.com/color/e91e63. 157 | * Each one of the colors is a CMYKColor. 158 | * 159 | * @param count of tints to generate over the source color. It generates 10 by default. 160 | */ 161 | fun CMYKColor.tints(count: Int = 10): List = 162 | asColorInt().tints(count).map { it.asCmyk() } 163 | 164 | /** 165 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 166 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The complimentary color stands for the 167 | * color in the opposite side of the circle, so it's (hue + 180) % 360. 168 | */ 169 | fun CMYKColor.complimentary(): CMYKColor = asColorInt().complimentary().asCmyk() 170 | 171 | /** 172 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 173 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The triadic colors stand for 3 colors that 174 | * compose a perfect triangle (equal sides) over the circle, so they are equally far from each other. 175 | * 176 | * Triadic colors for h0 would be (hue + 120) % 360 and (hue + 240) % 360. 177 | */ 178 | fun CMYKColor.triadic(): Pair = 179 | asColorInt().triadic().let { Pair(it.first.asCmyk(), it.second.asCmyk()) } 180 | 181 | /** 182 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 183 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The tetradic colors stand for 4 colors 184 | * that compose a perfect square (equal sides) over the circle, so they are equally far from each other. 185 | * 186 | * Tetradic colors for h0 would be (hue + 90) % 360, (hue + 180) % 360 and (hue + 270) % 360. 187 | */ 188 | fun CMYKColor.tetradic(): Triple = 189 | asColorInt().tetradic().let { Triple(it.first.asCmyk(), it.second.asCmyk(), it.third.asCmyk()) } 190 | 191 | /** 192 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 193 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The analogous colors stand for 3 colors 194 | * that are together in the circle, separated by 30 degrees each. 195 | * 196 | * Analogous colors for h0 would be (hue + 30) % 360 & (hue - 30) % 360. 197 | */ 198 | fun CMYKColor.analogous(): Pair = 199 | asColorInt().analogous().let { Pair(it.first.asCmyk(), it.second.asCmyk()) } 200 | 201 | /** 202 | * Check if a color is dark (convert to XYZ & check Y component) 203 | */ 204 | fun CMYKColor.isDark(): Boolean = ColorUtils.calculateLuminance(this.asColorInt()) < 0.5 205 | 206 | /** 207 | * Returns a color that contrasts nicely with the one given (receiver). Fallbacks to white and 208 | * black, but optional light and dark colors can be passed. 209 | */ 210 | fun CMYKColor.contrasting( 211 | lightColor: CMYKColor = CMYKColor(0f, 0f, 0f, 0f), // white 212 | darkColor: CMYKColor = CMYKColor(0f, 0f, 0f, 1f) // black 213 | ) = if (isDark()) { 214 | lightColor 215 | } else { 216 | darkColor 217 | } 218 | -------------------------------------------------------------------------------- /library/src/main/java/dev/jorgecastillo/androidcolorx/library/ColorIntExtensions.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.graphics.Color 4 | import androidx.annotation.ColorInt 5 | import androidx.annotation.IntRange 6 | import androidx.core.graphics.ColorUtils 7 | import kotlin.math.abs 8 | import kotlin.math.roundToInt 9 | 10 | /** 11 | * Return a color-int from red, green, blue float components 12 | * in the range \([0..1]\). The alpha component is implicitly 13 | * 1.0 (fully opaque). If the components are out of range, the 14 | * returned color is undefined. 15 | * 16 | * @param red Red component \([0..1]\) of the color 17 | * @param green Green component \([0..1]\) of the color 18 | * @param blue Blue component \([0..1]\) of the color 19 | */ 20 | @ColorInt 21 | fun rgbColorInt(red: Float, green: Float, blue: Float): Int { 22 | return -0x1000000 or 23 | ((red * 255.0f + 0.5f).toInt() shl 16) or 24 | ((green * 255.0f + 0.5f).toInt() shl 8) or 25 | (blue * 255.0f + 0.5f).toInt() 26 | } 27 | 28 | /** 29 | * Convert HSL (hue-saturation-lightness) components to a RGB color. 30 | * 31 | * * hsl[0] is Hue [0 .. 360) 32 | * * hsl[1] is Saturation [0...1] 33 | * * hsl[2] is Lightness [0...1] 34 | * 35 | * If hsv values are out of range, they are pinned. 36 | * 37 | * @param hsl 3-element array which holds the input HSL components 38 | * @return the resulting RGB color 39 | */ 40 | @ColorInt 41 | fun hslToColor(hsl: FloatArray): Int { 42 | val h = hsl[0] 43 | val s = hsl[1] 44 | val l = hsl[2] 45 | 46 | val c = (1f - abs(2 * l - 1f)) * s 47 | val m = l - 0.5f * c 48 | val x = c * (1f - abs(h / 60f % 2f - 1f)) 49 | 50 | val hueSegment = h.toInt() / 60 51 | 52 | var r = 0f 53 | var g = 0f 54 | var b = 0f 55 | 56 | when (hueSegment) { 57 | 0 -> { 58 | r = (c + m) 59 | g = (x + m) 60 | b = m 61 | } 62 | 1 -> { 63 | r = (x + m) 64 | g = (c + m) 65 | b = m 66 | } 67 | 2 -> { 68 | r = m 69 | g = (c + m) 70 | b = (x + m) 71 | } 72 | 3 -> { 73 | r = m 74 | g = (x + m) 75 | b = (c + m) 76 | } 77 | 4 -> { 78 | r = (x + m) 79 | g = m 80 | b = (c + m) 81 | } 82 | 5, 6 -> { 83 | r = (c + m) 84 | g = m 85 | b = (x + m) 86 | } 87 | } 88 | 89 | r = constrain(r, 0f, 1f) 90 | g = constrain(g, 0f, 1f) 91 | b = constrain(b, 0f, 1f) 92 | 93 | return rgbColorInt(r, g, b) 94 | } 95 | 96 | /** 97 | * Convert RGB components to HSL (hue-saturation-lightness). 98 | * 99 | * * outHsl[0] is Hue [0 .. 360) 100 | * * outHsl[1] is Saturation [0...1] 101 | * * outHsl[2] is Lightness [0...1] 102 | * 103 | * 104 | * @param r red component value [0..255] 105 | * @param g green component value [0..255] 106 | * @param b blue component value [0..255] 107 | * @param outHsl 3-element array which holds the resulting HSL components 108 | */ 109 | fun rgbToHsl( 110 | @IntRange(from = 0x0, to = 0xFF) r: Int, 111 | @IntRange(from = 0x0, to = 0xFF) g: Int, 112 | @IntRange(from = 0x0, to = 0xFF) b: Int, 113 | outHsl: FloatArray 114 | ) { 115 | val rf = r / 255f 116 | val gf = g / 255f 117 | val bf = b / 255f 118 | 119 | val max = Math.max(rf, Math.max(gf, bf)) 120 | val min = Math.min(rf, Math.min(gf, bf)) 121 | val deltaMaxMin = max - min 122 | 123 | var h: Float 124 | val s: Float 125 | val l = (max + min) / 2f 126 | 127 | if (max == min) { 128 | // Monochromatic 129 | s = 0f 130 | h = s 131 | } else { 132 | if (max == rf) { 133 | h = (gf - bf) / deltaMaxMin % 6f 134 | } else if (max == gf) { 135 | h = (bf - rf) / deltaMaxMin + 2f 136 | } else { 137 | h = (rf - gf) / deltaMaxMin + 4f 138 | } 139 | 140 | s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)) 141 | } 142 | 143 | h = h * 60f % 360f 144 | if (h < 0) { 145 | h += 360f 146 | } 147 | 148 | outHsl[0] = constrain(h, 0f, 360f) 149 | outHsl[1] = constrain(s, 0f, 1f) 150 | outHsl[2] = constrain(l, 0f, 1f) 151 | } 152 | 153 | /** 154 | * Check if a color is dark (convert to XYZ & check Y component) 155 | */ 156 | fun @receiver:ColorInt Int.isDark(): Boolean = ColorUtils.calculateLuminance(this) < 0.5 157 | 158 | /** 159 | * Returns a color that contrasts nicely with the one given (receiver). Fallbacks to white and 160 | * black, but optional light and dark colors can be passed. 161 | */ 162 | fun @receiver:ColorInt Int.contrasting( 163 | @ColorInt lightColor: Int = Color.WHITE, 164 | @ColorInt darkColor: Int = Color.BLACK 165 | ) = if (isDark()) { 166 | lightColor 167 | } else { 168 | darkColor 169 | } 170 | 171 | /** 172 | * @param value amount to lighten in the range 0...1 173 | */ 174 | @ColorInt 175 | fun @receiver:ColorInt Int.lighten(value: Float): Int { 176 | val hsla = this.asHsla() 177 | var lightness = hsla.lightness 178 | lightness += value 179 | lightness = 0f.coerceAtLeast(lightness.coerceAtMost(1f)) 180 | return HSLAColor(hsla.hue, hsla.saturation, lightness, hsla.alpha).asColorInt() 181 | } 182 | 183 | /** 184 | * @param value amount to lighten in the range 0...100 185 | */ 186 | @ColorInt 187 | fun @receiver:ColorInt Int.lighten(value: Int): Int { 188 | val hsla = this.asHsla() 189 | var lightness = hsla.lightness 190 | lightness += value / 100f 191 | lightness = 0f.coerceAtLeast(lightness.coerceAtMost(1f)) 192 | return HSLAColor(hsla.hue, hsla.saturation, lightness, hsla.alpha).asColorInt() 193 | } 194 | 195 | /** 196 | * @param value amount to darken in the range 0...1 197 | */ 198 | @ColorInt 199 | fun @receiver:ColorInt Int.darken(value: Float): Int { 200 | val hsla = this.asHsla() 201 | var lightness = hsla.lightness 202 | lightness -= value 203 | lightness = 0f.coerceAtLeast(lightness.coerceAtMost(1f)) 204 | return HSLAColor(hsla.hue, hsla.saturation, lightness, hsla.alpha).asColorInt() 205 | } 206 | 207 | /** 208 | * @param value amount to darken in the range 0...100 209 | */ 210 | @ColorInt 211 | fun @receiver:ColorInt Int.darken(value: Int): Int { 212 | val hsla = this.asHsla() 213 | var lightness = hsla.lightness 214 | lightness -= value / 100f 215 | lightness = 0f.coerceAtLeast(lightness.coerceAtMost(1f)) 216 | return HSLAColor(hsla.hue, hsla.saturation, lightness, hsla.alpha).asColorInt() 217 | } 218 | 219 | /** 220 | * @return a list of shades for the given color like the ones in https://www.color-hex.com/color/e91e63. 221 | * Each one of the colors is a ColorInt. 222 | * 223 | * @param count of shades to generate over the source color. It generates 10 by default. 224 | */ 225 | fun @receiver:ColorInt Int.shades(count: Int = 10): List { 226 | require(count > 0) { "count must be > 0" } 227 | val hsla = this.asHsla() 228 | 229 | val start = (hsla.lightness * 10000000).roundToInt() 230 | val step = if (start > 0) { 231 | -1 * start / count 232 | } else 1 233 | return IntProgression.fromClosedRange(start, 0, step).map { i -> 234 | hsla.copy(lightness = i / 10000000f).asColorInt() 235 | } 236 | } 237 | 238 | /** 239 | * @return a list of tints for the given color like the ones in https://www.color-hex.com/color/e91e63. 240 | * Each one of the colors is a ColorInt. 241 | * 242 | * @param count of tints to generate over the source color. It generates 10 by default. 243 | */ 244 | fun @receiver:ColorInt Int.tints(count: Int = 10): List { 245 | require(count > 0) { "count must be > 0" } 246 | val hsla = this.asHsla() 247 | 248 | val start = (hsla.lightness * 10000000).roundToInt() 249 | val step = if (start < 10000000) (10000000 - start) / count else 1 250 | return IntProgression.fromClosedRange(start, 10000000, step).map { i -> 251 | hsla.copy(lightness = i / 10000000f).asColorInt() 252 | } 253 | } 254 | 255 | /** 256 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 257 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The complimentary color stands for the 258 | * color in the opposite side of the circle, so it's (hue + 180) % 360. 259 | */ 260 | fun @receiver:ColorInt Int.complimentary(): Int { 261 | val colorHSLA = this.asHsla() 262 | 263 | val hue = colorHSLA.hue // 0° to 359° 264 | val complimentaryHue = (hue + 180) % 360 265 | return colorHSLA.copy(hue = complimentaryHue).asColorInt() 266 | } 267 | 268 | /** 269 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 270 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The triadic colors stand for 3 colors that 271 | * compose a perfect triangle (equal sides) over the circle, so they are equally far from each other. 272 | * 273 | * Triadic colors for h0 would be (hue + 120) % 360 and (hue + 240) % 360. 274 | */ 275 | fun @receiver:ColorInt Int.triadic(): Pair { 276 | val colorHSLA = this.asHsla() 277 | val hue = colorHSLA.hue // 0° to 359° 278 | 279 | val h1 = colorHSLA.copy(hue = (hue + 120) % 360) 280 | val h2 = colorHSLA.copy(hue = (hue + 240) % 360) 281 | 282 | return Pair(h1.asColorInt(), h2.asColorInt()) 283 | } 284 | 285 | /** 286 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 287 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The tetradic colors stand for 4 colors 288 | * that compose a perfect square (equal sides) over the circle, so they are equally far from each other. 289 | * 290 | * Tetradic colors for h0 would be (hue + 90) % 360, (hue + 180) % 360 and (hue + 270) % 360. 291 | */ 292 | fun @receiver:ColorInt Int.tetradic(): Triple { 293 | val colorHSLA = this.asHsla() 294 | val hue = colorHSLA.hue // 0° to 359° 295 | 296 | val h1 = colorHSLA.copy(hue = (hue + 90) % 360) 297 | val h2 = colorHSLA.copy(hue = (hue + 180) % 360) 298 | val h3 = colorHSLA.copy(hue = (hue + 270) % 360) 299 | 300 | return Triple(h1.asColorInt(), h2.asColorInt(), h3.asColorInt()) 301 | } 302 | 303 | /** 304 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 305 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The analogous colors stand for 3 colors 306 | * that are together in the circle, separated by 30 degrees each. 307 | * 308 | * Analogous colors for h0 would be (hue + 30) % 360 & (hue - 30) % 360. 309 | */ 310 | fun @receiver:ColorInt Int.analogous(): Pair { 311 | val colorHSLA = this.asHsla() 312 | val hue = colorHSLA.hue // 0° to 359° 313 | 314 | val h1 = colorHSLA.copy(hue = (hue + 30) % 360) 315 | val h2 = colorHSLA.copy(hue = (hue + 330) % 360) 316 | 317 | return Pair(h1.asColorInt(), h2.asColorInt()) 318 | } 319 | -------------------------------------------------------------------------------- /library/src/main/java/dev/jorgecastillo/androidcolorx/library/HEXColor.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.graphics.Color 4 | import androidx.annotation.ColorInt 5 | import androidx.core.graphics.ColorUtils 6 | 7 | /** 8 | * Constructor: 9 | * 10 | * Parses the color string, and return the corresponding color-int. If the string cannot be parsed, 11 | * throws an IllegalArgumentException exception. Supported formats are: 12 | * #RRGGBB 13 | * #AARRGGBB 14 | * The following names are also accepted: red, blue, green, black, white, gray, cyan, magenta, 15 | * yellow, lightgray, darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy, olive, 16 | * purple, silver, and teal 17 | */ 18 | data class HEXColor(val hex: String) { 19 | init { 20 | // This is to enforce proper parsing. If the hex code does not match the Color.parseColor 21 | // format requirements we want it to throw. 22 | Color.parseColor(hex) 23 | } 24 | 25 | override fun toString(): String { 26 | return hex 27 | } 28 | 29 | override fun equals(other: Any?): Boolean { 30 | return other is HEXColor && asColorInt() == other.asColorInt() 31 | } 32 | } 33 | 34 | /** 35 | * Returns the color in complete hex format as in #FFFFFF. 36 | */ 37 | fun @receiver:ColorInt Int.asHex(): HEXColor = 38 | HEXColor(String.format("#%06X", 0xFFFFFFFF and this.toLong())) 39 | 40 | @ColorInt 41 | fun HEXColor.asColorInt(): Int = Color.parseColor(hex) 42 | 43 | fun HEXColor.asRgb(): RGBColor = asColorInt().asRgb() 44 | 45 | fun HEXColor.asArgb(): ARGBColor = asColorInt().asArgb() 46 | 47 | fun HEXColor.asCmyk(): CMYKColor = asColorInt().asCmyk() 48 | 49 | fun HEXColor.asHsl(): HSLColor = asColorInt().asHsl() 50 | 51 | fun HEXColor.asHsla(): HSLAColor = asColorInt().asHsla() 52 | 53 | fun HEXColor.asHsv(): HSVColor = asColorInt().asHsv() 54 | 55 | /** 56 | * @param value amount to lighten in the range 0...1 57 | */ 58 | fun HEXColor.lighten(value: Float): HEXColor = this.asColorInt().lighten(value).asHex() 59 | 60 | /** 61 | * @param value amount to lighten in the range 0...100 62 | */ 63 | fun HEXColor.lighten(value: Int): HEXColor = this.asColorInt().lighten(value).asHex() 64 | 65 | /** 66 | * @param value amount to darken in the range 0...1 67 | */ 68 | fun HEXColor.darken(value: Float): HEXColor = this.asColorInt().darken(value).asHex() 69 | 70 | /** 71 | * @param value amount to darken in the range 0...100 72 | */ 73 | fun HEXColor.darken(value: Int): HEXColor = this.asColorInt().darken(value).asHex() 74 | 75 | /** 76 | * @return a list of shades for the given color like the ones in https://www.color-hex.com/color/e91e63. 77 | * Each one of the colors is a HEXColor. 78 | * 79 | * @param count of shades to generate over the source color. It generates 10 by default. 80 | */ 81 | fun HEXColor.shades(count: Int = 10): List = asColorInt().shades(count).map { it.asHex() } 82 | 83 | /** 84 | * @return a list of tints for the given color like the ones in https://www.color-hex.com/color/e91e63. 85 | * Each one of the colors is a HEXColor. 86 | * 87 | * @param count of tints to generate over the source color. It generates 10 by default. 88 | */ 89 | fun HEXColor.tints(count: Int = 10): List = asColorInt().tints(count).map { it.asHex() } 90 | 91 | /** 92 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 93 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The complimentary color stands for the 94 | * color in the opposite side of the circle, so it's (hue + 180) % 360. 95 | */ 96 | fun HEXColor.complimentary(): HEXColor = asColorInt().complimentary().asHex() 97 | 98 | /** 99 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 100 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The triadic colors stand for 3 colors that 101 | * compose a perfect triangle (equal sides) over the circle, so they are equally far from each other. 102 | * 103 | * Triadic colors for h0 would be (hue + 120) % 360 and (hue + 240) % 360. 104 | */ 105 | fun HEXColor.triadic(): Pair = 106 | asColorInt().triadic().let { Pair(it.first.asHex(), it.second.asHex()) } 107 | 108 | /** 109 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 110 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The tetradic colors stand for 4 colors 111 | * that compose a perfect square (equal sides) over the circle, so they are equally far from each other. 112 | * 113 | * Tetradic colors for h0 would be (hue + 90) % 360, (hue + 180) % 360 and (hue + 270) % 360. 114 | */ 115 | fun HEXColor.tetradic(): Triple = 116 | asColorInt().tetradic().let { Triple(it.first.asHex(), it.second.asHex(), it.third.asHex()) } 117 | 118 | /** 119 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 120 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The analogous colors stand for 3 colors 121 | * that are together in the circle, separated by 30 degrees each. 122 | * 123 | * Analogous colors for h0 would be (hue + 30) % 360 & (hue - 30) % 360. 124 | */ 125 | fun HEXColor.analogous(): Pair = 126 | asColorInt().analogous().let { Pair(it.first.asHex(), it.second.asHex()) } 127 | 128 | /** 129 | * Check if a color is dark (convert to XYZ & check Y component) 130 | */ 131 | fun HEXColor.isDark(): Boolean = ColorUtils.calculateLuminance(this.asColorInt()) < 0.5 132 | 133 | /** 134 | * Returns a color that contrasts nicely with the one given (receiver). Fallbacks to white and 135 | * black, but optional light and dark colors can be passed. 136 | */ 137 | fun HEXColor.contrasting( 138 | lightColor: HEXColor = HEXColor("#FFFFFF"), 139 | darkColor: HEXColor = HEXColor("#000000") 140 | ) = if (isDark()) { 141 | lightColor 142 | } else { 143 | darkColor 144 | } 145 | -------------------------------------------------------------------------------- /library/src/main/java/dev/jorgecastillo/androidcolorx/library/HSLAColor.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.graphics.Color 4 | import androidx.annotation.ColorInt 5 | import androidx.annotation.NonNull 6 | import androidx.core.graphics.ColorUtils 7 | import androidx.core.graphics.ColorUtils.HSLToColor 8 | import kotlin.math.roundToInt 9 | 10 | /** 11 | * HSLA stands for hue-saturation-lightness-alpha. 12 | * 13 | * Hue is a value from 0...360 14 | * Saturation is a value from 0...1 15 | * Lightness is a value from 0...1 16 | * Alpha is a value from 0...1 17 | */ 18 | data class HSLAColor( 19 | val hue: Float, 20 | val saturation: Float, 21 | val lightness: Float, 22 | val alpha: Float 23 | ) { 24 | override fun toString(): String { 25 | return "${hue}º / $saturation / $lightness / $alpha" 26 | } 27 | } 28 | 29 | @NonNull 30 | fun @receiver:ColorInt Int.asHsla(): HSLAColor = this.let { color -> 31 | FloatArray(3).apply { ColorUtils.colorToHSL(color, this) }.let { 32 | HSLAColor(it[0], it[1], it[2], Color.alpha(color) / 255f) 33 | } 34 | } 35 | 36 | fun HSLAColor.asFloatArray(): FloatArray = floatArrayOf(hue, saturation, lightness, alpha) 37 | 38 | @ColorInt 39 | fun HSLAColor.asColorInt(): Int = 40 | HSLToColor(asFloatArray().dropLast(1).toFloatArray()).let { 41 | Color.argb((alpha * 255).toInt(), Color.red(it), Color.green(it), Color.blue(it)) 42 | } 43 | 44 | fun HSLAColor.asRgb(): RGBColor = asColorInt().asRgb() 45 | 46 | fun HSLAColor.asArgb(): ARGBColor = asColorInt().asArgb() 47 | 48 | fun HSLAColor.asCmyk(): CMYKColor = asHsl().asCmyk() 49 | 50 | fun HSLAColor.asHex(): HEXColor = asColorInt().asHex() 51 | 52 | fun HSLAColor.asHsl(): HSLColor = HSLColor(hue, saturation, lightness) 53 | 54 | fun HSLAColor.asHsv(): HSVColor = asColorInt().asHsv() 55 | 56 | /** 57 | * @param value amount to lighten in the range 0...1 58 | */ 59 | fun HSLAColor.lighten(value: Float): HSLAColor { 60 | var mutableLightness = this.lightness 61 | mutableLightness += value 62 | mutableLightness = 0f.coerceAtLeast(mutableLightness.coerceAtMost(1f)) 63 | return HSLAColor(hue, saturation, mutableLightness, alpha) 64 | } 65 | 66 | /** 67 | * @param value amount to lighten in the range 0...100 68 | */ 69 | fun HSLAColor.lighten(value: Int): HSLAColor { 70 | var mutableLightness = this.lightness 71 | mutableLightness += value / 100f 72 | mutableLightness = 0f.coerceAtLeast(mutableLightness.coerceAtMost(1f)) 73 | return HSLAColor(hue, saturation, mutableLightness, alpha) 74 | } 75 | 76 | /** 77 | * @param value amount to darken in the range 0...1 78 | */ 79 | fun HSLAColor.darken(value: Float): HSLAColor { 80 | var mutableLightness = this.lightness 81 | mutableLightness -= value 82 | mutableLightness = 0f.coerceAtLeast(mutableLightness.coerceAtMost(1f)) 83 | return HSLAColor(hue, saturation, mutableLightness, alpha) 84 | } 85 | 86 | /** 87 | * @param value amount to darken in the range 0...100 88 | */ 89 | fun HSLAColor.darken(value: Int): HSLAColor { 90 | var mutableLightness = this.lightness 91 | mutableLightness -= value / 100f 92 | mutableLightness = 0f.coerceAtLeast(mutableLightness.coerceAtMost(1f)) 93 | return HSLAColor(hue, saturation, mutableLightness, alpha) 94 | } 95 | 96 | /** 97 | * @return a list of shades for the given color like the ones in https://www.color-hex.com/color/e91e63. 98 | * Each one of the colors is a HSLColor. 99 | * 100 | * @param count of shades to generate over the source color. It generates 10 by default. 101 | */ 102 | fun HSLAColor.shades(count: Int = 10): List { 103 | require(count > 0) { "count must be > 0" } 104 | val start = (this.lightness * 10000000).roundToInt() 105 | val step = if (start > 0) { 106 | -1 * start / count 107 | } else 1 108 | return IntProgression.fromClosedRange(start, 0, step).map { i -> 109 | this.copy(lightness = i / 10000000f) 110 | } 111 | } 112 | 113 | /** 114 | * @return a list of tints for the given color like the ones in https://www.color-hex.com/color/e91e63. 115 | * Each one of the colors is a HSLColor. 116 | * 117 | * @param count of tints to generate over the source color. It generates 10 by default. 118 | */ 119 | fun HSLAColor.tints(count: Int = 10): List { 120 | require(count > 0) { "count must be > 0" } 121 | 122 | val start = (this.lightness * 10000000).roundToInt() 123 | val step = if (start < 10000000) (10000000 - start) / count else 1 124 | return IntProgression.fromClosedRange(start, 10000000, step).map { i -> 125 | this.copy(lightness = i / 10000000f) 126 | } 127 | } 128 | 129 | /** 130 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 131 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The complimentary color stands for the 132 | * color in the opposite side of the circle, so it's (hue + 180) % 360. 133 | */ 134 | fun HSLAColor.complimentary(): HSLAColor { 135 | val complimentaryHue = (hue + 180) % 360 136 | return this.copy(hue = complimentaryHue) 137 | } 138 | 139 | /** 140 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, 141 | * representing the 360° of the wheel; 0° being red, 180° being red's opposite colour cyan, and so 142 | * on. The triadic colors stand for 3 colors that compose a perfect triangle (equal sides) over the 143 | * circle, so they are equally far from each other. 144 | * 145 | * Triadic colors for h0 would be (hue + 120) % 360 and (hue + 240) % 360. 146 | */ 147 | fun HSLAColor.triadic(): Pair { 148 | val h1 = this.copy(hue = (this.hue + 120) % 360f) 149 | val h2 = this.copy(hue = (this.hue + 240) % 360f) 150 | 151 | return Pair(h1, h2) 152 | } 153 | 154 | /** 155 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 156 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The tetradic colors stand for 4 colors 157 | * that compose a perfect square (equal sides) over the circle, so they are equally far from each other. 158 | * 159 | * Tetradic colors for h0 would be (hue + 90) % 360, (hue + 180) % 360 and (hue + 270) % 360. 160 | */ 161 | fun HSLAColor.tetradic(): Triple { 162 | val hue = this.hue // 0° to 359° 163 | 164 | val h1 = this.copy(hue = (hue + 90) % 360) 165 | val h2 = this.copy(hue = (hue + 180) % 360) 166 | val h3 = this.copy(hue = (hue + 270) % 360) 167 | 168 | return Triple(h1, h2, h3) 169 | } 170 | 171 | /** 172 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 173 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The analogous colors stand for 3 colors 174 | * that are together in the circle, separated by 30 degrees each. 175 | * 176 | * Analogous colors for h0 would be (hue + 30) % 360 & (hue - 30) % 360. 177 | */ 178 | fun HSLAColor.analogous(): Pair { 179 | val h1 = this.copy(hue = (hue + 30) % 360) 180 | val h2 = this.copy(hue = (hue + 330) % 360) 181 | 182 | return Pair(h1, h2) 183 | } 184 | 185 | /** 186 | * Check if a color is dark (convert to XYZ & check Y component) 187 | */ 188 | fun HSLAColor.isDark(): Boolean = ColorUtils.calculateLuminance(this.asColorInt()) < 0.5 189 | 190 | /** 191 | * Returns a color that contrasts nicely with the one given (receiver). Fallbacks to white and 192 | * black, but optional light and dark colors can be passed. 193 | */ 194 | fun HSLAColor.contrasting( 195 | lightColor: HSLAColor = HSLAColor(0f, 0f, 1f, 1f), // white 196 | darkColor: HSLAColor = HSLAColor(0f, 0f, 0f, 1f) // black 197 | ) = if (isDark()) { 198 | lightColor 199 | } else { 200 | darkColor 201 | } 202 | -------------------------------------------------------------------------------- /library/src/main/java/dev/jorgecastillo/androidcolorx/library/HSLColor.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.graphics.Color 4 | import androidx.annotation.ColorInt 5 | import androidx.annotation.NonNull 6 | import androidx.core.graphics.ColorUtils 7 | import kotlin.math.abs 8 | import kotlin.math.max 9 | import kotlin.math.roundToInt 10 | 11 | /** 12 | * HSL stands for hue-saturation-lightness. 13 | * 14 | * Hue is a value from 0...360 15 | * Saturation is a value from 0...1 16 | * Lightness is a value from 0...1 17 | */ 18 | data class HSLColor( 19 | val hue: Float, 20 | val saturation: Float, 21 | val lightness: Float 22 | ) 23 | 24 | @NonNull 25 | fun @receiver:ColorInt Int.asHsl(): HSLColor = this.let { color -> 26 | FloatArray(3).apply { 27 | rgbToHsl(Color.red(color), Color.green(color), Color.blue(color), this) 28 | }.let { 29 | HSLColor(it[0], it[1], it[2]) 30 | } 31 | } 32 | 33 | @ColorInt 34 | fun HSLColor.asColorInt(): Int = hslToColor(floatArrayOf(hue, saturation, lightness)) 35 | 36 | private fun HSLColor.asRGBF(): FloatArray { 37 | val hsl = floatArrayOf(hue, saturation, lightness) 38 | 39 | val h = hsl[0] 40 | val s = hsl[1] 41 | val l = hsl[2] 42 | 43 | val c = (1f - abs(2 * l - 1f)) * s 44 | val m = l - 0.5f * c 45 | val x = c * (1f - abs(h / 60f % 2f - 1f)) 46 | 47 | val hueSegment = h.toInt() / 60 48 | 49 | var r = 0f 50 | var g = 0f 51 | var b = 0f 52 | 53 | when (hueSegment) { 54 | 0 -> { 55 | r = (c + m) 56 | g = (x + m) 57 | b = m 58 | } 59 | 1 -> { 60 | r = (x + m) 61 | g = (c + m) 62 | b = m 63 | } 64 | 2 -> { 65 | r = m 66 | g = (c + m) 67 | b = (x + m) 68 | } 69 | 3 -> { 70 | r = m 71 | g = (x + m) 72 | b = (c + m) 73 | } 74 | 4 -> { 75 | r = (x + m) 76 | g = m 77 | b = (c + m) 78 | } 79 | 5, 6 -> { 80 | r = (c + m) 81 | g = m 82 | b = (x + m) 83 | } 84 | } 85 | 86 | r = constrain(r, 0f, 1f) 87 | g = constrain(g, 0f, 1f) 88 | b = constrain(b, 0f, 1f) 89 | 90 | return floatArrayOf(r, g, b) 91 | } 92 | 93 | private fun FloatArray.asCmyk(): CMYKColor { 94 | val r1 = this[0] 95 | val g1 = this[1] 96 | val b1 = this[2] 97 | val k = 1.0f - max(r1, max(g1, b1)) 98 | 99 | val cyan = if (k == 1f) 0f else (1.0f - r1 - k) / (1.0f - k) 100 | val magenta = if (k == 1f) 0f else (1.0f - g1 - k) / (1.0f - k) 101 | val yellow = if (k == 1f) 0f else (1.0f - b1 - k) / (1.0f - k) 102 | 103 | return CMYKColor(cyan, magenta, yellow, k) 104 | } 105 | 106 | fun HSLColor.asRgb(): RGBColor = asColorInt().asRgb() 107 | 108 | fun HSLColor.asArgb(): ARGBColor = asRgb().asArgb() 109 | 110 | fun HSLColor.asCmyk(): CMYKColor = asRGBF().asCmyk() 111 | 112 | fun HSLColor.asHex(): HEXColor = asColorInt().asHex() 113 | 114 | fun HSLColor.asHsla(): HSLAColor = HSLAColor(hue, saturation, lightness, 1f) 115 | 116 | fun HSLColor.asHsv(): HSVColor = asColorInt().asHsv() 117 | 118 | /** 119 | * @param value amount to lighten in the range 0...1 120 | */ 121 | fun HSLColor.lighten(value: Float): HSLColor { 122 | var mutableLightness = this.lightness 123 | mutableLightness += value 124 | mutableLightness = 0f.coerceAtLeast(mutableLightness.coerceAtMost(1f)) 125 | return HSLColor(hue, saturation, mutableLightness) 126 | } 127 | 128 | /** 129 | * @param value amount to lighten in the range 0...100 130 | */ 131 | fun HSLColor.lighten(value: Int): HSLColor { 132 | var mutableLightness = this.lightness 133 | mutableLightness += value / 100f 134 | mutableLightness = 0f.coerceAtLeast(mutableLightness.coerceAtMost(1f)) 135 | return HSLColor(hue, saturation, mutableLightness) 136 | } 137 | 138 | /** 139 | * @param value amount to darken in the range 0...1 140 | */ 141 | fun HSLColor.darken(value: Float): HSLColor { 142 | var mutableLightness = this.lightness 143 | mutableLightness -= value 144 | mutableLightness = 0f.coerceAtLeast(mutableLightness.coerceAtMost(1f)) 145 | return HSLColor(hue, saturation, mutableLightness) 146 | } 147 | 148 | /** 149 | * @param value amount to darken in the range 0...100 150 | */ 151 | fun HSLColor.darken(value: Int): HSLColor { 152 | var mutableLightness = this.lightness 153 | mutableLightness -= value / 100f 154 | mutableLightness = 0f.coerceAtLeast(mutableLightness.coerceAtMost(1f)) 155 | return HSLColor(hue, saturation, mutableLightness) 156 | } 157 | 158 | /** 159 | * @return a list of shades for the given color like the ones in https://www.color-hex.com/color/e91e63. 160 | * Each one of the colors is a HSLColor. 161 | * 162 | * @param count of shades to generate over the source color. It generates 10 by default. 163 | */ 164 | fun HSLColor.shades(count: Int = 10): List { 165 | require(count > 0) { "count must be > 0" } 166 | val start = (this.lightness * 10000000).roundToInt() 167 | val step = if (start > 0) { 168 | -1 * start / count 169 | } else 1 170 | return IntProgression.fromClosedRange(start, 0, step).map { i -> 171 | this.copy(lightness = i / 10000000f) 172 | } 173 | } 174 | 175 | /** 176 | * @return a list of tints for the given color like the ones in https://www.color-hex.com/color/e91e63. 177 | * Each one of the colors is a HSLColor. 178 | * 179 | * @param count of tints to generate over the source color. It generates 10 by default. 180 | */ 181 | fun HSLColor.tints(count: Int = 10): List { 182 | require(count > 0) { "count must be > 0" } 183 | 184 | val start = (this.lightness * 10000000).roundToInt() 185 | val step = if (start < 10000000) (10000000 - start) / count else 1 186 | return IntProgression.fromClosedRange(start, 10000000, step).map { i -> 187 | this.copy(lightness = i / 10000000f) 188 | } 189 | } 190 | 191 | /** 192 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 193 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The complimentary color stands for the 194 | * color in the opposite side of the circle, so it's (hue + 180) % 360. 195 | */ 196 | fun HSLColor.complimentary(): HSLColor { 197 | val complimentaryHue = (hue + 180) % 360 198 | return this.copy(hue = complimentaryHue) 199 | } 200 | 201 | /** 202 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, 203 | * representing the 360° of the wheel; 0° being red, 180° being red's opposite colour cyan, and so 204 | * on. The triadic colors stand for 3 colors that compose a perfect triangle (equal sides) over the 205 | * circle, so they are equally far from each other. 206 | * 207 | * Triadic colors for h0 would be (hue + 120) % 360 and (hue + 240) % 360. 208 | */ 209 | fun HSLColor.triadic(): Pair { 210 | val h1 = this.copy(hue = (this.hue + 120) % 360f) 211 | val h2 = this.copy(hue = (this.hue + 240) % 360f) 212 | 213 | return Pair(h1, h2) 214 | } 215 | 216 | /** 217 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 218 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The tetradic colors stand for 4 colors 219 | * that compose a perfect square (equal sides) over the circle, so they are equally far from each other. 220 | * 221 | * Tetradic colors for h0 would be (hue + 90) % 360, (hue + 180) % 360 and (hue + 270) % 360. 222 | */ 223 | fun HSLColor.tetradic(): Triple { 224 | val hue = this.hue // 0° to 359° 225 | 226 | val h1 = this.copy(hue = (hue + 90) % 360) 227 | val h2 = this.copy(hue = (hue + 180) % 360) 228 | val h3 = this.copy(hue = (hue + 270) % 360) 229 | 230 | return Triple(h1, h2, h3) 231 | } 232 | 233 | /** 234 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 235 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The analogous colors stand for 3 colors 236 | * that are together in the circle, separated by 30 degrees each. 237 | * 238 | * Analogous colors for h0 would be (hue + 30) % 360 & (hue - 30) % 360. 239 | */ 240 | fun HSLColor.analogous(): Pair { 241 | val h1 = this.copy(hue = (hue + 30) % 360) 242 | val h2 = this.copy(hue = (hue + 330) % 360) 243 | 244 | return Pair(h1, h2) 245 | } 246 | 247 | /** 248 | * Check if a color is dark (convert to XYZ & check Y component) 249 | */ 250 | fun HSLColor.isDark(): Boolean = ColorUtils.calculateLuminance(this.asColorInt()) < 0.5 251 | 252 | /** 253 | * Returns a color that contrasts nicely with the one given (receiver). Fallbacks to white and 254 | * black, but optional light and dark colors can be passed. 255 | */ 256 | fun HSLColor.contrasting( 257 | lightColor: HSLColor = HSLColor(0f, 0f, 1f), // white 258 | darkColor: HSLColor = HSLColor(0f, 0f, 0f) // black 259 | ) = if (isDark()) { 260 | lightColor 261 | } else { 262 | darkColor 263 | } 264 | -------------------------------------------------------------------------------- /library/src/main/java/dev/jorgecastillo/androidcolorx/library/HSVColor.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.graphics.Color 4 | import androidx.annotation.ColorInt 5 | import androidx.annotation.NonNull 6 | import androidx.core.graphics.ColorUtils 7 | 8 | /** 9 | * HSV stands for hue-saturation-value (sometimes also known as HSB, hue-saturation-brightness). 10 | * 11 | * Hue is a value from 0...360 12 | * Saturation is a value from 0...1 13 | * Lightness is a value from 0...1 14 | */ 15 | data class HSVColor( 16 | val hue: Float, 17 | val saturation: Float, 18 | val value: Float 19 | ) 20 | 21 | @NonNull 22 | fun @receiver:ColorInt Int.asHsv(): HSVColor { 23 | val hsvArray = FloatArray(3) 24 | Color.colorToHSV(this, hsvArray) 25 | return HSVColor(hsvArray[0], hsvArray[1], hsvArray[2]) 26 | } 27 | 28 | @ColorInt 29 | fun HSVColor.asColorInt(): Int = Color.HSVToColor(floatArrayOf(hue, saturation, value)) 30 | 31 | fun HSVColor.asRgb(): RGBColor = asColorInt().asRgb() 32 | 33 | fun HSVColor.asArgb(): ARGBColor = asRgb().asArgb() 34 | 35 | fun HSVColor.asCmyk(): CMYKColor = asColorInt().asCmyk() 36 | 37 | fun HSVColor.asHex(): HEXColor = asColorInt().asHex() 38 | 39 | fun HSVColor.asHsl(): HSLColor = asColorInt().asHsl() 40 | 41 | fun HSVColor.asHsla(): HSLAColor = asColorInt().asHsla() 42 | 43 | /** 44 | * @param value amount to lighten in the range 0...1 45 | */ 46 | fun HSVColor.lighten(value: Float): HSVColor = this.asColorInt().lighten(value).asHsv() 47 | 48 | /** 49 | * @param value amount to lighten in the range 0...100 50 | */ 51 | fun HSVColor.lighten(value: Int): HSVColor = this.asColorInt().lighten(value).asHsv() 52 | 53 | /** 54 | * @param value amount to darken in the range 0...1 55 | */ 56 | fun HSVColor.darken(value: Float): HSVColor = this.asColorInt().darken(value).asHsv() 57 | 58 | /** 59 | * @param value amount to darken in the range 0...100 60 | */ 61 | fun HSVColor.darken(value: Int): HSVColor = this.asColorInt().darken(value).asHsv() 62 | 63 | /** 64 | * @return a list of shades for the given color like the ones in https://www.color-hex.com/color/e91e63. 65 | * Each one of the colors is a HSLColor. 66 | * 67 | * @param count of shades to generate over the source color. It generates 10 by default. 68 | */ 69 | fun HSVColor.shades(count: Int = 10): List = asColorInt().shades(count).map { it.asHsv() } 70 | 71 | /** 72 | * @return a list of tints for the given color like the ones in https://www.color-hex.com/color/e91e63. 73 | * Each one of the colors is a HSLColor. 74 | * 75 | * @param count of tints to generate over the source color. It generates 10 by default. 76 | */ 77 | fun HSVColor.tints(count: Int = 10): List = asColorInt().tints(count).map { it.asHsv() } 78 | 79 | /** 80 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 81 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The complimentary color stands for the 82 | * color in the opposite side of the circle, so it's (hue + 180) % 360. 83 | */ 84 | fun HSVColor.complimentary(): HSVColor = asColorInt().complimentary().asHsv() 85 | 86 | /** 87 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, 88 | * representing the 360° of the wheel; 0° being red, 180° being red's opposite colour cyan, and so 89 | * on. The triadic colors stand for 3 colors that compose a perfect triangle (equal sides) over the 90 | * circle, so they are equally far from each other. 91 | * 92 | * Triadic colors for h0 would be (hue + 120) % 360 and (hue + 240) % 360. 93 | */ 94 | fun HSVColor.triadic(): Pair { 95 | val h1 = this.copy(hue = (this.hue + 120) % 360f) 96 | val h2 = this.copy(hue = (this.hue + 240) % 360f) 97 | 98 | return Pair(h1, h2) 99 | } 100 | 101 | /** 102 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 103 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The tetradic colors stand for 4 colors 104 | * that compose a perfect square (equal sides) over the circle, so they are equally far from each other. 105 | * 106 | * Tetradic colors for h0 would be (hue + 90) % 360, (hue + 180) % 360 and (hue + 270) % 360. 107 | */ 108 | fun HSVColor.tetradic(): Triple { 109 | val hue = this.hue // 0° to 359° 110 | 111 | val h1 = this.copy(hue = (hue + 90) % 360) 112 | val h2 = this.copy(hue = (hue + 180) % 360) 113 | val h3 = this.copy(hue = (hue + 270) % 360) 114 | 115 | return Triple(h1, h2, h3) 116 | } 117 | 118 | /** 119 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 120 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The analogous colors stand for 3 colors 121 | * that are together in the circle, separated by 30 degrees each. 122 | * 123 | * Analogous colors for h0 would be (hue + 30) % 360 & (hue - 30) % 360. 124 | */ 125 | fun HSVColor.analogous(): Pair = 126 | asColorInt().analogous().let { Pair(it.first.asHsv(), it.second.asHsv()) } 127 | 128 | /** 129 | * Check if a color is dark (convert to XYZ & check Y component) 130 | */ 131 | fun HSVColor.isDark(): Boolean = ColorUtils.calculateLuminance(this.asColorInt()) < 0.5 132 | 133 | /** 134 | * Returns a color that contrasts nicely with the one given (receiver). Fallbacks to white and 135 | * black, but optional light and dark colors can be passed. 136 | */ 137 | fun HSVColor.contrasting( 138 | lightColor: HSVColor = HSVColor(0f, 0f, 1f), // white 139 | darkColor: HSVColor = HSVColor(0f, 0f, 0f) // black 140 | ) = if (isDark()) { 141 | lightColor 142 | } else { 143 | darkColor 144 | } 145 | -------------------------------------------------------------------------------- /library/src/main/java/dev/jorgecastillo/androidcolorx/library/RGBColor.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.graphics.Color 4 | import androidx.annotation.ColorInt 5 | import androidx.core.graphics.ColorUtils 6 | 7 | data class RGBColor( 8 | val red: Int, 9 | val green: Int, 10 | val blue: Int 11 | ) { 12 | override fun toString(): String { 13 | return "r:$red / g:$green / b:$blue" 14 | } 15 | } 16 | 17 | fun @receiver:ColorInt Int.asRgb(): RGBColor = 18 | RGBColor(Color.red(this), Color.green(this), Color.blue(this)) 19 | 20 | @ColorInt 21 | fun RGBColor.asColorInt(): Int = Color.rgb(red, green, blue) 22 | 23 | fun RGBColor.asArgb(): ARGBColor = asColorInt().asArgb() 24 | 25 | fun RGBColor.asCmyk(): CMYKColor = asColorInt().asCmyk() 26 | 27 | fun RGBColor.asHex(): HEXColor = asColorInt().asHex() 28 | 29 | fun RGBColor.asHsl(): HSLColor = asColorInt().asHsl() 30 | 31 | fun RGBColor.asHsla(): HSLAColor = asColorInt().asHsla() 32 | 33 | fun RGBColor.asHsv(): HSVColor = asColorInt().asHsv() 34 | 35 | /** 36 | * @param value amount to lighten in the range 0...1 37 | */ 38 | fun RGBColor.lighten(value: Float): RGBColor = this.asColorInt().lighten(value).asRgb() 39 | 40 | /** 41 | * @param value amount to lighten in the range 0...100 42 | */ 43 | fun RGBColor.lighten(value: Int): RGBColor = this.asColorInt().lighten(value).asRgb() 44 | 45 | /** 46 | * @param value amount to darken in the range 0...1 47 | */ 48 | fun RGBColor.darken(value: Float): RGBColor = this.asColorInt().darken(value).asRgb() 49 | 50 | /** 51 | * @param value amount to darken in the range 0...100 52 | */ 53 | fun RGBColor.darken(value: Int): RGBColor = this.asColorInt().darken(value).asRgb() 54 | 55 | /** 56 | * @return a list of shades for the given color like the ones in https://www.color-hex.com/color/e91e63. 57 | * Each one of the colors is a RGBColor. 58 | * 59 | * @param count of shades to generate over the source color. It generates 10 by default. 60 | */ 61 | fun RGBColor.shades(count: Int = 10): List = asColorInt().shades(count).map { it.asRgb() } 62 | 63 | /** 64 | * @return a list of tints for the given color like the ones in https://www.color-hex.com/color/e91e63. 65 | * Each one of the colors is a RGBColor. 66 | * 67 | * @param count of tints to generate over the source color. It generates 10 by default. 68 | */ 69 | fun RGBColor.tints(count: Int = 10): List = asColorInt().tints(count).map { it.asRgb() } 70 | 71 | /** 72 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 73 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The complimentary color stands for the 74 | * color in the opposite side of the circle, so it's (hue + 180) % 360. 75 | */ 76 | fun RGBColor.complimentary(): RGBColor = asColorInt().complimentary().asRgb() 77 | 78 | /** 79 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 80 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The triadic colors stand for 3 colors that 81 | * compose a perfect triangle (equal sides) over the circle, so they are equally far from each other. 82 | * 83 | * Triadic colors for h0 would be (hue + 120) % 360 and (hue + 240) % 360. 84 | */ 85 | fun RGBColor.triadic(): Pair = 86 | asColorInt().triadic().let { Pair(it.first.asRgb(), it.second.asRgb()) } 87 | 88 | /** 89 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 90 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The tetradic colors stand for 4 colors 91 | * that compose a perfect square (equal sides) over the circle, so they are equally far from each other. 92 | * 93 | * Tetradic colors for h0 would be (hue + 90) % 360, (hue + 180) % 360 and (hue + 270) % 360. 94 | */ 95 | fun RGBColor.tetradic(): Triple = 96 | asColorInt().tetradic().let { Triple(it.first.asRgb(), it.second.asRgb(), it.third.asRgb()) } 97 | 98 | /** 99 | * The Hue is the colour's position on the colour wheel, expressed in degrees from 0° to 359°, representing the 360° of 100 | * the wheel; 0° being red, 180° being red's opposite colour cyan, and so on. The analogous colors stand for 3 colors 101 | * that are together in the circle, separated by 30 degrees each. 102 | * 103 | * Analogous colors for h0 would be (hue + 30) % 360 & (hue - 30) % 360. 104 | */ 105 | fun RGBColor.analogous(): Pair = 106 | asColorInt().analogous().let { Pair(it.first.asRgb(), it.second.asRgb()) } 107 | 108 | /** 109 | * Check if a color is dark (convert to XYZ & check Y component) 110 | */ 111 | fun RGBColor.isDark(): Boolean = ColorUtils.calculateLuminance(this.asColorInt()) < 0.5 112 | 113 | /** 114 | * Returns a color that contrasts nicely with the one given (receiver). Fallbacks to white and 115 | * black, but optional light and dark colors can be passed. 116 | */ 117 | fun RGBColor.contrasting( 118 | lightColor: RGBColor = RGBColor(255, 255, 255), 119 | darkColor: RGBColor = RGBColor(0, 0, 0) 120 | ) = if (isDark()) { 121 | lightColor 122 | } else { 123 | darkColor 124 | } 125 | -------------------------------------------------------------------------------- /library/src/main/java/dev/jorgecastillo/androidcolorx/library/Utils.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | internal fun constrain(amount: Float, low: Float, high: Float): Float { 4 | return if (amount < low) low else if (amount > high) high else amount 5 | } 6 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | library 3 | 4 | -------------------------------------------------------------------------------- /library/src/test/java/dev/jorgecastillo/androidcolorx/library/ARGBColorTests.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.os.Build 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import junit.framework.TestCase.assertEquals 6 | import junit.framework.TestCase.assertFalse 7 | import junit.framework.TestCase.assertTrue 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.robolectric.annotation.Config 11 | 12 | @Config(sdk = [Build.VERSION_CODES.O_MR1]) 13 | @RunWith(AndroidJUnit4::class) 14 | class ARGBColorTests { 15 | 16 | @Test 17 | fun `is dark should return true for #e91e63`() { 18 | val color = ARGBColor(20, 233, 30, 99) 19 | 20 | assertTrue(color.isDark()) 21 | } 22 | 23 | @Test 24 | fun `is dark should return false for #1ee9a4`() { 25 | val color = ARGBColor(20, 30, 233, 164) 26 | 27 | assertFalse(color.isDark()) 28 | } 29 | 30 | @Test 31 | fun `lighten integer should enlighten the color`() { 32 | val color = ARGBColor(20, 233, 30, 99) 33 | 34 | assertEquals(ARGBColor(20, 242, 123, 163), color.lighten(20)) 35 | } 36 | 37 | @Test 38 | fun `lighten float should enlighten the color`() { 39 | val color = ARGBColor(20, 233, 30, 99) 40 | 41 | assertEquals(ARGBColor(20, 242, 123, 163), color.lighten(0.2f)) 42 | } 43 | 44 | @Test 45 | fun `darken integer should darken the color`() { 46 | val color = ARGBColor(20, 233, 30, 99) 47 | 48 | assertEquals(ARGBColor(20, 147, 14, 59), color.darken(20)) 49 | } 50 | 51 | @Test 52 | fun `darken float should darken the color`() { 53 | val color = ARGBColor(20, 233, 30, 99) 54 | 55 | assertEquals(ARGBColor(20, 147, 14, 59), color.darken(0.2f)) 56 | } 57 | 58 | @Test 59 | fun `darken integer should be equivalent to dark float`() { 60 | val color = ARGBColor(20, 233, 30, 99) 61 | 62 | assertEquals(color.darken(20), color.darken(0.2f)) 63 | } 64 | 65 | @Test 66 | fun `shades should be properly calculated`() { 67 | val color = ARGBColor(20, 233, 30, 99) 68 | 69 | assertEquals( 70 | listOf( 71 | ARGBColor(20, 233, 30, 99), 72 | ARGBColor(20, 216, 21, 87), 73 | ARGBColor(20, 192, 19, 78), 74 | ARGBColor(20, 168, 16, 68), 75 | ARGBColor(20, 144, 14, 58), 76 | ARGBColor(20, 120, 12, 48), 77 | ARGBColor(20, 96, 9, 39), 78 | ARGBColor(20, 72, 7, 29), 79 | ARGBColor(20, 48, 5, 19), 80 | ARGBColor(20, 24, 2, 10), 81 | ARGBColor(20, 0, 0, 0) 82 | ), 83 | color.shades() 84 | ) 85 | } 86 | 87 | @Test 88 | fun `shades for a specific count should be properly calculated`() { 89 | val color = ARGBColor(20, 233, 30, 99) 90 | 91 | assertEquals( 92 | listOf( 93 | ARGBColor(20, 233, 30, 99), 94 | ARGBColor(20, 160, 16, 65), 95 | ARGBColor(20, 80, 8, 32), 96 | ARGBColor(20, 0, 0, 0) 97 | ), 98 | color.shades(count = 3) 99 | ) 100 | } 101 | 102 | @Test 103 | fun `tints should be properly calculated`() { 104 | val color = ARGBColor(20, 233, 30, 99) 105 | 106 | assertEquals( 107 | listOf( 108 | ARGBColor(20, 233, 30, 99), 109 | ARGBColor(20, 235, 52, 115), 110 | ARGBColor(20, 237, 75, 130), 111 | ARGBColor(20, 240, 97, 146), 112 | ARGBColor(20, 242, 120, 161), 113 | ARGBColor(20, 244, 142, 177), 114 | ARGBColor(20, 246, 165, 193), 115 | ARGBColor(20, 248, 187, 208), 116 | ARGBColor(20, 251, 210, 224), 117 | ARGBColor(20, 253, 232, 239), 118 | ARGBColor(20, 255, 255, 255) 119 | ), 120 | color.tints() 121 | ) 122 | } 123 | 124 | @Test 125 | fun `tints for specific count should be properly calculated`() { 126 | val color = ARGBColor(20, 233, 30, 99) 127 | 128 | assertEquals( 129 | listOf( 130 | ARGBColor(20, 233, 30, 99), 131 | ARGBColor(20, 240, 105, 151), 132 | ARGBColor(20, 248, 180, 203), 133 | ARGBColor(20, 255, 255, 255) 134 | ), 135 | color.tints(count = 3) 136 | ) 137 | } 138 | 139 | @Test 140 | fun `complimentary colors should be calculated as expected`() { 141 | val color = ARGBColor(20, 233, 30, 99) 142 | 143 | assertEquals(ARGBColor(20, 30, 233, 164), color.complimentary()) 144 | } 145 | 146 | @Test 147 | fun `returns white as contrasting color for dark colors`() { 148 | val color = ARGBColor(255, 233, 30, 99) 149 | 150 | assertEquals(ARGBColor(255, 255, 255, 255), color.contrasting()) 151 | } 152 | 153 | @Test 154 | fun `returns black as contrasting color for light colors`() { 155 | val color = ARGBColor(255, 251, 234, 248) 156 | 157 | assertEquals(ARGBColor(255, 0, 0, 0), color.contrasting()) 158 | } 159 | 160 | @Test 161 | fun `returns passed light color as contrasting color for dark colors`() { 162 | val color = ARGBColor(255, 233, 30, 99) 163 | val lightColor = ARGBColor(255, 251, 234, 248) 164 | 165 | assertEquals( 166 | lightColor, 167 | color.contrasting( 168 | lightColor = lightColor, 169 | darkColor = ARGBColor(255, 0, 0, 0) 170 | ) 171 | ) 172 | } 173 | 174 | @Test 175 | fun `returns passed dark color as contrasting color for light colors`() { 176 | val color = ARGBColor(255, 251, 234, 248) 177 | val darkColor = ARGBColor(255, 233, 30, 99) 178 | 179 | assertEquals( 180 | darkColor, 181 | color.contrasting(lightColor = ARGBColor(255, 255, 255, 255), darkColor = darkColor) 182 | ) 183 | } 184 | 185 | @Test 186 | fun `triadic colors should be calculated as expected`() { 187 | val color = ARGBColor(20, 233, 30, 99) 188 | 189 | assertEquals( 190 | Pair(ARGBColor(20, 99, 233, 30), ARGBColor(20, 30, 99, 233)), 191 | color.triadic() 192 | ) 193 | } 194 | 195 | @Test 196 | fun `tetradic colors should be calculated as expected`() { 197 | val color = ARGBColor(20, 233, 30, 99) 198 | 199 | assertEquals( 200 | Triple( 201 | ARGBColor(20, 201, 233, 30), 202 | ARGBColor(20, 30, 233, 164), 203 | ARGBColor(20, 62, 30, 233) 204 | ), 205 | color.tetradic() 206 | ) 207 | } 208 | 209 | @Test 210 | fun `analogous colors should be calculated as expected`() { 211 | val color = ARGBColor(20, 233, 30, 99) 212 | 213 | assertEquals( 214 | Pair(ARGBColor(20, 233, 62, 30), ARGBColor(20, 233, 30, 201)), 215 | color.analogous() 216 | ) 217 | } 218 | 219 | @Test 220 | fun `converts to RGB and back just loses information about alpha`() { 221 | val color = ARGBColor(20, 233, 30, 99) 222 | 223 | assertEquals(color.copy(alpha = 255), color.asRgb().asArgb()) 224 | } 225 | 226 | @Test 227 | fun `converts to HEX and back is idempotent`() { 228 | val color = ARGBColor(20, 233, 30, 99) 229 | 230 | assertEquals(color, color.asHex().asArgb()) 231 | } 232 | 233 | @Test 234 | fun `converts to ColorInt and back is idempotent`() { 235 | val color = ARGBColor(20, 233, 30, 99) 236 | 237 | assertEquals(color, color.asColorInt().asArgb()) 238 | } 239 | 240 | @Test 241 | fun `converts to HSL and back is just loses information about alpha`() { 242 | val color = ARGBColor(20, 233, 30, 99) 243 | 244 | assertEquals(color.copy(alpha = 255), color.asHsl().asArgb()) 245 | } 246 | 247 | @Test 248 | fun `converts to HSLA and back is idempotent`() { 249 | val color = ARGBColor(20, 233, 30, 99) 250 | 251 | assertEquals(color, color.asHsla().asArgb()) 252 | } 253 | 254 | @Test 255 | fun `converts to CMYK and back just loses information about alpha`() { 256 | val color = ARGBColor(20, 233, 30, 99) 257 | 258 | assertEquals(color.copy(alpha = 255), color.asCmyk().asArgb()) 259 | } 260 | 261 | @Test 262 | fun `converts to HSV and back just loses information about alpha`() { 263 | val color = ARGBColor(20, 233, 30, 99) 264 | 265 | assertEquals(color.copy(alpha = 255), color.asHsv().asArgb()) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /library/src/test/java/dev/jorgecastillo/androidcolorx/library/AssertExtensions.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import assertk.all 4 | import assertk.assertThat 5 | import assertk.assertions.isEqualTo 6 | import assertk.assertions.isLessThanOrEqualTo 7 | import org.junit.Assert.fail 8 | import kotlin.math.abs 9 | import kotlin.math.round 10 | 11 | infix fun T.eqWithBigPrecisionLoss(other: T): Unit = when (this) { 12 | is CMYKColor -> { 13 | assertThat(this).all { 14 | assertThat(cyan.round(2)).isEqualTo((other as CMYKColor).cyan.round(2)) 15 | assertThat(magenta.round(2)).isEqualTo((other as CMYKColor).magenta.round(2)) 16 | assertThat(yellow.round(2)).isEqualTo((other as CMYKColor).yellow.round(2)) 17 | assertThat(key.round(2)).isEqualTo((other as CMYKColor).key.round(2)) 18 | } 19 | } 20 | is HSLColor -> { 21 | assertThat(this).all { 22 | assertThat(hue.round(2)).isEqualTo((other as HSLColor).hue.round(2)) 23 | assertThat(saturation.round(2)).isEqualTo((other as HSLColor).saturation.round(2)) 24 | assertThat(lightness.round(2)).isEqualTo((other as HSLColor).lightness.round(2)) 25 | } 26 | } 27 | is HSLAColor -> { 28 | assertThat(this).all { 29 | assertThat(hue.round(2)).isEqualTo((other as HSLAColor).hue.round(2)) 30 | assertThat(saturation.round(2)).isEqualTo((other as HSLAColor).saturation.round(2)) 31 | assertThat(lightness.round(2)).isEqualTo((other as HSLAColor).lightness.round(2)) 32 | assertThat(alpha.round(2)).isEqualTo((other as HSLAColor).alpha.round(2)) 33 | } 34 | } 35 | is HSVColor -> { 36 | assertThat(this).all { 37 | assertThat(hue.round(2)).isEqualTo((other as HSVColor).hue.round(2)) 38 | assertThat(saturation.round(2)).isEqualTo((other as HSVColor).saturation.round(2)) 39 | assertThat(value.round(2)).isEqualTo((other as HSVColor).value.round(2)) 40 | } 41 | } 42 | else -> fail() 43 | } 44 | 45 | infix fun Pair.eqWithBigPrecisionLoss(other: Pair) { 46 | first eqWithBigPrecisionLoss other.first 47 | second eqWithBigPrecisionLoss other.second 48 | } 49 | 50 | infix fun Triple.eqWithBigPrecisionLoss(other: Triple) { 51 | first eqWithBigPrecisionLoss other.first 52 | second eqWithBigPrecisionLoss other.second 53 | third eqWithBigPrecisionLoss other.third 54 | } 55 | 56 | infix fun List.eqWithBigPrecisionLoss(other: List) = 57 | assertThat(this).all { 58 | forEachIndexed { index, _ -> get(index) eqWithBigPrecisionLoss other[index] } 59 | } 60 | 61 | fun Float.round(decimals: Int): Double { 62 | var multiplier = 1.0 63 | repeat(decimals) { multiplier *= 10 } 64 | return round(this * multiplier) / multiplier 65 | } 66 | 67 | private const val MIN_ERROR_MARGIN = 0.000001f 68 | 69 | infix fun T.eqWithMinimumPrecisionLoss(other: T): Unit = when (this) { 70 | is CMYKColor -> { 71 | assertThat(this).all { 72 | assertThat(abs(cyan - (other as CMYKColor).cyan)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 73 | assertThat(abs(magenta - (other as CMYKColor).magenta)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 74 | assertThat(abs(yellow - (other as CMYKColor).yellow)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 75 | assertThat(abs(key - (other as CMYKColor).key)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 76 | } 77 | } 78 | is HSLColor -> { 79 | assertThat(this).all { 80 | assertThat(abs(hue - (other as HSLColor).hue)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 81 | assertThat(abs(saturation - (other as HSLColor).saturation)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 82 | assertThat(abs(lightness - (other as HSLColor).lightness)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 83 | } 84 | } 85 | is HSLAColor -> { 86 | assertThat(this).all { 87 | assertThat(abs(hue - (other as HSLAColor).hue)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 88 | assertThat(abs(saturation - (other as HSLAColor).saturation)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 89 | assertThat(abs(lightness - (other as HSLAColor).lightness)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 90 | assertThat(abs(alpha - (other as HSLAColor).alpha)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 91 | } 92 | } 93 | is HSVColor -> { 94 | assertThat(this).all { 95 | assertThat(abs(hue - (other as HSVColor).hue)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 96 | assertThat(abs(saturation - (other as HSVColor).saturation)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 97 | assertThat(abs(value - (other as HSVColor).value)).isLessThanOrEqualTo(MIN_ERROR_MARGIN) 98 | } 99 | } 100 | else -> fail() 101 | } 102 | 103 | infix fun Pair.eqWithMinimumPrecisionLoss(other: Pair) { 104 | first eqWithMinimumPrecisionLoss other.first 105 | second eqWithMinimumPrecisionLoss other.second 106 | } 107 | 108 | infix fun Triple.eqWithMinimumPrecisionLoss(other: Triple) { 109 | first eqWithMinimumPrecisionLoss other.first 110 | second eqWithMinimumPrecisionLoss other.second 111 | third eqWithMinimumPrecisionLoss other.third 112 | } 113 | 114 | infix fun List.eqWithMinimumPrecisionLoss(other: List) = 115 | assertThat(this).all { 116 | forEachIndexed { index, _ -> get(index) eqWithBigPrecisionLoss other[index] } 117 | } 118 | -------------------------------------------------------------------------------- /library/src/test/java/dev/jorgecastillo/androidcolorx/library/CMYKColorTests.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.os.Build 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import junit.framework.TestCase.assertEquals 6 | import junit.framework.TestCase.assertFalse 7 | import junit.framework.TestCase.assertTrue 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.robolectric.annotation.Config 11 | 12 | @Config(sdk = [Build.VERSION_CODES.O_MR1]) 13 | @RunWith(AndroidJUnit4::class) 14 | class CMYKColorTests { 15 | 16 | @Test 17 | fun `is dark should return true for #e91e63`() { 18 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 19 | 20 | assertTrue(color.isDark()) 21 | } 22 | 23 | @Test 24 | fun `is dark should return false for #1ee9a4`() { 25 | val color = CMYKColor(0.87f, 0f, 0.30f, 0.09f) 26 | 27 | assertFalse(color.isDark()) 28 | } 29 | 30 | @Test 31 | fun `lighten integer should enlighten the color`() { 32 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 33 | 34 | CMYKColor(0f, 0.49f, 0.33f, 0.05f) eqWithBigPrecisionLoss 35 | color.lighten(20) 36 | } 37 | 38 | @Test 39 | fun `lighten float should enlighten the color`() { 40 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 41 | 42 | CMYKColor(0f, 0.49f, 0.33f, 0.05f) eqWithBigPrecisionLoss 43 | color.lighten(0.2f) 44 | } 45 | 46 | @Test 47 | fun `darken integer should darken the color`() { 48 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 49 | 50 | CMYKColor(0f, 0.90f, 0.60f, 0.43f) eqWithBigPrecisionLoss 51 | color.darken(20) 52 | } 53 | 54 | @Test 55 | fun `darken float should darken the color`() { 56 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 57 | 58 | CMYKColor(0f, 0.90f, 0.60f, 0.43f) eqWithBigPrecisionLoss 59 | color.darken(0.2f) 60 | } 61 | 62 | @Test 63 | fun `darken integer should be equivalent to dark float`() { 64 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 65 | 66 | assertEquals(color.darken(20), color.darken(0.2f)) 67 | } 68 | 69 | @Test 70 | fun `shades should be properly calculated`() { 71 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 72 | 73 | listOf( 74 | CMYKColor(0f, 0.87f, 0.58f, 0.09f), 75 | CMYKColor(0f, 0.90f, 0.60f, 0.16f), 76 | CMYKColor(0f, 0.90f, 0.60f, 0.25f), 77 | CMYKColor(0f, 0.90f, 0.60f, 0.35f), 78 | CMYKColor(0f, 0.90f, 0.60f, 0.44f), 79 | CMYKColor(0f, 0.90f, 0.60f, 0.53f), 80 | CMYKColor(0f, 0.89f, 0.60f, 0.63f), 81 | CMYKColor(0f, 0.90f, 0.59f, 0.72f), 82 | CMYKColor(0f, 0.90f, 0.60f, 0.81f), 83 | CMYKColor(0f, 0.92f, 0.58f, 0.91f), 84 | CMYKColor(0f, 0f, 0f, 1.00f) 85 | ) eqWithBigPrecisionLoss color.shades() 86 | } 87 | 88 | @Test 89 | fun `shades for a specific count should be properly calculated`() { 90 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 91 | 92 | listOf( 93 | CMYKColor(0f, 0.87f, 0.58f, 0.09f), 94 | CMYKColor(0f, 0.90f, 0.60f, 0.38f), 95 | CMYKColor(0f, 0.90f, 0.59f, 0.69f), 96 | CMYKColor(0f, 0f, 0f, 1f) 97 | ) eqWithBigPrecisionLoss color.shades(count = 3) 98 | } 99 | 100 | @Test 101 | fun `tints should be properly calculated`() { 102 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 103 | 104 | listOf( 105 | CMYKColor(0f, 0.87f, 0.58f, 0.09f), 106 | CMYKColor(0f, 0.78f, 0.52f, 0.08f), 107 | CMYKColor(0f, 0.68f, 0.46f, 0.07f), 108 | CMYKColor(0f, 0.59f, 0.40f, 0.06f), 109 | CMYKColor(0f, 0.50f, 0.34f, 0.05f), 110 | CMYKColor(0f, 0.42f, 0.28f, 0.04f), 111 | CMYKColor(0f, 0.33f, 0.22f, 0.04f), 112 | CMYKColor(0f, 0.25f, 0.16f, 0.03f), 113 | CMYKColor(0f, 0.16f, 0.11f, 0.02f), 114 | CMYKColor(0f, 0.08f, 0.06f, 0.01f), 115 | CMYKColor(0f, 0f, 0f, 0.00f) 116 | ) eqWithBigPrecisionLoss color.tints() 117 | } 118 | 119 | @Test 120 | fun `tints for specific count should be properly calculated`() { 121 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 122 | 123 | listOf( 124 | CMYKColor(0f, 0.87f, 0.58f, 0.09f), 125 | CMYKColor(0f, 0.56f, 0.37f, 0.06f), 126 | CMYKColor(0f, 0.27f, 0.18f, 0.03f), 127 | CMYKColor(0f, 0f, 0f, 0.00f) 128 | ) eqWithBigPrecisionLoss color.tints(count = 3) 129 | } 130 | 131 | @Test 132 | fun `complimentary colors should be calculated as expected`() { 133 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 134 | 135 | CMYKColor( 136 | 0.87f, 137 | 0.00f, 138 | 0.29f, 139 | 0.09f 140 | ) eqWithBigPrecisionLoss color.complimentary() 141 | } 142 | 143 | @Test 144 | fun `returns white as contrasting color for dark colors`() { 145 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 146 | 147 | assertEquals(CMYKColor(0f, 0f, 0f, 0f), color.contrasting()) 148 | } 149 | 150 | @Test 151 | fun `returns black as contrasting color for light colors`() { 152 | val color = CMYKColor(0.87f, 0.00f, 0.29f, 0.09f) 153 | 154 | assertEquals(CMYKColor(0f, 0f, 0f, 1f), color.contrasting()) 155 | } 156 | 157 | @Test 158 | fun `returns passed light color as contrasting color for dark colors`() { 159 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 160 | val lightColor = CMYKColor(0.87f, 0.00f, 0.29f, 0.09f) 161 | 162 | assertEquals( 163 | lightColor, 164 | color.contrasting( 165 | lightColor = lightColor, 166 | darkColor = CMYKColor(0f, 0f, 0f, 1f) 167 | ) 168 | ) 169 | } 170 | 171 | @Test 172 | fun `returns passed dark color as contrasting color for light colors`() { 173 | val color = CMYKColor(0.87f, 0.00f, 0.29f, 0.09f) 174 | val darkColor = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 175 | 176 | assertEquals( 177 | darkColor, 178 | color.contrasting( 179 | lightColor = CMYKColor(0f, 0f, 0f, 0f), 180 | darkColor = darkColor 181 | ) 182 | ) 183 | } 184 | 185 | @Test 186 | fun `triadic colors should be calculated as expected`() { 187 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 188 | 189 | Pair( 190 | CMYKColor(0.58f, 0f, 0.87f, 0.09f), 191 | CMYKColor(0.87f, 0.58f, 0.00f, 0.09f) 192 | ) eqWithBigPrecisionLoss color.triadic() 193 | } 194 | 195 | @Test 196 | fun `tetradic colors should be calculated as expected`() { 197 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 198 | 199 | Triple( 200 | CMYKColor(0.15f, 0f, 0.87f, 0.09f), 201 | CMYKColor(0.87f, 0f, 0.29f, 0.09f), 202 | CMYKColor(0.72f, 0.87f, 0f, 0.09f) 203 | ) eqWithBigPrecisionLoss color.tetradic() 204 | } 205 | 206 | @Test 207 | fun `analogous colors should be calculated as expected`() { 208 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 209 | 210 | Pair( 211 | CMYKColor(0f, 0.72f, 0.87f, 0.09f), 212 | CMYKColor(0f, 0.87f, 0.15f, 0.09f) 213 | ) eqWithBigPrecisionLoss color.analogous() 214 | } 215 | 216 | @Test 217 | fun `converts to RGB and back is idempotent with understandable precision loss`() { 218 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 219 | 220 | color eqWithBigPrecisionLoss color.asRgb().asCmyk() 221 | } 222 | 223 | @Test 224 | fun `converts to ARGB and back is idempotent with understandable precision loss`() { 225 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 226 | 227 | color eqWithBigPrecisionLoss color.asArgb().asCmyk() 228 | } 229 | 230 | @Test 231 | fun `converts to ColorInt and back is idempotent with understandable precision loss`() { 232 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 233 | 234 | color eqWithBigPrecisionLoss color.asColorInt().asCmyk() 235 | } 236 | 237 | @Test 238 | fun `converts to HSL and back is idempotent with minimum loss`() { 239 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 240 | 241 | color eqWithMinimumPrecisionLoss color.asHsl().asCmyk() 242 | } 243 | 244 | @Test 245 | fun `converts to HSLA and back is idempotent with understandable precision loss`() { 246 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 247 | 248 | color eqWithMinimumPrecisionLoss color.asHsla().asCmyk() 249 | } 250 | 251 | @Test 252 | fun `converts to HEX and back is idempotent with understandable precision loss`() { 253 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 254 | 255 | color eqWithBigPrecisionLoss color.asHex().asCmyk() 256 | } 257 | 258 | @Test 259 | fun `converts to HSV and back is idempotent`() { 260 | val color = CMYKColor(0f, 0.87f, 0.58f, 0.09f) 261 | 262 | color eqWithBigPrecisionLoss color.asHsv().asCmyk() 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /library/src/test/java/dev/jorgecastillo/androidcolorx/library/ColorIntTests.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.graphics.Color 4 | import android.os.Build 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import junit.framework.TestCase.assertEquals 7 | import junit.framework.TestCase.assertFalse 8 | import junit.framework.TestCase.assertTrue 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import org.robolectric.annotation.Config 12 | 13 | @Config(sdk = [Build.VERSION_CODES.O_MR1]) 14 | @RunWith(AndroidJUnit4::class) 15 | class ColorIntTests { 16 | 17 | @Test 18 | fun `is dark should return true for #e91e63`() { 19 | val color = Color.parseColor("#e91e63") 20 | 21 | assertTrue(color.isDark()) 22 | } 23 | 24 | @Test 25 | fun `is dark should return false for #1ee9a4`() { 26 | val color = Color.parseColor("#1ee9a4") 27 | 28 | assertFalse(color.isDark()) 29 | } 30 | 31 | @Test 32 | fun `lighten integer should enlighten the color`() { 33 | val color = Color.parseColor("#e91e63") 34 | 35 | assertEquals(-885853, color.lighten(20)) 36 | } 37 | 38 | @Test 39 | fun `lighten float should enlighten the color`() { 40 | val color = Color.parseColor("#e91e63") 41 | 42 | assertEquals(-885853, color.lighten(0.2f)) 43 | } 44 | 45 | @Test 46 | fun `darken integer should darken the color`() { 47 | val color = Color.parseColor("#e91e63") 48 | 49 | assertEquals(-7139781, color.darken(20)) 50 | } 51 | 52 | @Test 53 | fun `darken float should darken the color`() { 54 | val color = Color.parseColor("#e91e63") 55 | 56 | assertEquals(-7139781, color.darken(0.2f)) 57 | } 58 | 59 | @Test 60 | fun `darken integer should be equivalent to dark float`() { 61 | val color = Color.parseColor("#e91e63") 62 | 63 | assertEquals(color.darken(20), color.darken(0.2f)) 64 | } 65 | 66 | @Test 67 | fun `shades should be properly calculated`() { 68 | val color = Color.parseColor("#e91e63") 69 | 70 | assertEquals( 71 | listOf( 72 | -1499549, 73 | -2615977, 74 | -4189362, 75 | -5763004, 76 | -7336390, 77 | -8909776, 78 | -10483417, 79 | -12056803, 80 | -13630189, 81 | -15203830, 82 | -16777216 83 | ), 84 | color.shades() 85 | ) 86 | } 87 | 88 | @Test 89 | fun `shades for a specific count should be properly calculated`() { 90 | val color = Color.parseColor("#e91e63") 91 | 92 | assertEquals( 93 | listOf( 94 | -1499549, 95 | -6287295, 96 | -11532256, 97 | -16777216 98 | ), 99 | color.shades(count = 3) 100 | ) 101 | } 102 | 103 | @Test 104 | fun `tints should be properly calculated`() { 105 | val color = Color.parseColor("#e91e63") 106 | 107 | assertEquals( 108 | listOf( 109 | -1499549, 110 | -1362829, 111 | -1225854, 112 | -1023598, 113 | -886623, 114 | -749903, 115 | -612927, 116 | -476208, 117 | -273696, 118 | -136977, 119 | -1 120 | ), 121 | color.tints() 122 | ) 123 | } 124 | 125 | @Test 126 | fun `tints for specific count should be properly calculated`() { 127 | val color = Color.parseColor("#e91e63") 128 | 129 | assertEquals( 130 | listOf( 131 | -1499549, 132 | -1021545, 133 | -478005, 134 | -1 135 | ), 136 | color.tints(count = 3) 137 | ) 138 | } 139 | 140 | @Test 141 | fun `complimentary colors should be calculated as expected`() { 142 | val color = Color.parseColor("#e91e63") 143 | 144 | assertEquals(-14751324, color.complimentary()) 145 | } 146 | 147 | @Test 148 | fun `returns white as contrasting color for dark colors`() { 149 | val color = Color.parseColor("#e91e63") 150 | 151 | assertEquals(Color.WHITE, color.contrasting()) 152 | } 153 | 154 | @Test 155 | fun `returns black as contrasting color for light colors`() { 156 | val color = Color.parseColor("#d4e7e4") 157 | 158 | assertEquals(Color.BLACK, color.contrasting()) 159 | } 160 | 161 | @Test 162 | fun `returns passed light color as contrasting color for dark colors`() { 163 | val color = Color.parseColor("#e91e63") 164 | val lightColor = Color.parseColor("#d4e7e4") 165 | 166 | assertEquals( 167 | lightColor, 168 | color.contrasting(lightColor = lightColor, darkColor = Color.BLACK) 169 | ) 170 | } 171 | 172 | @Test 173 | fun `returns passed dark color as contrasting color for light colors`() { 174 | val color = Color.parseColor("#d4e7e4") 175 | val darkColor = Color.parseColor("#e91e63") 176 | 177 | assertEquals( 178 | darkColor, 179 | color.contrasting(lightColor = Color.WHITE, darkColor = darkColor) 180 | ) 181 | } 182 | 183 | @Test 184 | fun `triadic colors should be calculated as expected`() { 185 | val color = Color.parseColor("#e91e63") 186 | 187 | assertEquals( 188 | Pair(-10229474, -14785559), 189 | color.triadic() 190 | ) 191 | } 192 | 193 | @Test 194 | fun `tetradic colors should be calculated as expected`() { 195 | val color = Color.parseColor("#e91e63") 196 | 197 | assertEquals( 198 | Triple(-3544802, -14751324, -12706071), 199 | color.tetradic() 200 | ) 201 | } 202 | 203 | @Test 204 | fun `analogous colors should be calculated as expected`() { 205 | val color = Color.parseColor("#e91e63") 206 | 207 | assertEquals( 208 | Pair(-1491426, -1499447), 209 | color.analogous() 210 | ) 211 | } 212 | 213 | @Test 214 | fun `converts to RGB and back is idempotent`() { 215 | val color = Color.parseColor("#e91e63") 216 | 217 | assertEquals(color, color.asRgb().asColorInt()) 218 | } 219 | 220 | @Test 221 | fun `converts to ARGB and back is idempotent`() { 222 | val color = Color.parseColor("#e91e63") 223 | 224 | assertEquals(color, color.asArgb().asColorInt()) 225 | } 226 | 227 | @Test 228 | fun `converts to ARGB assumes 255 alpha`() { 229 | val color = Color.parseColor("#e91e63") 230 | 231 | assertEquals(255, color.asArgb().alpha) 232 | } 233 | 234 | @Test 235 | fun `converts to HEX and back is idempotent`() { 236 | val color = Color.parseColor("#e91e63") 237 | 238 | assertEquals(color, color.asHex().asColorInt()) 239 | } 240 | 241 | @Test 242 | fun `converts to HSL and back is idempotent`() { 243 | val color = Color.parseColor("#e91e63") 244 | 245 | assertEquals(color, color.asHsl().asColorInt()) 246 | } 247 | 248 | @Test 249 | fun `converts to HSLA and back is idempotent`() { 250 | val color = Color.parseColor("#e91e63") 251 | 252 | assertEquals(color, color.asHsla().asColorInt()) 253 | } 254 | 255 | @Test 256 | fun `converts to CMYK and back is idempotent`() { 257 | val color = Color.parseColor("#e91e63") 258 | 259 | assertEquals(color, color.asCmyk().asColorInt()) 260 | } 261 | 262 | @Test 263 | fun `converts to HSV and back is idempotent`() { 264 | val color = Color.parseColor("#e91e63") 265 | 266 | assertEquals(color, color.asHsv().asColorInt()) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /library/src/test/java/dev/jorgecastillo/androidcolorx/library/HEXColorTests.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.os.Build 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import junit.framework.TestCase.assertEquals 6 | import junit.framework.TestCase.assertFalse 7 | import junit.framework.TestCase.assertTrue 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.robolectric.annotation.Config 11 | 12 | @Config(sdk = [Build.VERSION_CODES.O_MR1]) 13 | @RunWith(AndroidJUnit4::class) 14 | class HEXColorTests { 15 | 16 | @Test 17 | fun `is dark should return true for #e91e63`() { 18 | val color = HEXColor("#e91e63") 19 | 20 | assertTrue(color.isDark()) 21 | } 22 | 23 | @Test 24 | fun `is dark should return false for #1ee9a4`() { 25 | val color = HEXColor("#1ee9a4") 26 | 27 | assertFalse(color.isDark()) 28 | } 29 | 30 | @Test 31 | fun `lighten integer should enlighten the color`() { 32 | val color = HEXColor("#e91e63") 33 | 34 | assertEquals(HEXColor("#FFF27BA3"), color.lighten(20)) 35 | } 36 | 37 | @Test 38 | fun `lighten float should enlighten the color`() { 39 | val color = HEXColor("#e91e63") 40 | 41 | assertEquals(HEXColor("#FFF27BA3"), color.lighten(0.2f)) 42 | } 43 | 44 | @Test 45 | fun `darken integer should darken the color`() { 46 | val color = HEXColor("#e91e63") 47 | 48 | assertEquals(HEXColor("#FF930E3B"), color.darken(20)) 49 | } 50 | 51 | @Test 52 | fun `darken float should darken the color`() { 53 | val color = HEXColor("#e91e63") 54 | 55 | assertEquals(HEXColor("#FF930E3B"), color.darken(0.2f)) 56 | } 57 | 58 | @Test 59 | fun `darken integer should be equivalent to dark float`() { 60 | val color = HEXColor("#e91e63") 61 | 62 | assertEquals(color.darken(20), color.darken(0.2f)) 63 | } 64 | 65 | @Test 66 | fun `shades should be properly calculated`() { 67 | val color = HEXColor("#e91e63") 68 | 69 | assertEquals( 70 | listOf( 71 | HEXColor("#FFE91E63"), 72 | HEXColor("#FFD81557"), 73 | HEXColor("#FFC0134E"), 74 | HEXColor("#FFA81044"), 75 | HEXColor("#FF900E3A"), 76 | HEXColor("#FF780C30"), 77 | HEXColor("#FF600927"), 78 | HEXColor("#FF48071D"), 79 | HEXColor("#FF300513"), 80 | HEXColor("#FF18020A"), 81 | HEXColor("#FF000000") 82 | ), 83 | color.shades() 84 | ) 85 | } 86 | 87 | @Test 88 | fun `shades for a specific count should be properly calculated`() { 89 | val color = HEXColor("#e91e63") 90 | 91 | assertEquals( 92 | listOf( 93 | HEXColor("#FFE91E63"), 94 | HEXColor("#FFA01041"), 95 | HEXColor("#FF500820"), 96 | HEXColor("#FF000000") 97 | ), 98 | color.shades(count = 3) 99 | ) 100 | } 101 | 102 | @Test 103 | fun `tints should be properly calculated`() { 104 | val color = HEXColor("#e91e63") 105 | 106 | assertEquals( 107 | listOf( 108 | HEXColor("#FFE91E63"), 109 | HEXColor("#FFEB3473"), 110 | HEXColor("#FFED4B82"), 111 | HEXColor("#FFF06192"), 112 | HEXColor("#FFF278A1"), 113 | HEXColor("#FFF48EB1"), 114 | HEXColor("#FFF6A5C1"), 115 | HEXColor("#FFF8BBD0"), 116 | HEXColor("#FFFBD2E0"), 117 | HEXColor("#FFFDE8EF"), 118 | HEXColor("#FFFFFFFF") 119 | ), 120 | color.tints() 121 | ) 122 | } 123 | 124 | @Test 125 | fun `tints for specific count should be properly calculated`() { 126 | val color = HEXColor("#e91e63") 127 | 128 | assertEquals( 129 | listOf( 130 | HEXColor("#FFE91E63"), 131 | HEXColor("#FFF06997"), 132 | HEXColor("#FFF8B4CB"), 133 | HEXColor("#FFFFFFFF") 134 | ), 135 | color.tints(count = 3) 136 | ) 137 | } 138 | 139 | @Test 140 | fun `complimentary colors should be calculated as expected`() { 141 | val color = HEXColor("#e91e63") 142 | 143 | assertEquals(HEXColor("#FF1EE9A4"), color.complimentary()) 144 | } 145 | 146 | @Test 147 | fun `returns white as contrasting color for dark colors`() { 148 | val color = HEXColor("#e91e63") 149 | 150 | assertEquals(HEXColor("#ffffff"), color.contrasting()) 151 | } 152 | 153 | @Test 154 | fun `returns black as contrasting color for light colors`() { 155 | val color = HEXColor("#FF1EE9A4") 156 | 157 | assertEquals(HEXColor("#000000"), color.contrasting()) 158 | } 159 | 160 | @Test 161 | fun `returns passed light color as contrasting color for dark colors`() { 162 | val color = HEXColor("#e91e63") 163 | val lightColor = HEXColor("#FF1EE9A4") 164 | 165 | assertEquals( 166 | lightColor, 167 | color.contrasting(lightColor = lightColor, darkColor = HEXColor("#000000")) 168 | ) 169 | } 170 | 171 | @Test 172 | fun `returns passed dark color as contrasting color for light colors`() { 173 | val color = HEXColor("#FF1EE9A4") 174 | val darkColor = HEXColor("#e91e63") 175 | 176 | assertEquals( 177 | darkColor, 178 | color.contrasting(lightColor = HEXColor("#ffffff"), darkColor = darkColor) 179 | ) 180 | } 181 | 182 | @Test 183 | fun `triadic colors should be calculated as expected`() { 184 | val color = HEXColor("#e91e63") 185 | 186 | assertEquals( 187 | Pair(HEXColor("#FF63E91E"), HEXColor("#FF1E63E9")), 188 | color.triadic() 189 | ) 190 | } 191 | 192 | @Test 193 | fun `tetradic colors should be calculated as expected`() { 194 | val color = HEXColor("#e91e63") 195 | 196 | assertEquals( 197 | Triple( 198 | HEXColor("#FFC9E91E"), 199 | HEXColor("#FF1EE9A4"), 200 | HEXColor("#FF3E1EE9") 201 | ), 202 | color.tetradic() 203 | ) 204 | } 205 | 206 | @Test 207 | fun `analogous colors should be calculated as expected`() { 208 | val color = HEXColor("#e91e63") 209 | 210 | assertEquals( 211 | Pair(HEXColor("#FFE93E1E"), HEXColor("#FFE91EC9")), 212 | color.analogous() 213 | ) 214 | } 215 | 216 | @Test 217 | fun `converts to RGB and back is idempotent`() { 218 | val color = HEXColor("#e91e63") 219 | 220 | assertEquals(color, color.asRgb().asHex()) 221 | } 222 | 223 | @Test 224 | fun `converts to ARGB and back is idempotent`() { 225 | val color = HEXColor("#e91e63") 226 | 227 | assertEquals(color, color.asArgb().asHex()) 228 | } 229 | 230 | @Test 231 | fun `converts to ColorInt and back is idempotent`() { 232 | val color = HEXColor("#e91e63") 233 | 234 | assertEquals(color, color.asColorInt().asHex()) 235 | } 236 | 237 | @Test 238 | fun `converts to HSL and back is just loses information about alpha`() { 239 | val color = HEXColor("#55e91e63") 240 | 241 | assertEquals(color.copy(hex = "#FF${color.hex.drop(3)}"), color.asHsl().asHex()) 242 | } 243 | 244 | @Test 245 | fun `converts to HSLA and back is idempotent`() { 246 | val color = HEXColor("#55e91e63") 247 | 248 | assertEquals(color, color.asHsla().asHex()) 249 | } 250 | 251 | @Test 252 | fun `converts to CMYK and back just loses information about alpha`() { 253 | val color = HEXColor("#55e91e63") 254 | 255 | assertEquals(color.copy(hex = "#FF${color.hex.drop(3)}"), color.asCmyk().asHex()) 256 | } 257 | 258 | @Test 259 | fun `converts to HSV and back just loses information about alpha`() { 260 | val color = HEXColor("#55e91e63") 261 | 262 | assertEquals(color.copy(hex = "#FF${color.hex.drop(3)}"), color.asHsv().asHex()) 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /library/src/test/java/dev/jorgecastillo/androidcolorx/library/HSLAColorTests.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.os.Build 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import junit.framework.TestCase.assertEquals 6 | import junit.framework.TestCase.assertFalse 7 | import junit.framework.TestCase.assertTrue 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.robolectric.annotation.Config 11 | 12 | @Config(sdk = [Build.VERSION_CODES.O_MR1]) 13 | @RunWith(AndroidJUnit4::class) 14 | class HSLAColorTests { 15 | 16 | @Test 17 | fun `is dark should return true for #e91e63`() { 18 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 19 | 20 | assertTrue(color.isDark()) 21 | } 22 | 23 | @Test 24 | fun `is dark should return false for #1ee9a4`() { 25 | val color = HSLAColor(159.61f, 0.82f, 0.52f, 0.2f) 26 | 27 | assertFalse(color.isDark()) 28 | } 29 | 30 | @Test 31 | fun `lighten integer should enlighten the color`() { 32 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 33 | 34 | HSLAColor(339.61f, 0.82f, 0.72f, 0.2f) eqWithMinimumPrecisionLoss color.lighten(20) 35 | } 36 | 37 | @Test 38 | fun `lighten float should enlighten the color`() { 39 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 40 | 41 | HSLAColor(339.61f, 0.82f, 0.72f, 0.2f) eqWithMinimumPrecisionLoss color.lighten(0.2f) 42 | } 43 | 44 | @Test 45 | fun `darken integer should darken the color`() { 46 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 47 | 48 | HSLAColor(339.61f, 0.82f, 0.32f, 0.2f) eqWithMinimumPrecisionLoss color.darken(20) 49 | } 50 | 51 | @Test 52 | fun `darken float should darken the color`() { 53 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 54 | 55 | HSLAColor(339.61f, 0.82f, 0.32f, 0.2f) eqWithMinimumPrecisionLoss color.darken(0.2f) 56 | } 57 | 58 | @Test 59 | fun `darken integer should be equivalent to dark float`() { 60 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 61 | 62 | assertEquals(color.darken(20), color.darken(0.2f)) 63 | } 64 | 65 | @Test 66 | fun `shades should be properly calculated`() { 67 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 68 | 69 | assertEquals( 70 | listOf( 71 | HSLAColor(339.61f, 0.82f, 0.52f, 0.2f), 72 | HSLAColor(339.61f, 0.82f, 0.468f, 0.2f), 73 | HSLAColor(339.61f, 0.82f, 0.416f, 0.2f), 74 | HSLAColor(339.61f, 0.82f, 0.364f, 0.2f), 75 | HSLAColor(339.61f, 0.82f, 0.312f, 0.2f), 76 | HSLAColor(339.61f, 0.82f, 0.26f, 0.2f), 77 | HSLAColor(339.61f, 0.82f, 0.208f, 0.2f), 78 | HSLAColor(339.61f, 0.82f, 0.156f, 0.2f), 79 | HSLAColor(339.61f, 0.82f, 0.104f, 0.2f), 80 | HSLAColor(339.61f, 0.82f, 0.052f, 0.2f), 81 | HSLAColor(339.61f, 0.82f, 0.00f, 0.2f) 82 | ), 83 | color.shades() 84 | ) 85 | } 86 | 87 | @Test 88 | fun `shades with specific count should be properly calculated`() { 89 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 90 | 91 | assertEquals( 92 | listOf( 93 | HSLAColor(339.61f, 0.82f, 0.52f, 0.2f), 94 | HSLAColor(339.61f, 0.82f, 0.3466667f, 0.2f), 95 | HSLAColor(339.61f, 0.82f, 0.1733334f, 0.2f), 96 | HSLAColor(339.61f, 0.82f, 0.0000001f, 0.2f) 97 | ), 98 | color.shades(count = 3) 99 | ) 100 | } 101 | 102 | @Test 103 | fun `tints should be properly calculated`() { 104 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 105 | 106 | assertEquals( 107 | listOf( 108 | HSLAColor(339.61f, 0.82f, 0.52f, 0.2f), 109 | HSLAColor(339.61f, 0.82f, 0.568f, 0.2f), 110 | HSLAColor(339.61f, 0.82f, 0.616f, 0.2f), 111 | HSLAColor(339.61f, 0.82f, 0.664f, 0.2f), 112 | HSLAColor(339.61f, 0.82f, 0.712f, 0.2f), 113 | HSLAColor(339.61f, 0.82f, 0.76f, 0.2f), 114 | HSLAColor(339.61f, 0.82f, 0.808f, 0.2f), 115 | HSLAColor(339.61f, 0.82f, 0.856f, 0.2f), 116 | HSLAColor(339.61f, 0.82f, 0.904f, 0.2f), 117 | HSLAColor(339.61f, 0.82f, 0.952f, 0.2f), 118 | HSLAColor(339.61f, 0.82f, 1.00f, 0.2f) 119 | ), 120 | color.tints() 121 | ) 122 | } 123 | 124 | @Test 125 | fun `tints for specific count should be properly calculated`() { 126 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 127 | 128 | assertEquals( 129 | listOf( 130 | HSLAColor(339.61f, 0.82f, 0.52f, 0.2f), 131 | HSLAColor(339.61f, 0.82f, 0.68f, 0.2f), 132 | HSLAColor(339.61f, 0.82f, 0.84f, 0.2f), 133 | HSLAColor(339.61f, 0.82f, 1.00f, 0.2f) 134 | ), 135 | color.tints(count = 3) 136 | ) 137 | } 138 | 139 | @Test 140 | fun `complimentary colors should be calculated as expected`() { 141 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 142 | 143 | assertEquals( 144 | HSLAColor( 145 | 159.60999f, 146 | 0.82f, 147 | 0.52f, 148 | 0.2f 149 | ), 150 | color.complimentary() 151 | ) 152 | } 153 | 154 | @Test 155 | fun `returns white as contrasting color for dark colors`() { 156 | val color = HSLAColor(339.7f, 0.82f, 0.52f, 0.2f) 157 | 158 | assertEquals(HSLAColor(0f, 0f, 1f, 1f), color.contrasting()) 159 | } 160 | 161 | @Test 162 | fun `returns black as contrasting color for light colors`() { 163 | val color = HSLAColor(159.70f, 0.82f, 0.52f, 0.2f) 164 | 165 | assertEquals(HSLAColor(0f, 0f, 0f, 1f), color.contrasting()) 166 | } 167 | 168 | @Test 169 | fun `returns passed light color as contrasting color for dark colors`() { 170 | val color = HSLAColor(339.7f, 0.82f, 0.52f, 0.2f) 171 | val lightColor = HSLAColor(159.70f, 0.82f, 0.52f, 0.2f) 172 | 173 | assertEquals( 174 | lightColor, 175 | color.contrasting( 176 | lightColor = lightColor, 177 | darkColor = HSLAColor(0f, 0f, 0f, 1f) 178 | ) 179 | ) 180 | } 181 | 182 | @Test 183 | fun `returns passed dark color as contrasting color for light colors`() { 184 | val color = HSLAColor(159.70f, 0.82f, 0.52f, 0.2f) 185 | val darkColor = HSLAColor(339.7f, 0.82f, 0.52f, 0.2f) 186 | 187 | assertEquals( 188 | darkColor, 189 | color.contrasting( 190 | lightColor = HSLAColor(0f, 0f, 1f, 1f), 191 | darkColor = darkColor 192 | ) 193 | ) 194 | } 195 | 196 | @Test 197 | fun `triadic colors should be calculated as expected`() { 198 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 199 | 200 | assertEquals( 201 | Pair( 202 | HSLAColor(99.609985f, 0.82f, 0.52f, 0.2f), 203 | HSLAColor(219.60999f, 0.82f, 0.52f, 0.2f) 204 | ), 205 | color.triadic() 206 | ) 207 | } 208 | 209 | @Test 210 | fun `tetradic colors should be calculated as expected`() { 211 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 212 | 213 | assertEquals( 214 | Triple( 215 | HSLAColor(69.609985f, 0.82f, 0.52f, 0.2f), 216 | HSLAColor(159.60999f, 0.82f, 0.52f, 0.2f), 217 | HSLAColor(249.60999f, 0.82f, 0.52f, 0.2f) 218 | ), 219 | color.tetradic() 220 | ) 221 | } 222 | 223 | @Test 224 | fun `analogous colors should be calculated as expected`() { 225 | val color = HSLAColor(339.61f, 0.82f, 0.52f, 0.2f) 226 | 227 | assertEquals( 228 | Pair( 229 | HSLAColor(9.609985f, 0.82f, 0.52f, 0.2f), 230 | HSLAColor(309.61f, 0.82f, 0.52f, 0.2f) 231 | ), 232 | color.analogous() 233 | ) 234 | } 235 | 236 | @Test 237 | fun `converts to ColorInt and back is idempotent with understandable precision loss`() { 238 | val color = HSLAColor(339.7f, 0.82f, 0.52f, 0.2f) 239 | 240 | color eqWithBigPrecisionLoss color.asColorInt().asHsla() 241 | } 242 | 243 | @Test 244 | fun `converts to RGB and back just loses information about alpha`() { 245 | val color = HSLAColor(339.7f, 0.82f, 0.52f, 0.2f) 246 | 247 | color.copy(alpha = 1f) eqWithBigPrecisionLoss color.asRgb().asHsla() 248 | } 249 | 250 | @Test 251 | fun `converts to ARGB and back is idempotent with understandable precision loss`() { 252 | val color = HSLAColor(339.7f, 0.82f, 0.52f, 0.2f) 253 | 254 | color eqWithBigPrecisionLoss color.asArgb().asHsla() 255 | } 256 | 257 | @Test 258 | fun `converts to HEX and back is idempotent`() { 259 | val color = HSLAColor(339.7f, 0.82f, 0.52f, 0.2f) 260 | 261 | color eqWithBigPrecisionLoss color.asHex().asHsla() 262 | } 263 | 264 | @Test 265 | fun `converts to HSL and back just loses information about alpha`() { 266 | val color = HSLAColor(339.7f, 0.82f, 0.52f, 0.2f) 267 | 268 | assertEquals( 269 | color.copy(alpha = 1f), 270 | color.asHsl().asHsla() 271 | ) 272 | } 273 | 274 | @Test 275 | fun `converts to CMYK and back just loses information about alpha`() { 276 | val color = HSLAColor(339.7f, 0.82f, 0.52f, 0.2f) 277 | 278 | color.copy(alpha = 1f) eqWithBigPrecisionLoss color.asCmyk().asHsla() 279 | } 280 | 281 | @Test 282 | fun `converts to HSV and back just loses information about alpha`() { 283 | val color = HSLAColor(339.7f, 0.82f, 0.52f, 0.2f) 284 | 285 | color.copy(alpha = 1f) eqWithBigPrecisionLoss color.asHsv().asHsla() 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /library/src/test/java/dev/jorgecastillo/androidcolorx/library/HSLColorTests.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.os.Build 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import junit.framework.TestCase.assertEquals 6 | import junit.framework.TestCase.assertFalse 7 | import junit.framework.TestCase.assertTrue 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.robolectric.annotation.Config 11 | 12 | @Config(sdk = [Build.VERSION_CODES.O_MR1]) 13 | @RunWith(AndroidJUnit4::class) 14 | class HSLColorTests { 15 | 16 | @Test 17 | fun `is dark should return true for #e91e63`() { 18 | val color = HSLColor(339.61f, 0.82f, 0.52f) 19 | 20 | assertTrue(color.isDark()) 21 | } 22 | 23 | @Test 24 | fun `is dark should return false for #1ee9a4`() { 25 | val color = HSLColor(159.61f, 0.82f, 0.52f) 26 | 27 | assertFalse(color.isDark()) 28 | } 29 | 30 | @Test 31 | fun `lighten integer should enlighten the color`() { 32 | val color = HSLColor(339.61f, 0.82f, 0.52f) 33 | 34 | HSLColor(339.61f, 0.82f, 0.72f) eqWithMinimumPrecisionLoss color.lighten(20) 35 | } 36 | 37 | @Test 38 | fun `lighten float should enlighten the color`() { 39 | val color = HSLColor(339.61f, 0.82f, 0.52f) 40 | 41 | HSLColor(339.61f, 0.82f, 0.72f) eqWithMinimumPrecisionLoss color.lighten(0.2f) 42 | } 43 | 44 | @Test 45 | fun `darken integer should darken the color`() { 46 | val color = HSLColor(339.61f, 0.82f, 0.52f) 47 | 48 | HSLColor(339.61f, 0.82f, 0.32f) eqWithMinimumPrecisionLoss color.darken(20) 49 | } 50 | 51 | @Test 52 | fun `darken float should darken the color`() { 53 | val color = HSLColor(339.61f, 0.82f, 0.52f) 54 | 55 | HSLColor(339.61f, 0.82f, 0.32f) eqWithMinimumPrecisionLoss color.darken(0.2f) 56 | } 57 | 58 | @Test 59 | fun `darken integer should be equivalent to dark float`() { 60 | val color = HSLColor(339.61f, 0.82f, 0.52f) 61 | 62 | assertEquals(color.darken(20), color.darken(0.2f)) 63 | } 64 | 65 | @Test 66 | fun `shades should be properly calculated`() { 67 | val color = HSLColor(339.61f, 0.82f, 0.52f) 68 | 69 | assertEquals( 70 | listOf( 71 | HSLColor(339.61f, 0.82f, 0.52f), 72 | HSLColor(339.61f, 0.82f, 0.468f), 73 | HSLColor(339.61f, 0.82f, 0.416f), 74 | HSLColor(339.61f, 0.82f, 0.364f), 75 | HSLColor(339.61f, 0.82f, 0.312f), 76 | HSLColor(339.61f, 0.82f, 0.26f), 77 | HSLColor(339.61f, 0.82f, 0.208f), 78 | HSLColor(339.61f, 0.82f, 0.156f), 79 | HSLColor(339.61f, 0.82f, 0.104f), 80 | HSLColor(339.61f, 0.82f, 0.052f), 81 | HSLColor(339.61f, 0.82f, 0.00f) 82 | ), 83 | color.shades() 84 | ) 85 | } 86 | 87 | @Test 88 | fun `shades with specific count should be properly calculated`() { 89 | val color = HSLColor(339.61f, 0.82f, 0.52f) 90 | 91 | assertEquals( 92 | listOf( 93 | HSLColor(339.61f, 0.82f, 0.52f), 94 | HSLColor(339.61f, 0.82f, 0.3466667f), 95 | HSLColor(339.61f, 0.82f, 0.1733334f), 96 | HSLColor(339.61f, 0.82f, 0.0000001f) 97 | ), 98 | color.shades(count = 3) 99 | ) 100 | } 101 | 102 | @Test 103 | fun `tints should be properly calculated`() { 104 | val color = HSLColor(339.61f, 0.82f, 0.52f) 105 | 106 | assertEquals( 107 | listOf( 108 | HSLColor(339.61f, 0.82f, 0.52f), 109 | HSLColor(339.61f, 0.82f, 0.568f), 110 | HSLColor(339.61f, 0.82f, 0.616f), 111 | HSLColor(339.61f, 0.82f, 0.664f), 112 | HSLColor(339.61f, 0.82f, 0.712f), 113 | HSLColor(339.61f, 0.82f, 0.76f), 114 | HSLColor(339.61f, 0.82f, 0.808f), 115 | HSLColor(339.61f, 0.82f, 0.856f), 116 | HSLColor(339.61f, 0.82f, 0.904f), 117 | HSLColor(339.61f, 0.82f, 0.952f), 118 | HSLColor(339.61f, 0.82f, 1.00f) 119 | ), 120 | color.tints() 121 | ) 122 | } 123 | 124 | @Test 125 | fun `tints for specific count should be properly calculated`() { 126 | val color = HSLColor(339.61f, 0.82f, 0.52f) 127 | 128 | assertEquals( 129 | listOf( 130 | HSLColor(339.61f, 0.82f, 0.52f), 131 | HSLColor(339.61f, 0.82f, 0.68f), 132 | HSLColor(339.61f, 0.82f, 0.84f), 133 | HSLColor(339.61f, 0.82f, 1.00f) 134 | ), 135 | color.tints(count = 3) 136 | ) 137 | } 138 | 139 | @Test 140 | fun `complimentary colors should be calculated as expected`() { 141 | val color = HSLColor(339.61f, 0.82f, 0.52f) 142 | 143 | assertEquals( 144 | HSLColor(159.60999f, 0.82f, 0.52f), 145 | color.complimentary() 146 | ) 147 | } 148 | 149 | @Test 150 | fun `returns white as contrasting color for dark colors`() { 151 | val color = HSLColor(339.7f, 0.82f, 0.52f) 152 | 153 | assertEquals(HSLColor(0f, 0f, 1f), color.contrasting()) 154 | } 155 | 156 | @Test 157 | fun `returns black as contrasting color for light colors`() { 158 | val color = HSLColor(159.70f, 0.82f, 0.52f) 159 | 160 | assertEquals(HSLColor(0f, 0f, 0f), color.contrasting()) 161 | } 162 | 163 | @Test 164 | fun `returns passed light color as contrasting color for dark colors`() { 165 | val color = HSLColor(339.7f, 0.82f, 0.52f) 166 | val lightColor = HSLColor(159.70f, 0.82f, 0.52f) 167 | 168 | assertEquals( 169 | lightColor, 170 | color.contrasting( 171 | lightColor = lightColor, 172 | darkColor = HSLColor(0f, 0f, 0f) 173 | ) 174 | ) 175 | } 176 | 177 | @Test 178 | fun `returns passed dark color as contrasting color for light colors`() { 179 | val color = HSLColor(159.70f, 0.82f, 0.52f) 180 | val darkColor = HSLColor(339.7f, 0.82f, 0.52f) 181 | 182 | assertEquals( 183 | darkColor, 184 | color.contrasting( 185 | lightColor = HSLColor(0f, 0f, 1f), 186 | darkColor = darkColor 187 | ) 188 | ) 189 | } 190 | 191 | @Test 192 | fun `triadic colors should be calculated as expected`() { 193 | val color = HSLColor(339.61f, 0.82f, 0.52f) 194 | 195 | assertEquals( 196 | Pair( 197 | HSLColor(99.609985f, 0.82f, 0.52f), 198 | HSLColor(219.60999f, 0.82f, 0.52f) 199 | ), 200 | color.triadic() 201 | ) 202 | } 203 | 204 | @Test 205 | fun `tetradic colors should be calculated as expected`() { 206 | val color = HSLColor(339.61f, 0.82f, 0.52f) 207 | 208 | assertEquals( 209 | Triple( 210 | HSLColor(69.609985f, 0.82f, 0.52f), 211 | HSLColor(159.60999f, 0.82f, 0.52f), 212 | HSLColor(249.60999f, 0.82f, 0.52f) 213 | ), 214 | color.tetradic() 215 | ) 216 | } 217 | 218 | @Test 219 | fun `analogous colors should be calculated as expected`() { 220 | val color = HSLColor(339.61f, 0.82f, 0.52f) 221 | 222 | assertEquals( 223 | Pair( 224 | HSLColor(9.609985f, 0.82f, 0.52f), 225 | HSLColor(309.61f, 0.82f, 0.52f) 226 | ), 227 | color.analogous() 228 | ) 229 | } 230 | 231 | @Test 232 | fun `converts to ColorInt and back is idempotent with understandable precision loss`() { 233 | val color = HSLColor(339.7f, 0.82f, 0.52f) 234 | 235 | color eqWithBigPrecisionLoss color.asColorInt().asHsl() 236 | } 237 | 238 | @Test 239 | fun `converts to RGB and back is idempotent with understandable precision loss`() { 240 | val color = HSLColor(339.7f, 0.82f, 0.52f) 241 | 242 | color eqWithBigPrecisionLoss color.asRgb().asHsl() 243 | } 244 | 245 | @Test 246 | fun `converts to ARGB and back is idempotent with understandable precision loss`() { 247 | val color = HSLColor(339.7f, 0.82f, 0.52f) 248 | 249 | color eqWithBigPrecisionLoss color.asArgb().asHsl() 250 | } 251 | 252 | @Test 253 | fun `converts to ARGB assumes 255 alpha`() { 254 | val color = HSLColor(339.7f, 0.82f, 0.52f) 255 | 256 | assertEquals(255, color.asArgb().alpha) 257 | } 258 | 259 | @Test 260 | fun `converts to HEX and back is idempotent`() { 261 | val color = HSLColor(339.7f, 0.82f, 0.52f) 262 | 263 | color eqWithBigPrecisionLoss color.asHex().asHsl() 264 | } 265 | 266 | @Test 267 | fun `converts to HSLA and back is idempotent`() { 268 | val color = HSLColor(339.7f, 0.82f, 0.52f) 269 | 270 | assertEquals( 271 | color, 272 | color.asHsla().asHsl() 273 | ) 274 | } 275 | 276 | @Test 277 | fun `converts to HSLA assumes 1f alpha`() { 278 | val color = HSLColor(339.7f, 0.82f, 0.52f) 279 | 280 | assertEquals(1f, color.asHsla().alpha) 281 | } 282 | 283 | @Test 284 | fun `converts to CMYK and back is idempotent`() { 285 | val color = HSLColor(339.7f, 0.82f, 0.52f) 286 | 287 | color eqWithBigPrecisionLoss color.asCmyk().asHsl() 288 | } 289 | 290 | @Test 291 | fun `converts to HSV and back is idempotent`() { 292 | val color = HSLColor(339.7f, 0.82f, 0.52f) 293 | 294 | color eqWithBigPrecisionLoss color.asHsv().asHsl() 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /library/src/test/java/dev/jorgecastillo/androidcolorx/library/HSVColorTests.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.os.Build 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import junit.framework.TestCase.assertEquals 6 | import junit.framework.TestCase.assertFalse 7 | import junit.framework.TestCase.assertTrue 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.robolectric.annotation.Config 11 | 12 | @Config(sdk = [Build.VERSION_CODES.O_MR1]) 13 | @RunWith(AndroidJUnit4::class) 14 | class HSVColorTests { 15 | 16 | @Test 17 | fun `is dark should return true for #e91e63`() { 18 | val color = HSVColor(339.61f, 0.82f, 0.52f) 19 | 20 | assertTrue(color.isDark()) 21 | } 22 | 23 | @Test 24 | fun `is dark should return false for #1ee9a4`() { 25 | val color = HSVColor(290f, 0.17f, 1f) 26 | 27 | assertFalse(color.isDark()) 28 | } 29 | 30 | @Test 31 | fun `lighten integer should enlighten the color`() { 32 | val color = HSVColor(339.61f, 0.82f, 0.52f) 33 | 34 | HSVColor(339.43f, 0.81f, 0.85f) eqWithBigPrecisionLoss 35 | color.lighten(20) 36 | } 37 | 38 | @Test 39 | fun `lighten float should enlighten the color`() { 40 | val color = HSVColor(339.61f, 0.82f, 0.52f) 41 | 42 | HSVColor(339.43f, 0.81f, 0.85f) eqWithBigPrecisionLoss 43 | color.lighten(0.2f) 44 | } 45 | 46 | @Test 47 | fun `lighten integer should be equivalent to lighten float`() { 48 | val color = HSVColor(339.61f, 0.82f, 0.52f) 49 | 50 | assertEquals(color.lighten(20), color.lighten(0.2f)) 51 | } 52 | 53 | @Test 54 | fun `darken integer should darken the color`() { 55 | val color = HSVColor(339.61f, 0.82f, 0.52f) 56 | 57 | HSVColor(340f, 0.83f, 0.18f) eqWithBigPrecisionLoss 58 | color.darken(20) 59 | } 60 | 61 | @Test 62 | fun `darken float should darken the color`() { 63 | val color = HSVColor(339.61f, 0.82f, 0.52f) 64 | 65 | HSVColor(340f, 0.83f, 0.18f) eqWithBigPrecisionLoss 66 | color.darken(0.2f) 67 | } 68 | 69 | @Test 70 | fun `darken integer should be equivalent to dark float`() { 71 | val color = HSVColor(339.61f, 0.82f, 0.52f) 72 | 73 | assertEquals(color.darken(20), color.darken(0.2f)) 74 | } 75 | 76 | @Test 77 | fun `shades should be properly calculated`() { 78 | val color = HSVColor(339.61f, 0.82f, 0.52f) 79 | 80 | listOf( 81 | HSVColor(339.63f, 0.82f, 0.52f), 82 | HSVColor(339.8f, 0.82f, 0.47f), 83 | HSVColor(339.31f, 0.82f, 0.42f), 84 | HSVColor(339.47f, 0.82f, 0.36f), 85 | HSVColor(339.09f, 0.82f, 0.31f), 86 | HSVColor(339.27f, 0.82f, 0.26f), 87 | HSVColor(340.47f, 0.81f, 0.21f), 88 | HSVColor(340f, 0.82f, 0.16f), 89 | HSVColor(340.91f, 0.81f, 0.11f), 90 | HSVColor(338.18f, 0.85f, 0.05f), 91 | HSVColor(0.00f, 0.00f, 0.00f) 92 | ) eqWithBigPrecisionLoss color.shades() 93 | } 94 | 95 | @Test 96 | fun `shades with specific count should be properly calculated`() { 97 | val color = HSVColor(339.61f, 0.82f, 0.52f) 98 | 99 | listOf( 100 | HSVColor(339.63f, 0.82f, 0.52f), 101 | HSVColor(339.45f, 0.82f, 0.35f), 102 | HSVColor(340f, 0.82f, 0.17f), 103 | HSVColor(0f, 0f, 0f) 104 | ) eqWithBigPrecisionLoss color.shades(count = 3) 105 | } 106 | 107 | @Test 108 | fun `tints should be properly calculated`() { 109 | val color = HSVColor(339.61f, 0.82f, 0.52f) 110 | 111 | listOf( 112 | HSVColor(339.63f, 0.82f, 0.52f), 113 | HSVColor(339.4f, 0.82f, 0.64f), 114 | HSVColor(339.87f, 0.82f, 0.76f), 115 | HSVColor(339.65f, 0.79f, 0.85f), 116 | HSVColor(340.0f, 0.66f, 0.87f), 117 | HSVColor(339.51f, 0.54f, 0.89f), 118 | HSVColor(339.18f, 0.42f, 0.91f), 119 | HSVColor(339.73f, 0.31f, 0.94f), 120 | HSVColor(339.18f, 0.2f, 0.96f), 121 | HSVColor(340.8f, 0.1f, 0.98f), 122 | HSVColor(0.00f, 0.00f, 1.00f) 123 | ) eqWithBigPrecisionLoss color.tints() 124 | } 125 | 126 | @Test 127 | fun `tints with specific count should be properly calculated`() { 128 | val color = HSVColor(339.61f, 0.82f, 0.52f) 129 | 130 | listOf( 131 | HSVColor(339.63f, 0.82f, 0.52f), 132 | HSVColor(339.75f, 0.74f, 0.86f), 133 | HSVColor(339.51f, 0.35f, 0.93f), 134 | HSVColor(0.00f, 0.00f, 1.00f) 135 | ) eqWithBigPrecisionLoss color.tints(count = 3) 136 | } 137 | 138 | @Test 139 | fun `complimentary colors should be calculated as expected`() { 140 | val color = HSVColor(339.61f, 0.82f, 0.52f) 141 | 142 | HSVColor(159.63f, 0.82f, 0.52f) eqWithBigPrecisionLoss 143 | color.complimentary() 144 | } 145 | 146 | @Test 147 | fun `returns white as contrasting color for dark colors`() { 148 | val color = HSVColor(339.7f, 0.82f, 0.52f) 149 | 150 | assertEquals(HSVColor(0f, 0f, 1f), color.contrasting()) 151 | } 152 | 153 | @Test 154 | fun `returns black as contrasting color for light colors`() { 155 | val color = HSVColor(290f, 0.17f, 1f) 156 | 157 | assertEquals(HSVColor(0f, 0f, 0f), color.contrasting()) 158 | } 159 | 160 | @Test 161 | fun `returns passed light color as contrasting color for dark colors`() { 162 | val color = HSVColor(339.7f, 0.82f, 0.52f) 163 | val lightColor = HSVColor(290f, 0.17f, 1f) 164 | 165 | assertEquals( 166 | lightColor, 167 | color.contrasting( 168 | lightColor = lightColor, 169 | darkColor = HSVColor(0f, 0f, 0f) 170 | ) 171 | ) 172 | } 173 | 174 | @Test 175 | fun `returns passed dark color as contrasting color for light colors`() { 176 | val color = HSVColor(290f, 0.17f, 1f) 177 | val darkColor = HSVColor(339.7f, 0.82f, 0.52f) 178 | 179 | assertEquals( 180 | darkColor, 181 | color.contrasting( 182 | lightColor = HSVColor(0f, 0f, 1f), 183 | darkColor = darkColor 184 | ) 185 | ) 186 | } 187 | 188 | @Test 189 | fun `triadic colors should be calculated as expected`() { 190 | val color = HSVColor(339.61f, 0.82f, 0.52f) 191 | 192 | assertEquals( 193 | Pair( 194 | HSVColor(99.609985f, 0.82f, 0.52f), 195 | HSVColor(219.60999f, 0.82f, 0.52f) 196 | ), 197 | color.triadic() 198 | ) 199 | } 200 | 201 | @Test 202 | fun `tetradic colors should be calculated as expected`() { 203 | val color = HSVColor(339.61f, 0.82f, 0.52f) 204 | 205 | assertEquals( 206 | Triple( 207 | HSVColor(69.609985f, 0.82f, 0.52f), 208 | HSVColor(159.60999f, 0.82f, 0.52f), 209 | HSVColor(249.60999f, 0.82f, 0.52f) 210 | ), 211 | color.tetradic() 212 | ) 213 | } 214 | 215 | @Test 216 | fun `analogous colors should be calculated as expected`() { 217 | val color = HSVColor(339.61f, 0.82f, 0.52f) 218 | 219 | Pair( 220 | HSVColor(9.36f, 0.82f, 0.52f), 221 | HSVColor(309.91f, 0.82f, 0.52f) 222 | ) eqWithBigPrecisionLoss Pair( 223 | color.analogous().first, 224 | color.analogous().second 225 | ) 226 | } 227 | 228 | @Test 229 | fun `converts to ColorInt and back is idempotent with understandable precision loss`() { 230 | val color = HSVColor(339.63f, 0.82f, 0.52f) 231 | 232 | color eqWithBigPrecisionLoss color.asColorInt().asHsv() 233 | } 234 | 235 | @Test 236 | fun `converts to RGB and back is idempotent with understandable precision loss`() { 237 | val color = HSVColor(339.63f, 0.82f, 0.52f) 238 | 239 | color eqWithBigPrecisionLoss color.asRgb().asHsv() 240 | } 241 | 242 | @Test 243 | fun `converts to ARGB and back is idempotent with understandable precision loss`() { 244 | val color = HSVColor(339.63f, 0.82f, 0.52f) 245 | 246 | color eqWithBigPrecisionLoss color.asArgb().asHsv() 247 | } 248 | 249 | @Test 250 | fun `converts to ARGB assumes 255 alpha`() { 251 | val color = HSVColor(339.63f, 0.82f, 0.52f) 252 | 253 | assertEquals(255, color.asArgb().alpha) 254 | } 255 | 256 | @Test 257 | fun `converts to HEX and back is idempotent`() { 258 | val color = HSVColor(339.63f, 0.82f, 0.52f) 259 | 260 | color eqWithBigPrecisionLoss color.asHex().asHsv() 261 | } 262 | 263 | @Test 264 | fun `converts to HSL and back is idempotent`() { 265 | val color = HSVColor(339.63f, 0.82f, 0.52f) 266 | 267 | color eqWithBigPrecisionLoss color.asHsl().asHsv() 268 | } 269 | 270 | @Test 271 | fun `converts to HSLA and back is idempotent`() { 272 | val color = HSVColor(339.63f, 0.82f, 0.52f) 273 | 274 | color eqWithBigPrecisionLoss color.asHsla().asHsv() 275 | } 276 | 277 | @Test 278 | fun `converts to HSLA assumes 1f alpha`() { 279 | val color = HSVColor(339.63f, 0.82f, 0.52f) 280 | 281 | assertEquals(1f, color.asHsla().alpha) 282 | } 283 | 284 | @Test 285 | fun `converts to CMYK and back is idempotent`() { 286 | val color = HSVColor(339.63f, 0.82f, 0.52f) 287 | 288 | color eqWithBigPrecisionLoss color.asCmyk().asHsv() 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /library/src/test/java/dev/jorgecastillo/androidcolorx/library/RGBColorTests.kt: -------------------------------------------------------------------------------- 1 | package dev.jorgecastillo.androidcolorx.library 2 | 3 | import android.os.Build 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import junit.framework.TestCase.assertEquals 6 | import junit.framework.TestCase.assertFalse 7 | import junit.framework.TestCase.assertTrue 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.robolectric.annotation.Config 11 | 12 | @Config(sdk = [Build.VERSION_CODES.O_MR1]) 13 | @RunWith(AndroidJUnit4::class) 14 | class RGBColorTests { 15 | 16 | @Test 17 | fun `is dark should return true for #e91e63`() { 18 | val color = RGBColor(233, 30, 99) 19 | 20 | assertTrue(color.isDark()) 21 | } 22 | 23 | @Test 24 | fun `is dark should return false for #1ee9a4`() { 25 | val color = RGBColor(30, 233, 164) 26 | 27 | assertFalse(color.isDark()) 28 | } 29 | 30 | @Test 31 | fun `lighten integer should enlighten the color`() { 32 | val color = RGBColor(233, 30, 99) 33 | 34 | assertEquals(RGBColor(242, 123, 163), color.lighten(20)) 35 | } 36 | 37 | @Test 38 | fun `lighten float should enlighten the color`() { 39 | val color = RGBColor(233, 30, 99) 40 | 41 | assertEquals(RGBColor(242, 123, 163), color.lighten(0.2f)) 42 | } 43 | 44 | @Test 45 | fun `darken integer should darken the color`() { 46 | val color = RGBColor(233, 30, 99) 47 | 48 | assertEquals(RGBColor(147, 14, 59), color.darken(20)) 49 | } 50 | 51 | @Test 52 | fun `darken float should darken the color`() { 53 | val color = RGBColor(233, 30, 99) 54 | 55 | assertEquals(RGBColor(147, 14, 59), color.darken(0.2f)) 56 | } 57 | 58 | @Test 59 | fun `darken integer should be equivalent to dark float`() { 60 | val color = RGBColor(233, 30, 99) 61 | 62 | assertEquals(color.darken(20), color.darken(0.2f)) 63 | } 64 | 65 | @Test 66 | fun `shades should be properly calculated`() { 67 | val color = RGBColor(233, 30, 99) 68 | 69 | assertEquals( 70 | listOf( 71 | RGBColor(233, 30, 99), 72 | RGBColor(216, 21, 87), 73 | RGBColor(192, 19, 78), 74 | RGBColor(168, 16, 68), 75 | RGBColor(144, 14, 58), 76 | RGBColor(120, 12, 48), 77 | RGBColor(96, 9, 39), 78 | RGBColor(72, 7, 29), 79 | RGBColor(48, 5, 19), 80 | RGBColor(24, 2, 10), 81 | RGBColor(0, 0, 0) 82 | ), 83 | color.shades() 84 | ) 85 | } 86 | 87 | @Test 88 | fun `shades with specific count should be properly calculated`() { 89 | val color = RGBColor(233, 30, 99) 90 | 91 | assertEquals( 92 | listOf( 93 | RGBColor(233, 30, 99), 94 | RGBColor(160, 16, 65), 95 | RGBColor(80, 8, 32), 96 | RGBColor(0, 0, 0) 97 | ), 98 | color.shades(count = 3) 99 | ) 100 | } 101 | 102 | @Test 103 | fun `tints should be properly calculated`() { 104 | val color = RGBColor(233, 30, 99) 105 | 106 | assertEquals( 107 | listOf( 108 | RGBColor(233, 30, 99), 109 | RGBColor(235, 52, 115), 110 | RGBColor(237, 75, 130), 111 | RGBColor(240, 97, 146), 112 | RGBColor(242, 120, 161), 113 | RGBColor(244, 142, 177), 114 | RGBColor(246, 165, 193), 115 | RGBColor(248, 187, 208), 116 | RGBColor(251, 210, 224), 117 | RGBColor(253, 232, 239), 118 | RGBColor(255, 255, 255) 119 | ), 120 | color.tints() 121 | ) 122 | } 123 | 124 | @Test 125 | fun `tints for specific count should be properly calculated`() { 126 | val color = RGBColor(233, 30, 99) 127 | 128 | assertEquals( 129 | listOf( 130 | RGBColor(233, 30, 99), 131 | RGBColor(240, 105, 151), 132 | RGBColor(248, 180, 203), 133 | RGBColor(255, 255, 255) 134 | ), 135 | color.tints(count = 3) 136 | ) 137 | } 138 | 139 | @Test 140 | fun `complimentary colors should be calculated as expected`() { 141 | val color = RGBColor(233, 30, 99) 142 | 143 | assertEquals(RGBColor(30, 233, 164), color.complimentary()) 144 | } 145 | 146 | @Test 147 | fun `returns white as contrasting color for dark colors`() { 148 | val color = RGBColor(233, 30, 99) 149 | 150 | assertEquals(RGBColor(255, 255, 255), color.contrasting()) 151 | } 152 | 153 | @Test 154 | fun `returns black as contrasting color for light colors`() { 155 | val color = RGBColor(251, 234, 248) 156 | 157 | assertEquals(RGBColor(0, 0, 0), color.contrasting()) 158 | } 159 | 160 | @Test 161 | fun `returns passed light color as contrasting color for dark colors`() { 162 | val color = RGBColor(233, 30, 99) 163 | val lightColor = RGBColor(251, 234, 248) 164 | 165 | assertEquals( 166 | lightColor, 167 | color.contrasting(lightColor = lightColor, darkColor = RGBColor(0, 0, 0)) 168 | ) 169 | } 170 | 171 | @Test 172 | fun `returns passed dark color as contrasting color for light colors`() { 173 | val color = RGBColor(251, 234, 248) 174 | val darkColor = RGBColor(233, 30, 99) 175 | 176 | assertEquals( 177 | darkColor, 178 | color.contrasting(lightColor = RGBColor(255, 255, 255), darkColor = darkColor) 179 | ) 180 | } 181 | 182 | @Test 183 | fun `triadic colors should be calculated as expected`() { 184 | val color = RGBColor(233, 30, 99) 185 | 186 | assertEquals( 187 | Pair(RGBColor(99, 233, 30), RGBColor(30, 99, 233)), 188 | color.triadic() 189 | ) 190 | } 191 | 192 | @Test 193 | fun `tetradic colors should be calculated as expected`() { 194 | val color = RGBColor(233, 30, 99) 195 | 196 | assertEquals( 197 | Triple(RGBColor(201, 233, 30), RGBColor(30, 233, 164), RGBColor(62, 30, 233)), 198 | color.tetradic() 199 | ) 200 | } 201 | 202 | @Test 203 | fun `analogous colors should be calculated as expected`() { 204 | val color = RGBColor(233, 30, 99) 205 | 206 | assertEquals( 207 | Pair(RGBColor(233, 62, 30), RGBColor(233, 30, 201)), 208 | color.analogous() 209 | ) 210 | } 211 | 212 | @Test 213 | fun `converts to ARGB and back is idempotent`() { 214 | val color = RGBColor(233, 30, 99) 215 | 216 | assertEquals(color, color.asArgb().asRgb()) 217 | } 218 | 219 | @Test 220 | fun `converts to ARGB assumes 255 alpha`() { 221 | val color = RGBColor(233, 30, 99) 222 | 223 | assertEquals(255, color.asArgb().alpha) 224 | } 225 | 226 | @Test 227 | fun `converts to HEX and back is idempotent`() { 228 | val color = RGBColor(233, 30, 99) 229 | 230 | assertEquals(color, color.asHex().asRgb()) 231 | } 232 | 233 | @Test 234 | fun `converts to ColorInt and back is idempotent`() { 235 | val color = RGBColor(233, 30, 99) 236 | 237 | assertEquals(color, color.asColorInt().asRgb()) 238 | } 239 | 240 | @Test 241 | fun `converts to HSL and back is idempotent`() { 242 | val color = RGBColor(233, 30, 99) 243 | 244 | assertEquals(color, color.asHsl().asRgb()) 245 | } 246 | 247 | @Test 248 | fun `converts to HSLA and back is idempotent`() { 249 | val color = RGBColor(233, 30, 99) 250 | 251 | assertEquals(color, color.asHsla().asRgb()) 252 | } 253 | 254 | @Test 255 | fun `converts to CMYK and back is idempotent`() { 256 | val color = RGBColor(233, 30, 99) 257 | 258 | assertEquals(color, color.asCmyk().asRgb()) 259 | } 260 | 261 | @Test 262 | fun `converts to HSV and back is idempotent`() { 263 | val color = RGBColor(233, 30, 99) 264 | 265 | assertEquals(color, color.asHsv().asRgb()) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library' 2 | rootProject.name='AndroidColorX' 3 | --------------------------------------------------------------------------------