├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── apex-demo ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── dev │ │ └── romainguy │ │ └── apex │ │ ├── MainActivity.kt │ │ └── TestData.kt │ └── res │ ├── drawable-nodpi │ ├── car.jpg │ ├── mountains.jpg │ └── tokyo.jpg │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values-night │ └── themes.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── apex ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── romainguy │ └── apex │ ├── Button.kt │ ├── Colors.kt │ ├── Components.kt │ ├── Element.kt │ ├── Integration.kt │ ├── Layouts.kt │ ├── Providers.kt │ ├── Renderer.kt │ └── android │ └── AndroidRenderer.kt ├── assets └── apex_demo.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Apex -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apex 2 | 3 | Apex is just a simple proof of concept to demonstrate how easily you can build your own UI Toolkit 4 | from scratch. This code base is most likely full of bugs and design mistakes but it should help 5 | you understand the basics of a UI Toolkit. It is meant as a learning and demonstration tool only. 6 | 7 | Tested only in an emulator and only on API level 31. 8 | 9 | ## Concepts 10 | 11 | Widgets are called elements and are all instances of the `Element` class. Apex elements are not 12 | intended to be subclassed, instead you build widgets by adding components to an element. Each 13 | component has a single responsibility: layout, rendering, input event, or whatever else you want. 14 | Components can be anything, and in the current codebase some are classes, some are interfaces, some 15 | are enums. 16 | 17 | For instance a `Button` is an `Element` with the following components: 18 | 19 | - A `ButtonModel` (text, click listener, etc.), the public API of a button 20 | - A `RenderComponent`, to render the button 21 | - A `LayoutComponent`, to compute its own size and position the text 22 | - A `MotionInputComponent`, to react the touch events and handle clicks 23 | - An `InternalState`, to track the pressed state of the button 24 | 25 | Apex also offers `Provider` instances, which are roughly equivalent to Jetpack Compose's composition 26 | locals. They give access to global data throughout the tree: `Resources`, display density, the 27 | current theme, etc. Any `Element` can inject new providers or override existing providers by using 28 | the `ProviderComponent` component. `MainActivity` shows an example of using a `ThemeProvider` to 29 | modify the current theme. 30 | 31 | ## Exercises for the reader 32 | 33 | If you'd like to play with this codebase a bit, here are a few things you could try: 34 | 35 | - Optimize components lookup. Right now, every lookup iterates over a flat list. It's not a big deal 36 | since most elements will have a short list but this could be improved. Since it's intended that an 37 | element can own multiple components of the same type, you'd probably have a data structure that 38 | maps component types to a list (a linked hashmap for instance) 39 | - Optimize providers handling. Every layout/render/motion input phase currently re-applies the 40 | providers. It's not very efficient. And the layout phase doesn't correctly apply the providers at 41 | every level of the tree 42 | - Take the `MotionInputComponent` from `Button` and make it a generic, reusable API so you can 43 | perform clicks on the `Image` widget in `MainActivity` as well 44 | - Track changes in data models to re-layout/re-draw only when needed 45 | - Don't relayout/redraw on *every* v-sync. It's wasteful 46 | - Reduce memory allocations (esp. generated by the many `RectF` and `SizeF` instances, among 47 | other things) 48 | - Cleanup the inline/noinline/crossinline and reified generic mess in the various helper functions 49 | - Make this a multi-platform UI Toolkit! Remove Android-specific APIs (`Canvas`, `Bitmap`, etc.) 50 | and use your own abstractions. For rendering, use [skiko](https://github.com/JetBrains/skiko) 51 | 52 | ## Screenshot 53 | 54 | Not super exciting, but here it is: 55 | 56 | ![Apex demo: a photo centered on screen with two buttons below, Previous and Next](./assets/apex_demo.png) 57 | 58 | ## License 59 | 60 | See [LICENSE](./LICENSE). 61 | -------------------------------------------------------------------------------- /apex-demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /apex-demo/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | applicationId "dev.romainguy.apex" 11 | minSdk 31 12 | targetSdk 31 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | freeCompilerArgs += '-Xcontext-receivers' 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation 'androidx.core:core-ktx:1.7.0' 37 | implementation 'com.google.android.material:material:1.5.0' 38 | implementation project(path: ':apex') 39 | } 40 | -------------------------------------------------------------------------------- /apex-demo/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 -------------------------------------------------------------------------------- /apex-demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /apex-demo/src/main/java/dev/romainguy/apex/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.romainguy.apex 2 | 3 | import android.app.Activity 4 | import android.graphics.* 5 | import android.os.Bundle 6 | import android.util.SizeF 7 | 8 | enum class Id { 9 | ButtonNext, 10 | ButtonPrevious 11 | } 12 | 13 | class MainActivity : Activity() { 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | 17 | loadTestImages(this) 18 | 19 | var imageIndex = 0 20 | 21 | setContent { 22 | 23 | // Simpler, non-interactive version of the same UI: 24 | // Column { 25 | // Alignment(HorizontalAlignment.Center) 26 | // Alignment(VerticalAlignment.Center) 27 | // 28 | // Padding(16.0f) 29 | // 30 | // Image(ImageModel(TestImage[0])) 31 | // 32 | // Row { 33 | // Button(ButtonModel("Previous", State.Disabled)) 34 | // Button(ButtonModel("Next")) 35 | // } 36 | // } 37 | 38 | // Example of how to use providers to override global resources down the tree. 39 | // Providers can be set at any level: 40 | // Provider(ThemeProvider( 41 | // style = Paint.Style.FILL_AND_STROKE, 42 | // border = Color.valueOf(0.46f, 0.81f, 0.57f), 43 | // contentBackground = Color.valueOf(0.91f, 0.96f, 0.90f), 44 | // disabled = Color.valueOf(0.46f, 0.81f, 0.57f).desaturated(), 45 | // text = Color.valueOf(0.46f, 0.81f, 0.57f), 46 | // typeface = Typeface.create( 47 | // Typeface.create("sans-serif-condensed", Typeface.NORMAL), 48 | // 200, 49 | // false 50 | // ), 51 | // strokeWidth = 1.0f 52 | // )) 53 | 54 | Column { 55 | Alignment(HorizontalAlignment.Center) 56 | Alignment(VerticalAlignment.Center) 57 | 58 | Padding(16.0f) 59 | 60 | val image = Image(ImageModel(TestImage[imageIndex])) 61 | 62 | Row { 63 | Button(ButtonModel("Previous", State.Disabled) { previous -> 64 | if (imageIndex > 0) imageIndex-- 65 | requireChild(Id.ButtonNext).component().state = 66 | if (imageIndex < TestImage.size) State.Enabled else State.Disabled 67 | previous.component().state = 68 | if (imageIndex > 0) State.Enabled else State.Disabled 69 | image.component().bitmap = TestImage[imageIndex] 70 | }).addComponent(Id.ButtonPrevious) 71 | 72 | Button(ButtonModel("Next") { next -> 73 | if (imageIndex < TestImage.size - 1) imageIndex++ 74 | next.component().state = 75 | if (imageIndex < TestImage.size - 1) State.Enabled else State.Disabled 76 | requireChild(Id.ButtonPrevious).component().state = 77 | if (imageIndex > 0) State.Enabled else State.Disabled 78 | image.component().bitmap = TestImage[imageIndex] 79 | }).addComponent(Id.ButtonNext) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | // Example of creating your own widget. The code below uses convenience functions but it could 87 | // be written as: 88 | // val element = Element() 89 | // .addComponent(model) 90 | // .addComponent(object : LayoutComponent { … }) 91 | // .addComponent(object : RenderComponent { … }) 92 | 93 | class ImageModel(var bitmap: Bitmap) 94 | 95 | fun Element.Image(model: ImageModel) = ChildElement { 96 | addComponent(model) 97 | 98 | val paint = Paint().apply { 99 | isFilterBitmap = true 100 | } 101 | 102 | Layout { _, element, size -> 103 | val bitmap = element.component().bitmap 104 | var width = bitmap.width.toFloat() 105 | var height = bitmap.height.toFloat() 106 | 107 | val scale = if (width * size.height < size.width * height) { 108 | size.height / height 109 | } else { 110 | size.width / width 111 | } 112 | 113 | width *= scale 114 | height *= scale 115 | 116 | SizeF(width, height) 117 | } 118 | 119 | Render { _, element, renderer -> 120 | val bitmap = element.component().bitmap 121 | val bounds = element.component().bounds 122 | renderer.drawBitmap( 123 | bitmap, 124 | Rect(0, 0, bitmap.width, bitmap.height), 125 | Rect(0, 0, bounds.width(), bounds.height()), 126 | paint 127 | ) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /apex-demo/src/main/java/dev/romainguy/apex/TestData.kt: -------------------------------------------------------------------------------- 1 | package dev.romainguy.apex 2 | 3 | import android.content.Context 4 | import android.graphics.BitmapFactory 5 | import androidx.core.graphics.createBitmap 6 | 7 | val TestImage = Array(3) { createBitmap(1, 1) } 8 | 9 | fun loadTestImages(context: Context) { 10 | TestImage[0] = BitmapFactory.decodeResource(context.resources, R.drawable.tokyo) 11 | TestImage[1] = BitmapFactory.decodeResource(context.resources, R.drawable.mountains) 12 | TestImage[2] = BitmapFactory.decodeResource(context.resources, R.drawable.car) 13 | } 14 | -------------------------------------------------------------------------------- /apex-demo/src/main/res/drawable-nodpi/car.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/drawable-nodpi/car.jpg -------------------------------------------------------------------------------- /apex-demo/src/main/res/drawable-nodpi/mountains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/drawable-nodpi/mountains.jpg -------------------------------------------------------------------------------- /apex-demo/src/main/res/drawable-nodpi/tokyo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/drawable-nodpi/tokyo.jpg -------------------------------------------------------------------------------- /apex-demo/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /apex-demo/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 | -------------------------------------------------------------------------------- /apex-demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apex-demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apex-demo/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /apex-demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /apex-demo/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /apex-demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /apex-demo/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /apex-demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /apex-demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /apex-demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /apex-demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /apex-demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex-demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /apex-demo/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /apex-demo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /apex-demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Apex 3 | -------------------------------------------------------------------------------- /apex-demo/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /apex/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /apex/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | minSdk 31 11 | targetSdk 31 12 | versionCode 1 13 | versionName "1.0" 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 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_1_8 25 | targetCompatibility JavaVersion.VERSION_1_8 26 | } 27 | kotlinOptions { 28 | jvmTarget = '1.8' 29 | freeCompilerArgs += '-Xcontext-receivers' 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation 'androidx.core:core-ktx:1.7.0' 35 | } 36 | -------------------------------------------------------------------------------- /apex/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/apex/consumer-rules.pro -------------------------------------------------------------------------------- /apex/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 -------------------------------------------------------------------------------- /apex/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /apex/src/main/java/dev/romainguy/apex/Button.kt: -------------------------------------------------------------------------------- 1 | package dev.romainguy.apex 2 | 3 | import android.graphics.Paint 4 | import android.graphics.RectF 5 | import android.util.SizeF 6 | import android.view.MotionEvent 7 | 8 | enum class State { 9 | Enabled, 10 | Disabled 11 | } 12 | 13 | class ButtonModel( 14 | var label: String, 15 | var state: State = State.Enabled, 16 | var onClick: (element: Element) -> Unit = { } 17 | ) { 18 | val isEnabled get() = state == State.Enabled 19 | } 20 | 21 | fun Element.Button(model: ButtonModel) = ChildElement { 22 | addComponent(model) 23 | 24 | data class InternalState( 25 | var isPressed: Boolean = false, 26 | var textWidth: Float = 0.0f, 27 | var textHeight: Float = 0.0f 28 | ) 29 | addComponent(InternalState()) 30 | 31 | Padding(RectF(8.0f, 4.0f, 8.0f, 4.0f)) 32 | 33 | val paint = Paint().apply { 34 | isAntiAlias = true 35 | } 36 | 37 | Render { providers, element, renderer -> 38 | with (providers.get()) { 39 | val theme = providers.get() 40 | val bounds = element.component().bounds 41 | val radius = theme.cornerRadius.toPx() 42 | val internalState = element.component() 43 | 44 | if (theme.style != Paint.Style.STROKE) { 45 | paint.style = Paint.Style.FILL 46 | val color = if (model.isEnabled) theme.contentBackground else theme.contentDisabled 47 | paint.color = 48 | (if (!internalState.isPressed) color else color.complementary()).toArgb() 49 | } 50 | 51 | if (theme.style != Paint.Style.FILL) { 52 | paint.strokeWidth = theme.strokeWidth.toPx() 53 | paint.style = Paint.Style.STROKE 54 | val color = if (model.isEnabled) theme.border else theme.disabled 55 | paint.color = 56 | (if (!internalState.isPressed) color else color.complementary()).toArgb() 57 | 58 | } 59 | 60 | renderer.drawRoundRect( 61 | Rect(0f, 0f, bounds.width(), bounds.height()), 62 | Point(radius, radius), 63 | paint 64 | ) 65 | 66 | 67 | val color = if (model.isEnabled) theme.text else theme.disabled 68 | paint.color = (if (!internalState.isPressed) color else color.complementary()).toArgb() 69 | paint.style = Paint.Style.FILL 70 | paint.strokeWidth = 0.0f 71 | 72 | val x = (bounds.width() - internalState.textWidth) * 0.5f 73 | val y = (bounds.height() - internalState.textHeight) * 0.5f - paint.ascent() 74 | renderer.move(x, y) 75 | renderer.drawText(model.label, paint) 76 | renderer.move(-x, -y) 77 | } 78 | } 79 | 80 | Layout { providers, element, _ -> 81 | with (providers.get()) { 82 | val theme = providers.get() 83 | val padding = element.component().padding.toPx() 84 | val internalState = element.component() 85 | 86 | paint.typeface = theme.typeface 87 | paint.textSize = theme.fontSize.toPx() 88 | internalState.textWidth = paint.measureText(model.label) 89 | val fontMetrics = paint.fontMetrics 90 | internalState.textHeight = -fontMetrics.top + fontMetrics.bottom 91 | 92 | val strokeWidth = theme.strokeWidth.toPx() 93 | SizeF( 94 | internalState.textWidth + padding.left + padding.right + 2.0f * strokeWidth, 95 | internalState.textHeight + padding.top + padding.bottom 96 | ) 97 | } 98 | } 99 | 100 | MotionInput { _, element, event -> 101 | if (element.component().isEnabled) { 102 | val internalState = element.component() 103 | when (event.action) { 104 | MotionEvent.ACTION_DOWN -> { 105 | internalState.isPressed = true 106 | true 107 | } 108 | MotionEvent.ACTION_UP -> { 109 | if (internalState.isPressed) { 110 | internalState.isPressed = false 111 | element.component().onClick(element) 112 | } 113 | true 114 | } 115 | MotionEvent.ACTION_CANCEL -> { 116 | internalState.isPressed = false 117 | true 118 | } 119 | else -> false 120 | } 121 | } else { 122 | false 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /apex/src/main/java/dev/romainguy/apex/Colors.kt: -------------------------------------------------------------------------------- 1 | package dev.romainguy.apex 2 | 3 | import android.graphics.Color 4 | import androidx.core.graphics.* 5 | import kotlin.math.min 6 | 7 | fun Color.complementary(): Color { 8 | val (_, r, g, b) = toArgb() 9 | val out = FloatArray(3) 10 | ColorUtils.RGBToHSL(r, g, b, out) 11 | out[0] = (out[0] + 180.0f) % 360.0f 12 | return Color.valueOf(ColorUtils.HSLToColor(out)) 13 | } 14 | 15 | fun Color.desaturated(): Color { 16 | val (_, r, g, b) = toArgb() 17 | val out = FloatArray(3) 18 | ColorUtils.RGBToHSL(r, g, b, out) 19 | out[1] = min(out[1] * 0.1f, 1.0f) 20 | return Color.valueOf(ColorUtils.HSLToColor(out)) 21 | } 22 | -------------------------------------------------------------------------------- /apex/src/main/java/dev/romainguy/apex/Components.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("NOTHING_TO_INLINE") 2 | 3 | package dev.romainguy.apex 4 | 5 | import android.graphics.RectF 6 | import android.util.SizeF 7 | import android.view.MotionEvent 8 | 9 | val EmptySize = SizeF(0.0f, 0.0f) 10 | val UnboundedSize = SizeF(Float.MAX_VALUE, Float.MAX_VALUE) 11 | 12 | interface RenderComponent { 13 | fun render(providers: Providers, element: Element, renderer: Renderer) 14 | } 15 | 16 | fun Element.Render(render: (providers: Providers, element: Element, renderer: Renderer) -> Unit) { 17 | addComponent(object : RenderComponent { 18 | override fun render(providers: Providers, element: Element, renderer: Renderer) { 19 | render(providers, element, renderer) 20 | } 21 | }) 22 | } 23 | 24 | abstract class LayoutComponent { 25 | var bounds = RectF() 26 | 27 | abstract fun layout(providers: Providers, element: Element, size: SizeF): SizeF 28 | 29 | fun minSize(providers: Providers, element: Element) = EmptySize 30 | fun maxSize(providers: Providers, element: Element) = UnboundedSize 31 | } 32 | 33 | fun Element.Layout(layout: (providers: Providers, element: Element, size: SizeF) -> SizeF) { 34 | addComponent(object : LayoutComponent() { 35 | override fun layout(providers: Providers, element: Element, size: SizeF): SizeF { 36 | return layout(providers, element, size) 37 | } 38 | }) 39 | } 40 | 41 | interface MotionInputComponent { 42 | fun motionInput(providers: Providers, element: Element, event: MotionEvent): Boolean 43 | } 44 | 45 | fun Element.MotionInput( 46 | action: (providers: Providers, element: Element, event: MotionEvent) -> Boolean 47 | ) { 48 | addComponent(object : MotionInputComponent { 49 | override fun motionInput(providers: Providers, element: Element, event: MotionEvent): Boolean { 50 | return action(providers, element, event) 51 | } 52 | }) 53 | } 54 | 55 | interface ProviderComponent { 56 | fun provide(providers: Providers, element: Element) 57 | } 58 | 59 | inline fun Element.Provider(localProvider: T) { 60 | addComponent(object : ProviderComponent { 61 | override fun provide(providers: Providers, element: Element) { 62 | providers.set(localProvider) 63 | } 64 | }) 65 | } 66 | 67 | class PaddingComponent(val padding: RectF) { 68 | constructor(padding: Float) : this(RectF(padding, padding, padding, padding)) 69 | } 70 | 71 | inline fun Element.Padding(padding: RectF) { 72 | addComponent(PaddingComponent(padding)) 73 | } 74 | 75 | inline fun Element.Padding(padding: Float) { 76 | addComponent(PaddingComponent(padding)) 77 | } 78 | 79 | enum class VerticalAlignment { 80 | Start, 81 | Center, 82 | End 83 | } 84 | 85 | enum class HorizontalAlignment { 86 | Start, 87 | Center, 88 | End 89 | } 90 | 91 | inline fun Element.Alignment(alignment: VerticalAlignment) { 92 | addComponent(alignment) 93 | } 94 | 95 | inline fun Element.Alignment(alignment: HorizontalAlignment) { 96 | addComponent(alignment) 97 | } 98 | -------------------------------------------------------------------------------- /apex/src/main/java/dev/romainguy/apex/Element.kt: -------------------------------------------------------------------------------- 1 | package dev.romainguy.apex 2 | 3 | import kotlin.reflect.KClass 4 | 5 | open class Element(content: Element.() -> Unit = { }) { 6 | var parent = this 7 | private set 8 | 9 | private val children = mutableListOf() 10 | private val components = mutableListOf() 11 | 12 | init { 13 | content() 14 | } 15 | 16 | fun addChild(child: Element): Element { 17 | child.parent = this 18 | children.add(child) 19 | return this 20 | } 21 | 22 | fun forEachChild(action: (Element) -> Unit) { 23 | for (child in children) { 24 | action(child) 25 | } 26 | } 27 | 28 | fun findChild(predicate: (Element) -> Boolean): Element? { 29 | for (child in children) { 30 | if (predicate(child)) return child 31 | val candidate = child.findChild(predicate) 32 | if (candidate != null) return candidate 33 | } 34 | return null 35 | } 36 | 37 | fun requireChild(predicate: (Element) -> Boolean): Element { 38 | for (child in children) { 39 | if (predicate(child)) return child 40 | val candidate = child.findChild(predicate) 41 | if (candidate != null) return candidate 42 | } 43 | throw IllegalArgumentException("Cannot find child matching predicate") 44 | } 45 | 46 | inline fun findChild(componentValue: T) = 47 | findChild { it.componentOrNull(T::class) == componentValue } 48 | 49 | inline fun requireChild(componentValue: T) = 50 | requireChild { it.componentOrNull(T::class) == componentValue } 51 | 52 | fun addComponent(component: Any): Element { 53 | components.add(component) 54 | return this 55 | } 56 | 57 | @Suppress("UNCHECKED_CAST") 58 | fun component(type: KClass) = components.first { type.isInstance(it) } as T 59 | 60 | inline fun component() = component(T::class) 61 | 62 | @Suppress("UNCHECKED_CAST") 63 | fun componentOrNull(type: KClass) = 64 | components.firstOrNull { type.isInstance(it) } as T? 65 | 66 | inline fun componentOrNull() = componentOrNull(T::class) 67 | 68 | @Suppress("UNCHECKED_CAST") 69 | fun applyComponent(type: KClass, action: T.()-> Unit) { 70 | for (component in components) { 71 | if (type.isInstance(component)) (component as T).action() 72 | } 73 | } 74 | 75 | inline fun applyComponent(noinline action: T.() -> Unit) { 76 | applyComponent(T::class, action) 77 | } 78 | 79 | fun ChildElement(content: Element.() -> Unit): Element { 80 | val child = Element() 81 | child.content() 82 | addChild(child) 83 | return child 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /apex/src/main/java/dev/romainguy/apex/Integration.kt: -------------------------------------------------------------------------------- 1 | package dev.romainguy.apex 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.graphics.PointF 7 | import android.graphics.RectF 8 | import android.util.SizeF 9 | import android.view.* 10 | import androidx.core.graphics.contains 11 | import dev.romainguy.apex.android.AndroidRenderer 12 | 13 | private fun layout(providers: Providers, element: Element, size: SizeF) { 14 | element.forEachChild { child -> 15 | val localProviders = providers.copyOf() 16 | 17 | child.applyComponent { 18 | provide(localProviders, child) 19 | } 20 | 21 | child.applyComponent { 22 | val minSize = minSize(localProviders, child) 23 | val maxSize = maxSize(localProviders, child) 24 | val childSize = min(max(minSize, layout(localProviders, child, size)), maxSize) 25 | val halign = child.componentOrNull() ?: HorizontalAlignment.Start 26 | val valign = child.componentOrNull() ?: VerticalAlignment.Start 27 | 28 | val x = when (halign) { 29 | HorizontalAlignment.Start -> 0.0f 30 | HorizontalAlignment.Center -> (size.width - childSize.width) * 0.5f 31 | HorizontalAlignment.End -> size.width - childSize.width 32 | } 33 | val y = when (valign) { 34 | VerticalAlignment.Start -> 0.0f 35 | VerticalAlignment.Center -> (size.height - childSize.height) * 0.5f 36 | VerticalAlignment.End -> size.height - childSize.height 37 | } 38 | 39 | bounds = RectF(x, y, x + childSize.width, y + childSize.height) 40 | } 41 | } 42 | } 43 | 44 | private fun draw(providers: Providers, element: Element, renderer: Renderer) { 45 | element.forEachChild { child -> 46 | val localProviders = providers.copyOf() 47 | 48 | child.applyComponent { 49 | provide(localProviders, child) 50 | } 51 | 52 | val bounds = child.componentOrNull()?.bounds ?: EmptyBounds 53 | renderer.move(bounds.left, bounds.top) 54 | child.applyComponent { 55 | render(localProviders, child, renderer) 56 | } 57 | draw(localProviders, child, renderer) 58 | renderer.move(-bounds.left, -bounds.top) 59 | } 60 | } 61 | 62 | private fun motion( 63 | providers: Providers, 64 | element: Element, 65 | event: MotionEvent, 66 | x: Float, 67 | y: Float 68 | ): Boolean { 69 | var done = false 70 | 71 | element.forEachChild { child -> 72 | val localProviders = providers.copyOf() 73 | 74 | child.applyComponent { 75 | provide(localProviders, child) 76 | } 77 | 78 | val sourceBounds = child.componentOrNull()?.bounds ?: EmptyBounds 79 | val bounds = RectF(sourceBounds).apply { offset(x, y) } 80 | 81 | if (PointF(event.x, event.y) in bounds) { 82 | child.applyComponent { 83 | if (motionInput(providers, child, event)) { 84 | done = true 85 | } 86 | } 87 | if (!done) { 88 | done = motion(providers, child, event, x + sourceBounds.left, y + sourceBounds.top) 89 | } 90 | } 91 | 92 | if (done) { 93 | return@forEachChild 94 | } 95 | } 96 | 97 | return done 98 | } 99 | 100 | private class RootElement(context: Context) : Element() { 101 | private val providers = Providers() 102 | 103 | var size: SizeF = SizeF(0.0f, 0.0f) 104 | 105 | private var surface: SurfaceHolder? = null 106 | 107 | private val frame = object : Choreographer.FrameCallback { 108 | override fun doFrame(frameTimeNanos: Long) { 109 | if (surface == null) return 110 | 111 | val rootProviders = providers.copyOf() 112 | applyComponent { 113 | provide(rootProviders, this@RootElement) 114 | } 115 | 116 | layout(rootProviders, this@RootElement, size) 117 | 118 | with (surface?.lockHardwareCanvas()!!) { 119 | drawColor(rootProviders.get().background.toArgb()) 120 | val renderer = AndroidRenderer(this) 121 | draw(rootProviders, this@RootElement, renderer) 122 | surface?.unlockCanvasAndPost(this) 123 | } 124 | 125 | Choreographer.getInstance().postFrameCallback(this) 126 | } 127 | } 128 | 129 | init { 130 | providers.set(DisplayProvider(context.resources.displayMetrics)) 131 | providers.set(DensityProvider(context.resources.displayMetrics.density)) 132 | providers.set(ResourcesProvider(context.resources)) 133 | providers.set(ThemeProvider()) 134 | } 135 | 136 | fun start(holder: SurfaceHolder) { 137 | surface = holder 138 | Choreographer.getInstance().postFrameCallback(frame) 139 | } 140 | 141 | fun stop() { 142 | Choreographer.getInstance().removeFrameCallback(frame) 143 | surface = null 144 | } 145 | 146 | fun onMotion(event: MotionEvent): Boolean { 147 | val rootProviders = providers.copyOf() 148 | applyComponent { 149 | provide(rootProviders, this@RootElement) 150 | } 151 | 152 | return motion(rootProviders, this, event, 0.0f, 0.0f) 153 | } 154 | } 155 | 156 | @SuppressLint("ClickableViewAccessibility") 157 | fun Activity.setContent(content: Element.() -> Unit) { 158 | val root = RootElement(this) 159 | root.content() 160 | 161 | val surface = SurfaceView(this) 162 | setContentView(surface) 163 | 164 | surface.setOnTouchListener { _, event -> 165 | root.onMotion(event!!) 166 | true 167 | } 168 | 169 | surface.holder.addCallback(object : SurfaceHolder.Callback { 170 | override fun surfaceCreated(holder: SurfaceHolder) { 171 | root.start(holder) 172 | } 173 | 174 | override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { 175 | root.size = SizeF(width.toFloat(), height.toFloat()) 176 | } 177 | 178 | override fun surfaceDestroyed(holder: SurfaceHolder) { 179 | root.stop() 180 | } 181 | }) 182 | } 183 | -------------------------------------------------------------------------------- /apex/src/main/java/dev/romainguy/apex/Layouts.kt: -------------------------------------------------------------------------------- 1 | package dev.romainguy.apex 2 | 3 | import android.graphics.RectF 4 | import android.util.SizeF 5 | import kotlin.math.max 6 | import kotlin.math.min 7 | 8 | val EmptyBounds = RectF() 9 | 10 | fun min(a: SizeF, b: SizeF) = SizeF( 11 | min(a.width, b.width), 12 | min(a.height, b.height) 13 | ) 14 | 15 | fun max(a: SizeF, b: SizeF) = SizeF( 16 | max(a.width, b.width), 17 | max(a.height, b.height) 18 | ) 19 | 20 | fun Element.Row(elementPadding: Float = 8.0f, content: Element.() -> Unit = { }) = ChildElement { 21 | Layout { providers, element, size -> 22 | val density = providers.get() 23 | val elementOffset = density.toPx(elementPadding) 24 | 25 | val padding = density.toPx( 26 | element.componentOrNull()?.padding ?: EmptyBounds 27 | ) 28 | 29 | var x = padding.left 30 | val y = padding.top 31 | var height = 0.0f 32 | 33 | element.forEachChild { child -> 34 | child.applyComponent { 35 | val minSize = minSize(providers, child) 36 | val maxSize = maxSize(providers, child) 37 | 38 | val sizeConstraint = SizeF( 39 | size.width - x - padding.right, 40 | size.height - y - padding.bottom, 41 | ) 42 | var childSize = layout(providers, child, sizeConstraint) 43 | childSize = min(max(minSize, childSize), maxSize) 44 | 45 | bounds = RectF(x, y, x + childSize.width, y + childSize.height) 46 | x += bounds.width() + elementOffset 47 | 48 | height = max(height, bounds.height()) 49 | } 50 | } 51 | 52 | SizeF( 53 | max(0.0f, x - elementOffset) + padding.right, 54 | height + padding.top + padding.bottom 55 | ) 56 | } 57 | 58 | content() 59 | } 60 | 61 | fun Element.Column(elementPadding: Float = 8.0f, content: Element.() -> Unit = { }) = ChildElement { 62 | Layout { providers, element, size -> 63 | val density = providers.get() 64 | val elementOffset = density.toPx(elementPadding) 65 | 66 | val padding = density.toPx( 67 | element.componentOrNull()?.padding ?: EmptyBounds 68 | ) 69 | 70 | val x = padding.left 71 | var y = padding.top 72 | var width = 0.0f 73 | 74 | element.forEachChild { child -> 75 | child.applyComponent { 76 | val minSize = minSize(providers, child) 77 | val maxSize = maxSize(providers, child) 78 | 79 | val sizeConstraint = SizeF( 80 | size.width - x - padding.right, 81 | size.height - y - padding.bottom, 82 | ) 83 | var childSize = layout(providers, child, sizeConstraint) 84 | childSize = min(max(minSize, childSize), maxSize) 85 | 86 | bounds = RectF(x, y, x + childSize.width, y + childSize.height) 87 | y += bounds.height() + elementOffset 88 | 89 | width = max(width, bounds.width()) 90 | } 91 | } 92 | 93 | SizeF( 94 | width + padding.left + padding.right, 95 | max(0.0f, y - elementOffset) + padding.bottom 96 | ) 97 | } 98 | 99 | content() 100 | } 101 | -------------------------------------------------------------------------------- /apex/src/main/java/dev/romainguy/apex/Providers.kt: -------------------------------------------------------------------------------- 1 | package dev.romainguy.apex 2 | 3 | import android.content.res.Resources 4 | import android.graphics.Color 5 | import android.graphics.Paint 6 | import android.graphics.RectF 7 | import android.graphics.Typeface 8 | import android.util.DisplayMetrics 9 | import kotlin.collections.MutableMap 10 | import kotlin.collections.mutableMapOf 11 | import kotlin.collections.set 12 | import kotlin.math.floor 13 | import kotlin.reflect.KClass 14 | 15 | class Providers { 16 | private val providers: MutableMap, Any> = mutableMapOf() 17 | 18 | inline fun get(): T { 19 | return this[T::class] 20 | } 21 | 22 | @Suppress("UNCHECKED_CAST") 23 | operator fun get(type: KClass) = providers[type] as T 24 | 25 | inline fun set(provider: T) { 26 | this[T::class] = provider 27 | } 28 | 29 | operator fun set(type: KClass, provider: T) { 30 | providers[type] = provider 31 | } 32 | 33 | fun copyOf(): Providers = Providers().apply { providers.putAll(this@Providers.providers) } 34 | } 35 | 36 | class DensityProvider(val density: Float) { 37 | fun toPx(v: Float) = floor((v * density) + 0.5f) 38 | fun toPx(r: RectF) = RectF(toPx(r.left), toPx(r.top), toPx(r.right), toPx(r.bottom)) 39 | } 40 | 41 | context(DensityProvider) 42 | @Suppress("NOTHING_TO_INLINE") 43 | inline fun Float.toPx() = toPx(this) 44 | 45 | context(DensityProvider) 46 | @Suppress("NOTHING_TO_INLINE") 47 | inline fun RectF.toPx() = toPx(this) 48 | 49 | class DisplayProvider(val display: DisplayMetrics) 50 | 51 | class ResourcesProvider(val resources: Resources) 52 | 53 | class ThemeProvider( 54 | val border: Color = Color.valueOf(0.25f, 0.53f, 0.94f), 55 | val background: Color = Color.valueOf(1.0f, 1.0f, 1.0f), 56 | val contentBackground: Color = Color.valueOf(0.75f, 0.85f, 0.95f), 57 | val disabled: Color = border.desaturated(), 58 | val contentDisabled: Color = Color.valueOf(0.92f, 0.92f, 0.92f), 59 | val text: Color = Color.valueOf(0.25f, 0.53f, 0.94f, 0.8f), 60 | val fontSize: Float = 18.0f, 61 | val typeface: Typeface = Typeface.create("sans-serif-condensed", Typeface.NORMAL), 62 | val style: Paint.Style = Paint.Style.STROKE, 63 | val cornerRadius: Float = 6.0f, 64 | val strokeWidth: Float = 1.5f 65 | ) 66 | -------------------------------------------------------------------------------- /apex/src/main/java/dev/romainguy/apex/Renderer.kt: -------------------------------------------------------------------------------- 1 | package dev.romainguy.apex 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Paint 5 | 6 | interface Renderer { 7 | // TODO: abstract Paint into platform-agnostic rendering style 8 | var position: Point 9 | 10 | fun move(x: Float, y: Float) { 11 | position = Point(position.x + x, position.y + y) 12 | } 13 | 14 | fun drawText(text: String, style: Paint) 15 | 16 | // TODO: abstract Bitmap into platform-agnostic image object 17 | fun drawBitmap(bitmap: Bitmap, style: Paint) 18 | fun drawBitmap(bitmap: Bitmap, src: Rect, dst: Rect, style: Paint) 19 | fun drawRect(rect: Rect, style: Paint) 20 | fun drawRoundRect(rect: Rect, radius: Point, style: Paint) 21 | } 22 | 23 | data class Point(val x: Float, val y: Float) 24 | 25 | data class Rect(val left: Float, val top: Float, val right: Float, val bottom: Float) 26 | 27 | fun Rect(left: Int, top: Int, right: Int, bottom: Int) = 28 | Rect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat()) 29 | 30 | fun Rect(left: Int, top: Int, right: Float, bottom: Float) = 31 | Rect(left.toFloat(), top.toFloat(), right, bottom) 32 | -------------------------------------------------------------------------------- /apex/src/main/java/dev/romainguy/apex/android/AndroidRenderer.kt: -------------------------------------------------------------------------------- 1 | package dev.romainguy.apex.android 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import androidx.core.graphics.withTranslation 7 | import dev.romainguy.apex.Point 8 | import dev.romainguy.apex.Rect 9 | import dev.romainguy.apex.Renderer 10 | 11 | internal fun Rect.toRect(): android.graphics.Rect { 12 | return android.graphics.Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt()) 13 | } 14 | 15 | internal class AndroidRenderer(val canvas: Canvas) : Renderer { 16 | override var position: Point = Point(0f, 0f) 17 | 18 | override fun drawText(text: String, style: Paint) { 19 | canvas.drawText(text, position.x, position.y, style) 20 | } 21 | 22 | override fun drawBitmap(bitmap: Bitmap, style: Paint) { 23 | canvas.drawBitmap(bitmap, position.x, position.y, style) 24 | } 25 | 26 | override fun drawBitmap(bitmap: Bitmap, src: Rect, dst: Rect, style: Paint) { 27 | canvas.withTranslation(position.x, position.y) { 28 | drawBitmap(bitmap, src.toRect(), dst.toRect(), style) 29 | } 30 | } 31 | 32 | override fun drawRect(rect: Rect, style: Paint) { 33 | canvas.withTranslation(position.x, position.y) { 34 | drawRect(rect.left, rect.top, rect.right, rect.bottom, style) 35 | } 36 | } 37 | 38 | override fun drawRoundRect(rect: Rect, radius: Point, style: Paint) { 39 | canvas.withTranslation(position.x, position.y) { 40 | drawRoundRect(rect.left, rect.top, rect.right, rect.bottom, radius.x, radius.y, style) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /assets/apex_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/assets/apex_demo.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:7.1.3' 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21" 10 | 11 | // NOTE: Do not place your application dependencies here; they belong 12 | // in the individual module build.gradle files 13 | } 14 | } 15 | 16 | task clean(type: Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/experiment-apex/b32046be7a9f3ea773e46352485737731c9a08e0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Nov 22 09:38:20 PST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | jcenter() // Warning: this repository is going to shut down soon 7 | } 8 | } 9 | rootProject.name = "Apex" 10 | include ':apex-demo' 11 | include ':apex' 12 | --------------------------------------------------------------------------------