├── .github └── workflows │ ├── android.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── gojek │ │ └── draftsman │ │ └── demo │ │ └── LaunchActivity.kt │ └── res │ ├── layout │ └── launch_activity_layout.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle.kts ├── draftsman-no-op ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── gojek │ └── draftsman │ └── Draftsman.kt ├── draftsman ├── art │ ├── draftsman-base.png │ ├── draftsman-drawer.png │ ├── draftsman-enabled.png │ ├── draftsman-grid.png │ ├── draftsman-margin.png │ ├── draftsman-nested-views.png │ ├── draftsman-overlay.png │ ├── draftsman-padding.png │ ├── draftsman-text.png │ ├── draftsman-view.png │ └── logo.svg ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── kotlin │ └── com │ │ └── gojek │ │ └── draftsman │ │ ├── Draftsman.kt │ │ └── internal │ │ ├── Config.kt │ │ ├── DraftsmanLayout.kt │ │ ├── constants │ │ └── VisualizerIds.kt │ │ ├── drawer │ │ ├── DefaultAnimationListener.kt │ │ ├── DrawerAdapter.kt │ │ ├── DrawerInteractions.kt │ │ ├── DrawerOptionHandler.kt │ │ ├── DrawerPathGenerator.kt │ │ ├── ModernDrawerLayout.kt │ │ ├── Orientation.kt │ │ ├── ResourcesUtils.kt │ │ └── State.kt │ │ ├── extensions │ │ ├── SeekBar.kt │ │ ├── View.kt │ │ └── ViewGroup.kt │ │ ├── fragment │ │ ├── FileChooserFragment.kt │ │ └── RequestPermissionFragment.kt │ │ ├── model │ │ ├── DrawerItem.kt │ │ └── LayeredView.kt │ │ ├── utils │ │ ├── ColorUtils.kt │ │ ├── DimensionConverter.kt │ │ ├── DimensionUtils.kt │ │ ├── DrawerLayoutContentList.kt │ │ ├── LayeredViewBuilder.kt │ │ ├── RectUtils.kt │ │ └── TypeAlias.kt │ │ ├── visualizers │ │ ├── GridVisualizer.kt │ │ ├── InfoVisualizer.kt │ │ ├── LayeringVisualizer.kt │ │ ├── MarginVisualizer.kt │ │ ├── OverlayVisualizer.kt │ │ ├── PaddingVisualizer.kt │ │ └── TextVisualizer.kt │ │ └── widgets │ │ ├── ColorView.kt │ │ ├── HShapeView.kt │ │ ├── InspectionRootItemView.kt │ │ └── NestedViewBoundView.kt │ └── res │ ├── drawable │ ├── draftsman_drawer_item_bg.xml │ ├── draftsman_ic_change_root.xml │ ├── draftsman_ic_close.xml │ ├── draftsman_ic_drawer_close.xml │ ├── draftsman_ic_drawer_open.xml │ ├── draftsman_ic_grid.xml │ ├── draftsman_ic_margin.xml │ ├── draftsman_ic_padding.xml │ ├── draftsman_ic_place_overlay.xml │ ├── draftsman_info_view_bg.xml │ ├── draftsman_rounded_btn_bg.xml │ └── draftsman_vertical_divider.xml │ ├── layout │ ├── draftsman_drawer_item_header.xml │ ├── draftsman_drawer_item_range.xml │ ├── draftsman_drawer_item_selection.xml │ ├── draftsman_drawer_item_toggle.xml │ ├── draftsman_drawer_layout.xml │ ├── draftsman_info_layout.xml │ ├── overlay_opacity_control_layout.xml │ └── view_layering_layout.xml │ └── values │ ├── attrs.xml │ └── styles.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scripts ├── publish-module.gradle └── publish-root.gradle └── settings.gradle.kts /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: set up JDK 11 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '11' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew assemble 27 | 28 | - name: Archive aar 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: draftsman 32 | path: draftsman/build/outputs/aar/**.aar 33 | 34 | - name: Archive APK 35 | uses: actions/upload-artifact@v2 36 | with: 37 | name: app 38 | path: app/build/outputs/apk/**/**.apk 39 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - '[0-9]+.[0-9]+.[0-9]+' # Push events to any matching semantic tag. For example, 1.10.1 or 2.0.0. 8 | # For more details, see https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet 9 | # and https://docs.npmjs.com/about-semantic-versioning 10 | 11 | jobs: 12 | publish: 13 | name: Release build and publish 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: set up JDK 11 18 | uses: actions/setup-java@v2 19 | with: 20 | java-version: '11' 21 | distribution: 'temurin' 22 | cache: gradle 23 | - name: Release build 24 | run: ./gradlew :draftsman:assembleRelease :draftsman-no-op:assembleRelease 25 | - name: Publish to MavenCentral 26 | run: ./gradlew draftsman:publishReleasePublicationToSonatypeRepository --max-workers 1 closeAndReleaseSonatypeStagingRepository 27 | env: 28 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 29 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 30 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 31 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 32 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 33 | SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | # Built application files 3 | *.apk 4 | *.ap_ 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | build-time/ 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Log Files 25 | *.log 26 | 27 | # Android Studio captures folder 28 | captures/ 29 | 30 | # IntelliJ 31 | *.iml 32 | .idea/ 33 | 34 | # External native build folder generated in Android Studio 2.2 and later 35 | .externalNativeBuild 36 | 37 | # jacoco 38 | *.exec 39 | -------------------------------------------------------------------------------- /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 2022 asphalt 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 |
2 | 3 |
4 |

Draftsman

5 |
6 | 7 | Draftsman is an on-device layout inspector for Android apps. It allows you to view various properties of rendered Android Views such as width, height, class name, paddings, margins etc. 8 | 9 | Draftsman aims to combine capabilities of different Android tools such as `Show Layout Bounds` and Android Studio's layout inspector. 10 | 11 | The primary objective of Draftsman is to provide information around rendered views directly from your app which is easily accessible to devs, designers, PMs or any one who has installed the app. No need to use any external tool or rely on debug variant of your app. 12 | 13 | ## Features 14 | 15 | * Width & Height Info for any view 16 | 17 | * Class name for any view 18 | 19 | * Margin and Padding visualization 20 | 21 | * Dimension values in both Pixel(Px) and dp/sp 22 | 23 | * TextView color and size information 24 | 25 | * Overlay a grid 26 | 27 | * Overlay a image to compare UI 28 | 29 | Check usage guide at bottom for more details. 30 | 31 | ## Integration 32 | [![Maven Central](https://img.shields.io/maven-central/v/com.gojek.draftsman/draftsman.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.gojek.draftsman%22%20AND%20a:%22draftsman%22) 33 | 34 | Add following code to your build.gradle 35 | 36 | ```groovy 37 | repositories { 38 | mavenCentral() 39 | } 40 | 41 | dependencies { 42 | implementation "com.gojek.draftsman:draftsman:x.y.z" 43 | } 44 | ``` 45 | 46 | To enable Draftsman in an activity 47 | 48 | ```kotlin 49 | Draftsman.install(activity) 50 | ``` 51 | 52 | To exit from Draftsman, there is an exit button in information window. And if you want do it programmatically, you can call 53 | 54 | ```kotlin 55 | Draftsman.uninstall(activity) 56 | ``` 57 | 58 | ## Usage Guide 59 | 60 | This section will help you understand what information Draftsman can capture from a view. 61 | 62 | This is our base view 63 | 64 | ![draftsman-base.png](/draftsman/art/draftsman-base.png) 65 | 66 | **When Draftsman is enabled** 67 | 68 | This is how our view looks like once Draftsman is enabled. Inspectable views are highlighted and a arrow appears on right to open drawer. 69 | 70 | ![draftsman-enabled.png](/draftsman/art/draftsman-enabled.png) 71 | 72 | **Draftsman Setting Drawer** 73 | 74 | You can use this drawer to customise a few settings. 75 | 76 | ![draftsman-drawer.png](/draftsman/art/draftsman-drawer.png) 77 | 78 | **TextView Inspection** 79 | 80 | On tapping of first text "Hi Folks" we can observe this overlay. It provides information on height, width, textsize and textcolor. Dimensions can be observed in dp or px. 81 | 82 | Inspection details overlay can be closed using cross icon and Draftsman can be closed by pressing Exit button. 83 | 84 | ![draftsman-text.png](/draftsman/art/draftsman-text.png) 85 | 86 | **View Inspection** 87 | 88 | Any arbitrary view can be inspected as well allowing us to inspect height, width and class name 89 | 90 | ![draftsman-view.png](/draftsman/art/draftsman-view.png) 91 | 92 | **Margin and Padding** 93 | 94 | Tapping around views highlights margins and paddings if available. Padding is shown with green background and margin with red line. 95 | 96 | ![draftsman-padding.png](/draftsman/art/draftsman-padding.png) 97 | 98 | ![draftsman-margin.png](/draftsman/art/draftsman-margin.png) 99 | 100 | **Nested Views** 101 | 102 | If there are multiple views stacked within same bounds, Draftsman will prompt you to select the view to be inspected 103 | 104 | ![draftsman-nested-views.png](/draftsman/art/draftsman-nested-views.png) 105 | 106 | **Grid Overlay** 107 | 108 | You can also add a size configurable grid overlay on your screen from setting drawer. 109 | 110 | ![draftsman-grid.png](/draftsman/art/draftsman-grid.png) 111 | 112 | **Image Overlay** 113 | 114 | You can also overlay a screenshot on top of existing screen to check for UI differences. A slider on bottom can be used to fade out overlay image. 115 | 116 | Note: Storage read permission should be provided for this to work. 117 | 118 | ![draftsman-overlay.png](/draftsman/art/draftsman-overlay.png) 119 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | plugins { 3 | id("com.android.application") 4 | kotlin("android") 5 | kotlin("android.extensions") 6 | } 7 | 8 | android { 9 | compileSdkVersion(29) 10 | defaultConfig { 11 | minSdkVersion(21) 12 | targetSdkVersion(29) 13 | applicationId = "com.gojek.draftsman.demo" 14 | } 15 | } 16 | 17 | dependencies { 18 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.31") 19 | implementation("androidx.core:core-ktx:1.5.0") 20 | implementation("androidx.appcompat:appcompat:1.1.0") 21 | implementation("com.google.android.material:material:1.1.0") 22 | debugImplementation(project(":draftsman")) 23 | releaseImplementation(project(":draftsman-no-op")) 24 | } 25 | -------------------------------------------------------------------------------- /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.kts. 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 -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/gojek/draftsman/demo/LaunchActivity.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.demo 2 | 3 | import android.os.Bundle 4 | import android.widget.Toast 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.gojek.draftsman.Draftsman 7 | import kotlinx.android.synthetic.main.launch_activity_layout.* 8 | 9 | class LaunchActivity : AppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | 14 | setContentView(R.layout.launch_activity_layout) 15 | 16 | enable.setOnClickListener { 17 | Draftsman.install(this) 18 | Toast.makeText(this, "Added Draftsman overlay", Toast.LENGTH_SHORT).show() 19 | } 20 | 21 | disable.setOnClickListener { 22 | Draftsman.uninstall(this) 23 | Toast.makeText(this, "Removed Draftsman overlay", Toast.LENGTH_SHORT).show() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/launch_activity_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 26 | 27 | 34 | 35 | 39 | 40 | 41 | 42 | 50 | 51 | 52 | 53 | 59 | 60 | 64 | 65 | 70 | 71 | 72 | 73 | 80 | 81 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Draftsman Demo 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | maven(url = "https://plugins.gradle.org/m2/") 5 | mavenCentral() 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath("com.android.tools.build:gradle:4.1.1") 11 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31") 12 | classpath("io.github.gradle-nexus:publish-plugin:1.1.0") 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle.kts files 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | mavenCentral() 22 | google() 23 | jcenter() 24 | } 25 | } 26 | 27 | tasks.register("clean", Delete::class) { 28 | delete(rootProject.buildDir) 29 | } 30 | 31 | apply(plugin = "io.github.gradle-nexus.publish-plugin") 32 | apply(from = "${rootDir}/scripts/publish-root.gradle") 33 | -------------------------------------------------------------------------------- /draftsman-no-op/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("android") 4 | } 5 | 6 | android { 7 | compileSdkVersion(29) 8 | defaultConfig { 9 | minSdkVersion(21) 10 | targetSdkVersion(29) 11 | } 12 | 13 | sourceSets["main"].java.srcDirs( 14 | "src/main/kotlin" 15 | ) 16 | } 17 | 18 | ext { 19 | set("PUBLISH_GROUP_ID", "com.gojek.draftsman") 20 | set("PUBLISH_VERSION", "0.0.3") 21 | set("PUBLISH_ARTIFACT_ID", "draftsman-no-op") 22 | } 23 | 24 | apply(from = "${rootProject.projectDir}/scripts/publish-module.gradle") 25 | -------------------------------------------------------------------------------- /draftsman-no-op/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /draftsman-no-op/src/main/kotlin/com/gojek/draftsman/Draftsman.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman 2 | 3 | import android.app.Activity 4 | 5 | class Draftsman { 6 | 7 | companion object { 8 | 9 | fun install(activity: Activity) {} 10 | 11 | fun uninstall(activity: Activity) {} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /draftsman/art/draftsman-base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/draftsman/art/draftsman-base.png -------------------------------------------------------------------------------- /draftsman/art/draftsman-drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/draftsman/art/draftsman-drawer.png -------------------------------------------------------------------------------- /draftsman/art/draftsman-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/draftsman/art/draftsman-enabled.png -------------------------------------------------------------------------------- /draftsman/art/draftsman-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/draftsman/art/draftsman-grid.png -------------------------------------------------------------------------------- /draftsman/art/draftsman-margin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/draftsman/art/draftsman-margin.png -------------------------------------------------------------------------------- /draftsman/art/draftsman-nested-views.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/draftsman/art/draftsman-nested-views.png -------------------------------------------------------------------------------- /draftsman/art/draftsman-overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/draftsman/art/draftsman-overlay.png -------------------------------------------------------------------------------- /draftsman/art/draftsman-padding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/draftsman/art/draftsman-padding.png -------------------------------------------------------------------------------- /draftsman/art/draftsman-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/draftsman/art/draftsman-text.png -------------------------------------------------------------------------------- /draftsman/art/draftsman-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/draftsman/art/draftsman-view.png -------------------------------------------------------------------------------- /draftsman/art/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /draftsman/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("android") 4 | } 5 | 6 | android { 7 | compileSdkVersion(29) 8 | defaultConfig { 9 | minSdkVersion(21) 10 | targetSdkVersion(29) 11 | } 12 | 13 | sourceSets["main"].java.srcDirs( 14 | "src/main/kotlin" 15 | ) 16 | } 17 | dependencies { 18 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.31") 19 | implementation("androidx.appcompat:appcompat:1.1.0") 20 | implementation("androidx.recyclerview:recyclerview:1.1.0") 21 | } 22 | 23 | ext { 24 | set("PUBLISH_GROUP_ID", "com.gojek.draftsman") 25 | set("PUBLISH_VERSION", "0.0.3") 26 | set("PUBLISH_ARTIFACT_ID", "draftsman") 27 | } 28 | 29 | apply(from = "${rootProject.projectDir}/scripts/publish-module.gradle") 30 | -------------------------------------------------------------------------------- /draftsman/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/Draftsman.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman 2 | 3 | import android.app.Activity 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import com.gojek.draftsman.internal.DraftsmanLayout 7 | 8 | class Draftsman { 9 | 10 | private fun installVision( 11 | activity: Activity, 12 | view: View 13 | ) { 14 | 15 | val draftsman = DraftsmanLayout(activity) 16 | val location = IntArray(2) 17 | activity.findViewById(android.R.id.content).apply { 18 | addView(draftsman) 19 | getLocationOnScreen(location) 20 | draftsman.init(location[1], view) { uninstall(activity) } 21 | } 22 | } 23 | 24 | companion object { 25 | 26 | fun install(activity: Activity) { 27 | val root = activity.findViewById(android.R.id.content) 28 | val content = root.getChildAt(0) 29 | Draftsman().installVision(activity, content) 30 | } 31 | 32 | fun uninstall(activity: Activity) { 33 | val root = activity.findViewById(android.R.id.content) 34 | for (index in 0 until root.childCount) { 35 | val child = root.getChildAt(index) 36 | if (child is DraftsmanLayout) { 37 | root.removeView(child) 38 | break 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/Config.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal 2 | 3 | import android.graphics.Color 4 | 5 | internal object Config { 6 | 7 | var usePx = true 8 | 9 | var paddingEnabled = true 10 | 11 | var marginEnabled = true 12 | 13 | var gridEnabled = false 14 | 15 | val boundLineColor = Color.parseColor("#311B92") 16 | 17 | val selectedBoundColor = Color.parseColor("#2962FF") 18 | 19 | val marginColor = Color.parseColor("#cd0000") 20 | 21 | val paddingColor = Color.parseColor("#6676FF03") 22 | 23 | val gridColor = Color.parseColor("#777777") 24 | 25 | fun reset() { 26 | usePx = true 27 | paddingEnabled = true 28 | marginEnabled = true 29 | gridEnabled = false 30 | } 31 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/DraftsmanLayout.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.Paint.ANTI_ALIAS_FLAG 7 | import android.util.AttributeSet 8 | import android.view.LayoutInflater 9 | import android.view.MotionEvent 10 | import android.view.View 11 | import android.view.View.OnClickListener 12 | import android.widget.FrameLayout 13 | import androidx.recyclerview.widget.RecyclerView 14 | import com.gojek.draftsman.R 15 | import com.gojek.draftsman.internal.drawer.DrawerAdapter 16 | import com.gojek.draftsman.internal.drawer.DrawerOptionHandler 17 | import com.gojek.draftsman.internal.drawer.ModernDrawerLayout 18 | import com.gojek.draftsman.internal.model.* 19 | import com.gojek.draftsman.internal.utils.DimensionConverter 20 | import com.gojek.draftsman.internal.utils.ExitListener 21 | import com.gojek.draftsman.internal.utils.LayeredViewBuilder 22 | import com.gojek.draftsman.internal.utils.getDrawerLayoutList 23 | import com.gojek.draftsman.internal.visualizers.* 24 | 25 | internal class DraftsmanLayout @JvmOverloads constructor( 26 | context: Context, 27 | attributeSet: AttributeSet? = null, 28 | defStyleRes: Int = 0 29 | ) : FrameLayout( 30 | context, 31 | attributeSet, 32 | defStyleRes 33 | ), ViewGroupCallback, DraftsmanCallback { 34 | 35 | private val borderPaint = Paint(ANTI_ALIAS_FLAG).apply { 36 | style = Paint.Style.STROKE 37 | } 38 | 39 | private val infoVisualizer: InfoVisualizer by lazy { 40 | InfoVisualizer( 41 | this, 42 | { exitListener?.invoke() }, 43 | { 44 | isInfoEnabled = false 45 | infoVisualizer.hide() 46 | }, 47 | { 48 | Config.usePx = it 49 | invalidate() 50 | } 51 | ) 52 | } 53 | 54 | private var exitListener: ExitListener? = null 55 | 56 | private lateinit var root: View 57 | 58 | private var layeredViewBuilder: LayeredViewBuilder? = null 59 | 60 | private var layerVisualizer: LayeringVisualizer? = null 61 | 62 | private var layeredViews = listOf() 63 | 64 | private var selectedViews = mutableListOf() 65 | 66 | private var isInfoEnabled = true 67 | 68 | private val drawerOptionHandler = DrawerOptionHandler( 69 | context, 70 | this, 71 | this 72 | ) 73 | 74 | init { 75 | setWillNotDraw(false) 76 | Config.reset() 77 | addDrawerLayout() 78 | } 79 | 80 | fun init( 81 | topInset: Int, 82 | root: View, 83 | exitListener: ExitListener 84 | ) { 85 | this.root = root 86 | this.exitListener = exitListener 87 | layeredViewBuilder = LayeredViewBuilder(topInset) 88 | invalidate() 89 | } 90 | 91 | override fun onDraw(canvas: Canvas) { 92 | if (::root.isInitialized) { 93 | if (layeredViews.isEmpty()) { 94 | layeredViewBuilder?.let { 95 | layeredViews = it.buildLayeredView(root) 96 | } 97 | } 98 | 99 | layeredViews.forEach { 100 | drawBound(canvas, it) 101 | if (selectedViews.contains(it.view)) { 102 | showProperties(canvas, it) 103 | } 104 | } 105 | 106 | if (isInfoEnabled) { 107 | selectedViews.lastOrNull()?.let { 108 | infoVisualizer.updateInfoView(it) 109 | } 110 | } 111 | } 112 | } 113 | 114 | 115 | override fun onTouchEvent(event: MotionEvent): Boolean { 116 | if (event.actionMasked == MotionEvent.ACTION_UP && 117 | layerVisualizer == null 118 | ) { 119 | val possibleViews = getSelectedViews(event) 120 | if (possibleViews.size == 1) { 121 | onSelectView(possibleViews[0].view) 122 | } else { 123 | showLayeredViews(possibleViews) 124 | } 125 | } 126 | 127 | return null == layerVisualizer 128 | } 129 | 130 | override fun addView(index: Int, view: View?) { 131 | addView(view, index) 132 | } 133 | 134 | override fun onDetachedFromWindow() { 135 | drawerOptionHandler.cleanUp() 136 | super.onDetachedFromWindow() 137 | } 138 | 139 | override fun changeRootView(view: View) { 140 | root = view 141 | layeredViewBuilder?.let { 142 | layeredViews = it.buildLayeredView(root) 143 | } 144 | selectedViews.clear() 145 | invalidate() 146 | } 147 | 148 | override fun closeDrawer() { 149 | findViewById(R.id.draftsman_drawer).collapse() 150 | } 151 | 152 | private fun drawBound( 153 | canvas: Canvas, 154 | layeredView: LayeredView 155 | ) { 156 | if (selectedViews.contains(layeredView.view)) { 157 | borderPaint.apply { 158 | color = Config.selectedBoundColor 159 | strokeWidth = DimensionConverter.getPxOfDp(2f, context) 160 | } 161 | } else { 162 | borderPaint.apply { 163 | color = Config.boundLineColor 164 | strokeWidth = 1f 165 | } 166 | } 167 | canvas.drawRect(layeredView.position, borderPaint) 168 | } 169 | 170 | private fun showProperties( 171 | canvas: Canvas, 172 | layeredView: LayeredView 173 | ) { 174 | if (Config.paddingEnabled) { 175 | PaddingVisualizer.draw( 176 | canvas, 177 | layeredView.view, 178 | layeredView.position 179 | ) 180 | } 181 | if (Config.marginEnabled) { 182 | MarginVisualizer.draw( 183 | canvas, 184 | layeredView.view, 185 | layeredView.position 186 | ) 187 | } 188 | } 189 | 190 | private fun getSelectedViews(event: MotionEvent): List { 191 | val possibleViews = mutableListOf() 192 | val views = layeredViews.filter { 193 | it.position.contains(event.x, event.y) 194 | } 195 | 196 | if (views.size == 1) { 197 | possibleViews.add(views[0]) 198 | } else { 199 | val smallestArea = views.map { it.position.width().times(it.position.height()) }.min() 200 | val smallestAreaViews = views.filter { 201 | it.position.width().times(it.position.height()) == smallestArea 202 | } 203 | possibleViews.addAll(smallestAreaViews) 204 | } 205 | 206 | return possibleViews 207 | } 208 | 209 | private fun showLayeredViews(views: List) { 210 | layerVisualizer = LayeringVisualizer(this).apply { 211 | showLayeredViews(views) 212 | setOnLayeredViewSelectListener { 213 | onSelectView(it) 214 | remove(this@DraftsmanLayout) 215 | layerVisualizer = null 216 | } 217 | setOnDismissListener(OnClickListener { 218 | remove(this@DraftsmanLayout) 219 | layerVisualizer = null 220 | }) 221 | } 222 | } 223 | 224 | private fun onSelectView(view: View) { 225 | if (selectedViews.contains(view)) { 226 | selectedViews.remove(view) 227 | } else { 228 | selectedViews.add(view) 229 | isInfoEnabled = true 230 | } 231 | invalidate() 232 | } 233 | 234 | private fun addDrawerLayout() { 235 | val recyclerView = LayoutInflater.from(context).inflate( 236 | R.layout.draftsman_drawer_layout, 237 | this 238 | ).findViewById(R.id.draftsman_config_drawer) 239 | val adapter = DrawerAdapter(drawerOptionHandler.getDrawerInteractions()).also { 240 | drawerOptionHandler.adapter = it 241 | } 242 | recyclerView.adapter = adapter 243 | adapter.setData(getDrawerLayoutList()) 244 | } 245 | } 246 | 247 | internal interface ViewGroupCallback { 248 | fun addView(index: Int, view: View?) 249 | fun invalidate() 250 | fun removeView(view: View?) 251 | } 252 | 253 | internal interface DraftsmanCallback { 254 | fun changeRootView(view: View) 255 | fun closeDrawer() 256 | } 257 | -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/constants/VisualizerIds.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.constants 2 | 3 | internal const val PADDING_VISUALIZER = 1 4 | 5 | internal const val MARGIN_VISUALIZER = 2 6 | 7 | internal const val GRID_VISUALIZER = 3 8 | 9 | internal const val GRID_SIZE_SEEKER = 4 10 | 11 | internal const val INSPECTION_ROOT_SELECTION = 5 12 | 13 | internal const val OVERLAY_SELECTION = 6 -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/drawer/DefaultAnimationListener.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.drawer 2 | 3 | import android.animation.Animator 4 | 5 | internal open class DefaultAnimationListener : Animator.AnimatorListener { 6 | 7 | override fun onAnimationRepeat(animation: Animator?) { 8 | 9 | } 10 | 11 | override fun onAnimationEnd(animation: Animator?) { 12 | } 13 | 14 | override fun onAnimationCancel(animation: Animator?) { 15 | } 16 | 17 | override fun onAnimationStart(animation: Animator?) { 18 | } 19 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/drawer/DrawerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.drawer 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.ImageView 7 | import android.widget.TextView 8 | import androidx.appcompat.widget.AppCompatSeekBar 9 | import androidx.appcompat.widget.SwitchCompat 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.gojek.draftsman.R 12 | import com.gojek.draftsman.internal.extensions.doOnProgressChange 13 | import com.gojek.draftsman.internal.model.* 14 | import com.gojek.draftsman.internal.utils.buildGridSize 15 | import kotlin.math.max 16 | 17 | private const val VIEW_TYPE_HEADER = 1 18 | 19 | private const val VIEW_TYPE_TOGGLE_ITEM = 2 20 | 21 | private const val VIEW_TYPE_RANGE_ITEM = 3 22 | 23 | private const val VIEW_TYPE_SELECTION_ITEM = 4 24 | 25 | internal class DrawerAdapter( 26 | private val interactions: DrawerInteractions 27 | ) : RecyclerView.Adapter() { 28 | 29 | private val items = mutableListOf() 30 | 31 | fun setData(data: List) { 32 | items.apply { 33 | clear() 34 | addAll(data) 35 | } 36 | notifyDataSetChanged() 37 | } 38 | 39 | fun updateItemAtPosition(item: DrawerItem, position: Int) { 40 | items[position] = item 41 | notifyItemChanged(position) 42 | } 43 | 44 | fun getList() = items 45 | 46 | override fun getItemCount() = items.size 47 | 48 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbsViewHolder { 49 | return when (viewType) { 50 | VIEW_TYPE_TOGGLE_ITEM -> ToggleViewHolder( 51 | getView(R.layout.draftsman_drawer_item_toggle, parent) 52 | ) 53 | VIEW_TYPE_RANGE_ITEM -> RangeViewHolder( 54 | getView(R.layout.draftsman_drawer_item_range, parent) 55 | ) 56 | VIEW_TYPE_SELECTION_ITEM -> SelectionViewHolder( 57 | getView(R.layout.draftsman_drawer_item_selection, parent) 58 | ) 59 | else -> HeaderViewHolder( 60 | getView(R.layout.draftsman_drawer_item_header, parent) 61 | ) 62 | } 63 | } 64 | 65 | override fun onBindViewHolder(holder: AbsViewHolder, position: Int) { 66 | holder.bind(items[position]) 67 | } 68 | 69 | override fun getItemViewType(position: Int): Int { 70 | return when (items[position]) { 71 | is DrawerHeadingItem -> VIEW_TYPE_HEADER 72 | is DrawerToggleItem -> VIEW_TYPE_TOGGLE_ITEM 73 | is DrawerRangeItem -> VIEW_TYPE_RANGE_ITEM 74 | is DrawerSelectionItem -> VIEW_TYPE_SELECTION_ITEM 75 | } 76 | } 77 | 78 | private fun getView(layoutResId: Int, parent: ViewGroup): View { 79 | return LayoutInflater.from(parent.context).inflate( 80 | layoutResId, 81 | parent, 82 | false 83 | ) 84 | } 85 | 86 | internal abstract inner class AbsViewHolder( 87 | itemView: View 88 | ) : RecyclerView.ViewHolder(itemView) { 89 | 90 | abstract fun bind(drawerItem: DrawerItem) 91 | } 92 | 93 | private inner class HeaderViewHolder( 94 | itemView: View 95 | ) : AbsViewHolder(itemView) { 96 | override fun bind(drawerItem: DrawerItem) { 97 | itemView.run { 98 | findViewById(R.id.drawer_item_heading).text = 99 | (drawerItem as DrawerHeadingItem).title 100 | } 101 | } 102 | } 103 | 104 | private inner class ToggleViewHolder( 105 | itemView: View 106 | ) : AbsViewHolder(itemView) { 107 | override fun bind(drawerItem: DrawerItem) { 108 | val item = drawerItem as DrawerToggleItem 109 | itemView.run { 110 | findViewById(R.id.drawer_item_toggle_icon).setImageResource(item.icon) 111 | findViewById(R.id.drawer_item_toggle_label).text = item.title 112 | findViewById(R.id.drawer_item_toggle).apply { 113 | isChecked = item.enabled 114 | setOnCheckedChangeListener { _, isChecked -> 115 | interactions.onToggleChange( 116 | item, 117 | isChecked 118 | ) 119 | } 120 | } 121 | } 122 | } 123 | } 124 | 125 | private inner class RangeViewHolder( 126 | itemView: View 127 | ) : AbsViewHolder(itemView) { 128 | private val rangeText = itemView.findViewById(R.id.drawer_item_range_value) 129 | override fun bind(drawerItem: DrawerItem) { 130 | val item = drawerItem as DrawerRangeItem 131 | val seekbar = itemView.findViewById(R.id.drawer_item_seekbar).apply { 132 | max = item.maxValue 133 | doOnProgressChange { 134 | rangeText.text = "${buildGridSize(it + item.minValue)}" 135 | interactions.onRangeChange(item, it + item.minValue) 136 | } 137 | } 138 | rangeText.text = 139 | "${buildGridSize(max(item.minValue, seekbar.progress + item.minValue))}" 140 | itemView.findViewById(R.id.drawer_item_range_title).text = item.title 141 | } 142 | } 143 | 144 | private inner class SelectionViewHolder( 145 | itemView: View 146 | ) : AbsViewHolder(itemView) { 147 | override fun bind(drawerItem: DrawerItem) { 148 | val item = drawerItem as DrawerSelectionItem 149 | itemView.findViewById(R.id.drawer_item_selection_icon) 150 | .setImageResource(item.icon) 151 | itemView.findViewById(R.id.drawer_item_selection).text = item.selection 152 | itemView.setOnClickListener { interactions.onSelectItem(item) } 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/drawer/DrawerInteractions.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.drawer 2 | 3 | import com.gojek.draftsman.internal.model.DrawerRangeItem 4 | import com.gojek.draftsman.internal.model.DrawerSelectionItem 5 | import com.gojek.draftsman.internal.model.DrawerToggleItem 6 | 7 | internal interface DrawerInteractions { 8 | 9 | fun onSelectItem(item: DrawerSelectionItem) 10 | 11 | fun onToggleChange(item: DrawerToggleItem, isChecked: Boolean) 12 | 13 | fun onRangeChange(item: DrawerRangeItem, value: Int) 14 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/drawer/DrawerOptionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.drawer 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.graphics.Bitmap 7 | import android.net.Uri 8 | import android.os.Handler 9 | import android.os.HandlerThread 10 | import android.provider.MediaStore 11 | import android.view.View 12 | import android.view.View.GONE 13 | import android.view.View.VISIBLE 14 | import android.view.ViewGroup 15 | import android.widget.FrameLayout.LayoutParams 16 | import android.widget.FrameLayout.LayoutParams.MATCH_PARENT 17 | import android.widget.LinearLayout 18 | import android.widget.Toast 19 | import androidx.appcompat.app.AlertDialog 20 | import androidx.appcompat.app.AppCompatActivity 21 | import androidx.core.app.ActivityCompat 22 | import androidx.core.content.PermissionChecker 23 | import com.gojek.draftsman.internal.Config 24 | import com.gojek.draftsman.internal.DraftsmanCallback 25 | import com.gojek.draftsman.internal.DraftsmanLayout 26 | import com.gojek.draftsman.internal.ViewGroupCallback 27 | import com.gojek.draftsman.internal.constants.* 28 | import com.gojek.draftsman.internal.fragment.FileChooserFragment 29 | import com.gojek.draftsman.internal.fragment.RequestPermissionFragment 30 | import com.gojek.draftsman.internal.model.DrawerItem 31 | import com.gojek.draftsman.internal.model.DrawerRangeItem 32 | import com.gojek.draftsman.internal.model.DrawerSelectionItem 33 | import com.gojek.draftsman.internal.model.DrawerToggleItem 34 | import com.gojek.draftsman.internal.visualizers.GridVisualizer 35 | import com.gojek.draftsman.internal.visualizers.OverlayVisualizer 36 | import com.gojek.draftsman.internal.widgets.InspectionRootItemView 37 | 38 | internal class DrawerOptionHandler( 39 | private val context: Context, 40 | private val viewGroupCallback: ViewGroupCallback, 41 | private val draftsmanCallback: DraftsmanCallback 42 | ) { 43 | 44 | var adapter: DrawerAdapter? = null 45 | 46 | private var gridVisualizer: GridVisualizer? = null 47 | 48 | private var overlayVisualizer: OverlayVisualizer? = null 49 | 50 | private var handler: Handler? = null 51 | 52 | fun getDrawerInteractions() = object : DrawerInteractions { 53 | 54 | override fun onSelectItem(item: DrawerSelectionItem) { 55 | onSelectItemInternal(item) 56 | } 57 | 58 | override fun onToggleChange(item: DrawerToggleItem, isChecked: Boolean) { 59 | onToggleChangeInternal(item, isChecked) 60 | } 61 | 62 | override fun onRangeChange(item: DrawerRangeItem, value: Int) { 63 | onRangeChangeInternal(item, value) 64 | } 65 | } 66 | 67 | fun cleanUp() { 68 | removeOverlay() 69 | } 70 | 71 | private fun onToggleChangeInternal( 72 | item: DrawerToggleItem, 73 | isChecked: Boolean 74 | ) { 75 | when (item.toggleId) { 76 | MARGIN_VISUALIZER -> updateGlobalConfig { Config.marginEnabled = isChecked } 77 | PADDING_VISUALIZER -> updateGlobalConfig { Config.paddingEnabled = isChecked } 78 | GRID_VISUALIZER -> toggleGrid(isChecked) 79 | } 80 | } 81 | 82 | private fun updateGlobalConfig(update: () -> Unit) { 83 | update() 84 | viewGroupCallback.invalidate() 85 | } 86 | 87 | private fun toggleGrid(isEnabled: Boolean) { 88 | Config.gridEnabled = isEnabled 89 | if (isEnabled) { 90 | if (null == gridVisualizer) { 91 | gridVisualizer = GridVisualizer(context).apply { 92 | layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) 93 | } 94 | viewGroupCallback.addView(0, gridVisualizer) 95 | } 96 | 97 | val gridSizeRangeItem = DrawerRangeItem("Grid size", GRID_SIZE_SEEKER, 2, 8) 98 | val index = adapter?.getList() 99 | ?.indexOfFirst { it is DrawerToggleItem && it.toggleId == GRID_VISUALIZER } 100 | ?: 0 101 | adapter?.apply { 102 | getList().add(index + 1, gridSizeRangeItem) 103 | notifyItemRangeChanged(index + 1, getList().size - 1) 104 | } 105 | } else { 106 | val index = adapter?.getList() 107 | ?.indexOfFirst { it is DrawerRangeItem && it.rangeId == GRID_SIZE_SEEKER } 108 | ?: 0 109 | adapter?.apply { 110 | getList().removeAt(index) 111 | notifyItemRemoved(getList().lastIndex + 1) 112 | notifyItemRangeChanged(index, getList().lastIndex) 113 | } 114 | 115 | } 116 | gridVisualizer?.visibility = if (isEnabled) VISIBLE else GONE 117 | } 118 | 119 | private fun onRangeChangeInternal(item: DrawerRangeItem, value: Int) { 120 | when (item.rangeId) { 121 | GRID_SIZE_SEEKER -> gridVisualizer?.updateGridSize(value) 122 | } 123 | } 124 | 125 | private fun onSelectItemInternal(item: DrawerSelectionItem) { 126 | when (item.selectionId) { 127 | INSPECTION_ROOT_SELECTION -> showInspectionRootDialog() 128 | OVERLAY_SELECTION -> selectOverlay() 129 | } 130 | } 131 | 132 | private fun showInspectionRootDialog() { 133 | var alertDialog: AlertDialog? = null 134 | val rootView = LinearLayout(context).apply { 135 | orientation = LinearLayout.VERTICAL 136 | } 137 | val root = (context as Activity).findViewById(android.R.id.content) 138 | for (index in 0 until root.childCount) { 139 | val child = root.getChildAt(index) 140 | if (child !is DraftsmanLayout) { 141 | rootView.addView(InspectionRootItemView(context).apply { 142 | setText(child.javaClass.simpleName) 143 | setOnClickListener { 144 | onClickView(child, index) 145 | alertDialog?.dismiss() 146 | } 147 | }) 148 | } 149 | } 150 | alertDialog = AlertDialog.Builder(context) 151 | .setTitle("Select inspection root") 152 | .setView(rootView) 153 | .create().apply { show() } 154 | } 155 | 156 | private fun selectOverlay() { 157 | val storagePermission = Manifest.permission.READ_EXTERNAL_STORAGE 158 | draftsmanCallback.closeDrawer() 159 | if (hasPermission(storagePermission)) { 160 | selectImage() 161 | } else { 162 | requestPermission(storagePermission) { 163 | if (it == PermissionChecker.PERMISSION_GRANTED) { 164 | selectImage() 165 | } else if (!shouldShowPermissionRationale(storagePermission)) { 166 | Toast.makeText( 167 | context, 168 | "Need storage permission to place overlay", 169 | Toast.LENGTH_LONG 170 | ).show() 171 | } 172 | } 173 | } 174 | } 175 | 176 | private fun onClickView(child: View, index: Int) { 177 | draftsmanCallback.run { 178 | closeDrawer() 179 | changeRootView(child) 180 | } 181 | val name = if (index == 0) { 182 | "Activity Layout" 183 | } else { 184 | child.javaClass.simpleName 185 | } 186 | 187 | val item = 188 | findDrawerItem { it is DrawerSelectionItem && it.selectionId == INSPECTION_ROOT_SELECTION } 189 | val index = adapter?.getList()?.indexOf(item) ?: -1 190 | adapter?.updateItemAtPosition(item.copy(selection = name), index) 191 | } 192 | 193 | private fun findDrawerItem(predicate: (DrawerItem) -> Boolean): T { 194 | return adapter?.getList()?.find { predicate(it) } as T 195 | } 196 | 197 | private fun hasPermission(permission: String): Boolean { 198 | return PermissionChecker.checkSelfPermission( 199 | context, 200 | permission 201 | ) == PermissionChecker.PERMISSION_GRANTED 202 | } 203 | 204 | private fun requestPermission(permission: String, callback: (Int) -> Unit) { 205 | if (!RequestPermissionFragment.isAdded) { 206 | val activity = context as AppCompatActivity 207 | activity.supportFragmentManager.beginTransaction().apply { 208 | replace(android.R.id.content, RequestPermissionFragment) 209 | commitNow() 210 | } 211 | } 212 | RequestPermissionFragment.requestPermission(permission, callback) 213 | } 214 | 215 | private fun shouldShowPermissionRationale(permission: String): Boolean { 216 | val activity = context as AppCompatActivity 217 | return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission) 218 | } 219 | 220 | private fun selectImage() { 221 | if (!FileChooserFragment.isAdded) { 222 | val activity = context as AppCompatActivity 223 | activity.supportFragmentManager.beginTransaction().apply { 224 | replace(android.R.id.content, FileChooserFragment) 225 | commitNow() 226 | } 227 | } 228 | FileChooserFragment.selectFile { 229 | if (null != it) { 230 | showImage(it) 231 | } 232 | } 233 | } 234 | 235 | private fun showImage(uri: Uri) { 236 | if (null == handler) { 237 | val handlerThread = 238 | HandlerThread(DrawerOptionHandler::class.java.simpleName).apply { start() } 239 | handler = Handler(handlerThread.looper) 240 | } 241 | if (null == overlayVisualizer) { 242 | overlayVisualizer = OverlayVisualizer(context) { removeOverlay() }.apply { 243 | layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) 244 | viewGroupCallback.addView(0, this) 245 | } 246 | } 247 | handler?.post { 248 | val bitmap = MediaStore.Images.Media.getBitmap(context.contentResolver, uri) 249 | if (null != bitmap) { 250 | resizeBitmap(bitmap) 251 | } 252 | } 253 | } 254 | 255 | private fun resizeBitmap(bitmap: Bitmap) { 256 | val container = (context as Activity).findViewById(android.R.id.content) 257 | val size = getBitmapSize(bitmap, container) 258 | val resizedBitmap = Bitmap.createScaledBitmap(bitmap, size.first, size.second, true) 259 | overlayVisualizer?.setOverlayImage(resizedBitmap) 260 | } 261 | 262 | private fun getBitmapSize(bitmap: Bitmap, container: View): Pair { 263 | var pair = Pair(0, 0) 264 | val aspectRatio = bitmap.width.toFloat() / bitmap.height.toFloat() 265 | if (bitmap.width >= bitmap.height) { 266 | pair = if (bitmap.width >= container.width) { 267 | val height = container.width * aspectRatio 268 | Pair(container.width, height.toInt()) 269 | 270 | } else { 271 | Pair(bitmap.width, bitmap.height) 272 | } 273 | } else if (bitmap.height > bitmap.width) { 274 | pair = if (bitmap.height >= container.height) { 275 | val width = container.height * aspectRatio 276 | Pair(width.toInt(), container.height) 277 | } else { 278 | Pair(bitmap.width, bitmap.height) 279 | } 280 | } 281 | return pair 282 | } 283 | 284 | private fun removeOverlay() { 285 | viewGroupCallback.removeView(overlayVisualizer) 286 | (context as AppCompatActivity).supportFragmentManager.popBackStack() 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/drawer/DrawerPathGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.drawer 2 | 3 | import android.content.res.Resources 4 | import android.graphics.Path 5 | import android.graphics.RectF 6 | import com.gojek.draftsman.internal.drawer.Orientation.* 7 | 8 | internal class DrawerPathGenerator( 9 | private val height: Int, 10 | private val resources: Resources, 11 | private val sliderShapeDimension: Float, 12 | private val peekDistance: Float 13 | ) { 14 | 15 | fun getPath(orientation: Orientation, childDimension: Int): Pair { 16 | return when (orientation) { 17 | HORIZONTAL_RIGHT -> HorizontalRightPathGenerator( 18 | height, 19 | resources, 20 | sliderShapeDimension, 21 | peekDistance 22 | ).getPath(childDimension) 23 | HORIZONTAL_LEFT -> HorizontalLeftPathGenerator().getPath() 24 | VERTICAL_TOP -> VerticalTopPathGenerator().getPath() 25 | VERTICAL_BOTTOM -> VerticalBottomPathGenerator().getPath() 26 | } 27 | } 28 | 29 | private class HorizontalRightPathGenerator( 30 | private val height: Int, 31 | private val resources: Resources, 32 | private val sliderShapeDimension: Float, 33 | private val peekDistance: Float 34 | ) { 35 | 36 | fun getPath(childWidth: Int): Pair { 37 | 38 | val width = resources.displayMetrics.widthPixels.toFloat() 39 | val heightHalf = height.div(2f) 40 | val sliderShapeDimensionHalf = sliderShapeDimension.div(2) 41 | 42 | val rectF = RectF().apply { 43 | right = width 44 | left = width - sliderShapeDimensionHalf - peekDistance 45 | top = heightHalf - sliderShapeDimensionHalf 46 | bottom = top + sliderShapeDimension 47 | } 48 | 49 | val yOffset: Float = sliderShapeDimension * 0.05f 50 | val xOffset = sliderShapeDimensionHalf * 4 / 3 51 | 52 | val path = Path().apply { 53 | moveTo(sliderShapeDimension, heightHalf - sliderShapeDimensionHalf) 54 | rCubicTo( 55 | -xOffset, yOffset, 56 | -xOffset, sliderShapeDimension - yOffset, 57 | 0f, sliderShapeDimension 58 | ) 59 | addRect( 60 | sliderShapeDimension, 61 | 0f, 62 | childWidth + sliderShapeDimension, 63 | height.toFloat(), 64 | Path.Direction.CCW 65 | ) 66 | } 67 | 68 | return Pair(path, rectF) 69 | } 70 | } 71 | 72 | private class HorizontalLeftPathGenerator { 73 | 74 | fun getPath(): Pair { 75 | return Pair(Path(), RectF()) 76 | } 77 | 78 | } 79 | 80 | private class VerticalTopPathGenerator { 81 | 82 | fun getPath(): Pair { 83 | return Pair(Path(), RectF()) 84 | } 85 | 86 | } 87 | 88 | private class VerticalBottomPathGenerator { 89 | 90 | fun getPath(): Pair { 91 | return Pair(Path(), RectF()) 92 | } 93 | 94 | } 95 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/drawer/ModernDrawerLayout.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.drawer 2 | 3 | import android.animation.Animator 4 | import android.animation.AnimatorSet 5 | import android.animation.ObjectAnimator 6 | import android.animation.ValueAnimator 7 | import android.content.Context 8 | import android.graphics.* 9 | import android.graphics.Paint.ANTI_ALIAS_FLAG 10 | import android.graphics.drawable.ColorDrawable 11 | import android.graphics.drawable.Drawable 12 | import android.util.AttributeSet 13 | import android.view.MotionEvent 14 | import android.view.MotionEvent.* 15 | import android.view.View 16 | import android.view.View.MeasureSpec.* 17 | import android.view.ViewGroup 18 | import android.view.animation.AccelerateDecelerateInterpolator 19 | import com.gojek.draftsman.R 20 | import kotlin.math.abs 21 | import kotlin.math.min 22 | 23 | private val TAG = ModernDrawerLayout::class.java.simpleName 24 | 25 | internal class ModernDrawerLayout @JvmOverloads constructor( 26 | context: Context, 27 | attributeSet: AttributeSet? = null, 28 | defStyleRes: Int = 0 29 | ) : ViewGroup( 30 | context, 31 | attributeSet, 32 | defStyleRes 33 | ) { 34 | 35 | private val sliderShapeDimension = dpToPx(72, resources) 36 | 37 | private var orientation = Orientation.HORIZONTAL_RIGHT 38 | 39 | private var peekDistance = 0f 40 | 41 | private var bgDrawable: Drawable = ColorDrawable(Color.LTGRAY) 42 | 43 | private var sliderBitmap: Bitmap? = null 44 | 45 | private var expanded = false 46 | 47 | private val bitmapStartingPoint = PointF() 48 | 49 | private val touchArea = RectF() 50 | 51 | private val sliderIconRect = Rect() 52 | 53 | private var drawerOpenIcon: Drawable? = null 54 | 55 | private var drawerCloseIcon: Drawable? = null 56 | 57 | private var drawerIcon: Drawable? = null 58 | 59 | private var previousX = 0f 60 | 61 | private var maxTranslation = 0f 62 | 63 | private var animator: Animator? = null 64 | 65 | private var motionDownTime: Long = 0 66 | 67 | private var sliderPath = Path() 68 | 69 | private val shadowPaint = Paint(ANTI_ALIAS_FLAG).apply { 70 | color = Color.WHITE 71 | setShadowLayer(dpToPx(8, context.resources), 0f, 0f, Color.LTGRAY) 72 | } 73 | 74 | private val overlayPaint = Paint(ANTI_ALIAS_FLAG).apply { 75 | color = Color.parseColor("#999999") 76 | alpha = 0 77 | } 78 | 79 | init { 80 | setWillNotDraw(false) 81 | attributeSet?.let(::initAttrs) 82 | } 83 | 84 | fun setIcons(openIcon: Drawable, closeIcon: Drawable) { 85 | drawerOpenIcon = openIcon 86 | drawerCloseIcon = closeIcon 87 | drawerIcon = drawerOpenIcon 88 | } 89 | 90 | fun collapse() { 91 | if (expanded) { 92 | collapseDrawer() 93 | } 94 | } 95 | 96 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 97 | 98 | require(childCount <= 1) { "ModernDrawerLayout can have only one child" } 99 | 100 | var width = getSize(widthMeasureSpec) 101 | var height = getSize(heightMeasureSpec) 102 | 103 | var widthMeasureMode = getMode(widthMeasureSpec) 104 | var heightMeasureMode = getMode(heightMeasureSpec) 105 | 106 | if (widthMeasureMode != EXACTLY && heightMeasureMode != EXACTLY) { 107 | if (isInEditMode) { 108 | if (widthMeasureMode == AT_MOST) { 109 | widthMeasureMode = EXACTLY 110 | } else if (widthMeasureMode == UNSPECIFIED) { 111 | widthMeasureMode = EXACTLY 112 | width = 300 113 | } 114 | if (heightMeasureMode == AT_MOST) { 115 | heightMeasureMode = EXACTLY 116 | } else if (heightMeasureMode == UNSPECIFIED) { 117 | heightMeasureMode = EXACTLY 118 | height = 300 119 | } 120 | 121 | } else { 122 | throw IllegalArgumentException("ModernDrawerLayout has to be measured exactly") 123 | } 124 | } 125 | 126 | if (orientation == Orientation.HORIZONTAL_LEFT || 127 | orientation == Orientation.HORIZONTAL_RIGHT 128 | ) { 129 | width += resources.displayMetrics.widthPixels - peekDistance.toInt() 130 | } else { 131 | height += resources.displayMetrics.heightPixels - peekDistance.toInt() 132 | } 133 | 134 | setMeasuredDimension(width, height) 135 | 136 | 137 | val childLp = getChildAt(0).layoutParams 138 | 139 | val childWidthMeasureSpec = 140 | getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLp.width) 141 | val childHeightMeasureSpec = 142 | getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLp.height) 143 | getChildAt(0).measure(childWidthMeasureSpec, childHeightMeasureSpec) 144 | maxTranslation = -min( 145 | width - resources.displayMetrics.widthPixels - peekDistance - sliderShapeDimension, 146 | getChildAt(0).measuredWidth - peekDistance 147 | ) 148 | } 149 | 150 | override fun onLayout(changed: Boolean, p1: Int, p2: Int, p3: Int, p4: Int) { 151 | val child = getChildAt(0) 152 | child.layout( 153 | resources.displayMetrics.widthPixels - peekDistance.toInt(), 154 | paddingTop, 155 | resources.displayMetrics.widthPixels - peekDistance.toInt() + child.measuredWidth, 156 | paddingTop + measuredHeight 157 | ) 158 | if (changed) { 159 | generateSliderBitmap() 160 | } 161 | } 162 | 163 | override fun dispatchDraw(canvas: Canvas) { 164 | canvas.apply { 165 | drawPaint(overlayPaint) 166 | val count = save() 167 | translate(bitmapStartingPoint.x, bitmapStartingPoint.y) 168 | drawPath(sliderPath, shadowPaint) 169 | drawBitmap(sliderBitmap!!, 0f, 0f, null) 170 | restoreToCount(count) 171 | } 172 | drawerIcon?.bounds = sliderIconRect 173 | drawerIcon?.draw(canvas) 174 | super.dispatchDraw(canvas) 175 | } 176 | 177 | override fun onTouchEvent(event: MotionEvent): Boolean { 178 | return when (event.actionMasked) { 179 | ACTION_DOWN -> { 180 | motionDownTime = System.currentTimeMillis() 181 | touchArea.contains(event.x, event.y) 182 | } 183 | ACTION_UP -> { 184 | if (System.currentTimeMillis() - motionDownTime <= 300) { 185 | toggleState() 186 | } else { 187 | expandOrCollapse() 188 | } 189 | previousX = 0f 190 | true 191 | } 192 | ACTION_MOVE -> { 193 | moveDrawer(event.rawX) 194 | false 195 | } 196 | else -> false 197 | } 198 | } 199 | 200 | override fun onDetachedFromWindow() { 201 | if (animator?.isRunning == true) { 202 | animator?.cancel() 203 | } 204 | super.onDetachedFromWindow() 205 | } 206 | 207 | private fun generateSliderBitmap() { 208 | 209 | bitmapStartingPoint.x = 210 | resources.displayMetrics.widthPixels - peekDistance - sliderShapeDimension 211 | bitmapStartingPoint.y = 0f 212 | 213 | val result = DrawerPathGenerator( 214 | height, 215 | resources, 216 | sliderShapeDimension, 217 | peekDistance 218 | ).getPath( 219 | orientation, 220 | getChildAt(0).measuredWidth 221 | ) 222 | 223 | sliderBitmap = Bitmap.createBitmap( 224 | getChildAt(0).measuredWidth + sliderShapeDimension.toInt(), 225 | height, 226 | Bitmap.Config.ARGB_8888 227 | ) 228 | if (null != background) { 229 | if (bgDrawable != background) { 230 | bgDrawable = background 231 | setBackgroundResource(0) 232 | } else { 233 | bgDrawable = ColorDrawable(Color.WHITE) 234 | } 235 | } 236 | bgDrawable.bounds = Rect( 237 | 0, 238 | 0, 239 | getChildAt(0).measuredWidth + sliderShapeDimension.toInt(), 240 | height 241 | ) 242 | val canvas = Canvas(sliderBitmap!!).apply { 243 | clipPath(result.first) 244 | } 245 | bgDrawable.draw(canvas) 246 | touchArea.set(result.second) 247 | calculateIconDrawRect() 248 | sliderPath = result.first 249 | } 250 | 251 | private fun expandDrawer() { 252 | val expandAnimation = ObjectAnimator.ofFloat( 253 | this, View.TRANSLATION_X, 254 | maxTranslation 255 | ).apply { 256 | duration = 500 257 | interpolator = AccelerateDecelerateInterpolator() 258 | addListener(object : DefaultAnimationListener() { 259 | override fun onAnimationEnd(animation: Animator?) { 260 | expanded = true 261 | onStateChanged() 262 | } 263 | }) 264 | } 265 | 266 | val overlayAnimation = ValueAnimator.ofInt(overlayPaint.alpha, 127).apply { 267 | duration = 500 268 | interpolator = AccelerateDecelerateInterpolator() 269 | addUpdateListener { 270 | overlayPaint.alpha = it.animatedValue as Int 271 | postInvalidate() 272 | } 273 | } 274 | 275 | animator = AnimatorSet().apply { 276 | playTogether(overlayAnimation, expandAnimation) 277 | start() 278 | } 279 | expanded = true 280 | } 281 | 282 | private fun collapseDrawer() { 283 | val collapseAnimation = ObjectAnimator.ofFloat( 284 | this, View.TRANSLATION_X, 285 | 0f 286 | ).apply { 287 | duration = 500 288 | interpolator = AccelerateDecelerateInterpolator() 289 | addListener(object : DefaultAnimationListener() { 290 | override fun onAnimationEnd(animation: Animator?) { 291 | expanded = false 292 | onStateChanged() 293 | } 294 | }) 295 | } 296 | 297 | val overlayAnimation = ValueAnimator.ofInt(overlayPaint.alpha, 0).apply { 298 | duration = 500 299 | interpolator = AccelerateDecelerateInterpolator() 300 | addUpdateListener { 301 | overlayPaint.alpha = it.animatedValue as Int 302 | postInvalidate() 303 | } 304 | } 305 | 306 | animator = AnimatorSet().apply { 307 | playTogether(overlayAnimation, collapseAnimation) 308 | start() 309 | } 310 | expanded = false 311 | } 312 | 313 | private fun toggleState() { 314 | if (expanded) { 315 | collapseDrawer() 316 | } else { 317 | expandDrawer() 318 | } 319 | } 320 | 321 | private fun calculateIconDrawRect() { 322 | val vCenter = (touchArea.top + (touchArea.bottom - touchArea.top) / 2).toInt() 323 | val hCenter = (touchArea.left + (touchArea.right - touchArea.left) / 2).toInt() 324 | val iconDimensionHalf = dpToPx(16, resources).toInt() 325 | sliderIconRect.apply { 326 | top = vCenter - iconDimensionHalf 327 | bottom = vCenter + iconDimensionHalf 328 | left = hCenter - iconDimensionHalf 329 | right = hCenter + iconDimensionHalf 330 | } 331 | } 332 | 333 | private fun initAttrs(attributeSet: AttributeSet) { 334 | val typedArray = 335 | context.obtainStyledAttributes(attributeSet, R.styleable.ModernDrawerLayout) 336 | peekDistance = 337 | typedArray.getDimension(R.styleable.ModernDrawerLayout_peekDistance, peekDistance) 338 | drawerOpenIcon = typedArray.getDrawable(R.styleable.ModernDrawerLayout_drawerOpenIcon) 339 | drawerCloseIcon = typedArray.getDrawable(R.styleable.ModernDrawerLayout_drawerCloseIcon) 340 | typedArray.recycle() 341 | drawerIcon = drawerOpenIcon 342 | } 343 | 344 | private fun onStateChanged() { 345 | drawerIcon = if (expanded) drawerCloseIcon else drawerOpenIcon 346 | invalidate() 347 | } 348 | 349 | private fun moveDrawer(x: Float) { 350 | if (previousX != 0f) { 351 | val dx = x - previousX 352 | previousX = x 353 | var newTranslationX = translationX + dx 354 | if (newTranslationX < maxTranslation) { 355 | newTranslationX = maxTranslation 356 | } else if (newTranslationX > 0) { 357 | newTranslationX = 0f 358 | } 359 | translationX = newTranslationX 360 | overlayPaint.alpha = (127 * abs(newTranslationX / maxTranslation)).toInt() 361 | postInvalidate() 362 | } else { 363 | previousX = x 364 | } 365 | } 366 | 367 | private fun expandOrCollapse() { 368 | if (translationX != 0f || translationX != maxTranslation) { 369 | val distanceToExpand = maxTranslation - translationX 370 | if (abs(distanceToExpand) > abs(translationX)) { 371 | collapseDrawer() 372 | } else { 373 | expandDrawer() 374 | } 375 | } 376 | } 377 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/drawer/Orientation.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.drawer 2 | 3 | 4 | internal enum class Orientation { 5 | 6 | HORIZONTAL_LEFT, 7 | 8 | HORIZONTAL_RIGHT, 9 | 10 | VERTICAL_TOP, 11 | 12 | VERTICAL_BOTTOM 13 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/drawer/ResourcesUtils.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.drawer 2 | 3 | import android.content.res.Resources 4 | import android.util.TypedValue 5 | 6 | internal fun dpToPx(dp: Int, resources: Resources) = 7 | TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), resources.displayMetrics) -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/drawer/State.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.drawer 2 | 3 | internal object State { 4 | 5 | const val COLLAPSED = 0 6 | 7 | const val EXPANDED = 1 8 | 9 | const val EXPANDING = 2 10 | 11 | const val COLLAPSING = 3 12 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/extensions/SeekBar.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.extensions 2 | 3 | import android.widget.SeekBar 4 | 5 | internal fun SeekBar.doOnProgressChange(action: (Int) -> Unit) { 6 | setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 7 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { 8 | action(progress) 9 | } 10 | 11 | override fun onStartTrackingTouch(seekBar: SeekBar?) { 12 | } 13 | 14 | override fun onStopTrackingTouch(seekBar: SeekBar?) { 15 | } 16 | }) 17 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/extensions/View.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.extensions 2 | 3 | import android.view.View 4 | import android.view.View.GONE 5 | import android.view.View.VISIBLE 6 | 7 | internal fun View.gone() { 8 | visibility = GONE 9 | } 10 | 11 | internal fun View.visible() { 12 | visibility = VISIBLE 13 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/extensions/ViewGroup.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.extensions 2 | 3 | import android.view.ViewGroup 4 | 5 | internal fun ViewGroup.removeAllViewGroups() { 6 | for (index in 0 until childCount) { 7 | if (getChildAt(index) is ViewGroup) { 8 | removeView(getChildAt(index)) 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/fragment/FileChooserFragment.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.fragment 2 | 3 | import android.app.Activity.RESULT_OK 4 | import android.content.Intent 5 | import android.net.Uri 6 | import androidx.fragment.app.Fragment 7 | 8 | private const val REQUEST_CODE = 1101 9 | 10 | internal object FileChooserFragment : Fragment() { 11 | 12 | private var resultCallback: (Uri?) -> Unit = {} 13 | 14 | fun selectFile(callback: (Uri?) -> Unit) { 15 | resultCallback = callback 16 | startActivityForResult( 17 | Intent(Intent.ACTION_GET_CONTENT).apply { type = "image/*" }, 18 | REQUEST_CODE 19 | ) 20 | } 21 | 22 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 23 | super.onActivityResult(requestCode, resultCode, data) 24 | if (resultCode == RESULT_OK && null != data) { 25 | resultCallback(data.data) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/fragment/RequestPermissionFragment.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.fragment 2 | 3 | import androidx.fragment.app.Fragment 4 | 5 | private const val REQUEST_CODE = 1100 6 | 7 | internal object RequestPermissionFragment : Fragment() { 8 | 9 | private var resultCallback: (Int) -> Unit = {} 10 | 11 | fun requestPermission(permission: String, callback: (Int) -> Unit) { 12 | resultCallback = callback 13 | requestPermissions(arrayOf(permission), REQUEST_CODE) 14 | } 15 | 16 | override fun onRequestPermissionsResult( 17 | requestCode: Int, 18 | permissions: Array, 19 | grantResults: IntArray 20 | ) { 21 | resultCallback(grantResults[0]) 22 | } 23 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/model/DrawerItem.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.model 2 | 3 | sealed class DrawerItem 4 | 5 | internal class DrawerHeadingItem(val title: String) : DrawerItem() 6 | 7 | internal class DrawerToggleItem( 8 | val icon: Int, 9 | val title: String, 10 | val toggleId: Int, 11 | val enabled: Boolean 12 | ) : DrawerItem() 13 | 14 | internal data class DrawerRangeItem( 15 | val title: String, 16 | val rangeId: Int, 17 | val minValue: Int, 18 | val maxValue: Int, 19 | val enabled: Boolean = false 20 | ) : DrawerItem() 21 | 22 | internal data class DrawerSelectionItem( 23 | val icon: Int, 24 | val selection: String, 25 | val selectionId: Int 26 | ) : DrawerItem() -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/model/LayeredView.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.model 2 | 3 | import android.graphics.RectF 4 | import android.view.View 5 | 6 | internal data class LayeredView( 7 | val layer: Int, 8 | val view: View, 9 | val position: RectF 10 | ) -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/utils/ColorUtils.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.utils 2 | 3 | internal fun getColorHex(color: Int): String { 4 | return String.format("#%06X %d%%", 0xFFFFFF and color, getAlphaPercentage(color)) 5 | } 6 | 7 | private fun getAlphaPercentage(color: Int): Int { 8 | return (color shr 24 and 0xff) * 100 / 255 9 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/utils/DimensionConverter.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.utils 2 | 3 | import android.content.Context 4 | import android.util.SparseArray 5 | import android.util.TypedValue 6 | 7 | internal object DimensionConverter { 8 | 9 | private val dpStore = SparseArray() 10 | 11 | private val spStore = SparseArray() 12 | 13 | fun getDpOf(px: Int, context: Context): Float { 14 | if (dpStore.indexOfKey(px) < 0) { 15 | dpStore.put( 16 | px, px.div( 17 | context.resources.displayMetrics.density 18 | ) 19 | ) 20 | } 21 | 22 | return dpStore[px] 23 | } 24 | 25 | fun getSpOf(px: Int, context: Context): Float { 26 | if (spStore.indexOfKey(px) < 0) { 27 | spStore.put( 28 | px, px.div( 29 | context.resources.displayMetrics.scaledDensity 30 | ) 31 | ) 32 | } 33 | 34 | return spStore[px] 35 | } 36 | 37 | fun getPxOfSp(sp: Float, context: Context): Float { 38 | return TypedValue.applyDimension( 39 | TypedValue.COMPLEX_UNIT_SP, 40 | sp, 41 | context.resources.displayMetrics 42 | ) 43 | } 44 | 45 | fun getPxOfDp(dp: Float, context: Context): Float { 46 | return TypedValue.applyDimension( 47 | TypedValue.COMPLEX_UNIT_DIP, 48 | dp, 49 | context.resources.displayMetrics 50 | ) 51 | } 52 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/utils/DimensionUtils.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.utils 2 | 3 | import android.content.Context 4 | import com.gojek.draftsman.internal.Config 5 | 6 | internal fun buildDimenString( 7 | dimension: Int, 8 | context: Context 9 | ) = if (Config.usePx) "$dimension px" 10 | else 11 | "${ 12 | DimensionConverter.getDpOf( 13 | dimension, 14 | context 15 | ).toInt() 16 | } dp" 17 | 18 | internal fun buildTextSizeString( 19 | dimension: Float, 20 | context: Context 21 | ) = if (Config.usePx) "$dimension px" 22 | else 23 | "${ 24 | DimensionConverter.getSpOf( 25 | dimension.toInt(), 26 | context 27 | ).toInt() 28 | } sp" 29 | 30 | internal fun buildGridSize(size: Int) = "${size}x${size}dp" 31 | -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/utils/DrawerLayoutContentList.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.utils 2 | 3 | import com.gojek.draftsman.R 4 | import com.gojek.draftsman.internal.constants.* 5 | import com.gojek.draftsman.internal.model.DrawerHeadingItem 6 | import com.gojek.draftsman.internal.model.DrawerItem 7 | import com.gojek.draftsman.internal.model.DrawerSelectionItem 8 | import com.gojek.draftsman.internal.model.DrawerToggleItem 9 | 10 | internal fun getDrawerLayoutList(): List { 11 | return listOf( 12 | DrawerHeadingItem("Toggles"), 13 | DrawerToggleItem(R.drawable.draftsman_ic_margin, "Margin", MARGIN_VISUALIZER, true), 14 | DrawerToggleItem(R.drawable.draftsman_ic_padding, "Padding", PADDING_VISUALIZER, true), 15 | DrawerHeadingItem("Ruler/Grid"), 16 | DrawerToggleItem(R.drawable.draftsman_ic_grid, "Enable", GRID_VISUALIZER, false), 17 | DrawerHeadingItem("Root Element"), 18 | DrawerSelectionItem( 19 | R.drawable.draftsman_ic_change_root, 20 | "Activity Layout", 21 | INSPECTION_ROOT_SELECTION 22 | ), 23 | DrawerHeadingItem("Overlay"), 24 | DrawerSelectionItem(R.drawable.draftsman_ic_place_overlay, "Place overlay", OVERLAY_SELECTION) 25 | ) 26 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/utils/LayeredViewBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.utils 2 | 3 | import android.graphics.RectF 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import com.gojek.draftsman.internal.model.LayeredView 7 | 8 | internal class LayeredViewBuilder( 9 | private val topInset: Int 10 | ) { 11 | 12 | private var locationRectF = RectF() 13 | 14 | private val location = IntArray(2) 15 | 16 | private val layeredViews = mutableListOf() 17 | 18 | fun buildLayeredView( 19 | view: View 20 | ): List { 21 | layeredViews.clear() 22 | locateViews(view, 0) 23 | return layeredViews 24 | } 25 | 26 | private fun locateViews( 27 | view: View, 28 | layer: Int 29 | ) { 30 | if (view is ViewGroup) { 31 | locateViewGroup(view, layer) 32 | } else { 33 | if (view.visibility == View.VISIBLE) { 34 | locate(view, layer) 35 | } 36 | } 37 | } 38 | 39 | private fun locateViewGroup( 40 | viewGroup: ViewGroup, 41 | layer: Int 42 | ) { 43 | locate(viewGroup, layer) 44 | for (index in 0 until viewGroup.childCount) { 45 | locateViews(viewGroup.getChildAt(index), layer.plus(1)) 46 | } 47 | } 48 | 49 | private fun locate( 50 | view: View, 51 | layer: Int 52 | ) { 53 | view.getLocationOnScreen(location) 54 | val left = location[0].toFloat() 55 | val top = location[1].minus(topInset).toFloat() 56 | locationRectF = RectF( 57 | left, 58 | top, 59 | left.plus(view.width), 60 | top.plus(view.height) 61 | ) 62 | 63 | layeredViews.add( 64 | LayeredView( 65 | layer, 66 | view, 67 | locationRectF 68 | ) 69 | ) 70 | } 71 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/utils/RectUtils.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.utils 2 | 3 | import android.graphics.RectF 4 | 5 | internal fun isAllSame(list: List): Boolean { 6 | var isAllSame = true 7 | for (index in 0 until list.lastIndex) { 8 | isAllSame = list[index] == list[index + 1] 9 | if (!isAllSame) { 10 | break 11 | } 12 | } 13 | return isAllSame 14 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/utils/TypeAlias.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.utils 2 | 3 | import android.view.View 4 | 5 | internal typealias DimensionConfigChange = (Boolean) -> Unit 6 | 7 | internal typealias OnLayeredViewSelection = (View) -> Unit 8 | 9 | internal typealias ExitListener = () -> Unit 10 | 11 | internal typealias InfoDismissListener = () -> Unit -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/visualizers/GridVisualizer.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.visualizers 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.Paint.ANTI_ALIAS_FLAG 7 | import android.view.View 8 | import com.gojek.draftsman.internal.Config 9 | import com.gojek.draftsman.internal.utils.DimensionConverter 10 | 11 | internal class GridVisualizer(context: Context) : View(context) { 12 | 13 | private val paint = Paint(ANTI_ALIAS_FLAG).apply { 14 | style = Paint.Style.STROKE 15 | strokeWidth = 1f 16 | color = Config.gridColor 17 | } 18 | 19 | private var gridSize = DimensionConverter.getPxOfDp(2f, context).toInt() 20 | 21 | fun updateGridSize(newSize: Int) { 22 | val newSizePx = DimensionConverter.getPxOfDp(newSize.toFloat(), context).toInt() 23 | if (gridSize != newSizePx) { 24 | gridSize = newSizePx 25 | invalidate() 26 | } 27 | } 28 | 29 | override fun onDraw(canvas: Canvas) { 30 | val heightFloat = height.toFloat() 31 | var indexFloat: Float 32 | for (index in 0..width step gridSize) { 33 | indexFloat = index.toFloat() 34 | canvas.drawLine( 35 | indexFloat, 36 | 0f, 37 | indexFloat, 38 | heightFloat, 39 | paint 40 | ) 41 | } 42 | val widthFloat = width.toFloat() 43 | for (index in 0..height step gridSize) { 44 | indexFloat = index.toFloat() 45 | canvas.drawLine( 46 | 0f, 47 | indexFloat, 48 | widthFloat, 49 | indexFloat, 50 | paint 51 | ) 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/visualizers/InfoVisualizer.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.visualizers 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.Switch 7 | import android.widget.TextView 8 | import com.gojek.draftsman.R 9 | import com.gojek.draftsman.internal.extensions.gone 10 | import com.gojek.draftsman.internal.extensions.visible 11 | import com.gojek.draftsman.internal.utils.* 12 | import com.gojek.draftsman.internal.widgets.ColorView 13 | 14 | internal class InfoVisualizer( 15 | parent: ViewGroup, 16 | exitListener: ExitListener, 17 | infoDismissListener: InfoDismissListener, 18 | private val configChange: DimensionConfigChange 19 | ) { 20 | 21 | private val layout = LayoutInflater.from(parent.context).inflate( 22 | R.layout.draftsman_info_layout, 23 | parent, 24 | false 25 | ).also { 26 | it.findViewById(R.id.exit_draftsman).setOnClickListener { 27 | exitListener() 28 | } 29 | it.findViewById(R.id.close_info_view).setOnClickListener { 30 | infoDismissListener() 31 | } 32 | parent.addView(it, parent.childCount - 1) 33 | } 34 | 35 | fun updateInfoView( 36 | view: View 37 | ) { 38 | 39 | setText( 40 | R.id.info_widget, 41 | view.javaClass.name 42 | ) 43 | 44 | setText( 45 | R.id.info_width, 46 | "Width ${ 47 | buildDimenString( 48 | view.width, 49 | view.context 50 | ) 51 | }" 52 | ) 53 | 54 | setText( 55 | R.id.info_height, 56 | "Height ${ 57 | buildDimenString( 58 | view.height, 59 | view.context 60 | ) 61 | }" 62 | ) 63 | 64 | if (view is TextView && 65 | view.text.isNotBlank() 66 | ) { 67 | setText( 68 | R.id.info_text_size, 69 | "TextSize ${ 70 | buildTextSizeString( 71 | view.textSize, 72 | view.context 73 | ) 74 | }" 75 | ) 76 | 77 | setText( 78 | R.id.info_text_color, 79 | "TextColor ${getColorHex(view.currentTextColor)}" 80 | ) 81 | 82 | with(layout) { 83 | findViewById(R.id.info_text_color_container).visible() 84 | findViewById(R.id.info_text_color_view).color = view.currentTextColor 85 | } 86 | } else { 87 | with(layout) { 88 | findViewById(R.id.info_text_color_container).gone() 89 | findViewById(R.id.info_text_size).gone() 90 | } 91 | } 92 | 93 | with(layout) { 94 | findViewById(R.id.info_view_container).visible() 95 | findViewById(R.id.dop_px_switch).setOnCheckedChangeListener { _, isChecked -> 96 | configChange(isChecked) 97 | } 98 | } 99 | } 100 | 101 | fun hide() { 102 | layout.findViewById(R.id.info_view_container).gone() 103 | } 104 | 105 | private fun setText( 106 | textViewId: Int, 107 | textToShow: String 108 | ) { 109 | layout.findViewById(textViewId).apply { 110 | text = textToShow 111 | visible() 112 | } 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/visualizers/LayeringVisualizer.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.visualizers 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.TextView 7 | import com.gojek.draftsman.R 8 | import com.gojek.draftsman.internal.model.LayeredView 9 | import com.gojek.draftsman.internal.utils.OnLayeredViewSelection 10 | import com.gojek.draftsman.internal.widgets.NestedViewBoundView 11 | 12 | internal class LayeringVisualizer(parent: ViewGroup) { 13 | 14 | private val layout = LayoutInflater.from(parent.context).inflate( 15 | R.layout.view_layering_layout, 16 | parent, 17 | false 18 | ).also { 19 | parent.addView(it) 20 | } 21 | 22 | fun showLayeredViews(views: List) { 23 | with(layout) { 24 | findViewById(R.id.nested_view_text).text = 25 | "There are ${views.size} views with same boundaries here. Select the one you want to inspect" 26 | findViewById(R.id.nested_view_bound_view).showViewBounds(views) 27 | } 28 | } 29 | 30 | fun setOnLayeredViewSelectListener(listener: OnLayeredViewSelection) { 31 | layout.findViewById(R.id.nested_view_bound_view).layeredViewSelection = 32 | listener 33 | } 34 | 35 | fun setOnDismissListener(listener: View.OnClickListener) { 36 | layout.findViewById(R.id.close_layering_view).setOnClickListener(listener) 37 | } 38 | 39 | fun remove(parent: ViewGroup) { 40 | parent.removeView(layout) 41 | } 42 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/visualizers/MarginVisualizer.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.visualizers 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.RectF 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import com.gojek.draftsman.internal.Config 9 | import com.gojek.draftsman.internal.utils.buildDimenString 10 | import com.gojek.draftsman.internal.visualizers.TextVision.ORIENTATION_UNKNOWN 11 | import com.gojek.draftsman.internal.widgets.HShapeView 12 | 13 | internal object MarginVisualizer { 14 | 15 | fun draw( 16 | canvas: Canvas, 17 | view: View, 18 | locationRectF: RectF 19 | ) { 20 | val layoutParams = view.layoutParams 21 | if (layoutParams is ViewGroup.MarginLayoutParams) { 22 | val marginRectF = RectF() 23 | if (layoutParams.leftMargin > 0) { 24 | marginRectF.set( 25 | locationRectF.left.minus( 26 | layoutParams.leftMargin 27 | ), 28 | locationRectF.top, 29 | locationRectF.left, 30 | locationRectF.bottom 31 | ) 32 | 33 | drawValues( 34 | canvas, 35 | view.context, 36 | layoutParams.leftMargin, 37 | marginRectF 38 | ) 39 | } 40 | 41 | if (layoutParams.rightMargin > 0) { 42 | marginRectF.set( 43 | locationRectF.right, 44 | locationRectF.top, 45 | locationRectF.right.plus( 46 | layoutParams.rightMargin 47 | ), 48 | locationRectF.bottom 49 | ) 50 | 51 | drawValues( 52 | canvas, 53 | view.context, 54 | layoutParams.rightMargin, 55 | marginRectF 56 | ) 57 | } 58 | 59 | if (layoutParams.topMargin > 0) { 60 | marginRectF.set( 61 | locationRectF.left, 62 | locationRectF.top.minus( 63 | layoutParams.topMargin 64 | ), 65 | locationRectF.right, 66 | locationRectF.top 67 | ) 68 | 69 | drawValues( 70 | canvas, 71 | view.context, 72 | layoutParams.topMargin, 73 | marginRectF 74 | ) 75 | } 76 | 77 | if (layoutParams.bottomMargin > 0) { 78 | marginRectF.set( 79 | locationRectF.left, 80 | locationRectF.bottom, 81 | locationRectF.right, 82 | locationRectF.bottom.plus( 83 | layoutParams.bottomMargin 84 | ) 85 | ) 86 | 87 | drawValues( 88 | canvas, 89 | view.context, 90 | layoutParams.bottomMargin, 91 | marginRectF 92 | ) 93 | } 94 | } 95 | } 96 | 97 | private fun drawValues( 98 | canvas: Canvas, 99 | context: Context, 100 | margin: Int, 101 | marginRectF: RectF 102 | ) { 103 | val text = buildDimenString( 104 | margin, 105 | context 106 | ) 107 | val orientation = 108 | TextVision.measureText( 109 | context, 110 | text, 111 | marginRectF 112 | ) 113 | 114 | HShapeView(context).draw( 115 | Config.marginColor, 116 | marginRectF, 117 | margin == marginRectF.height().toInt(), 118 | canvas 119 | ) 120 | 121 | if (orientation != ORIENTATION_UNKNOWN) { 122 | TextVision.drawText( 123 | canvas, 124 | text, 125 | orientation, 126 | marginRectF 127 | ) 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/visualizers/OverlayVisualizer.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.visualizers 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Canvas 6 | import android.graphics.drawable.BitmapDrawable 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.widget.FrameLayout 10 | import android.widget.SeekBar 11 | import android.widget.TextView 12 | import com.gojek.draftsman.R 13 | import com.gojek.draftsman.internal.extensions.doOnProgressChange 14 | import com.gojek.draftsman.internal.drawer.dpToPx 15 | 16 | class OverlayVisualizer( 17 | context: Context, 18 | onClickClose: () -> Unit 19 | ) : FrameLayout(context) { 20 | 21 | private val opacityControl: View 22 | 23 | private val opacityText: TextView 24 | 25 | init { 26 | setWillNotDraw(false) 27 | opacityControl = 28 | LayoutInflater.from(context).inflate(R.layout.overlay_opacity_control_layout, this) 29 | .findViewById(R.id.opacity_control) 30 | opacityText = findViewById(R.id.opacity) 31 | findViewById(R.id.opacity_seekbar).doOnProgressChange { 32 | opacityText.text = "$it%" 33 | setOpacity() 34 | postInvalidate() 35 | } 36 | findViewById(R.id.close_overlay).setOnClickListener { 37 | onClickClose() 38 | } 39 | } 40 | 41 | private var bitmap: BitmapDrawable? = null 42 | 43 | fun setOverlayImage(image: Bitmap) { 44 | bitmap = BitmapDrawable(resources, image) 45 | setOpacity() 46 | postInvalidate() 47 | } 48 | 49 | override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { 50 | val margin = dpToPx(16, resources).toInt() 51 | opacityControl.layout( 52 | l + margin, 53 | b - margin - dpToPx(48, resources).toInt(), 54 | r - margin, 55 | b - margin 56 | ) 57 | } 58 | 59 | override fun dispatchDraw(canvas: Canvas) { 60 | canvas.apply { 61 | if (null != bitmap) { 62 | bitmap?.setBounds(0, 0, width, height) 63 | bitmap?.draw(canvas) 64 | } 65 | } 66 | 67 | super.dispatchDraw(canvas) 68 | } 69 | 70 | private fun setOpacity() { 71 | val opacity = opacityText.text.replace(Regex("%"), "").toInt() 72 | bitmap?.alpha = 255 * opacity / 100 73 | } 74 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/visualizers/PaddingVisualizer.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.visualizers 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.Paint.ANTI_ALIAS_FLAG 7 | import android.graphics.RectF 8 | import android.view.View 9 | import com.gojek.draftsman.internal.Config 10 | import com.gojek.draftsman.internal.utils.buildDimenString 11 | import com.gojek.draftsman.internal.visualizers.TextVision.ORIENTATION_UNKNOWN 12 | 13 | internal object PaddingVisualizer { 14 | 15 | private val paddingPaint by lazy { 16 | Paint(ANTI_ALIAS_FLAG).apply { 17 | color = Config.paddingColor 18 | } 19 | } 20 | 21 | fun draw( 22 | canvas: Canvas, 23 | view: View, 24 | locationRectF: RectF 25 | ) { 26 | val paddingRectF = RectF() 27 | if (view.paddingLeft > 0) { 28 | paddingRectF.set( 29 | locationRectF.left, 30 | locationRectF.top, 31 | locationRectF.left.plus(view.paddingLeft), 32 | locationRectF.bottom 33 | ) 34 | canvas.drawRect( 35 | paddingRectF, 36 | paddingPaint 37 | ) 38 | 39 | drawPaddingValues( 40 | view.context, 41 | canvas, 42 | view.paddingLeft, 43 | paddingRectF 44 | ) 45 | } 46 | 47 | if (view.paddingRight > 0) { 48 | paddingRectF.set( 49 | locationRectF.right.minus(view.paddingRight), 50 | locationRectF.top, 51 | locationRectF.right, 52 | locationRectF.bottom 53 | ) 54 | canvas.drawRect( 55 | paddingRectF, 56 | paddingPaint 57 | ) 58 | drawPaddingValues( 59 | view.context, 60 | canvas, 61 | view.paddingRight, 62 | paddingRectF 63 | ) 64 | } 65 | 66 | 67 | if (view.paddingTop > 0) { 68 | paddingRectF.set( 69 | locationRectF.left, 70 | locationRectF.top, 71 | locationRectF.right, 72 | locationRectF.top.plus(view.paddingTop) 73 | ) 74 | canvas.drawRect( 75 | paddingRectF, 76 | paddingPaint 77 | ) 78 | drawPaddingValues( 79 | view.context, 80 | canvas, 81 | view.paddingTop, 82 | paddingRectF 83 | ) 84 | } 85 | if (view.paddingBottom > 0) { 86 | paddingRectF.set( 87 | locationRectF.left, 88 | locationRectF.bottom.minus(view.paddingBottom), 89 | locationRectF.right, 90 | locationRectF.bottom 91 | 92 | ) 93 | canvas.drawRect( 94 | paddingRectF, 95 | paddingPaint 96 | ) 97 | drawPaddingValues( 98 | view.context, 99 | canvas, 100 | view.paddingBottom, 101 | paddingRectF 102 | ) 103 | } 104 | } 105 | 106 | private fun drawPaddingValues( 107 | context: Context, 108 | canvas: Canvas, 109 | padding: Int, 110 | locationRectF: RectF 111 | ) { 112 | val text = buildDimenString( 113 | padding, 114 | context 115 | ) 116 | 117 | val orientation = 118 | TextVision.measureText( 119 | context, 120 | text, 121 | locationRectF 122 | ) 123 | if (orientation != ORIENTATION_UNKNOWN) { 124 | TextVision.drawText( 125 | canvas, 126 | text, 127 | orientation, 128 | locationRectF 129 | ) 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/visualizers/TextVisualizer.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.visualizers 2 | 3 | import android.content.Context 4 | import android.graphics.* 5 | import android.text.TextPaint 6 | import com.gojek.draftsman.internal.utils.DimensionConverter 7 | 8 | private const val MAX_TEXT_SIZE = 12f 9 | private const val MIN_TEXT_SIZE = 8f 10 | 11 | internal object TextVision { 12 | 13 | private val textPaint by lazy { 14 | TextPaint(Paint.ANTI_ALIAS_FLAG).apply { 15 | color = Color.BLACK 16 | style = Paint.Style.FILL 17 | } 18 | } 19 | 20 | fun measureText( 21 | context: Context, 22 | text: String, 23 | locationRectF: RectF 24 | ): Int { 25 | textPaint.textSize = DimensionConverter.getPxOfSp( 26 | MAX_TEXT_SIZE, 27 | context 28 | ) 29 | var orientation = 30 | ORIENTATION_UNKNOWN 31 | var textWidth: Float 32 | val minTextSize = DimensionConverter.getPxOfSp( 33 | MIN_TEXT_SIZE, 34 | context 35 | ) 36 | val textBounds = Rect() 37 | while (textPaint.textSize >= minTextSize 38 | ) { 39 | textWidth = textPaint.measureText(text) 40 | textPaint.getTextBounds( 41 | text, 42 | 0, 43 | text.length, 44 | textBounds 45 | ) 46 | if (locationRectF.width() > textWidth && 47 | locationRectF.height() > textBounds.height() 48 | ) { 49 | orientation = 50 | ORIENTATION_HORIZONTAL 51 | break 52 | } else if (locationRectF.height() > textWidth && 53 | locationRectF.width() > textBounds.height() 54 | ) { 55 | orientation = 56 | ORIENTATION_VERTICAL 57 | break 58 | } 59 | 60 | textPaint.textSize -= context.resources.displayMetrics.scaledDensity 61 | } 62 | 63 | return orientation 64 | } 65 | 66 | fun drawText( 67 | canvas: Canvas, 68 | text: String, 69 | orientation: Int, 70 | locationRectF: RectF 71 | ) { 72 | val textWidth = textPaint.measureText(text) 73 | val textBounds = Rect() 74 | textPaint.getTextBounds( 75 | text, 76 | 0, 77 | text.length, 78 | textBounds 79 | ) 80 | val textHeight = textBounds.height() 81 | 82 | if (orientation == ORIENTATION_HORIZONTAL) { 83 | 84 | val startOffset = locationRectF.width() 85 | .minus(textWidth) 86 | .div(2f) 87 | 88 | 89 | canvas.drawText( 90 | text, 91 | locationRectF.left.plus(startOffset), 92 | locationRectF.centerY().plus( 93 | textHeight.div(2f) 94 | ), 95 | textPaint 96 | ) 97 | } else { 98 | 99 | val path = Path().apply { 100 | moveTo( 101 | locationRectF.centerX().minus(textHeight.div(2f)), 102 | locationRectF.centerY().minus(textWidth.div(2f)) 103 | ) 104 | lineTo( 105 | locationRectF.centerX().minus(textHeight.div(2f)), 106 | locationRectF.centerY().plus(textWidth.div(2f)) 107 | ) 108 | } 109 | 110 | canvas.drawTextOnPath( 111 | text, 112 | path, 113 | 0f, 114 | 0f, 115 | textPaint 116 | ) 117 | } 118 | } 119 | 120 | const val ORIENTATION_HORIZONTAL = 1 121 | const val ORIENTATION_VERTICAL = 2 122 | const val ORIENTATION_UNKNOWN = -1 123 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/widgets/ColorView.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.widgets 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.graphics.Paint.ANTI_ALIAS_FLAG 8 | import android.util.AttributeSet 9 | import android.view.View 10 | import com.gojek.draftsman.internal.utils.DimensionConverter 11 | 12 | internal class ColorView @JvmOverloads constructor( 13 | context: Context, 14 | attributeSet: AttributeSet? = null, 15 | defStyleRes: Int = 0 16 | ) : View( 17 | context, 18 | attributeSet, 19 | defStyleRes 20 | ) { 21 | private val strokePaint = Paint(ANTI_ALIAS_FLAG).apply { 22 | style = Paint.Style.STROKE 23 | strokeWidth = DimensionConverter.getPxOfDp(1f, context) 24 | color = Color.WHITE 25 | } 26 | 27 | var color: Int? = null 28 | set(value) { 29 | field = value 30 | invalidate() 31 | } 32 | 33 | override fun onDraw(canvas: Canvas) { 34 | color?.let { 35 | canvas.run { 36 | drawColor(it) 37 | drawRect( 38 | strokePaint.strokeWidth, 39 | strokePaint.strokeWidth, 40 | width - strokePaint.strokeWidth, 41 | height - strokePaint.strokeWidth, 42 | strokePaint 43 | ) 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/widgets/HShapeView.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.widgets 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.Paint.ANTI_ALIAS_FLAG 7 | import android.graphics.Path 8 | import android.graphics.RectF 9 | import com.gojek.draftsman.internal.utils.DimensionConverter 10 | 11 | internal class HShapeView(context: Context) { 12 | 13 | private val length = DimensionConverter.getPxOfDp(16f, context) 14 | 15 | private val paint = Paint(ANTI_ALIAS_FLAG).apply { 16 | strokeWidth = DimensionConverter.getPxOfDp(1f, context) 17 | style = Paint.Style.STROKE 18 | } 19 | 20 | fun draw( 21 | color: Int, 22 | rectF: RectF, 23 | isVertical: Boolean, 24 | canvas: Canvas 25 | ) { 26 | paint.color = color 27 | canvas.drawPath( 28 | getPath( 29 | rectF, 30 | isVertical 31 | ), 32 | paint 33 | ) 34 | } 35 | 36 | private fun getPath( 37 | rectF: RectF, 38 | isVertical: Boolean 39 | ) = if (isVertical) getVerticalH(rectF) 40 | else getHorizontalH(rectF) 41 | 42 | private fun getVerticalH(rectF: RectF): Path { 43 | val halfStroke = paint.strokeWidth.div(2) 44 | val path = Path() 45 | path.apply { 46 | moveTo( 47 | rectF.centerX() - length.div(2), 48 | rectF.bottom - halfStroke 49 | ) 50 | rLineTo(length, 0f) 51 | moveTo( 52 | rectF.centerX(), 53 | rectF.bottom - halfStroke 54 | ) 55 | rLineTo(0f, -(rectF.height() - halfStroke)) 56 | rMoveTo(-length.div(2f), 0f) 57 | rLineTo(length, 0f) 58 | } 59 | return path 60 | } 61 | 62 | private fun getHorizontalH(rectF: RectF): Path { 63 | val halfStroke = paint.strokeWidth.div(2) 64 | val path = Path() 65 | path.apply { 66 | moveTo( 67 | rectF.left + halfStroke, 68 | rectF.centerY() - length.div(2) 69 | ) 70 | rLineTo(0f, length) 71 | moveTo( 72 | rectF.left + halfStroke, 73 | rectF.centerY() 74 | ) 75 | rLineTo(rectF.width() - halfStroke, 0f) 76 | rMoveTo(0f, -length.div(2f)) 77 | rLineTo(0f, length) 78 | } 79 | return path 80 | } 81 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/widgets/InspectionRootItemView.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.widgets 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import android.widget.LinearLayout 6 | import android.widget.LinearLayout.LayoutParams.WRAP_CONTENT 7 | import android.widget.TextView 8 | import com.gojek.draftsman.internal.utils.DimensionConverter 9 | 10 | internal class InspectionRootItemView(context: Context) : LinearLayout(context) { 11 | 12 | init { 13 | val padding = DimensionConverter.getPxOfDp(8f, context).toInt() 14 | setPadding(padding, padding, padding, padding) 15 | val textView = TextView(context).apply { 16 | layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT) 17 | textSize = 18f 18 | setTextColor(Color.parseColor("#555555")) 19 | setPadding(DimensionConverter.getPxOfDp(16f, context).toInt(), 0, 0, 0) 20 | } 21 | addView(textView) 22 | } 23 | 24 | fun setText(text: String) { 25 | (getChildAt(0) as TextView).text = text 26 | } 27 | } -------------------------------------------------------------------------------- /draftsman/src/main/kotlin/com/gojek/draftsman/internal/widgets/NestedViewBoundView.kt: -------------------------------------------------------------------------------- 1 | package com.gojek.draftsman.internal.widgets 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.graphics.Paint.ANTI_ALIAS_FLAG 8 | import android.graphics.RectF 9 | import android.util.AttributeSet 10 | import android.view.MotionEvent 11 | import android.view.View 12 | import android.view.View.MeasureSpec.getSize 13 | import com.gojek.draftsman.internal.model.LayeredView 14 | import com.gojek.draftsman.internal.utils.DimensionConverter 15 | import com.gojek.draftsman.internal.utils.OnLayeredViewSelection 16 | import com.gojek.draftsman.internal.drawer.dpToPx 17 | 18 | internal class NestedViewBoundView @JvmOverloads constructor( 19 | context: Context, 20 | attributeSet: AttributeSet? = null, 21 | defStyleRes: Int = 0 22 | ) : View( 23 | context, 24 | attributeSet, 25 | defStyleRes 26 | ) { 27 | private val strokePaint = Paint(ANTI_ALIAS_FLAG).apply { 28 | style = Paint.Style.STROKE 29 | strokeWidth = DimensionConverter.getPxOfDp(2f, context) 30 | color = Color.WHITE 31 | } 32 | 33 | private val textPaint = Paint(ANTI_ALIAS_FLAG).apply { 34 | textSize = DimensionConverter.getPxOfSp(14f, context) 35 | color = Color.WHITE 36 | } 37 | 38 | private val padding = DimensionConverter.getPxOfDp(32f, context) 39 | private val textPadding = DimensionConverter.getPxOfDp(8f, context) 40 | 41 | private val layeredViews = mutableListOf() 42 | 43 | private var viewProcessing: Runnable? = null 44 | 45 | private var numberOfViews = 0 46 | 47 | var layeredViewSelection: OnLayeredViewSelection = {} 48 | 49 | fun showViewBounds(views: List) { 50 | viewProcessing = Runnable { 51 | val strokeHalf = strokePaint.strokeWidth.div(2) 52 | var spacing: Float 53 | views.sortedBy { it.layer } 54 | .forEachIndexed { index, layeredView -> 55 | spacing = index.times(padding).plus(strokeHalf) 56 | layeredViews.add( 57 | LayeredView( 58 | layeredView.layer, 59 | layeredView.view, 60 | RectF( 61 | spacing, 62 | spacing, 63 | width.minus(spacing), 64 | height.minus(spacing) 65 | ) 66 | ) 67 | ) 68 | } 69 | invalidate() 70 | viewProcessing = null 71 | } 72 | 73 | numberOfViews = views.size 74 | requestLayout() 75 | } 76 | 77 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 78 | val w = getSize(widthMeasureSpec) 79 | var h = dpToPx(220, resources).toInt() 80 | if (numberOfViews > 2) { 81 | h += (numberOfViews - 2) * padding.toInt() * 2 82 | } 83 | setMeasuredDimension(w, h) 84 | } 85 | 86 | override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 87 | super.onLayout(changed, left, top, right, bottom) 88 | if (changed) { 89 | if (null != viewProcessing) { 90 | consumeViewProcessing() 91 | } 92 | } 93 | } 94 | 95 | override fun onDraw(canvas: Canvas) { 96 | var name: String 97 | layeredViews.forEach { 98 | name = it.view.javaClass.simpleName 99 | canvas.run { 100 | drawRect(it.position, strokePaint) 101 | drawText( 102 | name, 103 | it.position.left + textPadding, 104 | it.position.top + textPaint.textSize + textPadding, 105 | textPaint 106 | ) 107 | } 108 | 109 | } 110 | } 111 | 112 | override fun onTouchEvent(event: MotionEvent): Boolean { 113 | if (event.action == MotionEvent.ACTION_UP) { 114 | for (index in layeredViews.lastIndex downTo 0) { 115 | if (layeredViews[index].position.contains(event.x, event.y)) { 116 | layeredViewSelection(layeredViews[index].view) 117 | break 118 | } 119 | } 120 | } 121 | 122 | return true 123 | } 124 | 125 | private fun consumeViewProcessing() { 126 | post(viewProcessing) 127 | } 128 | } -------------------------------------------------------------------------------- /draftsman/src/main/res/drawable/draftsman_drawer_item_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /draftsman/src/main/res/drawable/draftsman_ic_change_root.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /draftsman/src/main/res/drawable/draftsman_ic_close.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /draftsman/src/main/res/drawable/draftsman_ic_drawer_close.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /draftsman/src/main/res/drawable/draftsman_ic_drawer_open.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 21 | -------------------------------------------------------------------------------- /draftsman/src/main/res/drawable/draftsman_ic_grid.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 19 | 26 | 33 | 40 | 47 | 54 | 61 | 68 | 69 | -------------------------------------------------------------------------------- /draftsman/src/main/res/drawable/draftsman_ic_margin.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 19 | 26 | 33 | 40 | 47 | 54 | 61 | 68 | 75 | 82 | 89 | 96 | 103 | 110 | 117 | 124 | 131 | 138 | 145 | 152 | 153 | -------------------------------------------------------------------------------- /draftsman/src/main/res/drawable/draftsman_ic_padding.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 19 | 26 | 33 | 40 | 47 | 54 | 61 | 68 | 75 | 82 | 89 | 96 | 103 | 110 | 117 | 124 | 131 | 138 | 145 | 152 | 153 | -------------------------------------------------------------------------------- /draftsman/src/main/res/drawable/draftsman_ic_place_overlay.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 19 | 26 | 33 | 40 | 46 | 52 | 59 | 67 | 75 | 83 | 90 | 91 | -------------------------------------------------------------------------------- /draftsman/src/main/res/drawable/draftsman_info_view_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /draftsman/src/main/res/drawable/draftsman_rounded_btn_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /draftsman/src/main/res/drawable/draftsman_vertical_divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /draftsman/src/main/res/layout/draftsman_drawer_item_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | -------------------------------------------------------------------------------- /draftsman/src/main/res/layout/draftsman_drawer_item_range.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 24 | 25 | 33 | -------------------------------------------------------------------------------- /draftsman/src/main/res/layout/draftsman_drawer_item_selection.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 19 | 30 | 31 | 37 | 38 | -------------------------------------------------------------------------------- /draftsman/src/main/res/layout/draftsman_drawer_item_toggle.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 23 | 24 | 36 | 37 | -------------------------------------------------------------------------------- /draftsman/src/main/res/layout/draftsman_drawer_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /draftsman/src/main/res/layout/draftsman_info_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 19 | 20 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 47 | 48 | 54 | 55 | 62 | 63 | 70 | 71 | 77 | 78 | 83 | 84 | 85 | 86 | 90 | 91 | 97 | 98 | 105 | 106 | 113 | 114 | 115 | 116 | 117 | 130 | 131 | -------------------------------------------------------------------------------- /draftsman/src/main/res/layout/overlay_opacity_control_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 20 | 21 | 31 | 32 | 40 | 41 | -------------------------------------------------------------------------------- /draftsman/src/main/res/layout/view_layering_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 24 | 25 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /draftsman/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /draftsman/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /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 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 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=officiaL 20 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gojek/draftsman/b0909fb15abad702dd8b8a985f7c60e2f92bdbf0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Feb 26 15:31:14 IST 2020 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-6.5.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 | -------------------------------------------------------------------------------- /scripts/publish-module.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'signing' 3 | 4 | task androidSourcesJar(type: Jar) { 5 | archiveClassifier.set('sources') 6 | if (project.plugins.findPlugin("com.android.library")) { 7 | // For Android libraries 8 | from android.sourceSets.main.java.srcDirs 9 | from android.sourceSets.main.kotlin.srcDirs 10 | } else { 11 | // For pure Kotlin libraries, in case you have them 12 | from sourceSets.main.java.srcDirs 13 | from sourceSets.main.kotlin.srcDirs 14 | } 15 | } 16 | 17 | artifacts { 18 | archives androidSourcesJar 19 | } 20 | 21 | group = PUBLISH_GROUP_ID 22 | version = PUBLISH_VERSION 23 | 24 | afterEvaluate { 25 | publishing { 26 | publications { 27 | release(MavenPublication) { 28 | groupId PUBLISH_GROUP_ID 29 | artifactId PUBLISH_ARTIFACT_ID 30 | version PUBLISH_VERSION 31 | 32 | // Two artifacts, the `aar` (or `jar`) and the sources 33 | if (project.plugins.findPlugin("com.android.library")) { 34 | from components.release 35 | } else { 36 | from components.java 37 | } 38 | 39 | artifact androidSourcesJar 40 | //artifact javadocJar 41 | 42 | pom { 43 | name = PUBLISH_ARTIFACT_ID 44 | description = 'Draftsman' 45 | url = 'https://github.com/gojek/draftsman' 46 | licenses { 47 | license { 48 | name = 'Draftsman License' 49 | url = 'https://github.com/gojek/draftsman/blob/main/LICENSE' 50 | } 51 | } 52 | developers { 53 | developer { 54 | id = 'jitinsharma' 55 | name = 'Jitin Sharma' 56 | email = 'jitin.sharma@gojek.com' 57 | } 58 | } 59 | scm { 60 | connection = 'scm:git:github.com/gojek/draftsman.git' 61 | developerConnection = 'scm:git:ssh://github.com/gojek/draftsman.git' 62 | url = 'https://github.com/gojek/draftsman/tree/main' 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | signing { 71 | useInMemoryPgpKeys( 72 | rootProject.ext["signing.keyId"], 73 | rootProject.ext["signing.key"], 74 | rootProject.ext["signing.password"], 75 | ) 76 | sign publishing.publications 77 | } 78 | -------------------------------------------------------------------------------- /scripts/publish-root.gradle: -------------------------------------------------------------------------------- 1 | // Create variables with empty default values 2 | ext["ossrhUsername"] = '' 3 | ext["ossrhPassword"] = '' 4 | ext["sonatypeStagingProfileId"] = '' 5 | ext["signing.keyId"] = '' 6 | ext["signing.password"] = '' 7 | ext["signing.key"] = '' 8 | 9 | File secretPropsFile = project.rootProject.file('local.properties') 10 | if (secretPropsFile.exists()) { 11 | // Read local.properties file first if it exists 12 | Properties p = new Properties() 13 | new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } 14 | p.each { name, value -> ext[name] = value } 15 | } else { 16 | // Use system environment variables 17 | ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') 18 | ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') 19 | ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') 20 | ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') 21 | ext["signing.password"] = System.getenv('SIGNING_PASSWORD') 22 | ext["signing.key"] = System.getenv('SIGNING_KEY') 23 | } 24 | 25 | // Set up Sonatype repository 26 | nexusPublishing { 27 | repositories { 28 | sonatype { 29 | stagingProfileId = sonatypeStagingProfileId 30 | username = ossrhUsername 31 | password = ossrhPassword 32 | nexusUrl.set(uri("https://oss.sonatype.org/service/local/")) 33 | snapshotRepositoryUrl.set(uri("https://oss.sonatype.org/content/repositories/snapshots/")) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include ( ":app", ":draftsman", ":draftsman-no-op") 2 | rootProject.name = "Draftsman" 3 | --------------------------------------------------------------------------------